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.
- package/README.md +220 -39
- package/dist/alignmentQuality.d.ts +5 -5
- package/dist/alphaFoldModels.d.ts +4 -4
- package/dist/alphaFoldModels.js +4 -4
- package/dist/chooseMappedEntity.d.ts +11 -13
- package/dist/chooseMappedEntity.js +9 -12
- package/dist/chooseMappedEntity.js.map +1 -1
- package/dist/coordinates.d.ts +19 -19
- package/dist/coordinates.js.map +1 -1
- package/dist/dbIds.d.ts +1 -2
- package/dist/extractPerResidueConfidence.d.ts +1 -2
- package/dist/extractStructureSequences.d.ts +26 -34
- package/dist/extractStructureSequences.js +2 -2
- package/dist/extractStructureSequences.js.map +1 -1
- package/dist/fetchUtils.d.ts +7 -7
- package/dist/fetchUtils.js +26 -8
- package/dist/fetchUtils.js.map +1 -1
- package/dist/index.d.ts +1 -3
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/dist/isoformRanking.d.ts +4 -1
- package/dist/mappings.d.ts +9 -12
- package/dist/mappings.js +4 -5
- package/dist/mappings.js.map +1 -1
- package/dist/pairwiseAlignment.d.ts +5 -4
- package/dist/pairwiseAlignment.js +9 -7
- package/dist/pairwiseAlignment.js.map +1 -1
- package/dist/pdbEntry.d.ts +32 -0
- package/dist/pdbEntry.js +182 -0
- package/dist/pdbEntry.js.map +1 -0
- package/dist/pdbUniProtMapping.d.ts +14 -39
- package/dist/pdbUniProtMapping.js +22 -140
- package/dist/pdbUniProtMapping.js.map +1 -1
- package/dist/structureFormat.d.ts +11 -18
- package/dist/structureFormat.js +4 -6
- package/dist/structureFormat.js.map +1 -1
- package/dist/structureSources.d.ts +37 -13
- package/dist/structureSources.js +144 -57
- package/dist/structureSources.js.map +1 -1
- package/dist/structureUrls.d.ts +13 -16
- package/dist/structureUrls.js +25 -23
- package/dist/structureUrls.js.map +1 -1
- package/dist/types.d.ts +2 -5
- package/dist/types.js +2 -5
- package/dist/types.js.map +1 -1
- package/dist/uniProtSearch.js +3 -8
- package/dist/uniProtSearch.js.map +1 -1
- package/esm/alignmentQuality.d.ts +5 -5
- package/esm/alphaFoldModels.d.ts +4 -4
- package/esm/alphaFoldModels.js +4 -4
- package/esm/chooseMappedEntity.d.ts +11 -13
- package/esm/chooseMappedEntity.js +9 -12
- package/esm/chooseMappedEntity.js.map +1 -1
- package/esm/coordinates.d.ts +19 -19
- package/esm/coordinates.js.map +1 -1
- package/esm/dbIds.d.ts +1 -2
- package/esm/extractPerResidueConfidence.d.ts +1 -2
- package/esm/extractStructureSequences.d.ts +26 -34
- package/esm/extractStructureSequences.js +2 -2
- package/esm/extractStructureSequences.js.map +1 -1
- package/esm/fetchUtils.d.ts +7 -7
- package/esm/fetchUtils.js +26 -8
- package/esm/fetchUtils.js.map +1 -1
- package/esm/index.d.ts +1 -3
- package/esm/index.js +2 -4
- package/esm/index.js.map +1 -1
- package/esm/isoformRanking.d.ts +4 -1
- package/esm/mappings.d.ts +9 -12
- package/esm/mappings.js +4 -5
- package/esm/mappings.js.map +1 -1
- package/esm/pairwiseAlignment.d.ts +5 -4
- package/esm/pairwiseAlignment.js +7 -5
- package/esm/pairwiseAlignment.js.map +1 -1
- package/esm/pdbEntry.d.ts +32 -0
- package/esm/pdbEntry.js +174 -0
- package/esm/pdbEntry.js.map +1 -0
- package/esm/pdbUniProtMapping.d.ts +14 -39
- package/esm/pdbUniProtMapping.js +22 -137
- package/esm/pdbUniProtMapping.js.map +1 -1
- package/esm/structureFormat.d.ts +11 -18
- package/esm/structureFormat.js +4 -6
- package/esm/structureFormat.js.map +1 -1
- package/esm/structureSources.d.ts +37 -13
- package/esm/structureSources.js +139 -55
- package/esm/structureSources.js.map +1 -1
- package/esm/structureUrls.d.ts +13 -16
- package/esm/structureUrls.js +23 -23
- package/esm/structureUrls.js.map +1 -1
- package/esm/types.d.ts +2 -5
- package/esm/types.js +2 -5
- package/esm/types.js.map +1 -1
- package/esm/uniProtSearch.js +3 -8
- package/esm/uniProtSearch.js.map +1 -1
- package/package.json +3 -2
- package/src/alignmentQuality.ts +5 -5
- package/src/alphaFoldModels.ts +4 -4
- package/src/chooseMappedEntity.ts +12 -15
- package/src/coordinates.ts +14 -17
- package/src/dbIds.ts +1 -1
- package/src/extractPerResidueConfidence.ts +1 -1
- package/src/extractStructureSequences.ts +26 -33
- package/src/fetchUtils.ts +30 -8
- package/src/index.ts +2 -4
- package/src/isoformRanking.ts +4 -4
- package/src/mappings.ts +9 -12
- package/src/pairwiseAlignment.ts +12 -9
- package/src/pdbEntry.ts +240 -0
- package/src/pdbUniProtMapping.ts +25 -164
- package/src/structureFormat.ts +11 -18
- package/src/structureSources.ts +194 -70
- package/src/structureUrls.ts +25 -23
- package/src/types.ts +2 -5
- package/src/uniProtSearch.ts +3 -8
- package/dist/pdbeBestStructures.d.ts +0 -23
- package/dist/pdbeBestStructures.js +0 -79
- package/dist/pdbeBestStructures.js.map +0 -1
- package/dist/pdbeEntryMolecules.d.ts +0 -7
- package/dist/pdbeEntryMolecules.js +0 -39
- package/dist/pdbeEntryMolecules.js.map +0 -1
- package/dist/siftsNumbering.d.ts +0 -23
- package/dist/siftsNumbering.js +0 -44
- package/dist/siftsNumbering.js.map +0 -1
- package/esm/pdbeBestStructures.d.ts +0 -23
- package/esm/pdbeBestStructures.js +0 -73
- package/esm/pdbeBestStructures.js.map +0 -1
- package/esm/pdbeEntryMolecules.d.ts +0 -7
- package/esm/pdbeEntryMolecules.js +0 -35
- package/esm/pdbeEntryMolecules.js.map +0 -1
- package/esm/siftsNumbering.d.ts +0 -23
- package/esm/siftsNumbering.js +0 -40
- package/esm/siftsNumbering.js.map +0 -1
- package/src/pdbeBestStructures.ts +0 -94
- package/src/pdbeEntryMolecules.ts +0 -37
- 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
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
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.
|
|
32
|
-
* rows must
|
|
33
|
-
*
|
|
34
|
-
*
|
|
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)
|
package/src/pairwiseAlignment.ts
CHANGED
|
@@ -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
|
-
|
|
397
|
-
|
|
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(
|
|
403
|
-
: needlemanWunsch(
|
|
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
|
|
418
|
-
|
|
419
|
-
|
|
417
|
+
export function alignSequences(
|
|
418
|
+
transcript: string,
|
|
419
|
+
structure: string,
|
|
420
420
|
algorithm: AlignmentAlgorithm,
|
|
421
421
|
): PairwiseAlignment {
|
|
422
|
-
return scoredAlignment(
|
|
422
|
+
return scoredAlignment(transcript, structure, algorithm).alignment
|
|
423
423
|
}
|
|
424
|
+
|
|
425
|
+
/** @deprecated renamed to `alignSequences`, which runs either algorithm */
|
|
426
|
+
export const runLocalAlignment = alignSequences
|
package/src/pdbEntry.ts
ADDED
|
@@ -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
|
+
}
|
package/src/pdbUniProtMapping.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
//
|
|
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).
|
|
10
|
-
//
|
|
9
|
+
// UniProt 14). The entry's own UniProt alignment, which `fetchPdbEntry` reads
|
|
10
|
+
// from RCSB, is the authority on that correspondence.
|
|
11
11
|
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
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
|
|
17
|
+
* structure-sequence positions, this package's native coordinate. */
|
|
24
18
|
export interface UniProtStructureSegment {
|
|
25
19
|
entityId: string
|
|
26
|
-
/** author chain id, when the
|
|
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
|
|
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.
|
|
194
|
-
* entities as the mmCIF does, and a PDB-format file loaded through
|
|
195
|
-
* them per chain (1H26.pdb's chain C is entity 2, which in
|
|
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
|
|
213
|
-
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
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
|
|
244
|
-
* transcript's, on a chain that fuses two. Local
|
|
245
|
-
* boundary, and on every GPCR fusion construct
|
|
246
|
-
* residues onto the partner, 33 of them onto T4
|
|
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
|
|
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
|
-
//
|
|
301
|
-
// repeat the same entity-level correspondence. They collapse to one
|
|
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 => {
|
package/src/structureFormat.ts
CHANGED
|
@@ -1,19 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Which molstar trajectory parser a structure needs.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
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
|
-
*
|
|
12
|
-
*
|
|
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
|
|
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
|
|
53
|
-
*
|
|
54
|
-
*
|
|
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.
|