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.
- package/LICENSE +21 -0
- package/README.md +148 -0
- package/dist/alignmentQuality.d.ts +58 -0
- package/dist/alignmentQuality.js +108 -0
- package/dist/alignmentQuality.js.map +1 -0
- package/dist/alphaFoldModels.d.ts +29 -0
- package/dist/alphaFoldModels.js +60 -0
- package/dist/alphaFoldModels.js.map +1 -0
- package/dist/caCoordsToPdb.d.ts +18 -0
- package/dist/caCoordsToPdb.js +95 -0
- package/dist/caCoordsToPdb.js.map +1 -0
- package/dist/chooseMappedEntity.d.ts +55 -0
- package/dist/chooseMappedEntity.js +140 -0
- package/dist/chooseMappedEntity.js.map +1 -0
- package/dist/coordinates.d.ts +66 -0
- package/dist/coordinates.js +66 -0
- package/dist/coordinates.js.map +1 -0
- package/dist/dbIds.d.ts +16 -0
- package/dist/dbIds.js +42 -0
- package/dist/dbIds.js.map +1 -0
- package/dist/extractPerResidueConfidence.d.ts +49 -0
- package/dist/extractPerResidueConfidence.js +42 -0
- package/dist/extractPerResidueConfidence.js.map +1 -0
- package/dist/extractStructureSequences.d.ts +142 -0
- package/dist/extractStructureSequences.js +217 -0
- package/dist/extractStructureSequences.js.map +1 -0
- package/dist/fetchUtils.d.ts +34 -0
- package/dist/fetchUtils.js +91 -0
- package/dist/fetchUtils.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/invertMap.d.ts +2 -0
- package/dist/invertMap.js +8 -0
- package/dist/invertMap.js.map +1 -0
- package/dist/isoformRanking.d.ts +47 -0
- package/dist/isoformRanking.js +91 -0
- package/dist/isoformRanking.js.map +1 -0
- package/dist/mappings.d.ts +58 -0
- package/dist/mappings.js +193 -0
- package/dist/mappings.js.map +1 -0
- package/dist/package.json +1 -0
- package/dist/pairwiseAlignment.d.ts +35 -0
- package/dist/pairwiseAlignment.js +337 -0
- package/dist/pairwiseAlignment.js.map +1 -0
- package/dist/pdbUniProtMapping.d.ts +75 -0
- package/dist/pdbUniProtMapping.js +241 -0
- package/dist/pdbUniProtMapping.js.map +1 -0
- package/dist/pdbeBestStructures.d.ts +23 -0
- package/dist/pdbeBestStructures.js +79 -0
- package/dist/pdbeBestStructures.js.map +1 -0
- package/dist/pdbeEntryMolecules.d.ts +7 -0
- package/dist/pdbeEntryMolecules.js +39 -0
- package/dist/pdbeEntryMolecules.js.map +1 -0
- package/dist/siftsNumbering.d.ts +23 -0
- package/dist/siftsNumbering.js +44 -0
- package/dist/siftsNumbering.js.map +1 -0
- package/dist/stripStopCodon.d.ts +15 -0
- package/dist/stripStopCodon.js +24 -0
- package/dist/stripStopCodon.js.map +1 -0
- package/dist/structureFormat.d.ts +40 -0
- package/dist/structureFormat.js +54 -0
- package/dist/structureFormat.js.map +1 -0
- package/dist/structureSources.d.ts +28 -0
- package/dist/structureSources.js +76 -0
- package/dist/structureSources.js.map +1 -0
- package/dist/structureUrls.d.ts +54 -0
- package/dist/structureUrls.js +147 -0
- package/dist/structureUrls.js.map +1 -0
- package/dist/types.d.ts +14 -0
- package/dist/types.js +27 -0
- package/dist/types.js.map +1 -0
- package/dist/uniProtSearch.d.ts +32 -0
- package/dist/uniProtSearch.js +114 -0
- package/dist/uniProtSearch.js.map +1 -0
- package/esm/alignmentQuality.d.ts +58 -0
- package/esm/alignmentQuality.js +100 -0
- package/esm/alignmentQuality.js.map +1 -0
- package/esm/alphaFoldModels.d.ts +29 -0
- package/esm/alphaFoldModels.js +55 -0
- package/esm/alphaFoldModels.js.map +1 -0
- package/esm/caCoordsToPdb.d.ts +18 -0
- package/esm/caCoordsToPdb.js +91 -0
- package/esm/caCoordsToPdb.js.map +1 -0
- package/esm/chooseMappedEntity.d.ts +55 -0
- package/esm/chooseMappedEntity.js +134 -0
- package/esm/chooseMappedEntity.js.map +1 -0
- package/esm/coordinates.d.ts +66 -0
- package/esm/coordinates.js +58 -0
- package/esm/coordinates.js.map +1 -0
- package/esm/dbIds.d.ts +16 -0
- package/esm/dbIds.js +35 -0
- package/esm/dbIds.js.map +1 -0
- package/esm/extractPerResidueConfidence.d.ts +49 -0
- package/esm/extractPerResidueConfidence.js +38 -0
- package/esm/extractPerResidueConfidence.js.map +1 -0
- package/esm/extractStructureSequences.d.ts +142 -0
- package/esm/extractStructureSequences.js +205 -0
- package/esm/extractStructureSequences.js.map +1 -0
- package/esm/fetchUtils.d.ts +34 -0
- package/esm/fetchUtils.js +82 -0
- package/esm/fetchUtils.js.map +1 -0
- package/esm/index.d.ts +23 -0
- package/esm/index.js +30 -0
- package/esm/index.js.map +1 -0
- package/esm/invertMap.d.ts +2 -0
- package/esm/invertMap.js +5 -0
- package/esm/invertMap.js.map +1 -0
- package/esm/isoformRanking.d.ts +47 -0
- package/esm/isoformRanking.js +86 -0
- package/esm/isoformRanking.js.map +1 -0
- package/esm/mappings.d.ts +58 -0
- package/esm/mappings.js +179 -0
- package/esm/mappings.js.map +1 -0
- package/esm/pairwiseAlignment.d.ts +35 -0
- package/esm/pairwiseAlignment.js +328 -0
- package/esm/pairwiseAlignment.js.map +1 -0
- package/esm/pdbUniProtMapping.d.ts +75 -0
- package/esm/pdbUniProtMapping.js +231 -0
- package/esm/pdbUniProtMapping.js.map +1 -0
- package/esm/pdbeBestStructures.d.ts +23 -0
- package/esm/pdbeBestStructures.js +73 -0
- package/esm/pdbeBestStructures.js.map +1 -0
- package/esm/pdbeEntryMolecules.d.ts +7 -0
- package/esm/pdbeEntryMolecules.js +35 -0
- package/esm/pdbeEntryMolecules.js.map +1 -0
- package/esm/siftsNumbering.d.ts +23 -0
- package/esm/siftsNumbering.js +40 -0
- package/esm/siftsNumbering.js.map +1 -0
- package/esm/stripStopCodon.d.ts +15 -0
- package/esm/stripStopCodon.js +20 -0
- package/esm/stripStopCodon.js.map +1 -0
- package/esm/structureFormat.d.ts +40 -0
- package/esm/structureFormat.js +48 -0
- package/esm/structureFormat.js.map +1 -0
- package/esm/structureSources.d.ts +28 -0
- package/esm/structureSources.js +71 -0
- package/esm/structureSources.js.map +1 -0
- package/esm/structureUrls.d.ts +54 -0
- package/esm/structureUrls.js +134 -0
- package/esm/structureUrls.js.map +1 -0
- package/esm/types.d.ts +14 -0
- package/esm/types.js +23 -0
- package/esm/types.js.map +1 -0
- package/esm/uniProtSearch.d.ts +32 -0
- package/esm/uniProtSearch.js +110 -0
- package/esm/uniProtSearch.js.map +1 -0
- package/package.json +63 -0
- package/src/alignmentQuality.ts +132 -0
- package/src/alphaFoldModels.ts +91 -0
- package/src/caCoordsToPdb.ts +111 -0
- package/src/chooseMappedEntity.ts +198 -0
- package/src/coordinates.ts +119 -0
- package/src/dbIds.ts +46 -0
- package/src/extractPerResidueConfidence.ts +88 -0
- package/src/extractStructureSequences.ts +342 -0
- package/src/fetchUtils.ts +110 -0
- package/src/index.ts +34 -0
- package/src/invertMap.ts +4 -0
- package/src/isoformRanking.ts +139 -0
- package/src/mappings.ts +230 -0
- package/src/pairwiseAlignment.ts +423 -0
- package/src/pdbUniProtMapping.ts +320 -0
- package/src/pdbeBestStructures.ts +94 -0
- package/src/pdbeEntryMolecules.ts +37 -0
- package/src/siftsNumbering.ts +61 -0
- package/src/stripStopCodon.ts +20 -0
- package/src/structureFormat.ts +70 -0
- package/src/structureSources.ts +101 -0
- package/src/structureUrls.ts +171 -0
- package/src/types.ts +30 -0
- package/src/uniProtSearch.ts +212 -0
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/** A polymer entity of a loaded structure: its mmCIF entity id, its one-letter
|
|
2
|
+
* sequence, and the molstar `label_seq_id` of each of those residues.
|
|
3
|
+
*
|
|
4
|
+
* The entity id is what lets every downstream step talk about "the gene's
|
|
5
|
+
* protein" by identity instead of by the fragile entity-[0] position.
|
|
6
|
+
*
|
|
7
|
+
* `seqIds` is what lets it talk about a *residue* by identity. Structure
|
|
8
|
+
* positions in this plugin are 0-based indices into `seq`; molstar addresses
|
|
9
|
+
* residues by `label_seq_id`. Those are related by `+1` only when the entity's
|
|
10
|
+
* sequence covers every residue from 1 — true for any mmCIF with an
|
|
11
|
+
* `entity_poly_seq` category (all of RCSB and AlphaFold) and for PDB files
|
|
12
|
+
* carrying SEQRES records, because molstar synthesizes the category from them.
|
|
13
|
+
*
|
|
14
|
+
* A PDB-format file with no SEQRES has no such category, so molstar falls back
|
|
15
|
+
* to `StructureSequence.fromHierarchy` and takes `label_seq_id` straight from
|
|
16
|
+
* the observed residues' author numbering: a chain whose coordinates start at
|
|
17
|
+
* residue 94 yields seqIds 94.., and an unobserved loop leaves a hole. Deriving
|
|
18
|
+
* the id arithmetically there is off by the whole offset, in both directions —
|
|
19
|
+
* every hover reads the wrong residue and every highlight paints the wrong one.
|
|
20
|
+
* So carry the real ids and convert through them.
|
|
21
|
+
*/
|
|
22
|
+
export interface Entity {
|
|
23
|
+
entityId: string
|
|
24
|
+
seq: string
|
|
25
|
+
seqIds: number[]
|
|
26
|
+
/** author chain ids carrying this entity, e.g. ['A', 'B'] for a homodimer;
|
|
27
|
+
* what a user recognises from the PDB entry page, where the entity id is
|
|
28
|
+
* molstar's own bookkeeping */
|
|
29
|
+
chains: string[]
|
|
30
|
+
/** DNA, RNA or a hybrid, by molstar's entity subtype. Its one-letter
|
|
31
|
+
* sequence spells amino acids too (A, C, G, T, U), so it has to be kept
|
|
32
|
+
* out of the transcript alignment rather than merely scoring low. */
|
|
33
|
+
nucleicAcid?: boolean
|
|
34
|
+
/** `auth_seq_id` per position: the numbering the depositors chose, which
|
|
35
|
+
* for an RCSB entry is the one papers and UniProt cite (1TUP's position 154
|
|
36
|
+
* is R248) and what Mol*'s own hover label shows. Display only; every
|
|
37
|
+
* coordinate the plugin computes with stays a 0-based position, and molstar
|
|
38
|
+
* is addressed through `seqIds`. Absent when the model has no atomic
|
|
39
|
+
* hierarchy to read it from. See `residueNumber`. */
|
|
40
|
+
authSeqIds?: number[]
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface Column<T> {
|
|
44
|
+
rowCount: number
|
|
45
|
+
value(row: number): T
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface StructureModel {
|
|
49
|
+
obj?: {
|
|
50
|
+
data: {
|
|
51
|
+
entities?: {
|
|
52
|
+
subtype: Column<string>
|
|
53
|
+
getEntityIndex(id: string): number
|
|
54
|
+
}
|
|
55
|
+
sourceData?: { data: unknown }
|
|
56
|
+
sequence: {
|
|
57
|
+
sequences: readonly {
|
|
58
|
+
entityId: string
|
|
59
|
+
sequence: {
|
|
60
|
+
code: {
|
|
61
|
+
toArray(): ArrayLike<string>
|
|
62
|
+
}
|
|
63
|
+
seqId: {
|
|
64
|
+
toArray(): ArrayLike<number>
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}[]
|
|
68
|
+
}
|
|
69
|
+
atomicHierarchy?: {
|
|
70
|
+
chains: {
|
|
71
|
+
label_entity_id: Column<string>
|
|
72
|
+
auth_asym_id: Column<string>
|
|
73
|
+
}
|
|
74
|
+
residues?: {
|
|
75
|
+
label_seq_id: Column<number>
|
|
76
|
+
auth_seq_id: Column<number>
|
|
77
|
+
}
|
|
78
|
+
residueAtomSegments?: { offsets: ArrayLike<number>; count: number }
|
|
79
|
+
chainAtomSegments?: { index: ArrayLike<number> }
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Observed residues' label_seq_id -> auth_seq_id, per entity. The first chain
|
|
86
|
+
* carrying an entity wins, so a homodimer's copies agree. */
|
|
87
|
+
function authSeqIdsByEntity(model: StructureModel) {
|
|
88
|
+
const hierarchy = model.obj?.data.atomicHierarchy
|
|
89
|
+
const byEntity = new Map<string, Map<number, number>>()
|
|
90
|
+
const { residues, residueAtomSegments, chainAtomSegments, chains } =
|
|
91
|
+
hierarchy ?? {}
|
|
92
|
+
if (!residues || !residueAtomSegments || !chainAtomSegments || !chains) {
|
|
93
|
+
return undefined
|
|
94
|
+
}
|
|
95
|
+
for (let residue = 0; residue < residueAtomSegments.count; residue++) {
|
|
96
|
+
const atom = residueAtomSegments.offsets[residue]!
|
|
97
|
+
const entityId = chains.label_entity_id.value(
|
|
98
|
+
chainAtomSegments.index[atom]!,
|
|
99
|
+
)
|
|
100
|
+
let ids = byEntity.get(entityId)
|
|
101
|
+
if (!ids) {
|
|
102
|
+
ids = new Map()
|
|
103
|
+
byEntity.set(entityId, ids)
|
|
104
|
+
}
|
|
105
|
+
const labelSeqId = residues.label_seq_id.value(residue)
|
|
106
|
+
if (!ids.has(labelSeqId)) {
|
|
107
|
+
ids.set(labelSeqId, residues.auth_seq_id.value(residue))
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return byEntity
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Author numbering for every SEQRES position, observed or not. A residue with
|
|
115
|
+
* no atoms has no auth_seq_id of its own, so it takes the offset of the nearest
|
|
116
|
+
* observed residue before it (after it, at an unobserved N-terminus): a
|
|
117
|
+
* disordered loop keeps counting the way the paper does. With nothing observed
|
|
118
|
+
* the label numbering stands.
|
|
119
|
+
*/
|
|
120
|
+
export function fillAuthSeqIds(
|
|
121
|
+
seqIds: number[],
|
|
122
|
+
observed: Map<number, number> | undefined,
|
|
123
|
+
) {
|
|
124
|
+
const out: number[] = []
|
|
125
|
+
let offset: number | undefined
|
|
126
|
+
const firstObserved = seqIds.find(id => observed?.has(id))
|
|
127
|
+
if (firstObserved !== undefined) {
|
|
128
|
+
offset = observed!.get(firstObserved)! - firstObserved
|
|
129
|
+
}
|
|
130
|
+
for (const id of seqIds) {
|
|
131
|
+
const auth = observed?.get(id)
|
|
132
|
+
if (auth !== undefined) {
|
|
133
|
+
offset = auth - id
|
|
134
|
+
}
|
|
135
|
+
out.push(id + (offset ?? 0))
|
|
136
|
+
}
|
|
137
|
+
return out
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The number a residue is called by: author numbering when the file carries
|
|
141
|
+
* it, else molstar's label_seq_id, which is the author numbering already for a
|
|
142
|
+
* SEQRES-less PDB and 1..N for everything else. */
|
|
143
|
+
export function residueNumber(entity: Entity | undefined, pos: number) {
|
|
144
|
+
return entity?.authSeqIds?.[pos] ?? entity?.seqIds[pos] ?? pos + 1
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The 0-based half-open position range covering an inclusive range of author
|
|
149
|
+
* residue numbers, the form a paper or a spec names a site by ("R248",
|
|
150
|
+
* "residues 102-292"). Undefined when no residue of the entity carries a number
|
|
151
|
+
* in the range, so a typo selects nothing rather than something else.
|
|
152
|
+
*/
|
|
153
|
+
export function residueRangeToPositions(
|
|
154
|
+
entity: Entity | undefined,
|
|
155
|
+
range: { start: number; end: number },
|
|
156
|
+
) {
|
|
157
|
+
if (!entity) {
|
|
158
|
+
return undefined
|
|
159
|
+
}
|
|
160
|
+
let first: number | undefined
|
|
161
|
+
let last: number | undefined
|
|
162
|
+
for (let pos = 0; pos < entity.seq.length; pos++) {
|
|
163
|
+
const n = residueNumber(entity, pos)
|
|
164
|
+
if (n >= range.start && n <= range.end) {
|
|
165
|
+
first ??= pos
|
|
166
|
+
last = pos
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return first === undefined || last === undefined
|
|
170
|
+
? undefined
|
|
171
|
+
: { start: first, end: last + 1 }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function chainsByEntity(model: StructureModel) {
|
|
175
|
+
const chains = model.obj?.data.atomicHierarchy?.chains
|
|
176
|
+
const byEntity = new Map<string, string[]>()
|
|
177
|
+
if (chains) {
|
|
178
|
+
for (let i = 0; i < chains.label_entity_id.rowCount; i++) {
|
|
179
|
+
const entityId = chains.label_entity_id.value(i)
|
|
180
|
+
const chain = chains.auth_asym_id.value(i)
|
|
181
|
+
const list = byEntity.get(entityId) ?? []
|
|
182
|
+
if (!list.includes(chain)) {
|
|
183
|
+
list.push(chain)
|
|
184
|
+
}
|
|
185
|
+
byEntity.set(entityId, list)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return byEntity
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// mmCIF entity_poly.type values for nucleic-acid polymers. Molstar derives the
|
|
192
|
+
// subtype from residue components when a file (PDB format) carries no
|
|
193
|
+
// entity_poly, so the flag is available for either format.
|
|
194
|
+
const NUCLEIC_ACID_SUBTYPES = new Set([
|
|
195
|
+
'polydeoxyribonucleotide',
|
|
196
|
+
'polyribonucleotide',
|
|
197
|
+
'polydeoxyribonucleotide/polyribonucleotide hybrid',
|
|
198
|
+
'peptide nucleic acid',
|
|
199
|
+
])
|
|
200
|
+
|
|
201
|
+
function isNucleicAcid(model: StructureModel, entityId: string) {
|
|
202
|
+
const entities = model.obj?.data.entities
|
|
203
|
+
if (!entities) {
|
|
204
|
+
return false
|
|
205
|
+
}
|
|
206
|
+
const index = entities.getEntityIndex(entityId)
|
|
207
|
+
return index >= 0 && NUCLEIC_ACID_SUBTYPES.has(entities.subtype.value(index))
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** The slice of molstar's MmcifFormat data read here; other formats have no
|
|
211
|
+
* `db` */
|
|
212
|
+
interface MmcifSourceData {
|
|
213
|
+
db?: {
|
|
214
|
+
entity_poly?: {
|
|
215
|
+
entity_id: Column<string>
|
|
216
|
+
pdbx_seq_one_letter_code_can: Column<string>
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function canonicalSequencesByEntity(model: StructureModel) {
|
|
222
|
+
const source = model.obj?.data.sourceData?.data as MmcifSourceData | undefined
|
|
223
|
+
const entityPoly = source?.db?.entity_poly
|
|
224
|
+
const byEntity = new Map<string, string>()
|
|
225
|
+
if (entityPoly) {
|
|
226
|
+
for (let i = 0; i < entityPoly.entity_id.rowCount; i++) {
|
|
227
|
+
byEntity.set(
|
|
228
|
+
entityPoly.entity_id.value(i),
|
|
229
|
+
entityPoly.pdbx_seq_one_letter_code_can.value(i).replaceAll(/\s/g, ''),
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return byEntity
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* One letter per position, so `seq[pos]` and `seqIds[pos]` name the same
|
|
238
|
+
* residue. Molstar's `label` column is not that: it spells a residue with no
|
|
239
|
+
* one-letter code by its component id (MSE, TPO, ACE) and an alternate site as
|
|
240
|
+
* "(S|P)", which shifted every later position. `code` writes X for those; the
|
|
241
|
+
* mmCIF canonical sequence then supplies the parent residue (MSE is M) when it
|
|
242
|
+
* has one letter per position, which a chromophore spelled "TYG" does not.
|
|
243
|
+
*/
|
|
244
|
+
export function oneLetterSequence(
|
|
245
|
+
codes: ArrayLike<string>,
|
|
246
|
+
canonical: string | undefined,
|
|
247
|
+
) {
|
|
248
|
+
const letters = Array.from(codes)
|
|
249
|
+
return letters
|
|
250
|
+
.map((c, i) =>
|
|
251
|
+
c === 'X' && canonical?.length === letters.length ? canonical[i]! : c,
|
|
252
|
+
)
|
|
253
|
+
.join('')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function extractEntities(model: StructureModel): Entity[] | undefined {
|
|
257
|
+
const chains = chainsByEntity(model)
|
|
258
|
+
const authIds = authSeqIdsByEntity(model)
|
|
259
|
+
const canonical = canonicalSequencesByEntity(model)
|
|
260
|
+
return model.obj?.data.sequence.sequences.map(s => {
|
|
261
|
+
const seqIds = Array.from(s.sequence.seqId.toArray())
|
|
262
|
+
return {
|
|
263
|
+
entityId: s.entityId,
|
|
264
|
+
seq: oneLetterSequence(
|
|
265
|
+
s.sequence.code.toArray(),
|
|
266
|
+
canonical.get(s.entityId),
|
|
267
|
+
),
|
|
268
|
+
seqIds,
|
|
269
|
+
chains: chains.get(s.entityId) ?? [],
|
|
270
|
+
...(isNucleicAcid(model, s.entityId) ? { nucleicAcid: true } : {}),
|
|
271
|
+
...(authIds
|
|
272
|
+
? { authSeqIds: fillAuthSeqIds(seqIds, authIds.get(s.entityId)) }
|
|
273
|
+
: {}),
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** A user-facing name for an entity: its chains when known, else its id. */
|
|
279
|
+
export function entityLabel(entity: Entity) {
|
|
280
|
+
const chains =
|
|
281
|
+
entity.chains.length > 0
|
|
282
|
+
? `Chain ${entity.chains.join('/')}`
|
|
283
|
+
: `Entity ${entity.entityId}`
|
|
284
|
+
return `${chains} (${entity.seq.length} ${entity.nucleicAcid ? 'nt' : 'aa'})`
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** The protein chains' sequences, for the launch dialog's isoform matching,
|
|
288
|
+
* which has no use for a DNA strand that happens to align. */
|
|
289
|
+
export function extractStructureSequences(
|
|
290
|
+
model: StructureModel,
|
|
291
|
+
): string[] | undefined {
|
|
292
|
+
return extractEntities(model)
|
|
293
|
+
?.filter(e => !e.nucleicAcid)
|
|
294
|
+
.map(e => e.seq)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* The molstar `label_seq_id`s for a set of 0-based structure positions. Unknown
|
|
299
|
+
* positions are dropped rather than guessed, so an out-of-range position paints
|
|
300
|
+
* nothing instead of painting something wrong.
|
|
301
|
+
*/
|
|
302
|
+
export function toLabelSeqIds(
|
|
303
|
+
entity: Entity | undefined,
|
|
304
|
+
positions: Iterable<number>,
|
|
305
|
+
): number[] {
|
|
306
|
+
if (!entity) {
|
|
307
|
+
return []
|
|
308
|
+
}
|
|
309
|
+
const out: number[] = []
|
|
310
|
+
for (const pos of positions) {
|
|
311
|
+
const id = entity.seqIds[pos]
|
|
312
|
+
if (id !== undefined) {
|
|
313
|
+
out.push(id)
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return out
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** As toLabelSeqIds, for a half-open [start, end) structure-position range. */
|
|
320
|
+
export function rangeToLabelSeqIds(
|
|
321
|
+
entity: Entity | undefined,
|
|
322
|
+
range: { start: number; end: number } | undefined,
|
|
323
|
+
): number[] {
|
|
324
|
+
if (!entity || !range) {
|
|
325
|
+
return []
|
|
326
|
+
}
|
|
327
|
+
const start = Math.max(0, range.start)
|
|
328
|
+
const end = Math.min(entity.seqIds.length, range.end)
|
|
329
|
+
return end > start ? entity.seqIds.slice(start, end) : []
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Reverse of `seqIds`: molstar's label_seq_id -> 0-based structure position. */
|
|
333
|
+
export function makeLabelSeqIdIndex(entity: Entity | undefined) {
|
|
334
|
+
const index = new Map<number, number>()
|
|
335
|
+
entity?.seqIds.forEach((id, pos) => {
|
|
336
|
+
// first wins: duplicate ids (micro-heterogeneity) collapse to one residue
|
|
337
|
+
if (!index.has(id)) {
|
|
338
|
+
index.set(id, pos)
|
|
339
|
+
}
|
|
340
|
+
})
|
|
341
|
+
return index
|
|
342
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
function hostOf(url: string) {
|
|
2
|
+
try {
|
|
3
|
+
return new URL(url).host
|
|
4
|
+
} catch {
|
|
5
|
+
return url
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const MAX_DETAIL = 200
|
|
10
|
+
|
|
11
|
+
function firstLine(body: string) {
|
|
12
|
+
const line = body.split('\n', 1)[0]?.trim() ?? ''
|
|
13
|
+
return line.length > MAX_DETAIL ? `${line.slice(0, MAX_DETAIL)}…` : line
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The error a failed response deserves: status and host, plus at most the first
|
|
18
|
+
* line of the body. A whole body is an HTML error page or a megabyte of JSON,
|
|
19
|
+
* and pasting it into the dialog buries the one line that says what went wrong.
|
|
20
|
+
*/
|
|
21
|
+
export async function httpError(response: Response, url: string) {
|
|
22
|
+
const detail = firstLine(await response.text().catch(() => ''))
|
|
23
|
+
const status = `HTTP ${response.status}${response.statusText ? ` ${response.statusText}` : ''} from ${hostOf(url)}`
|
|
24
|
+
return new Error(detail ? `${status}: ${detail}` : status)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* `fetch` rejects with a bare `TypeError: Failed to fetch` for a DNS failure,
|
|
29
|
+
* an offline browser and a refused cross-origin request alike, naming neither
|
|
30
|
+
* the host nor the cause. Anything else — an abort, most notably — passes
|
|
31
|
+
* through untouched.
|
|
32
|
+
*/
|
|
33
|
+
export function networkError(url: string, cause: unknown) {
|
|
34
|
+
return cause instanceof TypeError
|
|
35
|
+
? new Error(
|
|
36
|
+
`Could not reach ${hostOf(url)}. Check your network connection; the server may also be refusing cross-origin requests from this page.`,
|
|
37
|
+
{ cause },
|
|
38
|
+
)
|
|
39
|
+
: cause
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Request options plus the `fetch` to make the request with. Every network
|
|
44
|
+
* function here takes them, so a caller can hand in a host's instrumented
|
|
45
|
+
* fetch, a test double, or nothing at all and get `globalThis.fetch`. Resolved
|
|
46
|
+
* per call rather than captured at import, so a page that installs its fetch
|
|
47
|
+
* late is still honored.
|
|
48
|
+
*/
|
|
49
|
+
export interface FetchOptions extends RequestInit {
|
|
50
|
+
fetch?: typeof globalThis.fetch
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function rawfetch(url: string, args?: FetchOptions) {
|
|
54
|
+
const { fetch: f = globalThis.fetch, ...init } = args ?? {}
|
|
55
|
+
try {
|
|
56
|
+
return await f(url, init)
|
|
57
|
+
} catch (e) {
|
|
58
|
+
throw networkError(url, e)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function myfetch(url: string, args?: FetchOptions) {
|
|
63
|
+
const response = await rawfetch(url, args)
|
|
64
|
+
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
throw await httpError(response, url)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return response
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function jsonfetch<T = unknown>(
|
|
73
|
+
url: string,
|
|
74
|
+
args?: FetchOptions,
|
|
75
|
+
): Promise<T> {
|
|
76
|
+
// The caller names the shape it expects; `Response.json` promises `any`, and
|
|
77
|
+
// narrowing it here rather than at the return keeps the body free of casts.
|
|
78
|
+
const response: { json: () => Promise<T> } = await myfetch(url, args)
|
|
79
|
+
return response.json()
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* An AbortSignal's reason as a real Error. `signal.reason` is `any` — usually a
|
|
84
|
+
* DOMException, but a caller can abort with anything at all — and throwing a
|
|
85
|
+
* non-Error loses the stack and breaks `instanceof Error` checks in the UI's
|
|
86
|
+
* error rendering. Normalize at every throw site.
|
|
87
|
+
*/
|
|
88
|
+
export function abortError(signal: AbortSignal) {
|
|
89
|
+
return signal.reason instanceof Error
|
|
90
|
+
? signal.reason
|
|
91
|
+
: new Error('Aborted', { cause: signal.reason })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function timeout(time: number, signal?: AbortSignal) {
|
|
95
|
+
return new Promise<void>((resolve, reject) => {
|
|
96
|
+
if (signal?.aborted) {
|
|
97
|
+
reject(abortError(signal))
|
|
98
|
+
} else {
|
|
99
|
+
const id = setTimeout(resolve, time)
|
|
100
|
+
signal?.addEventListener(
|
|
101
|
+
'abort',
|
|
102
|
+
() => {
|
|
103
|
+
clearTimeout(id)
|
|
104
|
+
reject(abortError(signal))
|
|
105
|
+
},
|
|
106
|
+
{ once: true },
|
|
107
|
+
)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Alignment
|
|
2
|
+
export * from './types.ts'
|
|
3
|
+
export * from './mappings.ts'
|
|
4
|
+
export * from './pairwiseAlignment.ts'
|
|
5
|
+
export * from './alignmentQuality.ts'
|
|
6
|
+
|
|
7
|
+
// Coordinates
|
|
8
|
+
export * from './coordinates.ts'
|
|
9
|
+
export * from './invertMap.ts'
|
|
10
|
+
export * from './stripStopCodon.ts'
|
|
11
|
+
|
|
12
|
+
// Chain choice
|
|
13
|
+
export * from './chooseMappedEntity.ts'
|
|
14
|
+
export * from './extractStructureSequences.ts'
|
|
15
|
+
export * from './extractPerResidueConfidence.ts'
|
|
16
|
+
export * from './isoformRanking.ts'
|
|
17
|
+
|
|
18
|
+
// SIFTS
|
|
19
|
+
export * from './pdbUniProtMapping.ts'
|
|
20
|
+
export * from './siftsNumbering.ts'
|
|
21
|
+
|
|
22
|
+
// Structure sources
|
|
23
|
+
export * from './alphaFoldModels.ts'
|
|
24
|
+
export * from './pdbeBestStructures.ts'
|
|
25
|
+
export * from './pdbeEntryMolecules.ts'
|
|
26
|
+
export * from './structureSources.ts'
|
|
27
|
+
export * from './uniProtSearch.ts'
|
|
28
|
+
export * from './dbIds.ts'
|
|
29
|
+
|
|
30
|
+
// Urls and formats
|
|
31
|
+
export * from './structureUrls.ts'
|
|
32
|
+
export * from './structureFormat.ts'
|
|
33
|
+
export * from './caCoordsToPdb.ts'
|
|
34
|
+
export * from './fetchUtils.ts'
|
package/src/invertMap.ts
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { stripStopCodon } from './stripStopCodon.ts'
|
|
2
|
+
import {
|
|
3
|
+
alignTranscriptToEntity,
|
|
4
|
+
chooseMappedEntity,
|
|
5
|
+
} from './chooseMappedEntity.ts'
|
|
6
|
+
|
|
7
|
+
/** A transcript isoform, named by whatever id the caller ranks by. `seq` is its
|
|
8
|
+
* translation, absent when none could be computed. */
|
|
9
|
+
export interface Isoform {
|
|
10
|
+
id: string
|
|
11
|
+
seq?: string
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RankedIsoform {
|
|
15
|
+
id: string
|
|
16
|
+
length: number
|
|
17
|
+
/** identical residues against the structure, for an isoform that does not
|
|
18
|
+
* match it exactly; undefined with no structure to compare to */
|
|
19
|
+
identical?: number
|
|
20
|
+
/** that alignment's score, which is what ranks non-matching isoforms */
|
|
21
|
+
score?: number
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ClassifiedIsoforms {
|
|
25
|
+
// protein matches the structure residues, longest first
|
|
26
|
+
matches: RankedIsoform[]
|
|
27
|
+
// has a protein sequence but doesn't match the structure, best-aligning first
|
|
28
|
+
nonMatches: RankedIsoform[]
|
|
29
|
+
// ids of the isoforms no protein sequence could be computed for
|
|
30
|
+
noData: string[]
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The picker re-ranks on every render and each non-exact isoform costs an
|
|
34
|
+
// O(len²) alignment, so results are cached by sequence pair: a titin-sized
|
|
35
|
+
// isoform against a long chain is a second of main-thread work each time.
|
|
36
|
+
const alignmentCache = new Map<string, { identical: number; score: number }>()
|
|
37
|
+
const ALIGNMENT_CACHE_LIMIT = 200
|
|
38
|
+
|
|
39
|
+
function alignToStructure(isoform: string, structure: string) {
|
|
40
|
+
const key = `${isoform}\n${structure}`
|
|
41
|
+
let result = alignmentCache.get(key)
|
|
42
|
+
if (result === undefined) {
|
|
43
|
+
const scored = alignTranscriptToEntity(isoform, structure, 'smith_waterman')
|
|
44
|
+
result = { identical: scored?.matches ?? 0, score: scored?.score ?? 0 }
|
|
45
|
+
if (alignmentCache.size >= ALIGNMENT_CACHE_LIMIT) {
|
|
46
|
+
alignmentCache.clear()
|
|
47
|
+
}
|
|
48
|
+
alignmentCache.set(key, result)
|
|
49
|
+
}
|
|
50
|
+
return result
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The single rule for ranking transcript isoforms against a structure, shared
|
|
55
|
+
* by a picker UI and an auto-selection. An isoform whose translation is the
|
|
56
|
+
* structure's sequence wins outright. Among the rest, the best alignment score
|
|
57
|
+
* comes first, then the most identical residues, then length.
|
|
58
|
+
*
|
|
59
|
+
* The score has to lead: an isoform carrying an exon the structure lacks
|
|
60
|
+
* aligns every structure residue too, across a gap, so it ties on identical
|
|
61
|
+
* residues with the isoform the structure was made from (1MH1 against Rac1
|
|
62
|
+
* and Rac1b).
|
|
63
|
+
*/
|
|
64
|
+
export function classifyIsoforms({
|
|
65
|
+
isoforms,
|
|
66
|
+
structureSequence,
|
|
67
|
+
}: {
|
|
68
|
+
isoforms: readonly Isoform[]
|
|
69
|
+
structureSequence?: string
|
|
70
|
+
}): ClassifiedIsoforms {
|
|
71
|
+
const matches: RankedIsoform[] = []
|
|
72
|
+
const nonMatches: RankedIsoform[] = []
|
|
73
|
+
const noData: string[] = []
|
|
74
|
+
const structure = structureSequence
|
|
75
|
+
? stripStopCodon(structureSequence)
|
|
76
|
+
: undefined
|
|
77
|
+
for (const { id, seq } of isoforms) {
|
|
78
|
+
if (seq === undefined) {
|
|
79
|
+
noData.push(id)
|
|
80
|
+
} else if (structure && stripStopCodon(seq) === structure) {
|
|
81
|
+
matches.push({ id, length: seq.length })
|
|
82
|
+
} else {
|
|
83
|
+
nonMatches.push({
|
|
84
|
+
id,
|
|
85
|
+
length: seq.length,
|
|
86
|
+
...(structure ? alignToStructure(seq, structure) : {}),
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const byLengthDesc = (a: RankedIsoform, b: RankedIsoform) =>
|
|
91
|
+
b.length - a.length
|
|
92
|
+
const byAlignmentThenLength = (a: RankedIsoform, b: RankedIsoform) =>
|
|
93
|
+
(b.score ?? 0) - (a.score ?? 0) ||
|
|
94
|
+
(b.identical ?? 0) - (a.identical ?? 0) ||
|
|
95
|
+
byLengthDesc(a, b)
|
|
96
|
+
return {
|
|
97
|
+
matches: matches.toSorted(byLengthDesc),
|
|
98
|
+
nonMatches: nonMatches.toSorted(byAlignmentThenLength),
|
|
99
|
+
noData,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** The id of the isoform to open, by the ranking above. */
|
|
104
|
+
export function selectBestTranscript(args: {
|
|
105
|
+
isoforms: readonly Isoform[]
|
|
106
|
+
structureSequence?: string
|
|
107
|
+
}) {
|
|
108
|
+
const { matches, nonMatches } = classifyIsoforms(args)
|
|
109
|
+
return (matches[0] ?? nonMatches[0])?.id
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Which of a structure's protein chains isoforms are compared against: one some
|
|
114
|
+
* isoform translates to exactly, else the chain `chooseMappedEntity` would map
|
|
115
|
+
* the longest isoform to, never simply the first (CDK2, on 1H26).
|
|
116
|
+
* `structureSequences` must hold protein chains only.
|
|
117
|
+
*/
|
|
118
|
+
export function pickStructureSequence(
|
|
119
|
+
structureSequences: string[] | undefined,
|
|
120
|
+
isoforms: readonly Isoform[] | undefined,
|
|
121
|
+
): string | undefined {
|
|
122
|
+
const translations = (isoforms ?? []).flatMap(({ seq }) =>
|
|
123
|
+
seq === undefined ? [] : [stripStopCodon(seq)],
|
|
124
|
+
)
|
|
125
|
+
const translated = new Set(translations)
|
|
126
|
+
const exact = structureSequences?.find(s => translated.has(stripStopCodon(s)))
|
|
127
|
+
const longest = translations.reduce<string | undefined>(
|
|
128
|
+
(a, b) => (a !== undefined && a.length >= b.length ? a : b),
|
|
129
|
+
undefined,
|
|
130
|
+
)
|
|
131
|
+
const aligned =
|
|
132
|
+
structureSequences && longest
|
|
133
|
+
? chooseMappedEntity(longest, structureSequences, 'smith_waterman')
|
|
134
|
+
: undefined
|
|
135
|
+
return (
|
|
136
|
+
exact ??
|
|
137
|
+
(aligned ? structureSequences?.[aligned.index] : structureSequences?.[0])
|
|
138
|
+
)
|
|
139
|
+
}
|