Skip to content

Commit 484620e

Browse files
igerberclaude
andcommitted
Codex CI R4 P1: enforce zero diagonal on callable conley_metric
P1 [new in R4] — `_validate_callable_metric_result` accepted any symmetric / finite / non-negative matrix, even when d(i, i) > 0. The Conley sandwich relies on K(0) = 1 to reduce the i=j term to the HC0 diagonal X_i ε_i² X_i'; a positive self-distance silently attenuates the HC0 contribution by K(d_ii / h) < 1 and misstates Conley SEs. Built-in metrics ("haversine", "euclidean") satisfy d(i, i) = 0 by construction, so the existing parity tests didn't catch this gap on the callable surface. Adds a sixth check to the callable validator: |d(i, i)| <= 1e-10 for all i, else ValueError naming the violated invariant ("nonzero self-distance ... requires d(i, i) = 0 so the kernel reduces to K(0) = 1 on the HC0 diagonal contribution"). Tests: - test_callable_metric_nonzero_diagonal_raises: symmetric / finite / non-negative with d(i, i) = 0.5 raises. - test_callable_metric_near_zero_diagonal_accepted: sub-tolerance diagonal noise (1e-13) accepted, mirroring the symmetry contract. - test_pairwise_distance_callable updated: the constant_metric fixture now zeroes its diagonal (otherwise it would fail the new check); behaviorally equivalent off-diagonal coverage. Docs: - docs/methodology/REGISTRY.md § "Callable conley_metric validation" extended: 6th check added to the list + a paragraph explaining the HC0-reduction rationale. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 0a8b438 commit 484620e

3 files changed

Lines changed: 80 additions & 11 deletions

File tree

diff_diff/conley.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,10 +118,19 @@ def _validate_callable_metric_result(result: object, n: int) -> np.ndarray:
118118
"""Validate the output of a user-supplied callable ``conley_metric``.
119119
120120
A user-supplied distance callable must return an ``(n, n)`` matrix of
121-
finite, non-negative, symmetric values; otherwise downstream code
122-
produces opaque BLAS errors or silently-wrong vcov estimates. This
123-
helper performs all five checks at the boundary and produces a
124-
targeted :class:`ValueError` for each failure.
121+
finite, non-negative, symmetric values with **zero diagonal**;
122+
otherwise downstream code produces opaque BLAS errors or silently-
123+
wrong vcov estimates. This helper performs all six checks at the
124+
boundary and produces a targeted :class:`ValueError` for each
125+
failure.
126+
127+
The zero-diagonal invariant is load-bearing for the Conley sandwich:
128+
the ``i = j`` term contributes ``K(d_ii / h) · X_i ε_i² X_i'``, which
129+
must reduce to the HC0 diagonal ``X_i ε_i² X_i'`` (i.e., ``K(0) = 1``).
130+
A callable with positive self-distance would attenuate the HC0
131+
contribution by ``K(d_ii / h) < 1`` and silently misstate Conley SEs.
132+
Built-in metrics (``"haversine"``, ``"euclidean"``) satisfy this by
133+
construction.
125134
126135
Returns
127136
-------
@@ -133,7 +142,8 @@ def _validate_callable_metric_result(result: object, n: int) -> np.ndarray:
133142
ValueError
134143
Result cannot cast to ``float64``; shape is not ``(n, n)``;
135144
contains NaN/inf; contains negative entries; is not symmetric
136-
within ``atol=1e-10``.
145+
within ``atol=1e-10``; or has any nonzero diagonal entry
146+
``|d(i, i)| > 1e-10``.
137147
"""
138148
try:
139149
arr = np.asarray(result, dtype=np.float64)
@@ -164,6 +174,15 @@ def _validate_callable_metric_result(result: object, n: int) -> np.ndarray:
164174
"distance matrix must satisfy d(i, j) = d(j, i). Max |D - D.T| = "
165175
f"{asymmetry:.2e}, tolerance 1e-10."
166176
)
177+
diag_max = float(np.max(np.abs(np.diag(arr)))) if arr.size else 0.0
178+
if diag_max > 1e-10:
179+
raise ValueError(
180+
"conley_metric callable returned a matrix with nonzero self-"
181+
"distance on the diagonal; the Conley sandwich requires "
182+
"d(i, i) = 0 so the kernel reduces to K(0) = 1 on the HC0 "
183+
f"diagonal contribution. Max |diag(D)| = {diag_max:.2e}, "
184+
"tolerance 1e-10."
185+
)
167186
return arr
168187

169188

docs/methodology/REGISTRY.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3130,9 +3130,16 @@ validated at the boundary via `_validate_callable_metric_result`:
31303130
3. All entries are finite (NaN/inf raises).
31313131
4. All entries are non-negative (negative distances raise).
31323132
5. Symmetric to within `atol=1e-10` (asymmetric matrix raises).
3133+
6. Zero diagonal: `|d(i, i)| ≤ 1e-10` for all `i` (nonzero diagonal raises).
31333134

31343135
Each failure produces a `ValueError` naming the violated invariant.
3135-
Sub-tolerance asymmetry (eps-level roundoff) is accepted.
3136+
Sub-tolerance asymmetry (eps-level roundoff) is accepted. The zero-
3137+
diagonal invariant is load-bearing for the Conley sandwich: the
3138+
`i = j` term contributes `K(d_ii / h) · X_i ε_i² X_i'`, which must
3139+
reduce to the HC0 diagonal `X_i ε_i² X_i'` (i.e., `K(0) = 1`). A
3140+
callable with positive self-distance would attenuate the HC0 term
3141+
by `K(d_ii / h) < 1` and silently misstate Conley SEs. Built-in
3142+
metrics (`"haversine"`, `"euclidean"`) satisfy this by construction.
31363143

31373144
**Edge cases / restrictions:**
31383145
- `DifferenceInDifferences(vcov_type="conley")` is supported (Wave A #118): pass `unit=<col>` to `fit(...)` (NOT on `__init__`; unused unless Conley is set; not part of `get_params()` / `set_params()`).

tests/test_conley_vcov.py

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -164,16 +164,22 @@ def test_pairwise_distance_euclidean_matches_pdist(self):
164164
np.testing.assert_allclose(D, D_scipy, atol=1e-12)
165165

166166
def test_pairwise_distance_callable(self):
167-
"""A user-supplied callable is dispatched and its output preserved."""
167+
"""A user-supplied callable is dispatched and its output preserved.
168+
Output must satisfy the validator's invariants (zero diagonal, finite,
169+
non-negative, symmetric)."""
168170
coords = np.array([[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]])
169171

170-
def constant_metric(c1, c2):
172+
def constant_offdiag_metric(c1, c2):
171173
n1 = len(c1)
172174
n2 = len(c2)
173-
return np.full((n1, n2), 5.0)
175+
out = np.full((n1, n2), 5.0)
176+
np.fill_diagonal(out, 0.0)
177+
return out
174178

175-
D = _pairwise_distance_matrix(coords, constant_metric)
176-
np.testing.assert_allclose(D, np.full((3, 3), 5.0))
179+
D = _pairwise_distance_matrix(coords, constant_offdiag_metric)
180+
expected = np.full((3, 3), 5.0)
181+
np.fill_diagonal(expected, 0.0)
182+
np.testing.assert_allclose(D, expected)
177183

178184
def test_pairwise_distance_unknown_metric_raises(self):
179185
"""Unknown metric strings raise ValueError from the dispatcher."""
@@ -244,6 +250,43 @@ def asymmetric_metric(c1, c2):
244250
with pytest.raises(ValueError, match="asymmetric matrix"):
245251
_pairwise_distance_matrix(coords, asymmetric_metric)
246252

253+
def test_callable_metric_nonzero_diagonal_raises(self):
254+
"""Callable returning a symmetric/finite/non-negative matrix with a
255+
positive self-distance still raises, because the Conley sandwich
256+
requires d(i, i) = 0 (K(0) = 1 reduces the i=j term to the HC0
257+
diagonal X_i ε_i² X_i'). A nonzero self-distance silently attenuates
258+
the HC0 contribution by K(d_ii / h) < 1 and misstates Conley SEs.
259+
Codex CI R4 P1."""
260+
coords = np.array([[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]])
261+
262+
def positive_diagonal_metric(c1, c2):
263+
# Symmetric, finite, non-negative — but d(i, i) = 0.5 > 0.
264+
n1 = len(c1)
265+
n2 = len(c2)
266+
out = np.full((n1, n2), 5.0)
267+
np.fill_diagonal(out, 0.5)
268+
return out
269+
270+
with pytest.raises(ValueError, match=r"nonzero self-distance"):
271+
_pairwise_distance_matrix(coords, positive_diagonal_metric)
272+
273+
def test_callable_metric_near_zero_diagonal_accepted(self):
274+
"""Sub-tolerance diagonal (roundoff scale) is accepted, mirroring
275+
the symmetry-tolerance contract."""
276+
coords = np.array([[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]])
277+
278+
def near_zero_diagonal_metric(c1, c2):
279+
n1 = len(c1)
280+
n2 = len(c2)
281+
out = np.full((n1, n2), 5.0)
282+
np.fill_diagonal(out, 0.0)
283+
# Diagonal noise well below the 1e-10 tolerance
284+
out[0, 0] = 1e-13
285+
return out
286+
287+
D = _pairwise_distance_matrix(coords, near_zero_diagonal_metric)
288+
assert D.shape == (3, 3)
289+
247290
def test_callable_metric_non_array_result_raises(self):
248291
"""Callable returning a non-castable result raises a targeted ValueError."""
249292
coords = np.array([[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]])

0 commit comments

Comments
 (0)