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);