p2s_mapper 1.4.0 → 2.1.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 (134) hide show
  1. package/README.md +220 -39
  2. package/dist/alignmentQuality.d.ts +5 -5
  3. package/dist/alphaFoldModels.d.ts +4 -4
  4. package/dist/alphaFoldModels.js +4 -4
  5. package/dist/chooseMappedEntity.d.ts +11 -13
  6. package/dist/chooseMappedEntity.js +9 -12
  7. package/dist/chooseMappedEntity.js.map +1 -1
  8. package/dist/coordinates.d.ts +19 -19
  9. package/dist/coordinates.js.map +1 -1
  10. package/dist/dbIds.d.ts +1 -2
  11. package/dist/extractPerResidueConfidence.d.ts +1 -2
  12. package/dist/extractStructureSequences.d.ts +26 -34
  13. package/dist/extractStructureSequences.js +2 -2
  14. package/dist/extractStructureSequences.js.map +1 -1
  15. package/dist/fetchUtils.d.ts +7 -7
  16. package/dist/fetchUtils.js +26 -8
  17. package/dist/fetchUtils.js.map +1 -1
  18. package/dist/index.d.ts +1 -3
  19. package/dist/index.js +2 -4
  20. package/dist/index.js.map +1 -1
  21. package/dist/isoformRanking.d.ts +4 -1
  22. package/dist/mappings.d.ts +9 -12
  23. package/dist/mappings.js +4 -5
  24. package/dist/mappings.js.map +1 -1
  25. package/dist/pairwiseAlignment.d.ts +5 -4
  26. package/dist/pairwiseAlignment.js +9 -7
  27. package/dist/pairwiseAlignment.js.map +1 -1
  28. package/dist/pdbEntry.d.ts +32 -0
  29. package/dist/pdbEntry.js +182 -0
  30. package/dist/pdbEntry.js.map +1 -0
  31. package/dist/pdbUniProtMapping.d.ts +14 -39
  32. package/dist/pdbUniProtMapping.js +22 -140
  33. package/dist/pdbUniProtMapping.js.map +1 -1
  34. package/dist/structureFormat.d.ts +11 -18
  35. package/dist/structureFormat.js +4 -6
  36. package/dist/structureFormat.js.map +1 -1
  37. package/dist/structureSources.d.ts +37 -13
  38. package/dist/structureSources.js +144 -57
  39. package/dist/structureSources.js.map +1 -1
  40. package/dist/structureUrls.d.ts +13 -16
  41. package/dist/structureUrls.js +25 -23
  42. package/dist/structureUrls.js.map +1 -1
  43. package/dist/types.d.ts +2 -5
  44. package/dist/types.js +2 -5
  45. package/dist/types.js.map +1 -1
  46. package/dist/uniProtSearch.js +3 -8
  47. package/dist/uniProtSearch.js.map +1 -1
  48. package/esm/alignmentQuality.d.ts +5 -5
  49. package/esm/alphaFoldModels.d.ts +4 -4
  50. package/esm/alphaFoldModels.js +4 -4
  51. package/esm/chooseMappedEntity.d.ts +11 -13
  52. package/esm/chooseMappedEntity.js +9 -12
  53. package/esm/chooseMappedEntity.js.map +1 -1
  54. package/esm/coordinates.d.ts +19 -19
  55. package/esm/coordinates.js.map +1 -1
  56. package/esm/dbIds.d.ts +1 -2
  57. package/esm/extractPerResidueConfidence.d.ts +1 -2
  58. package/esm/extractStructureSequences.d.ts +26 -34
  59. package/esm/extractStructureSequences.js +2 -2
  60. package/esm/extractStructureSequences.js.map +1 -1
  61. package/esm/fetchUtils.d.ts +7 -7
  62. package/esm/fetchUtils.js +26 -8
  63. package/esm/fetchUtils.js.map +1 -1
  64. package/esm/index.d.ts +1 -3
  65. package/esm/index.js +2 -4
  66. package/esm/index.js.map +1 -1
  67. package/esm/isoformRanking.d.ts +4 -1
  68. package/esm/mappings.d.ts +9 -12
  69. package/esm/mappings.js +4 -5
  70. package/esm/mappings.js.map +1 -1
  71. package/esm/pairwiseAlignment.d.ts +5 -4
  72. package/esm/pairwiseAlignment.js +7 -5
  73. package/esm/pairwiseAlignment.js.map +1 -1
  74. package/esm/pdbEntry.d.ts +32 -0
  75. package/esm/pdbEntry.js +174 -0
  76. package/esm/pdbEntry.js.map +1 -0
  77. package/esm/pdbUniProtMapping.d.ts +14 -39
  78. package/esm/pdbUniProtMapping.js +22 -137
  79. package/esm/pdbUniProtMapping.js.map +1 -1
  80. package/esm/structureFormat.d.ts +11 -18
  81. package/esm/structureFormat.js +4 -6
  82. package/esm/structureFormat.js.map +1 -1
  83. package/esm/structureSources.d.ts +37 -13
  84. package/esm/structureSources.js +139 -55
  85. package/esm/structureSources.js.map +1 -1
  86. package/esm/structureUrls.d.ts +13 -16
  87. package/esm/structureUrls.js +23 -23
  88. package/esm/structureUrls.js.map +1 -1
  89. package/esm/types.d.ts +2 -5
  90. package/esm/types.js +2 -5
  91. package/esm/types.js.map +1 -1
  92. package/esm/uniProtSearch.js +3 -8
  93. package/esm/uniProtSearch.js.map +1 -1
  94. package/package.json +3 -2
  95. package/src/alignmentQuality.ts +5 -5
  96. package/src/alphaFoldModels.ts +4 -4
  97. package/src/chooseMappedEntity.ts +12 -15
  98. package/src/coordinates.ts +14 -17
  99. package/src/dbIds.ts +1 -1
  100. package/src/extractPerResidueConfidence.ts +1 -1
  101. package/src/extractStructureSequences.ts +26 -33
  102. package/src/fetchUtils.ts +30 -8
  103. package/src/index.ts +2 -4
  104. package/src/isoformRanking.ts +4 -4
  105. package/src/mappings.ts +9 -12
  106. package/src/pairwiseAlignment.ts +12 -9
  107. package/src/pdbEntry.ts +240 -0
  108. package/src/pdbUniProtMapping.ts +25 -164
  109. package/src/structureFormat.ts +11 -18
  110. package/src/structureSources.ts +194 -70
  111. package/src/structureUrls.ts +25 -23
  112. package/src/types.ts +2 -5
  113. package/src/uniProtSearch.ts +3 -8
  114. package/dist/pdbeBestStructures.d.ts +0 -23
  115. package/dist/pdbeBestStructures.js +0 -79
  116. package/dist/pdbeBestStructures.js.map +0 -1
  117. package/dist/pdbeEntryMolecules.d.ts +0 -7
  118. package/dist/pdbeEntryMolecules.js +0 -39
  119. package/dist/pdbeEntryMolecules.js.map +0 -1
  120. package/dist/siftsNumbering.d.ts +0 -23
  121. package/dist/siftsNumbering.js +0 -44
  122. package/dist/siftsNumbering.js.map +0 -1
  123. package/esm/pdbeBestStructures.d.ts +0 -23
  124. package/esm/pdbeBestStructures.js +0 -73
  125. package/esm/pdbeBestStructures.js.map +0 -1
  126. package/esm/pdbeEntryMolecules.d.ts +0 -7
  127. package/esm/pdbeEntryMolecules.js +0 -35
  128. package/esm/pdbeEntryMolecules.js.map +0 -1
  129. package/esm/siftsNumbering.d.ts +0 -23
  130. package/esm/siftsNumbering.js +0 -40
  131. package/esm/siftsNumbering.js.map +0 -1
  132. package/src/pdbeBestStructures.ts +0 -94
  133. package/src/pdbeEntryMolecules.ts +0 -37
  134. package/src/siftsNumbering.ts +0 -61
package/src/mappings.ts CHANGED
@@ -6,13 +6,11 @@ export interface AlignmentRow {
6
6
  }
7
7
 
8
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.
9
+ * A two-row alignment: row 0 is the transcript and row 1 is the structure.
10
+ * alignSequences takes them in that order and every consumer depends on it,
11
+ * but the type cannot enforce it, and swapped rows invert every coordinate map
12
+ * without an error. Read the rows with the accessors below, so a call site
13
+ * names the sequence it means.
16
14
  */
17
15
  export interface PairwiseAlignment {
18
16
  consensus: string
@@ -28,11 +26,10 @@ export const alignmentLength = (pa: PairwiseAlignment) =>
28
26
  transcriptAlignedSeq(pa).length
29
27
 
30
28
  /**
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.
29
+ * Why a pairwise alignment can't be used, or undefined if it's usable. Both
30
+ * rows must have the same non-zero length, because every coordinate map walks
31
+ * them in step. Check an alignment from outside the package with this; the
32
+ * internal map builders throw on it.
36
33
  */
37
34
  export function pairwiseAlignmentProblem(pa: PairwiseAlignment) {
38
35
  const transcript = transcriptAlignedSeq(pa)
@@ -68,7 +68,7 @@ function getScore(a: string, b: string) {
68
68
  const GAP_OPEN = -10
69
69
  const GAP_EXTEND = -0.5
70
70
 
71
- interface AlignmentResult {
71
+ export interface AlignmentResult {
72
72
  alignedSeq1: string
73
73
  alignedSeq2: string
74
74
  score: number
@@ -393,14 +393,14 @@ export function selfScore(seq: string) {
393
393
  }
394
394
 
395
395
  export function scoredAlignment(
396
- seq1: string,
397
- seq2: string,
396
+ transcript: string,
397
+ structure: string,
398
398
  algorithm: AlignmentAlgorithm,
399
399
  ): { alignment: PairwiseAlignment; score: number } {
400
400
  const { alignedSeq1, alignedSeq2, score } =
401
401
  algorithm === 'smith_waterman'
402
- ? smithWaterman(seq1, seq2)
403
- : needlemanWunsch(seq1, seq2)
402
+ ? smithWaterman(transcript, structure)
403
+ : needlemanWunsch(transcript, structure)
404
404
 
405
405
  return {
406
406
  alignment: {
@@ -414,10 +414,13 @@ export function scoredAlignment(
414
414
  }
415
415
  }
416
416
 
417
- export function runLocalAlignment(
418
- seq1: string,
419
- seq2: string,
417
+ export function alignSequences(
418
+ transcript: string,
419
+ structure: string,
420
420
  algorithm: AlignmentAlgorithm,
421
421
  ): PairwiseAlignment {
422
- return scoredAlignment(seq1, seq2, algorithm).alignment
422
+ return scoredAlignment(transcript, structure, algorithm).alignment
423
423
  }
424
+
425
+ /** @deprecated renamed to `alignSequences`, which runs either algorithm */
426
+ export const runLocalAlignment = alignSequences
@@ -0,0 +1,240 @@
1
+ // What RCSB knows about one PDB entry that a mapping needs, in one GraphQL
2
+ // call: each polymer entity's sequence, its author chains, and its alignment
3
+ // to UniProt. The alignment is what SIFTS publishes per chain, numbered the
4
+ // same way (`entity_beg_seq_id` is label_seq_id, 1-based into the SEQRES),
5
+ // and agreed with PDBe's SIFTS on every residue of 23 entries checked
6
+ // (2026-10-11, 1TUP, 2RH1, 4HHB, 7K00 among them), in a tenth of the time.
7
+
8
+ import {
9
+ HttpError,
10
+ isDefinitiveFailure,
11
+ jsonfetch,
12
+ timeout,
13
+ } from './fetchUtils.ts'
14
+
15
+ import type { FetchOptions } from './fetchUtils.ts'
16
+ import type {
17
+ UniProtStructureMapping,
18
+ UniProtStructureSegment,
19
+ } from './pdbUniProtMapping.ts'
20
+
21
+ export const RCSB_DATA_GRAPHQL_URL = 'https://data.rcsb.org/graphql'
22
+
23
+ export interface PdbEntry {
24
+ /** every UniProt accession the entry's entities align to */
25
+ mappings: UniProtStructureMapping[]
26
+ /** one sequence per protein entity, in entity order; nucleic acids left
27
+ * out, since A, C, G, T and U are amino-acid letters too */
28
+ proteinSequences: string[]
29
+ }
30
+
31
+ export function pdbEntryQuery(pdbId: string) {
32
+ return `{ entry(entry_id: "${pdbId.toUpperCase()}") { polymer_entities {
33
+ rcsb_id
34
+ entity_poly { type pdbx_seq_one_letter_code_can }
35
+ rcsb_polymer_entity_container_identifiers { auth_asym_ids }
36
+ rcsb_polymer_entity_align {
37
+ reference_database_name reference_database_accession
38
+ aligned_regions { entity_beg_seq_id ref_beg_seq_id length }
39
+ }
40
+ uniprots { rcsb_id rcsb_uniprot_entry_name }
41
+ } } }`
42
+ }
43
+
44
+ const RETRY_DELAYS_MS = [1000, 3000]
45
+ // bounded, since a view reports itself loading until the entry answers
46
+ const TIMEOUT_MS = 20_000
47
+
48
+ /**
49
+ * A GraphQL request. The server answers 200 whatever happened, so a failure
50
+ * is read from the body's `errors`, and counts as a 400: the server
51
+ * understood the query and declined it, so asking again gets the same answer.
52
+ */
53
+ export async function graphqlFetch(
54
+ url: string,
55
+ query: string,
56
+ variables: Record<string, unknown> | undefined,
57
+ opts?: FetchOptions,
58
+ ) {
59
+ const json: unknown = await jsonfetch(url, {
60
+ ...opts,
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ query, variables }),
64
+ })
65
+ const errors = isRecord(json) ? json.errors : undefined
66
+ if (Array.isArray(errors) && errors.length > 0) {
67
+ const first: unknown = errors[0]
68
+ const message =
69
+ isRecord(first) && typeof first.message === 'string'
70
+ ? first.message
71
+ : 'unknown error'
72
+ throw new HttpError(`${new URL(url).host}: ${message}`, 400)
73
+ }
74
+ return isRecord(json) ? json.data : undefined
75
+ }
76
+
77
+ /**
78
+ * The entry, retried twice: one failed request would otherwise leave a fusion
79
+ * construct mapped onto its partner for the whole session. An id RCSB has no
80
+ * entry for is a 404, which is not retried, since that answer cannot change.
81
+ */
82
+ export async function fetchPdbEntry(
83
+ pdbId: string,
84
+ opts?: FetchOptions,
85
+ ): Promise<PdbEntry> {
86
+ const { signal: caller, ...rest } = opts ?? {}
87
+ const signal = caller ?? undefined
88
+ const deadline = () =>
89
+ signal
90
+ ? AbortSignal.any([signal, AbortSignal.timeout(TIMEOUT_MS)])
91
+ : AbortSignal.timeout(TIMEOUT_MS)
92
+ for (let attempt = 0; ; attempt++) {
93
+ try {
94
+ const entry = parsePdbEntry(
95
+ await graphqlFetch(
96
+ RCSB_DATA_GRAPHQL_URL,
97
+ pdbEntryQuery(pdbId),
98
+ undefined,
99
+ { ...rest, signal: deadline() },
100
+ ),
101
+ )
102
+ if (!entry) {
103
+ throw new HttpError(`RCSB has no entry ${pdbId.toUpperCase()}`, 404)
104
+ }
105
+ return entry
106
+ } catch (e) {
107
+ const delay = RETRY_DELAYS_MS[attempt]
108
+ // the caller's own abort is not a failure to retry either
109
+ if (delay === undefined || isDefinitiveFailure(e) || signal?.aborted) {
110
+ throw e
111
+ }
112
+ await timeout(delay, signal)
113
+ }
114
+ }
115
+ }
116
+
117
+ /** The entry's UniProt alignment alone. */
118
+ export async function fetchUniProtStructureMappings(
119
+ pdbId: string,
120
+ opts?: FetchOptions,
121
+ ) {
122
+ return (await fetchPdbEntry(pdbId, opts)).mappings
123
+ }
124
+
125
+ function isRecord(value: unknown): value is Record<string, unknown> {
126
+ return typeof value === 'object' && value !== null && !Array.isArray(value)
127
+ }
128
+
129
+ function finiteNumber(value: unknown) {
130
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined
131
+ }
132
+
133
+ function strings(value: unknown) {
134
+ return Array.isArray(value)
135
+ ? value.filter((v): v is string => typeof v === 'string')
136
+ : []
137
+ }
138
+
139
+ /**
140
+ * Parses the `data` of `pdbEntryQuery`. Undefined when RCSB has no such
141
+ * entry; an entry nothing aligns to UniProt (DNA alone, say) parses to no
142
+ * mappings. An unparseable alignment is skipped rather than failing the
143
+ * entry, so a chimera with one malformed region still maps its other chains.
144
+ */
145
+ export function parsePdbEntry(data: unknown): PdbEntry | undefined {
146
+ const entry = isRecord(data) ? data.entry : undefined
147
+ if (!isRecord(entry)) {
148
+ return undefined
149
+ }
150
+ const entities = Array.isArray(entry.polymer_entities)
151
+ ? entry.polymer_entities.filter(isRecord)
152
+ : []
153
+ const byAccession = new Map<string, UniProtStructureMapping>()
154
+ const proteinSequences: string[] = []
155
+ for (const entity of entities.toSorted(
156
+ (a, b) => entityNumber(a.rcsb_id) - entityNumber(b.rcsb_id),
157
+ )) {
158
+ const entityId = String(entityNumber(entity.rcsb_id))
159
+ const poly = isRecord(entity.entity_poly) ? entity.entity_poly : undefined
160
+ const sequence = poly?.pdbx_seq_one_letter_code_can
161
+ if (
162
+ typeof poly?.type === 'string' &&
163
+ poly.type.startsWith('polypeptide') &&
164
+ typeof sequence === 'string' &&
165
+ sequence.length > 0
166
+ ) {
167
+ proteinSequences.push(sequence)
168
+ }
169
+ const ids = entity.rcsb_polymer_entity_container_identifiers
170
+ const chains = strings(isRecord(ids) ? ids.auth_asym_ids : undefined)
171
+ const names = new Map(
172
+ (Array.isArray(entity.uniprots) ? entity.uniprots : [])
173
+ .filter(isRecord)
174
+ .map(u => [u.rcsb_id, strings(u.rcsb_uniprot_entry_name)[0]] as const),
175
+ )
176
+ for (const align of Array.isArray(entity.rcsb_polymer_entity_align)
177
+ ? entity.rcsb_polymer_entity_align.filter(isRecord)
178
+ : []) {
179
+ const accession = align.reference_database_accession
180
+ if (
181
+ align.reference_database_name !== 'UniProt' ||
182
+ typeof accession !== 'string'
183
+ ) {
184
+ continue
185
+ }
186
+ const segments = (
187
+ Array.isArray(align.aligned_regions) ? align.aligned_regions : []
188
+ ).flatMap(region => parseSegments(region, entityId, chains))
189
+ if (segments.length === 0) {
190
+ continue
191
+ }
192
+ const mapping = byAccession.get(accession) ?? {
193
+ accession,
194
+ name: names.get(accession),
195
+ segments: [],
196
+ }
197
+ mapping.segments.push(...segments)
198
+ byAccession.set(accession, mapping)
199
+ }
200
+ }
201
+ return { mappings: [...byAccession.values()], proteinSequences }
202
+ }
203
+
204
+ function entityNumber(rcsbId: unknown) {
205
+ return typeof rcsbId === 'string' ? Number(rcsbId.split('_')[1]) : NaN
206
+ }
207
+
208
+ /** One segment per chain carrying the entity, so a PDB-format file, whose
209
+ * entities Mol* numbers per chain, still matches by chain. */
210
+ function parseSegments(
211
+ region: unknown,
212
+ entityId: string,
213
+ chains: string[],
214
+ ): UniProtStructureSegment[] {
215
+ if (!isRecord(region)) {
216
+ return []
217
+ }
218
+ const entityBeg = finiteNumber(region.entity_beg_seq_id)
219
+ const refBeg = finiteNumber(region.ref_beg_seq_id)
220
+ const length = finiteNumber(region.length)
221
+ if (
222
+ entityBeg === undefined ||
223
+ refBeg === undefined ||
224
+ length === undefined ||
225
+ length < 1
226
+ ) {
227
+ return []
228
+ }
229
+ const segment = {
230
+ entityId,
231
+ unpStart: refBeg,
232
+ unpEnd: refBeg + length - 1,
233
+ // SEQRES/label_seq_id is 1-based, structure positions are 0-based
234
+ structStart: entityBeg - 1,
235
+ structEnd: entityBeg + length - 2,
236
+ }
237
+ return chains.length === 0
238
+ ? [segment]
239
+ : chains.map(chainId => ({ ...segment, chainId }))
240
+ }
@@ -1,4 +1,4 @@
1
- // SIFTS UniProt <-> PDB residue mapping.
1
+ // UniProt <-> PDB residue mapping.
2
2
  //
3
3
  // UniProt feature coordinates are positions in the UniProt canonical sequence.
4
4
  // For an AlphaFold model that *is* the structure's own sequence, so a feature at
@@ -6,33 +6,23 @@
6
6
  // entry it is not: the deposited construct is usually a fragment, often with
7
7
  // tags or engineered residues, so the two numberings differ by a per-segment
8
8
  // offset (1TUP's p53 chain starts at UniProt 94; 6VXX's spike SEQRES 33 is
9
- // UniProt 14). SIFTS is the authoritative alignment between the two, published
10
- // per entity/chain by PDBe.
9
+ // UniProt 14). The entry's own UniProt alignment, which `fetchPdbEntry` reads
10
+ // from RCSB, is the authority on that correspondence.
11
11
  //
12
- // `residue_number` in the PDBe response is the 1-based index into the entity's
13
- // SEQRES, i.e. label_seq_id, which is exactly this plugin's structure-sequence
14
- // position + 1 (see applyLociInteractivity.ts for the other place that boundary
15
- // is crossed).
16
-
17
- import { isDefinitiveFailure, jsonfetch, timeout } from './fetchUtils.ts'
18
-
19
- import type { FetchOptions } from './fetchUtils.ts'
12
+ // Structure positions here index the entity's SEQRES from 0, i.e.
13
+ // label_seq_id - 1, which is this package's structure-sequence position.
20
14
 
21
15
  /** One contiguous UniProt <-> structure correspondence for a single entity.
22
16
  * `unp*` are 1-based UniProt positions; `struct*` are 0-based inclusive
23
- * structure-sequence positions, this plugin's native coordinate. */
17
+ * structure-sequence positions, this package's native coordinate. */
24
18
  export interface UniProtStructureSegment {
25
19
  entityId: string
26
- /** author chain id, when the response names it */
20
+ /** author chain id, when the source names it */
27
21
  chainId?: string
28
22
  unpStart: number
29
23
  unpEnd: number
30
24
  structStart: number
31
25
  structEnd: number
32
- /** the endpoints' author residue numbers, the numbering a paper cites and a
33
- * Mol* hover shows; absent when the response omits them */
34
- authorStart?: number
35
- authorEnd?: number
36
26
  }
37
27
 
38
28
  export interface UniProtStructureMapping {
@@ -49,140 +39,11 @@ export interface UniProtStructureMapping {
49
39
  */
50
40
  export type MapUniProtPosition = (uniprotPos: number) => number | undefined
51
41
 
52
- export function pdbeSiftsUrl(pdbId: string) {
53
- return `https://www.ebi.ac.uk/pdbe/api/mappings/uniprot/${pdbId.toLowerCase()}`
54
- }
55
-
56
- const SIFTS_RETRY_DELAYS_MS = [1000, 3000]
57
- // bounded, since a view reports itself loading until SIFTS answers
58
- const SIFTS_TIMEOUT_MS = 20_000
59
-
60
- /**
61
- * SIFTS for an entry, retried twice: one failed request would otherwise leave a
62
- * fusion construct mapped onto its partner for the whole session.
63
- *
64
- * A 4xx is not retried. PDBe answers 404 for an entry it has no UniProt
65
- * mapping for at all — a DNA-only or obsolete entry — and that answer cannot
66
- * change, so the retries only postponed it: measured 2026-09-16, `1hhb` and
67
- * `1bna` each took three requests and ~4.8s to say so, which a caller shows as
68
- * four and a half seconds of "resolving" for a structure that will never
69
- * resolve.
70
- */
71
- export async function fetchUniProtStructureMappings(
72
- pdbId: string,
73
- opts?: FetchOptions,
74
- ) {
75
- const { signal: caller, ...rest } = opts ?? {}
76
- const signal = caller ?? undefined
77
- const deadline = () =>
78
- signal
79
- ? AbortSignal.any([signal, AbortSignal.timeout(SIFTS_TIMEOUT_MS)])
80
- : AbortSignal.timeout(SIFTS_TIMEOUT_MS)
81
- for (let attempt = 0; ; attempt++) {
82
- try {
83
- return parseUniProtStructureMappings(
84
- await jsonfetch(pdbeSiftsUrl(pdbId), { ...rest, signal: deadline() }),
85
- )
86
- } catch (e) {
87
- const delay = SIFTS_RETRY_DELAYS_MS[attempt]
88
- // the caller's own abort is not a failure to retry either
89
- if (delay === undefined || isDefinitiveFailure(e) || signal?.aborted) {
90
- throw e
91
- }
92
- await timeout(delay, signal)
93
- }
94
- }
95
- }
96
-
97
- function isRecord(value: unknown): value is Record<string, unknown> {
98
- return typeof value === 'object' && value !== null && !Array.isArray(value)
99
- }
100
-
101
- function finiteNumber(value: unknown) {
102
- return typeof value === 'number' && Number.isFinite(value) ? value : undefined
103
- }
104
-
105
- function residueNumber(endpoint: unknown) {
106
- return isRecord(endpoint) ? finiteNumber(endpoint.residue_number) : undefined
107
- }
108
-
109
- function authorResidueNumber(endpoint: unknown) {
110
- return isRecord(endpoint)
111
- ? finiteNumber(endpoint.author_residue_number)
112
- : undefined
113
- }
114
-
115
- function parseSegment(mapping: unknown): UniProtStructureSegment | undefined {
116
- if (!isRecord(mapping)) {
117
- return undefined
118
- }
119
- const entityId = finiteNumber(mapping.entity_id)
120
- const unpStart = finiteNumber(mapping.unp_start)
121
- const unpEnd = finiteNumber(mapping.unp_end)
122
- const structStart = residueNumber(mapping.start)
123
- const structEnd = residueNumber(mapping.end)
124
- const authorStart = authorResidueNumber(mapping.start)
125
- const authorEnd = authorResidueNumber(mapping.end)
126
- return entityId === undefined ||
127
- unpStart === undefined ||
128
- unpEnd === undefined ||
129
- structStart === undefined ||
130
- structEnd === undefined
131
- ? undefined
132
- : {
133
- entityId: String(entityId),
134
- ...(typeof mapping.chain_id === 'string'
135
- ? { chainId: mapping.chain_id }
136
- : {}),
137
- unpStart,
138
- unpEnd,
139
- // SEQRES/label_seq_id is 1-based, structure positions are 0-based
140
- structStart: structStart - 1,
141
- structEnd: structEnd - 1,
142
- ...(authorStart === undefined ? {} : { authorStart }),
143
- ...(authorEnd === undefined ? {} : { authorEnd }),
144
- }
145
- }
146
-
147
- /**
148
- * Parses a PDBe `mappings/uniprot/{pdbId}` response into one entry per UniProt
149
- * accession. Unparseable segments are skipped rather than failing the whole
150
- * response — a chimera with one malformed segment still maps its other chains.
151
- * The response is keyed by pdb id, so the single entry is taken whatever its key.
152
- */
153
- export function parseUniProtStructureMappings(
154
- json: unknown,
155
- ): UniProtStructureMapping[] {
156
- const entry = isRecord(json) ? Object.values(json)[0] : undefined
157
- const uniprot = isRecord(entry) ? entry.UniProt : undefined
158
- if (!isRecord(uniprot)) {
159
- return []
160
- }
161
- const result: UniProtStructureMapping[] = []
162
- for (const [accession, value] of Object.entries(uniprot)) {
163
- const mappings = isRecord(value) ? value.mappings : undefined
164
- const segments = Array.isArray(mappings)
165
- ? mappings.map(parseSegment).filter(s => s !== undefined)
166
- : []
167
- if (segments.length > 0) {
168
- result.push({
169
- accession,
170
- name:
171
- isRecord(value) && typeof value.name === 'string'
172
- ? value.name
173
- : undefined,
174
- segments,
175
- })
176
- }
177
- }
178
- return result
179
- }
180
-
181
42
  function coveredResidues(segments: UniProtStructureSegment[]) {
182
43
  return segments.reduce((a, s) => a + (s.structEnd - s.structStart + 1), 0)
183
44
  }
184
45
 
185
- /** The entity a SIFTS segment is matched against: its mmCIF id and the author
46
+ /** The entity a segment is matched against: its mmCIF id and the author
186
47
  * chains carrying it. */
187
48
  export interface SegmentEntity {
188
49
  entityId: string
@@ -190,10 +51,10 @@ export interface SegmentEntity {
190
51
  }
191
52
 
192
53
  /**
193
- * The segments of one accession that describe this entity. SIFTS numbers
194
- * entities as the mmCIF does, and a PDB-format file loaded through Mol* numbers
195
- * them per chain (1H26.pdb's chain C is entity 2, which in SIFTS is cyclin A),
196
- * so a segment naming its chain is matched by chain.
54
+ * The segments of one accession that describe this entity. The mapping
55
+ * numbers entities as the mmCIF does, and a PDB-format file loaded through
56
+ * Mol* numbers them per chain (1H26.pdb's chain C is entity 2, which in the
57
+ * mmCIF is cyclin A), so a segment naming its chain is matched by chain.
197
58
  */
198
59
  function segmentsForEntity(
199
60
  mapping: UniProtStructureMapping,
@@ -209,12 +70,11 @@ function segmentsForEntity(
209
70
  }
210
71
 
211
72
  /**
212
- * Picks the UniProt entry that describes a given entity, with its segments
213
- * narrowed to that entity. Only the entity the plugin has mapped to the
214
- * transcript is relevant — a heteromer maps each of its chains to a different
215
- * accession, and using the wrong one would silently annotate the wrong protein.
216
- * When several accessions cover the same entity (a chimeric construct) the one
217
- * contributing the most residues wins.
73
+ * Picks the UniProt entry for a given entity, with its segments narrowed to
74
+ * that entity. Pass the entity mapped to the transcript: a heteromer's chains
75
+ * align to different accessions, and the wrong one would annotate the wrong
76
+ * protein. When several accessions cover one entity (a chimeric construct), the
77
+ * one covering the most residues wins.
218
78
  */
219
79
  export function chooseUniProtMappingForEntity(
220
80
  mappings: UniProtStructureMapping[],
@@ -240,10 +100,11 @@ const OWN_PSEUDOCOUNT = 5
240
100
  const MIN_OWN_IDENTITY = 0.5
241
101
 
242
102
  /**
243
- * Mapped structure positions that SIFTS assigns to a protein other than the
244
- * transcript's, on a chain that fuses two. Local alignment bridges a fusion
245
- * boundary, and on every GPCR fusion construct checked it scattered receptor
246
- * residues onto the partner, 33 of them onto T4 lysozyme in 2RH1.
103
+ * Mapped structure positions the entry's UniProt alignment assigns to a
104
+ * protein other than the transcript's, on a chain that fuses two. Local
105
+ * alignment bridges a fusion boundary, and on every GPCR fusion construct
106
+ * checked it scattered receptor residues onto the partner, 33 of them onto T4
107
+ * lysozyme in 2RH1.
247
108
  *
248
109
  * `mapped` is each mapped structure position and whether its pair is
249
110
  * identical. The transcript's own accession is the one whose covered positions
@@ -252,7 +113,7 @@ const MIN_OWN_IDENTITY = 0.5
252
113
  * fused to CDK2 covers 11 positions against 224 chance hits on the kinase, and
253
114
  * counting unmapped the peptide. Unless that accession reaches half identity
254
115
  * the transcript is taken to be neither protein, and nothing is unmapped.
255
- * Residues SIFTS assigns to no accession, such as tags, stay mapped.
116
+ * Residues assigned to no accession, such as tags, stay mapped.
256
117
  */
257
118
  export function fusionPartnerPositions(
258
119
  mappings: UniProtStructureMapping[],
@@ -297,8 +158,8 @@ export function fusionPartnerPositions(
297
158
  return partners
298
159
  }
299
160
 
300
- // SIFTS lists one mapping per *chain*, so the several chains of a homodimer
301
- // repeat the same entity-level correspondence. They collapse to one segment.
161
+ // The mapping lists one segment per *chain*, so the several chains of a
162
+ // homodimer repeat the same entity-level correspondence. They collapse to one.
302
163
  function dedupeSegments(segments: UniProtStructureSegment[]) {
303
164
  const seen = new Set<string>()
304
165
  return segments.filter(s => {
@@ -1,19 +1,14 @@
1
1
  /**
2
2
  * Which molstar trajectory parser a structure needs.
3
3
  *
4
- * Getting this wrong is not a graceful failure in either direction: parsing a
5
- * PDB file as mmCIF throws ("Unexpected token. Expected data_, loop_, or data
6
- * name"), and — worse — parsing an mmCIF as PDB *succeeds* and yields a model
7
- * with zero polymer entities and thousands of misread atoms, so the view loads
8
- * with no sequence, no alignment and no genome mapping, and nothing reports an
9
- * error.
4
+ * A wrong choice fails either way. Parsing a PDB file as mmCIF throws
5
+ * ("Unexpected token. Expected data_, loop_, or data name"). Parsing an mmCIF
6
+ * as PDB succeeds and yields a model with zero polymer entities and thousands of
7
+ * misread atoms, so the structure loads with no sequence, no alignment and no
8
+ * genome mapping, and with no error.
10
9
  *
11
- * So detection lives here and is applied as the default at every entry point,
12
- * rather than at one of them.
13
- *
14
- * The two names are the subset of molstar's `BuiltInTrajectoryFormat` a
15
- * structure loaded from a file or a url can be, spelled out rather than
16
- * imported so nothing here depends on molstar.
10
+ * The two names are the subset of molstar's `BuiltInTrajectoryFormat` that a
11
+ * structure file or URL can be, written out so nothing here imports molstar.
17
12
  */
18
13
  export type StructureFormat = 'pdb' | 'mmcif'
19
14
 
@@ -35,8 +30,7 @@ export function structureFileExtension(nameOrUrl: string) {
35
30
 
36
31
  /**
37
32
  * Format for a filename or URL. Unknown extensions fall back to mmCIF, which is
38
- * what every URL this plugin generates itself serves (AlphaFold and RCSB both
39
- * hand out `.cif`).
33
+ * what AlphaFold and RCSB serve (`.cif`).
40
34
  */
41
35
  export function structureFormatFromName(nameOrUrl: string): StructureFormat {
42
36
  return PDB_EXTENSIONS.has(structureFileExtension(nameOrUrl)) ? 'pdb' : 'mmcif'
@@ -49,10 +43,9 @@ export function isBinaryStructureUrl(url: string) {
49
43
  }
50
44
 
51
45
  /**
52
- * Format for structure text that arrived without a filename — an inline `data`
53
- * snapshot, a pasted file, or the PDB the plugin synthesizes from Foldseek Cα
54
- * coordinates. Sniffs the content, which is strictly more reliable than a name
55
- * and is the only signal available here.
46
+ * Format for structure text with no filename, such as inline data, a pasted
47
+ * file, or a PDB built by `caCoordsToPdb`. The content is more reliable than a
48
+ * name, and here it is the only signal.
56
49
  *
57
50
  * mmCIF is line-oriented and its first meaningful line is a `data_` block
58
51
  * header (comments and blank lines may precede it); PDB has none.