m3triq 0.2.15 → 0.2.19
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/README.md +20 -8
- package/dist/cli.js +2 -2
- package/dist/client.d.ts +16 -1
- package/dist/client.js +12 -2
- package/dist/commands/glycan.js +2 -2
- package/dist/commands/md.js +13 -1
- package/dist/commands/predict.js +63 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -78,9 +78,16 @@ GPU-accelerated simulations to validate docking poses.
|
|
|
78
78
|
```bash
|
|
79
79
|
m3t md run --protein 5NJ8 --ligand-smiles "CCO" --mode quick
|
|
80
80
|
m3t md run --diffdock-job <job-id> --mode standard
|
|
81
|
+
m3t md run --protein <oriented.pdb> --membrane # cell-membrane MD (POPC bilayer, CHARMM36)
|
|
82
|
+
m3t md run --protein 5NJ8 --ligand-smiles "CCO" --gpu h100 # run on an H100 (80GB VRAM)
|
|
83
|
+
m3t md run --protein 5NJ8 --ligand-smiles "CCO" --gpu h200 # …or an H200 (141GB, ~1.4× bandwidth)
|
|
81
84
|
m3t md results <job-id>
|
|
82
85
|
```
|
|
83
86
|
|
|
87
|
+
`--membrane` runs cell-membrane MD for membrane proteins (GPCRs, transporters, ion channels): the protein is embedded in a POPC lipid bilayer with CHARMM36 instead of a plain water box. The input **must be a membrane-oriented structure** (transmembrane axis along Z, e.g. from the [OPM database](https://opm.phar.umich.edu)) — the worker does not orient the protein for you, so a raw RCSB structure produces a misaligned bilayer.
|
|
88
|
+
|
|
89
|
+
`--gpu` selects the GPU backend: `a100` (default, GCP A100-40GB), `h100` (Nebius H100-80GB) or `h200` (Nebius H200-141GB) — both Nebius GPUs live in eu-north1. For typical soluble protein-ligand MD the A100 is the cost-effective default; reach for a Nebius GPU when the system is large enough to need the big VRAM (membrane bilayers, large multi-chain complexes) or when you want lower wall-clock latency. `h200` has ~1.4× the memory bandwidth of `h100` (and MD is bandwidth-bound), so it's usually a bit faster for a modestly higher rate. Nebius runs are billed at the matching GPU rate.
|
|
90
|
+
|
|
84
91
|
## Structure Prediction
|
|
85
92
|
|
|
86
93
|
```bash
|
|
@@ -93,6 +100,11 @@ m3t predict esmfold2 --sequence MKFLILLFNILCL... # monomer
|
|
|
93
100
|
m3t predict esmfold2 --chain A:EVQL... --chain B:DIQM... # complex (antibody-antigen, PPI)
|
|
94
101
|
m3t predict esmfold2-batch inputs.json # fold N complexes (results in input order)
|
|
95
102
|
|
|
103
|
+
# AF2-Multimer — fold a protein complex (homo-/hetero-oligomer) with deep MSA + PDB templates
|
|
104
|
+
m3t predict af2multimer --protein MKFL... --copies 3 # homotrimer (the biological unit)
|
|
105
|
+
m3t predict af2multimer --protein EVQL... --protein DIQM... # hetero-complex (distinct chains)
|
|
106
|
+
m3t predict af2multimer --protein A... --protein B... --copies 2,1 # 2×A + 1×B
|
|
107
|
+
|
|
96
108
|
# Boltz-2 — biomolecular complex (protein + DNA/RNA + ligand) with binding affinity
|
|
97
109
|
m3t predict boltz2 --protein MKFL... --ligand "CC(=O)O" --rna GGUC...
|
|
98
110
|
m3t predict boltz2 --protein MKFL... --ligand "CC(=O)O" --depth exhaustive # + real per-chain MSAs
|
|
@@ -116,19 +128,19 @@ m3t glycan build "Neu5Ac(a2-3)Gal(b1-4)Glc" --out sialyllactose.sdf --attach N-l
|
|
|
116
128
|
|
|
117
129
|
# Co-fold a protein with a glycan
|
|
118
130
|
m3t predict openfold3 --protein MKFL... \
|
|
119
|
-
--glycan "Man(a1-3)[Man(a1-6)]Man(b1-4)GlcNAc(b1-4)GlcNAc" --depth exhaustive #
|
|
120
|
-
m3t predict openfold3 --protein MKFL... --glycan "Gal(b1-4)GlcNAc" --glycan-
|
|
131
|
+
--glycan "Man(a1-3)[Man(a1-6)]Man(b1-4)GlcNAc(b1-4)GlcNAc" --depth exhaustive # one connected SMILES ligand (default)
|
|
132
|
+
m3t predict openfold3 --protein MKFL... --glycan "Gal(b1-4)GlcNAc" --glycan-ccd # experimental CCD + bondedAtomPairs (see caveat)
|
|
121
133
|
m3t predict boltz2 --protein MKFL... --glycan "Gal(b1-4)GlcNAc" --depth exhaustive
|
|
122
134
|
```
|
|
123
135
|
|
|
124
136
|
- **IUPAC-condensed**: reducing end on the **right**, linkages in `()`, branches in `[]`
|
|
125
137
|
(e.g. `Neu5Ac(a2-3)Gal(b1-4)Glc`). Common monosaccharides map to PDB CCD codes.
|
|
126
|
-
- `--glycan`
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
- `--glycan` defaults to **one connected SMILES ligand**. The AF3-glycan literature
|
|
139
|
+
recommends per-residue CCD + `bondedAtomPairs` — but that assumes the model honors the
|
|
140
|
+
bonds, and the **hosted OpenFold3 NIM does not** (verified: a 2-sugar CCD glycan comes out
|
|
141
|
+
with the sugars ~4 Å apart, disconnected). SMILES is one bonded molecule, so it stays
|
|
142
|
+
connected. `--glycan-ccd` opts into the CCD path for AF3-proper backends — validate the
|
|
143
|
+
result with the geometry checker.
|
|
132
144
|
- `--glycan` on **Boltz-2** rides as a single SMILES ligand (Boltz-2 has no inter-entity
|
|
133
145
|
bond field).
|
|
134
146
|
- **pLDDT is unreliable for carbohydrates** — high confidence can accompany wrong
|
package/dist/cli.js
CHANGED
|
@@ -26,7 +26,7 @@ const program = new Command();
|
|
|
26
26
|
program
|
|
27
27
|
.name('m3t')
|
|
28
28
|
.description('M3TRIQ — protein-ligand analysis from the terminal')
|
|
29
|
-
.version('0.2.
|
|
29
|
+
.version('0.2.19')
|
|
30
30
|
.option('--json', 'Output as JSON (machine-readable)')
|
|
31
31
|
.hook('preAction', (thisCommand) => {
|
|
32
32
|
const opts = thisCommand.optsWithGlobals();
|
|
@@ -107,5 +107,5 @@ export function getConsoleUrl() {
|
|
|
107
107
|
/** Create an agents client for MCP tool calls. */
|
|
108
108
|
export function createAgentsClient() {
|
|
109
109
|
const config = getEffectiveConfig();
|
|
110
|
-
return new AgentsClient(config.agents_url);
|
|
110
|
+
return new AgentsClient(config.agents_url, config.api_key ?? '');
|
|
111
111
|
}
|
package/dist/client.d.ts
CHANGED
|
@@ -61,6 +61,20 @@ export declare class M3triqClient {
|
|
|
61
61
|
}): Promise<{
|
|
62
62
|
job_id: string;
|
|
63
63
|
}>;
|
|
64
|
+
createAf2MultimerComplexJob(params: {
|
|
65
|
+
project_id: string;
|
|
66
|
+
chains: {
|
|
67
|
+
id?: string;
|
|
68
|
+
sequence: string;
|
|
69
|
+
count?: number;
|
|
70
|
+
}[];
|
|
71
|
+
title?: string;
|
|
72
|
+
save_structure?: boolean;
|
|
73
|
+
}): Promise<{
|
|
74
|
+
job_id: string;
|
|
75
|
+
task_id?: string;
|
|
76
|
+
n_chains?: number;
|
|
77
|
+
}>;
|
|
64
78
|
createEsmfold2BatchJob(params: {
|
|
65
79
|
project_id: string;
|
|
66
80
|
inputs: {
|
|
@@ -127,6 +141,7 @@ export declare class M3triqClient {
|
|
|
127
141
|
*/
|
|
128
142
|
export declare class AgentsClient {
|
|
129
143
|
private baseUrl;
|
|
130
|
-
|
|
144
|
+
private apiKey;
|
|
145
|
+
constructor(baseUrl: string, apiKey?: string);
|
|
131
146
|
callMcpTool(server: string, tool: string, params?: Record<string, unknown>): Promise<McpCallResult>;
|
|
132
147
|
}
|
package/dist/client.js
CHANGED
|
@@ -123,6 +123,9 @@ export class M3triqClient {
|
|
|
123
123
|
async createEsmcEmbedJob(params) {
|
|
124
124
|
return this.request('POST', '/api/jobs/create_esmc_embed/', params);
|
|
125
125
|
}
|
|
126
|
+
async createAf2MultimerComplexJob(params) {
|
|
127
|
+
return this.request('POST', '/api/jobs/create_af2_multimer_complex/', params);
|
|
128
|
+
}
|
|
126
129
|
async createEsmfold2BatchJob(params) {
|
|
127
130
|
return this.request('POST', '/api/jobs/create_esmfold2_batch/', params);
|
|
128
131
|
}
|
|
@@ -229,14 +232,21 @@ export class M3triqClient {
|
|
|
229
232
|
*/
|
|
230
233
|
export class AgentsClient {
|
|
231
234
|
baseUrl;
|
|
232
|
-
|
|
235
|
+
apiKey;
|
|
236
|
+
constructor(baseUrl, apiKey = '') {
|
|
233
237
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
238
|
+
this.apiKey = apiKey;
|
|
234
239
|
}
|
|
235
240
|
async callMcpTool(server, tool, params = {}) {
|
|
236
241
|
const url = `${this.baseUrl}/mcp/call`;
|
|
242
|
+
// /mcp/call runs MCP tools with the platform's internal credentials, so it authenticates
|
|
243
|
+
// the caller and checks project access. Without this header the request is rejected 403.
|
|
244
|
+
const headers = { 'Content-Type': 'application/json' };
|
|
245
|
+
if (this.apiKey)
|
|
246
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
237
247
|
const res = await fetch(url, {
|
|
238
248
|
method: 'POST',
|
|
239
|
-
headers
|
|
249
|
+
headers,
|
|
240
250
|
body: JSON.stringify({ server, tool, params }),
|
|
241
251
|
});
|
|
242
252
|
if (!res.ok) {
|
package/dist/commands/glycan.js
CHANGED
|
@@ -59,11 +59,11 @@ async function runGlycanBuild(iupac, opts) {
|
|
|
59
59
|
if (result.bonds && result.bonds.length > 0) {
|
|
60
60
|
lines.push(`Glycosidic bonds: ${result.bonds.map(b => `${b.child_idx}.${b.child_atom}→${b.parent_idx}.${b.parent_atom} (${b.linkage})`).join(', ')}`);
|
|
61
61
|
}
|
|
62
|
-
lines.push('Use with: m3t predict openfold3 --protein <seq> --glycan "' + iupac + '" --depth exhaustive (co-folds as
|
|
62
|
+
lines.push('Use with: m3t predict openfold3 --protein <seq> --glycan "' + iupac + '" --depth exhaustive (co-folds as one connected SMILES ligand)');
|
|
63
63
|
}
|
|
64
64
|
else if (result.ccd_note) {
|
|
65
65
|
lines.push(`Note: ${result.ccd_note}`);
|
|
66
|
-
lines.push('Use with: m3t predict openfold3 --protein <seq> --glycan "' + iupac + '" --depth exhaustive (
|
|
66
|
+
lines.push('Use with: m3t predict openfold3 --protein <seq> --glycan "' + iupac + '" --depth exhaustive (SMILES ligand)');
|
|
67
67
|
}
|
|
68
68
|
if (result.attachment) {
|
|
69
69
|
const a = result.attachment;
|
package/dist/commands/md.js
CHANGED
|
@@ -17,15 +17,27 @@ export function registerMdCommands(program) {
|
|
|
17
17
|
.option('--mode <mode>', 'Simulation mode: quick (10ns ~1hr) or standard (50ns ~5hrs)', 'quick')
|
|
18
18
|
.option('--ns <n>', 'Override simulation duration in nanoseconds (1-100)')
|
|
19
19
|
.option('--temperature <K>', 'Temperature in Kelvin', '300')
|
|
20
|
+
.option('--membrane', 'Cell-membrane MD: embed the protein in a POPC lipid bilayer (CHARMM36) instead of a water box. For membrane proteins (GPCRs, transporters, channels). Provide a membrane-oriented structure (TM axis along Z, e.g. from OPM); larger system so it runs slower than a soluble-protein MD of the same length.')
|
|
20
21
|
.description('Submit a molecular dynamics simulation job')
|
|
21
22
|
.action(async (opts) => {
|
|
22
23
|
const project = requireProject();
|
|
23
24
|
const agents = createAgentsClient();
|
|
25
|
+
// --gpu (h100/h200 via Nebius) is deliberately NOT exposed yet: the Nebius backend is
|
|
26
|
+
// still pending provisioning. The plumbing below stays so it can be re-enabled by
|
|
27
|
+
// restoring the .option() line above once that lands.
|
|
28
|
+
const gpu = 'a100';
|
|
29
|
+
if (gpu !== 'a100') {
|
|
30
|
+
process.stderr.write(`Error: unsupported --gpu value\n`);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
24
33
|
const params = {
|
|
25
34
|
mode: opts.mode,
|
|
26
35
|
temperature_k: parseFloat(opts.temperature),
|
|
27
36
|
project_id: project.id,
|
|
37
|
+
gpu,
|
|
28
38
|
};
|
|
39
|
+
if (opts.membrane)
|
|
40
|
+
params.membrane = true;
|
|
29
41
|
// Input source: DiffDock job OR protein + ligand
|
|
30
42
|
if (opts.diffdockJob) {
|
|
31
43
|
params.diffdock_job_id = opts.diffdockJob;
|
|
@@ -60,7 +72,7 @@ export function registerMdCommands(program) {
|
|
|
60
72
|
}
|
|
61
73
|
if (opts.ns)
|
|
62
74
|
params.simulation_ns = parseFloat(opts.ns);
|
|
63
|
-
process.stderr.write(`Submitting MD simulation (${opts.mode} mode)...\n`);
|
|
75
|
+
process.stderr.write(`Submitting MD simulation (${opts.mode} mode, ${gpu.toUpperCase()} GPU)...\n`);
|
|
64
76
|
const data = await agents.callMcpTool('md', 'run_md_simulation', params);
|
|
65
77
|
// Extract job ID from response
|
|
66
78
|
const jobId = extractJobId(data);
|
package/dist/commands/predict.js
CHANGED
|
@@ -72,6 +72,16 @@ export function registerPredictCommands(program) {
|
|
|
72
72
|
.action(async (opts) => {
|
|
73
73
|
await runBoltz2(opts);
|
|
74
74
|
});
|
|
75
|
+
// m3t predict af2multimer — fold a protein complex (homo-/hetero-oligomer) with AF2-Multimer
|
|
76
|
+
predict.command('af2multimer')
|
|
77
|
+
.description('Fold a protein complex (homo-/hetero-oligomer) with AF2-Multimer — deep MSA + PDB templates (self-hosted A100). Use --copies for an oligomer (e.g. a homotrimer).')
|
|
78
|
+
.option('--protein <seq>', 'Protein chain sequence (or @path to file). Repeatable for hetero-complexes (distinct chains).', collectArg, [])
|
|
79
|
+
.option('--copies <csv>', 'Copies per --protein: a single number applies to all (homomer), or a CSV paired by order (e.g. "2,1"). Default 1.', '1')
|
|
80
|
+
.option('--no-save-structure', 'Do not save the predicted structure to the project')
|
|
81
|
+
.option('--name <name>', 'Custom job title')
|
|
82
|
+
.action(async (opts) => {
|
|
83
|
+
await runAf2Multimer(opts);
|
|
84
|
+
});
|
|
75
85
|
// m3t predict openfold3
|
|
76
86
|
predict.command('openfold3')
|
|
77
87
|
.description('Predict biomolecular complex (protein + DNA/RNA + ligand) with OpenFold3 / AlphaFold3-class (NVIDIA NIM). Real MSAs + optional PDB templates.')
|
|
@@ -79,8 +89,8 @@ export function registerPredictCommands(program) {
|
|
|
79
89
|
.option('--dna <seq>', 'DNA sequence (or @path to file). Repeatable.', collectArg, [])
|
|
80
90
|
.option('--rna <seq>', 'RNA sequence (or @path to file). Repeatable.', collectArg, [])
|
|
81
91
|
.option('--ligand <smiles_or_ccd>', 'Ligand as SMILES or CCD code (e.g. ATP). Repeatable.', collectArg, [])
|
|
82
|
-
.option('--glycan <iupac>', 'Carbohydrate in IUPAC-condensed notation (e.g. "Gal(b1-4)GlcNAc"). Co-folded as
|
|
83
|
-
.option('--glycan-
|
|
92
|
+
.option('--glycan <iupac>', 'Carbohydrate in IUPAC-condensed notation (e.g. "Gal(b1-4)GlcNAc"). Co-folded as one connected SMILES ligand (default — the hosted OpenFold3 NIM does NOT honor inter-residue bonds, so a CCD chain comes out disconnected). Repeatable.', collectArg, [])
|
|
93
|
+
.option('--glycan-ccd', 'EXPERIMENTAL: co-fold the glycan as per-residue CCD codes + glycosidic bondedAtomPairs (the AF3-recommended form). Verified that the hosted OpenFold3 NIM IGNORES the bonds → sugars come out ~4 A apart, disconnected; only use with a backend that honors bondedAtomPairs.')
|
|
84
94
|
.option('--depth <tier>', 'MSA depth for protein chains: standard (UniRef30, default) | exhaustive (+envDB, best for orphan/phage) | custom | none. (fast→standard, deep→exhaustive accepted)', 'standard')
|
|
85
95
|
.option('--templates', 'Seed the fold with PDB structural templates per protein chain (OpenFold3 self-aligns).')
|
|
86
96
|
.option('--samples <n>', 'Number of diffusion samples (1-25, default: 1)', (v) => parseInt(v, 10), 1)
|
|
@@ -233,17 +243,15 @@ async function runOpenfold3(opts) {
|
|
|
233
243
|
const parsed = parseLigand(lig);
|
|
234
244
|
molecules.push({ type: 'ligand', id: chainIds[ci++], ...('ccd_code' in parsed ? { ccd_codes: [parsed.ccd_code] } : { smiles: parsed.smiles }) });
|
|
235
245
|
}
|
|
236
|
-
// Glycans:
|
|
237
|
-
// +
|
|
246
|
+
// Glycans: default to ONE connected SMILES ligand. The AF3 literature recommends
|
|
247
|
+
// per-residue CCD + bondedAtomPairs, but that assumes the model honors the bonds —
|
|
248
|
+
// and the hosted OpenFold3 NIM does NOT (verified: a 2-residue CCD glycan comes out
|
|
249
|
+
// with the sugars ~4 A apart, disconnected). SMILES is one bonded molecule, so it
|
|
250
|
+
// stays connected. --glycan-ccd opts into the CCD path for AF3-proper backends.
|
|
238
251
|
const bonds = [];
|
|
239
252
|
for (const g of opts.glycan) {
|
|
240
253
|
const built = await buildGlycanViaMcp(g, false);
|
|
241
|
-
|
|
242
|
-
// literature finds a single SMILES blob "frequently fails to reproduce correct
|
|
243
|
-
// conformations even for simple linear oligosaccharides", and recommends
|
|
244
|
-
// bondedAtomPairs + monosaccharide CCD codes. Fall back to SMILES only when the
|
|
245
|
-
// glycan can't be decomposed, or when the user forces it with --glycan-smiles.
|
|
246
|
-
const useCcd = !opts.glycanSmiles && built.ccd_supported && !!built.residues && built.residues.length > 0;
|
|
254
|
+
const useCcd = !!opts.glycanCcd && built.ccd_supported && !!built.residues && built.residues.length > 0;
|
|
247
255
|
if (useCcd) {
|
|
248
256
|
const idxToChain = {};
|
|
249
257
|
for (const res of built.residues) {
|
|
@@ -257,11 +265,11 @@ async function runOpenfold3(opts) {
|
|
|
257
265
|
atom2: [idxToChain[b.parent_idx], 1, b.parent_atom],
|
|
258
266
|
});
|
|
259
267
|
}
|
|
260
|
-
process.stderr.write(`Glycan "${g}" → ${built.residues.length} sugar residues + ${built.bonds?.length ?? 0} glycosidic bonds (CCD + bondedAtomPairs)
|
|
268
|
+
process.stderr.write(`Glycan "${g}" → ${built.residues.length} sugar residues + ${built.bonds?.length ?? 0} glycosidic bonds (CCD + bondedAtomPairs) — NOTE: the hosted NIM may not honor the bonds (sugars can come out disconnected); validate with the geometry checker.\n`);
|
|
261
269
|
}
|
|
262
270
|
else {
|
|
263
|
-
if (
|
|
264
|
-
process.stderr.write(`Glycan "${g}": CCD decomposition unavailable (${built.ccd_note ?? 'unsupported monosaccharide/topology'});
|
|
271
|
+
if (opts.glycanCcd && !built.ccd_supported) {
|
|
272
|
+
process.stderr.write(`Glycan "${g}": CCD decomposition unavailable (${built.ccd_note ?? 'unsupported monosaccharide/topology'}); using a single SMILES ligand.\n`);
|
|
265
273
|
}
|
|
266
274
|
if (!built.smiles) {
|
|
267
275
|
process.stderr.write(`Error: could not build glycan "${g}".\n`);
|
|
@@ -328,7 +336,7 @@ async function runOpenfold3(opts) {
|
|
|
328
336
|
n_rna: opts.rna.length,
|
|
329
337
|
ligands: opts.ligand.length,
|
|
330
338
|
glycans: opts.glycan,
|
|
331
|
-
glycan_repr: opts.
|
|
339
|
+
glycan_repr: opts.glycanCcd ? 'ccd+bonds' : 'smiles',
|
|
332
340
|
n_bonds: bonds.length,
|
|
333
341
|
},
|
|
334
342
|
});
|
|
@@ -353,6 +361,47 @@ async function runOpenfold3(opts) {
|
|
|
353
361
|
lines.push(`View: ${url}`);
|
|
354
362
|
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'));
|
|
355
363
|
}
|
|
364
|
+
async function runAf2Multimer(opts) {
|
|
365
|
+
const proteins = opts.protein.map(resolveSequence);
|
|
366
|
+
if (proteins.length === 0) {
|
|
367
|
+
process.stderr.write('Error: at least one --protein is required.\n');
|
|
368
|
+
process.exit(1);
|
|
369
|
+
}
|
|
370
|
+
// Parse --copies: a single value applies to every chain (homomer); a CSV pairs by order.
|
|
371
|
+
const copyTokens = (opts.copies || '1').split(',').map(s => parseInt(s.trim(), 10));
|
|
372
|
+
if (copyTokens.some(n => !Number.isFinite(n) || n < 1)) {
|
|
373
|
+
process.stderr.write(`Error: --copies must be positive integers, got "${opts.copies}".\n`);
|
|
374
|
+
process.exit(1);
|
|
375
|
+
}
|
|
376
|
+
const chains = proteins.map((sequence, i) => ({
|
|
377
|
+
sequence,
|
|
378
|
+
count: copyTokens.length === 1 ? copyTokens[0] : (copyTokens[i] ?? 1),
|
|
379
|
+
}));
|
|
380
|
+
const totalChains = chains.reduce((s, c) => s + c.count, 0);
|
|
381
|
+
if (totalChains > 12) {
|
|
382
|
+
process.stderr.write(`Error: total chains (with copies) is ${totalChains}; max 12.\n`);
|
|
383
|
+
process.exit(1);
|
|
384
|
+
}
|
|
385
|
+
const project = requireProject();
|
|
386
|
+
const client = createClient();
|
|
387
|
+
const consoleUrl = getConsoleUrl();
|
|
388
|
+
const summary = chains.map(c => `${c.sequence.length}aa×${c.count}`).join(' + ');
|
|
389
|
+
const result = await client.createAf2MultimerComplexJob({
|
|
390
|
+
project_id: project.id,
|
|
391
|
+
chains,
|
|
392
|
+
title: opts.name,
|
|
393
|
+
save_structure: opts.saveStructure !== false,
|
|
394
|
+
});
|
|
395
|
+
const url = jobUrl(consoleUrl, project.id, result.job_id);
|
|
396
|
+
maybeOpenBrowser(url);
|
|
397
|
+
const lines = [
|
|
398
|
+
`AF2-Multimer complex queued (${totalChains} chains: ${summary})`,
|
|
399
|
+
`Job ID: ${result.job_id.substring(0, 8)}`,
|
|
400
|
+
`Deep MSA + PDB templates; ~30-60s warm, ~5-8 min cold A100 start`,
|
|
401
|
+
`View: ${url}`,
|
|
402
|
+
];
|
|
403
|
+
output({ job_id: result.job_id, n_chains: totalChains, chains, url }, lines.join('\n'));
|
|
404
|
+
}
|
|
356
405
|
function collectArg(value, prev) {
|
|
357
406
|
return [...prev, value];
|
|
358
407
|
}
|