jbrowse-plugin-msaview 3.2.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/AddHighlightModel/GenomeMouseoverHighlight.js +1 -1
  2. package/dist/AddHighlightModel/MsaToGenomeHighlight.js +1 -1
  3. package/dist/AddHighlightModel/index.js +1 -1
  4. package/dist/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.js +61 -17
  5. package/dist/LaunchMsaView/components/BlastQuery/BlastManualPanel.js +1 -1
  6. package/dist/LaunchMsaView/components/BlastQuery/BlastPanel.js +2 -2
  7. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.d.ts +12 -0
  8. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.js +21 -2
  9. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.d.ts +1 -0
  10. package/dist/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.js +29 -0
  11. package/dist/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.js +1 -1
  12. package/dist/LaunchMsaView/components/BlastQuery/consts.d.ts +28 -0
  13. package/dist/LaunchMsaView/components/BlastQuery/consts.js +21 -0
  14. package/dist/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.js +1 -1
  15. package/dist/LaunchMsaView/components/OrthologQuery/OrthologPanel.js +9 -4
  16. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.d.ts +9 -0
  17. package/dist/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.js +20 -0
  18. package/dist/LaunchMsaView/components/SubmitCancelActions.test.js +1 -1
  19. package/dist/LaunchMsaView/components/useFeatureSequence.js +1 -1
  20. package/dist/LaunchMsaView/detectQueryRow.d.ts +15 -2
  21. package/dist/LaunchMsaView/detectQueryRow.js +20 -21
  22. package/dist/LaunchMsaView/detectQueryRow.test.js +15 -15
  23. package/dist/LaunchMsaView/useQueryRowName.js +5 -8
  24. package/dist/MsaViewPanel/afterCreateAutoruns.js +2 -2
  25. package/dist/MsaViewPanel/components/ErrorBoundary.d.ts +2 -2
  26. package/dist/MsaViewPanel/components/JobLink.js +7 -1
  27. package/dist/MsaViewPanel/components/LaunchProgress.d.ts +17 -0
  28. package/dist/MsaViewPanel/components/LaunchProgress.js +41 -0
  29. package/dist/MsaViewPanel/components/MsaViewPanel.js +6 -3
  30. package/dist/MsaViewPanel/components/MsaViewPanel.test.d.ts +1 -0
  31. package/dist/MsaViewPanel/components/MsaViewPanel.test.js +68 -0
  32. package/dist/MsaViewPanel/doLaunchBlast.d.ts +1 -1
  33. package/dist/MsaViewPanel/doLaunchBlast.js +84 -52
  34. package/dist/MsaViewPanel/doLaunchOrthologs.d.ts +7 -5
  35. package/dist/MsaViewPanel/doLaunchOrthologs.js +64 -30
  36. package/dist/MsaViewPanel/doLaunchOrthologs.test.js +106 -1
  37. package/dist/MsaViewPanel/genomeToMSA.js +4 -2
  38. package/dist/MsaViewPanel/genomeToMSA.test.js +34 -0
  39. package/dist/MsaViewPanel/model.d.ts +41 -11
  40. package/dist/MsaViewPanel/model.js +6 -0
  41. package/dist/MsaViewPanel/observeProteinHighlights.test.js +11 -0
  42. package/dist/MsaViewPanel/syncGenomeHoverToMsaColumn.test.js +1 -0
  43. package/dist/MsaViewPanel/util.d.ts +18 -0
  44. package/dist/MsaViewPanel/util.js +17 -0
  45. package/dist/jbrowse-plugin-msaview.umd.production.min.js +47 -35
  46. package/dist/jbrowse-plugin-msaview.umd.production.min.js.map +4 -4
  47. package/dist/utils/blastCache.d.ts +10 -6
  48. package/dist/utils/blastCache.js +15 -3
  49. package/dist/utils/ebiBlast.d.ts +1 -1
  50. package/dist/utils/msa.d.ts +12 -0
  51. package/dist/utils/msa.js +35 -12
  52. package/dist/utils/msaRows.d.ts +31 -0
  53. package/dist/utils/msaRows.js +67 -0
  54. package/dist/utils/pantherOrthologs.d.ts +79 -0
  55. package/dist/utils/pantherOrthologs.js +262 -0
  56. package/dist/utils/phmmer.d.ts +53 -0
  57. package/dist/utils/phmmer.js +118 -0
  58. package/dist/utils/taxonomyNames.d.ts +1 -1
  59. package/dist/utils/taxonomyNames.js +6 -1
  60. package/dist/version.d.ts +1 -1
  61. package/dist/version.js +1 -1
  62. package/package.json +27 -21
  63. package/src/AddHighlightModel/GenomeMouseoverHighlight.tsx +1 -1
  64. package/src/AddHighlightModel/MsaToGenomeHighlight.tsx +1 -1
  65. package/src/AddHighlightModel/index.tsx +1 -1
  66. package/src/LaunchMsaView/components/BlastQuery/BlastAutomaticPanel.tsx +88 -30
  67. package/src/LaunchMsaView/components/BlastQuery/BlastManualPanel.tsx +1 -1
  68. package/src/LaunchMsaView/components/BlastQuery/BlastPanel.tsx +4 -4
  69. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.test.ts +50 -0
  70. package/src/LaunchMsaView/components/BlastQuery/CachedBlastResults.tsx +23 -3
  71. package/src/LaunchMsaView/components/BlastQuery/MsaAlgorithmSelect.tsx +1 -1
  72. package/src/LaunchMsaView/components/BlastQuery/consts.ts +40 -0
  73. package/src/LaunchMsaView/components/ManualMSALoader/ManualMSALoader.tsx +1 -1
  74. package/src/LaunchMsaView/components/OrthologQuery/OrthologPanel.tsx +21 -5
  75. package/src/LaunchMsaView/components/OrthologQuery/OrthologSourceSelect.tsx +52 -0
  76. package/src/LaunchMsaView/components/SubmitCancelActions.test.tsx +1 -1
  77. package/src/LaunchMsaView/components/useFeatureSequence.ts +1 -1
  78. package/src/LaunchMsaView/detectQueryRow.test.ts +17 -15
  79. package/src/LaunchMsaView/detectQueryRow.ts +34 -23
  80. package/src/LaunchMsaView/useQueryRowName.ts +6 -9
  81. package/src/MsaViewPanel/afterCreateAutoruns.ts +2 -2
  82. package/src/MsaViewPanel/components/ErrorBoundary.tsx +2 -1
  83. package/src/MsaViewPanel/components/JobLink.tsx +7 -2
  84. package/src/MsaViewPanel/components/LaunchProgress.tsx +62 -0
  85. package/src/MsaViewPanel/components/MsaViewPanel.test.tsx +83 -0
  86. package/src/MsaViewPanel/components/MsaViewPanel.tsx +7 -4
  87. package/src/MsaViewPanel/doLaunchBlast.ts +127 -69
  88. package/src/MsaViewPanel/doLaunchOrthologs.test.ts +119 -2
  89. package/src/MsaViewPanel/doLaunchOrthologs.ts +100 -38
  90. package/src/MsaViewPanel/genomeToMSA.test.ts +37 -0
  91. package/src/MsaViewPanel/genomeToMSA.ts +6 -2
  92. package/src/MsaViewPanel/model.ts +38 -5
  93. package/src/MsaViewPanel/observeProteinHighlights.test.ts +13 -0
  94. package/src/MsaViewPanel/syncGenomeHoverToMsaColumn.test.ts +1 -0
  95. package/src/MsaViewPanel/util.ts +18 -0
  96. package/src/utils/blastCache.ts +33 -12
  97. package/src/utils/ebiBlast.ts +1 -1
  98. package/src/utils/msa.ts +43 -12
  99. package/src/utils/msaRows.ts +95 -0
  100. package/src/utils/pantherOrthologs.ts +399 -0
  101. package/src/utils/phmmer.ts +174 -0
  102. package/src/utils/taxonomyNames.ts +6 -1
  103. package/src/version.ts +1 -1
  104. package/dist/MsaViewPanel/components/LoadingBLAST.d.ts +0 -6
  105. package/dist/MsaViewPanel/components/LoadingBLAST.js +0 -26
  106. package/src/MsaViewPanel/components/LoadingBLAST.tsx +0 -48
@@ -25,6 +25,7 @@ describe('genomeToMSA', () => {
25
25
 
26
26
  const model = {
27
27
  querySeqName: 'hg38.chr1',
28
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
28
29
  transcriptToMsaMap: undefined,
29
30
  mafRegion: {
30
31
  refName: 'chr1',
@@ -47,6 +48,7 @@ describe('genomeToMSA', () => {
47
48
 
48
49
  const model = {
49
50
  querySeqName: 'hg38.chr1',
51
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
50
52
  transcriptToMsaMap: undefined,
51
53
  mafRegion: {
52
54
  refName: 'chr1',
@@ -75,6 +77,7 @@ describe('genomeToMSA', () => {
75
77
 
76
78
  const model = {
77
79
  querySeqName: 'hg38.chr1',
80
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
78
81
  transcriptToMsaMap: undefined,
79
82
  mafRegion: {
80
83
  refName: 'chr1',
@@ -107,6 +110,7 @@ describe('genomeToMSA', () => {
107
110
 
108
111
  const model = {
109
112
  querySeqName: 'hg38.chr1',
113
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
110
114
  transcriptToMsaMap: undefined,
111
115
  mafRegion: {
112
116
  refName: 'chr1',
@@ -136,6 +140,7 @@ describe('genomeToMSA', () => {
136
140
 
137
141
  const model = {
138
142
  querySeqName: 'hg38.chr1',
143
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
139
144
  transcriptToMsaMap: undefined,
140
145
  mafRegion: {
141
146
  refName: 'chr1',
@@ -165,6 +170,7 @@ describe('genomeToMSA', () => {
165
170
 
166
171
  const model = {
167
172
  querySeqName: 'hg38.chr1',
173
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
168
174
  transcriptToMsaMap: undefined,
169
175
  mafRegion: {
170
176
  refName: 'chr1',
@@ -193,6 +199,7 @@ describe('genomeToMSA', () => {
193
199
 
194
200
  const model = {
195
201
  querySeqName: 'hg38.chr1',
202
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
196
203
  transcriptToMsaMap: undefined,
197
204
  mafRegion: {
198
205
  refName: 'chr1',
@@ -225,6 +232,7 @@ describe('genomeToMSA', () => {
225
232
 
226
233
  const model = {
227
234
  querySeqName: 'QUERY',
235
+ rows: [['QUERY', 'MKVLTAEEK']],
228
236
  transcriptToMsaMap: {
229
237
  refName: 'chr1',
230
238
  // g2p is keyed by 0-based genome position, the hover coord is 1-based
@@ -254,6 +262,7 @@ describe('genomeToMSA', () => {
254
262
  const mockSeqPosToVisibleCol = vi.fn()
255
263
  const model = {
256
264
  querySeqName: 'QUERY',
265
+ rows: [['QUERY', 'MKVLTAEEK']],
257
266
  transcriptToMsaMap: {
258
267
  refName: 'chr1',
259
268
  g2p: { 1004: 10 },
@@ -277,6 +286,7 @@ describe('genomeToMSA', () => {
277
286
 
278
287
  const model = {
279
288
  querySeqName: 'QUERY',
289
+ rows: [['QUERY', 'MKVLTAEEK']],
280
290
  transcriptToMsaMap: {
281
291
  refName: 'chr1',
282
292
  g2p: { 1000: 0 }, // No entry for 1004
@@ -301,6 +311,7 @@ describe('genomeToMSA', () => {
301
311
 
302
312
  const model = {
303
313
  querySeqName: 'QUERY',
314
+ rows: [['QUERY', 'MKVLTAEEK']],
304
315
  transcriptToMsaMap: undefined,
305
316
  mafRegion: undefined,
306
317
  connectedView: { initialized: true },
@@ -310,4 +321,30 @@ describe('genomeToMSA', () => {
310
321
  const result = genomeToMSA({ model })
311
322
  expect(result).toBeUndefined()
312
323
  })
324
+
325
+ // seqPosToVisibleCol answers 0 for a row name it does not know, so without a
326
+ // guard an alignment whose query row is missing -- the default 'QUERY' on an
327
+ // uploaded file, or the empty name the manual panel leaves when it matches
328
+ // nothing -- lights column 0 on every genome hover
329
+ test('returns undefined when querySeqName names no row here', () => {
330
+ mockGetSession.mockReturnValue({
331
+ hovered: {
332
+ hoverFeature: {},
333
+ hoverPosition: { coord: 1005, refName: 'chr1' },
334
+ },
335
+ } as any)
336
+
337
+ const seqPosToVisibleCol = vi.fn(() => 0)
338
+ const model = {
339
+ querySeqName: 'QUERY',
340
+ rows: [['some_other_row', 'MKVLTAEEK']],
341
+ transcriptToMsaMap: { refName: 'chr1', g2p: { 1004: 3 } },
342
+ mafRegion: undefined,
343
+ connectedView: { initialized: true },
344
+ seqPosToVisibleCol,
345
+ } as any
346
+
347
+ expect(genomeToMSA({ model })).toBeUndefined()
348
+ expect(seqPosToVisibleCol).not.toHaveBeenCalled()
349
+ })
313
350
  })
@@ -1,6 +1,6 @@
1
1
  import { getSession } from '@jbrowse/core/util'
2
2
 
3
- import { hasHoverPosition } from './util'
3
+ import { hasHoverPosition, hasQueryRow } from './util'
4
4
 
5
5
  import type { JBrowsePluginMsaViewModel } from './model'
6
6
 
@@ -8,7 +8,11 @@ export function genomeToMSA({ model }: { model: JBrowsePluginMsaViewModel }) {
8
8
  const { hovered } = getSession(model)
9
9
  const { querySeqName, transcriptToMsaMap, connectedView, mafRegion } = model
10
10
 
11
- if (!connectedView?.initialized || !hasHoverPosition(hovered)) {
11
+ if (
12
+ !connectedView?.initialized ||
13
+ !hasHoverPosition(hovered) ||
14
+ !hasQueryRow(model)
15
+ ) {
12
16
  return undefined
13
17
  }
14
18
 
@@ -25,11 +25,12 @@ import {
25
25
  msaCoordToGenomeRegions,
26
26
  } from './msaCoordToGenomeCoord'
27
27
 
28
- import type { MafRegion, MsaViewInitState } from './types'
29
28
  import type {
30
29
  BlastDatabase,
31
30
  MsaAlgorithm,
31
+ PhmmerDatabase,
32
32
  } from '../LaunchMsaView/components/BlastQuery/consts'
33
+ import type { MafRegion, MsaViewInitState } from './types'
33
34
  import type { Feature } from '@jbrowse/core/util'
34
35
  import type { Instance } from '@jbrowse/mobx-state-tree'
35
36
  import type { LinearGenomeViewModel } from '@jbrowse/plugin-linear-genome-view'
@@ -44,16 +45,48 @@ export interface IRegion {
44
45
  end: number
45
46
  }
46
47
 
47
- export interface BlastParams {
48
- blastDatabase: BlastDatabase
49
- msaAlgorithm: MsaAlgorithm
48
+ /**
49
+ * A search to run, discriminated by the program that runs it: the two arms
50
+ * differ in which databases they name and in whether an aligner runs at all, so
51
+ * splitting them is what lets doLaunchBlast read the database without asserting
52
+ * whose it is.
53
+ *
54
+ * The field is still `blastDatabase` rather than `database`: it is persisted in
55
+ * session snapshots and in the IndexedDB result cache, so renaming it would
56
+ * orphan every row already written.
57
+ */
58
+ export type BlastParams = {
50
59
  selectedTranscript?: Feature
51
60
  proteinSequence: string
52
- }
61
+ } & (
62
+ | {
63
+ /** absent on params written before phmmer existed, which were all blastp */
64
+ searchProgram?: 'blastp'
65
+ blastDatabase: BlastDatabase
66
+ msaAlgorithm: MsaAlgorithm
67
+ }
68
+ | {
69
+ searchProgram: 'phmmer'
70
+ /** phmmer names its databases its own way: `swissprot`, not `uniprotkb_swissprot` */
71
+ blastDatabase: PhmmerDatabase
72
+ /** phmmer aligns as it searches, so there is no aligner to choose */
73
+ msaAlgorithm?: undefined
74
+ }
75
+ )
76
+
77
+ /**
78
+ * Where the ortholog set comes from. NCBI's sets cover vertebrates and
79
+ * insects; PANTHER's span its 144 reference proteomes, human to yeast to
80
+ * Arabidopsis, so a gene from outside NCBI's scope aligns only through it.
81
+ */
82
+ export const orthologSources = ['ncbi', 'panther'] as const
83
+ export type OrthologSource = (typeof orthologSources)[number]
53
84
 
54
85
  export interface OrthologParams {
55
86
  /** NCBI taxon id of the assembly the query gene came from */
56
87
  taxId: number
88
+ /** `ncbi` when omitted, so every launch written before this key keeps its meaning */
89
+ source?: OrthologSource
57
90
  /**
58
91
  * taxon ids to include as rows. The query taxon has its own row already, so
59
92
  * it is excluded from this set whether or not it is named.
@@ -30,6 +30,7 @@ function makeModel({ highlightColumns }: { highlightColumns?: number[] } = {}) {
30
30
  const calls: (number[] | undefined)[] = []
31
31
  const model = {
32
32
  querySeqName: 'query',
33
+ rows: [['query', 'MKVLTAEEK']],
33
34
  connectedViewId: CONNECTED,
34
35
  // g2p is indexed by genome coord; identity keeps the arithmetic out of the way
35
36
  transcriptToMsaMap: {
@@ -249,6 +250,18 @@ describe('scope and redundant writes', () => {
249
250
  expect(calls).toEqual([])
250
251
  })
251
252
 
253
+ // seqPosToGlobalCol answers 0 for a row name it does not know, so without a
254
+ // guard a structure hover would light column 0 of whatever row is first
255
+ test('a query row this alignment does not have contributes no column', () => {
256
+ const { model, calls } = makeModel()
257
+ Object.assign(model, { rows: [['some_other_row', 'MKVLTAEEK']] })
258
+ const run = observeProteinHighlights(model)
259
+
260
+ session({ hover: [{ start: 10, end: 12 }] })
261
+ run()
262
+ expect(calls).toEqual([])
263
+ })
264
+
252
265
  test('nothing happens until the view is connected and mapped', () => {
253
266
  const { calls } = makeModel()
254
267
  const bare = {
@@ -28,6 +28,7 @@ function makeModel() {
28
28
  const calls: (number | undefined)[] = []
29
29
  const model = {
30
30
  querySeqName: 'hg38.chr1',
31
+ rows: [['hg38.chr1', 'ACGTACGTAC']],
31
32
  transcriptToMsaMap: undefined,
32
33
  mafRegion,
33
34
  connectedView: { initialized: true, assemblyNames: ['hg38'] },
@@ -1,3 +1,21 @@
1
+ /**
2
+ * Whether `querySeqName` names a row this alignment actually has.
3
+ *
4
+ * react-msaview's `seqPosToGlobalCol` answers 0 for a name it does not know, so
5
+ * without this every genome position maps to the first column and hovering the
6
+ * genome — or a connected structure — lights column 0 of an unrelated row. The
7
+ * name is wrong more often than it looks: it defaults to `QUERY`, which an
8
+ * uploaded alignment has no reason to carry, and the manual panel leaves it
9
+ * empty when it cannot match the protein to a row.
10
+ *
11
+ * The other direction has no such hole: msaCoordToGenomeRegions needs the query
12
+ * row's sequence to map a column at all, so a missing row is already nothing
13
+ * there.
14
+ */
15
+ export function hasQueryRow(model: { rows: string[][]; querySeqName: string }) {
16
+ return model.rows.some(r => r[0] === model.querySeqName)
17
+ }
18
+
1
19
  export function hasHoverPosition(
2
20
  hovered: unknown,
3
21
  ): hovered is { hoverPosition: { coord: number; refName: string } } {
@@ -3,6 +3,8 @@ import { createDbOpener } from './idb'
3
3
  import type {
4
4
  BlastDatabase,
5
5
  MsaAlgorithm,
6
+ PhmmerDatabase,
7
+ SearchProgram,
6
8
  } from '../LaunchMsaView/components/BlastQuery/consts'
7
9
  import type { DBSchema } from 'idb'
8
10
 
@@ -13,14 +15,17 @@ const DB_VERSION = 2
13
15
  export interface CachedBlastResult {
14
16
  id: string
15
17
  proteinSequence: string
16
- blastDatabase: BlastDatabase
18
+ blastDatabase: BlastDatabase | PhmmerDatabase
17
19
  /**
18
20
  * Only ever set on rows cached by a version that still queried NCBI, where
19
21
  * the choice between blastp and quick-blastp was real. Kept so those rows
20
22
  * still display; never written now.
21
23
  */
22
24
  blastProgram?: string
23
- msaAlgorithm: MsaAlgorithm
25
+ /** absent on rows cached before phmmer existed, which were all blastp */
26
+ searchProgram?: SearchProgram
27
+ /** absent on phmmer rows, which are aligned by the search itself */
28
+ msaAlgorithm?: MsaAlgorithm
24
29
  msa: string
25
30
  tree: string
26
31
  treeMetadata: string
@@ -52,13 +57,25 @@ const getDB = createDbOpener<BlastCacheDB>(
52
57
  },
53
58
  )
54
59
 
55
- function createCacheKey(
56
- proteinSequence: string,
57
- blastDatabase: BlastDatabase,
58
- msaAlgorithm: MsaAlgorithm,
59
- transcriptId?: string,
60
- ) {
60
+ function createCacheKey({
61
+ proteinSequence,
62
+ blastDatabase,
63
+ msaAlgorithm,
64
+ searchProgram,
65
+ transcriptId,
66
+ }: {
67
+ proteinSequence: string
68
+ blastDatabase: BlastDatabase | PhmmerDatabase
69
+ msaAlgorithm?: MsaAlgorithm
70
+ searchProgram?: SearchProgram
71
+ transcriptId?: string
72
+ }) {
61
73
  const idPart = transcriptId ? `:${transcriptId}` : ''
74
+ // phmmer keys are prefixed and blastp keys are left exactly as they were, so
75
+ // results cached before phmmer existed still resolve
76
+ if (searchProgram === 'phmmer') {
77
+ return `phmmer:${blastDatabase}${idPart}:${proteinSequence}`
78
+ }
62
79
  // msaAlgorithm is part of the key because the stored msa/tree are produced by
63
80
  // it — without it, re-running the same query under a different algorithm
64
81
  // overwrites the earlier result and drops it from the history list
@@ -69,6 +86,7 @@ export async function saveBlastResult({
69
86
  proteinSequence,
70
87
  blastDatabase,
71
88
  msaAlgorithm,
89
+ searchProgram,
72
90
  msa,
73
91
  tree,
74
92
  treeMetadata,
@@ -79,8 +97,9 @@ export async function saveBlastResult({
79
97
  geneName,
80
98
  }: {
81
99
  proteinSequence: string
82
- blastDatabase: BlastDatabase
83
- msaAlgorithm: MsaAlgorithm
100
+ blastDatabase: BlastDatabase | PhmmerDatabase
101
+ msaAlgorithm?: MsaAlgorithm
102
+ searchProgram?: SearchProgram
84
103
  msa: string
85
104
  tree: string
86
105
  treeMetadata: string
@@ -91,17 +110,19 @@ export async function saveBlastResult({
91
110
  geneName?: string
92
111
  }) {
93
112
  const db = await getDB()
94
- const id = createCacheKey(
113
+ const id = createCacheKey({
95
114
  proteinSequence,
96
115
  blastDatabase,
97
116
  msaAlgorithm,
117
+ searchProgram,
98
118
  transcriptId,
99
- )
119
+ })
100
120
  const entry: CachedBlastResult = {
101
121
  id,
102
122
  proteinSequence,
103
123
  blastDatabase,
104
124
  msaAlgorithm,
125
+ searchProgram,
105
126
  msa,
106
127
  tree,
107
128
  treeMetadata,
@@ -1,7 +1,7 @@
1
1
  import { fetchEbiResult, submitEbiJob, waitForEbiJob } from './ebiJobDispatcher'
2
2
 
3
- import type { BlastHit } from './types'
4
3
  import type { BlastDatabase } from '../LaunchMsaView/components/BlastQuery/consts'
4
+ import type { BlastHit } from './types'
5
5
 
6
6
  const TOOL = 'ncbiblast'
7
7
 
package/src/utils/msa.ts CHANGED
@@ -32,6 +32,43 @@ const algorithms: Record<
32
32
  },
33
33
  }
34
34
 
35
+ /**
36
+ * Build a tree from an alignment that already exists, which is what the phmmer
37
+ * path needs: phmmer produces the alignment itself, so there is no aligner run
38
+ * to take a guide tree from — and a guide tree is a byproduct of deciding
39
+ * progressive alignment order, not a phylogeny, so it is not what we would want
40
+ * even if there were one. simple_phylogeny is clustalw2's neighbour-joining on
41
+ * a real distance matrix, Kimura-corrected for protein distances.
42
+ */
43
+ export async function launchTree({
44
+ alignment,
45
+ onProgress,
46
+ }: {
47
+ alignment: string
48
+ onProgress: (arg: string) => void
49
+ }) {
50
+ const tool = 'simple_phylogeny'
51
+ onProgress('Building tree...')
52
+
53
+ const jobId = await submitEbiJob({
54
+ tool,
55
+ params: {
56
+ sequence: alignment,
57
+ tree: 'phylip',
58
+ clustering: 'Neighbour-joining',
59
+ kimura: 'true',
60
+ },
61
+ })
62
+ await waitForEbiJob({
63
+ tool,
64
+ jobId,
65
+ onCountdown: s => {
66
+ onProgress(`Re-checking tree status in... ${s}`)
67
+ },
68
+ })
69
+ return fetchEbiResult({ tool, jobId, type: 'tree' })
70
+ }
71
+
35
72
  export async function launchMSA({
36
73
  algorithm,
37
74
  sequence,
@@ -56,16 +93,10 @@ export async function launchMSA({
56
93
  onProgress(`Re-checking MSA status in... ${s}`)
57
94
  },
58
95
  })
59
- return {
60
- msa: await fetchEbiResult({
61
- tool: algorithm,
62
- jobId,
63
- type: config.msaResult,
64
- }),
65
- tree: await fetchEbiResult({
66
- tool: algorithm,
67
- jobId,
68
- type: config.treeResult,
69
- }),
70
- }
96
+ // one finished job, two result files, neither derived from the other
97
+ const [msa, tree] = await Promise.all([
98
+ fetchEbiResult({ tool: algorithm, jobId, type: config.msaResult }),
99
+ fetchEbiResult({ tool: algorithm, jobId, type: config.treeResult }),
100
+ ])
101
+ return { msa, tree }
71
102
  }
@@ -0,0 +1,95 @@
1
+ import { makeId } from '../LaunchMsaView/components/util'
2
+
3
+ import type { PhmmerRow } from './phmmer'
4
+ import type { TaxonomyInfo } from './taxonomyNames'
5
+ import type { BlastHitDescription } from './types'
6
+
7
+ /**
8
+ * Turning search results into the rows the view is given, kept free of any
9
+ * jbrowse or network import so the whole assembly can be run and checked
10
+ * outside a browser — see test/phmmerLive.test.ts.
11
+ */
12
+ export function buildRowMetadata(
13
+ desc: BlastHitDescription,
14
+ taxonomyInfo: Map<number, TaxonomyInfo>,
15
+ ) {
16
+ const metadata: Record<string, string> = {}
17
+ const taxInfo = desc.taxid ? taxonomyInfo.get(desc.taxid) : undefined
18
+
19
+ if (taxInfo?.sciname) {
20
+ metadata['Scientific name'] = taxInfo.sciname
21
+ }
22
+ if (taxInfo?.commonName) {
23
+ metadata['Common name'] = taxInfo.commonName
24
+ }
25
+ if (desc.accession) {
26
+ metadata.Accession = desc.accession
27
+ }
28
+ if (desc.id) {
29
+ metadata.ID = desc.id
30
+ }
31
+ if (desc.title) {
32
+ metadata.Description = desc.title
33
+ }
34
+
35
+ return metadata
36
+ }
37
+
38
+ /**
39
+ * One target can match the query in several places and phmmer emits a row per
40
+ * matched envelope — four for lamprey albumin against human albumin, which has
41
+ * three domains. Those rows share an accession and so would share a name, and
42
+ * duplicate names silently collapse rows in both the MSA and the tree, so the
43
+ * envelope disambiguates them.
44
+ */
45
+ export function makeRowNames(
46
+ rows: PhmmerRow[],
47
+ taxonomyInfo: Map<number, TaxonomyInfo>,
48
+ ) {
49
+ const baseNames = rows.map(row => makeId(row, taxonomyInfo))
50
+ const counts = new Map<string, number>()
51
+ for (const name of baseNames) {
52
+ counts.set(name, (counts.get(name) ?? 0) + 1)
53
+ }
54
+
55
+ const used = new Set<string>()
56
+ return baseNames.map((base, i) => {
57
+ let name =
58
+ counts.get(base)! > 1 ? `${base}_${rows[i]!.range ?? i + 1}` : base
59
+ while (used.has(name)) {
60
+ name = `${name}_${i + 1}`
61
+ }
62
+ used.add(name)
63
+ return name
64
+ })
65
+ }
66
+
67
+ /**
68
+ * The phmmer alignment as the view receives it: aligned FASTA whose first row
69
+ * is the query, plus the per-row metadata keyed by the same names, which are
70
+ * also what the tree's leaves are labelled with.
71
+ */
72
+ export function buildPhmmerMsa({
73
+ rows,
74
+ queryRow,
75
+ taxonomyInfo,
76
+ querySeqName = 'QUERY',
77
+ }: {
78
+ rows: PhmmerRow[]
79
+ queryRow: string
80
+ taxonomyInfo: Map<number, TaxonomyInfo>
81
+ querySeqName?: string
82
+ }) {
83
+ const treeMetadata: Record<string, Record<string, string>> = {}
84
+ const rowNames = makeRowNames(rows, taxonomyInfo)
85
+ const sequences = rows.map((row, i) => {
86
+ const rowName = rowNames[i]!
87
+ treeMetadata[rowName] = buildRowMetadata(row, taxonomyInfo)
88
+ return `>${rowName}\n${row.aligned}`
89
+ })
90
+
91
+ return {
92
+ msa: [`>${querySeqName}\n${queryRow}`, ...sequences].join('\n'),
93
+ treeMetadata,
94
+ }
95
+ }