c385c1661b72541e1e8fb28187323f3b31c431e7 angie Fri May 29 18:24:49 2026 -0700 If new columns are present in ripples server output then use them to draw RIVET-like recombination diagram in pop-up. diff --git src/hg/hgPhyloPlace/runUsher.c src/hg/hgPhyloPlace/runUsher.c index 844bdbcbb77..79f59cd4d17 100644 --- src/hg/hgPhyloPlace/runUsher.c +++ src/hg/hgPhyloPlace/runUsher.c @@ -538,33 +538,37 @@ *ptr = '\0'; info->nextClade = hashStoreName(wordStore, words[1]); } if (wordCount > 2) { // Chop extra chars in usher-sampled clades.txt output char *ptr = strstr(words[2], "*|"); if (ptr) *ptr = '\0'; info->pangoLineage = hashStoreName(wordStore, words[2]); } } lineFileClose(&lf); } -static char *finalRecombHeaderExpected = "recomb_node_id\tdonor_node_id\tacceptor_node_id\trecombinant_num_desc\tdonor_num_desc\tacceptor_num_desc\t" +static char *finalRecombHeaderExpectedOld = "recomb_node_id\tdonor_node_id\tacceptor_node_id\trecombinant_num_desc\tdonor_num_desc\tacceptor_num_desc\t" "breakpoint interval 1\tbreakpoint interval 2\trecombinant clade\trecombinant lineage\tdonor clade\tdonor lineage\t" "acceptor clade\tacceptor lineage\trepresentative descendant\toriginal parsimony score\tparsimony score improvement"; +static char *finalRecombHeaderExpected = "recomb_node_id\tdonor_node_id\tacceptor_node_id\trecombinant_num_desc\tdonor_num_desc\tacceptor_num_desc\t" + "breakpoint interval 1\tbreakpoint interval 2\trecombinant clade\trecombinant lineage\tdonor clade\tdonor lineage\t" + "acceptor clade\tacceptor lineage\trepresentative descendant\toriginal parsimony score\tparsimony score improvement\t" + "recombinant mutations\tdonor mutations\tacceptor mutations"; static uint parseUint(char *word, struct lineFile *lf) /* Parse non-negative int from word. Use lf for error reporting if necessary. */ { if (!isAllDigits(word)) lineFileAbort(lf, "Expected a non-negative number but got '%s'", word); int val = atol(word); if (val < 0) lineFileAbort(lf, "Expected a non-negative number but got '%s'", word); return val; } #define RIPPLES_GENOME_SIZE "GENOME_SIZE" static void parseBpRange(char *word, uint genomeSize, uint *retMin, uint *retMax, struct lineFile *lf) @@ -603,83 +607,100 @@ } if (differentString(p, ")")) lineFileAbort(lf, "Expected breakpoint range like '(1,2)' but didn't find final ')', got '%s'", word); if (max < min) lineFileAbort(lf, "Expected breakpoint range like '(1,2)' but got max < min: '%s'", word); if (min < 0 || max < 0) lineFileAbort(lf, "Expected breakpoint range like '(1,2)' but got negative number: '%s'", word); *retMin = min; *retMax = max; } static struct recombinantInfo *parseOneRecombinant(char *line, uint genomeSize, struct lineFile *lf) /* Allocate and return one struct recombinantInfo with values extracted from line. Use lf for error reporting if necessary. */ { struct recombinantInfo *ri = NULL; -char *words[18]; +char *words[21]; int wordCount = chopTabs(line, words); -if (wordCount != 17) - lineFileAbort(lf, "Expected 17 tab-separated words but got %d", wordCount); +if (wordCount != 17 && wordCount != 20) + lineFileAbort(lf, "Expected 17 or 20 tab-separated words but got %d", wordCount); AllocVar(ri); ri->recombNodeId = cloneString(words[0]); ri->donorNodeId = cloneString(words[1]); ri->acceptorNodeId = cloneString(words[2]); ri->recombNumDesc = parseUint(words[3], lf); ri->donorNumDesc = parseUint(words[4], lf); ri->acceptorNumDesc = parseUint(words[5], lf); parseBpRange(words[6], genomeSize, &(ri->bp1Min), &(ri->bp1Max), lf); parseBpRange(words[7], genomeSize, &(ri->bp2Min), &(ri->bp2Max), lf); ri->recombClade = cloneString(words[8]); ri->recombLineage = cloneString(words[9]); ri->donorClade = cloneString(words[10]); ri->donorLineage = cloneString(words[11]); ri->acceptorClade = cloneString(words[12]); ri->acceptorLineage = cloneString(words[13]); ri->representative = cloneString(words[14]); ri->originalParsimony = parseUint(words[15], lf); ri->parsimonyImprovement = parseUint(words[16], lf); +if (wordCount > 17) + { + ri->recombMutations = cloneString(words[17]); + ri->donorMutations = cloneString(words[18]); + ri->acceptorMutations = cloneString(words[19]); + } return ri; } static void recombinantInfoFree(struct recombinantInfo **pRi) /* Free a struct recombinantInfo and strings that it points to. */ { if (pRi && *pRi) { struct recombinantInfo *ri = *pRi; freeMem(ri->recombNodeId); freeMem(ri->donorNodeId); freeMem(ri->acceptorNodeId); freeMem(ri->recombClade); freeMem(ri->recombLineage); freeMem(ri->donorClade); freeMem(ri->donorLineage); freeMem(ri->acceptorClade); freeMem(ri->acceptorLineage); freeMem(ri->representative); freez(pRi); } } -static int recombinantInfoCmp(const void *el1, const void *el2) +static int recombinantInfoCmpImp(const void *el1, const void *el2) /* For sorting by parsimonyImprovement, descending. */ { struct recombinantInfo *ri1 = *(struct recombinantInfo **)el1; struct recombinantInfo *ri2 = *(struct recombinantInfo **)el2; return ri2->parsimonyImprovement - ri1->parsimonyImprovement; } +static int recombinantInfoCmpRecombImp(const void *el1, const void *el2) +/* For sorting by recombNodeId, ascending, and parsimonyImprovement, descending. */ +{ +struct recombinantInfo *ri1 = *(struct recombinantInfo **)el1; +struct recombinantInfo *ri2 = *(struct recombinantInfo **)el2; +int diff = strcmp(ri1->recombNodeId, ri2->recombNodeId); +if (diff == 0) + diff = ri2->parsimonyImprovement - ri1->parsimonyImprovement; +return diff; +} + static boolean mergeRecombinants(struct recombinantInfo **pRiOldList, struct recombinantInfo **pRiNew) /* If riNew has a greater parsimony improvement than riOldList then replace riOldList with riNew. * If riNew has a smaller parsimony improvement than riOldList then free up riNew. * If riNew has the same parsimony improvement then attempt to merge its breakpoint ranges with * each member of riOldList. * Return TRUE if any of those were successful, FALSE only if there were breakpoint ranges that * could not be merged. */ { boolean success = FALSE; struct recombinantInfo *riNew = *pRiNew, *riOld = *pRiOldList; if (riNew->parsimonyImprovement > riOld->parsimonyImprovement) { slFreeListWithFunc(pRiOldList, recombinantInfoFree); *pRiOldList = *pRiNew; *pRiNew = NULL; @@ -762,31 +783,31 @@ if (dy == NULL) dy = dyStringNew(0); else dyStringClear(dy); dyStringPrintf(dy, "%s %s %s", ri->recombNodeId, ri->donorNodeId, ri->acceptorNodeId); return dy->string; } static struct recombinantInfo *parseRecombinants(char *filename, uint genomeSize) /* Parse final_recombination.tsv from ripples search and merge rows where possible. */ { struct recombinantInfo *recombList = NULL; struct lineFile *lf = lineFileOpen(filename, TRUE); char *line; lineFileNext(lf, &line, NULL); -if (isNotEmpty(line) && differentString(line, finalRecombHeaderExpected)) +if (isNotEmpty(line) && differentString(line, finalRecombHeaderExpected) && differentString(line, finalRecombHeaderExpectedOld)) lineFileAbort(lf, "Header fields do not match expected. Header:\n%s\nExpected:\n%s", line, finalRecombHeaderExpected); // Hash reported recombinants by concatenated node IDs because there tend to be many lines per // triplet that have all the same info except for parsimony score improvement (where we want to // keep only the highest improvement found per triplet) and breakpoints (where we want to merge // overlapping breakpoint regions). struct hash *mergedRecombinants = hashNew(0); while (lineFileNext(lf, &line, NULL)) { struct recombinantInfo *riNew = parseOneRecombinant(line, genomeSize, lf); char *key = getRecombinantKey(riNew); struct hashEl *hel = hashLookup(mergedRecombinants, key); if (hel) { if (! mergeRecombinants((struct recombinantInfo **)&(hel->val), &riNew)) @@ -794,33 +815,35 @@ } else { hashAdd(mergedRecombinants, key, riNew); } } lineFileClose(&lf); // Add all mergedRecombinants to list, and sort by parsimony improvement (highest first) struct hashEl *hel, *helList = hashElListHash(mergedRecombinants); for (hel = helList; hel != NULL; hel = hel->next) { struct recombinantInfo *riList = hel->val; recombList = slCat(recombList, riList); } hashElFreeList(&helList); -slSort(&recombList, recombinantInfoCmp); +slSort(&recombList, recombinantInfoCmpImp); // Filter the list to keep only the top 3 findings for each potential recombinant recombList = filterRecombinants(recombList); +// Finally, sort the list by recombNodeId (and then improvement) because it's confusing to alternate between those. +slSort(&recombList, recombinantInfoCmpRecombImp); return recombList; } static char *descendantsHeaderExpected = "#node_id\tdescendants"; static struct hash *parseDescendants(char *filename) /* Parse descendants.tsv from ripples search into hash of node ID to slName list of leaves. */ { struct hash *hash = hashNew(0); struct lineFile *lf = lineFileOpen(filename, TRUE); char *line; lineFileNext(lf, &line, NULL); if (isNotEmpty(line) && differentString(line, descendantsHeaderExpected)) lineFileAbort(lf, "Header fields do not match expected. Header:\n%s\nExpected:\n%s", line, descendantsHeaderExpected);