Verification rule

The reference implementation is frozen. oracle/hawkes_oracle.py is the source of truth for correctness. It was written directly from docs/spec/mathematics.md and is never edited to make a test pass.

The rule

  • The pre-commit hook rejects every edit to oracle/*.py.

  • Every compiled implementation (C++, from H2) is tested against the reference.

  • Two independent simulators (thinning, cluster) cross-check each other.

  • Tests are never weakened to pass. If the reference and the C++ code disagree, one of them is wrong; the disagreement is diagnosed, not smoothed over.

Why freeze it

Two independent implementations prove each other correct. The moment the reference can be edited to match the thing it is checking, it stops being a reference and becomes a second copy of the same bug. The cost of freezing is friction; the cost of an unfrozen reference is that every later gate’s evidence evaporates.

How the gradient is established

oracle/test_oracle_gradient.py checks the analytic gradient against the likelihood by two methods that share no code path.

Complex-step differentiation is the primary check. It evaluates the likelihood at x + ih, where i² = -1 and h = 1e-20, and reads the derivative off the imaginary part. There is no subtraction, so there is no cancellation: the method is exact to machine precision and can tell a correct gradient from one that is wrong at the tenth digit. It runs over every parameter entry — M ∈ {1, 2, 3}, N ∈ {10, 30, 60}, eight random parameter sets per combination — against a tolerance of rtol 1e-9, atol 1e-10. The tightest attained relative error across the grid is around 1e-13, several orders of magnitude inside the bound.

Central finite differences are the independent second opinion. They use no complex arithmetic, so an error in the complex-step harness cannot hide behind agreement between two copies of the same mistake. The step is h = 1e-6, and failing requires both a relative error above 1e-5 and an absolute error above 1e-6.

Why a pair of bounds rather than one: central differences carry O(h²) truncation error, which lands around 1e-8 relative on large gradients, and the relative error of a difference of nearly equal numbers blows up when the true derivative is near zero even though the absolute error stays at 1e-9. Requiring both to be large before failing separates finite-difference noise from a defect. A sign error or a missing term produces errors of order 1e-2, which no bound on this list would hide.

The earlier version of this suite used a single relative bound of 1e-5 with an absolute backstop applied to the maximum over the whole array, and spot-checked only four α and four β entries per case. Both are fixed: on the first point the bound is now per entry, and on the second the coverage is every entry.

Edge cases with closed-form answers

The gradient check would pass on a likelihood that is wrong in the same way everywhere, so the suite also pins the likelihood against answers worked out by hand:

  • N = 0 — the likelihood is exactly −ΣμᵢT; all gradients except ∂/∂μ are zero.

  • N = 1 — baseline intensity at the event, closed-form compensator.

  • N = 1, M = 1 — same, with the β derivative carrying the α/β² and α/β terms expanded explicitly.

  • Simultaneous events — two events at the same timestamp must not excite each other, because the spec sums over t^j_m < t and not ≤. This is the defect most likely to survive a gradient check, since it is a difference of measure-zero input that random continuous timestamps never produce.

  • M = 1, many events — checked against a separately written one-line recursion, so the O(N²M²) oracle is compared to an O(N) implementation of the same mathematics.

  • λᵢ ≤ 0 — must return −inf and a NaN gradient, never a finite number. Clipping would turn a diagnosable failure into a plausible-looking result.

Changing the reference

It is possible, but it is recorded. If the reference is genuinely wrong:

  1. Write down the flaw and the correct formula, citing the spec statement it contradicts.

  2. If the spec itself is wrong, amend the spec first and record why — the spec is upstream of the reference, not downstream.

  3. Bypass the hook deliberately (git commit --no-verify) with the reasoning in the message, and re-tag.

An unfrozen reference with no record of why it moved is the failure mode this rule exists to prevent.

Revision history

  • H1-oracle-locked — 2026-10-18. The reference implementation and the first gradient test.

  • H1-oracle-locked-v2 — 2026-10-19. The reference implementation is unchanged; it was re-verified against complex-step over every parameter entry and passed at 1e-9. What moved is the test suite, which gained full entry coverage, seven closed-form edge cases, and per-entry rather than array-wide tolerance. The standalone tests/verify_complex_step.py was folded into the suite, so the complex-step check now runs under pytest instead of being a script someone has to remember to invoke.

  • H2 — 2026-10-19. The reference is still unchanged. It now checks a second, independent implementation: tests/test_oracle_agreement.py compares the C++ core against it over M in {1,2,3,4,6} and N up to 1000, entry by entry in all three gradient blocks. Measured worst case 7.5e-14 on the value and 3.6e-12 per gradient entry. The core’s gradient is additionally checked against complex-step differentiation of the reference likelihood, using the reference suite’s own helper, so the check shares no code path with either analytic implementation.

Two implementations proving each other

The point of freezing the reference is that a disagreement is information. H2 produced several, and every one was resolved by finding the defect in the new code rather than by adjusting the reference or the tolerance:

  • The first event loop let two events at the same timestamp excite each other. The reference is right about t^m_j < t; the loop was wrong.

  • A gradient block was returned with the wrong shape. The reference’s shape is correct; the binding was wrong.

An implementation that has never disagreed with the reference has not been tested yet, in the sense that matters.