You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
refactor(rust): take the signal-level k-mer path from escapepod-signal (#222)
* refactor(rust): take the signal-level k-mer encoding from escapepod-signal
leech held the only copy of this rule, inside a `crate-type = ["cdylib"]`
Python extension module — so a native runtime for a leech `signal_kmer` model
could not link it and had to transcribe it, which is a second definition that
diverges silently. That is not hypothetical in this stack: `extract_levels` was
written twice with different centring conventions and moved 25 of 100 features,
and escapepod-classify reproduced a superseded feature definition for two
months, its golden missing it because all 19 fixture reads took the other
branch.
It is also the natural pair to `escapepod_signal::mapping`, which *produces*
the base-to-signal map the encoding consumes. The producing half was already
upstream and the consuming half was not. escapepod-rs#272 fixed that; this is
leech taking the call (escapepod-signal v0.16.0).
Two primitives delegate, both byte-identical to what was here:
- `encoding.rs::encode_signal_kmer_inner` -> `seq_encoding::encode_signal_kmer`
- `features.rs::sequence_to_int` -> `seq_encoding::sequence_to_int`, the same
A/C/G/T-U table with the same `-1` for anything else (upstream's
`UNKNOWN_BASE` is `-1`)
A third overlap is deliberately NOT delegated. The k-mer context slice in
`signal_mapping.rs` covers the same range with the same padding as upstream's
`sequence_ints_with_context`, but leech needs it as BASES — it serializes
`sequence_with_kmer_context` as a string — where upstream returns ints. Same
rule, different type; swapping it would change a serialized chunk field.
This is a delegation, so identity is the only acceptable outcome, and the
evidence is that nothing had to change to accommodate it: 198 parity tests pass
untouched, including `test_backend_parity.py`, which compares every array in the
npz between the Rust and Python backends — and the Python reference is not
touched by this commit, so it is an independent implementation.
Also here, because the pin moves anyway:
- `escapepod>=0.16.0`, and the full suite re-run against that Python package as
well as against the Rust crate (they are independent: leech_core links the
crate statically, `leech.io` imports the wheel). 1467 passed on both.
- `uv.lock` gains onnx/onnxruntime/onnxscript/protobuf. #220 added the `onnx`
extra to pyproject and never locked it — CI installs from pyproject so it
worked, but `uv sync --extra onnx` would not have.
- The ONNX contract stops telling consumers to reimplement the encoder and
names the crate that owns it.
* refactor(rust): take the k-mer context windowing from escapepod-signal too
The third and last overlap, and the one that needed an upstream change first
(escapepod-rs#274, released in 0.16.1). It could not delegate before: leech
needs the context window as BASES, because the corpus serializes
`sequence_with_kmer_context` as a string, where upstream only offered ints.
escapepod-signal now exposes both forms over one windowing rule, with
`sequence_to_int(bases) == ints` pinned by a test there.
So all three halves of the signal-level k-mer path live upstream now:
`mapping` produces the base-to-signal map, `sequence_bases_with_context` cuts
the window it covers plus the k-mer context, and `encode_signal_kmer` scatters
the one-hot context along the signal axis. leech held the middle and the last
of those, inside a cdylib no Rust consumer could link.
This is the one of the three most worth getting out of a second copy. It is
where `before` and `after` are not interchangeable — swap them and every k-mer
is read from a window displaced by `before - after` bases, silently, and
`encode_signal_kmer` cannot detect it because it only sees the total width.
Identical by construction and by test: the ranges agree
(`(seq_start - before)..(seq_end + after)` is upstream's
`core_start - before` plus `n_bases + before + after` with
`n_bases = seq_end - seq_start`), and both pad rather than shift, leech with
`b'N'` and upstream with `UNKNOWN_BASE_CHAR`, which is `b'N'`. 198 parity tests
pass untouched, and `sequence_with_kmer_context` is one of the fields
`test_backend_parity.py` compares array-by-array between the Rust and Python
backends — the Python side is not touched here, so it is an independent check.
Only `rust/Cargo.toml`'s git tag moves to v0.16.1; `Cargo.lock` pins it at
b4c9afae. The `escapepod` Python pin stays `>=0.16.0`, because the new function
is in the Rust crate and not in the Python bindings, and the two are
independent — leech_core links the crate statically, `leech.io` imports the
wheel.
Full suite 1467 passed, 44 skipped.
0 commit comments