896eb991fff50f44f18811a05171e36fa111163d braney Thu Sep 24 17:13:20 2026 -0700 quickLift: leave empty blocks out of a protein alignment before lifting it, refs #38249 The UniProt bigPsl files store block sizes in bases, and pslFromBigPsl divides them by three, so a block shorter than a codon loads with size 0. pslTransMap rejects such an alignment and aborts, which replaced the whole lifted SwissProt track with a BUG message. About one alignment in eight has a block like this. quickLiftPsl now lifts a copy with the empty blocks removed. quickLiftTester has a new case in the shape of Q96ME1-2. diff --git src/hg/lib/tests/quickLiftTester.c src/hg/lib/tests/quickLiftTester.c index e29112e585e..a38e4f39598 100644 --- src/hg/lib/tests/quickLiftTester.c +++ src/hg/lib/tests/quickLiftTester.c @@ -2,31 +2,31 @@ * * pslTransMap does the mapping in nucleotide space and leaves the result there, so * quickLiftPsl has to put the query side back into protein units afterwards. That fixup * has to agree with pslIsProtein(), because every reader downstream asks that question and * then multiplies block sizes by three or by one on the answer. A psl that says "++" while * its blocks sit in minus-strand target coordinates passes no check and draws at a third of * its length in the wrong place, which is what #38349 found. * * So each case here runs pslCheck2() over the lifted alignment as well as printing it. * PSL_CHECK_IGNORE_INSERT_CNTS is on because pslCheck2's own comment says protein psls do * not compute the insert counts consistently; everything else is checked, including the * target range, which is where a wrong strand shows up. * * No database and no files: the chains are built here in the shape quickLiftSourceRanges * leaves in the hash, which is the reference on the query side after a chainSwap. - * refs #38349 */ + * refs #38349, #38249 */ /* Copyright (C) 2026 The Regents of the University of California * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */ #include "common.h" #include "hash.h" #include "chain.h" #include "psl.h" #include "liftOver.h" /* for struct sqlConnection, struct cart and struct trackDb, named in prototypes in * quickLift.h, which does not declare them itself */ #include "jksql.h" #include "cart.h" #include "trackDb.h" #include "quickLift.h" @@ -187,37 +187,61 @@ pslOnSource("prot", 100, 100, TRUE)); } static void mrnaBothStrands() /* The same two chains over an mRNA alignment, which the protein fixup must not touch. */ { int refStarts[] = {100}, srcStarts[] = {100}, sizes[] = {300}; runCase("mRNA, chain on the same strand", chainFromBlocks('+', 1, refStarts, srcStarts, sizes), pslOnSource("mrna", 100, 100, FALSE)); runCase("mRNA, chain on the opposite strand", chainFromBlocks('-', 1, refStarts, srcStarts, sizes), pslOnSource("mrna", 100, 600, FALSE)); } +static void emptyBlock() +/* #38249. The UniProt bigPsl files store block sizes in bases and pslFromBigPsl divides + * them by three, so a block shorter than a codon loads with size 0. pslTransMap rejects + * such an alignment and aborts, which took down the whole lifted SwissProt track. The lift + * has to leave the empty block out and map the rest. */ +{ +int refStarts[] = {100}, srcStarts[] = {100}, sizes[] = {600}; +// The shape of Q96ME1-2 in hg19, whose first block is a single base: query on the minus +// strand, so the empty block sits at the far end of the query, where pslCheck notices it. +struct psl *psl = pslNew("prot", 100, 0, 100, srcChrom, seqSize, 100, 400, "-+", 2, 0); + +psl->blockCount = 2; +psl->blockSizes[0] = 0; +psl->qStarts[0] = 0; +psl->tStarts[0] = 100; +psl->blockSizes[1] = 90; +psl->qStarts[1] = 10; +psl->tStarts[1] = 130; +psl->match = 90; +runCase("protein, a block shorter than a codon", + chainFromBlocks('+', 1, refStarts, srcStarts, sizes), psl); +} + static void noChain() /* Nothing covers the alignment, so it is dropped rather than half-lifted. */ { int refStarts[] = {100}, srcStarts[] = {100}, sizes[] = {300}; runCase("no chain over the alignment", chainFromBlocks('+', 1, refStarts, srcStarts, sizes), pslOnSource("prot", 100, 700, TRUE)); } int main(int argc, char *argv[]) { if (argc != 1) usage(); sameStrandProtein(); oppositeStrandProtein(); targetGap(); splitCodon(); mrnaBothStrands(); +emptyBlock(); noChain(); printf("passed\n"); return 0; }