Repository navigation
Releases: sriram98v/haystackfm
Release list
v0.8.0
Added
- Deduplication of identical input sequences in every index (
FmIndex,BidirFmIndex,
RlFmIndex,BidirRlFmIndex,MoveFmIndex,BMoveIndex; CPU and GPU builds). Input
sequences with byte-identical bases (#layout included; headers may differ) form an
equivalence class, and only the first member, its representative, is stored in the
text. A sequence that is only a prefix or substring of another is not grouped.SeqIds
keep their meaning (the position in the builder's input) for every sequence, stored or
not, andsequence(id)returns any member's bases. SeqClasses(seq_classes()on every index) plusequivalence_class(id),
representative(id)andnum_distinct_sequences()accessors.- Grouped query variants that report a match in a class once, on its representative:
locate_grouped/locate_rows_grouped(FM, RL, Move),locate_interval_grouped
(bidirectional indexes),BMoveIndex::locate_grouped, andfind_smems_grouped/
find_mems_grouped(BidirFmIndex,BidirRlFmIndex).BMoveIndex::count_cursorand
count_fwd(&FwdInterval)(bidirectional indexes) count a cursor's occurrences over every
member.
Changed
- Breaking (results). A match in a sequence with identical copies is reported once per
class member:locate,locate_rows,locate_interval,locate_fwd,
FwdInterval::locate,BMoveIndex::locate,Mem::positions(withlocate = true),
FmIndex::locate_gpuandMemHit::positions(with the index's ownseq_boundaries)
list one hit per member, consecutively and by ascending id, in place of the
representative's hit. Counts include every member:count,count_interval,
Mem::match_count,MemHit::match_countand the GPU batch MEM counts (so
match_count == positions.len()still holds for flat MEMs). Cursorsize()and the
count_*_in/count_wild_*helpers still count the stored text. Inputs without
identical sequences give the same results as before. - Breaking.
seq_boundaries()(now also onFmIndex),map_position,
locate_positions(_many),text_len()andBMoveCursor::toeholddescribe the stored
(deduplicated) text: boundaries are indexed by stored slot rather thanSeqId, and raw
positions lie in representatives only.find_smems_gpu/find_mems_gpuexpand hits
only when passed the index's ownseq_boundaries(); with any otherref_boundaries
table (e.g. one built in input order from the original sequences) positions are indices
into that table, one per stored occurrence, not expanded. - Breaking (files). Format versions
"HFM\x04","HRL\x04"/"HRB\x04"and
"HMV\x02"/"HBM\x02"store the sequence classes and the duplicate-row count weights.
Older files still load (every sequence in a class of its own); blobs written by this
version cannot be read by older builds. Loading now checks every format's sequence
tables (header count, boundaries ending at the text length), so a corrupt older
FmIndexfile that used to load is now rejected.
Known limitations
- The WASM bindings (
FmIndexHandle) return the expandedlocate/countresults but do
not yet expose the classes: noseq_classes,equivalence_class,representative,
num_distinct_sequencesor*_groupedvariants from JavaScript.
v0.7.0
Added
#record delimiter:DnaSequence::from_str("ACGT#TTGA")(orDnaSequence::join)
indexes several records (e.g. FASTA entries) as one logical sequence — oneSeqId, one
header, offsets counted across the joined string with each#taking one position. A
#matches nothing: no hit spans it, SMEM/MEM extension stops at it as at$, and a#
in a query matches nothing. Leading, trailing and doubled#are rejected.
alphabet::decode_sequencerenders extracted sequences with#.
Changed
- Breaking (run-length files). Run-length format
"HRL\x03"/"HRB\x03":
extraction rows are stored per#-delimited piece. Version 2 files still load; blobs
written by this version cannot be read by older builds.FORMAT_MAGIC/
BIDIR_FORMAT_MAGICnow read"HRL\x03"/"HRB\x03". - Breaking.
#is no longer an invalid character:encode_byte(b'#')/
encode_char('#')returnSome(SENTINEL)instead ofNone, and
DnaSequence::from_straccepts an interior#(see Added) where it used to return
InvalidCharacter.
Fixed
- Locate on an index built from
DnaSequence::from_encodedbases containing interior
sentinels: every piece start is now force-sampled, so the LF walk never crosses one.
Indexes already serialized from such text keep their old samples; rebuild them.
v0.6.9
Performance
BidirFmIndexnow runs the same SMEM/MEM sweeps as the run-length index: pruned MEM
sweep, lockstep SMEM backward phase, a derived k-mer interval cache (at most 2^21
intervals, built on construction and load, never serialized) and one batched locate per
query.BidirFmIndexsweep steps extend only the query symbol's compatible codes instead of
ranking every symbol, answer an extension inside one 64-row occ block from that block
alone, and filter left-maximal MEM rows a block at a time. On 200 Mbp at-O3:
find_mems4.4x,find_memswith locate 3.7x,find_smems1.3x faster than 0.6.8.
Fixed
BidirFmIndex::from_bytesrejects forward and reverse halves that do not match.
v0.6.8
First release with the run-length, move and b-move indexes. It covers 0.6.0 through 0.6.8; each patch version is one commit on the release-0.6.8 branch, squash-merged to main in #28.
0.6.8
Fixed
- The run-length k-mer interval cache holds at most 2^21 intervals and stops at the last
depth that fits: on a reference dense in ambiguity codes (or a crafted file, since loading
builds the cache) it could otherwise allocate gigabytes. RlFmIndex::locate_rowspanics on rows outside the text instead of reading SA samples out
of bounds; corrupted bidirectional halves yield no MEM rather than a wrapped count.
Performance
- On a 200 Mbp strain collection (n/r 4.3, 2000 × 150 bp reads,
min_len19, -O3) the
run-length bidirectional index finds SMEMs 1.6× and MEMs 5.3× faster than
BidirFmIndex(3.5× with locate), with a 2.42 B/base serialized index vs 3.66.
0.6.7
Added
BidirRlFmIndexderives a k-mer interval cache (every ACGT k-mer up to depth
min(8, log4 n), built by the sweeps' own extension so results are identical) that seeds
the first, widest steps of each SMEM pivot and MEM start. Rebuilt at load, never
serialized.
0.6.6
Changed
- Run-length blocks store the next block's first run start in-line, so run searches and
scans stay on one cache line; rank samples hold up to six symbols in one line.
0.6.5
Fixed
RlFmIndex::from_bytesrejects a zero-length first sequence and sequence end rows at or
past the text length (a crafted file could panic during load), and
BidirRlFmIndex::from_bytesrejects halves with different C arrays.- Run-length locate caps each LF walk at the sample rate (was the text length) and checks
the position addition for overflow.
0.6.4
Changed
- Run-length
rank_all_pairreads a narrow range's ranks off one predecessor search, so
BidirInterval::children_*cost one rank pass; the run-length sweeps take each step's
compatible extension and drop set from it. - The run-length MEM sweep computes each drop's left-maximal occurrence count from
coverages instead of left-extending, and builds a drop set only for accepted candidates.
0.6.3
Changed
- Breaking (run-length files):
RlBwtis laid out as 64-byte blocks of 8 runs (run
starts, head nibbles and per-symbol rank samples on one cache line) behind a row→run
bucket directory: a rank, LF step or extension reads two lines instead of a four-deep
chain. Only the runs are serialized, as"HRL\x02"/"HRB\x02"; blocks and directory are
rebuilt and the runs validated at load."\x01"run-length files no longer load. RlBwt::from_runs/from_bwt/from_occ/build_cpudrop theirOccEncoding
argument andRlFmIndexConfig::head_encodingis removed (there is no run-head table).- Run-length SMEM/MEM finding: MEM starts stop as soon as every occurrence is
left-extendable and skip drop sets belowmin_len; SMEM prefixes are extended left in
lockstep, reading right-maximality off the next-longer prefix and pruning equal-coverage
prefixes; locate is batched per query with interleaved, prefetched LF walks. Results are
identical toBidirFmIndex's.
Added
BwtRank::extend_ranks(c, lo, hi): one bidirectional extension step (both ranks and the
in-range count of smaller symbols). The default keeps the occ table's behaviour;RlBwt
walks the few runs of a narrow interval with one predecessor search and skips symbols
absent from the BWT.
0.6.2
Added
BidirRlFmIndex::find_smems/find_mems: SMEM and MEM finding on the run-length index,
identical toBidirFmIndex's. The sweep is shared through a crate-private trait over both
index types; per-occurrence left-maximality now reads the forward BWT instead of the
retained text.RlFmIndex::sequence/sequence_by_header: extraction by LF-walk from one stored
sentinel row per sequence (the text is not retained).- k-mer lookup table on
RlFmIndex(RlFmIndexConfig::lookup_depth) seeding backward
search;BidirRlFmIndex::lookup_interval/lookup_depth. RlFmIndex::locate_positions_many;BidirRlFmIndex::extend_left_fwd,locate_fwd,
count_interval;FwdInterval::extend_leftgeneric overFmHalf.- GPU
RlFmIndex::build/BidirRlFmIndex::build(featuregpu): an uncompressed GPU
build, then converted. examples/size_compare.rs: uncompressed vs run-length size, SMEM/MEM and extraction
parity and timings on a FASTA.
Changed
"HRL\x01"/"HRB\x01"files also carry the sequence end rows and the lookup table.
0.6.1
Added
- Move-structure indexes (
haystackfm::move_index,haystackfm::move_table):MoveTable: a permutation of[0, n)stored as 16-byte rows (start, image, image row,
payload).stepis one row read plus a short fast-forward. Optional Nishimoto–Tabei
balancing bounds the fast-forward.MoveFmIndex/MoveFmIndexConfig: an r-index on move tables.count,locate,locate_positions.- Locate runs in
O(r)space. SA samples at run boundaries give a toehold during
search. Balancedφ/φ⁻¹move tables with permuted-LCP samples enumerate the
interval from a toehold anywhere inside it. locate: falsebuilds a count-only index.
BMoveIndex/BMoveCursor(b-move): forward and reverse LF move tables driving a
cursor that carries both intervals, their rows and a toehold.- Operations:
extend_*,children_*,count_*_in,count_wild_*,*_compatible,
locate(&cursor),count. BMoveCursor::interval()gives the equivalentBidirInterval.
- Operations:
- Serialization under
"HMV\x01"/"HBM\x01". Every table is validated on load. benches/move_query.rscompares plain, run-length and move-structure indexes.
0.6.0
Added
- Run-length indexes for repetitive collections, the substrate for the move / b-move
structures:RlBwt(haystackfm::rlbwt): a run-length BWT (run-head occ table, run starts with a
bucket directory, per-symbol run-length prefix sums; ~9.6 bytes per run). Produced by
RlBwt::from_runs,from_bwt,from_occ(convert an existing index) andbuild_cpu;
inspected byruns,num_runs,run_ratio,get,to_bwt,heap_size_bytes.RlFmIndex/RlFmIndexConfig(haystackfm::rl_fm_index):count,locate,
locate_positions,locate_rows,map_position, header accessors; locate LF-walks to
the sampled SA.sa_sample_rate = 0builds a count-only index.From<&FmIndex>.BidirRlFmIndex: theBidirIntervalcursor (extend_*,children_*,count_*_in,
count_wild_*,*_compatible),count,locate_interval.From<&BidirFmIndex>.- Serialization under their own magics,
"HRL\x01"and"HRB\x01", validated in full at
load (run structure, head table, C array, SA samples).
BwtRank: the rank/access trait the query code needs, implemented byOccTableand
RlBwt.FmHalf(sealed): one half of a bidirectional index, as seen by the cursor.Bwt::runs,FmIndex::occ,OccTable::encoding,OccTable::heap_size_bytes,
SampledSuffixArray::heap_size_bytes.benches/rl_query.rs: run-length vs plaincount/locate/extend_righton seeded
random and repetitive inputs.
Changed
BidirInterval::extend_*,children_*,count_*_in,count_wild_*and
extend_*_compatibleare generic overFmHalf(source-compatible:&FmIndexstill
works). Contraction still takes&FmIndex.
Fixed
FmIndex::from_bytes(and soBidirFmIndex::from_bytes) now checks the occ table's and
SA samples' buffers against their declared lengths before returning. Their record reads
are unchecked, so a corrupted or hand-crafted blob could previously read out of bounds
on the first query; it is now aDeserializeError.- The
BidirIntervaldocs had the extension formulae's directions swapped (right extension
runs on the reverse index).
v0.5.0
Added
AlphabetFns::new/from_compatible_fn/compatible/core/tag: an alphabet's
matching semantics as data — oneSymbolSetof compatible reference codes per query
code plus the core symbol set and a tag.CArray::present_symbols.LookupHitandLookupTable::MAX_VARIANTS_PER_ENTRY: a k-mer entry now exposes its
exact interval and the intervals of its wildcard variants, and whether it is complete.examples/gen_fixtures.rswrites the frozen-format fixtures undertests/fixtures/;
version-2 fixtures (v2_*.hfm) added, including anExactDnaone.- The WGSL
COMPAT/COMPAT_LENtables inlocate_search.wgslandmem_find.wgslare
parsed out of the shader sources by a plain unit test and checked againstIupacDna,
instead of against a hand-copied Rust array. serde_raw::u32_pairsblob codec forVec<(u32, u32)>.OccTable::rank_with_below(c, i)/rank_with_below_pair(c, lo, hi):rank(c, ·)and
the class rank of every symbol belowcfrom one block-record load per border — the
two numbers a bidirectional extension step needs.OccTable::rank_all_pair(lo, hi):
rank_allon both borders with both records prefetched first.BidirInterval::contract_right/BidirFmIndex::contract_right: the inverse of
extend_right(Pc → P), the mirror ofcontract_leftrun on the reverse half (select,
LCP widening and class rank on the reverse index, forward interval recovered from the
offset). Same cost and error behaviour ascontract_left.BidirInterval::contract_left/BidirFmIndex::contract_left: the inverse of
extend_left(cP → P) without re-walking. OneOccTable::selecton the forward half
gives ψ of the interval's first row, the forward LCP array widens that sub-range to the
full interval of P (O(1) when every occurrence of P is preceded by c, O(log n) otherwise
via block minima + a sparse table), and the existing class rank recovers the reverse
interval. Works across sentinel-preceded occurrences and IUPAC reference codes.
has_lcp()reports whether contraction is available.LcpArray(src/lcp.rs): Kasai LCP over the SA rows stored capped atu16::MAX, with
psv_below/nsv_below. Built during CPU construction while the full suffix array is
resident; opt-in viaFmIndexConfig::build_lcp(defaultfalse). ABidirFmIndex
builds one per half (the forward one backscontract_left/parent_fwd, the reverse
onecontract_right), so opting in grows its resident and serialized size by ~2.7
B/base per half. Default builds carry none:has_lcp()isfalseand the contraction
/ parent operations returnLcpNotBuilt.BidirFmIndex::has_lcp()is true only when
both halves carry one; usefwd().has_lcp()/rev().has_lcp()for a single direction.OccTable::select(c, r): position of the r-th occurrence ofcin the BWT, from the
existing superblock/block/lane-mask layout plus a small hint array (rebuilt on load,
not serialized).- Serialized indexes now start with a 4-byte format marker (
"HFM"plus a version byte,
currently 2; see Changed). Legacy blobs without it still load, with
has_lcp() == false. benches/serialize.rsandexamples/serde_profile.rsmeasureto_bytes/
from_bytes, the single-threaded startup cost a consumer pays before any query.FmIndexError::LcpNotBuilt,PatternTooLong,InvalidContraction.FwdInterval(BidirInterval::fwd()): a forward-only row range withextend_left
(an LF step, valid on any sub-range of an interval),parent(the nearest suffix-tree
ancestorP[..d'],d' = max(LCP[lo], LCP[hi]), widened over the LCP array) and
locate.BidirFmIndex::extend_left_fwd/parent_fwd/locate_fwdand
fwd()/rev()accessors;FmIndex::locate_rows(lo, hi). Together with
contract_leftthese support sliding-window MEM sweeps whose ancestor MEMs are read off
aparentwalk instead of re-extended.BidirFmIndex::lookup_interval(kmer)/lookup_depth(): seed a cursor for a
lookup_depth-mer from the k-mer tables of both halves, skipping that many extensions.SymbolSet: a 16-bit set of alphabet codes withEMPTY,ALL,BASES,WILDCARDS
(codes 5..=15),NON_SENTINEL,single,below,from_codes, set algebra and
iteration.AlphabetFns::compatible_set(q)returns the codesqmatches under that
alphabet as aSymbolSet.OccTable::rank_set,rank_set_pairandrank_all: class ranks over the occurrence
table. Every block record already holds all lanes, so counting "any symbol in this set"
costs one block touch regardless of the set's size, andrank_allreturns every symbol's
rank at once.- Wildcard-aware bidirectional cursor operations, on both
BidirInterval(taking the
relevant&FmIndexhalf) andBidirFmIndex:count_wild_right/count_wild_left: occurrences followed / preceded in the reference
by an ambiguity code (Nor a degenerate IUPAC symbol). Two occ-block touches, no text
access, independent of how many wildcard codes exist — a cursor walk can ask at every
step and fan out only when the answer is non-zero.count_right_in/count_left_in: the same for an arbitrarySymbolSet, e.g.
compatible_set(base).intersection(SymbolSet::WILDCARDS).children_right/children_left: every child interval for all 16 codes from two
rank_allcalls, bit-identical to the per-codeextend_*results; slot 0 is the
sentinel child (occurrences at a reference end / start) and child sizes sum to the
parent's size.extend_right_compatible/extend_left_compatible: the compatible-symbol fan-out
find_smems/find_memsuse internally, now public, driven by the index's own
alphabet;BidirFmIndex::compatible_setexposes that alphabet's match set.
Changed
- Breaking (on-disk format). Serialized indexes are format version 3 (
"HFM\x03"):
the alphabet's matching tables are stored in full (35 bytes) instead of a tag, so an
index built with a customAlphabetloads back with the semantics it was built with,
and the lookup table is stored in CSR form with wildcard variants (see Fixed). A
built-in tag (0, 1) must match its tables and tags 2..=127 are reserved; both are
rejected withDeserializeError. Version-2, version-1 and legacy blobs still load;
their lookup table is converted in place when it is provably complete (pure-ACGT
reference, orExactDna) and rebuilt at load otherwise.FORMAT_MAGICnow reads
"HFM\x03",FORMAT_MAGIC_V2names the previous marker. Blobs written by this version
cannot be read by older builds. - Breaking.
AlphabetFnsfields are private; construct withAlphabetFns::newor
AlphabetFns::from_compatible_fnand read withcompatible(q)/core()/tag().
compatible_setremains as an alias. The fan-out order ofextend_*_compatible,
backward search and MEM extension is ascending code order (unchanged for the built-in
alphabets, whose tables were already ascending). - Breaking.
LookupTable::getreturnsOption<LookupHit>(exact slot plus variant
intervals and a completeness flag) instead ofOption<(u32, u32)>, and returnsNone
for a k-mer of the wrong length rather than debug-asserting. Its build takes the
AlphabetFnsrather than a core-symbol slice. Memory on a pure-ACGT reference is about
12 × 4^depthbytes (was 8) plus 8 bytes per stored wildcard variant. - Backward search and the locate resolve table intersect the query code's compatibility
mask with the text's present symbols once per step, instead of walking a slice and
checking each symbol's count: one loop iteration per present compatible code. BidirInterval::extend_right/extend_lefttouch two occ block records per step
instead of four: the LF rank and thebelow(c)class rank at each border now come from
one fused load (rank_with_below_pair), with thebelow(c)lane mask precomputed per
symbol at build/load time.children_right/children_leftprefetch both borders
before ranking. Results are bit-identical.- Breaking (on-disk format). Serialized indexes are format version 2 (
"HFM\x02"):
the occ table's checkpoints and block records, the SA samples, the LCP arrays and the
text are written as length-prefixed little-endian byte blobs thatfrom_bytescopies
in bulk instead of decoding one element at a time through serde. Loading a 117 MB
bidirectional index (10 Mbp,sa_sample_rate: 1,OneHot, no LCP) drops from ~630 ms
to ~15 ms under the crate's release profile (~68 ms to ~10 ms at opt-level 3).
Version-1 ("HFM\x01") and legacy blobs still load unchanged; any other"HFM"version
is rejected with a clearDeserializeError.FORMAT_MAGICnow reads"HFM\x02"and
FORMAT_MAGIC_V1names the previous marker. Blobs written by this version cannot be
read by older builds. BidirFmIndex::to_bytesreturnsSerializeErrorinstead of silently truncating the
length prefix when the forward half exceeds 4 GiB.- Breaking.
BidirIntervalgains alen: u32field (the matched pattern length,
maintained by every extension and contraction); struct literals must supply it. BidirFmIndex::build_cpu*now builds the reverse half with the configured
lookup_depth(previously always 0) solookup_intervalcan seed both halves; lookup
memory doubles when the table is enabled.- Breaking.
FmIndexConfiggainsbuild_lcp: bool(defaultfalse); exhaustive
struct literals must supply it. Set it totrueonly when the cursor contraction /
parent operations are needed; CPU-built indexes then grow by ~2.7 bytes per base per
half. GPU construction never builds the LCP. BidirInterval::extend_right/extend_leftcompute the paired-interval offset with a
single class rank per border instead of one scalar rank per smaller symbol. Results are
unchanged; extending by a high IUPAC ...
v0.4.0
Breaking behavior change. The public API is unchanged — cargo semver-checks reports no
semver update required — but find_mems returns materially different results, so this takes
a minor bump under the pre-1.0 policy above rather than the patch bump an API-only check
would allow.
Fixed
-
Breaking.
BidirFmIndex::find_memsnow enumerates MEMs in the MUMmer / BWA sense:
a query interval is reported when at least one of its occurrences is maximal in both
directions at that occurrence. It previously required every occurrence to be
maximal, so a single extendable occurrence in any one reference deleted the interval.
Both the right-maximality test (the forward loop stopped only when the occurrence set went
empty, discarding occurrences that dropped out mid-extension) and the left-maximality test
(rejecting the interval if any occurrence was left-extendable) were affected.Consequences of the old behavior: at most one interval per query start position, and a
result set that collapsed ontofind_smems, making MEM-vs-SMEM comparisons through this
API a guaranteed null result. In a database of near-identical references, per-reference
match recovery was systematically sparse.For
query = ACGTACGTACagainstACGTACGTACandACGTACTwithmin_len = 2, the result
goes from{(0,10)}to{(0,2), (0,6), (0,10), (4,10), (8,10)}.find_smemsis unaffected and unchanged: the containment-maximal MEMs under the new
definition are exactly the SMEMs it already returned.
Changed
- Breaking.
Mem::match_countandMem::positionsfromfind_memsnow cover only the
maximal occurrences of a match, not every occurrence of the matched substring. find_memsoutput size is now bounded by O(|query|²) rather than |query|;min_lenis the
only guard. Expectbenches/mem_bench.rsandbenches/mem_positions_bench.rstimings to
move accordingly.
Known issues
find_mems_gpustill runs the previous whole-set algorithm and is not equivalent to
find_mems. Theshaders/mem_find.wgslMODE_MEM port is tracked inKNOWN-ISSUES.md.
v0.3.0 — serve sequence bases by id
[0.3.0] - 2026-07-24
Breaking release: the index now retains the indexed text and can serve the bases of any
sequence, so callers no longer need to keep their own copy of every reference.
Added
FmIndex::sequence(SeqId)/BidirFmIndex::sequence(SeqId)— the bases of one indexed
sequence as a borrowed slice, in O(1), with the trailing sentinel excluded. Returns
alphabet codes (A = 1,C = 2, …), not ASCII; usedecode_charto render them.FmIndex::sequence_by_header(&str)/BidirFmIndex::sequence_by_header(&str)— the same
by header, equivalent toseq_id(h).and_then(|id| self.sequence(id)).encode_byte,encode_charanddecode_charre-exported at the crate root, so callers
can move queries into (and results out of) the alphabet's code space without reaching
into thealphabetmodule.
Changed
- Breaking. The serialized layout gains a
textfield, so indexes written by 0.2.0 and
earlier are rejected byfrom_bytesand must be rebuilt. - The concatenated text is no longer dropped after BWT construction. This costs ~n bytes of
resident and serialized size and forgoes a build-time peak-memory reduction, in exchange
for random-access substrings. An FM-index can otherwise only recover text by LF-walking
backwards one symbol at a time — far too slow for a caller rescoring a read against a
candidate diagonal, which is the case this exists to serve. - Only the forward half of a
BidirFmIndexretains text. The reverse half's copy is a
redundant reversal that is never served, so it is released at build time, keeping the
overhead at ~n rather than ~2n.
Upgrading
Indexes written by 0.2.0 and earlier are rejected by from_bytes and must be rebuilt.
Full changelog: v0.2.0...v0.3.0
webgpu-fmidx v0.3.0
What's New
Full IUPAC Nucleotide Alphabet Support (GPU)
- Updated all GPU shaders (locate_search, locate_resolve, mem_find, mem_resolve) to support the full 16-symbol IUPAC alphabet (ALPHA=6 → 16)
- Expanded C-array params in all GPU Params structs from 6 to 16 values with correct WGSL uniform alignment
- Updated Rust driver structs in locate.rs, mem_find.rs, mem_resolve.rs to use
[u32; 16]C-array bindings
Full IUPAC Support (CPU) — v0.2.0
- All 16 IUPAC ambiguity codes (A, C, G, T, N, R, Y, S, W, K, M, B, D, H, V) supported in index, count, locate, SMEM, and MEM
- IUPAC base-set intersection matching in CPU backward search and SMEM/MEM finding