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/quickLift.c src/hg/lib/quickLift.c
index c2769612503..5516bfe6e79 100644
--- src/hg/lib/quickLift.c
+++ src/hg/lib/quickLift.c
@@ -655,50 +655,101 @@
     lifted->strand[2] = 0;
     }
 
 lifted->qStart /= 3;
 lifted->qEnd /= 3;
 lifted->qSize /= 3;
 lifted->qBaseInsert /= 3;
 for (i = 0; i < lifted->blockCount; i++)
     {
     lifted->blockSizes[i] /= 3;
     lifted->qStarts[i] /= 3;
     }
 return TRUE;
 }
 
+static struct psl *pslWithoutEmptyBlocks(struct psl *psl)
+/* A copy of a protein alignment with its zero-length blocks taken out, or NULL if it has
+ * none.  The copy can be left with no blocks at all.  The UniProt bigPsl files store block sizes in bases, and a block shorter than a
+ * codon comes out of pslFromBigPsl with size 0.  Drawing does not mind, but pslTransMap
+ * checks the alignment after converting it to nucleotides and aborts on the empty block,
+ * which takes down every item in the track.  About one alignment in eight has one. */
+{
+int i, j;
+
+for (i = 0; i < psl->blockCount; i++)
+    if (psl->blockSizes[i] == 0)
+        break;
+if (i == psl->blockCount)
+    return NULL;
+
+struct psl *copy = pslClone(psl);
+for (i = 0, j = 0; i < copy->blockCount; i++)
+    {
+    if (copy->blockSizes[i] == 0)
+        continue;
+    copy->blockSizes[j] = copy->blockSizes[i];
+    copy->qStarts[j] = copy->qStarts[i];
+    copy->tStarts[j] = copy->tStarts[i];
+    j++;
+    }
+copy->blockCount = j;
+if (j == 0)
+    return copy;
+
+// Removing an end block moves the bounds, and pslIsProtein compares tEnd with the last
+// block, so put them back in step.  pslRecalcBounds does not know about protein units.
+int last = j - 1;
+int tStart = copy->tStarts[0];
+int tEnd = copy->tStarts[last] + 3 * copy->blockSizes[last];
+if (copy->strand[1] == '-')
+    reverseIntRange(&tStart, &tEnd, copy->tSize);
+copy->tStart = tStart;
+copy->tEnd = tEnd;
+copy->qStart = copy->qStarts[0];
+copy->qEnd = copy->qStarts[last] + copy->blockSizes[last];
+pslComputeInsertCounts(copy);
+return copy;
+}
+
 struct psl *quickLiftPsl(struct hash *chainHash, struct hash **pMapPsls, struct psl *psl)
 // Map the target side of an alignment from the other assembly onto our current reference.
 // The query side (the mRNA, EST or protein the alignment is to) is left alone.  Returns
 // NULL if the alignment doesn't map.  pMapPsls points at a hash of mapping alignments the
 // caller keeps across a run of items; point it at a NULL hash to start.
 {
 struct chain *chain = liftOverChainForRange(chainHash, psl->tName, psl->tStart, psl->tEnd);
 if (chain == NULL)
     return NULL;
 
 struct psl *mapPsl = mapPslForChain(pMapPsls, chain);
 
 // pslTransMap aborts when the two alignments disagree about the size of the sequence they
 // share.  That means the chain and the track were built against different versions of the
 // other assembly, so drop the item rather than taking the CGI down with it.
 if (psl->tSize != mapPsl->qSize)
     return NULL;
 
-struct psl *lifted = pslTransMap(pslTransMapNoOpts, psl, pslTypeUnspecified,
-                                 mapPsl, pslTypeUnspecified);
+struct psl *trimmed = pslIsProtein(psl) ? pslWithoutEmptyBlocks(psl) : NULL;
+if (trimmed && (trimmed->blockCount == 0))
+    {
+    pslFree(&trimmed);
+    return NULL;
+    }
+struct psl *lifted = pslTransMap(pslTransMapNoOpts, trimmed ? trimmed : psl,
+                                 pslTypeUnspecified, mapPsl, pslTypeUnspecified);
+pslFree(&trimmed);
 if (lifted != NULL)
     {
     // before counting, so quickLiftPslCounts sees both sides in the same units
     if (pslIsProtein(psl))
         quickLiftPslBackToProtein(lifted);
     quickLiftPslCounts(psl, lifted);
     }
 return lifted;
 }
 
 static struct chain *chainFromPsl(struct psl *psl)
 /* The inverse of chainToPsl.  Score and id are the caller's to fill in, since an alignment
  * does not carry them. */
 {
 struct chain *chain;