From 0296c82ea0104fdc4e1555a4083e835da31ef47e Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:41:02 +0200 Subject: [PATCH 1/2] Make calibration hotfixes reload-safe --- src/pyrecest/calibration/__init__.py | 66 +++++++++++++++++++++++++--- 1 file changed, 60 insertions(+), 6 deletions(-) diff --git a/src/pyrecest/calibration/__init__.py b/src/pyrecest/calibration/__init__.py index 92ea0b58b..940c4226c 100644 --- a/src/pyrecest/calibration/__init__.py +++ b/src/pyrecest/calibration/__init__.py @@ -27,6 +27,30 @@ "times_s", } ) +_ORIGINAL_AGGREGATE_SUMMARY_METRIC_ATTR = ( + "_pyrecest_original_aggregate_summary_metric" +) +_ORIGINAL_AGGREGATE_TIME_OFFSET_SWEEPS_ATTR = ( + "_pyrecest_original_aggregate_time_offset_sweeps" +) +_ORIGINAL_BIAS_NUMERIC_ARRAY_ATTR = "_pyrecest_original_bias_as_numeric_array" +_ORIGINAL_BIAS_NONNEGATIVE_INT_ATTR = "_pyrecest_original_bias_as_nonnegative_int" +_ORIGINAL_BIAS_NONNEGATIVE_FINITE_FLOAT_ATTR = ( + "_pyrecest_original_bias_as_nonnegative_finite_float" +) + +if not hasattr(_time_offset_module, _ORIGINAL_AGGREGATE_SUMMARY_METRIC_ATTR): + setattr( + _time_offset_module, + _ORIGINAL_AGGREGATE_SUMMARY_METRIC_ATTR, + _time_offset_module._aggregate_summary_metric, + ) +if not hasattr(_time_offset_module, _ORIGINAL_AGGREGATE_TIME_OFFSET_SWEEPS_ATTR): + setattr( + _time_offset_module, + _ORIGINAL_AGGREGATE_TIME_OFFSET_SWEEPS_ATTR, + _time_offset_module.aggregate_time_offset_sweeps, + ) def _is_rejected_real_scalar(value: Any) -> bool: @@ -118,7 +142,9 @@ def _as_nonnegative_summary_count(value: Any, name: str) -> float: return result -_base_aggregate_summary_metric = _time_offset_module._aggregate_summary_metric +_base_aggregate_summary_metric = getattr( + _time_offset_module, _ORIGINAL_AGGREGATE_SUMMARY_METRIC_ATTR +) def _aggregate_summary_metric( @@ -145,9 +171,34 @@ def _aggregate_summary_metric( from . import bias as _bias_module # noqa: E402 -_base_bias_as_numeric_array = _bias_module._as_numeric_array -_base_bias_as_nonnegative_int = _bias_module._as_nonnegative_int -_base_bias_as_nonnegative_finite_float = _bias_module._as_nonnegative_finite_float +if not hasattr(_bias_module, _ORIGINAL_BIAS_NUMERIC_ARRAY_ATTR): + setattr( + _bias_module, + _ORIGINAL_BIAS_NUMERIC_ARRAY_ATTR, + _bias_module._as_numeric_array, + ) +if not hasattr(_bias_module, _ORIGINAL_BIAS_NONNEGATIVE_INT_ATTR): + setattr( + _bias_module, + _ORIGINAL_BIAS_NONNEGATIVE_INT_ATTR, + _bias_module._as_nonnegative_int, + ) +if not hasattr(_bias_module, _ORIGINAL_BIAS_NONNEGATIVE_FINITE_FLOAT_ATTR): + setattr( + _bias_module, + _ORIGINAL_BIAS_NONNEGATIVE_FINITE_FLOAT_ATTR, + _bias_module._as_nonnegative_finite_float, + ) + +_base_bias_as_numeric_array = getattr( + _bias_module, _ORIGINAL_BIAS_NUMERIC_ARRAY_ATTR +) +_base_bias_as_nonnegative_int = getattr( + _bias_module, _ORIGINAL_BIAS_NONNEGATIVE_INT_ATTR +) +_base_bias_as_nonnegative_finite_float = getattr( + _bias_module, _ORIGINAL_BIAS_NONNEGATIVE_FINITE_FLOAT_ATTR +) def _as_bias_numeric_array(value: Any, name: str) -> np.ndarray: @@ -194,7 +245,6 @@ def _as_numeric_vector(value: Any, name: str) -> np.ndarray: _aggregate_std_metric, _validate_error_metric, ) -from .time_offset import aggregate_time_offset_sweeps as _aggregate_time_offset_sweeps from .time_offset import ( # noqa: E402 apply_time_offset, fit_time_offset, @@ -205,6 +255,10 @@ def _as_numeric_vector(value: Any, name: str) -> np.ndarray: time_offset_sweep, ) +_base_aggregate_time_offset_sweeps = getattr( + _time_offset_module, _ORIGINAL_AGGREGATE_TIME_OFFSET_SWEEPS_ATTR +) + def aggregate_time_offset_sweeps( sweeps: Iterable[Iterable[Mapping[str, float]]], @@ -215,7 +269,7 @@ def aggregate_time_offset_sweeps( metric = _validate_error_metric(metric) materialized_sweeps = [list(sweep) for sweep in sweeps] - rows = _aggregate_time_offset_sweeps(materialized_sweeps, metric=metric) + rows = _base_aggregate_time_offset_sweeps(materialized_sweeps, metric=metric) if metric == "std": return rows From 8951b22d844caa24433e999ec12455cd88ec71bd Mon Sep 17 00:00:00 2001 From: Florian Pfaff <6773539+FlorianPfaff@users.noreply.github.com> Date: Sat, 1 Aug 2026 09:41:18 +0200 Subject: [PATCH 2/2] Add calibration reload regression coverage --- tests/calibration/test_calibration_reload.py | 35 ++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 tests/calibration/test_calibration_reload.py diff --git a/tests/calibration/test_calibration_reload.py b/tests/calibration/test_calibration_reload.py new file mode 100644 index 000000000..1fe093b81 --- /dev/null +++ b/tests/calibration/test_calibration_reload.py @@ -0,0 +1,35 @@ +import importlib + +import numpy as np +import pyrecest.calibration as calibration + + +def _summary_row() -> dict[str, float]: + return { + "time_offset_s": 0.0, + "count": 1.0, + "mean": 1.0, + "std": 0.0, + "rmse": 1.0, + "p95": 1.0, + "max": 1.0, + } + + +def test_calibration_hotfixes_are_reload_idempotent(): + module = calibration + + for _ in range(2): + module = importlib.reload(module) + + aggregated = module.aggregate_time_offset_sweeps([[_summary_row()]]) + assert aggregated[0]["rmse"] == 1.0 + + examples = module.make_bias_training_examples( + measurement_times_s=np.array([0.0]), + measurement_values=np.array([[2.0]]), + reference_times_s=np.array([0.0]), + reference_values=np.array([[1.0]]), + max_time_delta_s=None, + ) + np.testing.assert_array_equal(examples.residual, np.array([[1.0]]))