m3triq 0.2.13 → 0.2.15
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 +59 -1
- package/dist/cli.js +3 -1
- package/dist/client.d.ts +1 -1
- package/dist/client.js +2 -0
- package/dist/commands/glycan.d.ts +4 -0
- package/dist/commands/glycan.js +73 -0
- package/dist/commands/msa.js +14 -15
- package/dist/commands/predict.js +102 -7
- package/dist/types.d.ts +30 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -85,7 +85,7 @@ m3t md results <job-id>
|
|
|
85
85
|
|
|
86
86
|
```bash
|
|
87
87
|
m3t predict esmfold MKFLILLFNILCL... # Fast (~10s, max 1024aa)
|
|
88
|
-
m3t predict alphafold2 MKFLILLFNILCL... #
|
|
88
|
+
m3t predict alphafold2 MKFLILLFNILCL... # AF2-ptm monomer via deep MSA + ColabFold (~5-15min)
|
|
89
89
|
|
|
90
90
|
# ESMFold2-Fast — monomer or multi-chain complex (self-hosted A100, max 2048aa total).
|
|
91
91
|
# Beats AlphaFold3 on antibody-antigen DockQ from single sequence; returns pLDDT/pTM/ipTM.
|
|
@@ -95,8 +95,66 @@ m3t predict esmfold2-batch inputs.json # fold N complexes
|
|
|
95
95
|
|
|
96
96
|
# Boltz-2 — biomolecular complex (protein + DNA/RNA + ligand) with binding affinity
|
|
97
97
|
m3t predict boltz2 --protein MKFL... --ligand "CC(=O)O" --rna GGUC...
|
|
98
|
+
m3t predict boltz2 --protein MKFL... --ligand "CC(=O)O" --depth exhaustive # + real per-chain MSAs
|
|
99
|
+
|
|
100
|
+
# OpenFold3 — AlphaFold3-class complex (protein + DNA/RNA + ligand); real MSAs
|
|
101
|
+
m3t predict openfold3 --protein EVQL... --protein DIQM... # complex, auto-paired
|
|
102
|
+
m3t predict openfold3 --protein MKFL... --ligand ATP --depth exhaustive
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Carbohydrate / Glycan Co-folding
|
|
106
|
+
|
|
107
|
+
Protein–carbohydrate interactions (e.g. lectins, phage receptor-binding proteins,
|
|
108
|
+
glycan-recognizing binders) are poorly served by classical docking — Vina/GNINA
|
|
109
|
+
score sugars badly. The reliable route is to **define** the glycan, then **co-fold**
|
|
110
|
+
it with the protein in an AlphaFold3-class model.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Define a glycan: IUPAC-condensed → SMILES + 3D SDF + per-residue CCD/bond decomposition
|
|
114
|
+
m3t glycan build "Gal(b1-4)GlcNAc"
|
|
115
|
+
m3t glycan build "Neu5Ac(a2-3)Gal(b1-4)Glc" --out sialyllactose.sdf --attach N-linked
|
|
116
|
+
|
|
117
|
+
# Co-fold a protein with a glycan
|
|
118
|
+
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
|
|
121
|
+
m3t predict boltz2 --protein MKFL... --glycan "Gal(b1-4)GlcNAc" --depth exhaustive
|
|
98
122
|
```
|
|
99
123
|
|
|
124
|
+
- **IUPAC-condensed**: reducing end on the **right**, linkages in `()`, branches in `[]`
|
|
125
|
+
(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.
|
|
132
|
+
- `--glycan` on **Boltz-2** rides as a single SMILES ligand (Boltz-2 has no inter-entity
|
|
133
|
+
bond field).
|
|
134
|
+
- **pLDDT is unreliable for carbohydrates** — high confidence can accompany wrong
|
|
135
|
+
stereochemistry. Validate ring pucker / glycosidic torsions on the output.
|
|
136
|
+
- `--depth exhaustive` (metagenomic envDB) is recommended for orphan / phage / bacterial
|
|
137
|
+
families where standard databases come back sparse.
|
|
138
|
+
|
|
139
|
+
## MSA Generation
|
|
140
|
+
|
|
141
|
+
Generate a multiple-sequence alignment (`.a3m`) — the evolutionary input folders use.
|
|
142
|
+
Two orthogonal knobs: **`--depth`** = coverage (databases), **`--pair`** = species
|
|
143
|
+
pairing (only matters for multi-chain complexes; on by default, a monomer is never paired).
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
m3t msa --sequence MKFL... # standard, monomer
|
|
147
|
+
m3t msa --chain A:EVQL... --chain B:DIQM... # standard complex, auto-paired
|
|
148
|
+
m3t msa --chain A:EVQL... --chain B:DIQM... --no-pair # complex, force unpaired
|
|
149
|
+
m3t msa --sequence MKFL... --depth exhaustive --templates # + metagenomic envDB + PDB templates
|
|
150
|
+
m3t msa --sequence MKFL... --depth custom \
|
|
151
|
+
--databases uniref30,envdb --max-sequences 300 # fine control (max_sequences caps depth)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
- `--depth`: `standard` (UniRef30, default) | `exhaustive` (+ metagenomic envDB) | `custom`
|
|
155
|
+
- `--templates`: also save per-chain PDB structural templates (`templates_<chain>.json`)
|
|
156
|
+
- Output: one `.a3m` per chain in the project's Files tab; repeat sequences are cached (instant)
|
|
157
|
+
|
|
100
158
|
## Protein Embeddings
|
|
101
159
|
|
|
102
160
|
```bash
|
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.15')
|
|
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);
|
package/dist/client.d.ts
CHANGED
package/dist/client.js
CHANGED
|
@@ -166,6 +166,7 @@ export class M3triqClient {
|
|
|
166
166
|
title: params.title,
|
|
167
167
|
description: params.description ?? '',
|
|
168
168
|
result_data: params.result_data,
|
|
169
|
+
...(params.prediction_inputs ? { prediction_inputs: params.prediction_inputs } : {}),
|
|
169
170
|
}),
|
|
170
171
|
});
|
|
171
172
|
if (!res.ok) {
|
|
@@ -187,6 +188,7 @@ export class M3triqClient {
|
|
|
187
188
|
title: params.title,
|
|
188
189
|
description: params.description ?? '',
|
|
189
190
|
result_data: params.result_data,
|
|
191
|
+
...(params.prediction_inputs ? { prediction_inputs: params.prediction_inputs } : {}),
|
|
190
192
|
}),
|
|
191
193
|
});
|
|
192
194
|
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 CCD + bonds)');
|
|
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 (falls back to 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/msa.js
CHANGED
|
@@ -4,32 +4,33 @@ import { requireProject } from '../config.js';
|
|
|
4
4
|
import { output } from '../output.js';
|
|
5
5
|
import { jobUrl, maybeOpenBrowser } from '../url.js';
|
|
6
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
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
7
|
+
// all on the self-hosted ColabFold MSA-search engine (GPU-MMseqs2). Two independent
|
|
8
|
+
// knobs:
|
|
9
|
+
// --depth = COVERAGE (which databases): standard (UniRef30) | exhaustive (+envDB,
|
|
10
|
+
// for remote-homology/orphan families) | custom (pick databases)
|
|
11
|
+
// --pair = species PAIRING, only meaningful for a multi-chain complex (on by
|
|
12
|
+
// default; a monomer is never paired). --no-pair to force unpaired.
|
|
12
13
|
// --templates also searches the PDB and saves per-chain structural templates.
|
|
13
14
|
// Produces a downloadable .a3m artifact per chain (+ templates_<chain>.json).
|
|
14
|
-
const MSA_DEPTHS = ['
|
|
15
|
+
const MSA_DEPTHS = ['standard', 'exhaustive', 'custom'];
|
|
15
16
|
export function registerMsaCommands(program) {
|
|
16
17
|
program.command('msa')
|
|
17
18
|
.description('Generate a multiple-sequence alignment (a3m) — per-chain and (for complexes) species-paired')
|
|
18
19
|
.option('--chain <id:seq>', 'Chain as "id:sequence" or "id:@path". Repeatable for multi-chain (paired) MSAs.', collectArg, [])
|
|
19
20
|
.option('--sequence <seq>', 'Single-chain shortcut (chain id "A"). Use --chain for multi-chain.')
|
|
20
|
-
.option('--depth <
|
|
21
|
+
.option('--depth <coverage>', 'Coverage / databases: standard (UniRef30) | exhaustive (+ envDB) | custom', 'standard')
|
|
21
22
|
.option('--templates', 'Also search the PDB for structural templates; saves a templates_<chain>.json per chain.')
|
|
22
23
|
.option('--databases <csv>', 'For --depth custom: comma-separated database ids (e.g. "uniref30,envdb"). See get_msa_capabilities.')
|
|
23
24
|
.option('--max-sequences <n>', 'For --depth custom: alignment depth/breadth cap.', (v) => parseInt(v, 10))
|
|
24
|
-
.option('--pair', '
|
|
25
|
-
.option('--no-pair', '
|
|
25
|
+
.option('--pair', 'Species-paired MSA — only matters for a multi-chain complex (on by default; a monomer is never paired).')
|
|
26
|
+
.option('--no-pair', 'Force unpaired (per-chain only), even for a complex.')
|
|
26
27
|
.option('--name <name>', 'Custom job title')
|
|
27
28
|
.action(async (opts) => {
|
|
28
29
|
await runMsa(opts);
|
|
29
30
|
});
|
|
30
31
|
}
|
|
31
32
|
async function runMsa(opts) {
|
|
32
|
-
const depth = (opts.depth || '
|
|
33
|
+
const depth = (opts.depth || 'standard').toLowerCase();
|
|
33
34
|
if (!MSA_DEPTHS.includes(depth)) {
|
|
34
35
|
process.stderr.write(`Error: --depth must be one of ${MSA_DEPTHS.join(' | ')}, got "${opts.depth}".\n`);
|
|
35
36
|
process.exit(1);
|
|
@@ -85,11 +86,9 @@ async function runMsa(opts) {
|
|
|
85
86
|
const url = jobUrl(consoleUrl, project.id, result.job_id);
|
|
86
87
|
maybeOpenBrowser(url);
|
|
87
88
|
const timeEst = depth === 'exhaustive'
|
|
88
|
-
?
|
|
89
|
-
:
|
|
90
|
-
|
|
91
|
-
: '~3-5 min (ColabFold UniRef30 + paired; +cold start if scaled to zero)';
|
|
92
|
-
const extras = [pair ? 'paired' : '', templates ? '+templates' : ''].filter(Boolean).join(', ');
|
|
89
|
+
? `~4-6 min (UniRef30 + envDB on 2 T4s${pair ? ' + paired' : ''}; +cold start if scaled to zero)`
|
|
90
|
+
: `~3-5 min (UniRef30${pair ? ' + paired' : ''}; +cold start if scaled to zero)`;
|
|
91
|
+
const extras = [pair ? 'paired' : 'unpaired', templates ? '+templates' : ''].filter(Boolean).join(', ');
|
|
93
92
|
const lines = [
|
|
94
93
|
`MSA job queued (${chains.length} chain${chains.length > 1 ? 's' : ''}, depth=${depth}${extras ? `, ${extras}` : ''})`,
|
|
95
94
|
`Job ID: ${result.job_id.substring(0, 8)}`,
|
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)
|
|
@@ -76,7 +79,9 @@ export function registerPredictCommands(program) {
|
|
|
76
79
|
.option('--dna <seq>', 'DNA sequence (or @path to file). Repeatable.', collectArg, [])
|
|
77
80
|
.option('--rna <seq>', 'RNA sequence (or @path to file). Repeatable.', collectArg, [])
|
|
78
81
|
.option('--ligand <smiles_or_ccd>', 'Ligand as SMILES or CCD code (e.g. ATP). Repeatable.', collectArg, [])
|
|
79
|
-
.option('--
|
|
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.')
|
|
84
|
+
.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
85
|
.option('--templates', 'Seed the fold with PDB structural templates per protein chain (OpenFold3 self-aligns).')
|
|
81
86
|
.option('--samples <n>', 'Number of diffusion samples (1-25, default: 1)', (v) => parseInt(v, 10), 1)
|
|
82
87
|
.option('--name <name>', 'Custom job title')
|
|
@@ -101,6 +106,11 @@ async function runPredict(sequence, method, name) {
|
|
|
101
106
|
output(data, `${method} prediction created\nJob ID: ${result.job_id.substring(0, 8)}\nLength: ${sequence.length} residues\nEstimated: ${timeEst}`);
|
|
102
107
|
}
|
|
103
108
|
async function runBoltz2(opts) {
|
|
109
|
+
const depth = normalizeMsaDepth(opts.depth || 'none');
|
|
110
|
+
if (!['none', 'standard', 'exhaustive', 'custom'].includes(depth)) {
|
|
111
|
+
process.stderr.write(`Error: --depth must be none | standard | exhaustive | custom, got "${opts.depth}".\n`);
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
104
114
|
const polymers = [];
|
|
105
115
|
for (const seq of opts.protein)
|
|
106
116
|
polymers.push({ molecule_type: 'protein', sequence: resolveSequence(seq) });
|
|
@@ -113,12 +123,23 @@ async function runBoltz2(opts) {
|
|
|
113
123
|
process.exit(1);
|
|
114
124
|
}
|
|
115
125
|
const ligands = opts.ligand.map(parseLigand);
|
|
126
|
+
// Glycans → SMILES ligands (Boltz-2 has no inter-entity bond field, so a glycan
|
|
127
|
+
// rides in as one SMILES molecule).
|
|
128
|
+
for (const g of opts.glycan) {
|
|
129
|
+
const built = await buildGlycanViaMcp(g, false);
|
|
130
|
+
if (!built.smiles) {
|
|
131
|
+
process.stderr.write(`Error: could not build glycan "${g}".\n`);
|
|
132
|
+
process.exit(1);
|
|
133
|
+
}
|
|
134
|
+
process.stderr.write(`Glycan "${g}" → SMILES ligand (${built.formula ?? '?'})\n`);
|
|
135
|
+
ligands.push({ smiles: built.smiles });
|
|
136
|
+
}
|
|
116
137
|
const project = requireProject();
|
|
117
138
|
const client = createClient();
|
|
118
139
|
const agents = createAgentsClient();
|
|
119
140
|
const consoleUrl = getConsoleUrl();
|
|
120
141
|
const summary = describeComplex(polymers, ligands);
|
|
121
|
-
process.stderr.write(`Boltz-2: ${summary}\n`);
|
|
142
|
+
process.stderr.write(`Boltz-2: ${summary}${depth !== 'none' ? ` (MSA depth=${depth})` : ''}\n`);
|
|
122
143
|
process.stderr.write('Running prediction (NVIDIA NIM, ~1-5 min)...');
|
|
123
144
|
const startTime = Date.now();
|
|
124
145
|
const mcpData = await agents.callMcpTool('bionemo', 'predict_structure_boltz2', {
|
|
@@ -127,6 +148,7 @@ async function runBoltz2(opts) {
|
|
|
127
148
|
diffusion_samples: opts.samples,
|
|
128
149
|
recycling_steps: opts.recycling,
|
|
129
150
|
sampling_steps: opts.sampling,
|
|
151
|
+
msa_depth: depth,
|
|
130
152
|
});
|
|
131
153
|
const result = mcpData;
|
|
132
154
|
if (result.error || !result.success) {
|
|
@@ -152,8 +174,18 @@ async function runBoltz2(opts) {
|
|
|
152
174
|
quality: result.quality,
|
|
153
175
|
num_structures_returned: result.num_structures_returned,
|
|
154
176
|
diffusion_samples: result.diffusion_samples,
|
|
177
|
+
msa_depth: depth,
|
|
155
178
|
model: 'boltz2',
|
|
156
179
|
},
|
|
180
|
+
prediction_inputs: {
|
|
181
|
+
model: 'boltz2',
|
|
182
|
+
msa_depth: depth,
|
|
183
|
+
n_protein: opts.protein.length,
|
|
184
|
+
n_dna: opts.dna.length,
|
|
185
|
+
n_rna: opts.rna.length,
|
|
186
|
+
ligands: ligands.length,
|
|
187
|
+
glycans: opts.glycan,
|
|
188
|
+
},
|
|
157
189
|
});
|
|
158
190
|
const url = jobUrl(consoleUrl, project.id, job.job_id);
|
|
159
191
|
maybeOpenBrowser(url);
|
|
@@ -181,10 +213,10 @@ async function runBoltz2(opts) {
|
|
|
181
213
|
}, lines.join('\n'));
|
|
182
214
|
}
|
|
183
215
|
async function runOpenfold3(opts) {
|
|
184
|
-
const depth = (opts.depth || '
|
|
185
|
-
const OF3_DEPTHS = ['
|
|
216
|
+
const depth = normalizeMsaDepth(opts.depth || 'standard');
|
|
217
|
+
const OF3_DEPTHS = ['standard', 'exhaustive', 'custom', 'none'];
|
|
186
218
|
if (!OF3_DEPTHS.includes(depth)) {
|
|
187
|
-
process.stderr.write(`Error: --depth must be one of ${OF3_DEPTHS.join(' | ')}, got "${opts.depth}".\n`);
|
|
219
|
+
process.stderr.write(`Error: --depth must be one of ${OF3_DEPTHS.join(' | ')} (fast/deep accepted as aliases), got "${opts.depth}".\n`);
|
|
188
220
|
process.exit(1);
|
|
189
221
|
}
|
|
190
222
|
// Build the OpenFold3 molecules array (proteins get chain ids A, B, ...).
|
|
@@ -201,8 +233,46 @@ async function runOpenfold3(opts) {
|
|
|
201
233
|
const parsed = parseLigand(lig);
|
|
202
234
|
molecules.push({ type: 'ligand', id: chainIds[ci++], ...('ccd_code' in parsed ? { ccd_codes: [parsed.ccd_code] } : { smiles: parsed.smiles }) });
|
|
203
235
|
}
|
|
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.
|
|
238
|
+
const bonds = [];
|
|
239
|
+
for (const g of opts.glycan) {
|
|
240
|
+
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;
|
|
247
|
+
if (useCcd) {
|
|
248
|
+
const idxToChain = {};
|
|
249
|
+
for (const res of built.residues) {
|
|
250
|
+
const id = chainIds[ci++];
|
|
251
|
+
idxToChain[res.index] = id;
|
|
252
|
+
molecules.push({ type: 'ligand', id, ccd_codes: [res.ccd] });
|
|
253
|
+
}
|
|
254
|
+
for (const b of built.bonds ?? []) {
|
|
255
|
+
bonds.push({
|
|
256
|
+
atom1: [idxToChain[b.child_idx], 1, b.child_atom],
|
|
257
|
+
atom2: [idxToChain[b.parent_idx], 1, b.parent_atom],
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
process.stderr.write(`Glycan "${g}" → ${built.residues.length} sugar residues + ${built.bonds?.length ?? 0} glycosidic bonds (CCD + bondedAtomPairs)\n`);
|
|
261
|
+
}
|
|
262
|
+
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`);
|
|
265
|
+
}
|
|
266
|
+
if (!built.smiles) {
|
|
267
|
+
process.stderr.write(`Error: could not build glycan "${g}".\n`);
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
molecules.push({ type: 'ligand', id: chainIds[ci++], smiles: built.smiles });
|
|
271
|
+
process.stderr.write(`Glycan "${g}" → SMILES ligand (${built.formula ?? '?'})\n`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
204
274
|
if (molecules.length === 0) {
|
|
205
|
-
process.stderr.write('Error: at least one --protein, --dna, --rna, or --
|
|
275
|
+
process.stderr.write('Error: at least one --protein, --dna, --rna, --ligand, or --glycan is required.\n');
|
|
206
276
|
process.exit(1);
|
|
207
277
|
}
|
|
208
278
|
const project = requireProject();
|
|
@@ -210,7 +280,7 @@ async function runOpenfold3(opts) {
|
|
|
210
280
|
const agents = createAgentsClient();
|
|
211
281
|
const consoleUrl = getConsoleUrl();
|
|
212
282
|
const summary = molecules.map(m => `${m.type}(${m.id})`).join(' + ');
|
|
213
|
-
process.stderr.write(`OpenFold3: ${summary} (depth=${depth}${opts.templates ? ', +templates' : ''})\n`);
|
|
283
|
+
process.stderr.write(`OpenFold3: ${summary} (depth=${depth}${opts.templates ? ', +templates' : ''}${bonds.length ? `, ${bonds.length} bonds` : ''})\n`);
|
|
214
284
|
process.stderr.write('Running prediction (NVIDIA NIM; real MSA may cold-start the engine, up to a few min)...');
|
|
215
285
|
const startTime = Date.now();
|
|
216
286
|
const mcpData = await agents.callMcpTool('bionemo', 'predict_complex_structure_openfold3', {
|
|
@@ -219,6 +289,7 @@ async function runOpenfold3(opts) {
|
|
|
219
289
|
templates: !!opts.templates,
|
|
220
290
|
diffusion_samples: opts.samples,
|
|
221
291
|
output_format: 'pdb',
|
|
292
|
+
...(bonds.length > 0 ? { bonds } : {}),
|
|
222
293
|
});
|
|
223
294
|
const result = mcpData;
|
|
224
295
|
if (result.error || !result.success) {
|
|
@@ -248,6 +319,18 @@ async function runOpenfold3(opts) {
|
|
|
248
319
|
templates_applied: !!result.templates_applied,
|
|
249
320
|
model: 'openfold3',
|
|
250
321
|
},
|
|
322
|
+
prediction_inputs: {
|
|
323
|
+
model: 'openfold3',
|
|
324
|
+
msa_depth: depth,
|
|
325
|
+
templates: !!opts.templates,
|
|
326
|
+
n_protein: opts.protein.length,
|
|
327
|
+
n_dna: opts.dna.length,
|
|
328
|
+
n_rna: opts.rna.length,
|
|
329
|
+
ligands: opts.ligand.length,
|
|
330
|
+
glycans: opts.glycan,
|
|
331
|
+
glycan_repr: opts.glycanSmiles ? 'smiles' : 'ccd+bonds',
|
|
332
|
+
n_bonds: bonds.length,
|
|
333
|
+
},
|
|
251
334
|
});
|
|
252
335
|
const url = jobUrl(consoleUrl, project.id, job.job_id);
|
|
253
336
|
maybeOpenBrowser(url);
|
|
@@ -265,12 +348,24 @@ async function runOpenfold3(opts) {
|
|
|
265
348
|
lines.push(`pLDDT: ${conf.plddt}`);
|
|
266
349
|
if (opts.templates)
|
|
267
350
|
lines.push(`Templates: ${result.templates_applied ? 'applied' : 'requested but not applied (NIM dropped them)'}`);
|
|
351
|
+
if (opts.glycan.length > 0)
|
|
352
|
+
lines.push('Note: validate the glycan geometry (ring pucker / glycosidic torsions) — pLDDT is unreliable for carbohydrates.');
|
|
268
353
|
lines.push(`View: ${url}`);
|
|
269
354
|
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
355
|
}
|
|
271
356
|
function collectArg(value, prev) {
|
|
272
357
|
return [...prev, value];
|
|
273
358
|
}
|
|
359
|
+
// MSA depth vocabulary aligns with the MSA service (standard|exhaustive|custom|none).
|
|
360
|
+
// fast/deep are accepted as legacy aliases (fast→standard, deep→exhaustive).
|
|
361
|
+
function normalizeMsaDepth(d) {
|
|
362
|
+
const v = (d || '').toLowerCase();
|
|
363
|
+
if (v === 'fast')
|
|
364
|
+
return 'standard';
|
|
365
|
+
if (v === 'deep')
|
|
366
|
+
return 'exhaustive';
|
|
367
|
+
return v;
|
|
368
|
+
}
|
|
274
369
|
function resolveSequence(input) {
|
|
275
370
|
// @path → read file (FASTA-aware: strip header/whitespace)
|
|
276
371
|
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';
|