@moldesk/adapters 0.0.0-stage → 0.0.3
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/LICENSE +21 -0
- package/dist/bindcraft2/index.d.ts +2 -0
- package/dist/bindcraft2/index.js +348 -0
- package/dist/bindcraft2/index.test.d.ts +1 -0
- package/dist/bindcraft2/index.test.js +232 -0
- package/dist/boltz/index.d.ts +2 -0
- package/dist/boltz/index.js +285 -0
- package/dist/boltz/index.test.d.ts +1 -0
- package/dist/boltz/index.test.js +368 -0
- package/dist/catalog.d.ts +6 -0
- package/dist/catalog.js +37 -0
- package/dist/catalog.test.d.ts +1 -0
- package/dist/catalog.test.js +33 -0
- package/dist/diffdock/index.d.ts +2 -0
- package/dist/diffdock/index.js +470 -0
- package/dist/diffdock/index.test.d.ts +1 -0
- package/dist/diffdock/index.test.js +595 -0
- package/dist/index.d.ts +121 -0
- package/dist/index.js +2 -0
- package/dist/ligandmpnn/index.d.ts +2 -0
- package/dist/ligandmpnn/index.js +80 -0
- package/dist/ligandmpnn/index.test.d.ts +1 -0
- package/dist/ligandmpnn/index.test.js +85 -0
- package/dist/opendde/index.d.ts +2 -0
- package/dist/opendde/index.js +551 -0
- package/dist/opendde/index.test.d.ts +1 -0
- package/dist/opendde/index.test.js +577 -0
- package/dist/outputs.d.ts +8 -0
- package/dist/outputs.js +79 -0
- package/dist/outputs.test.d.ts +1 -0
- package/dist/outputs.test.js +69 -0
- package/dist/params.d.ts +13 -0
- package/dist/params.js +69 -0
- package/dist/params.test.d.ts +1 -0
- package/dist/params.test.js +41 -0
- package/dist/planned.d.ts +1 -0
- package/dist/planned.js +9 -0
- package/dist/proteinmpnn/index.d.ts +2 -0
- package/dist/proteinmpnn/index.js +84 -0
- package/dist/proteinmpnn/index.test.d.ts +1 -0
- package/dist/proteinmpnn/index.test.js +101 -0
- package/package.json +32 -3
- package/README.md +0 -3
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { MoldeskError } from "@moldesk/registry";
|
|
4
|
+
import { runCommand } from "@moldesk/runtime";
|
|
5
|
+
import { parse as parseYaml, YAMLParseError } from "yaml";
|
|
6
|
+
import { collectGlobOutputs } from "../outputs.js";
|
|
7
|
+
/**
|
|
8
|
+
* Boltz's CLI writes `<out_dir>/boltz_results_<input_stem>/predictions/<record_id>/…`
|
|
9
|
+
* (verified against both pinned source revisions' `src/boltz/main.py` and
|
|
10
|
+
* `src/boltz/data/write/writer.py`). The matcher in `outputs.ts` is segment-wise
|
|
11
|
+
* (`*` per path component, no `**`), so these globs are written as four precise
|
|
12
|
+
* single-segment wildcards rather than a recursive pattern.
|
|
13
|
+
*/
|
|
14
|
+
const OUTPUTS = [
|
|
15
|
+
{ id: "structures", glob: "boltz_results_*/predictions/*/*.cif", required: true },
|
|
16
|
+
{ id: "confidence", glob: "boltz_results_*/predictions/*/confidence_*.json", required: true },
|
|
17
|
+
// Only produced when an affinity checkpoint/config is used; not every job requests it.
|
|
18
|
+
{ id: "affinity", glob: "boltz_results_*/predictions/*/affinity_*.json", required: false },
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* Params verified directly against the pinned CLI's `@click.option` declarations:
|
|
22
|
+
* - jwohlwend/boltz @ cb04aeccdd480fd4db707f0bbafde538397fa2ac, src/boltz/main.py
|
|
23
|
+
* - Novel-Therapeutics/boltz-community @ 401f18150cc7184b7e1bb37239a25035ef6b3a57, src/boltz/main.py
|
|
24
|
+
*
|
|
25
|
+
* `--accelerator` choices differ between the two pinned CLIs: official boltz only
|
|
26
|
+
* accepts `gpu|cpu|tpu`; boltz-community additionally accepts `mps`. Both pinned
|
|
27
|
+
* revisions define the remaining flags below identically. No static `default` is
|
|
28
|
+
* declared for `accelerator` — see `resolveParamDefaults`.
|
|
29
|
+
*/
|
|
30
|
+
const PARAMS = [
|
|
31
|
+
{ name: "accelerator", type: "string", enum: ["gpu", "mps", "cpu"], description: "Accelerator to use for prediction (platform-dependent default; see resolveParamDefaults)" },
|
|
32
|
+
{ name: "devices", type: "number", default: 1, min: 1, description: "Number of devices to use for prediction" },
|
|
33
|
+
{ name: "recycling_steps", type: "number", min: 0, description: "Number of recycling steps" },
|
|
34
|
+
{ name: "sampling_steps", type: "number", min: 1, description: "Number of diffusion sampling steps" },
|
|
35
|
+
{ name: "diffusion_samples", type: "number", min: 1, description: "Number of diffusion samples" },
|
|
36
|
+
{ name: "max_parallel_samples", type: "number", min: 1, description: "Maximum samples to predict in parallel" },
|
|
37
|
+
{ name: "use_msa_server", type: "boolean", description: "Use the MMSeqs2 server for MSA generation" },
|
|
38
|
+
{ name: "no_kernels", type: "boolean", description: "Disable custom kernels" },
|
|
39
|
+
{ name: "seed", type: "number", description: "Random seed (omit for no seeding)" },
|
|
40
|
+
];
|
|
41
|
+
/** Resolves the venv's `boltz` console-script entry point, installed by `uv pip install`
|
|
42
|
+
* from the `[project.scripts]` `boltz = "boltz.main:cli"` entry both pinned distributions declare. */
|
|
43
|
+
function boltzExecutable(runtimeExecutable) {
|
|
44
|
+
return path.join(path.dirname(runtimeExecutable), "boltz");
|
|
45
|
+
}
|
|
46
|
+
function darwinAcceleratorError(requested) {
|
|
47
|
+
return new MoldeskError({
|
|
48
|
+
code: "INVALID_RUN_PARAMS",
|
|
49
|
+
message: `--param accelerator=${requested} is not valid for this installation: the darwin-arm64 install uses boltz-community without CUDA support.`,
|
|
50
|
+
remediation: `Use --param accelerator=mps (or omit --param accelerator to use the platform default) on darwin-arm64, or "cpu".`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
function mpsOffDarwinError() {
|
|
54
|
+
return new MoldeskError({
|
|
55
|
+
code: "INVALID_RUN_PARAMS",
|
|
56
|
+
message: "--param accelerator=mps requires the darwin-arm64 (boltz-community) installation; the official boltz CLI's --accelerator does not accept \"mps\".",
|
|
57
|
+
remediation: "Use --param accelerator=gpu (or omit --param accelerator to use the platform default) or \"cpu\" on this installation.",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/** Minimal structural check: parses the document as real YAML (not a regex — a
|
|
61
|
+
* regex like `/^sequences\s*:/m` matches on malformed documents such as
|
|
62
|
+
* `sequences: [` that would fail deep inside a long Boltz run) and requires a
|
|
63
|
+
* non-empty top-level `sequences` array. Not a full Boltz job-schema validator —
|
|
64
|
+
* just enough to reject an obviously-invalid job before a long prediction run. */
|
|
65
|
+
function checkYamlJobStructure(inputPath, text) {
|
|
66
|
+
if (text.trim().length === 0) {
|
|
67
|
+
throw new MoldeskError({
|
|
68
|
+
code: "INVALID_RUN_INPUT",
|
|
69
|
+
message: `Boltz job file is empty: ${inputPath}.`,
|
|
70
|
+
remediation: "Provide a non-empty .yaml job file with a top-level `sequences` key.",
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
let doc;
|
|
74
|
+
try {
|
|
75
|
+
doc = parseYaml(text);
|
|
76
|
+
}
|
|
77
|
+
catch (error) {
|
|
78
|
+
const reason = error instanceof YAMLParseError ? error.message : String(error);
|
|
79
|
+
throw new MoldeskError({
|
|
80
|
+
code: "INVALID_RUN_INPUT",
|
|
81
|
+
message: `Boltz job file is not valid YAML: ${inputPath}. ${reason}`,
|
|
82
|
+
remediation: "Fix the YAML syntax error and retry.",
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
const sequences = doc !== null && typeof doc === "object" && !Array.isArray(doc)
|
|
86
|
+
? doc.sequences
|
|
87
|
+
: undefined;
|
|
88
|
+
if (!Array.isArray(sequences) || sequences.length === 0) {
|
|
89
|
+
throw new MoldeskError({
|
|
90
|
+
code: "INVALID_RUN_INPUT",
|
|
91
|
+
message: `Boltz job file is missing a non-empty top-level "sequences" array: ${inputPath}.`,
|
|
92
|
+
remediation: "Add a top-level `sequences` key (a non-empty list) per the Boltz job schema (see examples/boltz/).",
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export const boltzAdapter = {
|
|
97
|
+
modelName: "boltz",
|
|
98
|
+
params: PARAMS,
|
|
99
|
+
async validateInput(inputPath) {
|
|
100
|
+
if (!fs.existsSync(inputPath)) {
|
|
101
|
+
throw new MoldeskError({
|
|
102
|
+
code: "INVALID_RUN_INPUT",
|
|
103
|
+
message: `Input file not found: ${inputPath}.`,
|
|
104
|
+
remediation: "Provide a path to an existing .yaml, .yml, or .fasta job file.",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
const ext = path.extname(inputPath).toLowerCase();
|
|
108
|
+
if (![".yaml", ".yml", ".fasta"].includes(ext)) {
|
|
109
|
+
throw new MoldeskError({
|
|
110
|
+
code: "INVALID_RUN_INPUT",
|
|
111
|
+
message: `Boltz requires a .yaml, .yml, or .fasta input file, got "${ext || "(no extension)"}".`,
|
|
112
|
+
remediation: "Provide a .yaml/.yml job file or a .fasta sequence file.",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
if (ext === ".fasta") {
|
|
116
|
+
const text = fs.readFileSync(inputPath, "utf8");
|
|
117
|
+
if (text.trim().length === 0) {
|
|
118
|
+
throw new MoldeskError({
|
|
119
|
+
code: "INVALID_RUN_INPUT",
|
|
120
|
+
message: `Boltz .fasta input is empty: ${inputPath}.`,
|
|
121
|
+
remediation: "Provide a non-empty FASTA file.",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
checkYamlJobStructure(inputPath, fs.readFileSync(inputPath, "utf8"));
|
|
127
|
+
},
|
|
128
|
+
/**
|
|
129
|
+
* `accelerator`'s correct default depends on which platform-specific runtime entry
|
|
130
|
+
* was actually installed (`installed.runtime.python.platform`), not `process.platform`
|
|
131
|
+
* — the installed record is the source of truth. `run.ts` calls this before
|
|
132
|
+
* `validateParams` and merges the result in as an additional default.
|
|
133
|
+
*/
|
|
134
|
+
resolveParamDefaults(context) {
|
|
135
|
+
const platform = context.installed.runtime.python?.platform;
|
|
136
|
+
if (platform === "darwin-arm64")
|
|
137
|
+
return { accelerator: "mps" };
|
|
138
|
+
return { accelerator: "gpu" };
|
|
139
|
+
},
|
|
140
|
+
async installPlan(_context) {
|
|
141
|
+
return {
|
|
142
|
+
steps: [
|
|
143
|
+
{ id: "source", description: "Fetch the pinned Boltz source revision for this platform" },
|
|
144
|
+
{ id: "python", description: "Create an isolated managed Python environment" },
|
|
145
|
+
{ id: "dependencies", description: "Install pinned Python dependencies (official boltz on Linux/CUDA, boltz-community on Darwin/MPS)" },
|
|
146
|
+
{ id: "postInstall", description: "Run platform-specific post-install fixups (e.g. boltz-fix-macos-libomp on Darwin)" },
|
|
147
|
+
{ id: "verify", description: "Verify the CLI, checkpoints, and accelerator (CUDA/MPS) availability" },
|
|
148
|
+
],
|
|
149
|
+
};
|
|
150
|
+
},
|
|
151
|
+
async command(context) {
|
|
152
|
+
const params = context.params;
|
|
153
|
+
const accelerator = params.accelerator;
|
|
154
|
+
if (accelerator === "mps" && context.platform !== "darwin-arm64") {
|
|
155
|
+
throw mpsOffDarwinError();
|
|
156
|
+
}
|
|
157
|
+
if (accelerator === "gpu" && context.platform === "darwin-arm64") {
|
|
158
|
+
throw darwinAcceleratorError("gpu");
|
|
159
|
+
}
|
|
160
|
+
const args = [
|
|
161
|
+
"predict",
|
|
162
|
+
context.inputPath,
|
|
163
|
+
"--out_dir", context.outputDir,
|
|
164
|
+
"--cache", path.join(context.assetsDir, "boltz-cache"),
|
|
165
|
+
];
|
|
166
|
+
if (accelerator !== undefined)
|
|
167
|
+
args.push("--accelerator", accelerator);
|
|
168
|
+
if (params.devices !== undefined)
|
|
169
|
+
args.push("--devices", String(params.devices));
|
|
170
|
+
if (params.recycling_steps !== undefined)
|
|
171
|
+
args.push("--recycling_steps", String(params.recycling_steps));
|
|
172
|
+
if (params.sampling_steps !== undefined)
|
|
173
|
+
args.push("--sampling_steps", String(params.sampling_steps));
|
|
174
|
+
if (params.diffusion_samples !== undefined)
|
|
175
|
+
args.push("--diffusion_samples", String(params.diffusion_samples));
|
|
176
|
+
if (params.max_parallel_samples !== undefined)
|
|
177
|
+
args.push("--max_parallel_samples", String(params.max_parallel_samples));
|
|
178
|
+
if (params.use_msa_server === true)
|
|
179
|
+
args.push("--use_msa_server");
|
|
180
|
+
if (params.no_kernels === true)
|
|
181
|
+
args.push("--no_kernels");
|
|
182
|
+
if (params.seed !== undefined)
|
|
183
|
+
args.push("--seed", String(params.seed));
|
|
184
|
+
return { executable: boltzExecutable(context.runtimeExecutable), args };
|
|
185
|
+
},
|
|
186
|
+
async collectOutputs(context) {
|
|
187
|
+
return collectGlobOutputs(context.outputDir, OUTPUTS);
|
|
188
|
+
},
|
|
189
|
+
async verifyInstallation(context) {
|
|
190
|
+
const venvBin = path.join(context.modelDir, ".venv", "bin");
|
|
191
|
+
const python = path.join(venvBin, "python");
|
|
192
|
+
const boltzCli = path.join(venvBin, "boltz");
|
|
193
|
+
const cacheDir = path.join(context.assetsDir, "boltz-cache");
|
|
194
|
+
// Checkpoint/CCD assets are populated and checksum-verified at install time
|
|
195
|
+
// (see manifest `assets:`), never lazily on first run — check their presence
|
|
196
|
+
// here so a run never silently re-triggers an unverified download.
|
|
197
|
+
const requiredFiles = [
|
|
198
|
+
python,
|
|
199
|
+
boltzCli,
|
|
200
|
+
path.join(cacheDir, "mols"),
|
|
201
|
+
path.join(cacheDir, "boltz2_conf.ckpt"),
|
|
202
|
+
path.join(cacheDir, "boltz2_aff.ckpt"),
|
|
203
|
+
];
|
|
204
|
+
const missing = requiredFiles.filter((file) => !fs.existsSync(file));
|
|
205
|
+
if (missing.length > 0)
|
|
206
|
+
return { passed: false, output: `Missing ${missing.join(", ")}` };
|
|
207
|
+
const runner = context.runner ?? runCommand;
|
|
208
|
+
const platform = context.platform ?? detectInstalledPlatform(context.modelDir);
|
|
209
|
+
const probe = platform === "darwin-arm64" ? MPS_PROBE : CUDA_PROBE;
|
|
210
|
+
const result = await runner(python, ["-c", probe], { timeoutMs: 60_000 });
|
|
211
|
+
if (result.code !== 0)
|
|
212
|
+
return { passed: false, output: result.stderr.trim() };
|
|
213
|
+
// The pinned CLI's --accelerator choices are baked in at build time (verified
|
|
214
|
+
// against source above), but assert it here too so a future dependency
|
|
215
|
+
// upgrade that silently drops "mps" from the installed CLI is caught by
|
|
216
|
+
// install verification rather than surfacing only during a run.
|
|
217
|
+
if (platform === "darwin-arm64") {
|
|
218
|
+
const help = await runner(boltzCli, ["predict", "--help"], { timeoutMs: 30_000 });
|
|
219
|
+
if (help.code !== 0 || !help.stdout.includes("mps")) {
|
|
220
|
+
return { passed: false, output: `Installed boltz CLI does not advertise an "mps" --accelerator choice: ${(help.stdout || help.stderr).trim()}` };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
return { passed: true, output: result.stdout.trim() };
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
/** Fallback for when `context.platform` isn't populated (e.g. a caller invoking
|
|
227
|
+
* `verifyInstallation` directly outside the core installer): best-effort read of
|
|
228
|
+
* the installed runtime's recorded platform from `installation.json`, used only
|
|
229
|
+
* to pick which verification probe to run (CUDA vs MPS). Never guesses from
|
|
230
|
+
* `process.platform` — falls back to CUDA probing when the record can't be read, matching
|
|
231
|
+
* the Linux/CUDA path's expectations. */
|
|
232
|
+
function detectInstalledPlatform(modelDir) {
|
|
233
|
+
try {
|
|
234
|
+
const raw = JSON.parse(fs.readFileSync(path.join(modelDir, "installation.json"), "utf8"));
|
|
235
|
+
return raw.runtime?.python?.platform;
|
|
236
|
+
}
|
|
237
|
+
catch {
|
|
238
|
+
return undefined;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
/** Pinned distribution versions (see `models/boltz/manifest.yaml`). Kept as
|
|
242
|
+
* constants here, not read from the manifest at verify time, because
|
|
243
|
+
* `InstallContext` doesn't carry parsed manifest data — bump alongside the
|
|
244
|
+
* manifest's runtime entries' `requirements[].version`. Both distributions are
|
|
245
|
+
* installed from git (source+revision), so the manifest's `version` field
|
|
246
|
+
* isn't itself enforced by the installer (a hash check only applies to
|
|
247
|
+
* PyPI-artifact-pinned requirements, which git-sourced ones can't be) —
|
|
248
|
+
* asserting the exact installed distribution version here is what actually
|
|
249
|
+
* verifies "package version and source revision correspond" (rule 1) for
|
|
250
|
+
* these two requirements. */
|
|
251
|
+
const PINNED_BOLTZ_COMMUNITY_VERSION = "2.10.12";
|
|
252
|
+
const PINNED_OFFICIAL_BOLTZ_VERSION = "2.2.1";
|
|
253
|
+
const CUDA_PROBE = `
|
|
254
|
+
import torch, boltz
|
|
255
|
+
from importlib.metadata import version
|
|
256
|
+
assert torch.cuda.is_available(), "torch.cuda.is_available() is False"
|
|
257
|
+
assert torch.cuda.device_count() >= 1, "no CUDA devices visible"
|
|
258
|
+
assert torch.version.cuda is not None, "torch was not built with CUDA support (torch.version.cuda is None)"
|
|
259
|
+
dist_version = version("boltz")
|
|
260
|
+
assert dist_version == "${PINNED_OFFICIAL_BOLTZ_VERSION}", f"expected pinned boltz==${PINNED_OFFICIAL_BOLTZ_VERSION}, got {dist_version}"
|
|
261
|
+
print(f"cuda ok: torch={torch.__version__} cuda_build={torch.version.cuda} devices={torch.cuda.device_count()} boltz={dist_version}")
|
|
262
|
+
`.trim();
|
|
263
|
+
const MPS_PROBE = `
|
|
264
|
+
import platform, torch, boltz
|
|
265
|
+
from importlib.metadata import version, PackageNotFoundError
|
|
266
|
+
assert platform.system() == "Darwin", f"expected Darwin, got {platform.system()}"
|
|
267
|
+
assert platform.machine() == "arm64", f"expected arm64, got {platform.machine()}"
|
|
268
|
+
assert torch.backends.mps.is_built(), "torch was not built with MPS support"
|
|
269
|
+
assert torch.backends.mps.is_available(), "MPS backend is not available on this host"
|
|
270
|
+
x = torch.ones(4, device="mps") * 2
|
|
271
|
+
result = x.cpu().tolist()
|
|
272
|
+
assert result == [2.0, 2.0, 2.0, 2.0], f"unexpected MPS tensor op result: {result}"
|
|
273
|
+
try:
|
|
274
|
+
dist_version = version("boltz-community")
|
|
275
|
+
except PackageNotFoundError:
|
|
276
|
+
dist_version = None
|
|
277
|
+
assert dist_version is not None, "expected the boltz-community distribution to be installed, not official boltz"
|
|
278
|
+
assert dist_version == "${PINNED_BOLTZ_COMMUNITY_VERSION}", f"expected pinned boltz-community==${PINNED_BOLTZ_COMMUNITY_VERSION}, got {dist_version}"
|
|
279
|
+
try:
|
|
280
|
+
official_version = version("boltz")
|
|
281
|
+
except PackageNotFoundError:
|
|
282
|
+
official_version = None
|
|
283
|
+
assert official_version is None, f"official boltz distribution is also installed (version {official_version}) alongside boltz-community — both expose the same boltz import/CLI and must never coexist"
|
|
284
|
+
print(f"mps ok: torch={torch.__version__} boltz-community={dist_version}")
|
|
285
|
+
`.trim();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
5
|
+
import { boltzAdapter } from "./index.js";
|
|
6
|
+
const dirs = [];
|
|
7
|
+
function tempDir() {
|
|
8
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "moldesk-boltz-test-"));
|
|
9
|
+
dirs.push(dir);
|
|
10
|
+
return dir;
|
|
11
|
+
}
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
for (const dir of dirs.splice(0))
|
|
14
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
describe("boltzAdapter.validateInput", () => {
|
|
17
|
+
it("accepts a .yaml job file with a top-level sequences key", async () => {
|
|
18
|
+
const dir = tempDir();
|
|
19
|
+
const input = path.join(dir, "job.yaml");
|
|
20
|
+
fs.writeFileSync(input, "sequences:\n - protein:\n id: A\n sequence: MKV\n");
|
|
21
|
+
await expect(boltzAdapter.validateInput(input)).resolves.toBeUndefined();
|
|
22
|
+
});
|
|
23
|
+
it("accepts a .yml job file", async () => {
|
|
24
|
+
const dir = tempDir();
|
|
25
|
+
const input = path.join(dir, "job.yml");
|
|
26
|
+
fs.writeFileSync(input, "sequences:\n - protein:\n id: A\n sequence: MKV\n");
|
|
27
|
+
await expect(boltzAdapter.validateInput(input)).resolves.toBeUndefined();
|
|
28
|
+
});
|
|
29
|
+
it("accepts a non-empty .fasta file without a sequences-key check", async () => {
|
|
30
|
+
const dir = tempDir();
|
|
31
|
+
const input = path.join(dir, "job.fasta");
|
|
32
|
+
fs.writeFileSync(input, ">A\nMKV\n");
|
|
33
|
+
await expect(boltzAdapter.validateInput(input)).resolves.toBeUndefined();
|
|
34
|
+
});
|
|
35
|
+
it("rejects a missing file", async () => {
|
|
36
|
+
await expect(boltzAdapter.validateInput(path.join(tempDir(), "missing.yaml"))).rejects.toMatchObject({
|
|
37
|
+
code: "INVALID_RUN_INPUT",
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
it("rejects an unsupported extension", async () => {
|
|
41
|
+
const dir = tempDir();
|
|
42
|
+
const input = path.join(dir, "job.pdb");
|
|
43
|
+
fs.writeFileSync(input, "ATOM\n");
|
|
44
|
+
await expect(boltzAdapter.validateInput(input)).rejects.toMatchObject({ code: "INVALID_RUN_INPUT" });
|
|
45
|
+
});
|
|
46
|
+
it("rejects an empty .yaml file", async () => {
|
|
47
|
+
const dir = tempDir();
|
|
48
|
+
const input = path.join(dir, "job.yaml");
|
|
49
|
+
fs.writeFileSync(input, "");
|
|
50
|
+
await expect(boltzAdapter.validateInput(input)).rejects.toMatchObject({ code: "INVALID_RUN_INPUT" });
|
|
51
|
+
});
|
|
52
|
+
it("rejects a .yaml file missing a top-level sequences key", async () => {
|
|
53
|
+
const dir = tempDir();
|
|
54
|
+
const input = path.join(dir, "job.yaml");
|
|
55
|
+
fs.writeFileSync(input, "version: 1\nname: not-a-job\n");
|
|
56
|
+
await expect(boltzAdapter.validateInput(input)).rejects.toMatchObject({ code: "INVALID_RUN_INPUT" });
|
|
57
|
+
});
|
|
58
|
+
it("rejects an empty .fasta file", async () => {
|
|
59
|
+
const dir = tempDir();
|
|
60
|
+
const input = path.join(dir, "job.fasta");
|
|
61
|
+
fs.writeFileSync(input, " \n");
|
|
62
|
+
await expect(boltzAdapter.validateInput(input)).rejects.toMatchObject({ code: "INVALID_RUN_INPUT" });
|
|
63
|
+
});
|
|
64
|
+
it("rejects syntactically malformed YAML that a regex-based check would miss", async () => {
|
|
65
|
+
const dir = tempDir();
|
|
66
|
+
const input = path.join(dir, "job.yaml");
|
|
67
|
+
// An unclosed flow sequence — `/^sequences\s*:/m` would match this and let
|
|
68
|
+
// it through; a real YAML parser must reject it.
|
|
69
|
+
fs.writeFileSync(input, "sequences: [\n");
|
|
70
|
+
await expect(boltzAdapter.validateInput(input)).rejects.toMatchObject({ code: "INVALID_RUN_INPUT" });
|
|
71
|
+
});
|
|
72
|
+
it("rejects a .yaml file where sequences is not an array", async () => {
|
|
73
|
+
const dir = tempDir();
|
|
74
|
+
const input = path.join(dir, "job.yaml");
|
|
75
|
+
fs.writeFileSync(input, "sequences: not-a-list\n");
|
|
76
|
+
await expect(boltzAdapter.validateInput(input)).rejects.toMatchObject({ code: "INVALID_RUN_INPUT" });
|
|
77
|
+
});
|
|
78
|
+
it("rejects a .yaml file where sequences is an empty array", async () => {
|
|
79
|
+
const dir = tempDir();
|
|
80
|
+
const input = path.join(dir, "job.yaml");
|
|
81
|
+
fs.writeFileSync(input, "sequences: []\n");
|
|
82
|
+
await expect(boltzAdapter.validateInput(input)).rejects.toMatchObject({ code: "INVALID_RUN_INPUT" });
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
describe("boltzAdapter.resolveParamDefaults", () => {
|
|
86
|
+
it("resolves accelerator:mps for a darwin-arm64 install", () => {
|
|
87
|
+
const defaults = boltzAdapter.resolveParamDefaults({
|
|
88
|
+
manifestName: "boltz",
|
|
89
|
+
installed: { runtime: { kind: "python", python: { platform: "darwin-arm64", platformSelected: true } } },
|
|
90
|
+
});
|
|
91
|
+
expect(defaults).toEqual({ accelerator: "mps" });
|
|
92
|
+
});
|
|
93
|
+
it("resolves accelerator:gpu for a linux-x64 install", () => {
|
|
94
|
+
const defaults = boltzAdapter.resolveParamDefaults({
|
|
95
|
+
manifestName: "boltz",
|
|
96
|
+
installed: { runtime: { kind: "python", python: { platform: "linux-x64", platformSelected: true } } },
|
|
97
|
+
});
|
|
98
|
+
expect(defaults).toEqual({ accelerator: "gpu" });
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
describe("boltzAdapter.command", () => {
|
|
102
|
+
it("builds the expected boltz predict invocation", async () => {
|
|
103
|
+
const spec = await boltzAdapter.command({
|
|
104
|
+
manifestName: "boltz",
|
|
105
|
+
inputPath: "/run/input/job.yaml",
|
|
106
|
+
outputDir: "/run/output",
|
|
107
|
+
modelDir: "/models/boltz",
|
|
108
|
+
assetsDir: "/models/boltz/assets",
|
|
109
|
+
params: {
|
|
110
|
+
accelerator: "gpu",
|
|
111
|
+
devices: 1,
|
|
112
|
+
recycling_steps: 3,
|
|
113
|
+
sampling_steps: 200,
|
|
114
|
+
diffusion_samples: 1,
|
|
115
|
+
max_parallel_samples: 5,
|
|
116
|
+
use_msa_server: true,
|
|
117
|
+
no_kernels: false,
|
|
118
|
+
seed: 42,
|
|
119
|
+
},
|
|
120
|
+
runtimeExecutable: "/models/boltz/.venv/bin/python",
|
|
121
|
+
platform: "linux-x64",
|
|
122
|
+
});
|
|
123
|
+
expect(spec.executable).toBe(path.join("/models/boltz/.venv/bin", "boltz"));
|
|
124
|
+
expect(spec.args[0]).toBe("predict");
|
|
125
|
+
expect(spec.args[1]).toBe("/run/input/job.yaml");
|
|
126
|
+
expect(spec.args).toContain("--out_dir");
|
|
127
|
+
expect(spec.args).toContain("/run/output");
|
|
128
|
+
expect(spec.args).toContain("--cache");
|
|
129
|
+
expect(spec.args).toContain(path.join("/models/boltz/assets", "boltz-cache"));
|
|
130
|
+
expect(spec.args).toContain("--accelerator");
|
|
131
|
+
expect(spec.args).toContain("gpu");
|
|
132
|
+
expect(spec.args).toContain("--devices");
|
|
133
|
+
expect(spec.args).toContain("--recycling_steps");
|
|
134
|
+
expect(spec.args).toContain("--sampling_steps");
|
|
135
|
+
expect(spec.args).toContain("--diffusion_samples");
|
|
136
|
+
expect(spec.args).toContain("--max_parallel_samples");
|
|
137
|
+
expect(spec.args).toContain("--use_msa_server");
|
|
138
|
+
expect(spec.args).not.toContain("--no_kernels");
|
|
139
|
+
expect(spec.args).toContain("--seed");
|
|
140
|
+
expect(spec.args).toContain("42");
|
|
141
|
+
});
|
|
142
|
+
it("omits boolean flags when false", async () => {
|
|
143
|
+
const spec = await boltzAdapter.command({
|
|
144
|
+
manifestName: "boltz",
|
|
145
|
+
inputPath: "/run/input/job.yaml",
|
|
146
|
+
outputDir: "/run/output",
|
|
147
|
+
modelDir: "/models/boltz",
|
|
148
|
+
assetsDir: "/models/boltz/assets",
|
|
149
|
+
params: { accelerator: "mps", use_msa_server: false, no_kernels: false },
|
|
150
|
+
runtimeExecutable: "/models/boltz/.venv/bin/python",
|
|
151
|
+
platform: "darwin-arm64",
|
|
152
|
+
});
|
|
153
|
+
expect(spec.args).not.toContain("--use_msa_server");
|
|
154
|
+
expect(spec.args).not.toContain("--no_kernels");
|
|
155
|
+
});
|
|
156
|
+
it("rejects accelerator=mps on a non-darwin-arm64 installation", async () => {
|
|
157
|
+
await expect(boltzAdapter.command({
|
|
158
|
+
manifestName: "boltz",
|
|
159
|
+
inputPath: "/run/input/job.yaml",
|
|
160
|
+
outputDir: "/run/output",
|
|
161
|
+
modelDir: "/models/boltz",
|
|
162
|
+
assetsDir: "/models/boltz/assets",
|
|
163
|
+
params: { accelerator: "mps" },
|
|
164
|
+
runtimeExecutable: "/models/boltz/.venv/bin/python",
|
|
165
|
+
platform: "linux-x64",
|
|
166
|
+
})).rejects.toMatchObject({ code: "INVALID_RUN_PARAMS" });
|
|
167
|
+
});
|
|
168
|
+
it("rejects accelerator=gpu on a darwin-arm64 installation", async () => {
|
|
169
|
+
await expect(boltzAdapter.command({
|
|
170
|
+
manifestName: "boltz",
|
|
171
|
+
inputPath: "/run/input/job.yaml",
|
|
172
|
+
outputDir: "/run/output",
|
|
173
|
+
modelDir: "/models/boltz",
|
|
174
|
+
assetsDir: "/models/boltz/assets",
|
|
175
|
+
params: { accelerator: "gpu" },
|
|
176
|
+
runtimeExecutable: "/models/boltz/.venv/bin/python",
|
|
177
|
+
platform: "darwin-arm64",
|
|
178
|
+
})).rejects.toMatchObject({ code: "INVALID_RUN_PARAMS" });
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
describe("boltzAdapter.collectOutputs", () => {
|
|
182
|
+
function writePrediction(outputDir, options = {}) {
|
|
183
|
+
const predDir = path.join(outputDir, "boltz_results_job", "predictions", "record1");
|
|
184
|
+
fs.mkdirSync(predDir, { recursive: true });
|
|
185
|
+
fs.writeFileSync(path.join(predDir, "record1_model_0.cif"), "data_record1\n");
|
|
186
|
+
fs.writeFileSync(path.join(predDir, "confidence_record1_model_0.json"), "{}");
|
|
187
|
+
if (options.withAffinity) {
|
|
188
|
+
fs.writeFileSync(path.join(predDir, "affinity_record1.json"), "{}");
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
it("collects structures, confidence, and (when present) affinity outputs", async () => {
|
|
192
|
+
const dir = tempDir();
|
|
193
|
+
const outputDir = path.join(dir, "output");
|
|
194
|
+
writePrediction(outputDir, { withAffinity: true });
|
|
195
|
+
const outputs = await boltzAdapter.collectOutputs({
|
|
196
|
+
manifestName: "boltz",
|
|
197
|
+
inputPath: path.join(dir, "input", "job.yaml"),
|
|
198
|
+
outputDir,
|
|
199
|
+
modelDir: dir,
|
|
200
|
+
assetsDir: path.join(dir, "assets"),
|
|
201
|
+
params: {},
|
|
202
|
+
runtimeExecutable: "python",
|
|
203
|
+
});
|
|
204
|
+
expect(outputs.map((o) => o.id).sort()).toEqual(["affinity", "confidence", "structures"]);
|
|
205
|
+
});
|
|
206
|
+
it("succeeds without affinity output since it is not required", async () => {
|
|
207
|
+
const dir = tempDir();
|
|
208
|
+
const outputDir = path.join(dir, "output");
|
|
209
|
+
writePrediction(outputDir);
|
|
210
|
+
const outputs = await boltzAdapter.collectOutputs({
|
|
211
|
+
manifestName: "boltz",
|
|
212
|
+
inputPath: path.join(dir, "input", "job.yaml"),
|
|
213
|
+
outputDir,
|
|
214
|
+
modelDir: dir,
|
|
215
|
+
assetsDir: path.join(dir, "assets"),
|
|
216
|
+
params: {},
|
|
217
|
+
runtimeExecutable: "python",
|
|
218
|
+
});
|
|
219
|
+
expect(outputs.map((o) => o.id).sort()).toEqual(["confidence", "structures"]);
|
|
220
|
+
});
|
|
221
|
+
it("throws MISSING_REQUIRED_OUTPUT when no .cif files were produced", async () => {
|
|
222
|
+
const dir = tempDir();
|
|
223
|
+
const outputDir = path.join(dir, "output");
|
|
224
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
225
|
+
await expect(boltzAdapter.collectOutputs({
|
|
226
|
+
manifestName: "boltz",
|
|
227
|
+
inputPath: path.join(dir, "input", "job.yaml"),
|
|
228
|
+
outputDir,
|
|
229
|
+
modelDir: dir,
|
|
230
|
+
assetsDir: path.join(dir, "assets"),
|
|
231
|
+
params: {},
|
|
232
|
+
runtimeExecutable: "python",
|
|
233
|
+
})).rejects.toMatchObject({ code: "MISSING_REQUIRED_OUTPUT" });
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
describe("boltzAdapter.verifyInstallation", () => {
|
|
237
|
+
/** Creates the venv scripts + the three install-time-populated cache assets
|
|
238
|
+
* (mols/, boltz2_conf.ckpt, boltz2_aff.ckpt) verifyInstallation now requires
|
|
239
|
+
* to exist before running any probe. */
|
|
240
|
+
function createInstalledFixture(dir, assetsDir) {
|
|
241
|
+
fs.mkdirSync(path.join(dir, ".venv", "bin"), { recursive: true });
|
|
242
|
+
fs.writeFileSync(path.join(dir, ".venv", "bin", "python"), "#!/bin/sh\n");
|
|
243
|
+
fs.writeFileSync(path.join(dir, ".venv", "bin", "boltz"), "#!/bin/sh\n");
|
|
244
|
+
const cacheDir = path.join(assetsDir, "boltz-cache");
|
|
245
|
+
fs.mkdirSync(path.join(cacheDir, "mols"), { recursive: true });
|
|
246
|
+
fs.writeFileSync(path.join(cacheDir, "boltz2_conf.ckpt"), "");
|
|
247
|
+
fs.writeFileSync(path.join(cacheDir, "boltz2_aff.ckpt"), "");
|
|
248
|
+
}
|
|
249
|
+
it("fails when the venv's boltz console script is missing", async () => {
|
|
250
|
+
const dir = tempDir();
|
|
251
|
+
fs.mkdirSync(path.join(dir, ".venv", "bin"), { recursive: true });
|
|
252
|
+
fs.writeFileSync(path.join(dir, ".venv", "bin", "python"), "#!/bin/sh\n");
|
|
253
|
+
const result = await boltzAdapter.verifyInstallation({ manifestName: "boltz", modelDir: dir, assetsDir: path.join(dir, "assets") });
|
|
254
|
+
expect(result.passed).toBe(false);
|
|
255
|
+
expect(result.output).toContain("boltz");
|
|
256
|
+
});
|
|
257
|
+
it("fails when the install-time-populated checkpoint/CCD cache is missing, even with a working venv", async () => {
|
|
258
|
+
const dir = tempDir();
|
|
259
|
+
fs.mkdirSync(path.join(dir, ".venv", "bin"), { recursive: true });
|
|
260
|
+
fs.writeFileSync(path.join(dir, ".venv", "bin", "python"), "#!/bin/sh\n");
|
|
261
|
+
fs.writeFileSync(path.join(dir, ".venv", "bin", "boltz"), "#!/bin/sh\n");
|
|
262
|
+
// No boltz-cache/ populated — checkpoints/CCD must be verified present
|
|
263
|
+
// before a run is ever allowed, never lazily downloaded at run time.
|
|
264
|
+
const result = await boltzAdapter.verifyInstallation({
|
|
265
|
+
manifestName: "boltz",
|
|
266
|
+
modelDir: dir,
|
|
267
|
+
assetsDir: path.join(dir, "assets"),
|
|
268
|
+
runner: async () => ({ code: 0, stdout: "should not run", stderr: "" }),
|
|
269
|
+
});
|
|
270
|
+
expect(result.passed).toBe(false);
|
|
271
|
+
expect(result.output).toContain("boltz-cache");
|
|
272
|
+
});
|
|
273
|
+
it("runs the CUDA probe via the injected runner when no installation.json platform is recorded", async () => {
|
|
274
|
+
const dir = tempDir();
|
|
275
|
+
const assetsDir = path.join(dir, "assets");
|
|
276
|
+
createInstalledFixture(dir, assetsDir);
|
|
277
|
+
let capturedScript = "";
|
|
278
|
+
const result = await boltzAdapter.verifyInstallation({
|
|
279
|
+
manifestName: "boltz",
|
|
280
|
+
modelDir: dir,
|
|
281
|
+
assetsDir,
|
|
282
|
+
runner: async (_cmd, args) => {
|
|
283
|
+
capturedScript = args[1] ?? "";
|
|
284
|
+
return { code: 0, stdout: "cuda ok", stderr: "" };
|
|
285
|
+
},
|
|
286
|
+
});
|
|
287
|
+
expect(result.passed).toBe(true);
|
|
288
|
+
expect(capturedScript).toContain("torch.cuda.is_available()");
|
|
289
|
+
expect(capturedScript).toContain("torch.version.cuda is not None");
|
|
290
|
+
expect(capturedScript).toContain('== "2.2.1"');
|
|
291
|
+
});
|
|
292
|
+
it("runs the MPS probe when installation.json records a darwin-arm64 platform", async () => {
|
|
293
|
+
const dir = tempDir();
|
|
294
|
+
const assetsDir = path.join(dir, "assets");
|
|
295
|
+
createInstalledFixture(dir, assetsDir);
|
|
296
|
+
fs.writeFileSync(path.join(dir, "installation.json"), JSON.stringify({ runtime: { python: { platform: "darwin-arm64" } } }));
|
|
297
|
+
let capturedScript = "";
|
|
298
|
+
const result = await boltzAdapter.verifyInstallation({
|
|
299
|
+
manifestName: "boltz",
|
|
300
|
+
modelDir: dir,
|
|
301
|
+
assetsDir,
|
|
302
|
+
runner: async (cmd, args) => {
|
|
303
|
+
if (cmd.endsWith("boltz"))
|
|
304
|
+
return { code: 0, stdout: "--accelerator [gpu|cpu|tpu|mps]", stderr: "" };
|
|
305
|
+
capturedScript = args[1] ?? "";
|
|
306
|
+
return { code: 0, stdout: "mps ok", stderr: "" };
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
expect(result.passed).toBe(true);
|
|
310
|
+
expect(capturedScript).toContain("torch.backends.mps.is_available()");
|
|
311
|
+
expect(capturedScript).toContain('== "2.10.12"');
|
|
312
|
+
expect(capturedScript).toContain('official_version is None');
|
|
313
|
+
});
|
|
314
|
+
it("runs the MPS probe from context.platform even with no installation.json (pre-promotion staging verify)", async () => {
|
|
315
|
+
const dir = tempDir();
|
|
316
|
+
const assetsDir = path.join(dir, "assets");
|
|
317
|
+
createInstalledFixture(dir, assetsDir);
|
|
318
|
+
// No installation.json written yet — this is the staging-directory verify call
|
|
319
|
+
// the core installer makes BEFORE promotion, so installation.json cannot exist.
|
|
320
|
+
// The adapter must trust context.platform here, not fall back to CUDA.
|
|
321
|
+
let capturedScript = "";
|
|
322
|
+
const result = await boltzAdapter.verifyInstallation({
|
|
323
|
+
manifestName: "boltz",
|
|
324
|
+
modelDir: dir,
|
|
325
|
+
assetsDir,
|
|
326
|
+
platform: "darwin-arm64",
|
|
327
|
+
runner: async (cmd, args) => {
|
|
328
|
+
if (cmd.endsWith("boltz"))
|
|
329
|
+
return { code: 0, stdout: "--accelerator [gpu|cpu|tpu|mps]", stderr: "" };
|
|
330
|
+
capturedScript = args[1] ?? "";
|
|
331
|
+
return { code: 0, stdout: "mps ok", stderr: "" };
|
|
332
|
+
},
|
|
333
|
+
});
|
|
334
|
+
expect(result.passed).toBe(true);
|
|
335
|
+
expect(capturedScript).toContain("torch.backends.mps.is_available()");
|
|
336
|
+
});
|
|
337
|
+
it("fails when the installed CLI doesn't advertise an mps accelerator choice", async () => {
|
|
338
|
+
const dir = tempDir();
|
|
339
|
+
const assetsDir = path.join(dir, "assets");
|
|
340
|
+
createInstalledFixture(dir, assetsDir);
|
|
341
|
+
const result = await boltzAdapter.verifyInstallation({
|
|
342
|
+
manifestName: "boltz",
|
|
343
|
+
modelDir: dir,
|
|
344
|
+
assetsDir,
|
|
345
|
+
platform: "darwin-arm64",
|
|
346
|
+
runner: async (cmd) => {
|
|
347
|
+
if (cmd.endsWith("boltz"))
|
|
348
|
+
return { code: 0, stdout: "--accelerator [gpu|cpu|tpu]", stderr: "" };
|
|
349
|
+
return { code: 0, stdout: "mps ok", stderr: "" };
|
|
350
|
+
},
|
|
351
|
+
});
|
|
352
|
+
expect(result.passed).toBe(false);
|
|
353
|
+
expect(result.output).toContain("mps");
|
|
354
|
+
});
|
|
355
|
+
it("reports failure output when the probe fails", async () => {
|
|
356
|
+
const dir = tempDir();
|
|
357
|
+
const assetsDir = path.join(dir, "assets");
|
|
358
|
+
createInstalledFixture(dir, assetsDir);
|
|
359
|
+
const result = await boltzAdapter.verifyInstallation({
|
|
360
|
+
manifestName: "boltz",
|
|
361
|
+
modelDir: dir,
|
|
362
|
+
assetsDir,
|
|
363
|
+
runner: async () => ({ code: 1, stdout: "", stderr: "MPS backend is not available on this host" }),
|
|
364
|
+
});
|
|
365
|
+
expect(result.passed).toBe(false);
|
|
366
|
+
expect(result.output).toContain("MPS backend is not available");
|
|
367
|
+
});
|
|
368
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { ModelAdapterDefinition } from "./index.js";
|
|
2
|
+
import type { ModelManifestV1 } from "@moldesk/registry";
|
|
3
|
+
export declare const adapterCatalog: Record<string, ModelAdapterDefinition>;
|
|
4
|
+
export declare function getAdapter(modelName: string): ModelAdapterDefinition | undefined;
|
|
5
|
+
export declare function hasAdapter(modelName: string): boolean;
|
|
6
|
+
export declare function validateAdapterCatalog(manifests: ReadonlyArray<Pick<ModelManifestV1, "name" | "status">>, catalog?: Record<string, ModelAdapterDefinition>): string[];
|