diff --git a/src/access_moppy/qc/cmip7.py b/src/access_moppy/qc/cmip7.py index 6da894a3..a3b8a174 100644 --- a/src/access_moppy/qc/cmip7.py +++ b/src/access_moppy/qc/cmip7.py @@ -93,7 +93,12 @@ def _is_outside_allowed_range(observed: float, minimum: float, maximum: float) - ``-6e-24`` are treated as zero for a lower bound of ``0``. """ - tolerance = 1e-12 + # Many CMIP outputs are derived from float32 fields where boundary values + # (for example 100%) can round to slightly above/below the nominal limit. + # Use a scale-aware absolute tolerance so closed-interval checks remain + # robust without masking meaningfully out-of-range values. + scale = max(abs(observed), abs(minimum), abs(maximum), 1.0) + tolerance = max(1e-12, 8.0 * np.finfo(np.float32).eps * scale) lower_violation = observed < minimum and not np.isclose( observed, minimum, rtol=0.0, atol=tolerance ) diff --git a/tests/unit/test_cmip7_qc.py b/tests/unit/test_cmip7_qc.py index e562f2ed..f2b3f0f4 100644 --- a/tests/unit/test_cmip7_qc.py +++ b/tests/unit/test_cmip7_qc.py @@ -178,6 +178,21 @@ def test_validate_cmip7_output_allows_tiny_negative_noise_at_zero_bound(tmp_path validate_cmip7_output(path) +@pytest.mark.unit +def test_validate_cmip7_output_allows_float32_boundary_noise_for_percent_range( + tmp_path, +): + path = _write_cmip7_output( + tmp_path, + values=[0.0, 100.00003], + experiment_id="historical", + variable_id="snc", + units="%", + ) + + validate_cmip7_output(path) + + @pytest.mark.unit def test_validate_cmip7_output_tas_fails_for_picontrol_range(tmp_path): path = _write_cmip7_output(