m3triq 0.2.2 → 0.2.4
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/dist/cli.js +1 -1
- package/dist/commands/docking.js +110 -15
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -23,7 +23,7 @@ const program = new Command();
|
|
|
23
23
|
program
|
|
24
24
|
.name('m3t')
|
|
25
25
|
.description('M3TRIQ — protein-ligand analysis from the terminal')
|
|
26
|
-
.version('0.2.
|
|
26
|
+
.version('0.2.4')
|
|
27
27
|
.option('--json', 'Output as JSON (machine-readable)')
|
|
28
28
|
.hook('preAction', (thisCommand) => {
|
|
29
29
|
const opts = thisCommand.optsWithGlobals();
|
package/dist/commands/docking.js
CHANGED
|
@@ -27,19 +27,112 @@ function parseCompoundsFile(filePath) {
|
|
|
27
27
|
};
|
|
28
28
|
});
|
|
29
29
|
}
|
|
30
|
-
/**
|
|
31
|
-
|
|
30
|
+
/**
|
|
31
|
+
* Resolve PDB argument to file content.
|
|
32
|
+
* - existing file path → read it
|
|
33
|
+
* - 4-letter PDB ID (e.g. "3HTB") → fetch from RCSB
|
|
34
|
+
* - anything else (assume raw PDB text already) → return as-is
|
|
35
|
+
*/
|
|
36
|
+
async function resolvePdb(pdb) {
|
|
32
37
|
if (fs.existsSync(pdb)) {
|
|
33
38
|
return { content: fs.readFileSync(pdb, 'utf-8'), label: pdb.split('/').pop() || pdb };
|
|
34
39
|
}
|
|
40
|
+
if (/^[0-9A-Za-z]{4}$/.test(pdb)) {
|
|
41
|
+
const id = pdb.toUpperCase();
|
|
42
|
+
const res = await fetch(`https://files.rcsb.org/download/${id}.pdb`);
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
throw new Error(`PDB '${id}' not found at RCSB (HTTP ${res.status}). ` +
|
|
45
|
+
`Check the ID at https://rcsb.org/structure/${id} or pass a local .pdb file.`);
|
|
46
|
+
}
|
|
47
|
+
return { content: await res.text(), label: id };
|
|
48
|
+
}
|
|
35
49
|
return { content: pdb, label: pdb };
|
|
36
50
|
}
|
|
37
|
-
|
|
51
|
+
// HETATM residues that are not real ligands — waters, ions, common cryoprotectants
|
|
52
|
+
// and buffer components. Excluded from auto binding-site detection.
|
|
53
|
+
const FILLER_HETATMS = new Set([
|
|
54
|
+
'HOH', 'WAT', 'DOD', 'D2O',
|
|
55
|
+
'NA', 'CL', 'K', 'BR', 'I', 'F', 'MG', 'CA', 'ZN', 'FE', 'MN', 'CU', 'NI', 'CO', 'CD', 'HG', 'PB', 'BA', 'CS', 'SR',
|
|
56
|
+
'SO4', 'PO4', 'BO3', 'NO3', 'CO3', 'HCO3', 'OH', 'OXY', 'PER', 'SUL', 'MOO',
|
|
57
|
+
'GOL', 'EDO', 'PEG', 'PG4', 'PE4', 'PGE', 'MPD', 'BME', 'DTT', 'MRD', 'BU3', 'P6G', 'BTB', 'EPE', 'HEPES',
|
|
58
|
+
'TRS', 'TRIS', 'ACT', 'ACE', 'FMT', 'EOH', 'IPA', 'DMS', 'DMF', 'MES', 'BES', 'GLY', 'CIT', 'MLT', 'TLA',
|
|
59
|
+
'NH4', 'PYR', 'HED', 'BNG', 'LMT', 'OLA', 'OLB', 'OLC',
|
|
60
|
+
]);
|
|
61
|
+
/** Find the largest non-filler HETATM residue and return its centroid. Returns null if none. */
|
|
62
|
+
function autoDetectSite(pdbContent) {
|
|
63
|
+
const groups = new Map();
|
|
64
|
+
for (const line of pdbContent.split('\n')) {
|
|
65
|
+
if (!line.startsWith('HETATM'))
|
|
66
|
+
continue;
|
|
67
|
+
const resname = line.slice(17, 20).trim();
|
|
68
|
+
if (FILLER_HETATMS.has(resname.toUpperCase()))
|
|
69
|
+
continue;
|
|
70
|
+
const chain = line.slice(21, 22).trim() || ' ';
|
|
71
|
+
const resnum = parseInt(line.slice(22, 26).trim(), 10);
|
|
72
|
+
const x = parseFloat(line.slice(30, 38));
|
|
73
|
+
const y = parseFloat(line.slice(38, 46));
|
|
74
|
+
const z = parseFloat(line.slice(46, 54));
|
|
75
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z))
|
|
76
|
+
continue;
|
|
77
|
+
const key = `${resname}|${chain}|${resnum}`;
|
|
78
|
+
let g = groups.get(key);
|
|
79
|
+
if (!g) {
|
|
80
|
+
g = { resname, chain, resnum, xs: [], ys: [], zs: [] };
|
|
81
|
+
groups.set(key, g);
|
|
82
|
+
}
|
|
83
|
+
g.xs.push(x);
|
|
84
|
+
g.ys.push(y);
|
|
85
|
+
g.zs.push(z);
|
|
86
|
+
}
|
|
87
|
+
let best = null;
|
|
88
|
+
for (const g of groups.values()) {
|
|
89
|
+
if (!best || g.xs.length > best.xs.length)
|
|
90
|
+
best = g;
|
|
91
|
+
}
|
|
92
|
+
// A real ligand has ≥5 heavy atoms; anything smaller is likely a stray modification.
|
|
93
|
+
if (!best || best.xs.length < 5)
|
|
94
|
+
return null;
|
|
95
|
+
const n = best.xs.length;
|
|
96
|
+
const mean = (a) => Math.round((a.reduce((s, v) => s + v, 0) / n) * 1000) / 1000;
|
|
97
|
+
return {
|
|
98
|
+
resname: best.resname, chain: best.chain, resnum: best.resnum,
|
|
99
|
+
center: { x: mean(best.xs), y: mean(best.ys), z: mean(best.zs) },
|
|
100
|
+
atomCount: n,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Resolve the binding site for a docking job.
|
|
105
|
+
* Throws on explicit (0,0,0) (a known agent placeholder). When any coord is
|
|
106
|
+
* missing, runs autoDetectSite() on the PDB content; logs what was picked to
|
|
107
|
+
* stderr so the agent sees the decision.
|
|
108
|
+
*/
|
|
109
|
+
function resolveSite(pdbContent, pdbLabel, cx, cy, cz) {
|
|
110
|
+
const allSet = [cx, cy, cz].every(v => typeof v === 'number' && !Number.isNaN(v));
|
|
111
|
+
const allZero = allSet && cx === 0 && cy === 0 && cz === 0;
|
|
112
|
+
if (allZero) {
|
|
113
|
+
throw new Error(`Binding site (0,0,0) is not a valid center. To find real coordinates, ` +
|
|
114
|
+
`check the co-crystallized ligand at https://rcsb.org/structure/${pdbLabel}/ligands, ` +
|
|
115
|
+
`or omit --cx/--cy/--cz to auto-detect from the PDB.`);
|
|
116
|
+
}
|
|
117
|
+
if (allSet)
|
|
118
|
+
return { x: cx, y: cy, z: cz };
|
|
119
|
+
const site = autoDetectSite(pdbContent);
|
|
120
|
+
if (!site) {
|
|
121
|
+
throw new Error(`Could not auto-detect a binding site in ${pdbLabel} (no co-crystallized ligand ` +
|
|
122
|
+
`with ≥5 heavy atoms). Specify --cx/--cy/--cz manually — check ligands at ` +
|
|
123
|
+
`https://rcsb.org/structure/${pdbLabel}/ligands.`);
|
|
124
|
+
}
|
|
125
|
+
process.stderr.write(`Auto-detected binding site at HETATM ${site.resname} ${site.chain}${site.resnum} ` +
|
|
126
|
+
`(${site.atomCount} atoms): center=(${site.center.x}, ${site.center.y}, ${site.center.z})\n`);
|
|
127
|
+
return site.center;
|
|
128
|
+
}
|
|
129
|
+
/** Shared binding site options for site-directed docking (Vina/GNINA).
|
|
130
|
+
* Coords are OPTIONAL — if omitted, the CLI auto-detects from the PDB. */
|
|
38
131
|
function addSiteOptions(cmd) {
|
|
39
132
|
return cmd
|
|
40
|
-
.
|
|
41
|
-
.
|
|
42
|
-
.
|
|
133
|
+
.option('--cx <x>', 'Binding site center X (omit to auto-detect)', parseFloat)
|
|
134
|
+
.option('--cy <y>', 'Binding site center Y (omit to auto-detect)', parseFloat)
|
|
135
|
+
.option('--cz <z>', 'Binding site center Z (omit to auto-detect)', parseFloat)
|
|
43
136
|
.option('--sx <x>', 'Search box size X (default: 20)', parseFloat)
|
|
44
137
|
.option('--sy <y>', 'Search box size Y (default: 20)', parseFloat)
|
|
45
138
|
.option('--sz <z>', 'Search box size Z (default: 20)', parseFloat);
|
|
@@ -73,7 +166,7 @@ export function registerDockingCommands(program) {
|
|
|
73
166
|
const project = requireProject();
|
|
74
167
|
const client = createClient();
|
|
75
168
|
const consoleUrl = getConsoleUrl();
|
|
76
|
-
const { content: pdbContent } = resolvePdb(pdb);
|
|
169
|
+
const { content: pdbContent } = await resolvePdb(pdb);
|
|
77
170
|
const result = await client.createDiffDockJob({
|
|
78
171
|
project_id: project.id,
|
|
79
172
|
ligand_smiles: smiles,
|
|
@@ -106,7 +199,8 @@ async function runSiteDock(smiles, pdb, method, opts) {
|
|
|
106
199
|
const project = requireProject();
|
|
107
200
|
const client = createClient();
|
|
108
201
|
const consoleUrl = getConsoleUrl();
|
|
109
|
-
const { content: pdbContent, label: pdbLabel } = resolvePdb(pdb);
|
|
202
|
+
const { content: pdbContent, label: pdbLabel } = await resolvePdb(pdb);
|
|
203
|
+
const site = resolveSite(pdbContent, pdbLabel, opts.cx, opts.cy, opts.cz);
|
|
110
204
|
const title = `Dock ${smiles.substring(0, 30)} → ${pdbLabel}`;
|
|
111
205
|
const result = await client.createDockingJob({
|
|
112
206
|
project_id: project.id,
|
|
@@ -115,9 +209,9 @@ async function runSiteDock(smiles, pdb, method, opts) {
|
|
|
115
209
|
ligand_smiles: smiles,
|
|
116
210
|
scoring_function: method,
|
|
117
211
|
exhaustiveness: opts.exhaustiveness,
|
|
118
|
-
center_x:
|
|
119
|
-
center_y:
|
|
120
|
-
center_z:
|
|
212
|
+
center_x: site.x,
|
|
213
|
+
center_y: site.y,
|
|
214
|
+
center_z: site.z,
|
|
121
215
|
size_x: opts.sx,
|
|
122
216
|
size_y: opts.sy,
|
|
123
217
|
size_z: opts.sz,
|
|
@@ -133,7 +227,8 @@ async function runBatchDock(file, pdb, method, opts) {
|
|
|
133
227
|
const client = createClient();
|
|
134
228
|
const consoleUrl = getConsoleUrl();
|
|
135
229
|
const compounds = parseCompoundsFile(file);
|
|
136
|
-
const { content: pdbContent, label: pdbLabel } = resolvePdb(pdb);
|
|
230
|
+
const { content: pdbContent, label: pdbLabel } = await resolvePdb(pdb);
|
|
231
|
+
const site = resolveSite(pdbContent, pdbLabel, opts.cx, opts.cy, opts.cz);
|
|
137
232
|
const title = `Batch ${method.toUpperCase()} ${compounds.length} compounds → ${pdbLabel}`;
|
|
138
233
|
const result = await client.createBatchDockingJob({
|
|
139
234
|
project_id: project.id,
|
|
@@ -141,9 +236,9 @@ async function runBatchDock(file, pdb, method, opts) {
|
|
|
141
236
|
protein_pdb: pdbContent,
|
|
142
237
|
ligand_smiles_list: compounds.map(c => c.smiles),
|
|
143
238
|
scoring_function: method,
|
|
144
|
-
center_x:
|
|
145
|
-
center_y:
|
|
146
|
-
center_z:
|
|
239
|
+
center_x: site.x,
|
|
240
|
+
center_y: site.y,
|
|
241
|
+
center_z: site.z,
|
|
147
242
|
size_x: opts.sx,
|
|
148
243
|
size_y: opts.sy,
|
|
149
244
|
size_z: opts.sz,
|