Skip to content

feat: fingerprint numpy arrays by shape, dtype, and values - #6

Closed
AshSgDe29071999 wants to merge 1 commit into
LuShadowX:mainfrom
AshSgDe29071999:feat/numpy-comparator
Closed

feat: fingerprint numpy arrays by shape, dtype, and values#6
AshSgDe29071999 wants to merge 1 commit into
LuShadowX:mainfrom
AshSgDe29071999:feat/numpy-comparator

Conversation

@AshSgDe29071999

Copy link
Copy Markdown

Summary

numpy.ndarray fell through to the generic object path, so equal arrays could disagree across processes and float noise failed checks.

Changes

  • Lazy-import numpy and fingerprint arrays by shape, dtype, and contents
  • Round float values for stable comparison
  • Tests for float noise and shape/dtype differences

Closes #1

Closes #1

Lazy-import numpy and compare arrays structurally with float
rounding so near-equal float arrays do not false-fail checks.

@LuShadowX LuShadowX left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good work — lazy import, object-array handling, and tests included. Close to mergeable; three things.

1. This force-imports numpy for projects that never use it

try:
    import numpy as np
except ImportError:
    np = None

This sits in the hot path of fingerprint, which runs on every recorded outcome. If numpy is installed but the project under test never touches it, this imports it anyway — roughly 100ms, plus the memory, for no benefit.

sys.modules.get("numpy") gives you the same answer without ever triggering an import: if the target program has not imported numpy, no value can be an ndarray.

np = sys.modules.get("numpy")
if np is not None and isinstance(obj, np.ndarray):

2. The tolerance is absolute, so it depends on magnitude

np.round(x, decimals=12) is a fixed decimal cut, not a relative tolerance. The same relative error passes or fails depending on scale:

1.0   vs 1.0   + 1e-15   ->  equal        (relative 1e-15)
1e20  vs 1e20  + 1e5     ->  NOT equal    (also relative 1e-15)

Fine for values near 1, surprising elsewhere. Scaling by magnitude, or rounding the mantissa via np.frexp, would behave consistently. Either way it is worth a comment saying what guarantee is actually being offered, since this deliberately trades a little sensitivity for stability.

3. NaN only works by accident

.tolist() puts raw nan floats in the fingerprint, and nan != nan:

fingerprint(np.array([1.0, nan])) == fingerprint(np.array([1.0, nan]))   # False

It happens to work end to end only because compare.py round-trips through JSON and json.loads hands back the same module-level NaN object each time, so list comparison hits its identity shortcut. That is a CPython implementation detail — swap the serialiser and every array containing NaN becomes a false positive.

The scalar float path already handles this explicitly (["float", "nan"]). Worth doing the same here, and testing it: an array with NaN must fingerprint equal to itself.

Given a false positive is the one failure mode this project cannot afford, I would rather it be explicit than lucky.

Thanks again — the shape/dtype/values decomposition is the right structure.

@LuShadowX

Copy link
Copy Markdown
Owner

Thanks for this — the gap was real and #1 is now closed by #19, which
fingerprints arrays by shape, dtype and contents.

Closing this one rather than iterating. I checked the branch out and ran it,
and three things came out of that:

  1. The tolerance depends on magnitude. np.round(x, 12) is an absolute
    cut, so the same relative error is forgiven or not depending on how large
    the numbers are. A relative error of 1e-15 compared equal at magnitude 1
    and different at 1e6, 1e12 and 1e18. Float arrays holding large values
    would still produce false alarms.

  2. It disagrees with how the same float is treated outside an array.
    1.0 vs 1.0 + 1e-15 fingerprints as different on its own and equal
    inside an array. One question, two answers, depending on the container.

  3. NaN equality is accidental. Returning raw floats from .tolist()
    means two NaN arrays compare unequal in process; they only compare equal
    after the JSON round trip the comparison actually performs, because
    json.loads hands back the same NaN singleton (is is True). Change the
    serialiser and every NaN array becomes a false positive — the one failure
    mode this tool cannot afford.

Separately, the lazy import numpy runs for every fingerprinted value, so a
project with no numpy dependency pays to load it. #19 looks numpy up in
sys.modules instead, which engages only for code that already imported it.

#19 routes elements back through fingerprint() so the tolerance question
has one answer everywhere, and names nan/inf the way the scalar path already
did. Each of the three points above has a regression test.

Real issue to pick up, and the shape/dtype part was right — please do send
more.

@LuShadowX LuShadowX closed this Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Comparator for numpy arrays

2 participants