Repository navigation
feat(ENGKNOW-4117): native LIFTOVER_ANNOTATE command and every LIFTOVER mode on the same engine - #159
Conversation
… tests Adds UTestGorLiftoverCharacterization, pinning the exact output of today's LIFTOVER (-snp/-var/-seg/-bam, -all, -ref, -build, overlaps, empty REF, zero strand chains, nested chain, option errors, column collisions). Expectations that the native engine changes are marked // P4-CHANGES and kept in *_LEGACY constants. Probed literals: zeroLengthSeg, zeroStrandChainAll and multiAllelicRevcompl matched the brief unchanged. allScoreAboveInt had no literal in the brief and takes the observed row: chr12 90834 A G a1 chr12 200000 + mapped 1.234103462E10 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ading Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s and multi-contig nested chains Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…onomy Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…aining block A span contained in best block i now reports endpoints through block i, targetSize = sourceSize and sizeRatio 1.0 also when i's target is outside -mapped (unsupported_contig), giving one rule for contained spans. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d diag; count failon rows in stats Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e the annotate step clone safely Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…verArgs, link retargeting and non-empty guards Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he signatures Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… prefix default Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ation wording and examples Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ples from the docs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…iant list Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ntial test Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tial categories and classify unit tests Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ase as D8 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he oracle proof and widen the random inputs Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ate bam pair categories on the new model Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…gine LIFTOVER -snp, -var, -seg, -bam, -all and nested-query chains now run on LiftEngine through LiftoverRelocateAnalysis followed by sort genome; the query-string expansion is deleted. Behaviour changes are the approved N9 list, each a P4-CHANGES test or a differential category D1-D9. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… position An MPOS that is not an integer > 0 is kept unchanged on every row (R18); an integer > 0 keeps the N15 lift, and MRNM '*' still gives 0. The differential's new-model MPOS follows the same rule. Adds tests for the MPOS and MRNM edges, liftMate, empty chains (R17) and parse errors. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…re paths and clone handling Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ER status projection Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mate lift and the norrows outcome Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rder and minus-strand indels Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rows, split nested contigs and repeated options - LIFTOVER_ANNOTATE and LIFTOVER reject an empty or blank coordinate cell instead of reading it as 0, and name chrom:pos - chain loader rejects tStart < 0, qStart < 0, empty qChrom and empty tEnd/qStart/qEnd - a nested chain rejects a contig that reappears after another contig - LIFTOVER_ANNOTATE rejects a value option given more than once Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…n docs RowObj.colInt reads a lone '-' as 0, so a -seg start of '-' lifted [0,end) and a chain qStart of '-' became a valid block at 0. Input coordinates and chain tEnd/qStart/qEnd now reject it. LIFTOVER_ANNOTATE.rst no longer claims a reappearing file-chain contig uses its first run, or that a negative tStart gets the Liftover chain prefix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
| /** Writes a GOR file; rows are sorted by (chrom, pos) on columns 0 and 1. */ | ||
| private static String gorFile(String name, String header, List<String[]> rows) throws IOException { | ||
| List<String[]> sorted = new ArrayList<>(rows); | ||
| sorted.sort(Comparator.<String[], String>comparing(r -> r[0]).thenComparingInt(r -> Integer.parseInt(r[1]))); |
There was a problem hiding this comment.
No change here: this is test code, and the value is a position the test itself generated (fixture rows or RandomChains input), so it is always an integer. If it ever were not, the uncaught NumberFormatException would fail the test with a stack trace pointing at this line, which is the behaviour we want; catching it would only hide a broken fixture.
| /** NaiveLift's containing blocks for an input row under LIFTOVER's mode rule (N12, N14, N15). */ | ||
| private static List<NaiveLift.Hit> naiveHits(String[] in, String mode, List<ChainBlock> blocks) { | ||
| NaiveLift nl = new NaiveLift(blocks); | ||
| int p = Integer.parseInt(in[1]); |
There was a problem hiding this comment.
No change here: this is test code, and the value is a position the test itself generated (fixture rows or RandomChains input), so it is always an integer. If it ever were not, the uncaught NumberFormatException would fail the test with a stack trace pointing at this line, which is the behaviour we want; catching it would only hide a broken fixture.
| case "var": | ||
| return in[2].isEmpty() ? nl.junctionHits(p - 1) : nl.intervalHits(p, p + in[2].length() - 1); | ||
| case "seg": { | ||
| int end = Integer.parseInt(in[2]); |
There was a problem hiding this comment.
No change here: this is test code, and the value is a position the test itself generated (fixture rows or RandomChains input), so it is always an integer. If it ever were not, the uncaught NumberFormatException would fail the test with a stack trace pointing at this line, which is the behaviour we want; catching it would only hide a broken fixture.
| return end == p ? nl.junctionHits(p) : nl.intervalHits(p + 1, end); | ||
| } | ||
| default: { | ||
| int end = Integer.parseInt(in[2]); |
There was a problem hiding this comment.
No change here: this is test code, and the value is a position the test itself generated (fixture rows or RandomChains input), so it is always an integer. If it ever were not, the uncaught NumberFormatException would fail the test with a stack trace pointing at this line, which is the behaviour we want; catching it would only hide a broken fixture.
| Map<String, int[]> range = new LinkedHashMap<>(); | ||
| for (String[] r : bam.table.rows) { | ||
| int[] mm = range.computeIfAbsent(r[bam.chrom], k -> new int[]{Integer.MAX_VALUE, 0}); | ||
| mm[0] = Math.min(mm[0], Integer.parseInt(r[bam.pos]) - 1); |
There was a problem hiding this comment.
No change here: this is test code, and the value is a position the test itself generated (fixture rows or RandomChains input), so it is always an integer. If it ever were not, the uncaught NumberFormatException would fail the test with a stack trace pointing at this line, which is the behaviour we want; catching it would only hide a broken fixture.
| for (String[] r : bam.table.rows) { | ||
| int[] mm = range.computeIfAbsent(r[bam.chrom], k -> new int[]{Integer.MAX_VALUE, 0}); | ||
| mm[0] = Math.min(mm[0], Integer.parseInt(r[bam.pos]) - 1); | ||
| mm[1] = Math.max(mm[1], Integer.parseInt(r[bam.end])); |
There was a problem hiding this comment.
No change here: this is test code, and the value is a position the test itself generated (fixture rows or RandomChains input), so it is always an integer. If it ever were not, the uncaught NumberFormatException would fail the test with a stack trace pointing at this line, which is the behaviour we want; catching it would only hide a broken fixture.
| List<String[]> matePairs) { | ||
| List<String> expected = new ArrayList<>(); | ||
| for (String[] r : bam.table.rows) { | ||
| int s0 = Integer.parseInt(r[bam.pos]) - 1; |
There was a problem hiding this comment.
No change here: this is test code, and the value is a position the test itself generated (fixture rows or RandomChains input), so it is always an integer. If it ever were not, the uncaught NumberFormatException would fail the test with a stack trace pointing at this line, which is the behaviour we want; catching it would only hide a broken fixture.
| List<String> expected = new ArrayList<>(); | ||
| for (String[] r : bam.table.rows) { | ||
| int s0 = Integer.parseInt(r[bam.pos]) - 1; | ||
| int e0 = Integer.parseInt(r[bam.end]); |
There was a problem hiding this comment.
No change here: this is test code, and the value is a position the test itself generated (fixture rows or RandomChains input), so it is always an integer. If it ever were not, the uncaught NumberFormatException would fail the test with a stack trace pointing at this line, which is the behaviour we want; catching it would only hide a broken fixture.
| Map<String, Integer> expectedPerKey = new HashMap<>(); | ||
| for (String[] r : bam.table.rows) { | ||
| int n = Math.max(1, containing(chain, r[bam.chrom], Integer.parseInt(r[bam.pos]) - 1, | ||
| Integer.parseInt(r[bam.end])).size()); |
There was a problem hiding this comment.
No change here: this is test code, and the value is a position the test itself generated (fixture rows or RandomChains input), so it is always an integer. If it ever were not, the uncaught NumberFormatException would fail the test with a stack trace pointing at this line, which is the behaviour we want; catching it would only hide a broken fixture.
| } | ||
| Map<String, Integer> expectedPerKey = new HashMap<>(); | ||
| for (String[] r : bam.table.rows) { | ||
| int n = Math.max(1, containing(chain, r[bam.chrom], Integer.parseInt(r[bam.pos]) - 1, |
There was a problem hiding this comment.
No change here: this is test code, and the value is a position the test itself generated (fixture rows or RandomChains input), so it is always an integer. If it ever were not, the uncaught NumberFormatException would fail the test with a stack trace pointing at this line, which is the behaviour we want; catching it would only hide a broken fixture.
… LIFTOVER_LEGACY Liftover.scala becomes Liftover.java with the same arguments, output and errors. The pre-ENGKNOW-4117 implementation is restored unchanged as LiftoverLegacy.scala, registered as LIFTOVER_LEGACY, so results can be compared with the new LIFTOVER. It is temporary and will be removed. LIFTOVER_LEGACY is listed with the commands that contain input sources, so its chain is in the used files and cache signatures. Tests check that it reproduces the frozen pre-change output and, in every differential run, that the test oracle equals it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adds LIFTOVER tests for a bad -ref/-alt outside -var, -alt by name on a column other than #4, a case-insensitive output-name collision and a -bam input without SEQ. The LegacyLiftoverQuery javadoc now says that LIFTOVER_LEGACY holds the same expansion for now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Summary
ENGKNOW-4117: safer and faster liftover annotation, with a single liftover engine.
LIFTOVER_ANNOTATEpipe command: streams exactly one output row per input row and adds liftover columns (liftover_chrom,liftover_posorliftover_start/liftover_stop,liftover_strand,liftover_status, plus allele, endpoint and size diagnostics per mode). Modes-snp,-var,-seg,-seg1. Statuses includemapped,ambiguous,unmapped,inverted,size_changed,mapped_across_gap,split_contig,start_unmapped/stop_unmapped,unsupported_contigandpassthrough. SeeLIFTOVER_ANNOTATE.rst.LIFTOVERis rebuilt on the same engine for every mode (-snp,-var,-seg,-bam,-all, nested<(gor …)chains). The old query-string expansion is deleted, so there is one lift formula and one status rule.gorsat.liftover: per-contig in-memory chain index (primitive arrays, prefix-max, binary search),LiftEngine, chain loading/validation and the two analyses. TheLIFTOVERcommand itself is now Java (Liftover.java).LIFTOVER_LEGACY(temporary): the pre-changeLIFTOVER, restored unchanged asLiftoverLegacy.scala, so results can be compared with the newLIFTOVER. Same arguments. To be removed in a later release.path|uniqueId|policy); nested<(gor …)chains are not cached;<(nor …)chains are rejected.Behaviour changes in
LIFTOVERAll listed under
versionchanged:: 5.17.0inLIFTOVER.rst:ambiguousrow instead of duplicates.-varreverse-complements each allele separately.-bamis 1-based inclusive:-strand reads move by one base, MPOS is lifted (0 when the mate lands elsewhere or MRNM is*), a 1-base read on a block's last base maps, and an MPOS that is not a positive integer passes through.ambiguous(overlapping chains only).'0'-strand, empty or malformed chains fail.end < startis an error; a zero-length interval is a junction. Empty or-coordinate cells and repeated options are errors.Testing
./gradlew test :gortools:testScala): 4,815 JUnit tests, 96 skipped, and 683 ScalaTest tests. All pass except 3 inUTestOCIObjectStorageSourceType(no OCI credentials locally); this branch does not changedrivers.LIFTOVERoutput were written first.LIFTOVER_LEGACYreproduces all 18 frozen pre-change outputs and the 3 frozen errors, and in every differential run the test oracle's output equalsLIFTOVER_LEGACY's.gorsat.liftoverfiles andLiftoverAnnotate.java(measured before theLiftover.javaport; its branches are covered by the LIFTOVER characterization and parsing tests).-all984 vs 2,821 ms, BAM-all131 vs 454 ms.ref-gregor/managed/liftover) scanned: sorted, no overlaps, ties or bad strands.Follow-ups
-bamand-allchanges before release.🤖 Generated with Claude Code