How it works
Data structures defined in mosaic/benchmarks/core/config.py that the runner, status pipeline, and per-problem packages all read from. Contributors writing a new domain (see Add a Backend) need to know these field-by-field; the Tesseract interface is documented separately because its audience extends past Mosaic itself.
SolverSpec
Each solver backend is described by a SolverSpec dataclass:
| Field | Type | Description |
|---|---|---|
dir |
str |
Subdirectory under the domain’s tesseracts/ folder |
name |
str |
Display name for plots and tables |
scheme |
str |
Numerical scheme (e.g. “LBM BGK”, “semi-Lagrangian + projection”) |
backend |
str |
Runtime / language: "jax", "pytorch", "julia", "cpp" |
ad_strategy |
str \| None |
How gradients are computed: "autodiff", "adjoint", "hybrid", or None |
family |
str |
Solver family for grouped styling: "projection", "lbm", "spectral", "fem" |
color |
str |
Hex color for plots |
uses_gpu |
bool |
Whether this solver targets GPU |
internal_dtype |
str |
"float32" or "float64" |
differentiable |
bool \| None |
Explicit VJP flag; None = runtime detection |
description |
str |
One-sentence solver description |
doc_url |
str |
Link to upstream documentation |
image_tag |
str \| None |
<image_name>:latest, populated by discover_solvers() |
input_overrides |
dict |
Per-solver default overrides merged into cfg.make_inputs for this solver |
Per-(solver, problem) exclusions and anomaly annotations are not stored on SolverSpec — they live on the Problem (problem.exclusions[key][solver_name] = Exclusion(...), registered via problem.exclude(key, {solver_name: Exclusion(...)})).
Problem
Each benchmark domain is configured by a Problem dataclass:
| Field | Type | Description |
|---|---|---|
name |
str |
CLI name (e.g. "ns-grid") |
tesseract_dir |
Path |
Path to the domain’s tesseracts/ directory |
output_key |
str |
Field name to compare across solvers |
solvers |
list[SolverSpec] |
Registered solver backends |
make_ic |
dict[str, IcSpec] |
Named initial condition generators, populated incrementally by problem.add_ic(...) |
make_inputs |
Callable |
Builds solver inputs from IC and physics parameters; user-provided (spec, ic, **physics) -> dict is wrapped at __post_init__ time to expose (solver_name, ic, **physics) -> dict |
error_fn |
Callable |
Computes the objective from solver outputs |
reference |
Callable \| None |
Analytic reference solution (ic, t, L, **physics) → arr, when one exists |
experiments |
dict[str, Experiment] |
Suite/experiment-key → registered runner closure, populated by .add_experiment(...) |
plot_fns |
dict[str, PlotFn] |
Suite/experiment-key → registered plot closure |
Per-experiment descriptions live on each Experiment.params dict, read by cfg.get_plot_description(suite, experiment) rather than from a problem-level field.
Each problem is a Python package at mosaic/benchmarks/problems/<slug>/ with config.py as the single entry point — it instantiates Problem(...), calls problem.add_ic(...) per IC, then problem.add_experiment(...) per experiment. Small domains keep everything in config.py; larger ones split into ics.py / physics.py / experiments.py by hand. The package re-exports problem; the registry in mosaic/benchmarks/problems/__init__.py collects them all.
Experiment
Returned by every Problem.add_experiment(...) call, stored on problem.experiments[key]:
| Field | Type | Description |
|---|---|---|
fn |
Callable |
Runner closure; the framework calls fn(cfg, tags, **overrides) -> dict |
params |
dict |
Introspection manifest (plot description, status checks, …) read by mosaic status and the docs build |
coords |
dict[str, Any] |
Typed sweep position, e.g. {"N": 32, "regime": "diffusive"}. Variant fan-out auto-tags with {"variant": <name>}. Persisted into result.json so aggregator plots find each cell. |
Aggregator plots (add_sweep_plot)
Problem.add_sweep_plot(name, fn, *, group_by=..., filter=...) registers a plot under _extra/sweep/<name>. At render time the framework walks every experiment with non-empty coords, applies filter (each coord_key=value must match exactly), partitions the survivors by group_by, and calls fn(payload, group) once per partition. payload is list[{"coords": ..., "exp_key": str, "result": dict}] with group_by keys removed (they’re constant in the partition; group carries them).