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 @@ -1,1244 +1,1295 @@ /* Copyright (C) 2023 The Regents of the University of California * See kent/LICENSE or http://genome.ucsc.edu/license/ for licensing information. */ #include "common.h" #include "obscure.h" #include "limits.h" #include "float.h" #include "asParse.h" #include "chain.h" #include "binRange.h" #include "basicBed.h" #include "liftOver.h" #include "hash.h" #include "bigBed.h" #include "bbiFile.h" #include "chainNetDbLoad.h" #include "hdb.h" #include "jksql.h" #include "hgConfig.h" #include "quickLift.h" #include "genePredReader.h" #include "bigChain.h" #include "bigLink.h" #include "chromAlias.h" #include "customTrack.h" #include "encode/encodePeak.h" #include "psl.h" #include "chainToPsl.h" #include "pslTransMap.h" #include "maf.h" struct bigBedInterval *quickLiftGetIntervals(char *quickLiftFile, struct bbiFile *bbi, char *chrom, int start, int end, struct hash **pChainHash) /* Return intervals from "other" species that will map to the current window. * These intervals are NOT YET MAPPED to the current assembly. */ { char *linkFileName = bigChainGetLinkFile(quickLiftFile); int maxGapBefore = 0; int maxGapAfter = 0; struct chain *chain, *chainList = chainLoadIdRangeHub(NULL, quickLiftFile, linkFileName, chrom, start, end, -1); struct lm *lm = lmInit(0); struct bigBedInterval *bbList = NULL, *bb; for(chain = chainList; chain; chain = chain->next) { struct cBlock *cb; cb = chain->blockList; if (cb == NULL) continue; int qStart = cb->qStart; int qEnd = cb->qEnd; // get the range for the links on the "other" species for(; cb; cb = cb->next) { if (cb->qStart < qStart) qStart = cb->qStart; if (cb->qEnd > qEnd) qEnd = cb->qEnd; } // now grab the items , probably we should parameterize the max number of items, but to what? struct bigBedInterval *thisInterval = NULL; if (chain->qStrand == '-') thisInterval = bigBedIntervalQuery(bbi, chain->qName, chain->qSize - qEnd, chain->qSize - qStart, 1000000, lm); else thisInterval = bigBedIntervalQuery(bbi, chain->qName, qStart, qEnd, 1000000, lm); // find how much of the items are beyond the viewport for(bb=thisInterval; bb; bb = bb->next) { if (bb->start < qStart) { int gap = qStart - bb->start; if (gap > maxGapBefore) maxGapBefore = gap; } if (bb->end > qEnd) { int gap = bb->end - qEnd; if (gap > maxGapAfter) maxGapAfter = gap; } } bbList = slCat(thisInterval, bbList); } // We are done with the chains we used to bound the data query; // release them before loading the wider set for the lift map below. // Without this, every quickLifted track leaked the cBlocks of every // chain overlapping the window. chainFreeList(&chainList); // now we need to grab the links outside of our viewport so we can map long items // probably we could reuse the chains from above but for the moment this is easier // For the moment we use the same padding on both sides so we don't have to worry about strand if (maxGapBefore > maxGapAfter) maxGapAfter = maxGapBefore; else maxGapBefore = maxGapAfter; // Cap the padding so a single oversized item doesn't drag in chains // (and their dense cBlock lists) covering many megabases. #define QUICKLIFT_MAX_GAP_PAD 1000000 if (maxGapBefore > QUICKLIFT_MAX_GAP_PAD) maxGapBefore = maxGapAfter = QUICKLIFT_MAX_GAP_PAD; int newStart = start - maxGapBefore * 2; if (newStart < 0) newStart = 0; int newEnd = end + maxGapAfter * 2; chainList = chainLoadIdRangeHub(NULL, quickLiftFile, linkFileName, chrom, newStart, newEnd, -1); for(chain = chainList; chain; chain = chain->next) { chainSwap(chain); if (*pChainHash == NULL) *pChainHash = newHash(0); liftOverAddChainHash(*pChainHash, chain); } return bbList; } static void make12(struct bed *bed) /* Make a bed12 out of something less than that. */ { bed->blockCount = 1; bed->blockSizes = needMem(sizeof(int)); bed->blockSizes[0] = bed->chromEnd - bed->chromStart; bed->chromStarts = needMem(sizeof(int)); bed->chromStarts[0] = 0; } static int snapToBlock(struct chain *chain, int pos, boolean forward) /* remapRangeList will only place a coordinate that falls inside an aligned block, so a * clipped end has to land on real alignment, not merely inside the chain. Return pos if * it is already aligned, otherwise the nearest aligned coordinate looking forward (for a * start) or backward (for an end). Return -1 if there is no such coordinate. * The asymmetry matches remapRangeList: a start needs b->tStart <= start < b->tEnd, so * b->tStart is legal; an end needs b->tStart < end <= b->tEnd, so b->tEnd is legal. */ { struct cBlock *b, *prev = NULL; for (b = chain->blockList; b != NULL; b = b->next) { if ((b->tStart <= pos) && (pos < b->tEnd)) return pos; if (forward && (b->tStart > pos)) return b->tStart; if (b->tEnd <= pos) prev = b; } return (!forward && (prev != NULL)) ? prev->tEnd : -1; } static boolean clipBedToChains(struct hash *chainHash, struct bed *bed) /* An item can be far bigger than the region we loaded chains for -- ClinVar has copy * number variants spanning most of a chromosome. Its ends then sit where no chain * reaches, remapRangeList can place neither of them, and the whole item is dropped even * though the part on screen maps perfectly well. Pull the ends in to the nearest aligned * base so the visible part can lift. Return TRUE if the item was clipped. */ { struct chain *chain = liftOverChainForRange(chainHash, bed->chrom, bed->chromStart, bed->chromEnd); if (chain == NULL) return FALSE; // nothing covers it, let it fail the way it used to int newStart = snapToBlock(chain, bed->chromStart, TRUE); int newEnd = snapToBlock(chain, bed->chromEnd, FALSE); if ((newStart < 0) || (newEnd < 0) || (newStart >= newEnd)) return FALSE; if ((newStart == bed->chromStart) && (newEnd == bed->chromEnd)) return FALSE; // both ends already sit on alignment, nothing to do /* Trim the blocks to the new range. Blocks are in ascending order, so walk them and * keep the part that survives; a block entirely outside the range is dropped. */ if (bed->blockCount > 0) { int i, keep = 0; for (i = 0; i < bed->blockCount; ++i) { int bStart = bed->chromStart + bed->chromStarts[i]; int bEnd = bStart + bed->blockSizes[i]; if (bStart < newStart) bStart = newStart; if (bEnd > newEnd) bEnd = newEnd; if (bStart >= bEnd) continue; bed->chromStarts[keep] = bStart - newStart; bed->blockSizes[keep] = bEnd - bStart; keep++; } if (keep == 0) return FALSE; bed->blockCount = keep; } bed->chromStart = newStart; bed->chromEnd = newEnd; if (bed->thickStart < newStart) bed->thickStart = newStart; if (bed->thickEnd > newEnd) bed->thickEnd = newEnd; if (bed->thickStart > bed->thickEnd) bed->thickStart = bed->thickEnd; return TRUE; } static struct bed *quickLiftBed(struct bbiFile *bbi, struct hash *chainHash, struct bigBedInterval *bb, boolean clip); struct bed *quickLiftIntervalsToBed(struct bbiFile *bbi, struct hash *chainHash, struct bigBedInterval *bb) /* Using chains stored in chainHash, port a bigBedInterval from another assembly to a bed * on the reference. */ { return quickLiftBed(bbi, chainHash, bb, FALSE); } struct bed *quickLiftIntervalsToBedClip(struct bbiFile *bbi, struct hash *chainHash, struct bigBedInterval *bb) /* Like quickLiftIntervalsToBed, but an item too big for the chains we loaded is pulled in * to what they cover rather than dropped. Callers that need the item's true extent (the * details page) should use quickLiftIntervalsToBed instead. */ { // quickLiftClipToChains=off restores the old behavior, where an item whose ends fall // outside the chains we loaded is dropped instead of being pulled in. boolean clip = cfgOptionBooleanDefault("quickLiftClipToChains", TRUE); return quickLiftBed(bbi, chainHash, bb, clip); } static struct bed *quickLiftBed(struct bbiFile *bbi, struct hash *chainHash, struct bigBedInterval *bb, boolean clip) /* Port a bigBedInterval to a bed on the reference. If clip, an item too big for the * chains we loaded is pulled in to what they cover rather than dropped. */ { char startBuf[16], endBuf[16]; char *bedRow[bbi->fieldCount]; char chromName[256]; static int lastChromId = -1; bbiCachedChromLookup(bbi, bb->chromId, lastChromId, chromName, sizeof(chromName)); bigBedIntervalToRow(bb, chromName, startBuf, endBuf, bedRow, ArraySize(bedRow)); struct bed *bed = bedLoadN(bedRow, bbi->definedFieldCount); char *error; if (bbi->definedFieldCount < 12) make12(bed); if (clip) clipBedToChains(chainHash, bed); if ((error = remapBlockedBed(chainHash, bed, 0.0, 0.1, TRUE, TRUE, NULL, NULL)) == NULL) return bed; //else //printf("bed %s error:%s
", bed->name, error); return NULL; } char *quickLiftGetChainPath(struct cart *cart, char *fromDb, char *toDb) /* Return the path from the quickLiftChain table for given assemblies. */ { if (!quickLiftEnabled(cart)) return 0; struct sqlConnection *conn = hConnectCentral(); char query[2048]; sqlSafef(query, sizeof(query), "select q.path from %s q where q.fromDb='%s' and q.toDb='%s'", quickLiftChainTable(), fromDb, toDb); char *path = sqlQuickString(conn, query); hDisconnectCentral(&conn); return path; } unsigned quickLiftGetChainId(struct cart *cart, char *fromDb, char *toDb) /* Return the id from the quickLiftChain table for given assemblies. */ { if (!quickLiftEnabled(cart)) return 0; unsigned ret = 0; struct sqlConnection *conn = hConnectCentral(); char query[2048]; // this needs to use the hg.conf setting sqlSafef(query, sizeof(query), "select q.id from quickLiftChain q where q.fromDb='%s' and q.toDb='%s'", fromDb, toDb); char *geneId = sqlQuickString(conn, query); hDisconnectCentral(&conn); if (geneId) ret = atoi(geneId); return ret; } #define QUICKLIFT_RANGE_PAD 100000 static struct chain *quickLiftLoadChains(char *quickLiftFile, char *chrom, int start, int end) /* Load the chains from quickLiftFile that overlap a padded window around the * destination range. */ { // A track can name the assembly it came from without naming a chain file, since nothing // stops a hub from setting one of the pair and not the other. With no chains there is // nothing to lift, and every caller copes with an empty answer. if (quickLiftFile == NULL) return NULL; // need to add some padding to these coordinates int padStart = start - QUICKLIFT_RANGE_PAD; if (padStart < 0) padStart = 0; char *linkFileName = bigChainGetLinkFile(quickLiftFile); return chainLoadIdRangeHub(NULL, quickLiftFile, linkFileName, chrom, padStart, end + QUICKLIFT_RANGE_PAD, -1); } static void quickLiftChainQueryRange(struct chain *chain, int *retQStart, int *retQEnd) /* Return the query-side ("other" species) coordinate range spanned by the * aligned blocks of chain, corrected for query strand. chain->blockList must * not be NULL. */ { struct cBlock *cb = chain->blockList; int qStart = cb->qStart; int qEnd = cb->qEnd; for(; cb; cb = cb->next) { if (cb->qStart < qStart) qStart = cb->qStart; if (cb->qEnd > qEnd) qEnd = cb->qEnd; } // correct for strand if (chain->qStrand == '-') { int saveStart = qStart; qStart = chain->qSize - qEnd; qEnd = chain->qSize - saveStart; } *retQStart = qStart; *retQEnd = qEnd; } static boolean quickLiftChainRangeIn(struct chain *chain, int tStart, int tEnd, int *retQStart, int *retQEnd) /* The query side range matching tStart..tEnd on the target, rather than the whole extent * of the chain's blocks. One block can be enormous: hg19 and hg38 run identical for * 12.8Mb on chr7, so the whole-block answer would ask the other assembly for millions of * bases either side of the window. Within a block the two sides are colinear, so the * part that matters can be worked out exactly. Returns FALSE if no block overlaps. */ { struct cBlock *cb; int qStart = 0, qEnd = 0; boolean any = FALSE; for (cb = chain->blockList; cb != NULL; cb = cb->next) { int s = max(cb->tStart, tStart); int e = min(cb->tEnd, tEnd); if (s >= e) continue; int qLo = cb->qStart + (s - cb->tStart); int qHi = cb->qStart + (e - cb->tStart); if (!any || (qLo < qStart)) qStart = qLo; if (!any || (qHi > qEnd)) qEnd = qHi; any = TRUE; } if (!any) return FALSE; // correct for strand if (chain->qStrand == '-') { int saveStart = qStart; qStart = chain->qSize - qEnd; qEnd = chain->qSize - saveStart; } *retQStart = qStart; *retQEnd = qEnd; return TRUE; } struct quickLiftRange *quickLiftSourceRanges(char *quickLiftFile, char *chrom, int start, int end, struct hash *chainHash) // The ranges in the other assembly that map into chrom:start-end on the reference. The // chains that do the mapping are added to chainHash, which is the form the lift functions // read. Use this when the items cannot be had from a query quickLiftSql knows how to make. { struct chain *chain, *chainList = quickLiftLoadChains(quickLiftFile, chrom, start, end); struct quickLiftRange *rangeList = NULL; for(chain = chainList; chain; chain = chain->next) { if (chain->blockList == NULL) continue; // pad the window the same way quickLiftLoadChains does, so an item that reaches into // the window from just outside it is still found int qStart, qEnd; int padStart = start - QUICKLIFT_RANGE_PAD; if (padStart < 0) padStart = 0; if (quickLiftChainRangeIn(chain, padStart, end + QUICKLIFT_RANGE_PAD, &qStart, &qEnd)) { struct quickLiftRange *range; AllocVar(range); range->chrom = cloneString(chain->qName); range->start = qStart; range->end = qEnd; slAddHead(&rangeList, range); } // the query range was read off the chain as it came, so swap only afterwards chainSwap(chain); liftOverAddChainHash(chainHash, chain); } slReverse(&rangeList); return rangeList; } struct hash *quickLiftChainHash(char *quickLiftFile, char *chrom, int start, int end) // Load the quickLift chains covering chrom:start-end on the reference and return them in a // hash keyed on the other assembly's sequence names, which is the shape the lift functions // want. Use this when the items were fetched some other way, so quickLiftSql was not the // thing that collected the chains. { struct hash *chainHash = newHash(8); quickLiftSourceRanges(quickLiftFile, chrom, start, end, chainHash); return chainHash; } struct slList *quickLiftSql(struct sqlConnection *conn, char *quickLiftFile, char *table, char *chrom, int start, int end, char *query, char *extraWhere, ItemLoader2 loader, int numFields,struct hash *chainHash) // retrieve items for which we have a loader from a SQL database for which we have a set quickLift chains. // Save the chains we used to map the item back to the current reference. { struct chain *chain, *chainList = quickLiftLoadChains(quickLiftFile, chrom, start, end); struct slList *item, *itemList = NULL; int rowOffset = 0; struct sqlResult *sr = NULL; char **row = NULL; for(chain = chainList; chain; chain = chain->next) { if (chain->blockList == NULL) continue; int qStart, qEnd; quickLiftChainQueryRange(chain, &qStart, &qEnd); // now grab the items if (query == NULL) sr = hRangeQuery(conn, table, chain->qName, qStart, qEnd, extraWhere, &rowOffset); else sr = sqlGetResult(conn, query); // numFields is what the loader will read, so it is also the least the row can have. // The native loaders check this; without it a table of the wrong type walks off the // end of the row. if ((numFields > 0) && (sqlCountColumns(sr) < numFields + rowOffset)) errAbort("table %s in %s has %d columns, need at least %d", table, sqlGetDatabase(conn), sqlCountColumns(sr), numFields + rowOffset); while ((row = sqlNextRow(sr)) != NULL) { item = loader(row + rowOffset, numFields); slAddHead(&itemList, item); } // now squirrel the swapped chains we used to use to make the retrieved items back to us chainSwap(chain); liftOverAddChainHash(chainHash, chain); } return itemList; } struct genePred *quickLiftGenePreds(struct sqlConnection *conn, char *quickLiftFile, char *table, char *chrom, int start, int end, char *extraWhere, struct hash *chainHash) // Like quickLiftSql, but load genePreds with a genePredReader so the actual set // of (extended) genePred columns in the table is honored. A fixed 15-column // loader misreads classic knownGene-style tables, whose trailing proteinID and // alignID columns are not extended genePred fields. { struct chain *chain, *chainList = quickLiftLoadChains(quickLiftFile, chrom, start, end); struct genePred *gpList = NULL; for(chain = chainList; chain; chain = chain->next) { if (chain->blockList == NULL) continue; int qStart, qEnd; quickLiftChainQueryRange(chain, &qStart, &qEnd); struct genePredReader *gpr = genePredReaderRangeQuery(conn, table, chain->qName, qStart, qEnd, extraWhere); struct genePred *gp; while ((gp = genePredReaderNext(gpr)) != NULL) slAddHead(&gpList, gp); genePredReaderFree(&gpr); // now squirrel the swapped chains we used to use to map the retrieved items back to us chainSwap(chain); liftOverAddChainHash(chainHash, chain); } return gpList; } struct bed *quickLiftBeds(struct bed *bedList, struct hash *chainHash, boolean blocked) // Map a list of bedd in query coordinates to our current reference { struct bed *liftedBedList = NULL; struct bed *nextBed; struct bed *bed; for(bed = bedList; bed; bed = nextBed) { // remapBlockedBed may want to add new beds after this bed if the region maps to more than one location nextBed = bed->next; bed->next = NULL; char *error; if (!blocked) { error = liftOverRemapRange(chainHash, 0.0, bed->chrom, bed->chromStart, bed->chromEnd, bed->strand[0], 0.001, &bed->chrom, (int *)&bed->chromStart, (int *)&bed->chromEnd, &bed->strand[0]); // probably this should keep track of cases where the input does NOT have thickStart == chromStart bed->thickStart = bed->chromStart; bed->thickEnd = bed->chromEnd; } else error = remapBlockedBed(chainHash, bed, 0.0, 0.1, TRUE, TRUE, NULL, NULL); if (error == NULL) { slAddHead(&liftedBedList, bed); } } return liftedBedList; } static long pslAlignedBases(struct psl *psl) /* Total size of the alignment's blocks, in whatever units the blocks are in. */ { long total = 0; int i; for (i = 0; i < psl->blockCount; i++) total += psl->blockSizes[i]; return total; } static void quickLiftPslCounts(struct psl *psl, struct psl *lifted) /* Put the original match, mismatch, repeat and N counts back on a lifted alignment, * scaled by how much of it survived the lift. pslTransMap recounts them off the blocks, * which reads every lifted alignment as a perfect match: the details page then claims * 100% identity and the browser draws every item at full shade. */ { // A protein alignment comes back from the lift in nucleotide space, so its block sizes, // and therefore its counts, are in different units than the ones we started with. double protMul = (pslIsProtein(psl) && !pslIsProtein(lifted)) ? 3.0 : 1.0; long origBases = pslAlignedBases(psl); long newBases = pslAlignedBases(lifted); if ((origBases <= 0) || (newBases <= 0)) return; double survived = newBases / (origBases * protMul); if (survived > 1.0) survived = 1.0; lifted->match = round(psl->match * protMul * survived); lifted->misMatch = round(psl->misMatch * protMul * survived); lifted->repMatch = round(psl->repMatch * protMul * survived); lifted->nCount = round(psl->nCount * protMul * survived); } static struct psl *mapPslForChain(struct hash **pMapPsls, struct chain *chain) /* The mapping alignment for one chain, made once and kept. The chains in a quickLift * chainHash have the other assembly on the target side, which is what remapBlockedBed * wants. pslTransMap wants it the other way round: query on the other assembly, target * on the reference. * Building this per item costs nothing at gene zoom, where a chain covers a handful of * blocks, and a great deal zoomed out, where the chain covering the window carries * thousands of blocks and an alignment track can have hundreds of thousands of items. */ { char key[32]; if (*pMapPsls == NULL) *pMapPsls = newHash(8); safef(key, sizeof key, "%p", chain); struct psl *mapPsl = hashFindVal(*pMapPsls, key); if (mapPsl == NULL) { mapPsl = chainToPsl(chain); pslSwap(mapPsl, FALSE); hashAdd(*pMapPsls, key, mapPsl); } return mapPsl; } static boolean quickLiftPslBackToProtein(struct psl *lifted) /* pslTransMap puts a protein alignment into nucleotide space to do the mapping and leaves * it there, so the query start, end and size come back three times too large and the base * alignment view refuses the alignment ("size of rna X is 604, has changed since alignment * was performed when it was 1812"). Put the query side back into protein units. Returns * FALSE, leaving the alignment alone, when the lift split a codon so the query side no * longer divides evenly. */ { int i; if ((lifted->qStart % 3) || (lifted->qEnd % 3) || (lifted->qSize % 3) || (lifted->qBaseInsert % 3)) return FALSE; for (i = 0; i < lifted->blockCount; i++) if ((lifted->blockSizes[i] % 3) || (lifted->qStarts[i] % 3)) return FALSE; // A protein psl always has its query on the forward strand, "++" or "+-". pslTransMap can // hand back strand[0] == '-' (it reverse complements the input when the two alignments // disagree about the shared sequence's strand), and "-+" would tell pslShow to reverse // complement the protein as though it were DNA. Turn it over so the minus lands on the // target side, where the protein display expects it. // pslRc makes the target strand explicit as it turns the alignment over, so the // assignment below is the other half of this test, not something to do as well. if (lifted->strand[0] == '-') pslRc(lifted); else { // A protein psl carries the target strand explicitly, and pslTransMap normalized the // target onto the forward strand on the way out. lifted->strand[1] = '+'; 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; struct cBlock *blockList = NULL, *b; int i; AllocVar(chain); chain->tName = cloneString(psl->tName); chain->tSize = psl->tSize; chain->tStart = psl->tStart; chain->tEnd = psl->tEnd; chain->qName = cloneString(psl->qName); chain->qSize = psl->qSize; chain->qStrand = psl->strand[0]; // chainToPsl turns the chain's query bounds the right way up for a psl, so turn them back if (chain->qStrand == '-') { chain->qStart = psl->qSize - psl->qEnd; chain->qEnd = psl->qSize - psl->qStart; } else { chain->qStart = psl->qStart; chain->qEnd = psl->qEnd; } for (i = psl->blockCount - 1; i >= 0; i--) { AllocVar(b); b->tStart = psl->tStarts[i]; b->tEnd = b->tStart + psl->blockSizes[i]; b->qStart = psl->qStarts[i]; b->qEnd = b->qStart + psl->blockSizes[i]; slAddHead(&blockList, b); } chain->blockList = blockList; return chain; } struct mafAli *quickLiftMafs(struct hash *chainHash, struct mafAli *mafList, char *sourceDb, char *refSrc, int refSrcSize) // Map MAF blocks from the other assembly onto our current reference. // // A MAF block has to be one contiguous run on its first row, and the lift does not keep // the reference contiguous: where the reference assembly has lost bases the columns for // them go away, and where it has gained bases the alignment says nothing about them. So a // block is cut at every chain block boundary. Inside one chain block the two assemblies // run in step, which is what lets the columns be carried over untouched: only the first // row's coordinates change, and mafSubset does the rest of the arithmetic. // // refSrc is the name the browser expects on the reference row, ".", with no hub // prefix. Blocks whose reference does not map are dropped. { struct mafAli *outList = NULL; struct mafAli *maf, *nextMaf; for (maf = mafList; maf != NULL; maf = nextMaf) { nextMaf = maf->next; maf->next = NULL; // The first row of a MAF is its reference, and a reference row is always forward. struct mafComp *ref = maf->components; if ((ref == NULL) || (ref->strand != '+') || (ref->size <= 0)) { mafAliFree(&maf); continue; } // the chains are keyed on the sequence name in the other assembly // mafSplitSrcGetChrom writes into what it is given, so it needs a copy, and the copy // has to be allocated: a maf component name comes from a hub and safecpy into a // fixed buffer would abort on a long one rather than truncate. char *srcBuf = cloneString(ref->src); char *srcChrom = mafSplitSrcGetChrom(srcBuf, sourceDb); int refStart = ref->start; int refEnd = refStart + ref->size; struct chain *chain = liftOverChainForRange(chainHash, srcChrom, refStart, refEnd); if (chain == NULL) { freeMem(srcBuf); mafAliFree(&maf); continue; } struct cBlock *cb; for (cb = chain->blockList; cb != NULL; cb = cb->next) { int runStart = max(cb->tStart, refStart); int runEnd = min(cb->tEnd, refEnd); if (runStart >= runEnd) continue; struct mafAli *sub = mafSubset(maf, ref->src, runStart, runEnd); if (sub == NULL) continue; int destStart = cb->qStart + (runStart - cb->tStart); if (chain->qStrand == '-') { // The lift turns the block over, so turn every row over with it. A chain // keeps its query side reverse complemented, so the forward start of the run // comes from the far end of it. mafFlipStrand(sub); destStart = chain->qSize - (cb->qStart + (runEnd - cb->tStart)); } struct mafComp *subRef = sub->components; freeMem(subRef->src); subRef->src = cloneString(refSrc); subRef->srcSize = refSrcSize; subRef->strand = '+'; subRef->start = destStart; slAddHead(&outList, sub); } freeMem(srcBuf); mafAliFree(&maf); } slReverse(&outList); return outList; } boolean quickLiftIsLifted(struct trackDb *tdb) // TRUE when this track's data comes from another assembly and there is enough to lift it. // Both halves have to be there: the chain file that does the lifting and the assembly the // data came from. A hub can set either one on its own, and half the pair is no use. { return (tdb != NULL) && (trackDbSetting(tdb, "quickLiftUrl") != NULL) && (trackDbSetting(tdb, "quickLiftDb") != NULL); } boolean quickLiftIsOwnChainTrack(struct trackDb *tdb) // TRUE when this is the chain track quickLift builds to show the lift itself. That stanza // carries quickLiftUrl and quickLiftDb like any lifted track, but its data is already in // reference coordinates and must not be lifted a second time. The giveaway is that its // bigDataUrl IS the quickLift chain file. { char *quickLiftFile = trackDbSetting(tdb, "quickLiftUrl"); if (quickLiftFile == NULL) return FALSE; if (startsWithNoCase("bigQuickLiftChain", tdb->type)) return TRUE; char *bigDataUrl = trackDbSetting(tdb, "bigDataUrl"); return (bigDataUrl != NULL) && sameString(bigDataUrl, quickLiftFile); } struct chain *quickLiftChain(struct hash *chainHash, struct hash **pMapPsls, struct chain *chain) // Map a chain's target side from the other assembly onto our current reference. A chain is // an alignment between that assembly and some other species, so this composes the two and // leaves a chain between the reference and that species. The query side is left alone. // Returns NULL if the chain doesn't map. The chain handed in is not modified. { // chainToPsl copies the header, and every chain loader leaves the header describing the // whole chain while loading only the blocks that overlap the range asked for. Correct it // for the conversion, then put it back: callers still want the whole-chain header, which // is what the native details page reports. int saveTStart = chain->tStart, saveTEnd = chain->tEnd; int saveQStart = chain->qStart, saveQEnd = chain->qEnd; struct cBlock *b = chain->blockList; if (b == NULL) return NULL; int tStart = b->tStart, tEnd = b->tEnd, qStart = b->qStart, qEnd = b->qEnd; for (; b != NULL; b = b->next) { if (b->tStart < tStart) tStart = b->tStart; if (b->tEnd > tEnd) tEnd = b->tEnd; if (b->qStart < qStart) qStart = b->qStart; if (b->qEnd > qEnd) qEnd = b->qEnd; } chain->tStart = tStart; chain->tEnd = tEnd; chain->qStart = qStart; chain->qEnd = qEnd; struct psl *psl = chainToPsl(chain); chain->tStart = saveTStart; chain->tEnd = saveTEnd; chain->qStart = saveQStart; chain->qEnd = saveQEnd; struct psl *lifted = quickLiftPsl(chainHash, pMapPsls, psl); pslFree(&psl); if (lifted == NULL) return NULL; struct chain *out = chainFromPsl(lifted); out->score = chain->score; out->id = chain->id; pslFree(&lifted); return out; } struct psl *quickLiftPsls(struct hash *chainHash, struct psl *pslList) // Map a list of alignments in the other assembly's coordinates onto our current reference. // Alignments that don't map are dropped. { struct psl *liftedList = NULL; struct psl *psl, *nextPsl; struct hash *mapPsls = NULL; for(psl = pslList; psl; psl = nextPsl) { nextPsl = psl->next; psl->next = NULL; struct psl *lifted = quickLiftPsl(chainHash, &mapPsls, psl); if (lifted != NULL) slAddHead(&liftedList, lifted); pslFree(&psl); } slReverse(&liftedList); return liftedList; } struct encodePeak *quickLiftPeaks(struct encodePeak *peakList, struct hash *chainHash) // Map a list of encodePeaks in query coordinates to our current reference. These can't go // through quickLiftBeds: the thickStart and thickEnd it assigns overlay signalValue and // pValue in struct encodePeak. { struct encodePeak *liftedList = NULL; struct encodePeak *nextPeak; struct encodePeak *peak; for(peak = peakList; peak; peak = nextPeak) { nextPeak = peak->next; peak->next = NULL; char *error = liftOverRemapRange(chainHash, 0.0, peak->chrom, peak->chromStart, peak->chromEnd, peak->strand[0], 0.001, &peak->chrom, (int *)&peak->chromStart, (int *)&peak->chromEnd, &peak->strand[0]); if (error == NULL) slAddHead(&liftedList, peak); } return liftedList; } boolean quickLiftEnabled(struct cart *cart) /* Return TRUE if feature is available */ { char *cfgEnabled = cartOrCfgOption(cart, "browser.quickLift"); return cfgEnabled && (sameString(cfgEnabled, "on") || sameString(cfgEnabled, "true")) ; } boolean quickLiftAlignmentsEnabled(struct cart *cart) /* Return TRUE if quickLift is allowed to lift alignment tracks: psl, bigPsl, chain, * bigChain, maf, bigMaf and wigMaf. Off unless hg.conf says * browser.quickLiftAlignments=on, and a cart variable of the same name overrides that so * one machine can show both answers. The hg.conf half is read with a literal * cfgOptionBooleanDefault rather than cartOrCfgOption because harvestHgConf.py only sees * the cfgOption* accessors, which is why browser.quickLift itself is missing from the * hg.conf catalog. */ { char *cartEnabled = cartOptionalString(cart, "browser.quickLiftAlignments"); if (cartEnabled != NULL) return sameString(cartEnabled, "on") || sameString(cartEnabled, "true") || sameString(cartEnabled, "yes"); return cfgOptionBooleanDefault("browser.quickLiftAlignments", FALSE); } static int hrCmp(const void *va, const void *vb) /* Compare to sort based on chromStart. */ { const struct quickLiftRegions *a = *((struct quickLiftRegions **)va); const struct quickLiftRegions *b = *((struct quickLiftRegions **)vb); return a->chromStart - b->chromStart; } struct quickLiftRegions *getMismatches(char *ourDb, char strand, char *chrom, char *liftDb, char *liftChrom, struct bigLink *bl, int querySize, int seqStart, int seqEnd, char * chainId) // Helper function to calculate mismatches in a bigLink block { struct quickLiftRegions *hrList = NULL, *hr; int tStart = bl->chromStart; int tEnd = bl->chromEnd; int width = tEnd - tStart; int qStart = bl->qStart; int qEnd = qStart + width; if (strand == '-') { int saveStart = qStart; qStart = querySize - qEnd; qEnd = querySize - saveStart; } // grab that DNA struct dnaSeq *tSeq = hDnaFromSeq(ourDb, chrom, tStart, tEnd, dnaUpper); struct dnaSeq *qSeq = hDnaFromSeq(liftDb, liftChrom, qStart, qEnd, dnaUpper); if (strand == '-') reverseComplement(qSeq->dna, qSeq->size); // now step through looking for mismatches char *tDna = tSeq->dna; char *qDna = qSeq->dna; unsigned tAddr = tStart; unsigned qAddr = qStart; for(; tAddr < tEnd; tAddr++, qAddr++, tDna++, qDna++) { if (tAddr < seqStart) continue; if (tAddr > seqEnd) break; if (*tDna != *qDna) { AllocVar(hr); slAddHead(&hrList, hr); hr->chrom = cloneString(chrom); hr->oChrom = cloneString(liftChrom); hr->chromStart = tAddr; hr->chromEnd = tAddr + 1; hr->oChromStart = qAddr; hr->oChromEnd = qAddr + 1; hr->bases = tDna; hr->otherBases = qDna; hr->baseCount = 1; hr->otherBaseCount = 1; hr->type = QUICKTYPE_MISMATCH; hr->id = chainId; } } return hrList; } struct quickLiftRegions *fillWithGap(struct bigChain *bc, unsigned previousTEnd, unsigned tStart, unsigned previousQEnd, unsigned qStart) { struct quickLiftRegions *hr; AllocVar(hr); hr->id = bc->name; hr->chrom = cloneString(bc->chrom); hr->oChrom = cloneString(bc->qName); hr->chromStart = previousTEnd; hr->chromEnd = tStart; if (bc->strand[0] == '-') { hr->oChromStart = bc->qSize - qStart; hr->oChromEnd = bc->qSize - previousQEnd; } else { hr->oChromStart = previousQEnd; hr->oChromEnd = qStart; } return hr; } struct quickLiftRegions *quickLiftGetRegions(char *ourDb, char *liftDb, char *quickLiftFile, char *chrom, int seqStart, int seqEnd) /* Figure out the highlight regions and cache them. */ { static struct hash *highLightsHash = NULL; struct quickLiftRegions *hrList = NULL; unsigned lengthLimit = atoi(cfgOptionDefault("quickLift.lengthLimit", "10000")); if (seqEnd - seqStart > lengthLimit) return hrList; if (highLightsHash != NULL) { if ((hrList = (struct quickLiftRegions *)hashFindVal(highLightsHash, quickLiftFile)) != NULL) return hrList; } else { highLightsHash = newHash(0); } struct bbiFile *bbiChain = bigBedFileOpenAlias(quickLiftFile, chromAliasFindAliases); struct lm *lm = lmInit(0); struct bigBedInterval *bbChain, *bbChainList = bigBedIntervalQuery(bbiChain, chrom, seqStart, seqEnd, 0, lm); char *links = bigChainGetLinkFile(quickLiftFile); struct bbiFile *bbiLink = bigBedFileOpenAlias(links, chromAliasFindAliases); struct bigBedInterval *bbLink, *bbLinkList = bigBedIntervalQuery(bbiLink, chrom, seqStart, seqEnd, 0, lm); char *chainRow[1024]; char *linkRow[1024]; char startBuf[16], endBuf[16]; for (bbChain = bbChainList; bbChain != NULL; bbChain = bbChain->next) { bigBedIntervalToRow(bbChain, chrom, startBuf, endBuf, chainRow, ArraySize(chainRow)); struct bigChain *bc = bigChainLoad(chainRow); int previousTEnd = -1; int previousQEnd = -1; for (bbLink = bbLinkList; bbLink != NULL; bbLink = bbLink->next) { bigBedIntervalToRow(bbLink, chrom, startBuf, endBuf, linkRow, ArraySize(linkRow)); struct bigLink *bl = bigLinkLoad(linkRow); if (!sameString(bl->name, bc->name)) continue; int tStart = bl->chromStart; int tEnd = bl->chromEnd; int qStart = bl->qStart; int qEnd = qStart + (tEnd - tStart); struct quickLiftRegions *hr; if ((previousTEnd != -1) && (previousTEnd == tStart)) { hr = fillWithGap(bc, previousTEnd, tStart, previousQEnd, qStart); slAddHead(&hrList, hr); hr->type = QUICKTYPE_DEL; struct dnaSeq *qSeq = NULL; if (bc->strand[0] == '-') { qSeq = hDnaFromSeq(liftDb, bc->qName, bc->qSize - hr->oChromEnd, bc->qSize - hr->oChromStart, dnaUpper); reverseComplement(qSeq->dna, qSeq->size); } else qSeq = hDnaFromSeq(liftDb, bc->qName, hr->oChromStart, hr->oChromEnd, dnaUpper); hr->otherBases = qSeq->dna; hr->otherBaseCount = hr->oChromEnd - hr->oChromStart; } else if ( (previousQEnd != -1) && (previousQEnd == qStart)) { hr = fillWithGap(bc, previousTEnd, tStart, previousQEnd, qStart); slAddHead(&hrList, hr); hr->type = QUICKTYPE_INSERT; struct dnaSeq *tSeq = hDnaFromSeq(ourDb, chrom, hr->chromStart, hr->chromEnd, dnaUpper); hr->bases = tSeq->dna; hr->baseCount = hr->chromEnd - hr->chromStart; } else if ( ((previousQEnd != -1) && (previousQEnd != qStart)) && ((previousTEnd != -1) && (previousTEnd != tStart))) { hr = fillWithGap(bc, previousTEnd, tStart, previousQEnd, qStart); hr->type = QUICKTYPE_DOUBLE; hr->baseCount = hr->chromEnd - hr->chromStart; hr->otherBaseCount = hr->oChromEnd - hr->oChromStart; slAddHead(&hrList, hr); } previousQEnd = qEnd; previousTEnd = tEnd; // now find the mismatches in this block struct quickLiftRegions *mismatches = getMismatches(ourDb, bc->strand[0], chrom, liftDb, bc->qName, bl, bc->qSize, seqStart, seqEnd, bc->name); hrList = slCat(mismatches, hrList); } } slSort(&hrList, hrCmp); hashAdd(highLightsHash, quickLiftFile, hrList); return hrList; } void quickLiftResolveTable(struct trackDb *tdb, char *trackTable, char **retTable, char **retLiftDb) /* Resolve the table name and liftDb for a quickLift track. For custom tracks, * sets *retLiftDb to CUSTOM_TRASH and *retTable to the dbTableName setting; * otherwise sets *retTable to trackTable. Caller should have already set * *retLiftDb to trackDbSetting(tdb, "quickLiftDb"). */ { if (isCustomTrack(trackTable)) { *retLiftDb = CUSTOM_TRASH; *retTable = trackDbSetting(tdb, "dbTableName"); } else *retTable = trackTable; } struct bed *quickLiftSqlLoadBeds(struct trackDb *tdb, char *trackTable, char *liftDb, char *chrom, int start, int end, char *extraWhere, ItemLoader2 loader, int numFields, boolean blocked) /* Load items from another assembly via quickLift SQL, map them back to the reference, * and return the lifted beds. Handles custom track table resolution internally. * Caller provides liftDb from trackDbSetting(tdb, "quickLiftDb"). */ { char *table; quickLiftResolveTable(tdb, trackTable, &table, &liftDb); struct hash *chainHash = newHash(8); struct sqlConnection *conn = hAllocConn(liftDb); char *quickLiftFile = cloneString(trackDbSetting(tdb, "quickLiftUrl")); struct bed *bed = (struct bed *)quickLiftSql(conn, quickLiftFile, table, chrom, start, end, NULL, extraWhere, loader, numFields, chainHash); struct bed *liftedBeds = quickLiftBeds(bed, chainHash, blocked); hFreeConn(&conn); return liftedBeds; } char *quickLiftChainTable() /* Return the name of the quickLiftChain table. */ { static char *quickLiftChainTable = NULL; if (quickLiftChainTable == NULL) quickLiftChainTable = cfgOptionEnvDefault("QUICKLIFTCHAINNAME", quickLiftChainTableConfVariable, defaultQuickLiftChainTableName); return quickLiftChainTable; } boolean quickLiftLiftPos(char *sourceDb, char *destDb, char *chrom, int start, int end, char **retChrom, int *retStart, int *retEnd) /* Map a position from source (sourceDb) coords to destination (destDb) coords * using the liftOver chain for sourceDb -> destDb. This is used to remap * hgFind results from quickLifted bigBed tracks (which return hits in the * source assembly's coordinates) back to the destination assembly the user * is viewing. Returns TRUE on success. */ { static struct hash *fileToChainHash = NULL; if (fileToChainHash == NULL) fileToChainHash = newHash(0); char key[1024]; safef(key, sizeof(key), "%s->%s", sourceDb, destDb); struct hash *chainHash = hashFindVal(fileToChainHash, key); if (chainHash == NULL) { char *chainFile = liftOverChainFile(sourceDb, destDb); if (chainFile == NULL) return FALSE; chainHash = newHash(0); // This reads every chain in the file up front. A bigChain-format // liftOver chain indexed on the source (fromDb) side would let us // load just the chains overlapping the hit; worth revisiting if the // upfront cost becomes an issue. readLiftOverMap(chainFile, chainHash); hashAdd(fileToChainHash, key, chainHash); } char strand = '+'; char *error = liftOverRemapRange(chainHash, 0.0, chrom, start, end, strand, 0.001, retChrom, retStart, retEnd, &strand); return (error == NULL); }