Building a Domain Tracker¶
Consist works well as a direct API, but for teams that run the same model repeatedly with different configurations, wrapping it in a thin class pays off. The wrapper holds the tracker, the config directory, and any other fixed inputs in one place — so individual run calls stay short and nothing important gets accidentally omitted.
Use the direct API for one-off scripts or early exploration. Build a wrapper when you find yourself copying the same setup across multiple scripts or run calls.
Why it matters for correctness¶
Every tracker.run(...) call that uses identity_inputs must include the right
config directory, or Consist will produce a stale cache hit without any error.
In a shared codebase with many contributors, that's easy to miss. A wrapper
fixes those inputs at construction time so individual callers can't forget them.
Full Example¶
A calibration workflow with multiple run types and a query helper:
from pathlib import Path
from typing import Any
import consist
from sqlalchemy import text
from consist import ExecutionOptions, Tracker
from consist.models.run_config_kv import RunConfigKV
class CalibrationTracker:
def __init__(self, *, workspace: Path, config_dir: Path) -> None:
self.workspace = workspace
self.config_dir = config_dir
self._tracker = Tracker(
run_dir=workspace / "runs",
db_path=workspace / "provenance.duckdb",
)
# Fixed for all runs — callers cannot forget these
self._identity_inputs = [("model_config", config_dir)]
self._cm = None
def __enter__(self) -> "CalibrationTracker":
self._cm = consist.use_tracker(self._tracker)
self._cm.__enter__()
return self
def __exit__(self, *exc) -> None:
self._cm.__exit__(*exc)
def evaluate(self, *, run_name: str, config: dict, output_dir: Path) -> Any:
return self._tracker.run(
fn=run_model,
name=run_name,
model="my_model_calibration",
config=config,
identity_inputs=self._identity_inputs,
outputs=["persons", "trips", "metrics"],
execution_options=ExecutionOptions(
runtime_kwargs={
"config_dir": self.config_dir,
"output_dir": output_dir,
}
),
)
def sweep(self, *, configs_by_label: dict[str, dict]) -> dict[str, Any]:
return {
label: self.evaluate(
run_name=f"sweep_{label}",
config=config,
output_dir=self.workspace / "outputs" / label,
)
for label, config in configs_by_label.items()
}
def query_top_k(self, *, metric: str, k: int = 5):
query = text("""
SELECT run_id, value_num
FROM global_tables.run_config_kv
WHERE key = :metric
ORDER BY value_num ASC
LIMIT :k
""")
return self._tracker.run_query(query.params(metric=metric, k=k))
def query_top_k_ibis(self, *, metric: str, k: int = 5):
kv = consist.ibis_view(self._tracker, model=RunConfigKV)
return (
kv.filter(kv.key == metric)
.select("run_id", "value_num")
.order_by("value_num")
.limit(k)
)
Prefer the Ibis helper for analysis-time reads over the typed view surface. Keep the SQLAlchemy / SQLModel version when you want to show the lower-level tracker internals or the exact SQL text being issued.
Usage:
with CalibrationTracker(
workspace=Path("./calibration_work"),
config_dir=Path("./configs/my_model"),
) as cal:
baseline = cal.evaluate(
run_name="baseline",
config={"sample_rate": 0.1, "mode_choice_coeff": 0.5},
output_dir=Path("./outputs/baseline"),
)
sweep_results = cal.sweep(
configs_by_label={
"low_coeff": {"sample_rate": 0.1, "mode_choice_coeff": 0.3},
"high_coeff": {"sample_rate": 0.1, "mode_choice_coeff": 0.8},
}
)
top_runs = cal.query_top_k(metric="mode_share_error", k=3)
Callers supply domain inputs (run_name, config, output_dir). The wrapper
owns tracker construction, identity inputs, model name, and output keys.
Design Notes¶
Name methods after model concepts. cal.evaluate(year=2030) is clearer than
tracker.run(fn=evaluate_fn, model="sim", year=2030, ...) at every call site.
Inside the method, call Consist's run, trace, or scenario directly.
__enter__ / __exit__ is optional. It's only needed if you want
module-level consist.* helpers (e.g. consist.log_output(...),
consist.scenario(...)) to find the tracker automatically. Direct calls to
self._tracker.run(...) work fine without it.
Don't over-abstract early. A single script that calls tracker.run(...)
a few times doesn't need a wrapper. The pattern earns its keep when multiple
scripts or team members all need the same fixed inputs.