m3triq 0.2.11 → 0.2.12
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/client.d.ts +10 -1
- package/dist/client.js +23 -0
- package/dist/commands/msa.js +37 -12
- package/dist/commands/predict.js +113 -5
- package/dist/types.d.ts +28 -0
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ const program = new Command();
|
|
|
25
25
|
program
|
|
26
26
|
.name('m3t')
|
|
27
27
|
.description('M3TRIQ — protein-ligand analysis from the terminal')
|
|
28
|
-
.version('0.2.
|
|
28
|
+
.version('0.2.12')
|
|
29
29
|
.option('--json', 'Output as JSON (machine-readable)')
|
|
30
30
|
.hook('preAction', (thisCommand) => {
|
|
31
31
|
const opts = thisCommand.optsWithGlobals();
|
package/dist/client.d.ts
CHANGED
|
@@ -87,8 +87,14 @@ export declare class M3triqClient {
|
|
|
87
87
|
id: string;
|
|
88
88
|
sequence: string;
|
|
89
89
|
}[];
|
|
90
|
-
depth: 'fast' | 'deep';
|
|
90
|
+
depth: 'fast' | 'deep' | 'exhaustive' | 'custom';
|
|
91
91
|
pair: boolean;
|
|
92
|
+
templates?: boolean;
|
|
93
|
+
custom?: {
|
|
94
|
+
databases: string[];
|
|
95
|
+
max_sequences?: number;
|
|
96
|
+
pair?: boolean;
|
|
97
|
+
};
|
|
92
98
|
title?: string;
|
|
93
99
|
}): Promise<{
|
|
94
100
|
job_id: string;
|
|
@@ -99,6 +105,9 @@ export declare class M3triqClient {
|
|
|
99
105
|
createBoltz2Job(params: Boltz2JobParams): Promise<{
|
|
100
106
|
job_id: string;
|
|
101
107
|
}>;
|
|
108
|
+
createOpenfold3Job(params: Boltz2JobParams): Promise<{
|
|
109
|
+
job_id: string;
|
|
110
|
+
}>;
|
|
102
111
|
createSandboxJob(params: SandboxParams): Promise<{
|
|
103
112
|
job_id: string;
|
|
104
113
|
}>;
|
package/dist/client.js
CHANGED
|
@@ -140,6 +140,8 @@ export class M3triqClient {
|
|
|
140
140
|
chains: params.chains,
|
|
141
141
|
depth: params.depth,
|
|
142
142
|
pair: params.pair,
|
|
143
|
+
templates: params.templates ?? false,
|
|
144
|
+
...(params.custom ? { custom: params.custom } : {}),
|
|
143
145
|
}),
|
|
144
146
|
});
|
|
145
147
|
if (!res.ok) {
|
|
@@ -172,6 +174,27 @@ export class M3triqClient {
|
|
|
172
174
|
}
|
|
173
175
|
return res.json();
|
|
174
176
|
}
|
|
177
|
+
async createOpenfold3Job(params) {
|
|
178
|
+
// OpenFold3 prediction runs client-side via the agents MCP, then is saved here
|
|
179
|
+
// as a completed job (same internal path Boltz-2 uses; server is_openfold3 branch).
|
|
180
|
+
const url = `${this.baseUrl}/api/jobs/create_prediction_internal/`;
|
|
181
|
+
const res = await fetch(url, {
|
|
182
|
+
method: 'POST',
|
|
183
|
+
headers: { ...this.headers, 'X-Internal-Service': 'true' },
|
|
184
|
+
body: JSON.stringify({
|
|
185
|
+
project_id: params.project_id,
|
|
186
|
+
job_type: 'openfold3_prediction',
|
|
187
|
+
title: params.title,
|
|
188
|
+
description: params.description ?? '',
|
|
189
|
+
result_data: params.result_data,
|
|
190
|
+
}),
|
|
191
|
+
});
|
|
192
|
+
if (!res.ok) {
|
|
193
|
+
const text = await res.text();
|
|
194
|
+
throw new Error(`API error ${res.status}: ${text.substring(0, 200)}`);
|
|
195
|
+
}
|
|
196
|
+
return res.json();
|
|
197
|
+
}
|
|
175
198
|
async createSandboxJob(params) {
|
|
176
199
|
return this.request('POST', '/api/jobs/create_sandbox_job/', params);
|
|
177
200
|
}
|
package/dist/commands/msa.js
CHANGED
|
@@ -3,15 +3,24 @@ import { createClient, getConsoleUrl } from '../cli.js';
|
|
|
3
3
|
import { requireProject } from '../config.js';
|
|
4
4
|
import { output } from '../output.js';
|
|
5
5
|
import { jobUrl, maybeOpenBrowser } from '../url.js';
|
|
6
|
-
// m3t msa — generate a multiple-sequence alignment (a3m) for one or more chains
|
|
7
|
-
//
|
|
8
|
-
//
|
|
6
|
+
// m3t msa — generate a multiple-sequence alignment (a3m) for one or more chains,
|
|
7
|
+
// all on the self-hosted ColabFold MSA-search engine (GPU-MMseqs2):
|
|
8
|
+
// fast = UniRef30, shallow, no pairing (quick triage)
|
|
9
|
+
// deep = UniRef30, full depth + species pairing (complexes/hard families)
|
|
10
|
+
// exhaustive = deep + the metagenomic envDB (remote-homology / orphan families)
|
|
11
|
+
// custom = you pick the databases (+ max_sequences / pairing)
|
|
12
|
+
// --templates also searches the PDB and saves per-chain structural templates.
|
|
13
|
+
// Produces a downloadable .a3m artifact per chain (+ templates_<chain>.json).
|
|
14
|
+
const MSA_DEPTHS = ['fast', 'deep', 'exhaustive', 'custom'];
|
|
9
15
|
export function registerMsaCommands(program) {
|
|
10
16
|
program.command('msa')
|
|
11
17
|
.description('Generate a multiple-sequence alignment (a3m) — per-chain and (for complexes) species-paired')
|
|
12
18
|
.option('--chain <id:seq>', 'Chain as "id:sequence" or "id:@path". Repeatable for multi-chain (paired) MSAs.', collectArg, [])
|
|
13
19
|
.option('--sequence <seq>', 'Single-chain shortcut (chain id "A"). Use --chain for multi-chain.')
|
|
14
|
-
.option('--depth <tier>', 'MSA depth:
|
|
20
|
+
.option('--depth <tier>', 'MSA depth: fast | deep | exhaustive | custom (see `m3t msa --help`)', 'fast')
|
|
21
|
+
.option('--templates', 'Also search the PDB for structural templates; saves a templates_<chain>.json per chain.')
|
|
22
|
+
.option('--databases <csv>', 'For --depth custom: comma-separated database ids (e.g. "uniref30,envdb"). See get_msa_capabilities.')
|
|
23
|
+
.option('--max-sequences <n>', 'For --depth custom: alignment depth/breadth cap.', (v) => parseInt(v, 10))
|
|
15
24
|
.option('--pair', 'Compute species-paired MSA across chains (complexes). Default on for ≥2 distinct chains.')
|
|
16
25
|
.option('--no-pair', 'Skip paired MSA (per-chain only).')
|
|
17
26
|
.option('--name <name>', 'Custom job title')
|
|
@@ -21,10 +30,20 @@ export function registerMsaCommands(program) {
|
|
|
21
30
|
}
|
|
22
31
|
async function runMsa(opts) {
|
|
23
32
|
const depth = (opts.depth || 'fast').toLowerCase();
|
|
24
|
-
if (depth
|
|
25
|
-
process.stderr.write(`Error: --depth must be
|
|
33
|
+
if (!MSA_DEPTHS.includes(depth)) {
|
|
34
|
+
process.stderr.write(`Error: --depth must be one of ${MSA_DEPTHS.join(' | ')}, got "${opts.depth}".\n`);
|
|
26
35
|
process.exit(1);
|
|
27
36
|
}
|
|
37
|
+
// Build the custom config when depth=custom.
|
|
38
|
+
let custom;
|
|
39
|
+
if (depth === 'custom') {
|
|
40
|
+
const databases = (opts.databases || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
41
|
+
if (databases.length === 0) {
|
|
42
|
+
process.stderr.write('Error: --depth custom requires --databases (e.g. --databases "uniref30,envdb").\n');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
custom = { databases, max_sequences: opts.maxSequences, pair: opts.pair };
|
|
46
|
+
}
|
|
28
47
|
const chains = [];
|
|
29
48
|
for (const raw of opts.chain) {
|
|
30
49
|
const colon = raw.indexOf(':');
|
|
@@ -50,6 +69,7 @@ async function runMsa(opts) {
|
|
|
50
69
|
// pair only matters for ≥2 distinct chains; let the server make the final call too.
|
|
51
70
|
const distinct = new Set(chains.map(c => c.sequence));
|
|
52
71
|
const pair = opts.pair && chains.length >= 2 && distinct.size >= 2;
|
|
72
|
+
const templates = !!opts.templates;
|
|
53
73
|
const project = requireProject();
|
|
54
74
|
const client = createClient();
|
|
55
75
|
const consoleUrl = getConsoleUrl();
|
|
@@ -58,21 +78,26 @@ async function runMsa(opts) {
|
|
|
58
78
|
chains,
|
|
59
79
|
depth: depth,
|
|
60
80
|
pair,
|
|
81
|
+
templates,
|
|
82
|
+
custom,
|
|
61
83
|
title: opts.name,
|
|
62
84
|
});
|
|
63
85
|
const url = jobUrl(consoleUrl, project.id, result.job_id);
|
|
64
86
|
maybeOpenBrowser(url);
|
|
65
|
-
const timeEst = depth === '
|
|
66
|
-
? '~
|
|
67
|
-
:
|
|
87
|
+
const timeEst = depth === 'exhaustive'
|
|
88
|
+
? '~4-6 min (ColabFold UniRef30 + envDB on 2 T4s; +cold start if scaled to zero)'
|
|
89
|
+
: depth === 'fast'
|
|
90
|
+
? '~2-4 min (ColabFold UniRef30; +cold start if scaled to zero)'
|
|
91
|
+
: '~3-5 min (ColabFold UniRef30 + paired; +cold start if scaled to zero)';
|
|
92
|
+
const extras = [pair ? 'paired' : '', templates ? '+templates' : ''].filter(Boolean).join(', ');
|
|
68
93
|
const lines = [
|
|
69
|
-
`MSA job queued (${chains.length} chain${chains.length > 1 ? 's' : ''}, depth=${depth}${
|
|
94
|
+
`MSA job queued (${chains.length} chain${chains.length > 1 ? 's' : ''}, depth=${depth}${extras ? `, ${extras}` : ''})`,
|
|
70
95
|
`Job ID: ${result.job_id.substring(0, 8)}`,
|
|
71
96
|
`Estimated: ${timeEst}`,
|
|
72
|
-
`Output: one .a3m artifact per chain (Files tab)`,
|
|
97
|
+
`Output: one .a3m artifact per chain${templates ? ' (+ templates_<chain>.json)' : ''} (Files tab)`,
|
|
73
98
|
`View: ${url}`,
|
|
74
99
|
];
|
|
75
|
-
output({ job_id: result.job_id, n_chains: chains.length, depth, pair, url }, lines.join('\n'));
|
|
100
|
+
output({ job_id: result.job_id, n_chains: chains.length, depth, pair, templates, url }, lines.join('\n'));
|
|
76
101
|
}
|
|
77
102
|
function collectArg(value, prev) {
|
|
78
103
|
return [...prev, value];
|
package/dist/commands/predict.js
CHANGED
|
@@ -13,13 +13,19 @@ export function registerPredictCommands(program) {
|
|
|
13
13
|
.action(async (sequence, opts) => {
|
|
14
14
|
await runPredict(sequence, 'esmfold', opts.name);
|
|
15
15
|
});
|
|
16
|
-
// m3t predict alphafold2
|
|
16
|
+
// m3t predict alphafold2 — RETIRED (the alphafold2-nim VM was decommissioned in
|
|
17
|
+
// favour of ESMFold2). Kept as a signpost so scripts get a clear redirect instead
|
|
18
|
+
// of a job that silently fails on the deleted backend.
|
|
17
19
|
predict.command('alphafold2')
|
|
18
|
-
.argument('
|
|
20
|
+
.argument('[sequence]', 'Amino acid sequence (single letter codes)')
|
|
19
21
|
.option('--name <name>', 'Name for the prediction')
|
|
20
|
-
.description('
|
|
21
|
-
.action(async (
|
|
22
|
-
|
|
22
|
+
.description('[RETIRED → use `predict esmfold2`] AlphaFold2 single-sequence prediction')
|
|
23
|
+
.action(async () => {
|
|
24
|
+
process.stderr.write('AlphaFold2 has been retired (the self-hosted backend was decommissioned).\n' +
|
|
25
|
+
'Use ESMFold2 instead — higher accuracy and it folds complexes too:\n' +
|
|
26
|
+
' m3t predict esmfold2 --sequence <seq> # monomer\n' +
|
|
27
|
+
' m3t predict esmfold2 --chain A:<seq> --chain B:<seq> # complex\n');
|
|
28
|
+
process.exit(2);
|
|
23
29
|
});
|
|
24
30
|
// m3t predict esmfold2 — monomer OR multi-chain (antibody-antigen, PPI)
|
|
25
31
|
predict.command('esmfold2')
|
|
@@ -68,6 +74,20 @@ export function registerPredictCommands(program) {
|
|
|
68
74
|
.action(async (opts) => {
|
|
69
75
|
await runBoltz2(opts);
|
|
70
76
|
});
|
|
77
|
+
// m3t predict openfold3
|
|
78
|
+
predict.command('openfold3')
|
|
79
|
+
.description('Predict biomolecular complex (protein + DNA/RNA + ligand) with OpenFold3 / AlphaFold3-class (NVIDIA NIM). Real MSAs + optional PDB templates.')
|
|
80
|
+
.option('--protein <seq>', 'Protein sequence (or @path to file). Repeatable for multi-chain complexes.', collectArg, [])
|
|
81
|
+
.option('--dna <seq>', 'DNA sequence (or @path to file). Repeatable.', collectArg, [])
|
|
82
|
+
.option('--rna <seq>', 'RNA sequence (or @path to file). Repeatable.', collectArg, [])
|
|
83
|
+
.option('--ligand <smiles_or_ccd>', 'Ligand as SMILES or CCD code (e.g. ATP). Repeatable.', collectArg, [])
|
|
84
|
+
.option('--depth <tier>', 'MSA depth for protein chains: fast | deep | exhaustive | none (default fast)', 'fast')
|
|
85
|
+
.option('--templates', 'Seed the fold with PDB structural templates per protein chain (OpenFold3 self-aligns).')
|
|
86
|
+
.option('--samples <n>', 'Number of diffusion samples (1-25, default: 1)', (v) => parseInt(v, 10), 1)
|
|
87
|
+
.option('--name <name>', 'Custom job title')
|
|
88
|
+
.action(async (opts) => {
|
|
89
|
+
await runOpenfold3(opts);
|
|
90
|
+
});
|
|
71
91
|
}
|
|
72
92
|
async function runPredict(sequence, method, name) {
|
|
73
93
|
const project = requireProject();
|
|
@@ -165,6 +185,94 @@ async function runBoltz2(opts) {
|
|
|
165
185
|
url,
|
|
166
186
|
}, lines.join('\n'));
|
|
167
187
|
}
|
|
188
|
+
async function runOpenfold3(opts) {
|
|
189
|
+
const depth = (opts.depth || 'fast').toLowerCase();
|
|
190
|
+
const OF3_DEPTHS = ['fast', 'deep', 'exhaustive', 'custom', 'none'];
|
|
191
|
+
if (!OF3_DEPTHS.includes(depth)) {
|
|
192
|
+
process.stderr.write(`Error: --depth must be one of ${OF3_DEPTHS.join(' | ')}, got "${opts.depth}".\n`);
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
// Build the OpenFold3 molecules array (proteins get chain ids A, B, ...).
|
|
196
|
+
const molecules = [];
|
|
197
|
+
const chainIds = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
198
|
+
let ci = 0;
|
|
199
|
+
for (const seq of opts.protein)
|
|
200
|
+
molecules.push({ type: 'protein', sequence: resolveSequence(seq), id: chainIds[ci++] });
|
|
201
|
+
for (const seq of opts.dna)
|
|
202
|
+
molecules.push({ type: 'dna', sequence: resolveSequence(seq), id: chainIds[ci++] });
|
|
203
|
+
for (const seq of opts.rna)
|
|
204
|
+
molecules.push({ type: 'rna', sequence: resolveSequence(seq), id: chainIds[ci++] });
|
|
205
|
+
for (const lig of opts.ligand) {
|
|
206
|
+
const parsed = parseLigand(lig);
|
|
207
|
+
molecules.push({ type: 'ligand', id: chainIds[ci++], ...('ccd_code' in parsed ? { ccd_codes: [parsed.ccd_code] } : { smiles: parsed.smiles }) });
|
|
208
|
+
}
|
|
209
|
+
if (molecules.length === 0) {
|
|
210
|
+
process.stderr.write('Error: at least one --protein, --dna, --rna, or --ligand is required.\n');
|
|
211
|
+
process.exit(1);
|
|
212
|
+
}
|
|
213
|
+
const project = requireProject();
|
|
214
|
+
const client = createClient();
|
|
215
|
+
const agents = createAgentsClient();
|
|
216
|
+
const consoleUrl = getConsoleUrl();
|
|
217
|
+
const summary = molecules.map(m => `${m.type}(${m.id})`).join(' + ');
|
|
218
|
+
process.stderr.write(`OpenFold3: ${summary} (depth=${depth}${opts.templates ? ', +templates' : ''})\n`);
|
|
219
|
+
process.stderr.write('Running prediction (NVIDIA NIM; real MSA may cold-start the engine, up to a few min)...');
|
|
220
|
+
const startTime = Date.now();
|
|
221
|
+
const mcpData = await agents.callMcpTool('bionemo', 'predict_complex_structure_openfold3', {
|
|
222
|
+
molecules,
|
|
223
|
+
msa_depth: depth,
|
|
224
|
+
templates: !!opts.templates,
|
|
225
|
+
diffusion_samples: opts.samples,
|
|
226
|
+
output_format: 'pdb',
|
|
227
|
+
});
|
|
228
|
+
const result = mcpData;
|
|
229
|
+
if (result.error || !result.success) {
|
|
230
|
+
const msg = result.error ?? result.message ?? 'Unknown error';
|
|
231
|
+
process.stderr.write(`\nFailed: ${msg}\n`);
|
|
232
|
+
process.exit(1);
|
|
233
|
+
}
|
|
234
|
+
const elapsed = Math.round((Date.now() - startTime) / 1000);
|
|
235
|
+
process.stderr.write(` done (${elapsed}s).\n`);
|
|
236
|
+
const conf = result.confidence ?? {};
|
|
237
|
+
const title = opts.name ?? `OpenFold3: ${result.complex || summary}`;
|
|
238
|
+
const job = await client.createOpenfold3Job({
|
|
239
|
+
project_id: project.id,
|
|
240
|
+
title,
|
|
241
|
+
description: `OpenFold3 complex prediction. ${result.quality ?? ''}`.trim(),
|
|
242
|
+
result_data: {
|
|
243
|
+
complex: result.complex,
|
|
244
|
+
structure_data: result.structure_data,
|
|
245
|
+
output_format: result.output_format,
|
|
246
|
+
confidence: result.confidence,
|
|
247
|
+
quality: result.quality,
|
|
248
|
+
num_structures_returned: result.num_structures_returned,
|
|
249
|
+
diffusion_samples: result.diffusion_samples,
|
|
250
|
+
inference_time_seconds: result.inference_time_seconds,
|
|
251
|
+
msa_depth: depth,
|
|
252
|
+
templates_requested: !!opts.templates,
|
|
253
|
+
templates_applied: !!result.templates_applied,
|
|
254
|
+
model: 'openfold3',
|
|
255
|
+
},
|
|
256
|
+
});
|
|
257
|
+
const url = jobUrl(consoleUrl, project.id, job.job_id);
|
|
258
|
+
maybeOpenBrowser(url);
|
|
259
|
+
const lines = [
|
|
260
|
+
`OpenFold3 prediction complete in ${elapsed}s`,
|
|
261
|
+
`Job ID: ${job.job_id.substring(0, 8)}`,
|
|
262
|
+
`Complex: ${result.complex}`,
|
|
263
|
+
`Quality: ${result.quality ?? 'unknown'}`,
|
|
264
|
+
];
|
|
265
|
+
if (conf.iptm !== null && conf.iptm !== undefined)
|
|
266
|
+
lines.push(`ipTM: ${conf.iptm.toFixed(3)}`);
|
|
267
|
+
if (conf.ptm !== null && conf.ptm !== undefined)
|
|
268
|
+
lines.push(`pTM: ${conf.ptm.toFixed(3)}`);
|
|
269
|
+
if (conf.plddt !== null && conf.plddt !== undefined)
|
|
270
|
+
lines.push(`pLDDT: ${conf.plddt}`);
|
|
271
|
+
if (opts.templates)
|
|
272
|
+
lines.push(`Templates: ${result.templates_applied ? 'applied' : 'requested but not applied (NIM dropped them)'}`);
|
|
273
|
+
lines.push(`View: ${url}`);
|
|
274
|
+
output({ job_id: job.job_id, elapsed_seconds: elapsed, complex: result.complex, quality: result.quality, confidence: result.confidence, msa_depth: depth, templates_requested: !!opts.templates, templates_applied: !!result.templates_applied, url }, lines.join('\n'));
|
|
275
|
+
}
|
|
168
276
|
function collectArg(value, prev) {
|
|
169
277
|
return [...prev, value];
|
|
170
278
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -129,6 +129,34 @@ export interface Boltz2JobParams {
|
|
|
129
129
|
result_data: Record<string, unknown>;
|
|
130
130
|
description?: string;
|
|
131
131
|
}
|
|
132
|
+
export interface Openfold3Molecule {
|
|
133
|
+
type: 'protein' | 'dna' | 'rna' | 'ligand';
|
|
134
|
+
sequence?: string;
|
|
135
|
+
id?: string;
|
|
136
|
+
smiles?: string;
|
|
137
|
+
ccd_codes?: string[];
|
|
138
|
+
}
|
|
139
|
+
export interface Openfold3McpResult {
|
|
140
|
+
success?: boolean;
|
|
141
|
+
model?: string;
|
|
142
|
+
complex?: string;
|
|
143
|
+
output_format?: string;
|
|
144
|
+
diffusion_samples?: number;
|
|
145
|
+
num_structures_returned?: number;
|
|
146
|
+
structure_data?: string;
|
|
147
|
+
confidence?: {
|
|
148
|
+
plddt?: number | null;
|
|
149
|
+
ptm?: number | null;
|
|
150
|
+
iptm?: number | null;
|
|
151
|
+
confidence_score?: number | null;
|
|
152
|
+
pde?: number | null;
|
|
153
|
+
} | null;
|
|
154
|
+
quality?: string;
|
|
155
|
+
templates_applied?: boolean;
|
|
156
|
+
inference_time_seconds?: number | null;
|
|
157
|
+
message?: string;
|
|
158
|
+
error?: string;
|
|
159
|
+
}
|
|
132
160
|
export interface SandboxParams {
|
|
133
161
|
project_id: string;
|
|
134
162
|
script: string;
|