diff --git a/src/pyrecest/scenarios.py b/src/pyrecest/scenarios.py index 62222f9c0..53a83e3c0 100644 --- a/src/pyrecest/scenarios.py +++ b/src/pyrecest/scenarios.py @@ -180,12 +180,16 @@ def _normalized_particle_weights(raw_weights: Any, particle_count: int, backend) if any(weight < 0.0 for weight in weight_values): raise ValueError("weights must be nonnegative") - weight_total = sum(weight_values) - if weight_total <= 0.0: + weight_scale = max(weight_values, default=0.0) + if weight_scale <= 0.0: raise ValueError("weights must have positive total mass") + scaled_weights = [weight / weight_scale for weight in weight_values] + scaled_total = sum(scaled_weights) + if scaled_total <= 0.0 or not math.isfinite(scaled_total): + raise ValueError("weights must have positive finite total mass") return backend.asarray( - [weight / weight_total for weight in weight_values], + [weight / scaled_total for weight in scaled_weights], dtype=backend.float64, ) diff --git a/tests/test_scenario_particle_weight_overflow.py b/tests/test_scenario_particle_weight_overflow.py new file mode 100644 index 000000000..fd4eba407 --- /dev/null +++ b/tests/test_scenario_particle_weight_overflow.py @@ -0,0 +1,36 @@ +import math +import sys +from pathlib import Path + +from pyrecest.scenarios import run_scenario + + +def test_particle_resampling_scenario_normalizes_extreme_finite_weights( + tmp_path: Path, +): + max_float = sys.float_info.max + scenario = tmp_path / "extreme_particle_weights.toml" + scenario.write_text( + f""" +[scenario] +type = "particle_resampling" +name = "extreme-particle-weights" +seed = 7 + +[data] +particles = [[0.0], [1.0]] +weights = [{max_float!r}, {max_float / 2.0!r}] +num_samples = 8 +""".strip(), + encoding="utf-8", + ) + + result = run_scenario(scenario) + + assert math.isclose(result.metrics["max_weight"], 2.0 / 3.0) + assert math.isclose(result.metrics["min_weight"], 1.0 / 3.0) + assert math.isclose( + result.metrics["max_weight"] + result.metrics["min_weight"], + 1.0, + ) + assert math.isfinite(result.metrics["effective_sample_size"])