p2s_mapper 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (172) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +148 -0
  3. package/dist/alignmentQuality.d.ts +58 -0
  4. package/dist/alignmentQuality.js +108 -0
  5. package/dist/alignmentQuality.js.map +1 -0
  6. package/dist/alphaFoldModels.d.ts +29 -0
  7. package/dist/alphaFoldModels.js +60 -0
  8. package/dist/alphaFoldModels.js.map +1 -0
  9. package/dist/caCoordsToPdb.d.ts +18 -0
  10. package/dist/caCoordsToPdb.js +95 -0
  11. package/dist/caCoordsToPdb.js.map +1 -0
  12. package/dist/chooseMappedEntity.d.ts +55 -0
  13. package/dist/chooseMappedEntity.js +140 -0
  14. package/dist/chooseMappedEntity.js.map +1 -0
  15. package/dist/coordinates.d.ts +66 -0
  16. package/dist/coordinates.js +66 -0
  17. package/dist/coordinates.js.map +1 -0
  18. package/dist/dbIds.d.ts +16 -0
  19. package/dist/dbIds.js +42 -0
  20. package/dist/dbIds.js.map +1 -0
  21. package/dist/extractPerResidueConfidence.d.ts +49 -0
  22. package/dist/extractPerResidueConfidence.js +42 -0
  23. package/dist/extractPerResidueConfidence.js.map +1 -0
  24. package/dist/extractStructureSequences.d.ts +142 -0
  25. package/dist/extractStructureSequences.js +217 -0
  26. package/dist/extractStructureSequences.js.map +1 -0
  27. package/dist/fetchUtils.d.ts +34 -0
  28. package/dist/fetchUtils.js +91 -0
  29. package/dist/fetchUtils.js.map +1 -0
  30. package/dist/index.d.ts +23 -0
  31. package/dist/index.js +46 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/invertMap.d.ts +2 -0
  34. package/dist/invertMap.js +8 -0
  35. package/dist/invertMap.js.map +1 -0
  36. package/dist/isoformRanking.d.ts +47 -0
  37. package/dist/isoformRanking.js +91 -0
  38. package/dist/isoformRanking.js.map +1 -0
  39. package/dist/mappings.d.ts +58 -0
  40. package/dist/mappings.js +193 -0
  41. package/dist/mappings.js.map +1 -0
  42. package/dist/package.json +1 -0
  43. package/dist/pairwiseAlignment.d.ts +35 -0
  44. package/dist/pairwiseAlignment.js +337 -0
  45. package/dist/pairwiseAlignment.js.map +1 -0
  46. package/dist/pdbUniProtMapping.d.ts +75 -0
  47. package/dist/pdbUniProtMapping.js +241 -0
  48. package/dist/pdbUniProtMapping.js.map +1 -0
  49. package/dist/pdbeBestStructures.d.ts +23 -0
  50. package/dist/pdbeBestStructures.js +79 -0
  51. package/dist/pdbeBestStructures.js.map +1 -0
  52. package/dist/pdbeEntryMolecules.d.ts +7 -0
  53. package/dist/pdbeEntryMolecules.js +39 -0
  54. package/dist/pdbeEntryMolecules.js.map +1 -0
  55. package/dist/siftsNumbering.d.ts +23 -0
  56. package/dist/siftsNumbering.js +44 -0
  57. package/dist/siftsNumbering.js.map +1 -0
  58. package/dist/stripStopCodon.d.ts +15 -0
  59. package/dist/stripStopCodon.js +24 -0
  60. package/dist/stripStopCodon.js.map +1 -0
  61. package/dist/structureFormat.d.ts +40 -0
  62. package/dist/structureFormat.js +54 -0
  63. package/dist/structureFormat.js.map +1 -0
  64. package/dist/structureSources.d.ts +28 -0
  65. package/dist/structureSources.js +76 -0
  66. package/dist/structureSources.js.map +1 -0
  67. package/dist/structureUrls.d.ts +54 -0
  68. package/dist/structureUrls.js +147 -0
  69. package/dist/structureUrls.js.map +1 -0
  70. package/dist/types.d.ts +14 -0
  71. package/dist/types.js +27 -0
  72. package/dist/types.js.map +1 -0
  73. package/dist/uniProtSearch.d.ts +32 -0
  74. package/dist/uniProtSearch.js +114 -0
  75. package/dist/uniProtSearch.js.map +1 -0
  76. package/esm/alignmentQuality.d.ts +58 -0
  77. package/esm/alignmentQuality.js +100 -0
  78. package/esm/alignmentQuality.js.map +1 -0
  79. package/esm/alphaFoldModels.d.ts +29 -0
  80. package/esm/alphaFoldModels.js +55 -0
  81. package/esm/alphaFoldModels.js.map +1 -0
  82. package/esm/caCoordsToPdb.d.ts +18 -0
  83. package/esm/caCoordsToPdb.js +91 -0
  84. package/esm/caCoordsToPdb.js.map +1 -0
  85. package/esm/chooseMappedEntity.d.ts +55 -0
  86. package/esm/chooseMappedEntity.js +134 -0
  87. package/esm/chooseMappedEntity.js.map +1 -0
  88. package/esm/coordinates.d.ts +66 -0
  89. package/esm/coordinates.js +58 -0
  90. package/esm/coordinates.js.map +1 -0
  91. package/esm/dbIds.d.ts +16 -0
  92. package/esm/dbIds.js +35 -0
  93. package/esm/dbIds.js.map +1 -0
  94. package/esm/extractPerResidueConfidence.d.ts +49 -0
  95. package/esm/extractPerResidueConfidence.js +38 -0
  96. package/esm/extractPerResidueConfidence.js.map +1 -0
  97. package/esm/extractStructureSequences.d.ts +142 -0
  98. package/esm/extractStructureSequences.js +205 -0
  99. package/esm/extractStructureSequences.js.map +1 -0
  100. package/esm/fetchUtils.d.ts +34 -0
  101. package/esm/fetchUtils.js +82 -0
  102. package/esm/fetchUtils.js.map +1 -0
  103. package/esm/index.d.ts +23 -0
  104. package/esm/index.js +30 -0
  105. package/esm/index.js.map +1 -0
  106. package/esm/invertMap.d.ts +2 -0
  107. package/esm/invertMap.js +5 -0
  108. package/esm/invertMap.js.map +1 -0
  109. package/esm/isoformRanking.d.ts +47 -0
  110. package/esm/isoformRanking.js +86 -0
  111. package/esm/isoformRanking.js.map +1 -0
  112. package/esm/mappings.d.ts +58 -0
  113. package/esm/mappings.js +179 -0
  114. package/esm/mappings.js.map +1 -0
  115. package/esm/pairwiseAlignment.d.ts +35 -0
  116. package/esm/pairwiseAlignment.js +328 -0
  117. package/esm/pairwiseAlignment.js.map +1 -0
  118. package/esm/pdbUniProtMapping.d.ts +75 -0
  119. package/esm/pdbUniProtMapping.js +231 -0
  120. package/esm/pdbUniProtMapping.js.map +1 -0
  121. package/esm/pdbeBestStructures.d.ts +23 -0
  122. package/esm/pdbeBestStructures.js +73 -0
  123. package/esm/pdbeBestStructures.js.map +1 -0
  124. package/esm/pdbeEntryMolecules.d.ts +7 -0
  125. package/esm/pdbeEntryMolecules.js +35 -0
  126. package/esm/pdbeEntryMolecules.js.map +1 -0
  127. package/esm/siftsNumbering.d.ts +23 -0
  128. package/esm/siftsNumbering.js +40 -0
  129. package/esm/siftsNumbering.js.map +1 -0
  130. package/esm/stripStopCodon.d.ts +15 -0
  131. package/esm/stripStopCodon.js +20 -0
  132. package/esm/stripStopCodon.js.map +1 -0
  133. package/esm/structureFormat.d.ts +40 -0
  134. package/esm/structureFormat.js +48 -0
  135. package/esm/structureFormat.js.map +1 -0
  136. package/esm/structureSources.d.ts +28 -0
  137. package/esm/structureSources.js +71 -0
  138. package/esm/structureSources.js.map +1 -0
  139. package/esm/structureUrls.d.ts +54 -0
  140. package/esm/structureUrls.js +134 -0
  141. package/esm/structureUrls.js.map +1 -0
  142. package/esm/types.d.ts +14 -0
  143. package/esm/types.js +23 -0
  144. package/esm/types.js.map +1 -0
  145. package/esm/uniProtSearch.d.ts +32 -0
  146. package/esm/uniProtSearch.js +110 -0
  147. package/esm/uniProtSearch.js.map +1 -0
  148. package/package.json +63 -0
  149. package/src/alignmentQuality.ts +132 -0
  150. package/src/alphaFoldModels.ts +91 -0
  151. package/src/caCoordsToPdb.ts +111 -0
  152. package/src/chooseMappedEntity.ts +198 -0
  153. package/src/coordinates.ts +119 -0
  154. package/src/dbIds.ts +46 -0
  155. package/src/extractPerResidueConfidence.ts +88 -0
  156. package/src/extractStructureSequences.ts +342 -0
  157. package/src/fetchUtils.ts +110 -0
  158. package/src/index.ts +34 -0
  159. package/src/invertMap.ts +4 -0
  160. package/src/isoformRanking.ts +139 -0
  161. package/src/mappings.ts +230 -0
  162. package/src/pairwiseAlignment.ts +423 -0
  163. package/src/pdbUniProtMapping.ts +320 -0
  164. package/src/pdbeBestStructures.ts +94 -0
  165. package/src/pdbeEntryMolecules.ts +37 -0
  166. package/src/siftsNumbering.ts +61 -0
  167. package/src/stripStopCodon.ts +20 -0
  168. package/src/structureFormat.ts +70 -0
  169. package/src/structureSources.ts +101 -0
  170. package/src/structureUrls.ts +171 -0
  171. package/src/types.ts +30 -0
  172. package/src/uniProtSearch.ts +212 -0
@@ -0,0 +1,134 @@
1
+ // Pure structure-URL builders and target parsers. Kept dependency-free so both
2
+ // the launch flow and the ProteinView model (e.g. resolving a `uniprotId`/`pdbId`
3
+ // shorthand at hydration) can import them without pulling in heavy launch code.
4
+ const ALPHAFOLD_VERSION = 'v6';
5
+ // UniProt endpoints. rest.* is the machine API (the GFF the feature tracks and
6
+ // the alignment's annotation rows both read, the FASTA the temporary protein
7
+ // assembly is built from); www.*/entry is the human-facing entry page the UI
8
+ // links to. Centralized so a path change is one edit rather than a grep.
9
+ export function uniprotGffUrl(uniprotId) {
10
+ return `https://rest.uniprot.org/uniprotkb/${uniprotId}.gff`;
11
+ }
12
+ export function uniprotFastaUrl(uniprotId) {
13
+ return `https://rest.uniprot.org/uniprotkb/${uniprotId}.fasta`;
14
+ }
15
+ export function uniprotEntryUrl(uniprotId) {
16
+ return `https://www.uniprot.org/uniprotkb/${uniprotId}/entry`;
17
+ }
18
+ /**
19
+ * The guessed filename of an accession's canonical AlphaFold model. A guess:
20
+ * the F1 fragment does not exist past the length cap and the version moves, so
21
+ * this is the last resort when the prediction API cannot be reached. Ask
22
+ * `fetchAlphaFoldModels` for the real files.
23
+ */
24
+ export function getAlphaFoldStructureUrl(uniprotId, version = ALPHAFOLD_VERSION) {
25
+ return `https://alphafold.ebi.ac.uk/files/AF-${uniprotId}-F1-model_${version}.cif`;
26
+ }
27
+ export function getPdbStructureUrl(pdbId) {
28
+ return `https://files.rcsb.org/download/${pdbId}.cif`;
29
+ }
30
+ /**
31
+ * Resolve the `{ pdbId }` shorthand to a concrete structure URL, so a
32
+ * hand-authored snapshot or a short launch URL doesn't have to know RCSB's
33
+ * filename format. An explicit `url` (or inline `data`) always wins.
34
+ * Idempotent: re-resolving an already-resolved spec returns the same url.
35
+ *
36
+ * `uniprotId` deliberately resolves to no url here, and still outranks a
37
+ * `pdbId`. An accession's files are whatever AlphaFold DB currently holds — an
38
+ * isoform model, several fragments, a new model version — so the structure
39
+ * keeps the accession and the structure loader asks the prediction API which
40
+ * file to open.
41
+ *
42
+ * Shared by the Structure model's snapshot preprocessor and the
43
+ * LaunchView-ProteinView extension point, which would otherwise each carry
44
+ * their own copy of the same precedence rule.
45
+ */
46
+ export function resolveStructureUrl({ url, data, uniprotId, pdbId, }) {
47
+ if (url !== undefined || data !== undefined) {
48
+ return url;
49
+ }
50
+ return uniprotId !== undefined || pdbId === undefined
51
+ ? undefined
52
+ : getPdbStructureUrl(pdbId);
53
+ }
54
+ // Hosts that serve the PDB archive, so a 4-character filename there really is
55
+ // that PDB entry. Restricted deliberately: a user-uploaded model happening to be
56
+ // named 1abc.cif must not pick up 1ABC's UniProt annotations.
57
+ const PDB_ARCHIVE_HOSTS = ['files.rcsb.org', 'www.ebi.ac.uk', 'ftp.ebi.ac.uk'];
58
+ /**
59
+ * The PDB id of a structure URL from the PDB archive, lowercased, or undefined
60
+ * for any other URL. Accepts the filename forms those hosts serve, e.g.
61
+ * `1TUP.cif`, `pdb1tup.ent.gz`, `1tup_updated.cif`.
62
+ */
63
+ export function getPdbIdFromUrl(url) {
64
+ const { host, file } = (() => {
65
+ try {
66
+ const parsed = new URL(url);
67
+ return { host: parsed.hostname, file: parsed.pathname.split('/').pop() };
68
+ }
69
+ catch {
70
+ return { host: undefined, file: undefined };
71
+ }
72
+ })();
73
+ if (host === undefined || !PDB_ARCHIVE_HOSTS.includes(host)) {
74
+ return undefined;
75
+ }
76
+ const match = /^(?:pdb)?([1-9][a-z0-9]{3})(?:_updated)?\.(?:cif|bcif|pdb|ent)(?:\.gz)?$/i.exec(file ?? '');
77
+ return match?.[1]?.toLowerCase();
78
+ }
79
+ // Foldseek targets may contain a description after the ID separated by a
80
+ // space, e.g. "AF-P16442-F1-model_v6 Histo-blood group ABO transferase".
81
+ function extractTargetId(target) {
82
+ return target.split(' ')[0];
83
+ }
84
+ /** The accession of an AlphaFold model id or url, with its isoform suffix:
85
+ * `AF-P16442-F1-model_v6` gives P16442, `AF-P04637-2-F1-model_v6` P04637-2. */
86
+ export function getUniprotIdFromAlphaFoldTarget(target) {
87
+ const match = /AF-([A-Z0-9]+(?:-\d+)?)-F\d+/.exec(extractTargetId(target));
88
+ return match?.[1];
89
+ }
90
+ // Foldseek names a pdb100 hit by the assembly file and chain it searched,
91
+ // `8f2h-assembly1.cif.gz_A`; the entry id is the first four characters.
92
+ export function getStructureUrlFromTarget(target, db) {
93
+ const targetId = extractTargetId(target);
94
+ if (targetId.startsWith('AF-')) {
95
+ return `https://alphafold.ebi.ac.uk/files/${targetId}.cif`;
96
+ }
97
+ const pdbId = db === 'pdb100'
98
+ ? /^([1-9][a-z0-9]{3})(?:[-_.]|$)/i.exec(targetId)?.[1]
99
+ : undefined;
100
+ return pdbId ? getPdbStructureUrl(pdbId) : undefined;
101
+ }
102
+ export function getConfidenceUrlFromTarget(target) {
103
+ const targetId = extractTargetId(target);
104
+ if (targetId.startsWith('AF-')) {
105
+ const confidenceId = targetId.replace('-model_', '-confidence_');
106
+ return `https://alphafold.ebi.ac.uk/files/${confidenceId}.json`;
107
+ }
108
+ return undefined;
109
+ }
110
+ /**
111
+ * What a structure is called in the UI: its PDB id, its AlphaFold accession,
112
+ * else the file's name. Shared by the view title, the alignment panel heading
113
+ * and the hover readout, so one structure reads the same everywhere.
114
+ */
115
+ export function structureDisplayLabel({ url, data, uniprotId, }) {
116
+ if (url === undefined) {
117
+ return data !== undefined
118
+ ? 'Uploaded structure'
119
+ : uniprotId
120
+ ? `AlphaFold ${uniprotId}`
121
+ : '';
122
+ }
123
+ const pdbId = getPdbIdFromUrl(url);
124
+ if (pdbId) {
125
+ return pdbId.toUpperCase();
126
+ }
127
+ const urlAccession = getUniprotIdFromAlphaFoldTarget(url);
128
+ if (urlAccession) {
129
+ return `AlphaFold ${urlAccession}`;
130
+ }
131
+ const name = url.split('/').pop()?.split('?')[0];
132
+ return name === undefined || name === '' ? url : name;
133
+ }
134
+ //# sourceMappingURL=structureUrls.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structureUrls.js","sourceRoot":"","sources":["../src/structureUrls.ts"],"names":[],"mappings":"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"}
package/esm/types.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ export type AlignmentAlgorithm = 'needleman_wunsch' | 'smith_waterman';
2
+ export declare const ALIGNMENT_ALGORITHMS: {
3
+ readonly NEEDLEMAN_WUNSCH: "needleman_wunsch";
4
+ readonly SMITH_WATERMAN: "smith_waterman";
5
+ };
6
+ export declare const DEFAULT_ALIGNMENT_ALGORITHM: AlignmentAlgorithm;
7
+ export declare const ALIGNMENT_ALGORITHM_VALUES: AlignmentAlgorithm[];
8
+ /**
9
+ * Parses an untrusted algorithm name (a URL session-spec param) into the union.
10
+ * The only place a coercion is needed: the model property is an enumeration, so
11
+ * everything downstream of hydration is already typed.
12
+ */
13
+ export declare function coerceAlignmentAlgorithm(value: string): AlignmentAlgorithm;
14
+ export declare const ALIGNMENT_ALGORITHM_LABELS: Record<AlignmentAlgorithm, string>;
package/esm/types.js ADDED
@@ -0,0 +1,23 @@
1
+ export const ALIGNMENT_ALGORITHMS = {
2
+ NEEDLEMAN_WUNSCH: 'needleman_wunsch',
3
+ SMITH_WATERMAN: 'smith_waterman',
4
+ };
5
+ export const DEFAULT_ALIGNMENT_ALGORITHM = 'smith_waterman';
6
+ export const ALIGNMENT_ALGORITHM_VALUES = [
7
+ 'needleman_wunsch',
8
+ 'smith_waterman',
9
+ ];
10
+ /**
11
+ * Parses an untrusted algorithm name (a URL session-spec param) into the union.
12
+ * The only place a coercion is needed: the model property is an enumeration, so
13
+ * everything downstream of hydration is already typed.
14
+ */
15
+ export function coerceAlignmentAlgorithm(value) {
16
+ return (ALIGNMENT_ALGORITHM_VALUES.find(v => v === value) ??
17
+ DEFAULT_ALIGNMENT_ALGORITHM);
18
+ }
19
+ export const ALIGNMENT_ALGORITHM_LABELS = {
20
+ needleman_wunsch: 'Needleman-Wunsch',
21
+ smith_waterman: 'Smith-Waterman',
22
+ };
23
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,gBAAgB,EAAE,kBAAkB;IACpC,cAAc,EAAE,gBAAgB;CACxB,CAAA;AAEV,MAAM,CAAC,MAAM,2BAA2B,GAAuB,gBAAgB,CAAA;AAE/E,MAAM,CAAC,MAAM,0BAA0B,GAAyB;IAC9D,kBAAkB;IAClB,gBAAgB;CACjB,CAAA;AAED;;;;GAIG;AACH,MAAM,UAAU,wBAAwB,CAAC,KAAa;IACpD,OAAO,CACL,0BAA0B,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,KAAK,CAAC;QACjD,2BAA2B,CAC5B,CAAA;AACH,CAAC;AAED,MAAM,CAAC,MAAM,0BAA0B,GAAuC;IAC5E,gBAAgB,EAAE,kBAAkB;IACpC,cAAc,EAAE,gBAAgB;CACjC,CAAA"}
@@ -0,0 +1,32 @@
1
+ import type { FetchOptions } from './fetchUtils.ts';
2
+ export interface UniProtEntry {
3
+ accession: string;
4
+ id?: string;
5
+ geneName?: string;
6
+ organismName?: string;
7
+ proteinName?: string;
8
+ isReviewed: boolean;
9
+ }
10
+ /**
11
+ * `gene_exact` rather than `gene`, which also matches synonyms and returns
12
+ * paralogs. Without a taxon the query runs across every species, so a mouse
13
+ * assembly whose tracks carry no taxId lists mouse beside human instead of
14
+ * silently answering with the human entry.
15
+ *
16
+ * The symbol is quoted because a bare one containing `:` — an HLA allele, a
17
+ * name straight out of an annotation file — reads as another field to
18
+ * UniProt's parser, which answers 400.
19
+ */
20
+ export declare function buildGeneNameQuery(geneName: string, organismId?: number): string;
21
+ export interface UniProtSearchResult {
22
+ entries: UniProtEntry[];
23
+ attemptedCount: number;
24
+ failedCount: number;
25
+ }
26
+ export declare function searchUniProtEntries({ recognizedIds, geneId, geneName, organismId, }: {
27
+ recognizedIds?: string[];
28
+ geneId?: string;
29
+ geneName?: string;
30
+ /** NCBI taxon id; undefined searches every species */
31
+ organismId?: number;
32
+ }, opts?: FetchOptions): Promise<UniProtSearchResult>;
@@ -0,0 +1,110 @@
1
+ import { jsonfetch } from "./fetchUtils.js";
2
+ import { buildUniProtXrefQuery, isRecognizedDatabaseId, stripTrailingVersion, } from "./dbIds.js";
3
+ const UNIPROT_FIELDS = 'accession,id,gene_names,organism_name,protein_name,reviewed';
4
+ function mapApiResultToEntry(result) {
5
+ return {
6
+ accession: result.primaryAccession,
7
+ id: result.uniProtkbId,
8
+ geneName: result.genes?.[0]?.geneName?.value,
9
+ organismName: result.organism?.commonName ?? result.organism?.scientificName,
10
+ proteinName: result.proteinDescription?.recommendedName?.fullName?.value,
11
+ isReviewed: result.entryType === 'UniProtKB reviewed (Swiss-Prot)',
12
+ };
13
+ }
14
+ async function searchUniProt(query, size, opts) {
15
+ const url = `https://rest.uniprot.org/uniprotkb/search?query=${encodeURIComponent(query)}&fields=${UNIPROT_FIELDS}&size=${size}`;
16
+ const data = await jsonfetch(url, opts);
17
+ return data.results.map(mapApiResultToEntry);
18
+ }
19
+ /**
20
+ * `gene_exact` rather than `gene`, which also matches synonyms and returns
21
+ * paralogs. Without a taxon the query runs across every species, so a mouse
22
+ * assembly whose tracks carry no taxId lists mouse beside human instead of
23
+ * silently answering with the human entry.
24
+ *
25
+ * The symbol is quoted because a bare one containing `:` — an HLA allele, a
26
+ * name straight out of an annotation file — reads as another field to
27
+ * UniProt's parser, which answers 400.
28
+ */
29
+ export function buildGeneNameQuery(geneName, organismId) {
30
+ return [
31
+ `gene_exact:"${geneName.replaceAll('"', '\\"')}"`,
32
+ organismId ? `organism_id:${organismId}` : undefined,
33
+ 'reviewed:true',
34
+ ]
35
+ .filter(s => s !== undefined)
36
+ .join(' AND ');
37
+ }
38
+ async function searchByXref(id, opts) {
39
+ const query = buildUniProtXrefQuery(id);
40
+ if (!query) {
41
+ return { entries: [], error: undefined };
42
+ }
43
+ try {
44
+ return { entries: await searchUniProt(query, 10, opts), error: undefined };
45
+ }
46
+ catch (e) {
47
+ console.error(`xref search failed for ${id}:`, e);
48
+ return { entries: [], error: e };
49
+ }
50
+ }
51
+ async function searchByGeneName(geneName, organismId, opts) {
52
+ try {
53
+ const entries = await searchUniProt(buildGeneNameQuery(geneName, organismId), organismId ? 5 : 10, opts);
54
+ return { entries, error: undefined };
55
+ }
56
+ catch (e) {
57
+ console.error(`gene name search failed for ${geneName}:`, e);
58
+ return { entries: [], error: e };
59
+ }
60
+ }
61
+ function deduplicateEntries(entries) {
62
+ const seen = new Set();
63
+ const result = [];
64
+ for (const entry of entries) {
65
+ if (!seen.has(entry.accession)) {
66
+ seen.add(entry.accession);
67
+ result.push(entry);
68
+ }
69
+ }
70
+ return result;
71
+ }
72
+ export async function searchUniProtEntries({ recognizedIds = [], geneId, geneName, organismId, }, opts) {
73
+ const idsToSearch = new Set(recognizedIds);
74
+ const strippedGeneId = geneId ? stripTrailingVersion(geneId) : undefined;
75
+ if (strippedGeneId && isRecognizedDatabaseId(strippedGeneId)) {
76
+ idsToSearch.add(strippedGeneId);
77
+ }
78
+ // The gene-name query runs alongside the xrefs rather than after them: it is
79
+ // only consulted when no xref found a reviewed entry, but waiting for that
80
+ // answer before starting it doubled the latency of the commonest case.
81
+ const [xrefResults, geneResult] = await Promise.all([
82
+ Promise.all([...idsToSearch].map(id => searchByXref(id, opts))),
83
+ geneName ? searchByGeneName(geneName, organismId, opts) : undefined,
84
+ ]);
85
+ let entries = deduplicateEntries(xrefResults.flatMap(r => r.entries));
86
+ // The gene-name answer is consulted only where no xref found a reviewed
87
+ // entry. Counting it otherwise reported "failed for 1 of 2 identifiers" over
88
+ // a gene that had in fact resolved, off a query nothing was waiting for.
89
+ const consultedGeneResult = geneResult && !entries.some(e => e.isReviewed) ? geneResult : undefined;
90
+ if (consultedGeneResult) {
91
+ entries = deduplicateEntries([...entries, ...consultedGeneResult.entries]);
92
+ }
93
+ const attemptedCount = idsToSearch.size + (consultedGeneResult ? 1 : 0);
94
+ const failedCount = xrefResults.filter(r => r.error !== undefined).length +
95
+ (consultedGeneResult?.error === undefined ? 0 : 1);
96
+ // Every attempt failing is a network problem, not an empty result. Throwing
97
+ // it stops consumers reporting "No UniProt ID found" over a dead connection.
98
+ if (entries.length === 0 &&
99
+ attemptedCount > 0 &&
100
+ attemptedCount === failedCount) {
101
+ throw (consultedGeneResult?.error ??
102
+ xrefResults.find(r => r.error !== undefined)?.error);
103
+ }
104
+ return {
105
+ entries: entries.toSorted((a, b) => Number(b.isReviewed) - Number(a.isReviewed)),
106
+ attemptedCount,
107
+ failedCount,
108
+ };
109
+ }
110
+ //# sourceMappingURL=uniProtSearch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"uniProtSearch.js","sourceRoot":"","sources":["../src/uniProtSearch.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "p2s_mapper",
3
+ "description": "Map a transcript's translation onto a protein structure: alignment, coordinates, chain choice, SIFTS, and the AlphaFold and PDBe lookups that find the structure",
4
+ "version": "1.0.0",
5
+ "repository": {
6
+ "type": "git",
7
+ "url": "git+https://github.com/GMOD/p2s_mapper.git"
8
+ },
9
+ "license": "MIT",
10
+ "type": "module",
11
+ "sideEffects": false,
12
+ "main": "dist/index.js",
13
+ "exports": {
14
+ "import": "./esm/index.js",
15
+ "require": "./dist/index.js"
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "esm",
20
+ "src"
21
+ ],
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "dependencies": {
26
+ "g2p_mapper": "^2.1.7"
27
+ },
28
+ "devDependencies": {
29
+ "@eslint/js": "^10.0.1",
30
+ "@types/node": "^25.9.5",
31
+ "eslint": "^10.8.0",
32
+ "eslint-plugin-import-x": "^4.17.1",
33
+ "git-cliff": "^2.13.1",
34
+ "prettier": "^3.9.6",
35
+ "rimraf": "^6.1.3",
36
+ "typescript": "^6.0.3",
37
+ "typescript-eslint": "^8.65.0",
38
+ "vitest": "^4.1.10"
39
+ },
40
+ "keywords": [
41
+ "bioinformatics"
42
+ ],
43
+ "author": {
44
+ "name": "Colin Diesh",
45
+ "email": "colin.diesh@gmail.com"
46
+ },
47
+ "scripts": {
48
+ "test": "vitest",
49
+ "clean": "rimraf dist esm",
50
+ "format": "prettier --write .",
51
+ "format:check": "prettier --check .",
52
+ "lint": "eslint --report-unused-disable-directives --max-warnings 0",
53
+ "typecheck": "tsc --noEmit -p tsconfig.lint.json",
54
+ "prebuild": "pnpm clean",
55
+ "build:esm": "tsc --outDir esm",
56
+ "build:es5": "tsc --module commonjs --moduleResolution bundler --outDir dist",
57
+ "postbuild:es5": "echo '{\"type\": \"commonjs\"}' > dist/package.json",
58
+ "preversion": "pnpm lint && pnpm format:check && pnpm typecheck && pnpm test --run && pnpm build",
59
+ "version": "git-cliff --tag v$npm_package_version --unreleased --prepend CHANGELOG.md && git add CHANGELOG.md",
60
+ "postversion": "git push --follow-tags",
61
+ "build": "pnpm build:esm && pnpm build:es5"
62
+ }
63
+ }
@@ -0,0 +1,132 @@
1
+ import {
2
+ alignmentLength,
3
+ structureAlignedSeq,
4
+ transcriptAlignedSeq,
5
+ } from './mappings.ts'
6
+
7
+ import type { PairwiseAlignment } from './mappings.ts'
8
+
9
+ export interface AlignmentQuality {
10
+ /** columns carrying a residue on both rows, i.e. mapped positions */
11
+ aligned: number
12
+ identical: number
13
+ transcriptLength: number
14
+ structureLength: number
15
+ /** identical over aligned; 0 when nothing aligned */
16
+ identity: number
17
+ /** identical over the shorter of the two sequences: the statistic that
18
+ * separates a real match from a chance local alignment, see below */
19
+ identityOverShorter: number
20
+ /** aligned over the structure's length */
21
+ structureCoverage: number
22
+ /** first and last transcript residue the alignment covers, 1-based
23
+ * inclusive; 0 when nothing aligned. A structure is usually a fragment, and
24
+ * which part of the protein it holds is the reader's first question. */
25
+ transcriptStart: number
26
+ transcriptEnd: number
27
+ }
28
+
29
+ /**
30
+ * Below either floor the mapped positions are as likely to be chance as
31
+ * homology. Local alignment picks the best-scoring stretch of two sequences,
32
+ * so identity over the aligned columns is inflated whatever the inputs:
33
+ * measured 2026-09-11 on random sequences, Smith-Waterman with BLOSUM62 gave
34
+ * 37% identity over 27 columns at 300 × 300 and 30% over 100 columns at
35
+ * 500 × 150, two thirds of the shorter chain, and p53 against a ribosomal
36
+ * protein (7K00 RPS11) gives 31 identities at a third identity. Neither local
37
+ * identity nor coverage alone separates those from a real match. Identical
38
+ * residues over the shorter sequence does: 0.03, 0.20 and 0.24 for the chance
39
+ * cases against 0.9 or better for any structure of the transcript's protein
40
+ * and about 0.6 for an ortholog.
41
+ *
42
+ * A short alignment has to be nearly perfect instead: a 10-residue decoy with
43
+ * 3 identities sits at the ratio floor, while a real bound peptide (1YCR's 15
44
+ * p53 residues, 4ZZJ's 7) is identical or one off. So under
45
+ * SHORT_ALIGNMENT_RESIDUES identical residues the ratio floor rises to
46
+ * SHORT_ALIGNMENT_IDENTITY, and under MIN_IDENTICAL_RESIDUES nothing passes,
47
+ * since a tetrapeptide is never the gene's product.
48
+ */
49
+ export const LOW_IDENTITY_OVER_SHORTER = 0.3
50
+ export const SHORT_ALIGNMENT_RESIDUES = 20
51
+ export const SHORT_ALIGNMENT_IDENTITY = 0.8
52
+ export const MIN_IDENTICAL_RESIDUES = 5
53
+
54
+ export function alignmentQuality(pa: PairwiseAlignment): AlignmentQuality {
55
+ const t = transcriptAlignedSeq(pa)
56
+ const s = structureAlignedSeq(pa)
57
+ let aligned = 0
58
+ let identical = 0
59
+ let transcriptLength = 0
60
+ let structureLength = 0
61
+ let transcriptStart = 0
62
+ let transcriptEnd = 0
63
+ for (let i = 0; i < alignmentLength(pa); i++) {
64
+ const a = t[i]!
65
+ const b = s[i]!
66
+ const ta = a !== '-'
67
+ const sb = b !== '-'
68
+ if (ta) {
69
+ transcriptLength++
70
+ }
71
+ if (sb) {
72
+ structureLength++
73
+ }
74
+ if (ta && sb) {
75
+ aligned++
76
+ transcriptStart = transcriptStart || transcriptLength
77
+ transcriptEnd = transcriptLength
78
+ if (a.toUpperCase() === b.toUpperCase()) {
79
+ identical++
80
+ }
81
+ }
82
+ }
83
+ const shorter = Math.min(transcriptLength, structureLength)
84
+ return {
85
+ aligned,
86
+ identical,
87
+ transcriptLength,
88
+ structureLength,
89
+ identity: aligned ? identical / aligned : 0,
90
+ identityOverShorter: shorter ? identical / shorter : 0,
91
+ structureCoverage: structureLength ? aligned / structureLength : 0,
92
+ transcriptStart,
93
+ transcriptEnd,
94
+ }
95
+ }
96
+
97
+ export function isLowSimilarity(q: AlignmentQuality) {
98
+ const floor =
99
+ q.identical < SHORT_ALIGNMENT_RESIDUES
100
+ ? SHORT_ALIGNMENT_IDENTITY
101
+ : LOW_IDENTITY_OVER_SHORTER
102
+ return q.identical < MIN_IDENTICAL_RESIDUES || q.identityOverShorter < floor
103
+ }
104
+
105
+ /** One line for the alignment header: "87% identity over 219 of 393
106
+ * structure residues". */
107
+ export function describeAlignmentQuality(q: AlignmentQuality) {
108
+ if (q.aligned === 0) {
109
+ return 'no residues aligned'
110
+ }
111
+ return `${Math.round(q.identity * 100)}% identity over ${q.aligned} of ${q.structureLength} structure residues`
112
+ }
113
+
114
+ /**
115
+ * The same counted from the transcript's side, for the always-visible header:
116
+ * how much of the gene's protein this structure speaks for is what decides
117
+ * whether a codon has a residue at all.
118
+ */
119
+ export function describeTranscriptCoverage(q: AlignmentQuality) {
120
+ if (q.aligned === 0) {
121
+ return 'no residues aligned'
122
+ }
123
+ return `${Math.round(q.identity * 100)}% identity, ${q.aligned} of ${q.transcriptLength} transcript residues covered`
124
+ }
125
+
126
+ /** Which residues of the transcript a fragment holds, or undefined when it
127
+ * covers the whole translation. */
128
+ export function describeCoveredRange(q: AlignmentQuality) {
129
+ return q.aligned === 0 || q.aligned === q.transcriptLength
130
+ ? undefined
131
+ : `covers transcript residues ${q.transcriptStart}–${q.transcriptEnd}`
132
+ }
@@ -0,0 +1,91 @@
1
+ import { httpError, rawfetch } from './fetchUtils.ts'
2
+
3
+ import type { FetchOptions } from './fetchUtils.ts'
4
+ import { stripStopCodon } from './stripStopCodon.ts'
5
+
6
+ export interface AlphaFoldModel {
7
+ /** UniProt accession, with an isoform suffix on an isoform model */
8
+ accession: string
9
+ url: string
10
+ confidenceUrl?: string
11
+ sequence: string
12
+ }
13
+
14
+ interface PredictionEntry {
15
+ uniprotAccession?: unknown
16
+ cifUrl?: unknown
17
+ plddtDocUrl?: unknown
18
+ sequence?: unknown
19
+ }
20
+
21
+ function isPredictionEntry(value: unknown): value is PredictionEntry {
22
+ return typeof value === 'object' && value !== null
23
+ }
24
+
25
+ export function parseAlphaFoldModels(json: unknown): AlphaFoldModel[] {
26
+ return (Array.isArray(json) ? json : []).flatMap(e =>
27
+ isPredictionEntry(e) &&
28
+ typeof e.uniprotAccession === 'string' &&
29
+ typeof e.cifUrl === 'string' &&
30
+ typeof e.sequence === 'string'
31
+ ? [
32
+ {
33
+ accession: e.uniprotAccession,
34
+ url: e.cifUrl,
35
+ confidenceUrl:
36
+ typeof e.plddtDocUrl === 'string' ? e.plddtDocUrl : undefined,
37
+ sequence: e.sequence,
38
+ },
39
+ ]
40
+ : [],
41
+ )
42
+ }
43
+
44
+ /**
45
+ * Every model AlphaFold DB has for an accession: the canonical one and any
46
+ * isoform models, each with its file url and sequence. Asked rather than
47
+ * derived from the accession, because a derived `AF-<acc>-F1-model_v6` does
48
+ * not exist past the length cap (dystrophin has fourteen isoform models and no
49
+ * F1) and moves with every model version. An accession AlphaFold has never
50
+ * folded answers 400 or 404, which is no models rather than an error.
51
+ */
52
+ export async function fetchAlphaFoldModels(
53
+ uniprotId: string,
54
+ opts?: FetchOptions,
55
+ ) {
56
+ const url = `https://alphafold.ebi.ac.uk/api/prediction/${encodeURIComponent(uniprotId)}`
57
+ const res = await rawfetch(url, opts)
58
+ if (res.status === 400 || res.status === 404) {
59
+ return []
60
+ }
61
+ if (!res.ok) {
62
+ throw await httpError(res, url)
63
+ }
64
+ return parseAlphaFoldModels(await res.json())
65
+ }
66
+
67
+ /**
68
+ * The model to open for a gene's transcripts: one folded from exactly a
69
+ * transcript's translation, canonical first, so the view maps it as an
70
+ * identity; else the canonical model; else the longest isoform model.
71
+ *
72
+ * Takes any record of sequences, since the structure loader knows one
73
+ * translation rather than a gene's isoforms.
74
+ */
75
+ export function pickAlphaFoldModel(
76
+ models: AlphaFoldModel[],
77
+ isoformSequences: Record<string, { seq: string }> | undefined,
78
+ ) {
79
+ const translations = new Set(
80
+ Object.values(isoformSequences ?? {}).map(v => stripStopCodon(v.seq)),
81
+ )
82
+ const canonicalFirst = [...models].sort(
83
+ (a, b) =>
84
+ Number(a.accession.includes('-')) - Number(b.accession.includes('-')),
85
+ )
86
+ return (
87
+ canonicalFirst.find(m => translations.has(m.sequence)) ??
88
+ canonicalFirst.find(m => !m.accession.includes('-')) ??
89
+ [...models].sort((a, b) => b.sequence.length - a.sequence.length)[0]
90
+ )
91
+ }