Skip to content

Commit 5960e39

Browse files
committed
feat(inference): add production IFU experiment runner with checkpointed orchestration
1 parent 143d821 commit 5960e39

8 files changed

Lines changed: 1344 additions & 0 deletions
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
#!/usr/bin/env python
2+
import argparse
3+
import json
4+
from pathlib import Path
5+
6+
import jax.numpy as jnp
7+
8+
from rubix.core.data import Galaxy, GasData, RubixData, StarsData
9+
from rubix.inference import (
10+
OptimizationObjectiveThresholds,
11+
RuntimeThresholds,
12+
benchmark_ifu_cube_optimization,
13+
benchmark_result_to_dict,
14+
check_ifu_optimization_guardrails,
15+
)
16+
17+
18+
class _SyntheticPipeline:
19+
"""Small synthetic pipeline for guardrail smoke checks."""
20+
21+
def __init__(self, template: jnp.ndarray):
22+
self.template = template
23+
24+
def run_sharded(self, rubixdata: RubixData) -> jnp.ndarray:
25+
return rubixdata.stars.age[0] * self.template
26+
27+
28+
def _make_data() -> RubixData:
29+
return RubixData(
30+
galaxy=Galaxy(),
31+
stars=StarsData(
32+
coords=jnp.zeros((1, 3)),
33+
velocity=jnp.zeros((1, 3)),
34+
mass=jnp.ones(1),
35+
age=jnp.array([0.0]),
36+
metallicity=jnp.array([0.01]),
37+
),
38+
gas=GasData(
39+
coords=jnp.zeros((1, 3)),
40+
velocity=jnp.zeros((1, 3)),
41+
mass=jnp.ones(1),
42+
),
43+
)
44+
45+
46+
def parse_args() -> argparse.Namespace:
47+
parser = argparse.ArgumentParser(
48+
description="Run optimization benchmark and assert runtime/loss guardrails."
49+
)
50+
parser.add_argument("--nx", type=int, default=8)
51+
parser.add_argument("--ny", type=int, default=8)
52+
parser.add_argument("--nw", type=int, default=64)
53+
parser.add_argument("--max-steps", type=int, default=120)
54+
parser.add_argument("--repeats", type=int, default=2)
55+
parser.add_argument("--max-mean-runtime-s", type=float, default=3.0)
56+
parser.add_argument("--max-median-runtime-s", type=float, default=3.0)
57+
parser.add_argument("--max-final-loss", type=float, default=1e-3)
58+
parser.add_argument("--max-best-loss", type=float, default=1e-3)
59+
parser.add_argument("--output-json", type=str, default="")
60+
return parser.parse_args()
61+
62+
63+
def main() -> None:
64+
args = parse_args()
65+
66+
cube = jnp.ones((args.nx, args.ny, args.nw), dtype=jnp.float32)
67+
target = 1.5 * cube
68+
69+
benchmark_result = benchmark_ifu_cube_optimization(
70+
pipeline=_SyntheticPipeline(cube),
71+
params_init={"stars": {"age": jnp.array([0.2])}},
72+
static_data=_make_data(),
73+
target=target,
74+
learning_rate=0.1,
75+
max_steps=args.max_steps,
76+
tol=1e-8,
77+
repeats=args.repeats,
78+
warmup=True,
79+
)
80+
81+
runtime_thresholds = RuntimeThresholds(
82+
max_mean_runtime_s=args.max_mean_runtime_s,
83+
max_median_runtime_s=args.max_median_runtime_s,
84+
)
85+
objective_thresholds = OptimizationObjectiveThresholds(
86+
max_final_loss=args.max_final_loss,
87+
max_best_loss=args.max_best_loss,
88+
)
89+
90+
check = check_ifu_optimization_guardrails(
91+
benchmark_result,
92+
runtime_thresholds,
93+
objective_thresholds,
94+
)
95+
96+
payload = {
97+
"benchmark": benchmark_result_to_dict(benchmark_result),
98+
"guardrail": {
99+
"passed": check.passed,
100+
"message": check.message,
101+
"failed_conditions": check.failed_conditions,
102+
},
103+
}
104+
105+
if args.output_json:
106+
output_path = Path(args.output_json)
107+
output_path.parent.mkdir(parents=True, exist_ok=True)
108+
output_path.write_text(json.dumps(payload, indent=2), encoding="utf-8")
109+
110+
if not check.passed:
111+
raise SystemExit(check.message)
112+
113+
114+
if __name__ == "__main__":
115+
main()

docs/inference_workflows.rst

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,48 @@ residual metrics) and persist science-ready outputs:
320320
--vi-steps 200 \
321321
--num-posterior-draws 16
322322
323+
324+
Production IFU Experiment
325+
-------------------------
326+
327+
Run a full configuration-driven IFU experiment with optional checkpointing and
328+
resume support:
329+
330+
.. code-block:: bash
331+
332+
python scripts/run_ifu_science_experiment.py \
333+
--config rubix/config/inference_experiment_template.yml
334+
335+
The template supports:
336+
337+
- deterministic or stochastic mode selection
338+
- runtime objective selection via ``run.objective``
339+
- chunked optimization/VI with stage checkpoints
340+
- posterior predictive output products and masked science metrics
341+
342+
343+
Real Data Runbook
344+
-----------------
345+
346+
1. Prepare cube-side arrays as ``.npy`` or ``.npz`` files with matching
347+
``(nx, ny, nw)`` shapes:
348+
``target``, optional ``mask``, optional ``weights``, optional ``sigma`` or
349+
``inv_variance``.
350+
2. Copy and edit
351+
``rubix/config/inference_experiment_template.yml``:
352+
set ``run.rubix_config_path``, ``data.*_path``, and stage hyperparameters.
353+
3. Run deterministic fitting first:
354+
355+
.. code-block:: bash
356+
357+
python scripts/run_ifu_science_experiment.py \
358+
--config rubix/config/inference_experiment_template.yml
359+
360+
4. If interrupted, resume from latest stage checkpoint by setting
361+
``optimization.resume_checkpoint`` or ``variational.resume_checkpoint``.
362+
5. Inspect outputs in ``run.output_dir``:
363+
``summary.json``, ``predictive_summary.npz``, ``residual_products.npz``.
364+
323365
Benchmarking Full-IFU Optimization
324366
----------------------------------
325367

@@ -333,3 +375,14 @@ memory diagnostics for full IFU cubes.
333375
--repeats 3 \
334376
--max-steps 200 \
335377
--use-mask --use-weights
378+
379+
For CI-style threshold checks on a representative synthetic IFU size:
380+
381+
.. code-block:: bash
382+
383+
python bench/check_inference_guardrails.py \
384+
--nx 8 --ny 8 --nw 64 \
385+
--max-steps 120 \
386+
--max-mean-runtime-s 3.0 \
387+
--max-final-loss 1e-3 \
388+
--output-json outputs/guardrails/opt_guardrail.json

docs/rubix.inference.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,14 @@ rubix.inference.checkpoint module
2020
:undoc-members:
2121
:show-inheritance:
2222

23+
rubix.inference.experiment module
24+
---------------------------------
25+
26+
.. automodule:: rubix.inference.experiment
27+
:members:
28+
:undoc-members:
29+
:show-inheritance:
30+
2331
rubix.inference.losses module
2432
-----------------------------
2533

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
run:
2+
rubix_config_path: rubix/config/rubix_config.yml
3+
mode: deterministic
4+
seed: 0
5+
noise_seed: 0
6+
output_dir: outputs/ifu_science
7+
checkpoint_dir: outputs/ifu_science/checkpoints
8+
params_init_overrides:
9+
stars:
10+
age: [1.0]
11+
metallicity: [0.01]
12+
objective:
13+
kind: combined
14+
terms:
15+
- kind: gaussian_nll
16+
inv_variance_key: inv_variance
17+
mask_key: mask
18+
- kind: huber
19+
delta: 0.2
20+
mask_key: mask
21+
weight: 0.05
22+
23+
data:
24+
target_path: data/target_cube.npy
25+
target_key:
26+
mask_path: data/mask_cube.npy
27+
mask_key:
28+
weights_path:
29+
weights_key:
30+
sigma_path:
31+
sigma_key:
32+
inv_variance_path: data/inv_variance_cube.npy
33+
inv_variance_key:
34+
35+
optimization:
36+
enabled: true
37+
learning_rate: 0.001
38+
max_steps: 500
39+
tol: 1.0e-6
40+
normalize_loss: true
41+
checkpoint_interval_steps: 100
42+
resume_checkpoint:
43+
44+
variational:
45+
enabled: true
46+
learning_rate: 0.005
47+
max_steps: 500
48+
tol: 1.0e-6
49+
num_samples: 4
50+
beta_kl: 0.001
51+
init_log_std: -2.0
52+
normalize_loss: true
53+
huber_delta: 0.2
54+
huber_weight: 0.0
55+
checkpoint_interval_steps: 100
56+
resume_checkpoint:
57+
58+
predictive:
59+
enabled: true
60+
num_draws: 16

rubix/inference/__init__.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@
1616
resume_variational_from_checkpoint,
1717
save_checkpoint,
1818
)
19+
from .experiment import (
20+
normalize_experiment_config,
21+
run_ifu_experiment,
22+
save_ifu_experiment_outputs,
23+
)
1924
from .losses import combine_loss_fns, huber_data_loss, masked_gaussian_nll
2025
from .modes import get_pipeline_name_for_mode, make_inference_pipeline
2126
from .objective_config import build_loss_from_config, build_loss_from_user_config
@@ -87,6 +92,7 @@
8792
"load_checkpoint",
8893
"make_optimization_checkpoint",
8994
"make_variational_checkpoint",
95+
"normalize_experiment_config",
9096
"build_age_metallicity_transforms",
9197
"build_ifu_cube_loss",
9298
"build_loss_from_config",
@@ -124,6 +130,8 @@
124130
"summarize_masked_metrics",
125131
"summarize_predictive_cube_samples",
126132
"save_checkpoint",
133+
"run_ifu_experiment",
134+
"save_ifu_experiment_outputs",
127135
"value_and_grad",
128136
"run_synthetic_science_recipe",
129137
"save_science_recipe_outputs",

0 commit comments

Comments
 (0)