File indexing completed on 2026-09-28 08:27:36
0001 import numpy as np
0002 import pytest
0003
0004 from optiphy.ana.compare_ab import (
0005 chi2_1d,
0006 compare_hits,
0007 hit_parser,
0008 two_proportion_z_score,
0009 )
0010
0011
0012 def test_chi2_1d_ignores_overall_normalization():
0013 """Proportional histograms should match despite different sample totals."""
0014 a = np.array([0.1, 0.2, 1.1, 1.2, 1.3, 1.4])
0015 b = np.array([0.1, 0.2, 0.3, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6])
0016
0017 chi2, ndf = chi2_1d(a, b, bins=[0.0, 1.0, 2.0])
0018
0019 assert chi2 == pytest.approx(0.0)
0020 assert ndf == 1
0021
0022
0023 def test_chi2_1d_detects_shape_difference():
0024 """Different bin proportions should produce a nonzero shape statistic."""
0025 a = np.array([0.1, 0.2, 0.3, 0.4, 1.1, 1.2])
0026 b = np.array([0.1, 0.2, 1.1, 1.2, 1.3, 1.4])
0027
0028 chi2, ndf = chi2_1d(a, b, bins=[0.0, 1.0, 2.0])
0029
0030 assert chi2 == pytest.approx(4.0 / 3.0)
0031 assert ndf == 1
0032
0033
0034 def test_two_proportion_z_score_accepts_equal_rates():
0035 """Equal hit fractions should have zero statistical separation."""
0036 assert two_proportion_z_score(250, 1000, 2500, 10000) == pytest.approx(0.0)
0037
0038
0039 def test_two_proportion_z_score_detects_rate_difference():
0040 """The count test should measure differing efficiencies, not raw totals."""
0041 assert two_proportion_z_score(226, 1000, 296, 1000) == pytest.approx(3.56402, rel=1e-5)
0042
0043
0044 @pytest.mark.parametrize(
0045 ("successes", "trials"),
0046 [(-1, 100), (101, 100), (0, 0)],
0047 )
0048 def test_two_proportion_z_score_rejects_invalid_counts(successes, trials):
0049 """Invalid hit and launched-photon counts should be rejected explicitly."""
0050 with pytest.raises(ValueError):
0051 two_proportion_z_score(successes, trials, 50, 100)
0052
0053
0054 @pytest.mark.parametrize(
0055 ("extra_args", "expected_status"),
0056 [([], 1), (["--report-only"], 0)],
0057 )
0058 def test_report_only_preserves_default_enforcement(tmp_path, extra_args, expected_status):
0059 """Statistical mismatches should remain fatal unless explicitly diagnostic."""
0060 g4_path = tmp_path / "g_hits.npy"
0061 gpu_path = tmp_path / "s_hits.npy"
0062 np.save(g4_path, np.zeros((1, 4, 4)))
0063 np.save(gpu_path, np.zeros((9, 4, 4)))
0064 args = hit_parser().parse_args(
0065 [
0066 str(g4_path),
0067 str(gpu_path),
0068 "--count-trials",
0069 "10",
0070 "--count-nsigma",
0071 "3",
0072 *extra_args,
0073 ]
0074 )
0075
0076 assert compare_hits(args) == expected_status
0077
0078
0079 @pytest.mark.parametrize(
0080 ("extra_args", "expected_status"),
0081 [([], 1), (["--report-only"], 0)],
0082 )
0083 def test_report_only_applies_to_shape_mismatches(tmp_path, extra_args, expected_status):
0084 """Diagnostic mode should report differing shapes without weakening its default."""
0085 g4_path = tmp_path / "g_hits.npy"
0086 gpu_path = tmp_path / "s_hits.npy"
0087 g4_hits = np.zeros((10, 4, 4))
0088 gpu_hits = np.zeros((10, 4, 4))
0089 gpu_hits[:, 0, 0] = 1.0
0090 np.save(g4_path, g4_hits)
0091 np.save(gpu_path, gpu_hits)
0092 args = hit_parser().parse_args(
0093 [
0094 str(g4_path),
0095 str(gpu_path),
0096 "--count-trials",
0097 "10",
0098 "--count-nsigma",
0099 "3",
0100 "--chi2-ndf-tolerance",
0101 "5",
0102 *extra_args,
0103 ]
0104 )
0105
0106 assert compare_hits(args) == expected_status
0107
0108
0109 def test_report_only_does_not_suppress_required_hit_failure(tmp_path):
0110 """Diagnostic statistics should still fail when a required hit input is empty."""
0111 g4_path = tmp_path / "g_hits.npy"
0112 gpu_path = tmp_path / "s_hits.npy"
0113 np.save(g4_path, np.zeros((0, 4, 4)))
0114 np.save(gpu_path, np.zeros((1, 4, 4)))
0115 args = hit_parser().parse_args(
0116 [
0117 str(g4_path),
0118 str(gpu_path),
0119 "--count-trials",
0120 "10",
0121 "--count-nsigma",
0122 "3",
0123 "--report-only",
0124 "--require-hits",
0125 ]
0126 )
0127
0128 assert compare_hits(args) == 1