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,111 @@
1
+ // 3-letter amino acid codes
2
+ const AA_3LETTER: Record<string, string> = {
3
+ A: 'ALA',
4
+ C: 'CYS',
5
+ D: 'ASP',
6
+ E: 'GLU',
7
+ F: 'PHE',
8
+ G: 'GLY',
9
+ H: 'HIS',
10
+ I: 'ILE',
11
+ K: 'LYS',
12
+ L: 'LEU',
13
+ M: 'MET',
14
+ N: 'ASN',
15
+ P: 'PRO',
16
+ Q: 'GLN',
17
+ R: 'ARG',
18
+ S: 'SER',
19
+ T: 'THR',
20
+ V: 'VAL',
21
+ W: 'TRP',
22
+ Y: 'TYR',
23
+ X: 'UNK', // Unknown
24
+ }
25
+
26
+ function padLeft(str: string, len: number) {
27
+ return str.padStart(len, ' ')
28
+ }
29
+
30
+ function padRight(str: string, len: number) {
31
+ return str.padEnd(len, ' ')
32
+ }
33
+
34
+ function formatCoord(val: number) {
35
+ return val.toFixed(3).padStart(8, ' ')
36
+ }
37
+
38
+ /**
39
+ * Convert Foldseek tCa coordinates and tSeq to PDB format
40
+ * tCa is a comma-separated string of x,y,z triplets
41
+ * tSeq is the amino acid sequence
42
+ */
43
+ export function caCoordsToPdb(
44
+ tCa: string,
45
+ tSeq: string,
46
+ chainId = 'A',
47
+ title?: string,
48
+ ) {
49
+ const coords = tCa.split(',').map(Number)
50
+ const lines: string[] = []
51
+
52
+ // Add header
53
+ if (title) {
54
+ lines.push(`TITLE ${title}`)
55
+ }
56
+ lines.push('REMARK 1 Generated from Foldseek Cα coordinates')
57
+
58
+ let atomNum = 1
59
+ let resNum = 1
60
+
61
+ for (let i = 0; i < coords.length - 2; i += 3) {
62
+ const x = coords[i]
63
+ const y = coords[i + 1]
64
+ const z = coords[i + 2]
65
+ const aa = tSeq[resNum - 1] ?? 'X'
66
+ const resName = AA_3LETTER[aa] ?? 'UNK'
67
+
68
+ if (x === undefined || y === undefined || z === undefined) {
69
+ break
70
+ }
71
+
72
+ // PDB ATOM record format
73
+ // Columns: 1-6 "ATOM ", 7-11 serial, 13-16 name, 17 altLoc, 18-20 resName,
74
+ // 22 chainID, 23-26 resSeq, 27 iCode, 31-38 x, 39-46 y, 47-54 z,
75
+ // 55-60 occupancy, 61-66 tempFactor, 77-78 element
76
+ const line =
77
+ 'ATOM ' +
78
+ padLeft(String(atomNum), 5) +
79
+ ' CA ' +
80
+ padRight(resName, 3) +
81
+ ' ' +
82
+ chainId +
83
+ padLeft(String(resNum), 4) +
84
+ ' ' +
85
+ formatCoord(x) +
86
+ formatCoord(y) +
87
+ formatCoord(z) +
88
+ ' 1.00 0.00 C'
89
+
90
+ lines.push(line)
91
+ atomNum++
92
+ resNum++
93
+ }
94
+
95
+ lines.push('END')
96
+
97
+ return lines.join('\n')
98
+ }
99
+
100
+ /**
101
+ * Whether a Foldseek hit carries tCa data that can be converted to PDB. A type
102
+ * predicate rather than a boolean so a guarded caller can pass `hit.tCa`/
103
+ * `hit.tSeq` straight to caCoordsToPdb without re-asserting they're defined.
104
+ */
105
+ export function hasValidCaCoords<T extends { tCa?: string; tSeq?: string }>(
106
+ hit: T,
107
+ ): hit is T & { tCa: string; tSeq: string } {
108
+ const { tCa, tSeq } = hit
109
+ // Need at least 3 coordinates (x,y,z for one residue) and a sequence
110
+ return !!tCa && !!tSeq && tCa.split(',').length >= 3
111
+ }
@@ -0,0 +1,198 @@
1
+ import {
2
+ alignmentTooLarge,
3
+ scoredAlignment,
4
+ selfScore,
5
+ } from './pairwiseAlignment.ts'
6
+ import { stripStopCodon } from './stripStopCodon.ts'
7
+ import { structureAlignedSeq, transcriptAlignedSeq } from './mappings.ts'
8
+
9
+ import type { PairwiseAlignment } from './mappings.ts'
10
+ import type { AlignmentAlgorithm } from './types.ts'
11
+
12
+ /**
13
+ * Whether a structure interaction (hover/click) should drive genome navigation.
14
+ * Only the transcript's mapped entity may — a hover on any other chain carries
15
+ * that chain's own label_seq_id and would mis-map through the wrong alignment.
16
+ * A structure with no resolved mapping (`mappedEntityId` undefined, e.g. a
17
+ * standalone viewer with no transcript) stays fully interactive.
18
+ */
19
+ export function interactionMatchesMappedEntity(
20
+ entityId: string,
21
+ mappedEntityId: string | undefined,
22
+ ): boolean {
23
+ return mappedEntityId === undefined || entityId === mappedEntityId
24
+ }
25
+
26
+ export interface EntitySelection extends ScoredAlignment {
27
+ /** index into the candidate array of the entity that is the transcript's */
28
+ index: number
29
+ }
30
+
31
+ export interface ScoredAlignment {
32
+ /** alignment of the transcript against the entity (stop codons stripped on
33
+ * both sides, matching the rest of the mapping pipeline) */
34
+ alignment: PairwiseAlignment
35
+ /** identical aligned residues */
36
+ matches: number
37
+ /** identical residues over the shorter sequence, the score that picks the
38
+ * entity; see `explainedFraction` */
39
+ explained: number
40
+ /** the alignment's BLOSUM62 score, gap penalties included */
41
+ score: number
42
+ }
43
+
44
+ /** A candidate entity: its sequence, and whether it is DNA/RNA, which is never
45
+ * the transcript's product however its letters happen to align. */
46
+ export interface EntityCandidate {
47
+ seq: string
48
+ nucleicAcid?: boolean
49
+ }
50
+
51
+ /**
52
+ * Identical residues over the shorter of the two sequences: how much of
53
+ * whichever is smaller, transcript or entity, the other reproduces.
54
+ *
55
+ * A raw match count favours whatever chain is longest, and a complex's partner
56
+ * usually is: on 1H26 the 11-residue p53 peptide matches 11 while CDK2 accrues
57
+ * 58 scattered identities, and on 4ZZJ SIRT1 beats the 7-residue p53 peptide
58
+ * 63 to 6. The Smith-Waterman score is no better there (56 vs 58). Dividing
59
+ * by length asks the question the picker actually has, which chain *is* this
60
+ * gene's product: the peptide scores 0.69 and 0.50, the partners 0.19 and
61
+ * 0.17, and across a ribosome's 55 chains no decoy passes 0.29.
62
+ *
63
+ * Dividing by the *entity's* length alone penalised a fusion construct: a
64
+ * 60-residue target fused to a 370-residue carrier scored 0.14, and a random
65
+ * 10-mer decoy chain with 3 identities beat it a third of the time (measured
66
+ * 2026-09-11). Dividing by the shorter sequence leaves every chain shorter
67
+ * than the transcript scored as before and lets a chain that contains the
68
+ * whole transcript score near 1 however long its tag is. The pseudocount
69
+ * keeps a two-residue fragment that happens to match (a tRNA end in 7K00)
70
+ * from scoring 1.0.
71
+ */
72
+ const EXPLAINED_PSEUDOCOUNT = 5
73
+
74
+ export function explainedFraction(
75
+ matches: number,
76
+ transcriptLength: number,
77
+ entityLength: number,
78
+ ) {
79
+ return (
80
+ matches / (Math.min(transcriptLength, entityLength) + EXPLAINED_PSEUDOCOUNT)
81
+ )
82
+ }
83
+
84
+ function countMatches(pa: PairwiseAlignment) {
85
+ const a = transcriptAlignedSeq(pa)
86
+ const b = structureAlignedSeq(pa)
87
+ let matches = 0
88
+ for (let i = 0; i < a.length; i++) {
89
+ const ca = a[i]
90
+ const cb = b[i]
91
+ if (ca !== '-' && cb !== '-' && ca?.toUpperCase() === cb?.toUpperCase()) {
92
+ matches++
93
+ }
94
+ }
95
+ return matches
96
+ }
97
+
98
+ /**
99
+ * Align one transcript to one entity, stop codons stripped on both sides. An
100
+ * exact match skips the DP entirely; an oversized pair returns undefined rather
101
+ * than locking up the tab. Used both to score every entity of a structure and
102
+ * to honour a user's explicit chain choice.
103
+ */
104
+ export function alignTranscriptToEntity(
105
+ transcript: string,
106
+ entitySeq: string,
107
+ algorithm: AlignmentAlgorithm,
108
+ ): ScoredAlignment | undefined {
109
+ const t = stripStopCodon(transcript)
110
+ const s = stripStopCodon(entitySeq)
111
+ if (!t || !s || alignmentTooLarge(t.length, s.length)) {
112
+ return undefined
113
+ }
114
+ if (s === t) {
115
+ return {
116
+ alignment: {
117
+ consensus: '|'.repeat(t.length),
118
+ alns: [
119
+ { id: 'seq1', seq: t },
120
+ { id: 'seq2', seq: s },
121
+ ],
122
+ },
123
+ matches: t.length,
124
+ explained: explainedFraction(t.length, t.length, s.length),
125
+ score: selfScore(t),
126
+ }
127
+ }
128
+ const { alignment, score } = scoredAlignment(t, s, algorithm)
129
+ const matches = countMatches(alignment)
130
+ return {
131
+ alignment,
132
+ matches,
133
+ explained: explainedFraction(matches, t.length, s.length),
134
+ score,
135
+ }
136
+ }
137
+
138
+ /**
139
+ * Pick which polymer entity of a structure corresponds to the transcript.
140
+ *
141
+ * The plugin historically hardcoded entity `[0]`, which silently mis-maps every
142
+ * heteromeric / protein-DNA / processed-peptide structure where the protein of
143
+ * interest is some other chain. Selecting by alignment makes the structure self-
144
+ * describe which entity is the gene's protein: an exact sequence match wins
145
+ * outright, otherwise the entity with the highest identity over the shorter
146
+ * sequence (see `explainedFraction`). Nucleic-acid entities are never
147
+ * candidates.
148
+ *
149
+ * Returns `undefined` only when there is nothing to map (no transcript or no
150
+ * protein entities) — never a silent fallback to the wrong entity.
151
+ */
152
+ export function chooseMappedEntity(
153
+ transcript: string,
154
+ entities: readonly (string | EntityCandidate)[],
155
+ algorithm: AlignmentAlgorithm,
156
+ ): EntitySelection | undefined {
157
+ const t = stripStopCodon(transcript)
158
+ if (!t) {
159
+ return undefined
160
+ }
161
+
162
+ const candidates = entities.map(e =>
163
+ typeof e === 'string'
164
+ ? { seq: stripStopCodon(e) }
165
+ : { ...e, seq: stripStopCodon(e.seq) },
166
+ )
167
+ const exactIndex = candidates.findIndex(
168
+ c => !c.nucleicAcid && c.seq.length > 0 && c.seq === t,
169
+ )
170
+ if (exactIndex !== -1) {
171
+ const exact = alignTranscriptToEntity(
172
+ t,
173
+ candidates[exactIndex]!.seq,
174
+ algorithm,
175
+ )!
176
+ return { index: exactIndex, ...exact }
177
+ }
178
+
179
+ // Each alignment is an O(len(t) * len(s)) main-thread DP, so a complex with
180
+ // many chains pays it once per chain. Homomers and repeated chains share a
181
+ // sequence, so align each distinct one once and reuse the result.
182
+ const bySeq = new Map<string, ScoredAlignment | undefined>()
183
+ let best: EntitySelection | undefined
184
+ for (let index = 0; index < candidates.length; index++) {
185
+ const { seq, nucleicAcid } = candidates[index]!
186
+ if (nucleicAcid) {
187
+ continue
188
+ }
189
+ if (!bySeq.has(seq)) {
190
+ bySeq.set(seq, alignTranscriptToEntity(t, seq, algorithm))
191
+ }
192
+ const scored = bySeq.get(seq)
193
+ if (scored && (!best || scored.explained > best.explained)) {
194
+ best = { index, ...scored }
195
+ }
196
+ }
197
+ return best
198
+ }
@@ -0,0 +1,119 @@
1
+ import {
2
+ structurePositionToAlignmentMap,
3
+ structureSeqVsTranscriptSeqMap,
4
+ transcriptPositionToAlignmentMap,
5
+ } from './mappings.ts'
6
+ import { invertMap } from './invertMap.ts'
7
+
8
+ import type { PairwiseAlignment } from './mappings.ts'
9
+
10
+ /**
11
+ * Branded coordinate spaces. Every position in this plugin is a bare `number`,
12
+ * but the *meaning* differs: a structure residue index is not interchangeable
13
+ * with a transcript residue index or an alignment column, and mixing them is
14
+ * the root cause of the off-by-one class of bugs. Branding makes the compiler
15
+ * reject cross-space mixing; construct values with the helpers below at the
16
+ * boundaries where raw numbers enter (molstar picks, hover events, features).
17
+ *
18
+ * Conventions: all are 0-based. StructurePos indexes the ungapped structure
19
+ * sequence; TranscriptPos the ungapped transcript sequence; AlignmentCol a
20
+ * column of the pairwise alignment (gaps included).
21
+ */
22
+ type Branded<B extends string> = number & { readonly __brand: B }
23
+ export type StructurePos = Branded<'StructurePos'>
24
+ export type TranscriptPos = Branded<'TranscriptPos'>
25
+ export type AlignmentCol = Branded<'AlignmentCol'>
26
+
27
+ export const structurePos = (n: number) => n as StructurePos
28
+ export const transcriptPos = (n: number) => n as TranscriptPos
29
+ export const alignmentCol = (n: number) => n as AlignmentCol
30
+
31
+ export interface CoordinateMaps {
32
+ structureSeqToTranscriptSeqPosition: Record<number, number>
33
+ transcriptSeqToStructureSeqPosition: Record<number, number>
34
+ structurePositionToAlignmentMap: Record<number, number>
35
+ transcriptPositionToAlignmentMap: Record<number, number>
36
+ alignmentToStructurePosition: Record<number, number>
37
+ alignmentToTranscriptPosition: Record<number, number>
38
+ }
39
+
40
+ export interface CoordinateMapper {
41
+ structureToTranscript: (p: StructurePos) => TranscriptPos | undefined
42
+ transcriptToStructure: (p: TranscriptPos) => StructurePos | undefined
43
+ structureToAlignment: (p: StructurePos) => AlignmentCol | undefined
44
+ alignmentToStructure: (c: AlignmentCol) => StructurePos | undefined
45
+ transcriptToAlignment: (p: TranscriptPos) => AlignmentCol | undefined
46
+ alignmentToTranscript: (c: AlignmentCol) => TranscriptPos | undefined
47
+ /**
48
+ * Raw lookup tables, for consumers that iterate the whole map rather than
49
+ * convert a single position (e.g. per-residue track rendering, selection of
50
+ * all aligned residues). Plain numbers — prefer the typed methods above for
51
+ * point conversions.
52
+ */
53
+ readonly maps: CoordinateMaps
54
+ }
55
+
56
+ /**
57
+ * Builds every coordinate conversion once from a pairwise alignment, replacing
58
+ * the scatter of per-getter map builds + repeated invertMap calls. The branded
59
+ * methods are the preferred API; `maps` stays unbranded for whole-map consumers.
60
+ */
61
+ export function makeCoordinateMapper(
62
+ pairwiseAlignment: PairwiseAlignment,
63
+ ): CoordinateMapper {
64
+ const {
65
+ structureSeqToTranscriptSeqPosition,
66
+ transcriptSeqToStructureSeqPosition,
67
+ } = structureSeqVsTranscriptSeqMap(pairwiseAlignment)
68
+ const s2aln = structurePositionToAlignmentMap(pairwiseAlignment)
69
+ const t2aln = transcriptPositionToAlignmentMap(pairwiseAlignment)
70
+ const aln2s = invertMap(s2aln)
71
+ const aln2t = invertMap(t2aln)
72
+
73
+ // The `as` casts below are the single brand-crossing point: indexing a
74
+ // Record<number,number> yields a plain number that we re-brand to its output
75
+ // space. Isolated here so the rest of the codebase stays brand-clean.
76
+ return {
77
+ structureToTranscript: p =>
78
+ structureSeqToTranscriptSeqPosition[p] as TranscriptPos | undefined,
79
+ transcriptToStructure: p =>
80
+ transcriptSeqToStructureSeqPosition[p] as StructurePos | undefined,
81
+ structureToAlignment: p => s2aln[p] as AlignmentCol | undefined,
82
+ alignmentToStructure: c => aln2s[c] as StructurePos | undefined,
83
+ transcriptToAlignment: p => t2aln[p] as AlignmentCol | undefined,
84
+ alignmentToTranscript: c => aln2t[c] as TranscriptPos | undefined,
85
+ maps: {
86
+ structureSeqToTranscriptSeqPosition,
87
+ transcriptSeqToStructureSeqPosition,
88
+ structurePositionToAlignmentMap: s2aln,
89
+ transcriptPositionToAlignmentMap: t2aln,
90
+ alignmentToStructurePosition: aln2s,
91
+ alignmentToTranscriptPosition: aln2t,
92
+ },
93
+ }
94
+ }
95
+
96
+ /**
97
+ * The 0-based half-open structure-position range covering an inclusive range
98
+ * of 1-based transcript residues, the numbering a UniProt feature or a domain
99
+ * map uses. Residues the structure lacks are skipped, so a range that runs
100
+ * past a fragment's end clamps to the last modeled residue; undefined when none
101
+ * of the range aligns to the structure.
102
+ */
103
+ export function transcriptRangeToStructureRange(
104
+ mapper: CoordinateMapper,
105
+ range: { start: number; end: number },
106
+ ) {
107
+ let first: number | undefined
108
+ let last: number | undefined
109
+ for (let residue = range.start; residue <= range.end; residue++) {
110
+ const pos = mapper.transcriptToStructure(transcriptPos(residue - 1))
111
+ if (pos !== undefined) {
112
+ first = first === undefined ? pos : Math.min(first, pos)
113
+ last = last === undefined ? pos : Math.max(last, pos)
114
+ }
115
+ }
116
+ return first === undefined || last === undefined
117
+ ? undefined
118
+ : { start: first, end: last + 1 }
119
+ }
package/src/dbIds.ts ADDED
@@ -0,0 +1,46 @@
1
+ // Single source of truth for database IDs that UniProt can cross-reference.
2
+ // Each entry carries everything downstream needs: the UniProt xref database
3
+ // keyword, the regex (Ensembl patterns cover human ENS, mouse ENSMUS, zebrafish
4
+ // ENSDAR, etc.), and a human-readable label. Recognition, label rendering, and
5
+ // xref-query building all derive from this list, so a new ID type is one entry.
6
+ type DbType = 'ensembl' | 'refseq' | 'ccds' | 'hgnc'
7
+
8
+ const DB_ID_PATTERNS: { db: DbType; pattern: RegExp; label: string }[] = [
9
+ { db: 'ensembl', pattern: /^ENS[A-Z]*G\d+/i, label: 'Ensembl gene' },
10
+ { db: 'ensembl', pattern: /^ENS[A-Z]*T\d+/i, label: 'Ensembl transcript' },
11
+ { db: 'ensembl', pattern: /^ENS[A-Z]*P\d+/i, label: 'Ensembl protein' },
12
+ { db: 'refseq', pattern: /^[NX]M_\d+/i, label: 'RefSeq mRNA' },
13
+ { db: 'refseq', pattern: /^[NX]R_\d+/i, label: 'RefSeq ncRNA' },
14
+ { db: 'refseq', pattern: /^[NX]P_\d+/i, label: 'RefSeq protein' },
15
+ { db: 'ccds', pattern: /^CCDS\d+/i, label: 'CCDS' },
16
+ { db: 'hgnc', pattern: /^HGNC:\d+/i, label: 'HGNC' },
17
+ ]
18
+
19
+ export function matchDbIdPattern(id: string) {
20
+ return DB_ID_PATTERNS.find(p => p.pattern.test(id))
21
+ }
22
+
23
+ /** Whether an ID is a database identifier UniProt can map. */
24
+ export function isRecognizedDatabaseId(id: string) {
25
+ return matchDbIdPattern(id) !== undefined
26
+ }
27
+
28
+ /** Human-readable label for an ID, e.g. "ENST00000123 (Ensembl transcript)".
29
+ * Unrecognized IDs are returned unadorned. */
30
+ export function getDbIdLabel(id: string) {
31
+ const match = matchDbIdPattern(id)
32
+ return match ? `${id} (${match.label})` : id
33
+ }
34
+
35
+ /** The UniProt xref query fragment for a recognized ID, e.g.
36
+ * "xref:ensembl-ENST00000123". HGNC strips its redundant "HGNC:" prefix. */
37
+ export function buildUniProtXrefQuery(id: string) {
38
+ const match = matchDbIdPattern(id)
39
+ return match
40
+ ? `xref:${match.db}-${match.db === 'hgnc' ? id.replace('HGNC:', '') : id}`
41
+ : undefined
42
+ }
43
+
44
+ export function stripTrailingVersion(s?: string) {
45
+ return s?.replace(/\.[^./]+$/, '')
46
+ }
@@ -0,0 +1,88 @@
1
+ // Minimal shape of the molstar Model fields we read. AlphaFold and ColabFold
2
+ // store the per-residue pLDDT in the B-factor column (B_iso_or_equiv), so the
3
+ // first atom of each residue yields pLDDT for a predicted model and the
4
+ // crystallographic B-factor for an experimental one.
5
+ interface ConfidenceModel {
6
+ obj?: {
7
+ data: {
8
+ atomicConformation: {
9
+ B_iso_or_equiv: { value: (row: number) => number }
10
+ }
11
+ atomicHierarchy: {
12
+ residueAtomSegments: {
13
+ offsets: ArrayLike<number>
14
+ count: number
15
+ }
16
+ chainAtomSegments: {
17
+ index: ArrayLike<number>
18
+ }
19
+ chains: {
20
+ label_entity_id: { value: (row: number) => string }
21
+ }
22
+ residues: {
23
+ label_seq_id: { value: (row: number) => number }
24
+ }
25
+ }
26
+ }
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Per-residue B-factor / pLDDT of one polymer entity, keyed by molstar's
32
+ * `label_seq_id`. Keyed by id rather than by residue order because the atomic
33
+ * hierarchy only holds *observed* residues: a chain with an unmodeled loop, or
34
+ * a chain that is not the first in the file, would otherwise plot every value
35
+ * against the wrong SEQRES position. Convert through `Entity.seqIds`.
36
+ */
37
+ export interface EntityConfidence {
38
+ entityId: string
39
+ byLabelSeqId: Map<number, number>
40
+ }
41
+
42
+ export function extractPerResidueConfidence(
43
+ model: ConfidenceModel,
44
+ ): EntityConfidence[] | undefined {
45
+ const data = model.obj?.data
46
+ if (!data) {
47
+ return undefined
48
+ }
49
+ const { B_iso_or_equiv } = data.atomicConformation
50
+ const { residueAtomSegments, chainAtomSegments, chains, residues } =
51
+ data.atomicHierarchy
52
+ const byEntity = new Map<string, Map<number, number>>()
53
+ for (let residue = 0; residue < residueAtomSegments.count; residue++) {
54
+ const atom = residueAtomSegments.offsets[residue]!
55
+ const entityId = chains.label_entity_id.value(
56
+ chainAtomSegments.index[atom]!,
57
+ )
58
+ let values = byEntity.get(entityId)
59
+ if (!values) {
60
+ values = new Map()
61
+ byEntity.set(entityId, values)
62
+ }
63
+ const seqId = residues.label_seq_id.value(residue)
64
+ if (!values.has(seqId)) {
65
+ values.set(seqId, B_iso_or_equiv.value(atom))
66
+ }
67
+ }
68
+ return [...byEntity].map(([entityId, byLabelSeqId]) => ({
69
+ entityId,
70
+ byLabelSeqId,
71
+ }))
72
+ }
73
+
74
+ /**
75
+ * AlphaFold-style pLDDT lives in [0, 100] and varies across residues. A
76
+ * constant column (common when a PDB has no B-factors) or out-of-range values
77
+ * indicate the track wouldn't be meaningful as confidence.
78
+ */
79
+ export function looksLikePlddt(
80
+ values: number[] | undefined,
81
+ ): values is number[] {
82
+ return (
83
+ !!values &&
84
+ values.length > 1 &&
85
+ values.every(v => v >= 0 && v <= 100) &&
86
+ new Set(values).size > 1
87
+ )
88
+ }