Skip to content

Accumulate jaro_winkler_distance in float32 like Miller (81/228 goldens differ) #43

Description

@schmug

Task

pysim/genome_compare.py:106 accumulates the Jaro distance in float64; Miller's genome-compare.cpp:83 accumulates it in float32 (float dw, and a 3.0f literal — every term, every addition and the final divide are single-precision). Change the final line to reproduce Miller's float32 accumulation, and tighten the oracle to assert bit-exactness in float32 rather than agreement to 1e-6. After this, jaro_winkler_distance should match the captured C++ output on 228/228 golden pairs bit-for-bit.

Context

This is small in magnitude — a 1-ULP effect that will not move the search landscape — but it is worth fixing because it is the one place the port's own precision thesis is not applied, in the one compare function whose C++ ends in 3.0f.

The gap is not an oversight: pysim/tests/test_genome_compare_oracle.py:36-37 says outright "Float32 vs float64 rounding is the only expected difference; the values are ratios of small integers, so equality to 1e-6 proves algorithmic identity." That reasoning is sound for proving algorithmic identity, which is what D0 was about. But the port's stated standard is stronger — pysim/indiv.py:23-30 claims the float/double boundary is modelled explicitly at every float store, and pysim/cints.py exists precisely so type width is never left implicit. The comment directly above the offending line (genome_compare.py:105) even reads "Float divisions (the C++ casts to float)" — the intent was understood; the code just doesn't do it.

Measured against the golden corpus (pysim/tests/golden/genome_golden.json, key jaro, 228 pairs), comparing bit-exactly after narrowing to float32:

current float64 accumulation : 81/228 differ from golden
Miller's float32 accumulation:   0/228 differ from golden

Max absolute deviation is 7.285e-08 — comfortably under TOL = 1e-6, which is why the suite passes green today. But pysim/indiv.py:397 narrows this value to float32 for the GENETIC_SIM_FWD sensor, and double rounding does not always recover the correct float32: 81 of 228 pairs (~36%) produce a sensor value one ULP away from what Miller's binary produces. So the README's headline "0/228 divergence" is true at 1e-6, not bit-exact, and the claim should be either made true or restated.

GENETIC_SIM_FWD is a gate sensor and was the subject of D0 (the critical defect that returned garbage for the project's entire life with zero tests) and D11. It is the most scrutinised function in the port; it should be the one that matches exactly.

Reproduction

  1. Load pysim/tests/golden/genome_golden.json["jaro"] (228 cases, each {g1, g2, distance}).
  2. For each case, build genes with the test's _gene helper and call pysim.genome_compare.jaro_winkler_distance.
  3. Compare np.float32(got) != np.float32(expected).

Expected: 0/228 differ.
Actual: 81/228 differ, each by exactly 1 float32 ULP.

Environment: Python 3.13.7, numpy 2.1.3 (the harness/requirements.txt pin). Verified 2026-07-18.

A one-line replacement reproducing Miller's accumulation order gives 0/228 — confirmed locally. Note the order matters as well as the width: Miller adds the three terms left-to-right with each intermediate landing in a float32 register, then divides by 3.0f.

Pointers

Constraints

  • Match Miller's accumulation order, not just the width. Narrowing only the final result is not equivalent and will not reach 0/228.
  • Do not change the matching logic (the sflags/transposition/match_range code). D0's three fixes are correct and golden-pinned; only the final arithmetic line is in scope.
  • Keep a teeth test. If TOL is tightened to bit-exact, TestJaroOracleHasTeeth must still demonstrate the oracle rejects a wrong implementation.
  • No new dependencies. numpy is already a dependency; np.float32 is sufficient.
  • Preserve the path:line C++ citation style used throughout pysim/.

Acceptance criteria

  • jaro_winkler_distance matches genome_golden.json["jaro"] on 228/228 pairs, bit-exactly in float32.
  • The oracle asserts bit-exactness rather than TOL = 1e-6, and its comment reflects the new standard.
  • A teeth test still proves the oracle fails against a float64-accumulating implementation (the current behaviour becomes the negative case).
  • pysim/README.md's D0 row states bit-exact parity.
  • Gates green: npx tsc --noEmit, npm test, python3 harness/test_m0_gate.py, python3 -m unittest discover -s pysim/tests -t . (expect 184+/184+).

Out of scope

  • The float64-vs-float32 chance draws in the mutation layer (genome.py:410, 437, 453, 454, 474) — same class, different layer, and that layer has no golden test at all. Tracked separately.
  • hamming_distance_bits/bytes (genome_compare.py:120, raises) — unported, and genome_similarity method 0 is the only method slsim uses.
  • Re-running any published M0 result. A 1-ULP sensor change does not invalidate prior numbers; note it, don't re-run.

Reasoning guidance

Prioritize responding quickly rather than thinking deeply — the diagnosis is complete and the fix is one line plus a test tightening. The only subtlety is accumulation order; get that right and the goldens confirm the rest.

Cross-references

Metadata

Metadata

Assignees

Labels

bugSomething isn't working

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions