Skip to content

Commit 502ea2f

Browse files
igerberclaude
andcommitted
spillover-conley: address CI codex R1 P3 — runtime survey-path attribution
Adds test_survey_path_attributes_warning_to_user_code in TestValidateMeatPsd. Mirrors the existing no-survey runtime attribution pattern (monkey-patch the kernel helper to force an indefinite combined meat) but exercises the SURVEY orchestrator _compute_stratified_conley_meat with conley_lag_cutoff=1. Captures the resulting PSD UserWarning and asserts attribution lands at user code (this test file), proving stacklevel=3 correctly bubbles the warning through both the helper (conley.py) and the orchestrator (two_stage.py). CI codex flagged the prior survey-path coverage as too weak: the existing test_survey_call_site_passes_stacklevel_3 is a static inspect.getsource() substring check on `stacklevel=3`, which can false-pass if runtime attribution drifts while the literal text remains. This commit adds the missing runtime check; the static check is retained as a complementary fast-fail. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 000d44f commit 502ea2f

1 file changed

Lines changed: 100 additions & 3 deletions

File tree

tests/test_conley_vcov.py

Lines changed: 100 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -843,9 +843,11 @@ def test_survey_call_site_passes_stacklevel_3(self):
843843
``stacklevel=3`` to ``_validate_meat_psd``. The pre-extraction
844844
inline warn used ``stacklevel=2``; after the helper extraction the
845845
+1 frame shift means the call site must pass ``stacklevel=3`` to
846-
attribute the warning to the same outer caller. If a future
847-
refactor drops the explicit kwarg this test surfaces it before the
848-
warning starts mis-attributing to inside the helper."""
846+
attribute the warning to the same outer caller. Pairs with the
847+
runtime test ``test_survey_path_attributes_warning_to_user_code``
848+
which exercises the actual frame walk; this static check pins the
849+
literal kwarg to surface bare-text regressions even if the runtime
850+
test's fixture changes."""
849851
import inspect
850852

851853
from diff_diff.two_stage import _compute_stratified_conley_meat
@@ -864,6 +866,101 @@ def test_survey_call_site_passes_stacklevel_3(self):
864866
"requires stacklevel=3 to preserve attribution."
865867
)
866868

869+
def test_survey_path_attributes_warning_to_user_code(self):
870+
"""Runtime warning-capture test on the SURVEY orchestrator
871+
``_compute_stratified_conley_meat``: when the panel-block path
872+
produces an indefinite combined meat the PSD warning must bubble
873+
through the orchestrator frame to land at user code (this test).
874+
Locks the stacklevel=3 contract end-to-end (not just by source
875+
substring), addressing CI codex R1 P3.
876+
877+
Mirrors the no-survey test's monkey-patch pattern: replaces the
878+
serial-Bartlett kernel helper bound inside ``two_stage.py`` with
879+
an aggressively-negative-off-diagonal stub so the serial meat is
880+
indefinite and the combined meat's min eigenvalue drops below
881+
the -1e-12 PSD threshold. Uses the minimal 4-PSU x 2-period x
882+
3-obs survey fixture from
883+
``tests/test_spillover.py::TestSpilloverDiDWaveE2Followup``."""
884+
from diff_diff import two_stage as two_stage_mod
885+
from diff_diff.survey import ResolvedSurveyDesign
886+
from diff_diff.two_stage import _compute_stratified_conley_meat
887+
888+
rng = np.random.default_rng(seed=29)
889+
n_obs, T, G, p_2 = 24, 2, 4, 3
890+
obs_per_psu_period = 3
891+
psu_id = np.repeat(np.arange(G), obs_per_psu_period * T)
892+
time_arr = np.tile(np.repeat(np.arange(T), obs_per_psu_period), G)
893+
Psi = rng.standard_normal((n_obs, p_2))
894+
psu_centroids = np.array([[40.0, -120.0], [40.1, -120.0], [40.2, -120.0], [40.3, -120.0]])
895+
coords = psu_centroids[psu_id]
896+
psu_strata = np.array([0, 0, 1, 1])
897+
resolved = ResolvedSurveyDesign(
898+
weights=np.ones(n_obs),
899+
weight_type="pweight",
900+
strata=np.repeat(psu_strata, obs_per_psu_period * T),
901+
psu=psu_id,
902+
fpc=np.full(n_obs, 20.0),
903+
n_strata=2,
904+
n_psu=4,
905+
lonely_psu="remove",
906+
)
907+
908+
# Monkey-patch the serial Bartlett kernel helper as bound inside
909+
# diff_diff.two_stage (the `from diff_diff.conley import ...`
910+
# rebind at module load time) so the serial meat is indefinite.
911+
# The combined meat = spatial + indefinite_serial then drops
912+
# below the -1e-12 PSD threshold.
913+
original = two_stage_mod._serial_bartlett_kernel_matrix
914+
915+
def _indefinite(t_codes: np.ndarray, L: int) -> np.ndarray:
916+
n = t_codes.shape[0]
917+
K = np.eye(n)
918+
for i in range(n):
919+
for j in range(n):
920+
if i != j:
921+
K[i, j] = -10.0
922+
return K
923+
924+
try:
925+
two_stage_mod._serial_bartlett_kernel_matrix = _indefinite
926+
with warnings.catch_warnings(record=True) as w:
927+
warnings.simplefilter("always")
928+
_compute_stratified_conley_meat(
929+
Psi,
930+
conley_coords=coords,
931+
conley_cutoff_km=0.30,
932+
conley_metric="euclidean",
933+
conley_kernel="bartlett",
934+
resolved_survey=resolved,
935+
conley_time=time_arr,
936+
conley_lag_cutoff=1,
937+
)
938+
finally:
939+
two_stage_mod._serial_bartlett_kernel_matrix = original
940+
941+
psd = [
942+
msg
943+
for msg in w
944+
if issubclass(msg.category, UserWarning) and "negative eigenvalue" in str(msg.message)
945+
]
946+
assert len(psd) >= 1, (
947+
f"Expected a PSD UserWarning from the indefinite combined "
948+
f"survey meat. Got: {[str(m.message) for m in w]}"
949+
)
950+
msg = psd[0]
951+
# Attribution must be in this test file (user code), proving the
952+
# warning bubbled through both the helper (_validate_meat_psd in
953+
# conley.py) and the survey orchestrator
954+
# (_compute_stratified_conley_meat in two_stage.py). A regression
955+
# of the call-site stacklevel from 3 to 2 would stick the
956+
# attribution inside two_stage.py; to 1 inside the helper itself.
957+
assert msg.filename.endswith("test_conley_vcov.py"), (
958+
f"Expected attribution to user code (test_conley_vcov.py); got "
959+
f"{msg.filename!r}:{msg.lineno}. The stacklevel=3 contract at "
960+
f"two_stage.py's _compute_stratified_conley_meat call site has "
961+
f"regressed."
962+
)
963+
867964

868965
# ---------------------------------------------------------------------------
869966
# TestConleyDirectHelper — _compute_conley_vcov correctness

0 commit comments

Comments
 (0)