m3triq 0.2.14 → 0.2.18
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 +47 -0
- package/dist/cli.js +4 -2
- package/dist/client.d.ts +17 -2
- package/dist/client.js +14 -2
- package/dist/commands/glycan.d.ts +4 -0
- package/dist/commands/glycan.js +73 -0
- package/dist/commands/md.js +11 -1
- package/dist/commands/msa.js +1 -2
- package/dist/commands/predict.js +151 -7
- package/dist/types.d.ts +30 -0
- 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,14 +100,54 @@ 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...
|
|
110
|
+
m3t predict boltz2 --protein MKFL... --ligand "CC(=O)O" --depth exhaustive # + real per-chain MSAs
|
|
98
111
|
|
|
99
112
|
# OpenFold3 — AlphaFold3-class complex (protein + DNA/RNA + ligand); real MSAs
|
|
100
113
|
m3t predict openfold3 --protein EVQL... --protein DIQM... # complex, auto-paired
|
|
101
114
|
m3t predict openfold3 --protein MKFL... --ligand ATP --depth exhaustive
|
|
102
115
|
```
|
|
103
116
|
|
|
117
|
+
## Carbohydrate / Glycan Co-folding
|
|
118
|
+
|
|
119
|
+
Protein–carbohydrate interactions (e.g. lectins, phage receptor-binding proteins,
|
|
120
|
+
glycan-recognizing binders) are poorly served by classical docking — Vina/GNINA
|
|
121
|
+
score sugars badly. The reliable route is to **define** the glycan, then **co-fold**
|
|
122
|
+
it with the protein in an AlphaFold3-class model.
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# Define a glycan: IUPAC-condensed → SMILES + 3D SDF + per-residue CCD/bond decomposition
|
|
126
|
+
m3t glycan build "Gal(b1-4)GlcNAc"
|
|
127
|
+
m3t glycan build "Neu5Ac(a2-3)Gal(b1-4)Glc" --out sialyllactose.sdf --attach N-linked
|
|
128
|
+
|
|
129
|
+
# Co-fold a protein with a glycan
|
|
130
|
+
m3t predict openfold3 --protein MKFL... \
|
|
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)
|
|
133
|
+
m3t predict boltz2 --protein MKFL... --glycan "Gal(b1-4)GlcNAc" --depth exhaustive
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
- **IUPAC-condensed**: reducing end on the **right**, linkages in `()`, branches in `[]`
|
|
137
|
+
(e.g. `Neu5Ac(a2-3)Gal(b1-4)Glc`). Common monosaccharides map to PDB CCD codes.
|
|
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.
|
|
144
|
+
- `--glycan` on **Boltz-2** rides as a single SMILES ligand (Boltz-2 has no inter-entity
|
|
145
|
+
bond field).
|
|
146
|
+
- **pLDDT is unreliable for carbohydrates** — high confidence can accompany wrong
|
|
147
|
+
stereochemistry. Validate ring pucker / glycosidic torsions on the output.
|
|
148
|
+
- `--depth exhaustive` (metagenomic envDB) is recommended for orphan / phage / bacterial
|
|
149
|
+
families where standard databases come back sparse.
|
|
150
|
+
|
|
104
151
|
## MSA Generation
|
|
105
152
|
|
|
106
153
|
Generate a multiple-sequence alignment (`.a3m`) — the evolutionary input folders use.
|
package/dist/cli.js
CHANGED
|
@@ -11,6 +11,7 @@ import { registerJobCommands } from './commands/jobs.js';
|
|
|
11
11
|
import { registerDockingCommands } from './commands/docking.js';
|
|
12
12
|
import { registerPredictCommands } from './commands/predict.js';
|
|
13
13
|
import { registerMsaCommands } from './commands/msa.js';
|
|
14
|
+
import { registerGlycanCommands } from './commands/glycan.js';
|
|
14
15
|
import { registerChemblCommands } from './commands/chembl.js';
|
|
15
16
|
import { registerSandboxCommands } from './commands/sandbox.js';
|
|
16
17
|
import { registerDesignCommands } from './commands/design.js';
|
|
@@ -25,7 +26,7 @@ const program = new Command();
|
|
|
25
26
|
program
|
|
26
27
|
.name('m3t')
|
|
27
28
|
.description('M3TRIQ — protein-ligand analysis from the terminal')
|
|
28
|
-
.version('0.2.
|
|
29
|
+
.version('0.2.18')
|
|
29
30
|
.option('--json', 'Output as JSON (machine-readable)')
|
|
30
31
|
.hook('preAction', (thisCommand) => {
|
|
31
32
|
const opts = thisCommand.optsWithGlobals();
|
|
@@ -39,6 +40,7 @@ registerJobCommands(program);
|
|
|
39
40
|
registerDockingCommands(program);
|
|
40
41
|
registerPredictCommands(program);
|
|
41
42
|
registerMsaCommands(program);
|
|
43
|
+
registerGlycanCommands(program);
|
|
42
44
|
registerChemblCommands(program);
|
|
43
45
|
registerSandboxCommands(program);
|
|
44
46
|
registerDesignCommands(program);
|
|
@@ -105,5 +107,5 @@ export function getConsoleUrl() {
|
|
|
105
107
|
/** Create an agents client for MCP tool calls. */
|
|
106
108
|
export function createAgentsClient() {
|
|
107
109
|
const config = getEffectiveConfig();
|
|
108
|
-
return new AgentsClient(config.agents_url);
|
|
110
|
+
return new AgentsClient(config.agents_url, config.api_key ?? '');
|
|
109
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: {
|
|
@@ -87,7 +101,7 @@ export declare class M3triqClient {
|
|
|
87
101
|
id: string;
|
|
88
102
|
sequence: string;
|
|
89
103
|
}[];
|
|
90
|
-
depth: 'standard' | 'exhaustive' | 'custom'
|
|
104
|
+
depth: 'standard' | 'exhaustive' | 'custom';
|
|
91
105
|
pair: boolean;
|
|
92
106
|
templates?: boolean;
|
|
93
107
|
custom?: {
|
|
@@ -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
|
}
|
|
@@ -166,6 +169,7 @@ export class M3triqClient {
|
|
|
166
169
|
title: params.title,
|
|
167
170
|
description: params.description ?? '',
|
|
168
171
|
result_data: params.result_data,
|
|
172
|
+
...(params.prediction_inputs ? { prediction_inputs: params.prediction_inputs } : {}),
|
|
169
173
|
}),
|
|
170
174
|
});
|
|
171
175
|
if (!res.ok) {
|
|
@@ -187,6 +191,7 @@ export class M3triqClient {
|
|
|
187
191
|
title: params.title,
|
|
188
192
|
description: params.description ?? '',
|
|
189
193
|
result_data: params.result_data,
|
|
194
|
+
...(params.prediction_inputs ? { prediction_inputs: params.prediction_inputs } : {}),
|
|
190
195
|
}),
|
|
191
196
|
});
|
|
192
197
|
if (!res.ok) {
|
|
@@ -227,14 +232,21 @@ export class M3triqClient {
|
|
|
227
232
|
*/
|
|
228
233
|
export class AgentsClient {
|
|
229
234
|
baseUrl;
|
|
230
|
-
|
|
235
|
+
apiKey;
|
|
236
|
+
constructor(baseUrl, apiKey = '') {
|
|
231
237
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
|
238
|
+
this.apiKey = apiKey;
|
|
232
239
|
}
|
|
233
240
|
async callMcpTool(server, tool, params = {}) {
|
|
234
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}`;
|
|
235
247
|
const res = await fetch(url, {
|
|
236
248
|
method: 'POST',
|
|
237
|
-
headers
|
|
249
|
+
headers,
|
|
238
250
|
body: JSON.stringify({ server, tool, params }),
|
|
239
251
|
});
|
|
240
252
|
if (!res.ok) {
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { GlycanBuildResult } from '../types.js';
|
|
3
|
+
export declare function registerGlycanCommands(program: Command): void;
|
|
4
|
+
export declare function buildGlycanViaMcp(iupac: string, embed3d?: boolean, attachment?: string): Promise<GlycanBuildResult>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createAgentsClient } from '../cli.js';
|
|
3
|
+
import { output } from '../output.js';
|
|
4
|
+
export function registerGlycanCommands(program) {
|
|
5
|
+
const glycan = program
|
|
6
|
+
.command('glycan')
|
|
7
|
+
.description('Build / define carbohydrate (glycan) ligands for co-folding and docking');
|
|
8
|
+
// m3t glycan build "<iupac>"
|
|
9
|
+
glycan
|
|
10
|
+
.command('build')
|
|
11
|
+
.argument('<iupac>', 'Glycan in IUPAC-condensed notation (reducing end on the right). e.g. "Gal(b1-4)GlcNAc"')
|
|
12
|
+
.description('Build a glycan from IUPAC-condensed notation → SMILES + 3D SDF + CCD/bond decomposition')
|
|
13
|
+
.option('--out <file.sdf>', 'Write the 3D structure to an SDF file')
|
|
14
|
+
.option('--no-3d', 'Skip 3D conformer generation (SMILES + decomposition only)')
|
|
15
|
+
.option('--attach <type>', 'Also emit a protein-glycosylation attachment template: N-linked | O-linked | O-linked-thr')
|
|
16
|
+
.action(async (iupac, opts) => {
|
|
17
|
+
await runGlycanBuild(iupac, opts);
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
export async function buildGlycanViaMcp(iupac, embed3d = true, attachment) {
|
|
21
|
+
const agents = createAgentsClient();
|
|
22
|
+
const data = (await agents.callMcpTool('rdkit', 'build_glycan', {
|
|
23
|
+
iupac,
|
|
24
|
+
embed_3d: embed3d,
|
|
25
|
+
...(attachment ? { attachment } : {}),
|
|
26
|
+
}));
|
|
27
|
+
if (data.success === false || data.error) {
|
|
28
|
+
throw new Error(data.error ?? `could not build glycan "${iupac}"`);
|
|
29
|
+
}
|
|
30
|
+
return data;
|
|
31
|
+
}
|
|
32
|
+
async function runGlycanBuild(iupac, opts) {
|
|
33
|
+
const embed3d = opts['3d'] !== false || !!opts.out; // need 3D if writing SDF
|
|
34
|
+
let result;
|
|
35
|
+
try {
|
|
36
|
+
result = await buildGlycanViaMcp(iupac, embed3d, opts.attach);
|
|
37
|
+
}
|
|
38
|
+
catch (e) {
|
|
39
|
+
process.stderr.write(`Error: ${e.message}\n`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (opts.out) {
|
|
44
|
+
if (!result.sdf) {
|
|
45
|
+
process.stderr.write('Warning: no 3D structure was generated; nothing written.\n');
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
fs.writeFileSync(opts.out, result.sdf);
|
|
49
|
+
process.stderr.write(`Wrote 3D structure → ${opts.out}\n`);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const lines = [
|
|
53
|
+
`Glycan: ${iupac}`,
|
|
54
|
+
`SMILES: ${result.smiles}`,
|
|
55
|
+
`Formula: ${result.formula ?? '?'} MW: ${result.molecular_weight ?? '?'} rotatable bonds: ${result.n_rotatable_bonds ?? '?'}`,
|
|
56
|
+
];
|
|
57
|
+
if (result.ccd_supported && result.residues) {
|
|
58
|
+
lines.push(`Residues (${result.n_residues}, reducing-end first): ${result.residues.map(r => `${r.name}[${r.ccd}]`).join(' · ')}`);
|
|
59
|
+
if (result.bonds && result.bonds.length > 0) {
|
|
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
|
+
}
|
|
62
|
+
lines.push('Use with: m3t predict openfold3 --protein <seq> --glycan "' + iupac + '" --depth exhaustive (co-folds as one connected SMILES ligand)');
|
|
63
|
+
}
|
|
64
|
+
else if (result.ccd_note) {
|
|
65
|
+
lines.push(`Note: ${result.ccd_note}`);
|
|
66
|
+
lines.push('Use with: m3t predict openfold3 --protein <seq> --glycan "' + iupac + '" --depth exhaustive (SMILES ligand)');
|
|
67
|
+
}
|
|
68
|
+
if (result.attachment) {
|
|
69
|
+
const a = result.attachment;
|
|
70
|
+
lines.push(`Attachment: glycan ${a.glycan_atom} → protein ${a.protein_residue} ${a.protein_atom} (${a.type})`);
|
|
71
|
+
}
|
|
72
|
+
output(result, lines.join('\n'));
|
|
73
|
+
}
|
package/dist/commands/md.js
CHANGED
|
@@ -17,15 +17,25 @@ 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.')
|
|
21
|
+
.option('--gpu <type>', 'GPU backend: a100 (default, GCP A100-40GB), h100 (Nebius H100-80GB) or h200 (Nebius H200-141GB) — both Nebius GPUs are in eu-north1. Use a Nebius GPU for large membrane/complex systems that need the big VRAM, or for lower wall-clock latency; h200 has ~1.4× h100 bandwidth (MD is bandwidth-bound) for a modestly higher rate.', 'a100')
|
|
20
22
|
.description('Submit a molecular dynamics simulation job')
|
|
21
23
|
.action(async (opts) => {
|
|
22
24
|
const project = requireProject();
|
|
23
25
|
const agents = createAgentsClient();
|
|
26
|
+
const gpu = String(opts.gpu || 'a100').toLowerCase();
|
|
27
|
+
if (gpu !== 'a100' && gpu !== 'h100' && gpu !== 'h200') {
|
|
28
|
+
process.stderr.write(`Error: --gpu must be 'a100', 'h100' or 'h200' (got '${opts.gpu}')\n`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
24
31
|
const params = {
|
|
25
32
|
mode: opts.mode,
|
|
26
33
|
temperature_k: parseFloat(opts.temperature),
|
|
27
34
|
project_id: project.id,
|
|
35
|
+
gpu,
|
|
28
36
|
};
|
|
37
|
+
if (opts.membrane)
|
|
38
|
+
params.membrane = true;
|
|
29
39
|
// Input source: DiffDock job OR protein + ligand
|
|
30
40
|
if (opts.diffdockJob) {
|
|
31
41
|
params.diffdock_job_id = opts.diffdockJob;
|
|
@@ -60,7 +70,7 @@ export function registerMdCommands(program) {
|
|
|
60
70
|
}
|
|
61
71
|
if (opts.ns)
|
|
62
72
|
params.simulation_ns = parseFloat(opts.ns);
|
|
63
|
-
process.stderr.write(`Submitting MD simulation (${opts.mode} mode)...\n`);
|
|
73
|
+
process.stderr.write(`Submitting MD simulation (${opts.mode} mode, ${gpu.toUpperCase()} GPU)...\n`);
|
|
64
74
|
const data = await agents.callMcpTool('md', 'run_md_simulation', params);
|
|
65
75
|
// Extract job ID from response
|
|
66
76
|
const jobId = extractJobId(data);
|
package/dist/commands/msa.js
CHANGED
|
@@ -12,8 +12,7 @@ import { jobUrl, maybeOpenBrowser } from '../url.js';
|
|
|
12
12
|
// default; a monomer is never paired). --no-pair to force unpaired.
|
|
13
13
|
// --templates also searches the PDB and saves per-chain structural templates.
|
|
14
14
|
// Produces a downloadable .a3m artifact per chain (+ templates_<chain>.json).
|
|
15
|
-
|
|
16
|
-
const MSA_DEPTHS = ['standard', 'exhaustive', 'custom', 'fast', 'deep'];
|
|
15
|
+
const MSA_DEPTHS = ['standard', 'exhaustive', 'custom'];
|
|
17
16
|
export function registerMsaCommands(program) {
|
|
18
17
|
program.command('msa')
|
|
19
18
|
.description('Generate a multiple-sequence alignment (a3m) — per-chain and (for complexes) species-paired')
|
package/dist/commands/predict.js
CHANGED
|
@@ -3,6 +3,7 @@ import { createAgentsClient, 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
|
+
import { buildGlycanViaMcp } from './glycan.js';
|
|
6
7
|
export function registerPredictCommands(program) {
|
|
7
8
|
const predict = program.command('predict').description('Predict 3D structure from sequence (single protein or biomolecular complex)');
|
|
8
9
|
// m3t predict esmfold
|
|
@@ -62,6 +63,8 @@ export function registerPredictCommands(program) {
|
|
|
62
63
|
.option('--dna <seq>', 'DNA sequence (or @path to file). Repeatable.', collectArg, [])
|
|
63
64
|
.option('--rna <seq>', 'RNA sequence (or @path to file). Repeatable.', collectArg, [])
|
|
64
65
|
.option('--ligand <smiles_or_ccd>', 'Ligand as SMILES or CCD code (e.g. ATP). Repeatable.', collectArg, [])
|
|
66
|
+
.option('--glycan <iupac>', 'Carbohydrate in IUPAC-condensed notation (e.g. "Gal(b1-4)GlcNAc"); built to a SMILES ligand. Repeatable.', collectArg, [])
|
|
67
|
+
.option('--depth <tier>', 'Attach real per-chain MSAs: none (default, NIM-internal) | standard | exhaustive | custom', 'none')
|
|
65
68
|
.option('--samples <n>', 'Number of structure samples (1-25, default: 1)', (v) => parseInt(v, 10), 1)
|
|
66
69
|
.option('--recycling <n>', 'Recycling steps (1-10, default: 3)', (v) => parseInt(v, 10), 3)
|
|
67
70
|
.option('--sampling <n>', 'Diffusion sampling steps (10-1000, default: 50)', (v) => parseInt(v, 10), 50)
|
|
@@ -69,6 +72,16 @@ export function registerPredictCommands(program) {
|
|
|
69
72
|
.action(async (opts) => {
|
|
70
73
|
await runBoltz2(opts);
|
|
71
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
|
+
});
|
|
72
85
|
// m3t predict openfold3
|
|
73
86
|
predict.command('openfold3')
|
|
74
87
|
.description('Predict biomolecular complex (protein + DNA/RNA + ligand) with OpenFold3 / AlphaFold3-class (NVIDIA NIM). Real MSAs + optional PDB templates.')
|
|
@@ -76,7 +89,9 @@ export function registerPredictCommands(program) {
|
|
|
76
89
|
.option('--dna <seq>', 'DNA sequence (or @path to file). Repeatable.', collectArg, [])
|
|
77
90
|
.option('--rna <seq>', 'RNA sequence (or @path to file). Repeatable.', collectArg, [])
|
|
78
91
|
.option('--ligand <smiles_or_ccd>', 'Ligand as SMILES or CCD code (e.g. ATP). Repeatable.', collectArg, [])
|
|
79
|
-
.option('--
|
|
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.')
|
|
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')
|
|
80
95
|
.option('--templates', 'Seed the fold with PDB structural templates per protein chain (OpenFold3 self-aligns).')
|
|
81
96
|
.option('--samples <n>', 'Number of diffusion samples (1-25, default: 1)', (v) => parseInt(v, 10), 1)
|
|
82
97
|
.option('--name <name>', 'Custom job title')
|
|
@@ -101,6 +116,11 @@ async function runPredict(sequence, method, name) {
|
|
|
101
116
|
output(data, `${method} prediction created\nJob ID: ${result.job_id.substring(0, 8)}\nLength: ${sequence.length} residues\nEstimated: ${timeEst}`);
|
|
102
117
|
}
|
|
103
118
|
async function runBoltz2(opts) {
|
|
119
|
+
const depth = normalizeMsaDepth(opts.depth || 'none');
|
|
120
|
+
if (!['none', 'standard', 'exhaustive', 'custom'].includes(depth)) {
|
|
121
|
+
process.stderr.write(`Error: --depth must be none | standard | exhaustive | custom, got "${opts.depth}".\n`);
|
|
122
|
+
process.exit(1);
|
|
123
|
+
}
|
|
104
124
|
const polymers = [];
|
|
105
125
|
for (const seq of opts.protein)
|
|
106
126
|
polymers.push({ molecule_type: 'protein', sequence: resolveSequence(seq) });
|
|
@@ -113,12 +133,23 @@ async function runBoltz2(opts) {
|
|
|
113
133
|
process.exit(1);
|
|
114
134
|
}
|
|
115
135
|
const ligands = opts.ligand.map(parseLigand);
|
|
136
|
+
// Glycans → SMILES ligands (Boltz-2 has no inter-entity bond field, so a glycan
|
|
137
|
+
// rides in as one SMILES molecule).
|
|
138
|
+
for (const g of opts.glycan) {
|
|
139
|
+
const built = await buildGlycanViaMcp(g, false);
|
|
140
|
+
if (!built.smiles) {
|
|
141
|
+
process.stderr.write(`Error: could not build glycan "${g}".\n`);
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
process.stderr.write(`Glycan "${g}" → SMILES ligand (${built.formula ?? '?'})\n`);
|
|
145
|
+
ligands.push({ smiles: built.smiles });
|
|
146
|
+
}
|
|
116
147
|
const project = requireProject();
|
|
117
148
|
const client = createClient();
|
|
118
149
|
const agents = createAgentsClient();
|
|
119
150
|
const consoleUrl = getConsoleUrl();
|
|
120
151
|
const summary = describeComplex(polymers, ligands);
|
|
121
|
-
process.stderr.write(`Boltz-2: ${summary}\n`);
|
|
152
|
+
process.stderr.write(`Boltz-2: ${summary}${depth !== 'none' ? ` (MSA depth=${depth})` : ''}\n`);
|
|
122
153
|
process.stderr.write('Running prediction (NVIDIA NIM, ~1-5 min)...');
|
|
123
154
|
const startTime = Date.now();
|
|
124
155
|
const mcpData = await agents.callMcpTool('bionemo', 'predict_structure_boltz2', {
|
|
@@ -127,6 +158,7 @@ async function runBoltz2(opts) {
|
|
|
127
158
|
diffusion_samples: opts.samples,
|
|
128
159
|
recycling_steps: opts.recycling,
|
|
129
160
|
sampling_steps: opts.sampling,
|
|
161
|
+
msa_depth: depth,
|
|
130
162
|
});
|
|
131
163
|
const result = mcpData;
|
|
132
164
|
if (result.error || !result.success) {
|
|
@@ -152,8 +184,18 @@ async function runBoltz2(opts) {
|
|
|
152
184
|
quality: result.quality,
|
|
153
185
|
num_structures_returned: result.num_structures_returned,
|
|
154
186
|
diffusion_samples: result.diffusion_samples,
|
|
187
|
+
msa_depth: depth,
|
|
155
188
|
model: 'boltz2',
|
|
156
189
|
},
|
|
190
|
+
prediction_inputs: {
|
|
191
|
+
model: 'boltz2',
|
|
192
|
+
msa_depth: depth,
|
|
193
|
+
n_protein: opts.protein.length,
|
|
194
|
+
n_dna: opts.dna.length,
|
|
195
|
+
n_rna: opts.rna.length,
|
|
196
|
+
ligands: ligands.length,
|
|
197
|
+
glycans: opts.glycan,
|
|
198
|
+
},
|
|
157
199
|
});
|
|
158
200
|
const url = jobUrl(consoleUrl, project.id, job.job_id);
|
|
159
201
|
maybeOpenBrowser(url);
|
|
@@ -181,10 +223,10 @@ async function runBoltz2(opts) {
|
|
|
181
223
|
}, lines.join('\n'));
|
|
182
224
|
}
|
|
183
225
|
async function runOpenfold3(opts) {
|
|
184
|
-
const depth = (opts.depth || '
|
|
185
|
-
const OF3_DEPTHS = ['
|
|
226
|
+
const depth = normalizeMsaDepth(opts.depth || 'standard');
|
|
227
|
+
const OF3_DEPTHS = ['standard', 'exhaustive', 'custom', 'none'];
|
|
186
228
|
if (!OF3_DEPTHS.includes(depth)) {
|
|
187
|
-
process.stderr.write(`Error: --depth must be one of ${OF3_DEPTHS.join(' | ')}, got "${opts.depth}".\n`);
|
|
229
|
+
process.stderr.write(`Error: --depth must be one of ${OF3_DEPTHS.join(' | ')} (fast/deep accepted as aliases), got "${opts.depth}".\n`);
|
|
188
230
|
process.exit(1);
|
|
189
231
|
}
|
|
190
232
|
// Build the OpenFold3 molecules array (proteins get chain ids A, B, ...).
|
|
@@ -201,8 +243,44 @@ async function runOpenfold3(opts) {
|
|
|
201
243
|
const parsed = parseLigand(lig);
|
|
202
244
|
molecules.push({ type: 'ligand', id: chainIds[ci++], ...('ccd_code' in parsed ? { ccd_codes: [parsed.ccd_code] } : { smiles: parsed.smiles }) });
|
|
203
245
|
}
|
|
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.
|
|
251
|
+
const bonds = [];
|
|
252
|
+
for (const g of opts.glycan) {
|
|
253
|
+
const built = await buildGlycanViaMcp(g, false);
|
|
254
|
+
const useCcd = !!opts.glycanCcd && built.ccd_supported && !!built.residues && built.residues.length > 0;
|
|
255
|
+
if (useCcd) {
|
|
256
|
+
const idxToChain = {};
|
|
257
|
+
for (const res of built.residues) {
|
|
258
|
+
const id = chainIds[ci++];
|
|
259
|
+
idxToChain[res.index] = id;
|
|
260
|
+
molecules.push({ type: 'ligand', id, ccd_codes: [res.ccd] });
|
|
261
|
+
}
|
|
262
|
+
for (const b of built.bonds ?? []) {
|
|
263
|
+
bonds.push({
|
|
264
|
+
atom1: [idxToChain[b.child_idx], 1, b.child_atom],
|
|
265
|
+
atom2: [idxToChain[b.parent_idx], 1, b.parent_atom],
|
|
266
|
+
});
|
|
267
|
+
}
|
|
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`);
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
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`);
|
|
273
|
+
}
|
|
274
|
+
if (!built.smiles) {
|
|
275
|
+
process.stderr.write(`Error: could not build glycan "${g}".\n`);
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
molecules.push({ type: 'ligand', id: chainIds[ci++], smiles: built.smiles });
|
|
279
|
+
process.stderr.write(`Glycan "${g}" → SMILES ligand (${built.formula ?? '?'})\n`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
204
282
|
if (molecules.length === 0) {
|
|
205
|
-
process.stderr.write('Error: at least one --protein, --dna, --rna, or --
|
|
283
|
+
process.stderr.write('Error: at least one --protein, --dna, --rna, --ligand, or --glycan is required.\n');
|
|
206
284
|
process.exit(1);
|
|
207
285
|
}
|
|
208
286
|
const project = requireProject();
|
|
@@ -210,7 +288,7 @@ async function runOpenfold3(opts) {
|
|
|
210
288
|
const agents = createAgentsClient();
|
|
211
289
|
const consoleUrl = getConsoleUrl();
|
|
212
290
|
const summary = molecules.map(m => `${m.type}(${m.id})`).join(' + ');
|
|
213
|
-
process.stderr.write(`OpenFold3: ${summary} (depth=${depth}${opts.templates ? ', +templates' : ''})\n`);
|
|
291
|
+
process.stderr.write(`OpenFold3: ${summary} (depth=${depth}${opts.templates ? ', +templates' : ''}${bonds.length ? `, ${bonds.length} bonds` : ''})\n`);
|
|
214
292
|
process.stderr.write('Running prediction (NVIDIA NIM; real MSA may cold-start the engine, up to a few min)...');
|
|
215
293
|
const startTime = Date.now();
|
|
216
294
|
const mcpData = await agents.callMcpTool('bionemo', 'predict_complex_structure_openfold3', {
|
|
@@ -219,6 +297,7 @@ async function runOpenfold3(opts) {
|
|
|
219
297
|
templates: !!opts.templates,
|
|
220
298
|
diffusion_samples: opts.samples,
|
|
221
299
|
output_format: 'pdb',
|
|
300
|
+
...(bonds.length > 0 ? { bonds } : {}),
|
|
222
301
|
});
|
|
223
302
|
const result = mcpData;
|
|
224
303
|
if (result.error || !result.success) {
|
|
@@ -248,6 +327,18 @@ async function runOpenfold3(opts) {
|
|
|
248
327
|
templates_applied: !!result.templates_applied,
|
|
249
328
|
model: 'openfold3',
|
|
250
329
|
},
|
|
330
|
+
prediction_inputs: {
|
|
331
|
+
model: 'openfold3',
|
|
332
|
+
msa_depth: depth,
|
|
333
|
+
templates: !!opts.templates,
|
|
334
|
+
n_protein: opts.protein.length,
|
|
335
|
+
n_dna: opts.dna.length,
|
|
336
|
+
n_rna: opts.rna.length,
|
|
337
|
+
ligands: opts.ligand.length,
|
|
338
|
+
glycans: opts.glycan,
|
|
339
|
+
glycan_repr: opts.glycanCcd ? 'ccd+bonds' : 'smiles',
|
|
340
|
+
n_bonds: bonds.length,
|
|
341
|
+
},
|
|
251
342
|
});
|
|
252
343
|
const url = jobUrl(consoleUrl, project.id, job.job_id);
|
|
253
344
|
maybeOpenBrowser(url);
|
|
@@ -265,12 +356,65 @@ async function runOpenfold3(opts) {
|
|
|
265
356
|
lines.push(`pLDDT: ${conf.plddt}`);
|
|
266
357
|
if (opts.templates)
|
|
267
358
|
lines.push(`Templates: ${result.templates_applied ? 'applied' : 'requested but not applied (NIM dropped them)'}`);
|
|
359
|
+
if (opts.glycan.length > 0)
|
|
360
|
+
lines.push('Note: validate the glycan geometry (ring pucker / glycosidic torsions) — pLDDT is unreliable for carbohydrates.');
|
|
268
361
|
lines.push(`View: ${url}`);
|
|
269
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'));
|
|
270
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
|
+
}
|
|
271
405
|
function collectArg(value, prev) {
|
|
272
406
|
return [...prev, value];
|
|
273
407
|
}
|
|
408
|
+
// MSA depth vocabulary aligns with the MSA service (standard|exhaustive|custom|none).
|
|
409
|
+
// fast/deep are accepted as legacy aliases (fast→standard, deep→exhaustive).
|
|
410
|
+
function normalizeMsaDepth(d) {
|
|
411
|
+
const v = (d || '').toLowerCase();
|
|
412
|
+
if (v === 'fast')
|
|
413
|
+
return 'standard';
|
|
414
|
+
if (v === 'deep')
|
|
415
|
+
return 'exhaustive';
|
|
416
|
+
return v;
|
|
417
|
+
}
|
|
274
418
|
function resolveSequence(input) {
|
|
275
419
|
// @path → read file (FASTA-aware: strip header/whitespace)
|
|
276
420
|
if (input.startsWith('@')) {
|
package/dist/types.d.ts
CHANGED
|
@@ -128,6 +128,36 @@ export interface Boltz2JobParams {
|
|
|
128
128
|
title: string;
|
|
129
129
|
result_data: Record<string, unknown>;
|
|
130
130
|
description?: string;
|
|
131
|
+
prediction_inputs?: Record<string, unknown>;
|
|
132
|
+
}
|
|
133
|
+
export interface GlycanResidue {
|
|
134
|
+
index: number;
|
|
135
|
+
name: string;
|
|
136
|
+
ccd: string;
|
|
137
|
+
}
|
|
138
|
+
export interface GlycanBond {
|
|
139
|
+
child_idx: number;
|
|
140
|
+
parent_idx: number;
|
|
141
|
+
child_atom: string;
|
|
142
|
+
parent_atom: string;
|
|
143
|
+
linkage: string;
|
|
144
|
+
}
|
|
145
|
+
export interface GlycanBuildResult {
|
|
146
|
+
success?: boolean;
|
|
147
|
+
input?: string;
|
|
148
|
+
smiles?: string;
|
|
149
|
+
sdf?: string;
|
|
150
|
+
formula?: string;
|
|
151
|
+
molecular_weight?: number;
|
|
152
|
+
n_rotatable_bonds?: number;
|
|
153
|
+
n_residues?: number;
|
|
154
|
+
ccd_supported?: boolean;
|
|
155
|
+
residues?: GlycanResidue[];
|
|
156
|
+
bonds?: GlycanBond[];
|
|
157
|
+
reducing_end?: GlycanResidue;
|
|
158
|
+
attachment?: Record<string, unknown>;
|
|
159
|
+
ccd_note?: string;
|
|
160
|
+
error?: string;
|
|
131
161
|
}
|
|
132
162
|
export interface Openfold3Molecule {
|
|
133
163
|
type: 'protein' | 'dna' | 'rna' | 'ligand';
|