From 63d875c62709b52a92a470a830b933e6526a77d4 Mon Sep 17 00:00:00 2001 From: Joel Eliason Date: Thu, 23 Jul 2026 16:22:20 -0600 Subject: [PATCH] fix(submodel): allow 1.645 as an observation_code constant The authoring prompt instructs 1.645 (normal z for the 5th-95th percentile range) for reconstructing an SD from box-plot whiskers, but the hardcoded-value validator only whitelisted 1.96 (2.5th-97.5th), so a literal 1.645 was rejected as a hardcoded value. Add 1.645 alongside 1.96 in ALLOWED_CONSTANTS. --- src/maple/core/calibration/submodel_target.py | 9 ++++--- .../core/test_submodel_target_validators.py | 26 +++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/maple/core/calibration/submodel_target.py b/src/maple/core/calibration/submodel_target.py index 8363772..20d03c1 100644 --- a/src/maple/core/calibration/submodel_target.py +++ b/src/maple/core/calibration/submodel_target.py @@ -3080,7 +3080,9 @@ def validate_no_hardcoded_values_in_observation_code(self) -> "SubmodelTarget": Allowed constants: - 0, 1, 2 (indexing, basic arithmetic) - - 1.96 (95% CI convention) + - 1.96 (normal z for the 2.5th-97.5th / 95% CI range) + - 1.645 (normal z for the 5th-95th percentile range; box-plot whisker + convention, parallel to 1.96) NOT allowed (should be inputs): - Time conversions like 24.0, 60.0 (add as unit_conversion input) @@ -3090,8 +3092,9 @@ def validate_no_hardcoded_values_in_observation_code(self) -> "SubmodelTarget": """ import ast - # Minimal set of truly necessary constants - ALLOWED_CONSTANTS = {0, 0.0, 1, 1.0, 2, 2.0, 1.96} + # Minimal set of truly necessary constants (normal z-values for the two + # standard percentile ranges used in SD-from-percentile reconstructions). + ALLOWED_CONSTANTS = {0, 0.0, 1, 1.0, 2, 2.0, 1.96, 1.645} errors = [] diff --git a/tests/unit/core/test_submodel_target_validators.py b/tests/unit/core/test_submodel_target_validators.py index 8230ded..a6e21dd 100644 --- a/tests/unit/core/test_submodel_target_validators.py +++ b/tests/unit/core/test_submodel_target_validators.py @@ -2110,3 +2110,29 @@ def test_percent_with_normal_shape_raises(self): def test_percent_with_logit_normal_passes(self): # logit_normal on a median-in-(0,1) percent observable is the correct shape. SubmodelTarget(**_percent_target("logit_normal")) + + +# ============================================================================ +# Test that 1.645 (5th-95th percentile z) is an allowed observation_code constant, +# parallel to 1.96 (2.5th-97.5th). The authoring prompt instructs 1.645 for +# box-plot-whisker SD reconstruction, so the validator must not flag it. +# ============================================================================ + +CODE_WITH_1645 = """ +def derive_observation(inputs, sample_size, rng, n_bootstrap): + import numpy as np + sd = inputs['test_value'] / (2 * 1.645) + return rng.normal(inputs['test_value'], sd / np.sqrt(sample_size), n_bootstrap) +""" + + +class TestAllowedConstant1645: + """1.645 must be whitelisted like 1.96 in observation_code.""" + + def test_1645_not_flagged_as_hardcoded(self): + data = make_algebraic_target(input_value=10.0, measurement_error_code=CODE_WITH_1645) + try: + SubmodelTarget(**data) + except ValidationError as e: + msg = str(e) + assert "1.645" not in msg and "hardcoded" not in msg.lower(), msg