Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions docs/inference_workflows.rst
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,26 @@ Generate posterior predictive cubes and science-ready residual summaries:
metrics = summarize_masked_metrics(summary["mean"], target_cube, mask=valid_voxel_mask)


Performance Guardrails
----------------------

Use guardrails to fail fast when runtime/objective regressions exceed expected
thresholds in benchmark runs.

.. code-block:: python

from rubix.inference import (
OptimizationObjectiveThresholds,
RuntimeThresholds,
check_ifu_optimization_guardrails,
)

runtime_limits = RuntimeThresholds(max_mean_runtime_s=2.0, max_median_runtime_s=2.0)
objective_limits = OptimizationObjectiveThresholds(max_final_loss=1e-3, max_best_loss=1e-3)
check = check_ifu_optimization_guardrails(bench_result, runtime_limits, objective_limits)
assert check.passed, check.message


Performance Notes
-----------------

Expand All @@ -284,6 +304,22 @@ For large particle counts, configure optional IFU accumulation controls:

These settings are used by the particlewise IFU builders in ``rubix.core.ifu``.


Synthetic Science Recipe
------------------------

Run an end-to-end synthetic workflow (optimize -> VI -> posterior predictive ->
residual metrics) and persist science-ready outputs:

.. code-block:: bash

python scripts/run_synthetic_science_recipe.py \
--output-dir outputs/science_recipe \
--nx 8 --ny 8 --nw 64 \
--optimize-steps 200 \
--vi-steps 200 \
--num-posterior-draws 16

Benchmarking Full-IFU Optimization
----------------------------------

Expand Down
16 changes: 16 additions & 0 deletions docs/rubix.inference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,14 @@ rubix.inference.parameterization module
:undoc-members:
:show-inheritance:

rubix.inference.performance_guardrails module
---------------------------------------------

.. automodule:: rubix.inference.performance_guardrails
:members:
:undoc-members:
:show-inheritance:

rubix.inference.validation module
---------------------------------

Expand All @@ -108,6 +116,14 @@ rubix.inference.vi_benchmark module
:undoc-members:
:show-inheritance:

rubix.inference.workflows module
--------------------------------

.. automodule:: rubix.inference.workflows
:members:
:undoc-members:
:show-inheritance:

Module contents
---------------

Expand Down
17 changes: 17 additions & 0 deletions rubix/inference/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@
build_age_metallicity_transforms,
inverse_transforms,
)
from .performance_guardrails import (
OptimizationObjectiveThresholds,
PerformanceCheckResult,
RuntimeThresholds,
VIObjectiveThresholds,
check_ifu_optimization_guardrails,
check_vi_guardrails,
)
from .posterior_predictive import (
compute_residual_products,
sample_posterior_predictive_cubes,
Expand All @@ -56,6 +64,7 @@
benchmark_variational_inference,
vi_benchmark_result_to_dict,
)
from .workflows import run_synthetic_science_recipe, save_science_recipe_outputs

__all__ = [
"IdentityTransform",
Expand All @@ -66,6 +75,10 @@
"IFUCubeBenchmarkResult",
"OptimizationResult",
"OptimizationState",
"OptimizationObjectiveThresholds",
"PerformanceCheckResult",
"RuntimeThresholds",
"VIObjectiveThresholds",
"VariationalResult",
"VariationalState",
"VIBenchmarkResult",
Expand All @@ -84,6 +97,8 @@
"benchmark_result_to_dict",
"compare_gradients",
"compute_residual_products",
"check_ifu_optimization_guardrails",
"check_vi_guardrails",
"estimate_array_nbytes",
"finite_difference_grad",
"forward",
Expand All @@ -110,4 +125,6 @@
"summarize_predictive_cube_samples",
"save_checkpoint",
"value_and_grad",
"run_synthetic_science_recipe",
"save_science_recipe_outputs",
]
154 changes: 154 additions & 0 deletions rubix/inference/performance_guardrails.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
from dataclasses import dataclass
from typing import Optional

from .benchmark import IFUCubeBenchmarkResult
from .vi_benchmark import VIBenchmarkResult


@dataclass(frozen=True)
class RuntimeThresholds:
"""Thresholds for runtime regression checks."""

max_mean_runtime_s: Optional[float] = None
max_median_runtime_s: Optional[float] = None


@dataclass(frozen=True)
class OptimizationObjectiveThresholds:
"""Thresholds for optimization loss quality checks."""

max_final_loss: Optional[float] = None
max_best_loss: Optional[float] = None


@dataclass(frozen=True)
class VIObjectiveThresholds:
"""Thresholds for variational inference objective quality checks."""

max_final_objective: Optional[float] = None
max_best_objective: Optional[float] = None
Comment on lines +16 to +29

Copilot AI Apr 21, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ObjectiveThresholds mixes optimization loss thresholds (max_final_loss/max_best_loss) with VI objective thresholds (max_final_objective/max_best_objective), but each guardrail checker only evaluates a subset. As written, setting the “wrong” fields for a given checker will be silently ignored and can incorrectly report passed=True. Consider splitting this into two threshold dataclasses (optimization vs VI), or add explicit validation in check_ifu_optimization_guardrails/check_vi_guardrails to raise a ValueError when irrelevant threshold fields are set.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot apply changes based on this feedback



@dataclass(frozen=True)
class PerformanceCheckResult:
"""Outcome of a performance guardrail check."""

passed: bool
message: str


def _check_runtime(
mean_runtime_s: float,
median_runtime_s: float,
runtime_thresholds: RuntimeThresholds,
) -> list[str]:
errors: list[str] = []
if runtime_thresholds.max_mean_runtime_s is not None:
if mean_runtime_s > runtime_thresholds.max_mean_runtime_s:
errors.append(
f"mean runtime {mean_runtime_s:.6f}s exceeds "
f"threshold {runtime_thresholds.max_mean_runtime_s:.6f}s"
)

if runtime_thresholds.max_median_runtime_s is not None:
if median_runtime_s > runtime_thresholds.max_median_runtime_s:
errors.append(
f"median runtime {median_runtime_s:.6f}s exceeds "
f"threshold {runtime_thresholds.max_median_runtime_s:.6f}s"
)

return errors


def check_ifu_optimization_guardrails(
result: IFUCubeBenchmarkResult,
runtime_thresholds: RuntimeThresholds,
objective_thresholds: OptimizationObjectiveThresholds,
) -> PerformanceCheckResult:
"""Check optimization benchmark result against runtime/objective thresholds.

Args:
result (IFUCubeBenchmarkResult): Optimization benchmark result.
runtime_thresholds (RuntimeThresholds): Runtime limits.
objective_thresholds (OptimizationObjectiveThresholds): Loss limits.

Returns:
PerformanceCheckResult: Pass/fail status and explanatory message.
"""
errors = _check_runtime(
result.mean_runtime_s,
result.median_runtime_s,
runtime_thresholds,
)

if objective_thresholds.max_final_loss is not None:
if result.final_loss > objective_thresholds.max_final_loss:
errors.append(
f"final loss {result.final_loss:.6e} exceeds "
f"threshold {objective_thresholds.max_final_loss:.6e}"
)

if objective_thresholds.max_best_loss is not None:
if result.best_loss > objective_thresholds.max_best_loss:
errors.append(
f"best loss {result.best_loss:.6e} exceeds "
f"threshold {objective_thresholds.max_best_loss:.6e}"
)

if errors:
return PerformanceCheckResult(
passed=False,
message="; ".join(errors),
)

return PerformanceCheckResult(
passed=True,
message="optimization benchmark satisfies configured thresholds",
)


def check_vi_guardrails(
result: VIBenchmarkResult,
runtime_thresholds: RuntimeThresholds,
objective_thresholds: VIObjectiveThresholds,
) -> PerformanceCheckResult:
"""Check VI benchmark result against runtime/objective thresholds.

Args:
result (VIBenchmarkResult): VI benchmark result.
runtime_thresholds (RuntimeThresholds): Runtime limits.
objective_thresholds (VIObjectiveThresholds): Objective limits.

Returns:
PerformanceCheckResult: Pass/fail status and explanatory message.
"""
errors = _check_runtime(
result.mean_runtime_s,
result.median_runtime_s,
runtime_thresholds,
)

if objective_thresholds.max_final_objective is not None:
if result.final_objective > objective_thresholds.max_final_objective:
errors.append(
f"final objective {result.final_objective:.6e} exceeds "
f"threshold {objective_thresholds.max_final_objective:.6e}"
)

if objective_thresholds.max_best_objective is not None:
if result.best_objective > objective_thresholds.max_best_objective:
errors.append(
f"best objective {result.best_objective:.6e} exceeds "
f"threshold {objective_thresholds.max_best_objective:.6e}"
)

if errors:
return PerformanceCheckResult(
passed=False,
message="; ".join(errors),
)

return PerformanceCheckResult(
passed=True,
message="VI benchmark satisfies configured thresholds",
)
Loading
Loading