p2s_mapper 1.0.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 (172) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -0
  3. package/dist/alignmentQuality.d.ts +58 -0
  4. package/dist/alignmentQuality.js +108 -0
  5. package/dist/alignmentQuality.js.map +1 -0
  6. package/dist/alphaFoldModels.d.ts +29 -0
  7. package/dist/alphaFoldModels.js +60 -0
  8. package/dist/alphaFoldModels.js.map +1 -0
  9. package/dist/caCoordsToPdb.d.ts +18 -0
  10. package/dist/caCoordsToPdb.js +95 -0
  11. package/dist/caCoordsToPdb.js.map +1 -0
  12. package/dist/chooseMappedEntity.d.ts +55 -0
  13. package/dist/chooseMappedEntity.js +140 -0
  14. package/dist/chooseMappedEntity.js.map +1 -0
  15. package/dist/coordinates.d.ts +66 -0
  16. package/dist/coordinates.js +66 -0
  17. package/dist/coordinates.js.map +1 -0
  18. package/dist/dbIds.d.ts +16 -0
  19. package/dist/dbIds.js +42 -0
  20. package/dist/dbIds.js.map +1 -0
  21. package/dist/extractPerResidueConfidence.d.ts +49 -0
  22. package/dist/extractPerResidueConfidence.js +42 -0
  23. package/dist/extractPerResidueConfidence.js.map +1 -0
  24. package/dist/extractStructureSequences.d.ts +142 -0
  25. package/dist/extractStructureSequences.js +217 -0
  26. package/dist/extractStructureSequences.js.map +1 -0
  27. package/dist/fetchUtils.d.ts +34 -0
  28. package/dist/fetchUtils.js +91 -0
  29. package/dist/fetchUtils.js.map +1 -0
  30. package/dist/index.d.ts +23 -0
  31. package/dist/index.js +46 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/invertMap.d.ts +2 -0
  34. package/dist/invertMap.js +8 -0
  35. package/dist/invertMap.js.map +1 -0
  36. package/dist/isoformRanking.d.ts +47 -0
  37. package/dist/isoformRanking.js +91 -0
  38. package/dist/isoformRanking.js.map +1 -0
  39. package/dist/mappings.d.ts +58 -0
  40. package/dist/mappings.js +193 -0
  41. package/dist/mappings.js.map +1 -0
  42. package/dist/package.json +1 -0
  43. package/dist/pairwiseAlignment.d.ts +35 -0
  44. package/dist/pairwiseAlignment.js +337 -0
  45. package/dist/pairwiseAlignment.js.map +1 -0
  46. package/dist/pdbUniProtMapping.d.ts +75 -0
  47. package/dist/pdbUniProtMapping.js +241 -0
  48. package/dist/pdbUniProtMapping.js.map +1 -0
  49. package/dist/pdbeBestStructures.d.ts +23 -0
  50. package/dist/pdbeBestStructures.js +79 -0
  51. package/dist/pdbeBestStructures.js.map +1 -0
  52. package/dist/pdbeEntryMolecules.d.ts +7 -0
  53. package/dist/pdbeEntryMolecules.js +39 -0
  54. package/dist/pdbeEntryMolecules.js.map +1 -0
  55. package/dist/siftsNumbering.d.ts +23 -0
  56. package/dist/siftsNumbering.js +44 -0
  57. package/dist/siftsNumbering.js.map +1 -0
  58. package/dist/stripStopCodon.d.ts +15 -0
  59. package/dist/stripStopCodon.js +24 -0
  60. package/dist/stripStopCodon.js.map +1 -0
  61. package/dist/structureFormat.d.ts +40 -0
  62. package/dist/structureFormat.js +54 -0
  63. package/dist/structureFormat.js.map +1 -0
  64. package/dist/structureSources.d.ts +28 -0
  65. package/dist/structureSources.js +76 -0
  66. package/dist/structureSources.js.map +1 -0
  67. package/dist/structureUrls.d.ts +54 -0
  68. package/dist/structureUrls.js +147 -0
  69. package/dist/structureUrls.js.map +1 -0
  70. package/dist/types.d.ts +14 -0
  71. package/dist/types.js +27 -0
  72. package/dist/types.js.map +1 -0
  73. package/dist/uniProtSearch.d.ts +32 -0
  74. package/dist/uniProtSearch.js +114 -0
  75. package/dist/uniProtSearch.js.map +1 -0
  76. package/esm/alignmentQuality.d.ts +58 -0
  77. package/esm/alignmentQuality.js +100 -0
  78. package/esm/alignmentQuality.js.map +1 -0
  79. package/esm/alphaFoldModels.d.ts +29 -0
  80. package/esm/alphaFoldModels.js +55 -0
  81. package/esm/alphaFoldModels.js.map +1 -0
  82. package/esm/caCoordsToPdb.d.ts +18 -0
  83. package/esm/caCoordsToPdb.js +91 -0
  84. package/esm/caCoordsToPdb.js.map +1 -0
  85. package/esm/chooseMappedEntity.d.ts +55 -0
  86. package/esm/chooseMappedEntity.js +134 -0
  87. package/esm/chooseMappedEntity.js.map +1 -0
  88. package/esm/coordinates.d.ts +66 -0
  89. package/esm/coordinates.js +58 -0
  90. package/esm/coordinates.js.map +1 -0
  91. package/esm/dbIds.d.ts +16 -0
  92. package/esm/dbIds.js +35 -0
  93. package/esm/dbIds.js.map +1 -0
  94. package/esm/extractPerResidueConfidence.d.ts +49 -0
  95. package/esm/extractPerResidueConfidence.js +38 -0
  96. package/esm/extractPerResidueConfidence.js.map +1 -0
  97. package/esm/extractStructureSequences.d.ts +142 -0
  98. package/esm/extractStructureSequences.js +205 -0
  99. package/esm/extractStructureSequences.js.map +1 -0
  100. package/esm/fetchUtils.d.ts +34 -0
  101. package/esm/fetchUtils.js +82 -0
  102. package/esm/fetchUtils.js.map +1 -0
  103. package/esm/index.d.ts +23 -0
  104. package/esm/index.js +30 -0
  105. package/esm/index.js.map +1 -0
  106. package/esm/invertMap.d.ts +2 -0
  107. package/esm/invertMap.js +5 -0
  108. package/esm/invertMap.js.map +1 -0
  109. package/esm/isoformRanking.d.ts +47 -0
  110. package/esm/isoformRanking.js +86 -0
  111. package/esm/isoformRanking.js.map +1 -0
  112. package/esm/mappings.d.ts +58 -0
  113. package/esm/mappings.js +179 -0
  114. package/esm/mappings.js.map +1 -0
  115. package/esm/pairwiseAlignment.d.ts +35 -0
  116. package/esm/pairwiseAlignment.js +328 -0
  117. package/esm/pairwiseAlignment.js.map +1 -0
  118. package/esm/pdbUniProtMapping.d.ts +75 -0
  119. package/esm/pdbUniProtMapping.js +231 -0
  120. package/esm/pdbUniProtMapping.js.map +1 -0
  121. package/esm/pdbeBestStructures.d.ts +23 -0
  122. package/esm/pdbeBestStructures.js +73 -0
  123. package/esm/pdbeBestStructures.js.map +1 -0
  124. package/esm/pdbeEntryMolecules.d.ts +7 -0
  125. package/esm/pdbeEntryMolecules.js +35 -0
  126. package/esm/pdbeEntryMolecules.js.map +1 -0
  127. package/esm/siftsNumbering.d.ts +23 -0
  128. package/esm/siftsNumbering.js +40 -0
  129. package/esm/siftsNumbering.js.map +1 -0
  130. package/esm/stripStopCodon.d.ts +15 -0
  131. package/esm/stripStopCodon.js +20 -0
  132. package/esm/stripStopCodon.js.map +1 -0
  133. package/esm/structureFormat.d.ts +40 -0
  134. package/esm/structureFormat.js +48 -0
  135. package/esm/structureFormat.js.map +1 -0
  136. package/esm/structureSources.d.ts +28 -0
  137. package/esm/structureSources.js +71 -0
  138. package/esm/structureSources.js.map +1 -0
  139. package/esm/structureUrls.d.ts +54 -0
  140. package/esm/structureUrls.js +134 -0
  141. package/esm/structureUrls.js.map +1 -0
  142. package/esm/types.d.ts +14 -0
  143. package/esm/types.js +23 -0
  144. package/esm/types.js.map +1 -0
  145. package/esm/uniProtSearch.d.ts +32 -0
  146. package/esm/uniProtSearch.js +110 -0
  147. package/esm/uniProtSearch.js.map +1 -0
  148. package/package.json +63 -0
  149. package/src/alignmentQuality.ts +132 -0
  150. package/src/alphaFoldModels.ts +91 -0
  151. package/src/caCoordsToPdb.ts +111 -0
  152. package/src/chooseMappedEntity.ts +198 -0
  153. package/src/coordinates.ts +119 -0
  154. package/src/dbIds.ts +46 -0
  155. package/src/extractPerResidueConfidence.ts +88 -0
  156. package/src/extractStructureSequences.ts +342 -0
  157. package/src/fetchUtils.ts +110 -0
  158. package/src/index.ts +34 -0
  159. package/src/invertMap.ts +4 -0
  160. package/src/isoformRanking.ts +139 -0
  161. package/src/mappings.ts +230 -0
  162. package/src/pairwiseAlignment.ts +423 -0
  163. package/src/pdbUniProtMapping.ts +320 -0
  164. package/src/pdbeBestStructures.ts +94 -0
  165. package/src/pdbeEntryMolecules.ts +37 -0
  166. package/src/siftsNumbering.ts +61 -0
  167. package/src/stripStopCodon.ts +20 -0
  168. package/src/structureFormat.ts +70 -0
  169. package/src/structureSources.ts +101 -0
  170. package/src/structureUrls.ts +171 -0
  171. package/src/types.ts +30 -0
  172. package/src/uniProtSearch.ts +212 -0
@@ -0,0 +1,230 @@
1
+ import { getCodonRanges } from 'g2p_mapper'
2
+
3
+ export interface AlignmentRow {
4
+ id: string
5
+ seq: string
6
+ }
7
+
8
+ /**
9
+ * A two-row alignment whose ROW ORDER IS A CONTRACT: row 0 is the transcript,
10
+ * row 1 is the structure. Every producer honors it — runLocalAlignment is
11
+ * always called as (transcript, structure), and the manual-import dialog tells
12
+ * the user to paste them in that order — and every consumer depends on it, but
13
+ * nothing in the type can enforce it, and swapping the two silently inverts
14
+ * every coordinate map. Read the rows through the accessors below rather than
15
+ * indexing, so a call site states which sequence it means.
16
+ */
17
+ export interface PairwiseAlignment {
18
+ consensus: string
19
+ alns: readonly [AlignmentRow, AlignmentRow]
20
+ }
21
+
22
+ export const transcriptAlignedSeq = (pa: PairwiseAlignment) => pa.alns[0].seq
23
+ export const structureAlignedSeq = (pa: PairwiseAlignment) => pa.alns[1].seq
24
+
25
+ /** Number of columns, gaps included. Both rows have this length by definition;
26
+ * `pairwiseAlignmentProblem` is what guarantees it. */
27
+ export const alignmentLength = (pa: PairwiseAlignment) =>
28
+ transcriptAlignedSeq(pa).length
29
+
30
+ /**
31
+ * Why a pairwise alignment can't be used, or undefined if it's usable. The two
32
+ * rows must be the same non-zero length — every coordinate map walks them in
33
+ * lockstep, so a ragged pair would map positions to nonsense. Callers that
34
+ * accept alignments from outside (the manual-import dialog) check this and
35
+ * report it; the internal map builders assert on it.
36
+ */
37
+ export function pairwiseAlignmentProblem(pa: PairwiseAlignment) {
38
+ const transcript = transcriptAlignedSeq(pa)
39
+ const structure = structureAlignedSeq(pa)
40
+ if (transcript.length === 0 || structure.length === 0) {
41
+ return 'The aligned sequences must not be empty'
42
+ }
43
+ if (transcript.length !== structure.length) {
44
+ return `The two aligned sequences must be the same length (got ${transcript.length} and ${structure.length})`
45
+ }
46
+ return undefined
47
+ }
48
+
49
+ const ungapped = (row: string) => row.replaceAll('-', '').toUpperCase()
50
+
51
+ /**
52
+ * Why an externally supplied alignment does not describe these two sequences,
53
+ * or undefined if it does. The coordinate maps index the transcript and the
54
+ * structure by counting residues along each row, so a row whose residues are
55
+ * not the sequence being mapped (an alignment made against UniProt canonical
56
+ * for a different isoform, or against another chain) shifts every position
57
+ * after the first difference without any map noticing.
58
+ */
59
+ export function pairwiseAlignmentSequenceProblem(
60
+ pa: PairwiseAlignment,
61
+ transcript: string,
62
+ structure: string,
63
+ ) {
64
+ const t = ungapped(transcriptAlignedSeq(pa))
65
+ const s = ungapped(structureAlignedSeq(pa))
66
+ if (t !== transcript.toUpperCase()) {
67
+ return `The first sequence is not this transcript's translation (${t.length} vs ${transcript.length} residues)`
68
+ }
69
+ if (s !== structure.toUpperCase()) {
70
+ return `The second sequence is not the mapped chain's sequence (${s.length} vs ${structure.length} residues)`
71
+ }
72
+ return undefined
73
+ }
74
+
75
+ export function structureSeqVsTranscriptSeqMap(
76
+ pairwiseAlignment: PairwiseAlignment,
77
+ ) {
78
+ const problem = pairwiseAlignmentProblem(pairwiseAlignment)
79
+ if (problem) {
80
+ throw new Error(problem)
81
+ }
82
+ const structureSeq = structureAlignedSeq(pairwiseAlignment)
83
+ const transcriptSeq = transcriptAlignedSeq(pairwiseAlignment)
84
+
85
+ let j = 0
86
+ let k = 0
87
+ const structureSeqToTranscriptSeqPosition: Record<number, number> = {}
88
+ const transcriptSeqToStructureSeqPosition: Record<number, number> = {}
89
+
90
+ for (let i = 0; i < structureSeq.length; i++) {
91
+ const inStructure = structureSeq[i] !== '-'
92
+ const inTranscript = transcriptSeq[i] !== '-'
93
+ if (inStructure && inTranscript) {
94
+ structureSeqToTranscriptSeqPosition[j] = k
95
+ transcriptSeqToStructureSeqPosition[k] = j
96
+ }
97
+ if (inStructure) {
98
+ j++
99
+ }
100
+ if (inTranscript) {
101
+ k++
102
+ }
103
+ }
104
+
105
+ return {
106
+ structureSeqToTranscriptSeqPosition,
107
+ transcriptSeqToStructureSeqPosition,
108
+ }
109
+ }
110
+
111
+ /** Each 0-based structure position opposite a transcript residue, and whether
112
+ * the two residues are identical. */
113
+ export function mappedStructureIdentity(pa: PairwiseAlignment) {
114
+ const transcript = transcriptAlignedSeq(pa)
115
+ const structure = structureAlignedSeq(pa)
116
+ const mapped = new Map<number, boolean>()
117
+ for (let i = 0, j = 0; i < structure.length; i++) {
118
+ if (structure[i] !== '-') {
119
+ if (transcript[i] !== '-') {
120
+ mapped.set(
121
+ j,
122
+ transcript[i]!.toUpperCase() === structure[i]!.toUpperCase(),
123
+ )
124
+ }
125
+ j++
126
+ }
127
+ }
128
+ return mapped
129
+ }
130
+
131
+ /**
132
+ * The alignment with each listed structure position taken out of its column,
133
+ * so both rows still spell their sequences and those residues map to nothing.
134
+ * An unmapped region, including the gap columns inside it, becomes its
135
+ * transcript residues against gaps, then gaps against its structure residues,
136
+ * rather than alternating column by column. Returns `pa` itself when nothing
137
+ * changes.
138
+ */
139
+ export function unmapStructurePositions(
140
+ pa: PairwiseAlignment,
141
+ positions: ReadonlySet<number>,
142
+ ): PairwiseAlignment {
143
+ const transcript = transcriptAlignedSeq(pa)
144
+ const structure = structureAlignedSeq(pa)
145
+ const t: string[] = []
146
+ const s: string[] = []
147
+ const c: string[] = []
148
+ const runT: string[] = []
149
+ const runS: string[] = []
150
+ const flushRun = () => {
151
+ t.push(...runT, ...runS.map(() => '-'))
152
+ s.push(...runT.map(() => '-'), ...runS)
153
+ c.push(...runT.map(() => ' '), ...runS.map(() => ' '))
154
+ runT.length = 0
155
+ runS.length = 0
156
+ }
157
+ let j = 0
158
+ let changed = false
159
+ for (let i = 0; i < structure.length; i++) {
160
+ const inStructure = structure[i] !== '-'
161
+ const inTranscript = transcript[i] !== '-'
162
+ if (inStructure && inTranscript && positions.has(j)) {
163
+ runT.push(transcript[i]!)
164
+ runS.push(structure[i]!)
165
+ changed = true
166
+ } else if (runT.length > 0 && !(inStructure && inTranscript)) {
167
+ if (inTranscript) {
168
+ runT.push(transcript[i]!)
169
+ }
170
+ if (inStructure) {
171
+ runS.push(structure[i]!)
172
+ }
173
+ } else {
174
+ flushRun()
175
+ t.push(transcript[i]!)
176
+ s.push(structure[i]!)
177
+ c.push(pa.consensus[i] ?? ' ')
178
+ }
179
+ if (inStructure) {
180
+ j++
181
+ }
182
+ }
183
+ flushRun()
184
+ return changed
185
+ ? {
186
+ consensus: c.join(''),
187
+ alns: [
188
+ { ...pa.alns[0], seq: t.join('') },
189
+ { ...pa.alns[1], seq: s.join('') },
190
+ ],
191
+ }
192
+ : pa
193
+ }
194
+
195
+ function seqPositionToAlignmentMap(seq: string) {
196
+ const map: Record<number, number> = {}
197
+ for (let i = 0, j = 0; i < seq.length; i++) {
198
+ if (seq[i] !== '-') {
199
+ map[j] = i
200
+ j++
201
+ }
202
+ }
203
+ return map
204
+ }
205
+
206
+ export function structurePositionToAlignmentMap(
207
+ pairwiseAlignment: PairwiseAlignment,
208
+ ) {
209
+ return seqPositionToAlignmentMap(structureAlignedSeq(pairwiseAlignment))
210
+ }
211
+
212
+ export function transcriptPositionToAlignmentMap(
213
+ pairwiseAlignment: PairwiseAlignment,
214
+ ) {
215
+ return seqPositionToAlignmentMap(transcriptAlignedSeq(pairwiseAlignment))
216
+ }
217
+
218
+ // Enclosing 0-based half-open [start, end) genome span for a codon. getCodonRanges
219
+ // returns the codon's separate genomic pieces (multiple when it straddles an
220
+ // exon/intron boundary); this collapses them to the outer span for navigation
221
+ // and highlighting. undefined when the protein position isn't mapped.
222
+ export function codonGenomeSpan(
223
+ p2gCodon: Record<number, number[]>,
224
+ proteinPos: number,
225
+ ): readonly [number, number] | undefined {
226
+ const ranges = getCodonRanges(p2gCodon, proteinPos)
227
+ return ranges && ranges.length > 0
228
+ ? [Math.min(...ranges.map(r => r[0])), Math.max(...ranges.map(r => r[1]))]
229
+ : undefined
230
+ }
@@ -0,0 +1,423 @@
1
+ import type { PairwiseAlignment } from './mappings.ts'
2
+ import type { AlignmentAlgorithm } from './types.ts'
3
+
4
+ // BLOSUM62 scoring matrix — standard 20 amino acids + B, Z, X, *
5
+ // prettier-ignore
6
+ const AMINO_ACIDS = ['A','R','N','D','C','Q','E','G','H','I','L','K','M','F','P','S','T','W','Y','V','B','Z','X','*']
7
+ // prettier-ignore
8
+ const BLOSUM62_RAW = [
9
+ // A R N D C Q E G H I L K M F P S T W Y V B Z X *
10
+ [ 4, -1, -2, -2, 0, -1, -1, 0, -2, -1, -1, -1, -1, -2, -1, 1, 0, -3, -2, 0, -2, -1, 0, -4], // A
11
+ [-1, 5, 0, -2, -3, 1, 0, -2, 0, -3, -2, 2, -1, -3, -2, -1, -1, -3, -2, -3, -1, 0, -1, -4], // R
12
+ [-2, 0, 6, 1, -3, 0, 0, 0, 1, -3, -3, 0, -2, -3, -2, 1, 0, -4, -2, -3, 3, 0, -1, -4], // N
13
+ [-2, -2, 1, 6, -3, 0, 2, -1, -1, -3, -4, -1, -3, -3, -1, 0, -1, -4, -3, -3, 4, 1, -1, -4], // D
14
+ [ 0, -3, -3, -3, 9, -3, -4, -3, -3, -1, -1, -3, -1, -2, -3, -1, -1, -2, -2, -1, -3, -3, -2, -4], // C
15
+ [-1, 1, 0, 0, -3, 5, 2, -2, 0, -3, -2, 1, 0, -3, -1, 0, -1, -2, -1, -2, 0, 3, -1, -4], // Q
16
+ [-1, 0, 0, 2, -4, 2, 5, -2, 0, -3, -3, 1, -2, -3, -1, 0, -1, -3, -2, -2, 1, 4, -1, -4], // E
17
+ [ 0, -2, 0, -1, -3, -2, -2, 6, -2, -4, -4, -2, -3, -3, -2, 0, -2, -2, -3, -3, -1, -2, -1, -4], // G
18
+ [-2, 0, 1, -1, -3, 0, 0, -2, 8, -3, -3, -1, -2, -1, -2, -1, -2, -2, 2, -3, 0, 0, -1, -4], // H
19
+ [-1, -3, -3, -3, -1, -3, -3, -4, -3, 4, 2, -3, 1, 0, -3, -2, -1, -3, -1, 3, -3, -3, -1, -4], // I
20
+ [-1, -2, -3, -4, -1, -2, -3, -4, -3, 2, 4, -2, 2, 0, -3, -2, -1, -2, -1, 1, -4, -3, -1, -4], // L
21
+ [-1, 2, 0, -1, -3, 1, 1, -2, -1, -3, -2, 5, -1, -3, -1, 0, -1, -3, -2, -2, 0, 1, -1, -4], // K
22
+ [-1, -1, -2, -3, -1, 0, -2, -3, -2, 1, 2, -1, 5, 0, -2, -1, -1, -1, -1, 1, -3, -1, -1, -4], // M
23
+ [-2, -3, -3, -3, -2, -3, -3, -3, -1, 0, 0, -3, 0, 6, -4, -2, -2, 1, 3, -1, -3, -3, -1, -4], // F
24
+ [-1, -2, -2, -1, -3, -1, -1, -2, -2, -3, -3, -1, -2, -4, 7, -1, -1, -4, -3, -2, -2, -1, -2, -4], // P
25
+ [ 1, -1, 1, 0, -1, 0, 0, 0, -1, -2, -2, 0, -1, -2, -1, 4, 1, -3, -2, -2, 0, 0, 0, -4], // S
26
+ [ 0, -1, 0, -1, -1, -1, -1, -2, -2, -1, -1, -1, -1, -2, -1, 1, 5, -2, -2, 0, -1, -1, 0, -4], // T
27
+ [-3, -3, -4, -4, -2, -2, -3, -2, -2, -3, -2, -3, -1, 1, -4, -3, -2, 11, 2, -3, -4, -3, -2, -4], // W
28
+ [-2, -2, -2, -3, -2, -1, -2, -3, 2, -1, -1, -2, -1, 3, -3, -2, -2, 2, 7, -1, -3, -2, -1, -4], // Y
29
+ [ 0, -3, -3, -3, -1, -2, -2, -3, -3, 3, 1, -2, 1, -1, -2, -2, 0, -3, -1, 4, -3, -2, -1, -4], // V
30
+ [-2, -1, 3, 4, -3, 0, 1, -1, 0, -3, -4, 0, -3, -3, -2, 0, -1, -4, -3, -3, 4, 1, -1, -4], // B
31
+ [-1, 0, 0, 1, -3, 3, 4, -2, 0, -3, -3, 1, -1, -3, -1, 0, -1, -3, -2, -2, 1, 4, -1, -4], // Z
32
+ [ 0, -1, -1, -1, -2, -1, -1, -1, -1, -1, -1, -1, -1, -1, -2, 0, 0, -2, -1, -1, -1, -1, -1, -4], // X
33
+ [-4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, -4, 1], // *
34
+ ]
35
+
36
+ // Score of a residue pair that isn't in the matrix at all.
37
+ const UNKNOWN_PAIR_SCORE = -4
38
+
39
+ // The DP reads the matrix once per cell, so both the residue→row lookup and the
40
+ // matrix itself are flat typed arrays keyed by char code: charCodeToRow maps
41
+ // 'a'/'A' to the same row, and unknown residues to -1 (scored as unknown).
42
+ const N_AA = AMINO_ACIDS.length
43
+ const charCodeToRow = new Int8Array(128).fill(-1)
44
+ for (let i = 0; i < N_AA; i++) {
45
+ const aa = AMINO_ACIDS[i]!
46
+ charCodeToRow[aa.charCodeAt(0)] = i
47
+ charCodeToRow[aa.toLowerCase().charCodeAt(0)] = i
48
+ }
49
+ const BLOSUM62 = new Int8Array(N_AA * N_AA)
50
+ for (let i = 0; i < N_AA; i++) {
51
+ for (let j = 0; j < N_AA; j++) {
52
+ BLOSUM62[i * N_AA + j] = BLOSUM62_RAW[i]![j]!
53
+ }
54
+ }
55
+
56
+ const rowOf = (c: string) => {
57
+ const code = c.charCodeAt(0)
58
+ return code < 128 ? charCodeToRow[code]! : -1
59
+ }
60
+
61
+ function getScore(a: string, b: string) {
62
+ const i = rowOf(a)
63
+ const j = rowOf(b)
64
+ return i < 0 || j < 0 ? UNKNOWN_PAIR_SCORE : BLOSUM62[i * N_AA + j]!
65
+ }
66
+
67
+ // Default gap penalties (matching EMBOSS defaults)
68
+ const GAP_OPEN = -10
69
+ const GAP_EXTEND = -0.5
70
+
71
+ interface AlignmentResult {
72
+ alignedSeq1: string
73
+ alignedSeq2: string
74
+ score: number
75
+ }
76
+
77
+ /**
78
+ * Ceiling on the dynamic-programming table, in cells. Both algorithms are
79
+ * O(m*n) time and hold one byte of traceback per cell, so this bounds the
80
+ * alignment at ~40 MB and roughly a second of main-thread work. Without it a
81
+ * titin-sized transcript or a long nucleic-acid chain locks up or OOMs the tab.
82
+ * Callers that have several candidates to align should check `alignmentTooLarge`
83
+ * and skip the oversized ones rather than let this throw.
84
+ */
85
+ export const MAX_ALIGNMENT_CELLS = 40_000_000
86
+
87
+ export function alignmentTooLarge(len1: number, len2: number) {
88
+ return (len1 + 1) * (len2 + 1) > MAX_ALIGNMENT_CELLS
89
+ }
90
+
91
+ function assertAlignable(len1: number, len2: number) {
92
+ if (alignmentTooLarge(len1, len2)) {
93
+ throw new Error(
94
+ `Sequences too long to align (${len1} x ${len2} residues exceeds the ${MAX_ALIGNMENT_CELLS} cell limit)`,
95
+ )
96
+ }
97
+ }
98
+
99
+ // Traceback states. Each cell stores, for each of the three matrices, which
100
+ // matrix its best predecessor came from — 2 bits per matrix, packed into one
101
+ // byte. Storing pointers instead of re-deriving them by comparing scores keeps
102
+ // traceback exact under Float32 rounding and lets the score matrices collapse
103
+ // to two rows.
104
+ const FROM_M = 0
105
+ const FROM_IX = 1
106
+ const FROM_IY = 2
107
+ // Smith-Waterman only: the local alignment restarts here, so traceback stops.
108
+ const FROM_NONE = 3
109
+
110
+ const M_SHIFT = 0
111
+ const IX_SHIFT = 2
112
+ const IY_SHIFT = 4
113
+
114
+ type Matrix = typeof FROM_M | typeof FROM_IX | typeof FROM_IY
115
+ type Pointer = Matrix | typeof FROM_NONE
116
+
117
+ const readPointer = (trace: Uint8Array, at: number, shift: number): Pointer =>
118
+ ((trace[at]! >> shift) & 0b11) as Pointer
119
+
120
+ interface FilledMatrices {
121
+ trace: Uint8Array
122
+ width: number
123
+ score: number
124
+ /** cell the traceback starts from: (m, n) for global, the best cell for local */
125
+ startI: number
126
+ startJ: number
127
+ startMatrix: Matrix
128
+ }
129
+
130
+ /**
131
+ * Fill the affine-gap DP tables for both algorithms; `local` switches
132
+ * Needleman-Wunsch (global, ends at the corner) to Smith-Waterman (restarts at
133
+ * 0, ends at the best-scoring cell). Only the previous and current score rows
134
+ * are kept — the full traceback lives in the packed `trace` byte array.
135
+ */
136
+ function fillMatrices(
137
+ seq1: string,
138
+ seq2: string,
139
+ gapOpen: number,
140
+ gapExtend: number,
141
+ local: boolean,
142
+ ): FilledMatrices {
143
+ const m = seq1.length
144
+ const n = seq2.length
145
+ assertAlignable(m, n)
146
+ const width = n + 1
147
+ const trace = new Uint8Array((m + 1) * width)
148
+
149
+ // Row 0. Global: only M[0][0] is reachable and Iy[0][j] is the leading-gap
150
+ // base case. Local: M starts at 0 everywhere, gaps are unreachable.
151
+ let prevM = new Float32Array(width)
152
+ let prevIx = new Float32Array(width).fill(-Infinity)
153
+ let prevIy = new Float32Array(width).fill(-Infinity)
154
+ let curM = new Float32Array(width)
155
+ let curIx = new Float32Array(width)
156
+ let curIy = new Float32Array(width)
157
+ if (!local) {
158
+ prevM.fill(-Infinity)
159
+ prevM[0] = 0
160
+ for (let j = 1; j <= n; j++) {
161
+ prevIy[j] = gapOpen + (j - 1) * gapExtend
162
+ }
163
+ }
164
+
165
+ let bestScore = local ? 0 : -Infinity
166
+ let bestI = 0
167
+ let bestJ = 0
168
+ let bestMatrix: Matrix = FROM_M
169
+
170
+ // Resolve seq2 to matrix rows once instead of once per cell.
171
+ const cols = new Int8Array(n)
172
+ for (let j = 0; j < n; j++) {
173
+ cols[j] = rowOf(seq2[j]!)
174
+ }
175
+
176
+ for (let i = 1; i <= m; i++) {
177
+ // Column 0, mirroring row 0.
178
+ curM[0] = local ? 0 : -Infinity
179
+ curIx[0] = local ? -Infinity : gapOpen + (i - 1) * gapExtend
180
+ curIy[0] = -Infinity
181
+ const rowBase = i * width
182
+ const scoreRow = rowOf(seq1[i - 1]!)
183
+ const scoreBase = scoreRow * N_AA
184
+
185
+ for (let j = 1; j <= n; j++) {
186
+ const col = cols[j - 1]!
187
+ const matchScore =
188
+ scoreRow < 0 || col < 0
189
+ ? UNKNOWN_PAIR_SCORE
190
+ : BLOSUM62[scoreBase + col]!
191
+
192
+ const dM = prevM[j - 1]!
193
+ const dIx = prevIx[j - 1]!
194
+ const dIy = prevIy[j - 1]!
195
+ // Ties resolve M > Ix > Iy, matching the previous implementation.
196
+ const bestDiag = Math.max(dM, dIx, dIy)
197
+ const diagFrom =
198
+ bestDiag === dM ? FROM_M : bestDiag === dIx ? FROM_IX : FROM_IY
199
+ const mScore = bestDiag + matchScore
200
+ const restart = local && mScore <= 0
201
+ curM[j] = restart ? 0 : mScore
202
+
203
+ const openIx = prevM[j]! + gapOpen
204
+ const extendIx = prevIx[j]! + gapExtend
205
+ curIx[j] = Math.max(openIx, extendIx)
206
+
207
+ const openIy = curM[j - 1]! + gapOpen
208
+ const extendIy = curIy[j - 1]! + gapExtend
209
+ curIy[j] = Math.max(openIy, extendIy)
210
+
211
+ trace[rowBase + j] =
212
+ ((restart ? FROM_NONE : diagFrom) << M_SHIFT) |
213
+ ((openIx >= extendIx ? FROM_M : FROM_IX) << IX_SHIFT) |
214
+ ((openIy >= extendIy ? FROM_M : FROM_IY) << IY_SHIFT)
215
+
216
+ if (local) {
217
+ const cellMax = Math.max(curM[j]!, curIx[j]!, curIy[j]!)
218
+ if (cellMax > bestScore) {
219
+ bestScore = cellMax
220
+ bestI = i
221
+ bestJ = j
222
+ bestMatrix =
223
+ cellMax === curM[j]!
224
+ ? FROM_M
225
+ : cellMax === curIx[j]!
226
+ ? FROM_IX
227
+ : FROM_IY
228
+ }
229
+ }
230
+ }
231
+
232
+ ;[prevM, curM] = [curM, prevM]
233
+ ;[prevIx, curIx] = [curIx, prevIx]
234
+ ;[prevIy, curIy] = [curIy, prevIy]
235
+ }
236
+
237
+ if (local) {
238
+ return {
239
+ trace,
240
+ width,
241
+ score: bestScore,
242
+ startI: bestI,
243
+ startJ: bestJ,
244
+ startMatrix: bestMatrix,
245
+ }
246
+ }
247
+ // prev* now holds row m.
248
+ const finalM = prevM[n]!
249
+ const finalIx = prevIx[n]!
250
+ const finalIy = prevIy[n]!
251
+ const score = Math.max(finalM, finalIx, finalIy)
252
+ return {
253
+ trace,
254
+ width,
255
+ score,
256
+ startI: m,
257
+ startJ: n,
258
+ startMatrix:
259
+ score === finalM ? FROM_M : score === finalIx ? FROM_IX : FROM_IY,
260
+ }
261
+ }
262
+
263
+ /**
264
+ * Walk the packed traceback back to an edge. Rows are built back-to-front into
265
+ * arrays and reversed once, rather than prepending onto a string per residue
266
+ * (which is quadratic in the alignment length).
267
+ */
268
+ function traceback(
269
+ seq1: string,
270
+ seq2: string,
271
+ filled: FilledMatrices,
272
+ ): AlignmentResult {
273
+ const { trace, width, startI, startJ, score } = filled
274
+ // Reversed: pushing here is prepending to the final string.
275
+ const out1: string[] = []
276
+ const out2: string[] = []
277
+
278
+ // Residues past the aligned region (the whole sequences when the local
279
+ // alignment is empty) hang off the end as gaps.
280
+ for (let k = seq1.length; k > startI; k--) {
281
+ out1.push(seq1[k - 1]!)
282
+ out2.push('-')
283
+ }
284
+ for (let k = seq2.length; k > startJ; k--) {
285
+ out1.push('-')
286
+ out2.push(seq2[k - 1]!)
287
+ }
288
+
289
+ let i = startI
290
+ let j = startJ
291
+ let matrix: Pointer = filled.startMatrix
292
+ while (i > 0 && j > 0) {
293
+ const at = i * width + j
294
+ if (matrix === FROM_M) {
295
+ const next = readPointer(trace, at, M_SHIFT)
296
+ if (next === FROM_NONE) {
297
+ break
298
+ }
299
+ out1.push(seq1[i - 1]!)
300
+ out2.push(seq2[j - 1]!)
301
+ matrix = next
302
+ i--
303
+ j--
304
+ } else if (matrix === FROM_IX) {
305
+ out1.push(seq1[i - 1]!)
306
+ out2.push('-')
307
+ matrix = readPointer(trace, at, IX_SHIFT)
308
+ i--
309
+ } else {
310
+ out1.push('-')
311
+ out2.push(seq2[j - 1]!)
312
+ matrix = readPointer(trace, at, IY_SHIFT)
313
+ j--
314
+ }
315
+ }
316
+
317
+ // Whatever is left runs into an edge, so it can only be gaps.
318
+ while (i > 0) {
319
+ out1.push(seq1[i - 1]!)
320
+ out2.push('-')
321
+ i--
322
+ }
323
+ while (j > 0) {
324
+ out1.push('-')
325
+ out2.push(seq2[j - 1]!)
326
+ j--
327
+ }
328
+
329
+ return {
330
+ alignedSeq1: out1.reverse().join(''),
331
+ alignedSeq2: out2.reverse().join(''),
332
+ score,
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Needleman-Wunsch global alignment algorithm
338
+ * Aligns entire sequences end-to-end
339
+ */
340
+ export function needlemanWunsch(
341
+ seq1: string,
342
+ seq2: string,
343
+ gapOpen = GAP_OPEN,
344
+ gapExtend = GAP_EXTEND,
345
+ ): AlignmentResult {
346
+ return traceback(
347
+ seq1,
348
+ seq2,
349
+ fillMatrices(seq1, seq2, gapOpen, gapExtend, false),
350
+ )
351
+ }
352
+
353
+ /**
354
+ * Smith-Waterman local alignment algorithm
355
+ * Finds the best local alignment between subsequences
356
+ */
357
+ export function smithWaterman(
358
+ seq1: string,
359
+ seq2: string,
360
+ gapOpen = GAP_OPEN,
361
+ gapExtend = GAP_EXTEND,
362
+ ): AlignmentResult {
363
+ return traceback(
364
+ seq1,
365
+ seq2,
366
+ fillMatrices(seq1, seq2, gapOpen, gapExtend, true),
367
+ )
368
+ }
369
+
370
+ function buildConsensus(alignedSeq1: string, alignedSeq2: string) {
371
+ const chars: string[] = []
372
+ for (let i = 0; i < alignedSeq1.length; i++) {
373
+ const a = alignedSeq1[i]!
374
+ const b = alignedSeq2[i]!
375
+ if (a !== '-' && b !== '-' && a.toUpperCase() === b.toUpperCase()) {
376
+ chars.push('|')
377
+ } else if (a !== '-' && b !== '-' && getScore(a, b) > 0) {
378
+ chars.push(':')
379
+ } else {
380
+ chars.push(' ')
381
+ }
382
+ }
383
+ return chars.join('')
384
+ }
385
+
386
+ /** The score of a sequence aligned to itself with no gaps. */
387
+ export function selfScore(seq: string) {
388
+ let score = 0
389
+ for (const c of seq) {
390
+ score += getScore(c, c)
391
+ }
392
+ return score
393
+ }
394
+
395
+ export function scoredAlignment(
396
+ seq1: string,
397
+ seq2: string,
398
+ algorithm: AlignmentAlgorithm,
399
+ ): { alignment: PairwiseAlignment; score: number } {
400
+ const { alignedSeq1, alignedSeq2, score } =
401
+ algorithm === 'smith_waterman'
402
+ ? smithWaterman(seq1, seq2)
403
+ : needlemanWunsch(seq1, seq2)
404
+
405
+ return {
406
+ alignment: {
407
+ consensus: buildConsensus(alignedSeq1, alignedSeq2),
408
+ alns: [
409
+ { id: 'a', seq: alignedSeq1 },
410
+ { id: 'b', seq: alignedSeq2 },
411
+ ],
412
+ },
413
+ score,
414
+ }
415
+ }
416
+
417
+ export function runLocalAlignment(
418
+ seq1: string,
419
+ seq2: string,
420
+ algorithm: AlignmentAlgorithm,
421
+ ): PairwiseAlignment {
422
+ return scoredAlignment(seq1, seq2, algorithm).alignment
423
+ }