m3triq 0.2.15 → 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 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 # CCD + bonds (default)
120
- m3t predict openfold3 --protein MKFL... --glycan "Gal(b1-4)GlcNAc" --glycan-smiles # force one SMILES blob
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` on **OpenFold3** defaults to the **per-residue CCD + glycosidic-bond**
127
- (`bondedAtomPairs`) representation the AF3-glycan literature finds a single SMILES blob
128
- "frequently fails to reproduce correct conformations even for simple oligosaccharides".
129
- Falls back to SMILES if the glycan can't be decomposed; `--glycan-smiles` forces SMILES.
130
- Best-effort: if the hosted model rejects explicit bonds the fold retries as separate
131
- ligand entities.
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.15')
29
+ .version('0.2.18')
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
- constructor(baseUrl: string);
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
- constructor(baseUrl) {
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: { 'Content-Type': 'application/json' },
249
+ headers,
240
250
  body: JSON.stringify({ server, tool, params }),
241
251
  });
242
252
  if (!res.ok) {
@@ -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 CCD + bonds)');
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 (falls back to SMILES ligand)');
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;
@@ -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);
@@ -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 a proper multi-residue sugar chain (per-residue CCD codes + glycosidic bonds, the AF3-recommended representation). Repeatable.', collectArg, [])
83
- .option('--glycan-smiles', 'Attach each glycan as a single SMILES blob instead of the default per-residue CCD + bonds. Less accurate for conformation; use only if the CCD path misbehaves.')
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: by default a proper multi-residue chain (one single-CCD ligand per sugar
237
- // + glycosidic bonds); --glycan-smiles forces a single SMILES ligand instead.
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
- // DEFAULT to the per-residue CCD + glycosidic-bond representation: the AF3 glycan
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)\n`);
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 (!opts.glycanSmiles && !built.ccd_supported) {
264
- process.stderr.write(`Glycan "${g}": CCD decomposition unavailable (${built.ccd_note ?? 'unsupported monosaccharide/topology'}); falling back to a single SMILES ligand.\n`);
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.glycanSmiles ? 'smiles' : 'ccd+bonds',
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "m3triq",
3
- "version": "0.2.15",
3
+ "version": "0.2.18",
4
4
  "description": "M3TRIQ \u2014 protein-ligand analysis from the terminal",
5
5
  "type": "module",
6
6
  "bin": {