Skip to content

Commit 9f43c4b

Browse files
author
Tobias Buck
committed
feat(inference): add validate-smoke-full sequence orchestrator
1 parent 12c36fd commit 9f43c4b

5 files changed

Lines changed: 189 additions & 0 deletions

File tree

docs/inference_workflows.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,14 @@ Real Data Runbook
388388
python scripts/generate_ifu_experiment_report.py \
389389
--output-dir outputs/ifu_realdata_smoke
390390
391+
For a one-command staged run, use the sequence orchestrator:
392+
393+
.. code-block:: bash
394+
395+
python scripts/run_ifu_experiment_sequence.py \
396+
--config rubix/config/inference_experiment_realdata_scaffold.yml \
397+
--output-root-dir outputs/ifu_sequence
398+
391399
Benchmarking Full-IFU Optimization
392400
----------------------------------
393401

rubix/inference/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
generate_ifu_experiment_report,
2121
normalize_experiment_config,
2222
run_ifu_experiment,
23+
run_ifu_experiment_sequence,
2324
save_ifu_experiment_outputs,
2425
validate_ifu_experiment_inputs,
2526
)
@@ -136,6 +137,7 @@
136137
"summarize_predictive_cube_samples",
137138
"save_checkpoint",
138139
"run_ifu_experiment",
140+
"run_ifu_experiment_sequence",
139141
"save_ifu_experiment_outputs",
140142
"validate_ifu_experiment_inputs",
141143
"value_and_grad",

rubix/inference/experiment.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import copy
34
import csv
45
import hashlib
56
import json
@@ -1317,3 +1318,84 @@ def generate_ifu_experiment_report(output_dir: str) -> dict[str, Any]:
13171318
report["artifacts"]["residual_products_keys"] = list(data.files)
13181319

13191320
return report
1321+
1322+
1323+
def run_ifu_experiment_sequence(
1324+
config: ExperimentConfigInput,
1325+
pipeline_factory: PipelineFactory = make_inference_pipeline,
1326+
run_validate: bool = True,
1327+
run_smoke: bool = True,
1328+
run_full: bool = True,
1329+
output_root_dir: Optional[str] = None,
1330+
) -> dict[str, Any]:
1331+
"""Run validate -> smoke -> full IFU workflow sequence.
1332+
1333+
Args:
1334+
config (ExperimentConfigInput): Mapping or YAML path following the
1335+
experiment schema consumed by :func:`normalize_experiment_config`.
1336+
pipeline_factory (PipelineFactory, optional): Pipeline builder used to
1337+
instantiate and prepare a Rubix pipeline. Defaults to
1338+
:func:`make_inference_pipeline`.
1339+
run_validate (bool, optional): Whether to run input validation phase.
1340+
Defaults to ``True``.
1341+
run_smoke (bool, optional): Whether to run smoke-only phase.
1342+
Defaults to ``True``.
1343+
run_full (bool, optional): Whether to run full optimization/VI phase.
1344+
Defaults to ``True``.
1345+
output_root_dir (Optional[str], optional): Optional root output
1346+
directory for phase artifacts. Defaults to ``None`` (use config).
1347+
1348+
Raises:
1349+
RuntimeError: If requested validation phase fails.
1350+
1351+
Returns:
1352+
dict[str, Any]: Per-phase outputs/statuses and output directories.
1353+
"""
1354+
raw_cfg = read_yaml(config) if isinstance(config, str) else dict(config)
1355+
base_cfg = normalize_experiment_config(raw_cfg)
1356+
1357+
if output_root_dir is None:
1358+
output_root = Path(str(base_cfg["run"]["output_dir"]))
1359+
else:
1360+
output_root = Path(output_root_dir)
1361+
output_root.mkdir(parents=True, exist_ok=True)
1362+
1363+
sequence: dict[str, Any] = {
1364+
"output_root_dir": str(output_root),
1365+
"validate": None,
1366+
"smoke": None,
1367+
"full": None,
1368+
}
1369+
1370+
if run_validate:
1371+
report = validate_ifu_experiment_inputs(
1372+
config=base_cfg,
1373+
pipeline_factory=pipeline_factory,
1374+
)
1375+
(output_root / "validate_report.json").write_text(
1376+
json.dumps(_to_jsonable(report), indent=2),
1377+
encoding="utf-8",
1378+
)
1379+
sequence["validate"] = {"ok": bool(report["ok"]), "report": report}
1380+
if not report["ok"]:
1381+
raise RuntimeError("validation phase failed; see validate_report.json")
1382+
1383+
if run_smoke:
1384+
smoke_cfg = copy.deepcopy(base_cfg)
1385+
smoke_cfg["run"]["smoke_only"] = True
1386+
smoke_cfg["optimization"]["enabled"] = False
1387+
smoke_cfg["variational"]["enabled"] = False
1388+
smoke_out = run_ifu_experiment(smoke_cfg, pipeline_factory=pipeline_factory)
1389+
smoke_dir = output_root / "smoke"
1390+
save_ifu_experiment_outputs(smoke_out, str(smoke_dir))
1391+
sequence["smoke"] = {"output_dir": str(smoke_dir), "outputs": smoke_out}
1392+
1393+
if run_full:
1394+
full_cfg = copy.deepcopy(base_cfg)
1395+
full_cfg["run"]["smoke_only"] = False
1396+
full_out = run_ifu_experiment(full_cfg, pipeline_factory=pipeline_factory)
1397+
full_dir = output_root / "full"
1398+
save_ifu_experiment_outputs(full_out, str(full_dir))
1399+
sequence["full"] = {"output_dir": str(full_dir), "outputs": full_out}
1400+
1401+
return sequence
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env python
2+
import argparse
3+
import json
4+
5+
from rubix.inference.experiment import run_ifu_experiment_sequence
6+
7+
8+
def parse_args() -> argparse.Namespace:
9+
parser = argparse.ArgumentParser(
10+
description="Run IFU workflow sequence: validate -> smoke -> full."
11+
)
12+
parser.add_argument("--config", type=str, required=True)
13+
parser.add_argument(
14+
"--output-root-dir",
15+
type=str,
16+
default=None,
17+
help="Optional root directory for validate/smoke/full outputs.",
18+
)
19+
parser.add_argument(
20+
"--skip-validate",
21+
action="store_true",
22+
help="Skip validation phase.",
23+
)
24+
parser.add_argument(
25+
"--skip-smoke",
26+
action="store_true",
27+
help="Skip smoke-only phase.",
28+
)
29+
parser.add_argument(
30+
"--skip-full",
31+
action="store_true",
32+
help="Skip full optimization/VI phase.",
33+
)
34+
return parser.parse_args()
35+
36+
37+
def main() -> None:
38+
args = parse_args()
39+
result = run_ifu_experiment_sequence(
40+
config=args.config,
41+
run_validate=not args.skip_validate,
42+
run_smoke=not args.skip_smoke,
43+
run_full=not args.skip_full,
44+
output_root_dir=args.output_root_dir,
45+
)
46+
print(json.dumps(result, indent=2, default=str))
47+
48+
49+
if __name__ == "__main__":
50+
main()

tests/test_inference_experiment.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
generate_ifu_experiment_report,
1414
normalize_experiment_config,
1515
run_ifu_experiment,
16+
run_ifu_experiment_sequence,
1617
save_ifu_experiment_outputs,
1718
validate_ifu_experiment_inputs,
1819
)
@@ -496,3 +497,49 @@ def test_generate_ifu_experiment_report_requires_summary(tmp_path):
496497
assert "summary.json" in str(exc)
497498
else:
498499
raise AssertionError("Expected FileNotFoundError when summary.json is missing")
500+
501+
502+
def test_run_ifu_experiment_sequence_writes_phase_outputs(tmp_path):
503+
cube = np.ones((2, 2, 4), dtype=np.float32)
504+
target_path = tmp_path / "target.npy"
505+
np.save(target_path, cube)
506+
np.save(tmp_path / "mask.npy", np.ones_like(cube))
507+
np.save(tmp_path / "ivar.npy", np.ones_like(cube))
508+
config_path = _write_config(tmp_path, str(target_path), str(tmp_path / "ckpt"))
509+
510+
def pipeline_factory(_cfg, _mode):
511+
return PreparedSyntheticPipeline(jnp.asarray(cube))
512+
513+
result = run_ifu_experiment_sequence(
514+
config=str(config_path),
515+
pipeline_factory=pipeline_factory,
516+
output_root_dir=str(tmp_path / "sequence"),
517+
)
518+
assert result["validate"]["ok"] is True
519+
assert (tmp_path / "sequence" / "validate_report.json").exists()
520+
assert (tmp_path / "sequence" / "smoke" / "summary.json").exists()
521+
assert (tmp_path / "sequence" / "full" / "summary.json").exists()
522+
523+
524+
def test_run_ifu_experiment_sequence_raises_on_failed_validation(tmp_path):
525+
cube = np.ones((2, 2, 4), dtype=np.float32)
526+
target_path = tmp_path / "target.npy"
527+
np.save(target_path, cube)
528+
# Intentional mismatch for validation failure
529+
np.save(tmp_path / "mask.npy", np.ones((2, 2, 5), dtype=np.float32))
530+
np.save(tmp_path / "ivar.npy", np.ones_like(cube))
531+
config_path = _write_config(tmp_path, str(target_path), str(tmp_path / "ckpt"))
532+
533+
def pipeline_factory(_cfg, _mode):
534+
return PreparedSyntheticPipeline(jnp.asarray(cube))
535+
536+
try:
537+
run_ifu_experiment_sequence(
538+
config=str(config_path),
539+
pipeline_factory=pipeline_factory,
540+
output_root_dir=str(tmp_path / "sequence"),
541+
)
542+
except RuntimeError as exc:
543+
assert "validation phase failed" in str(exc)
544+
else:
545+
raise AssertionError("Expected RuntimeError for failed validation phase")

0 commit comments

Comments
 (0)