Skip to content

Parameter studies (bt.sweep, bt.optimize)

Sweeps and deterministic optimisation over a cell authored as a function of its parameters. bt.sweep and bt.optimize are the module's sweep and optimize; the results are bt.study.Sweep and bt.study.Optimum. See the Parameter sweeps tutorial.

result = bt.sweep(build_cell, grid={"velocity": [0.1, 0.2, 0.3], "lane_y": [0.4, 0.5, 0.6]},
                  metrics=lambda tl: {"cycle": tl.duration, "clearance": float(tl.min_clearance())})
print(result.pivot("lane_y", "velocity", "cycle"))
best = bt.optimize(build_cell, space={"velocity": (0.1, 0.4, 0.05), "lane_y": (0.3, 0.7, 0.05)},
                   objective="cycle", constraints={"clearance": (">=", 0.4)}, method="descent")

study

Parameter studies over a cell: sweeps and deterministic optimisation.

A cell authored as a function of its parameters can be baked at every variant and read by the numbers that matter — that is what makes layout studies a loop and cycle time a regression test. sweep runs the grid and tables the numbers; optimize searches it (a full grid, or a coordinate descent on the grid) for the best feasible point. Neither uses a random number: every row is a deterministic bake, the same every run, so a study is as assertable as a single cell.

result = bt.sweep(build_cell, grid={"velocity": [0.1, 0.2, 0.3], "lane_y": [0.4, 0.5, 0.6]},
                  metrics=lambda tl: {"cycle": tl.duration, "clearance": float(tl.min_clearance())})
print(result.to_markdown())
print(result.pivot("lane_y", "velocity", "cycle"))

best = bt.optimize(build_cell, space={"velocity": (0.1, 0.4, 0.05), "lane_y": (0.3, 0.7, 0.05)},
                   objective="cycle", constraints={"clearance": (">=", 0.3)}, metrics=...)
print(best.params, best.row["cycle"])

build(**params) returns a Scene (baked with sequence= / sequences=, default: every sequence together) or a SequenceTimeline (used as is); metrics(timeline) returns a dict of numbers (default {"cycle": duration}). A variant that does not bake — a layout the planner cannot solve — is a row with ok=False and its error, not an exception: the table says where the cliff is.

Optimum

What optimize found: the best feasible parameters and their row, every evaluated row (a Sweep, in evaluation order), and how it got there.

Sweep

The rows of a study, in evaluation order: each is the variant's parameters, the metrics measured (missing on a failed row), ok and error.

ok property

ok

The rows that baked.

best

best(
    metric: Union[str, Callable[[dict], float]],
    *,
    minimize: bool = True,
    where: Optional[Callable[[dict], bool]] = None,
) -> Optional[dict]

The row with the smallest (or largest) metric among the rows that baked and satisfy where; ties go to the earlier row (grid order), so the answer is stable.

pareto

pareto(
    minimize: Sequence[str] = (),
    maximize: Sequence[str] = (),
) -> list[dict]

The non-dominated rows for several objectives at once — the trade-off front (a shorter cycle against a wider clearance).

pivot

pivot(
    rows: str, cols: str, metric: str, *, missing: str = "—"
) -> str

A two-axis view as a Markdown table: one row per value of parameter rows, one column per value of cols, metric in the cells (missing where the variant failed or was not run).

to_markdown

to_markdown() -> str

The rows as a Markdown table — parameters as written, metrics at table precision; the ok / error columns only when a row failed (CSV and JSON always carry them).

where

where(predicate: Callable[[dict], bool]) -> Sweep

The rows a predicate keeps (failed rows are never kept).

evaluate

evaluate(
    build,
    params: dict,
    *,
    metrics=None,
    sequence=None,
    sequences=None,
    max_duration: float = 120.0,
) -> dict

One variant: build, bake, measure. Returns the row (paramsmetricsok / error); a variant that fails to build or bake is a row with ok=False.

optimize

optimize(
    build: Callable[..., Any],
    space: dict[str, Any],
    *,
    objective: Union[
        str, Callable[[dict], float]
    ] = "cycle",
    minimize: bool = True,
    constraints: Union[
        None,
        dict[str, tuple[str, float]],
        Callable[[dict], bool],
    ] = None,
    metrics: Optional[
        Callable[[Any], dict[str, Any]]
    ] = None,
    sequence: Optional[str] = None,
    sequences: Optional[Sequence[str]] = None,
    max_duration: float = 120.0,
    method: str = "grid",
    start: Optional[dict] = None,
    max_evals: int = 500,
    workers: int = 1,
) -> Optimum

Searches space{name: [values]} or {name: (lo, hi, step)} — for the feasible point with the best objective (a metric name, or a callable over the row). Two deterministic methods:

  • "grid" — every point, in grid order, then the best feasible one (ties to the earlier point). Exhaustive and parallel (workers).
  • "descent" — coordinate descent on the grid: from start (default: the grid's middle point), try each parameter one step down and one step up, move to the best feasible improvement, repeat until nothing improves or max_evals is spent. Far fewer bakes on a large space; finds a local optimum, and says so in method. Infeasible neighbours are stepped over, not into.

Both return every evaluated row, so the search itself is a table.

sweep

sweep(
    build: Callable[..., Any],
    grid: Optional[dict[str, Iterable[Any]]] = None,
    *,
    points: Optional[Iterable[dict]] = None,
    metrics: Optional[
        Callable[[Any], dict[str, Any]]
    ] = None,
    sequence: Optional[str] = None,
    sequences: Optional[Sequence[str]] = None,
    max_duration: float = 120.0,
    workers: int = 1,
) -> Sweep

Bakes build(**params) at every point of grid (the Cartesian product of the lists, in order — the last parameter varies fastest) or at the explicit points, and returns the table. workers > 1 bakes in parallel processes — build and metrics must then be importable (module-level) functions; the rows still come back in grid order, so the result does not depend on scheduling.