From 540f1947c4e1c596effff797649774cf7b3d7b52 Mon Sep 17 00:00:00 2001 From: Yasset Perez-Riverol Date: Sat, 18 Apr 2026 07:58:14 +0100 Subject: [PATCH 1/6] feat(fragindex): EliasFano stub with empty-list codec --- .../edu/ucsd/msjava/fragindex/EliasFano.java | 24 +++++++++++++++++++ .../ucsd/msjava/fragindex/TestEliasFano.java | 15 ++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 src/main/java/edu/ucsd/msjava/fragindex/EliasFano.java create mode 100644 src/test/java/edu/ucsd/msjava/fragindex/TestEliasFano.java diff --git a/src/main/java/edu/ucsd/msjava/fragindex/EliasFano.java b/src/main/java/edu/ucsd/msjava/fragindex/EliasFano.java new file mode 100644 index 00000000..89da2fc9 --- /dev/null +++ b/src/main/java/edu/ucsd/msjava/fragindex/EliasFano.java @@ -0,0 +1,24 @@ +package edu.ucsd.msjava.fragindex; + +/** + * Elias-Fano compression for sorted (monotonically non-decreasing) int[] lists. + * Used by the fragment index to store peptide-id lists per fragment-mass bucket + * at ~0.5-1 byte per entry. + * + * Empty-list case handled here; monotonic-list encoding lands in a later task. + */ +public final class EliasFano { + private EliasFano() {} + + public static byte[] encode(int[] values) { + if (values.length == 0) return new byte[]{0, 0, 0, 0}; // length prefix only + throw new UnsupportedOperationException("non-empty encode not implemented yet"); + } + + public static int[] decode(byte[] encoded) { + int len = (encoded[0] & 0xff) | ((encoded[1] & 0xff) << 8) + | ((encoded[2] & 0xff) << 16) | ((encoded[3] & 0xff) << 24); + if (len == 0) return new int[0]; + throw new UnsupportedOperationException("non-empty decode not implemented yet"); + } +} diff --git a/src/test/java/edu/ucsd/msjava/fragindex/TestEliasFano.java b/src/test/java/edu/ucsd/msjava/fragindex/TestEliasFano.java new file mode 100644 index 00000000..34f57427 --- /dev/null +++ b/src/test/java/edu/ucsd/msjava/fragindex/TestEliasFano.java @@ -0,0 +1,15 @@ +package edu.ucsd.msjava.fragindex; + +import org.junit.Assert; +import org.junit.Test; + +public class TestEliasFano { + + @Test + public void emptyListRoundTrip() { + byte[] encoded = EliasFano.encode(new int[0]); + Assert.assertNotNull(encoded); + int[] decoded = EliasFano.decode(encoded); + Assert.assertEquals(0, decoded.length); + } +} From e4b01d6413f3dd8557bcd4418f4822dc319675d7 Mon Sep 17 00:00:00 2001 From: Yasset Perez-Riverol Date: Sat, 18 Apr 2026 08:05:09 +0100 Subject: [PATCH 2/6] feat(fragindex): naive int[] encoding behind EliasFano API --- .../edu/ucsd/msjava/fragindex/EliasFano.java | 33 +++++++++++++------ .../ucsd/msjava/fragindex/TestEliasFano.java | 22 +++++++++++++ 2 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/main/java/edu/ucsd/msjava/fragindex/EliasFano.java b/src/main/java/edu/ucsd/msjava/fragindex/EliasFano.java index 89da2fc9..97b8e6e2 100644 --- a/src/main/java/edu/ucsd/msjava/fragindex/EliasFano.java +++ b/src/main/java/edu/ucsd/msjava/fragindex/EliasFano.java @@ -1,24 +1,37 @@ package edu.ucsd.msjava.fragindex; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + /** - * Elias-Fano compression for sorted (monotonically non-decreasing) int[] lists. - * Used by the fragment index to store peptide-id lists per fragment-mass bucket - * at ~0.5-1 byte per entry. + * Simple Elias-Fano-inspired codec for sorted non-decreasing int[] lists. + * + * Layout (little-endian): + * [4 bytes: length N] + * [4 bytes: max value U, or 0 if N==0] + * [for each value i: 4 bytes raw int] * - * Empty-list case handled here; monotonic-list encoding lands in a later task. + * This first cut is correctness-only — plain int array encoding. A compact + * Elias-Fano layout replaces this in Task 7 once the API shape is stable. */ public final class EliasFano { private EliasFano() {} public static byte[] encode(int[] values) { - if (values.length == 0) return new byte[]{0, 0, 0, 0}; // length prefix only - throw new UnsupportedOperationException("non-empty encode not implemented yet"); + int n = values.length; + ByteBuffer buf = ByteBuffer.allocate(8 + 4 * n).order(ByteOrder.LITTLE_ENDIAN); + buf.putInt(n); + buf.putInt(n == 0 ? 0 : values[n - 1]); + for (int v : values) buf.putInt(v); + return buf.array(); } public static int[] decode(byte[] encoded) { - int len = (encoded[0] & 0xff) | ((encoded[1] & 0xff) << 8) - | ((encoded[2] & 0xff) << 16) | ((encoded[3] & 0xff) << 24); - if (len == 0) return new int[0]; - throw new UnsupportedOperationException("non-empty decode not implemented yet"); + ByteBuffer buf = ByteBuffer.wrap(encoded).order(ByteOrder.LITTLE_ENDIAN); + int n = buf.getInt(); + buf.getInt(); // max value; unused in this naive layout + int[] out = new int[n]; + for (int i = 0; i < n; i++) out[i] = buf.getInt(); + return out; } } diff --git a/src/test/java/edu/ucsd/msjava/fragindex/TestEliasFano.java b/src/test/java/edu/ucsd/msjava/fragindex/TestEliasFano.java index 34f57427..8dbf90d8 100644 --- a/src/test/java/edu/ucsd/msjava/fragindex/TestEliasFano.java +++ b/src/test/java/edu/ucsd/msjava/fragindex/TestEliasFano.java @@ -12,4 +12,26 @@ public void emptyListRoundTrip() { int[] decoded = EliasFano.decode(encoded); Assert.assertEquals(0, decoded.length); } + + @Test + public void singleValueRoundTrip() { + int[] original = {42}; + int[] decoded = EliasFano.decode(EliasFano.encode(original)); + Assert.assertArrayEquals(original, decoded); + } + + @Test + public void monotonicListRoundTrip() { + int[] original = {0, 1, 5, 12, 12, 18, 31, 47}; + int[] decoded = EliasFano.decode(EliasFano.encode(original)); + Assert.assertArrayEquals(original, decoded); + } + + @Test + public void largeRangeRoundTrip() { + int[] original = new int[1000]; + for (int i = 0; i < original.length; i++) original[i] = i * 53; + int[] decoded = EliasFano.decode(EliasFano.encode(original)); + Assert.assertArrayEquals(original, decoded); + } } From 4c7e68f7a355db762928581b70d11ed37f6cad43 Mon Sep 17 00:00:00 2001 From: Yasset Perez-Riverol Date: Sat, 18 Apr 2026 08:09:21 +0100 Subject: [PATCH 3/6] feat(fragindex): EliasFano.open() returns a Cursor for zero-copy decode --- .../edu/ucsd/msjava/fragindex/EliasFano.java | 26 +++++++++++++++++++ .../ucsd/msjava/fragindex/TestEliasFano.java | 18 +++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/main/java/edu/ucsd/msjava/fragindex/EliasFano.java b/src/main/java/edu/ucsd/msjava/fragindex/EliasFano.java index 97b8e6e2..11ff490e 100644 --- a/src/main/java/edu/ucsd/msjava/fragindex/EliasFano.java +++ b/src/main/java/edu/ucsd/msjava/fragindex/EliasFano.java @@ -34,4 +34,30 @@ public static int[] decode(byte[] encoded) { for (int i = 0; i < n; i++) out[i] = buf.getInt(); return out; } + + public static final class Cursor { + private final ByteBuffer buf; + private final int total; + private int index; + + Cursor(ByteBuffer buf, int total) { + this.buf = buf; + this.total = total; + this.index = 0; + } + + public boolean hasNext() { return index < total; } + public int next() { + int v = buf.getInt(); + index++; + return v; + } + } + + public static Cursor open(byte[] encoded) { + ByteBuffer buf = ByteBuffer.wrap(encoded).order(ByteOrder.LITTLE_ENDIAN); + int n = buf.getInt(); + buf.getInt(); // max (unused) + return new Cursor(buf, n); + } } diff --git a/src/test/java/edu/ucsd/msjava/fragindex/TestEliasFano.java b/src/test/java/edu/ucsd/msjava/fragindex/TestEliasFano.java index 8dbf90d8..c7e685ed 100644 --- a/src/test/java/edu/ucsd/msjava/fragindex/TestEliasFano.java +++ b/src/test/java/edu/ucsd/msjava/fragindex/TestEliasFano.java @@ -34,4 +34,22 @@ public void largeRangeRoundTrip() { int[] decoded = EliasFano.decode(EliasFano.encode(original)); Assert.assertArrayEquals(original, decoded); } + + @Test + public void iteratorMatchesArray() { + int[] original = {2, 3, 5, 7, 11, 13, 17, 19}; + byte[] encoded = EliasFano.encode(original); + EliasFano.Cursor it = EliasFano.open(encoded); + int i = 0; + while (it.hasNext()) { + Assert.assertEquals(original[i++], it.next()); + } + Assert.assertEquals(original.length, i); + } + + @Test + public void iteratorOnEmpty() { + EliasFano.Cursor it = EliasFano.open(EliasFano.encode(new int[0])); + Assert.assertFalse(it.hasNext()); + } } From f129d55035470188114fe00b7aa43f4aa6e4a1a9 Mon Sep 17 00:00:00 2001 From: Yasset Perez-Riverol Date: Sat, 18 Apr 2026 08:12:31 +0100 Subject: [PATCH 4/6] feat(fragindex): Fingerprint128 bit-set + popcount primitive --- .../ucsd/msjava/fragindex/Fingerprint128.java | 43 ++++++++++++++++ .../msjava/fragindex/TestFingerprint128.java | 51 +++++++++++++++++++ 2 files changed, 94 insertions(+) create mode 100644 src/main/java/edu/ucsd/msjava/fragindex/Fingerprint128.java create mode 100644 src/test/java/edu/ucsd/msjava/fragindex/TestFingerprint128.java diff --git a/src/main/java/edu/ucsd/msjava/fragindex/Fingerprint128.java b/src/main/java/edu/ucsd/msjava/fragindex/Fingerprint128.java new file mode 100644 index 00000000..0db338a3 --- /dev/null +++ b/src/main/java/edu/ucsd/msjava/fragindex/Fingerprint128.java @@ -0,0 +1,43 @@ +package edu.ucsd.msjava.fragindex; + +/** + * 128-bit fragment fingerprint split into b-ion / y-ion halves. + * + *

Each theoretical fragment hashes into one bit in the appropriate half via + * {@code bucket_index % 64}. At search time a spectrum's fingerprint is ANDed + * with each candidate peptide's fingerprint and popcounted — peptides whose + * fragment set doesn't share enough bits with the spectrum are pruned before + * any fragment-index lookup runs. + * + *

Threshold tuning (default {@code popcountAnd ≥ 8}) lives in the caller, + * not this class. + */ +public final class Fingerprint128 { + private long lo; // b-ion bits + private long hi; // y-ion bits + + public Fingerprint128() {} + + public Fingerprint128(long lo, long hi) { + this.lo = lo; + this.hi = hi; + } + + public void setBIonBucket(int bucketIndex) { + lo |= 1L << (bucketIndex & 63); + } + + public void setYIonBucket(int bucketIndex) { + hi |= 1L << (bucketIndex & 63); + } + + public int popcountB() { return Long.bitCount(lo); } + public int popcountY() { return Long.bitCount(hi); } + + public int popcountAnd(Fingerprint128 other) { + return Long.bitCount(lo & other.lo) + Long.bitCount(hi & other.hi); + } + + public long loBits() { return lo; } + public long hiBits() { return hi; } +} diff --git a/src/test/java/edu/ucsd/msjava/fragindex/TestFingerprint128.java b/src/test/java/edu/ucsd/msjava/fragindex/TestFingerprint128.java new file mode 100644 index 00000000..7ebb34e7 --- /dev/null +++ b/src/test/java/edu/ucsd/msjava/fragindex/TestFingerprint128.java @@ -0,0 +1,51 @@ +package edu.ucsd.msjava.fragindex; + +import org.junit.Assert; +import org.junit.Test; + +public class TestFingerprint128 { + + @Test + public void newFingerprintIsEmpty() { + Fingerprint128 fp = new Fingerprint128(); + Assert.assertEquals(0, fp.popcountAnd(fp)); + } + + @Test + public void bIonBitSetsLowHalf() { + Fingerprint128 fp = new Fingerprint128(); + fp.setBIonBucket(3); + Assert.assertEquals(1, fp.popcountB()); + Assert.assertEquals(0, fp.popcountY()); + } + + @Test + public void yIonBitSetsHighHalf() { + Fingerprint128 fp = new Fingerprint128(); + fp.setYIonBucket(3); + Assert.assertEquals(0, fp.popcountB()); + Assert.assertEquals(1, fp.popcountY()); + } + + @Test + public void intersectionCountsOnlySharedBits() { + Fingerprint128 a = new Fingerprint128(); + a.setBIonBucket(1); a.setBIonBucket(2); a.setBIonBucket(3); + a.setYIonBucket(1); a.setYIonBucket(2); + + Fingerprint128 b = new Fingerprint128(); + b.setBIonBucket(2); b.setBIonBucket(4); + b.setYIonBucket(1); b.setYIonBucket(5); + + // shared b-ion buckets: {2}; shared y-ion buckets: {1}; total popcount = 2 + Assert.assertEquals(2, a.popcountAnd(b)); + } + + @Test + public void largeBucketIndicesWrapModulo64() { + Fingerprint128 fp = new Fingerprint128(); + fp.setBIonBucket(0); + fp.setBIonBucket(64); // should collide with bucket 0 mod 64 + Assert.assertEquals(1, fp.popcountB()); + } +} From e45089139088729c5183007c799c75a4f30867fd Mon Sep 17 00:00:00 2001 From: Yasset Perez-Riverol Date: Sat, 18 Apr 2026 08:16:12 +0100 Subject: [PATCH 5/6] feat(fragindex): SlabBuilder + immutable Slab view, bucket round-trip Co-Authored-By: Claude Sonnet 4.6 --- .../java/edu/ucsd/msjava/fragindex/Slab.java | 70 +++++++++++++++++ .../ucsd/msjava/fragindex/SlabBuilder.java | 72 ++++++++++++++++++ .../msjava/fragindex/TestSlabBuilder.java | 75 +++++++++++++++++++ 3 files changed, 217 insertions(+) create mode 100644 src/main/java/edu/ucsd/msjava/fragindex/Slab.java create mode 100644 src/main/java/edu/ucsd/msjava/fragindex/SlabBuilder.java create mode 100644 src/test/java/edu/ucsd/msjava/fragindex/TestSlabBuilder.java diff --git a/src/main/java/edu/ucsd/msjava/fragindex/Slab.java b/src/main/java/edu/ucsd/msjava/fragindex/Slab.java new file mode 100644 index 00000000..65312caf --- /dev/null +++ b/src/main/java/edu/ucsd/msjava/fragindex/Slab.java @@ -0,0 +1,70 @@ +package edu.ucsd.msjava.fragindex; + +/** + * Immutable read-only view over one precursor-mass slab of the fragment index. + * + *

Returned by {@link SlabBuilder#finish()} once all peptides and fragments + * are loaded. Immutable by construction: the fingerprint array is never + * mutated after construction, and {@link #fingerprint(int)} returns a fresh + * snapshot rather than the internal object. Safe for concurrent readers. + */ +public final class Slab { + private final int slabId; + private final double minMassDa; + private final double maxMassDa; + private final int peptideCount; + private final Fingerprint128[] fingerprints; + private final byte[][] bucketEncoded; // bucket -> Elias-Fano-encoded peptide-id list + + Slab(int slabId, double minMassDa, double maxMassDa, + Fingerprint128[] fingerprints, byte[][] bucketEncoded) { + this.slabId = slabId; + this.minMassDa = minMassDa; + this.maxMassDa = maxMassDa; + this.peptideCount = fingerprints.length; + this.fingerprints = fingerprints; + this.bucketEncoded = bucketEncoded; + } + + public int slabId() { return slabId; } + public double minMassDa() { return minMassDa; } + public double maxMassDa() { return maxMassDa; } + public int peptideCount() { return peptideCount; } + + /** + * Returns the fingerprint bits for the given peptide as an immutable + * 2-long snapshot. The returned Fingerprint128 is a fresh object built + * from the peptide's lo/hi bit-words; mutating it has no effect on the + * slab's internal state. Callers that only need bit-level AND+popcount + * can use {@link #fingerprintLoBits(int)} / {@link #fingerprintHiBits(int)} + * for zero-allocation access. + */ + public Fingerprint128 fingerprint(int peptideId) { + Fingerprint128 src = fingerprints[peptideId]; + return new Fingerprint128(src.loBits(), src.hiBits()); + } + + /** Zero-allocation read of the b-ion fingerprint word for a peptide. */ + public long fingerprintLoBits(int peptideId) { + return fingerprints[peptideId].loBits(); + } + + /** Zero-allocation read of the y-ion fingerprint word for a peptide. */ + public long fingerprintHiBits(int peptideId) { + return fingerprints[peptideId].hiBits(); + } + + public int[] peptidesInBucket(int bucket) { + if (bucket < 0 || bucket >= bucketEncoded.length) return new int[0]; + byte[] enc = bucketEncoded[bucket]; + if (enc == null) return new int[0]; + return EliasFano.decode(enc); + } + + public EliasFano.Cursor bucketCursor(int bucket) { + if (bucket < 0 || bucket >= bucketEncoded.length || bucketEncoded[bucket] == null) { + return EliasFano.open(EliasFano.encode(new int[0])); + } + return EliasFano.open(bucketEncoded[bucket]); + } +} diff --git a/src/main/java/edu/ucsd/msjava/fragindex/SlabBuilder.java b/src/main/java/edu/ucsd/msjava/fragindex/SlabBuilder.java new file mode 100644 index 00000000..550a6773 --- /dev/null +++ b/src/main/java/edu/ucsd/msjava/fragindex/SlabBuilder.java @@ -0,0 +1,72 @@ +package edu.ucsd.msjava.fragindex; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.TreeMap; + +/** + * Writable buffer for assembling a single slab of the fragment index. + * + *

Used during index build. Caller flow: + *

+ *   SlabBuilder b = new SlabBuilder(slabId, minMassDa, maxMassDa);
+ *   int pid = b.addPeptide(peptideMassDa);
+ *   b.addFragment(pid, fragmentBucket, isB);
+ *   ...
+ *   Slab slab = b.finish();
+ * 
+ * + *

Not thread-safe. Each builder is owned by a single build thread. + */ +public final class SlabBuilder { + private final int slabId; + private final double minMassDa; + private final double maxMassDa; + private final List fingerprints = new ArrayList<>(); + private final TreeMap> bucketToPeptides = new TreeMap<>(); + private boolean finished; + + public SlabBuilder(int slabId, double minMassDa, double maxMassDa) { + this.slabId = slabId; + this.minMassDa = minMassDa; + this.maxMassDa = maxMassDa; + } + + public int addPeptide(double precursorMassDa) { + requireNotFinished(); + int pid = fingerprints.size(); + fingerprints.add(new Fingerprint128()); + return pid; + } + + public void addFragment(int peptideId, int bucket, boolean isB) { + requireNotFinished(); + Fingerprint128 fp = fingerprints.get(peptideId); + if (isB) fp.setBIonBucket(bucket); + else fp.setYIonBucket(bucket); + bucketToPeptides.computeIfAbsent(bucket, k -> new ArrayList<>()).add(peptideId); + } + + public Slab finish() { + requireNotFinished(); + finished = true; + int maxBucket = bucketToPeptides.isEmpty() ? 0 : bucketToPeptides.lastKey(); + byte[][] bucketEncoded = new byte[maxBucket + 1][]; + for (var entry : bucketToPeptides.entrySet()) { + List pids = entry.getValue(); + Collections.sort(pids); + int[] arr = new int[pids.size()]; + for (int i = 0; i < arr.length; i++) arr[i] = pids.get(i); + bucketEncoded[entry.getKey()] = EliasFano.encode(arr); + } + Fingerprint128[] fpArr = fingerprints.toArray(new Fingerprint128[0]); + return new Slab(slabId, minMassDa, maxMassDa, fpArr, bucketEncoded); + } + + private void requireNotFinished() { + if (finished) { + throw new IllegalStateException("SlabBuilder is single-use; finish() already called"); + } + } +} diff --git a/src/test/java/edu/ucsd/msjava/fragindex/TestSlabBuilder.java b/src/test/java/edu/ucsd/msjava/fragindex/TestSlabBuilder.java new file mode 100644 index 00000000..e65ae098 --- /dev/null +++ b/src/test/java/edu/ucsd/msjava/fragindex/TestSlabBuilder.java @@ -0,0 +1,75 @@ +package edu.ucsd.msjava.fragindex; + +import org.junit.Assert; +import org.junit.Test; + +public class TestSlabBuilder { + + @Test + public void buildSlabWithTwoPeptidesAndQueryByBucket() { + SlabBuilder b = new SlabBuilder(/*slabId=*/0, /*minMassDa=*/500.0, /*maxMassDa=*/550.0); + + // peptide 0: b-ion at bucket 10, y-ion at bucket 20 + int p0 = b.addPeptide(/*precursorMassDa=*/510.0); + b.addFragment(p0, /*bucket=*/10, /*isB=*/true); + b.addFragment(p0, /*bucket=*/20, /*isB=*/false); + + // peptide 1: b-ion at bucket 10 (shared with p0), y-ion at bucket 30 + int p1 = b.addPeptide(520.0); + b.addFragment(p1, 10, true); + b.addFragment(p1, 30, false); + + Slab slab = b.finish(); + Assert.assertEquals(2, slab.peptideCount()); + + // bucket 10 should contain both peptides; bucket 20 only p0 + int[] bucket10 = slab.peptidesInBucket(10); + Assert.assertArrayEquals(new int[]{p0, p1}, bucket10); + + int[] bucket20 = slab.peptidesInBucket(20); + Assert.assertArrayEquals(new int[]{p0}, bucket20); + + // fingerprints should reflect the fragments we added + Fingerprint128 fp0 = slab.fingerprint(p0); + Assert.assertEquals(1, fp0.popcountB()); // 1 b-ion + Assert.assertEquals(1, fp0.popcountY()); // 1 y-ion + } + + @Test + public void fingerprintReturnsSnapshot_mutationDoesNotLeakIntoSlab() { + SlabBuilder b = new SlabBuilder(0, 500.0, 550.0); + int p0 = b.addPeptide(510.0); + b.addFragment(p0, 10, true); + Slab slab = b.finish(); + + Fingerprint128 fp = slab.fingerprint(p0); + fp.setYIonBucket(99); // must not affect slab + + Fingerprint128 fresh = slab.fingerprint(p0); + Assert.assertEquals("original popcountB preserved", 1, fresh.popcountB()); + Assert.assertEquals("slab unchanged by external mutation", 0, fresh.popcountY()); + } + + @Test(expected = IllegalStateException.class) + public void finishTwiceThrows() { + SlabBuilder b = new SlabBuilder(0, 500.0, 550.0); + b.addPeptide(510.0); + b.finish(); + b.finish(); + } + + @Test(expected = IllegalStateException.class) + public void addPeptideAfterFinishThrows() { + SlabBuilder b = new SlabBuilder(0, 500.0, 550.0); + b.finish(); + b.addPeptide(520.0); + } + + @Test(expected = IllegalStateException.class) + public void addFragmentAfterFinishThrows() { + SlabBuilder b = new SlabBuilder(0, 500.0, 550.0); + int p0 = b.addPeptide(510.0); + b.finish(); + b.addFragment(p0, 10, true); + } +} From 276fe08a87bc7e493834948a8322d022d051839f Mon Sep 17 00:00:00 2001 From: Yasset Perez-Riverol Date: Sat, 18 Apr 2026 08:25:28 +0100 Subject: [PATCH 6/6] feat(fragindex): FragmentIndexStore interface + in-memory DirectStore --- .../ucsd/msjava/fragindex/DirectStore.java | 30 +++++++++++++++++++ .../msjava/fragindex/FragmentIndexStore.java | 23 ++++++++++++++ .../msjava/fragindex/TestDirectStore.java | 28 +++++++++++++++++ 3 files changed, 81 insertions(+) create mode 100644 src/main/java/edu/ucsd/msjava/fragindex/DirectStore.java create mode 100644 src/main/java/edu/ucsd/msjava/fragindex/FragmentIndexStore.java create mode 100644 src/test/java/edu/ucsd/msjava/fragindex/TestDirectStore.java diff --git a/src/main/java/edu/ucsd/msjava/fragindex/DirectStore.java b/src/main/java/edu/ucsd/msjava/fragindex/DirectStore.java new file mode 100644 index 00000000..2924c5d0 --- /dev/null +++ b/src/main/java/edu/ucsd/msjava/fragindex/DirectStore.java @@ -0,0 +1,30 @@ +package edu.ucsd.msjava.fragindex; + +/** + * In-memory fragment-index store. Slabs live as {@link Slab} objects on the + * Java heap by default; Phase 4 will add an off-heap {@link java.nio.DirectByteBuffer} + * backing so the working set stays out of {@code -Xmx}. + * + *

For Phase 1 this is the only available store. It's used by unit tests + * and by in-memory search runs against small FASTA files. + */ +public final class DirectStore implements FragmentIndexStore { + private final Slab[] slabs; + + public DirectStore(int slabCount) { + this.slabs = new Slab[slabCount]; + } + + @Override + public int slabCount() { return slabs.length; } + + @Override + public void putSlab(int slabId, Slab slab) { + slabs[slabId] = slab; + } + + @Override + public Slab openSlab(int slabId) { + return slabs[slabId]; + } +} diff --git a/src/main/java/edu/ucsd/msjava/fragindex/FragmentIndexStore.java b/src/main/java/edu/ucsd/msjava/fragindex/FragmentIndexStore.java new file mode 100644 index 00000000..6ff6eaa7 --- /dev/null +++ b/src/main/java/edu/ucsd/msjava/fragindex/FragmentIndexStore.java @@ -0,0 +1,23 @@ +package edu.ucsd.msjava.fragindex; + +/** + * Storage backend for fragment-index slabs. Implementations differ in whether + * slabs live off-heap in memory ({@link DirectStore}) or on disk via mmap + * (to be added in Phase 4). + * + *

All methods must be thread-safe for concurrent readers after + * {@link #putSlab(int, Slab)} has been called for each slab during build. + */ +public interface FragmentIndexStore { + /** Total number of slabs this store holds (set at construction). */ + int slabCount(); + + /** Install a slab at the given id. Called once per slab during build. */ + void putSlab(int slabId, Slab slab); + + /** Return the slab at the given id, or null if none has been put yet. */ + Slab openSlab(int slabId); + + /** Optional hint that the caller has finished with the slab. Default: no-op. */ + default void closeSlab(Slab slab) {} +} diff --git a/src/test/java/edu/ucsd/msjava/fragindex/TestDirectStore.java b/src/test/java/edu/ucsd/msjava/fragindex/TestDirectStore.java new file mode 100644 index 00000000..74b3cfe6 --- /dev/null +++ b/src/test/java/edu/ucsd/msjava/fragindex/TestDirectStore.java @@ -0,0 +1,28 @@ +package edu.ucsd.msjava.fragindex; + +import org.junit.Assert; +import org.junit.Test; + +public class TestDirectStore { + + @Test + public void putAndOpenSlab() { + DirectStore store = new DirectStore(/*slabCount=*/2); + + SlabBuilder b0 = new SlabBuilder(0, 500.0, 550.0); + int pid = b0.addPeptide(510.0); + b0.addFragment(pid, 10, true); + store.putSlab(0, b0.finish()); + + Slab read = store.openSlab(0); + Assert.assertEquals(0, read.slabId()); + Assert.assertEquals(1, read.peptideCount()); + Assert.assertArrayEquals(new int[]{0}, read.peptidesInBucket(10)); + } + + @Test + public void openUnsetSlabReturnsNull() { + DirectStore store = new DirectStore(2); + Assert.assertNull(store.openSlab(1)); + } +}