m3triq 0.2.6 → 0.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -24,7 +24,7 @@ const program = new Command();
24
24
  program
25
25
  .name('m3t')
26
26
  .description('M3TRIQ — protein-ligand analysis from the terminal')
27
- .version('0.2.6')
27
+ .version('0.2.8')
28
28
  .option('--json', 'Output as JSON (machine-readable)')
29
29
  .hook('preAction', (thisCommand) => {
30
30
  const opts = thisCommand.optsWithGlobals();
package/dist/client.d.ts CHANGED
@@ -39,6 +39,48 @@ export declare class M3triqClient {
39
39
  createStructurePrediction(params: StructurePredictionParams): Promise<{
40
40
  job_id: string;
41
41
  }>;
42
+ createEsmfold2Job(params: {
43
+ project_id: string;
44
+ chains: {
45
+ id: string;
46
+ sequence: string;
47
+ }[];
48
+ title?: string;
49
+ num_loops?: number;
50
+ num_sampling_steps?: number;
51
+ num_diffusion_samples?: number;
52
+ seed?: number;
53
+ }): Promise<{
54
+ job_id: string;
55
+ }>;
56
+ createEsmcEmbedJob(params: {
57
+ project_id: string;
58
+ sequences: string[];
59
+ title?: string;
60
+ return_layer?: string;
61
+ }): Promise<{
62
+ job_id: string;
63
+ }>;
64
+ createEsmfold2BatchJob(params: {
65
+ project_id: string;
66
+ inputs: {
67
+ label?: string;
68
+ chains: {
69
+ id: string;
70
+ sequence: string;
71
+ }[];
72
+ num_loops?: number;
73
+ num_sampling_steps?: number;
74
+ num_diffusion_samples?: number;
75
+ seed?: number;
76
+ }[];
77
+ title?: string;
78
+ default_num_loops?: number;
79
+ default_num_sampling_steps?: number;
80
+ default_num_diffusion_samples?: number;
81
+ }): Promise<{
82
+ job_id: string;
83
+ }>;
42
84
  createBoltz2Job(params: Boltz2JobParams): Promise<{
43
85
  job_id: string;
44
86
  }>;
package/dist/client.js CHANGED
@@ -117,6 +117,15 @@ export class M3triqClient {
117
117
  : '/api/jobs/create_esmfold_prediction/';
118
118
  return this.request('POST', endpoint, params);
119
119
  }
120
+ async createEsmfold2Job(params) {
121
+ return this.request('POST', '/api/jobs/create_esmfold2_prediction/', params);
122
+ }
123
+ async createEsmcEmbedJob(params) {
124
+ return this.request('POST', '/api/jobs/create_esmc_embed/', params);
125
+ }
126
+ async createEsmfold2BatchJob(params) {
127
+ return this.request('POST', '/api/jobs/create_esmfold2_batch/', params);
128
+ }
120
129
  async createBoltz2Job(params) {
121
130
  // Boltz-2 prediction is run client-side via the agents MCP, then saved here as a completed job.
122
131
  // Mirrors the RFantibody internal flow but with status='completed' and result_data already populated.
@@ -67,8 +67,8 @@ export function registerDesignCommands(program) {
67
67
  .argument('<target>', 'Target PDB ID (e.g., 6VXX) or path to .pdb file')
68
68
  .requiredOption('--hotspots <residues>', 'Binding hotspot residues (e.g., A100,A105,A110)')
69
69
  .option('--type <type>', 'Antibody type: nanobody or scfv', 'nanobody')
70
- .option('--designs <n>', 'Number of designs (1-10)', parseInt, 3)
71
- .option('--seqs <n>', 'Sequences per backbone (1-8)', parseInt, 1)
70
+ .option('--designs <n>', 'Number of designs (1-10)', (v) => parseInt(v, 10), 3)
71
+ .option('--seqs <n>', 'Sequences per backbone (1-8)', (v) => parseInt(v, 10), 1)
72
72
  .option('--cdr <loops>', 'CDR loop specs (e.g., H1:7,H2:6,H3:5-13)')
73
73
  .description('Design de novo antibodies with RFAntibody (Azure T4 GPU, ~3-5 min)')
74
74
  .action(async (target, opts) => {
@@ -21,6 +21,39 @@ export function registerPredictCommands(program) {
21
21
  .action(async (sequence, opts) => {
22
22
  await runPredict(sequence, 'alphafold2', opts.name);
23
23
  });
24
+ // m3t predict esmfold2 — monomer OR multi-chain (antibody-antigen, PPI)
25
+ predict.command('esmfold2')
26
+ .description('ESMFold2-Fast: monomer or multi-chain complex prediction (self-hosted A100). Beats AlphaFold3 on antibody-antigen DockQ from single sequence.')
27
+ .option('--chain <id:seq>', 'Chain in the form "id:sequence" or "id:@path". Repeatable for multi-chain complexes. Example: --chain A:MQIF... --chain B:DIQM...', collectArg, [])
28
+ .option('--sequence <seq>', 'Single-chain shortcut (auto-assigned chain id "A"). Use --chain for multi-chain.')
29
+ .option('--loops <n>', 'ESMFold2 recurrent loops, 1-64 (default 10)', (v) => parseInt(v, 10), 10)
30
+ .option('--steps <n>', 'Diffusion sampling steps, 1-200 (default 50)', (v) => parseInt(v, 10), 50)
31
+ .option('--samples <n>', 'Diffusion samples per fold, 1-25 (default 1). >1 = best-by-ipTM/pTM.', (v) => parseInt(v, 10), 1)
32
+ .option('--name <name>', 'Custom job title')
33
+ .action(async (opts) => {
34
+ await runEsmfold2(opts);
35
+ });
36
+ // m3t predict esmfold2-batch — fold a list of complexes from a JSON file
37
+ predict.command('esmfold2-batch')
38
+ .argument('<inputs.json>', 'Path to JSON file with an array of {label, chains: [{id, sequence}], optional num_loops/num_sampling_steps/num_diffusion_samples/seed}')
39
+ .option('--loops <n>', 'Default recurrent loops, 1-64 (per-input overrides)', (v) => parseInt(v, 10), 10)
40
+ .option('--steps <n>', 'Default diffusion sampling steps, 1-200', (v) => parseInt(v, 10), 50)
41
+ .option('--samples <n>', 'Default diffusion samples per fold, 1-25', (v) => parseInt(v, 10), 1)
42
+ .option('--name <name>', 'Custom job title')
43
+ .description('Batch primitive: fold N complexes in one job (results in input order — no sorting)')
44
+ .action(async (inputsPath, opts) => {
45
+ await runEsmfold2Batch(inputsPath, opts);
46
+ });
47
+ // m3t embed esmc — protein-language-model embeddings + pseudo-perplexity
48
+ const embed = program.command('embed').description('Compute protein-language-model embeddings');
49
+ embed.command('esmc')
50
+ .description('ESMC-6B per-residue embeddings + pseudo-perplexity (lower = more "natural" sequence)')
51
+ .option('--sequence <seq>', 'Single sequence (or @path to file). Repeatable.', collectArg, [])
52
+ .option('--layer <n>', 'Hidden layer to return: "last" (default) or an integer index', 'last')
53
+ .option('--name <name>', 'Custom job title')
54
+ .action(async (opts) => {
55
+ await runEsmcEmbed(opts);
56
+ });
24
57
  // m3t predict boltz2
25
58
  predict.command('boltz2')
26
59
  .description('Predict biomolecular complex (protein + DNA/RNA + ligand) with binding affinity (Boltz-2 / NVIDIA NIM, ~1-5 min)')
@@ -28,9 +61,9 @@ export function registerPredictCommands(program) {
28
61
  .option('--dna <seq>', 'DNA sequence (or @path to file). Repeatable.', collectArg, [])
29
62
  .option('--rna <seq>', 'RNA sequence (or @path to file). Repeatable.', collectArg, [])
30
63
  .option('--ligand <smiles_or_ccd>', 'Ligand as SMILES or CCD code (e.g. ATP). Repeatable.', collectArg, [])
31
- .option('--samples <n>', 'Number of structure samples (1-25, default: 1)', parseInt, 1)
32
- .option('--recycling <n>', 'Recycling steps (1-10, default: 3)', parseInt, 3)
33
- .option('--sampling <n>', 'Diffusion sampling steps (10-1000, default: 50)', parseInt, 50)
64
+ .option('--samples <n>', 'Number of structure samples (1-25, default: 1)', (v) => parseInt(v, 10), 1)
65
+ .option('--recycling <n>', 'Recycling steps (1-10, default: 3)', (v) => parseInt(v, 10), 3)
66
+ .option('--sampling <n>', 'Diffusion sampling steps (10-1000, default: 50)', (v) => parseInt(v, 10), 50)
34
67
  .option('--name <name>', 'Custom job title')
35
68
  .action(async (opts) => {
36
69
  await runBoltz2(opts);
@@ -161,6 +194,128 @@ function parseLigand(input) {
161
194
  return { ccd_code: input };
162
195
  return { smiles: input };
163
196
  }
197
+ async function runEsmfold2(opts) {
198
+ const chains = [];
199
+ for (const raw of opts.chain) {
200
+ const colon = raw.indexOf(':');
201
+ if (colon <= 0) {
202
+ process.stderr.write(`Error: --chain must be "id:sequence" or "id:@path", got "${raw}".\n`);
203
+ process.exit(1);
204
+ }
205
+ const id = raw.slice(0, colon).trim();
206
+ const seq = resolveSequence(raw.slice(colon + 1));
207
+ if (!id) {
208
+ process.stderr.write('Error: --chain id cannot be empty.\n');
209
+ process.exit(1);
210
+ }
211
+ chains.push({ id, sequence: seq });
212
+ }
213
+ if (chains.length === 0 && opts.sequence) {
214
+ chains.push({ id: 'A', sequence: resolveSequence(opts.sequence) });
215
+ }
216
+ if (chains.length === 0) {
217
+ process.stderr.write('Error: provide at least one --chain "id:seq" or --sequence "<seq>".\n');
218
+ process.exit(1);
219
+ }
220
+ const totalAa = chains.reduce((s, c) => s + c.sequence.length, 0);
221
+ if (totalAa > 2048) {
222
+ process.stderr.write(`Error: total residues ${totalAa} > 2048 (ESMFold2-Fast cap on single GPU).\n`);
223
+ process.exit(1);
224
+ }
225
+ const project = requireProject();
226
+ const client = createClient();
227
+ const consoleUrl = getConsoleUrl();
228
+ const result = await client.createEsmfold2Job({
229
+ project_id: project.id,
230
+ chains,
231
+ title: opts.name,
232
+ num_loops: opts.loops,
233
+ num_sampling_steps: opts.steps,
234
+ num_diffusion_samples: opts.samples,
235
+ });
236
+ const url = jobUrl(consoleUrl, project.id, result.job_id);
237
+ maybeOpenBrowser(url);
238
+ const isComplex = chains.length > 1;
239
+ const lines = [
240
+ `ESMFold2 ${isComplex ? `complex (${chains.length} chains)` : 'monomer'} queued`,
241
+ `Job ID: ${result.job_id.substring(0, 8)}`,
242
+ `Total residues: ${totalAa}`,
243
+ `Estimated: ~30-60s warm, ~5-7 min cold start`,
244
+ `View: ${url}`,
245
+ ];
246
+ output({ job_id: result.job_id, n_chains: chains.length, total_residues: totalAa, url }, lines.join('\n'));
247
+ }
248
+ async function runEsmfold2Batch(inputsPath, opts) {
249
+ if (!fs.existsSync(inputsPath)) {
250
+ process.stderr.write(`Error: inputs file not found: ${inputsPath}\n`);
251
+ process.exit(1);
252
+ }
253
+ let inputs;
254
+ try {
255
+ inputs = JSON.parse(fs.readFileSync(inputsPath, 'utf-8'));
256
+ }
257
+ catch (e) {
258
+ process.stderr.write(`Error: ${inputsPath} is not valid JSON (${e.message}).\n`);
259
+ process.exit(1);
260
+ }
261
+ if (!Array.isArray(inputs) || inputs.length === 0) {
262
+ process.stderr.write('Error: inputs file must contain a non-empty array of {label, chains, ...}.\n');
263
+ process.exit(1);
264
+ }
265
+ for (let i = 0; i < inputs.length; i++) {
266
+ const inp = inputs[i];
267
+ if (!inp || !Array.isArray(inp.chains) || inp.chains.length === 0) {
268
+ process.stderr.write(`Error: input #${i} missing required "chains" array.\n`);
269
+ process.exit(1);
270
+ }
271
+ }
272
+ const project = requireProject();
273
+ const client = createClient();
274
+ const consoleUrl = getConsoleUrl();
275
+ const result = await client.createEsmfold2BatchJob({
276
+ project_id: project.id,
277
+ inputs,
278
+ title: opts.name,
279
+ default_num_loops: opts.loops,
280
+ default_num_sampling_steps: opts.steps,
281
+ default_num_diffusion_samples: opts.samples,
282
+ });
283
+ const url = jobUrl(consoleUrl, project.id, result.job_id);
284
+ maybeOpenBrowser(url);
285
+ const lines = [
286
+ `ESMFold2 batch queued (${inputs.length} inputs)`,
287
+ `Job ID: ${result.job_id.substring(0, 8)}`,
288
+ `Defaults: loops=${opts.loops}, steps=${opts.steps}, samples=${opts.samples}`,
289
+ `Results returned in input order — no sorting applied.`,
290
+ `View: ${url}`,
291
+ ];
292
+ output({ job_id: result.job_id, n_inputs: inputs.length, url }, lines.join('\n'));
293
+ }
294
+ async function runEsmcEmbed(opts) {
295
+ if (opts.sequence.length === 0) {
296
+ process.stderr.write('Error: at least one --sequence is required.\n');
297
+ process.exit(1);
298
+ }
299
+ const sequences = opts.sequence.map(resolveSequence);
300
+ const project = requireProject();
301
+ const client = createClient();
302
+ const consoleUrl = getConsoleUrl();
303
+ const result = await client.createEsmcEmbedJob({
304
+ project_id: project.id,
305
+ sequences,
306
+ return_layer: opts.layer,
307
+ title: opts.name,
308
+ });
309
+ const url = jobUrl(consoleUrl, project.id, result.job_id);
310
+ maybeOpenBrowser(url);
311
+ const lines = [
312
+ `ESMC embedding job queued (${sequences.length} sequence${sequences.length > 1 ? 's' : ''})`,
313
+ `Job ID: ${result.job_id.substring(0, 8)}`,
314
+ `Layer: ${opts.layer}`,
315
+ `View: ${url}`,
316
+ ];
317
+ output({ job_id: result.job_id, n_sequences: sequences.length, url }, lines.join('\n'));
318
+ }
164
319
  function describeComplex(polymers, ligands) {
165
320
  const parts = polymers.map(p => `${p.molecule_type}(${p.sequence.length})`);
166
321
  if (ligands.length > 0) {
package/package.json CHANGED
@@ -1,13 +1,21 @@
1
1
  {
2
2
  "name": "m3triq",
3
- "version": "0.2.6",
4
- "description": "M3TRIQ protein-ligand analysis from the terminal",
3
+ "version": "0.2.8",
4
+ "description": "M3TRIQ \u2014 protein-ligand analysis from the terminal",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "m3t": "./dist/cli.js"
8
8
  },
9
9
  "main": "dist/cli.js",
10
- "keywords": ["bioinformatics", "chembl", "docking", "protein", "ligand", "drug-discovery", "cli"],
10
+ "keywords": [
11
+ "bioinformatics",
12
+ "chembl",
13
+ "docking",
14
+ "protein",
15
+ "ligand",
16
+ "drug-discovery",
17
+ "cli"
18
+ ],
11
19
  "repository": {
12
20
  "type": "git",
13
21
  "url": "https://github.com/M3TRIQ/m3t.git"
@@ -28,6 +36,8 @@
28
36
  "@types/node": "^22.0.0",
29
37
  "typescript": "^5.7.0"
30
38
  },
31
- "files": ["dist"],
39
+ "files": [
40
+ "dist"
41
+ ],
32
42
  "license": "MIT"
33
43
  }