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