jamhash 0.1.3

Fast non-cryptographic hashing for canonical packed DNA kmers
Documentation
[![Documentation](https://docs.rs/jamhash/badge.svg)](https://docs.rs/jamhash)
[![License](https://img.shields.io/crates/l/jamhash.svg)](https://github.com/St4NNi/jamhash#license)

# jamhash

`jamhash` provides a fast, non-cryptographic 64-bit hash for canonical DNA
kmers packed into a `u64` with two bits per base. `jam-rs` uses this mapping in
its reference-guided FracMinHash screening pipeline; this repository evaluates
the hash mapping, not the accuracy or safety of that complete system.

```bash
cargo add jamhash
```

The minimum supported Rust version (MSRV) is 1.86.

```rust
use jamhash::jamhash_u64;

// Canonicalization and two-bit packing are caller responsibilities.
let canonical_kmer = 0x0000_006f_93b4_b1bc;
assert_eq!(jamhash_u64(canonical_kmer), 0xaf47_a961_8481_4793);
```

The exact `jamhash_u64` formula and compatibility rules are documented in
[`ALGORITHM.md`](ALGORITHM.md). A `u64` can hold at most 32 two-bit bases. The
checked-in exhaustive evidence is specifically for k=21.

`jamhash_u64(0) == 0` is intentional. Callers must define a zero policy; current
`jam-rs` screening paths exclude zero before applying the FracMinHash threshold.
The archived analysis counted zero inclusively; both conventions are reported
below.

## Benchmarks

Criterion results at source revision `6c385660e1c115d015eb06a8bfa3aee73a452eb8`
on an AMD Ryzen 9 7945HX used rustc 1.97.1 (LLVM 22.1.6),
`x86_64-unknown-linux-gnu`, `opt-level=3`, LTO, and one codegen unit:

| Operation | Median | 95% CI | Throughput |
|---|---:|---:|---:|
| `jamhash_u64` | 2.722 ns | 2.715–2.728 ns | 367.37 million/s |
| XXH3 `xxh3_64`, pre-encoded big-endian `u64` | 5.811 ns | 5.807–5.824 ns | 172.07 million/s |
| fastmurmur3 `murmur3_x64_128`, low 64 bits, pre-encoded, seed 42 | 10.575 ns | 10.523–10.609 ns | 94.56 million/s |

All three functions received the same 14 fixed values. Input selection and byte
encoding were outside the measured operation, and inputs and outputs passed
through `black_box`. These are microbenchmark results for the named machine and
build, not a portable speed multiplier or an end-to-end `jam-rs` measurement.

Run the benchmark with:

```bash
cargo bench --bench benchmarks
```

## Exhaustive canonical k=21 evidence

The exhaustive input is not selected, sampled, or biologically filtered. It is
the full set of 2,199,023,255,552 packed values that can occur after
canonicalizing every k=21 bit-kmer with
`min(forward, reverse_complement)`.

**Across all 2,199,023,255,552 canonical 21-mers, `jamhash_u64` produced
130,928 collision pairs: 99.8901% of the 131,072 pairs expected from a random
64-bit mapping. Every collision was a doublet.** The result contains 130,928
duplicate outputs, 261,856 affected inputs, and no multiplicity above two.

| Hash | Collision pairs | Observed / random-map expectation | Affected inputs | Maximum multiplicity |
|---|---:|---:|---:|---:|
| `jamhash_u64` | 130,928 | 0.998901 | 261,856 | 2 |
| fastmurmur3, seed 42 | 139,644 | 1.065399 | 279,288 | 2 |
| XXH3 | 0 | N/A (permutation) | 0 | — |
| Wang 64-bit mixer | 0 | N/A (permutation) | 0 | — |

Zero collisions do not imply more random MinHash sampling. The exact XXH3 path
above hashes one fixed eight-byte word, and XXH3 is [bijective for eight-byte
inputs](https://github.com/Cyan4973/xxHash/blob/c0b5ea995d66691734b1a79ad89e73a0d2fd5a53/tests/collisions/README.md#examples);
the Wang mixer is also a 64-bit permutation. Their zero collision counts are
therefore structural. FracMinHash instead needs related kmers to behave like
reproducible, nearly independent uniform samples around its hash threshold.

The stored aggregate also records zero-inclusive FracMinHash threshold counts
and output-bit balance for the complete input. For `jamhash_u64`, the largest
absolute error at the recorded scales 10, 100, 1,000, and 10,000 was
`4.728272870180295e-7`; the largest recorded output-bit frequency error was
`1.0940702850348316e-6`. The checked-in verifier recomputes consistency and
collision summaries from this aggregate. The raw multi-terabyte hashset is not
in this checkout, but a surviving sorted partition was available for the
bounded zero-count check.

That posthoc check found exactly 1 zero-valued output from `jamhash_u64` across
the complete canonical packed k=21 domain. The archived pipeline used only
`hash < u64::MAX / scale`, so its original counts include zero. Current
`jam-rs` uses `hash != 0 && hash < u64::MAX / scale`; the runtime-equivalent
counts therefore subtract one without altering the archived evidence:

| Scale | Archived, zero-inclusive | Runtime-equivalent, zero excluded |
|---:|---:|---:|
| 10 | 219,901,285,797 | 219,901,285,796 |
| 100 | 21,990,205,024 | 21,990,205,023 |
| 1,000 | 2,199,005,865 | 2,199,005,864 |
| 10,000 | 219,883,231 | 219,883,230 |

The error figures above describe the archived zero-inclusive counts. Measurement
provenance and the derived values are in
[`zero_policy.json`](evidence/k21/zero_policy.json).

## Separate mutation study

A separate, smaller study selected canonical parents satisfying
`raw & 0x3f == 0`. It measured 4,295,504,166,912 single-base substitutions
from 68,182,605,824 parents; mutated words were not canonicalized again. With
the two-bit encoding above, two of the three possible substitutions at every
base change exactly one packed input bit; the third changes two. The test binned
the high byte of the wrapping `hash(parent) - hash(mutant)` difference into 256
equal bins:

| Hash | Mutation-difference χ² (lower is closer to uniform) | Mean output bits changed |
|---|---:|---:|
| `jamhash_u64` | 218.80 | 31.99999 |
| fastmurmur3, seed 42 | 251.07 | 32.00000 |
| XXH3 | 1,887,897.85 | 32.00000 |
| Wang 64-bit mixer | 3,914,408,417.27 | 32.05177 |

**For these packed k=21 mutation pairs, `jamhash_u64` was near the
uniform-difference baseline, while XXH3 and Wang retained strong arithmetic
differential structure.** XXH3's near-ideal 32-bit mean avalanche shows why
collision counts and average avalanche alone do not answer the MinHash
question. This result supports `jamhash_u64` for threshold sampling of related
packed k=21 inputs; it does not by itself establish that XXH3 is unusable.

The mutation observations are dependent. The χ² values are descriptive
comparisons, not p-values or direct measures of FracMinHash screening accuracy.
The aggregate results are in
[`linear_test.json`](evidence/k21/linear_test.json).

The collision study covered every canonical 21-mer using two-bit `A=00`,
`C=01`, `G=10`, `T=11` encoding and
`min(forward, reverse_complement)` canonicalization. Its archived aggregate is in
[`combined_metrics.json`](evidence/k21/combined_metrics.json).

The study used jamhash 0.1.2; version 0.1.3 keeps the identical `jamhash_u64`
mapping. Exact provenance, immutable source links, artifact hashes, parameters,
known environment details, and missing run metadata are recorded in
[`REPRODUCIBILITY.md`](REPRODUCIBILITY.md). Run the modest posthoc checks with:

```bash
python3 scripts/verify_evidence.py
```

## Evidence boundary

- The exhaustive aggregate establishes the reported collision counts,
  multiplicities, zero-inclusive threshold counts, and stored distribution
  summaries for all canonical packed k=21 inputs. Runtime-equivalent threshold
  counts exclude the single measured zero output.
- The separate mutation study supports only its reported comparisons for its
  selected parent rule and non-canonicalized mutations.
- Neither study measures screening sensitivity, specificity, false-positive or
  false-negative rates, behavior on labeled biological samples, or deployment
  safety. Those claims require representative labeled datasets, an evaluation
  of the complete screening pipeline, and operational validation.
- Adversarial resistance, collision behavior at other kmer sizes, and
  architecture-independent byte hashing have not been established.

## Secondary APIs

`jamhash_bytes` and `JamHasher` support arbitrary bytes and
`std::hash::Hasher`, but they are not covered by the `jamhash_u64` mapping
promise. Their paths for four or more bytes use native-endian reads, so their
outputs are architecture-local and must not be persisted or exchanged between
systems.

`JamHasher` treats each non-empty `write` call as a separate frame. Split writes
generally differ from one concatenated write, empty writes are no-ops, and even
a one-write result is not `jamhash_bytes`. `BuildHasherDefault<JamHasher>` uses
a fixed zero seed. It can be used for trusted, process-local keys, but not for
network data, uploaded sequence files, shared databases, or other
attacker-controlled keys.

The retained [SMHasher3 transcript](smhasher3_jamhash.txt) reports 188/188
passes for a historical JamHash adapter, but its source revision, command,
runner version, build flags, and machine were not recorded. It is not evidence
that this release was rerun through SMHasher3.

This crate is not cryptographic and provides no adversarial collision or
denial-of-service resistance.

## License

Licensed under either Apache-2.0 or MIT, at your option. See
[`LICENSE-APACHE`](LICENSE-APACHE) and [`LICENSE-MIT`](LICENSE-MIT).