dh-cli 0.8.8__tar.gz → 0.10.0__tar.gz
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.
- {dh_cli-0.8.8 → dh_cli-0.10.0}/PKG-INFO +1 -1
- {dh_cli-0.8.8 → dh_cli-0.10.0}/pyproject.toml +1 -1
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/__init__.py +4 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/commands/__init__.py +0 -2
- dh_cli-0.10.0/src/dh_cli/batch/commands/equilibrator.py +319 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/commands/finalize.py +67 -0
- dh_cli-0.10.0/src/dh_cli/batch/commands/rfd3.py +402 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/manifest.py +2 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/bedrock/commands.py +2 -5
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/test_rotate.py +2 -6
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/github/conftest.py +1 -1
- dh_cli-0.10.0/tests/test_equilibrator_command.py +229 -0
- dh_cli-0.10.0/tests/test_finalize_equilibrator.py +191 -0
- dh_cli-0.10.0/tests/test_finalize_rfd3.py +130 -0
- dh_cli-0.10.0/tests/test_rfd3_command.py +456 -0
- dh_cli-0.8.8/tests/bedrock/test_cursor_bedrock_isolation.py +0 -271
- {dh_cli-0.8.8 → dh_cli-0.10.0}/.gitignore +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/LICENSE +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/README.md +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/__init__.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/_identity.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/aws_batch.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/commands/boltz.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/commands/cancel.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/commands/clean.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/commands/embed_t5.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/commands/list_jobs.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/commands/local.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/commands/logs.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/commands/orca.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/commands/protmpnn.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/commands/protmpnn_to_boltz.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/commands/retry.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/commands/status.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/commands/submit.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/commands/train.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/commands/wait_for.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/fasta_utils.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/h5_utils.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/job_id.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/batch/s3_transport.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/bedrock/__init__.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/bedrock/cost_report.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/bedrock/pricing.yaml +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/cloud_commands.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/codeartifact.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/engines_studios/__init__.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/engines_studios/api_client.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/engines_studios/auth.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/engines_studios/engine_commands.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/engines_studios/progress.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/engines_studios/ssh_config.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/engines_studios/studio_commands.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/github_commands.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/hz/__init__.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/hz/deploy.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/hz/local.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/hz/test.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/hz/tf.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/hz/users.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/main.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/utility_commands.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/src/dh_cli/warehouse.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/batch/__init__.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/batch/test_aws_batch_resources.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/batch/test_image_override.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/batch/test_submit_cpu_only.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/batch/test_submit_image_validation.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/batch/test_submit_merge.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/conftest.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/fixtures/A_cache_write.json +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/fixtures/B_cache_read.json +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/fixtures/C_plain.json +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/fixtures/D_cursor_user.json +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/fixtures/E_service_role.json +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/fixtures/F_legacy_shared.json +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/fixtures/G_unknown_model.json +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/test_build_report.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/test_classify_arn.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/test_cli_exit_codes.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/test_cost_calc.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/test_cost_command.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/test_cur_reconciliation.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/test_key_command.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/test_render_formats.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/test_resolve_base_model.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/bedrock/test_s3_walker.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/github/__init__.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/github/test_engine_role_cannot_read_github_pat.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/github/test_identity.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/github/test_login.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/github/test_login_error_paths.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/github/test_login_security.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/github/test_logout.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/github/test_rotate.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/github/test_status.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/hz/test_init.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/hz/test_suites.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/hz/test_users.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/test_cloud_gcp.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/test_finalize_boltz_tar.py +0 -0
- {dh_cli-0.8.8 → dh_cli-0.10.0}/tests/test_finalize_protmpnn.py +0 -0
|
@@ -14,6 +14,7 @@ from .commands.boltz import boltz
|
|
|
14
14
|
from .commands.cancel import cancel
|
|
15
15
|
from .commands.clean import clean
|
|
16
16
|
from .commands.embed_t5 import embed_t5
|
|
17
|
+
from .commands.equilibrator import equilibrator
|
|
17
18
|
from .commands.finalize import finalize
|
|
18
19
|
from .commands.list_jobs import list_jobs
|
|
19
20
|
from .commands.local import local
|
|
@@ -22,6 +23,7 @@ from .commands.orca import orca
|
|
|
22
23
|
from .commands.protmpnn import protmpnn
|
|
23
24
|
from .commands.protmpnn_to_boltz import protmpnn_to_boltz
|
|
24
25
|
from .commands.retry import retry
|
|
26
|
+
from .commands.rfd3 import rfd3
|
|
25
27
|
from .commands.status import status
|
|
26
28
|
from .commands.submit import submit
|
|
27
29
|
from .commands.train import train
|
|
@@ -98,10 +100,12 @@ batch_cli.add_command(wait_for, name="wait-for")
|
|
|
98
100
|
|
|
99
101
|
# Register pipeline commands
|
|
100
102
|
batch_cli.add_command(embed_t5, name="embed-t5")
|
|
103
|
+
batch_cli.add_command(equilibrator, name="equilibrator")
|
|
101
104
|
batch_cli.add_command(boltz)
|
|
102
105
|
batch_cli.add_command(protmpnn)
|
|
103
106
|
batch_cli.add_command(orca)
|
|
104
107
|
batch_cli.add_command(protmpnn_to_boltz, name="protmpnn-to-boltz")
|
|
108
|
+
batch_cli.add_command(rfd3)
|
|
105
109
|
batch_cli.add_command(train)
|
|
106
110
|
|
|
107
111
|
__all__ = ["batch_cli"]
|
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
from .boltz import boltz
|
|
4
4
|
from .cancel import cancel
|
|
5
5
|
from .embed_t5 import embed_t5
|
|
6
|
-
from .finalize import finalize
|
|
7
6
|
from .list_jobs import list_jobs
|
|
8
7
|
from .local import local
|
|
9
8
|
from .logs import logs
|
|
@@ -15,7 +14,6 @@ __all__ = [
|
|
|
15
14
|
"boltz",
|
|
16
15
|
"cancel",
|
|
17
16
|
"embed_t5",
|
|
18
|
-
"finalize",
|
|
19
17
|
"list_jobs",
|
|
20
18
|
"local",
|
|
21
19
|
"logs",
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
"""eQuilibrator reaction ΔG°' pipeline command."""
|
|
2
|
+
|
|
3
|
+
import math
|
|
4
|
+
import secrets
|
|
5
|
+
import subprocess
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from ..aws_batch import BatchClient, BatchError, resolve_dependency
|
|
12
|
+
from ..job_id import get_aws_username
|
|
13
|
+
from ..manifest import (
|
|
14
|
+
BATCH_JOBS_BASE,
|
|
15
|
+
BatchConfig,
|
|
16
|
+
InputConfig,
|
|
17
|
+
JobManifest,
|
|
18
|
+
JobStatus,
|
|
19
|
+
OutputConfig,
|
|
20
|
+
create_job_directory,
|
|
21
|
+
save_local_stub,
|
|
22
|
+
save_manifest,
|
|
23
|
+
save_manifest_s3,
|
|
24
|
+
)
|
|
25
|
+
from ..s3_transport import s3_job_prefix, upload_directory
|
|
26
|
+
|
|
27
|
+
DEFAULT_QUEUE = "cpu-spot"
|
|
28
|
+
DEFAULT_JOB_DEFINITION = "dayhoff-equilibrator"
|
|
29
|
+
DEFAULT_IMAGE_URI = "074735440724.dkr.ecr.us-east-1.amazonaws.com/dayhoff:equilibrator-latest"
|
|
30
|
+
REACTIONS_PER_WORKER = 1000
|
|
31
|
+
MAX_WORKERS = 50
|
|
32
|
+
|
|
33
|
+
ASSETS_REPO_PATH = "/primordial/equilibrator-assets"
|
|
34
|
+
# ~4s/reaction * REACTIONS_PER_WORKER + sqlite copy + headroom.
|
|
35
|
+
TIMEOUT_SECONDS = 3 * 3600
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _generate_job_id() -> str:
|
|
39
|
+
"""Job ID as ``{username}-equilibrator-{YYYYMMDD}-{random8}``.
|
|
40
|
+
|
|
41
|
+
Built here (not via ``generate_job_id``) so the full ``equilibrator``
|
|
42
|
+
pipeline name survives — the generic helper truncates to 10 chars.
|
|
43
|
+
"""
|
|
44
|
+
username = get_aws_username()
|
|
45
|
+
date_str = datetime.now().strftime("%Y%m%d")
|
|
46
|
+
return f"{username}-equilibrator-{date_str}-{secrets.token_hex(4)}"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _is_primordial_path(path: Path) -> bool:
|
|
50
|
+
"""Check if a path is on Primordial (EFS)."""
|
|
51
|
+
return str(path).startswith("/primordial/")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _resolve_image_digest() -> str:
|
|
55
|
+
"""Best-effort resolution of the container image digest via ECR.
|
|
56
|
+
|
|
57
|
+
Patched out in tests. Never crashes the command.
|
|
58
|
+
"""
|
|
59
|
+
try:
|
|
60
|
+
result = subprocess.run(
|
|
61
|
+
[
|
|
62
|
+
"aws",
|
|
63
|
+
"ecr",
|
|
64
|
+
"describe-images",
|
|
65
|
+
"--repository-name",
|
|
66
|
+
"dayhoff",
|
|
67
|
+
"--image-ids",
|
|
68
|
+
"imageTag=equilibrator-latest",
|
|
69
|
+
"--query",
|
|
70
|
+
"imageDetails[0].imageDigest",
|
|
71
|
+
"--output",
|
|
72
|
+
"text",
|
|
73
|
+
],
|
|
74
|
+
capture_output=True,
|
|
75
|
+
text=True,
|
|
76
|
+
timeout=15,
|
|
77
|
+
)
|
|
78
|
+
digest = result.stdout.strip()
|
|
79
|
+
if result.returncode == 0 and digest and digest != "None":
|
|
80
|
+
return digest
|
|
81
|
+
except Exception:
|
|
82
|
+
pass
|
|
83
|
+
return DEFAULT_IMAGE_URI
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _resolve_assets_commit() -> str:
|
|
87
|
+
"""Best-effort current equilibrator-assets git commit short SHA.
|
|
88
|
+
|
|
89
|
+
Patched out in tests. Returns "unknown" on failure.
|
|
90
|
+
"""
|
|
91
|
+
try:
|
|
92
|
+
result = subprocess.run(
|
|
93
|
+
["git", "-C", ASSETS_REPO_PATH, "rev-parse", "--short", "HEAD"],
|
|
94
|
+
capture_output=True,
|
|
95
|
+
text=True,
|
|
96
|
+
timeout=10,
|
|
97
|
+
)
|
|
98
|
+
sha = result.stdout.strip()
|
|
99
|
+
if result.returncode == 0 and sha:
|
|
100
|
+
return sha
|
|
101
|
+
except Exception:
|
|
102
|
+
pass
|
|
103
|
+
return "unknown"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _read_reactions(input_path: Path) -> list[str]:
|
|
107
|
+
"""Return non-blank reaction lines from the input file, in order."""
|
|
108
|
+
return [line for line in input_path.read_text().splitlines() if line.strip()]
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _chunk_reactions(reactions: list[str], array_size: int) -> list[list[str]]:
|
|
112
|
+
"""Split reactions into ``array_size`` contiguous, balanced chunks.
|
|
113
|
+
|
|
114
|
+
First ``n % k`` chunks get ceil(n/k) lines, the rest get floor(n/k).
|
|
115
|
+
"""
|
|
116
|
+
n = len(reactions)
|
|
117
|
+
base = n // array_size
|
|
118
|
+
remainder = n % array_size
|
|
119
|
+
chunks: list[list[str]] = []
|
|
120
|
+
start = 0
|
|
121
|
+
for i in range(array_size):
|
|
122
|
+
size = base + (1 if i < remainder else 0)
|
|
123
|
+
chunks.append(reactions[start : start + size])
|
|
124
|
+
start += size
|
|
125
|
+
return chunks
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _write_chunks(input_dir: Path, chunks: list[list[str]]) -> None:
|
|
129
|
+
"""Write each chunk to ``input_dir/chunk_{i:03d}.txt``."""
|
|
130
|
+
input_dir.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
for i, chunk in enumerate(chunks):
|
|
132
|
+
(input_dir / f"chunk_{i:03d}.txt").write_text("\n".join(chunk) + "\n")
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@click.command()
|
|
136
|
+
@click.argument("input_file", type=click.Path(exists=True))
|
|
137
|
+
@click.option(
|
|
138
|
+
"--workers",
|
|
139
|
+
default=None,
|
|
140
|
+
type=int,
|
|
141
|
+
help=f"Number of parallel workers [default: ~1 per {REACTIONS_PER_WORKER} reactions]",
|
|
142
|
+
)
|
|
143
|
+
@click.option("--queue", default=DEFAULT_QUEUE, help=f"Batch queue [default: {DEFAULT_QUEUE}]")
|
|
144
|
+
@click.option("--dry-run", is_flag=True, help="Show plan without submitting")
|
|
145
|
+
@click.option("--base-path", default=BATCH_JOBS_BASE, help="Base path for job data")
|
|
146
|
+
@click.option("--after", "after", multiple=True, help="Job ID(s) to wait for before starting")
|
|
147
|
+
def equilibrator(input_file, workers, queue, dry_run, base_path, after):
|
|
148
|
+
"""Compute reaction ΔG°' with eQuilibrator.
|
|
149
|
+
|
|
150
|
+
Splits an input file of reaction SMILES (one per line) into chunks and
|
|
151
|
+
processes them in parallel using AWS Batch array jobs.
|
|
152
|
+
|
|
153
|
+
\b
|
|
154
|
+
Examples:
|
|
155
|
+
# Submit to AWS Batch (auto-sized workers)
|
|
156
|
+
dh batch equilibrator /primordial/reactions.txt
|
|
157
|
+
|
|
158
|
+
# Force a specific worker count
|
|
159
|
+
dh batch equilibrator reactions.txt --workers 20
|
|
160
|
+
|
|
161
|
+
# Preview what would run
|
|
162
|
+
dh batch equilibrator reactions.txt --dry-run
|
|
163
|
+
"""
|
|
164
|
+
input_path = Path(input_file).resolve()
|
|
165
|
+
|
|
166
|
+
click.echo(f"Counting reactions in {input_path}...")
|
|
167
|
+
reactions = _read_reactions(input_path)
|
|
168
|
+
num_reactions = len(reactions)
|
|
169
|
+
click.echo(f"Found {num_reactions} reaction(s)")
|
|
170
|
+
|
|
171
|
+
if num_reactions == 0:
|
|
172
|
+
click.echo(click.style("Error: No reactions found in input file", fg="red"), err=True)
|
|
173
|
+
raise SystemExit(1)
|
|
174
|
+
|
|
175
|
+
if workers is None:
|
|
176
|
+
workers = min(math.ceil(num_reactions / REACTIONS_PER_WORKER), MAX_WORKERS)
|
|
177
|
+
array_size = min(workers, num_reactions)
|
|
178
|
+
|
|
179
|
+
use_s3 = not _is_primordial_path(input_path) and str(base_path) == BATCH_JOBS_BASE
|
|
180
|
+
|
|
181
|
+
job_id = _generate_job_id()
|
|
182
|
+
|
|
183
|
+
click.echo()
|
|
184
|
+
click.echo(f"Job ID: {job_id}")
|
|
185
|
+
click.echo(f"Input: {input_path}")
|
|
186
|
+
click.echo(f"Reactions: {num_reactions}")
|
|
187
|
+
click.echo(f"Workers: {array_size}")
|
|
188
|
+
click.echo(f"Queue: {queue}")
|
|
189
|
+
click.echo(f"Job definition: {DEFAULT_JOB_DEFINITION}")
|
|
190
|
+
click.echo(f"Storage: {'S3 (remote)' if use_s3 else 'Primordial'}")
|
|
191
|
+
|
|
192
|
+
if dry_run:
|
|
193
|
+
click.echo()
|
|
194
|
+
click.echo(click.style("Dry run - job not submitted", fg="yellow"))
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
if not click.confirm("\nSubmit job?", default=True):
|
|
198
|
+
click.echo("Cancelled.")
|
|
199
|
+
raise SystemExit(0)
|
|
200
|
+
click.echo()
|
|
201
|
+
|
|
202
|
+
client = BatchClient()
|
|
203
|
+
|
|
204
|
+
response = client.batch.describe_job_definitions(jobDefinitionName=DEFAULT_JOB_DEFINITION, status="ACTIVE")
|
|
205
|
+
if not response.get("jobDefinitions"):
|
|
206
|
+
raise click.ClickException(
|
|
207
|
+
"equilibrator job definition not yet deployed; apply Terraform in environments/dev first"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
chunks = _chunk_reactions(reactions, array_size)
|
|
211
|
+
image_digest = _resolve_image_digest()
|
|
212
|
+
source_commit = _resolve_assets_commit()
|
|
213
|
+
|
|
214
|
+
s3_prefix = s3_job_prefix(job_id) if use_s3 else None
|
|
215
|
+
job_dir_path = Path(base_path) / job_id
|
|
216
|
+
|
|
217
|
+
if use_s3:
|
|
218
|
+
import tempfile as _tempfile
|
|
219
|
+
|
|
220
|
+
with _tempfile.TemporaryDirectory() as tmpdir:
|
|
221
|
+
tmp_input_dir = Path(tmpdir) / "input"
|
|
222
|
+
_write_chunks(tmp_input_dir, chunks)
|
|
223
|
+
click.echo("Uploading chunks to S3...")
|
|
224
|
+
upload_directory(tmp_input_dir, f"{s3_prefix}input/", glob="*.txt")
|
|
225
|
+
click.echo(f"Uploaded {array_size} chunks to S3")
|
|
226
|
+
else:
|
|
227
|
+
job_dir_path = create_job_directory(job_id, base_path)
|
|
228
|
+
click.echo(f"Created job directory: {job_dir_path}")
|
|
229
|
+
_write_chunks(job_dir_path / "input", chunks)
|
|
230
|
+
click.echo(f"Created {array_size} chunks")
|
|
231
|
+
|
|
232
|
+
manifest = JobManifest(
|
|
233
|
+
job_id=job_id,
|
|
234
|
+
user=job_id.split("-")[0],
|
|
235
|
+
pipeline="equilibrator",
|
|
236
|
+
status=JobStatus.PENDING,
|
|
237
|
+
image_uri=DEFAULT_IMAGE_URI,
|
|
238
|
+
image_digest=image_digest,
|
|
239
|
+
source_commit=source_commit,
|
|
240
|
+
storage_mode="s3" if use_s3 else "primordial",
|
|
241
|
+
s3_prefix=s3_prefix,
|
|
242
|
+
input=InputConfig(
|
|
243
|
+
source=str(input_path),
|
|
244
|
+
num_sequences=num_reactions,
|
|
245
|
+
num_chunks=array_size,
|
|
246
|
+
),
|
|
247
|
+
batch=BatchConfig(
|
|
248
|
+
queue=queue,
|
|
249
|
+
job_definition=DEFAULT_JOB_DEFINITION,
|
|
250
|
+
array_size=array_size,
|
|
251
|
+
),
|
|
252
|
+
output=OutputConfig(
|
|
253
|
+
destination=None,
|
|
254
|
+
finalized=False,
|
|
255
|
+
),
|
|
256
|
+
depends_on=list(after) if after else None,
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
if use_s3:
|
|
260
|
+
save_manifest_s3(manifest)
|
|
261
|
+
save_local_stub(job_id, s3_prefix)
|
|
262
|
+
else:
|
|
263
|
+
save_manifest(manifest, base_path)
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
resolved = [resolve_dependency(jid, base_path) for jid in after]
|
|
267
|
+
depends_on = [{"jobId": aws_id} for aws_id in resolved if aws_id is not None] or None
|
|
268
|
+
|
|
269
|
+
environment = {
|
|
270
|
+
"JOB_DIR": str(job_dir_path),
|
|
271
|
+
"JOB_ID": job_id,
|
|
272
|
+
}
|
|
273
|
+
if use_s3:
|
|
274
|
+
environment["STORAGE_MODE"] = "s3"
|
|
275
|
+
environment["S3_JOB_PREFIX"] = s3_prefix
|
|
276
|
+
|
|
277
|
+
batch_job_id = client.submit_job(
|
|
278
|
+
job_name=job_id,
|
|
279
|
+
job_definition=DEFAULT_JOB_DEFINITION,
|
|
280
|
+
job_queue=queue,
|
|
281
|
+
array_size=array_size,
|
|
282
|
+
environment=environment,
|
|
283
|
+
timeout_seconds=TIMEOUT_SECONDS,
|
|
284
|
+
retry_attempts=5,
|
|
285
|
+
depends_on=depends_on,
|
|
286
|
+
share_identifier=get_aws_username(),
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
manifest.status = JobStatus.SUBMITTED
|
|
290
|
+
manifest.batch.job_id = batch_job_id
|
|
291
|
+
if use_s3:
|
|
292
|
+
save_manifest_s3(manifest)
|
|
293
|
+
else:
|
|
294
|
+
save_manifest(manifest, base_path)
|
|
295
|
+
|
|
296
|
+
click.echo()
|
|
297
|
+
click.echo(click.style("Job submitted successfully!", fg="green"))
|
|
298
|
+
click.echo()
|
|
299
|
+
click.echo(f"AWS Batch Job ID: {batch_job_id}")
|
|
300
|
+
if depends_on:
|
|
301
|
+
click.echo(f"Waiting on: {', '.join(after)}")
|
|
302
|
+
click.echo()
|
|
303
|
+
click.echo("Next steps:")
|
|
304
|
+
click.echo(f" Check status: dh batch status {job_id}")
|
|
305
|
+
click.echo(f" View logs: dh batch logs {job_id}")
|
|
306
|
+
click.echo(f" Cancel: dh batch cancel {job_id}")
|
|
307
|
+
click.echo()
|
|
308
|
+
click.echo("After completion:")
|
|
309
|
+
click.echo(f" Finalize: dh batch finalize {job_id} --output ./results/")
|
|
310
|
+
|
|
311
|
+
except BatchError as e:
|
|
312
|
+
manifest.status = JobStatus.FAILED
|
|
313
|
+
manifest.error_message = str(e)
|
|
314
|
+
if use_s3:
|
|
315
|
+
save_manifest_s3(manifest)
|
|
316
|
+
else:
|
|
317
|
+
save_manifest(manifest, base_path)
|
|
318
|
+
click.echo(click.style(f"Failed to submit job: {e}", fg="red"), err=True)
|
|
319
|
+
raise SystemExit(1)
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Finalize command for combining results and cleaning up."""
|
|
2
2
|
|
|
3
|
+
import json
|
|
3
4
|
import shutil
|
|
4
5
|
import tarfile
|
|
5
6
|
import tempfile
|
|
@@ -143,6 +144,10 @@ def finalize(job_id, output, force, keep_intermediates, full_output, skip_dedup,
|
|
|
143
144
|
_finalize_boltz(output_dir, output_path, full_output=full_output)
|
|
144
145
|
elif manifest.pipeline == "protmpnn":
|
|
145
146
|
_finalize_protmpnn(output_dir, output_path)
|
|
147
|
+
elif manifest.pipeline == "rfd3":
|
|
148
|
+
_finalize_rfd3(output_dir, output_path)
|
|
149
|
+
elif manifest.pipeline == "equilibrator":
|
|
150
|
+
_finalize_equilibrator(output_dir, output_path, job_id=job_id)
|
|
146
151
|
else:
|
|
147
152
|
_finalize_generic(output_dir, output_path)
|
|
148
153
|
|
|
@@ -544,3 +549,65 @@ def _finalize_protmpnn(output_dir: Path, output_path: Path):
|
|
|
544
549
|
|
|
545
550
|
click.echo(f"{num_variants} variants from {num_configs} config(s), top confidence: {top_conf}")
|
|
546
551
|
click.echo(click.style(f"Results: {output_path / 'results.csv'}", fg="green"))
|
|
552
|
+
|
|
553
|
+
|
|
554
|
+
def _finalize_rfd3(output_dir: Path, output_path: Path):
|
|
555
|
+
"""Collate completed RFD3 per-config backbone dirs into the final output.
|
|
556
|
+
|
|
557
|
+
The worker promotes each config's flat `.cif.gz` + `.json` design files
|
|
558
|
+
into `output/<stem>/` and drops a `output/rfd3_<stem>.done` marker only
|
|
559
|
+
when validation passed. Finalize copies each completed config's directory
|
|
560
|
+
into `output_path/<stem>/`, skipping configs without a `.done` marker and
|
|
561
|
+
the loose worker-index markers.
|
|
562
|
+
"""
|
|
563
|
+
completed = []
|
|
564
|
+
for marker in sorted(output_dir.glob("rfd3_*.done")):
|
|
565
|
+
stem = marker.name[len("rfd3_") : -len(".done")]
|
|
566
|
+
if (output_dir / stem).is_dir():
|
|
567
|
+
completed.append(stem)
|
|
568
|
+
|
|
569
|
+
if not completed:
|
|
570
|
+
click.echo("No completed RFD3 configs found in output directory.", err=True)
|
|
571
|
+
raise SystemExit(1)
|
|
572
|
+
|
|
573
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
574
|
+
|
|
575
|
+
copied_count = 0
|
|
576
|
+
for stem in completed:
|
|
577
|
+
dest = output_path / stem
|
|
578
|
+
if dest.exists():
|
|
579
|
+
click.echo(f" Skipping {stem} (already exists)")
|
|
580
|
+
continue
|
|
581
|
+
shutil.copytree(output_dir / stem, dest)
|
|
582
|
+
copied_count += 1
|
|
583
|
+
|
|
584
|
+
click.echo(f"Collated {copied_count} RFD3 config(s)")
|
|
585
|
+
click.echo(click.style(f"Results: {output_path}", fg="green"))
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _finalize_equilibrator(output_dir: Path, output_path: Path, job_id: str | None = None) -> None:
|
|
589
|
+
"""Combine per-chunk JSONL worker outputs into one sorted JSONL file.
|
|
590
|
+
|
|
591
|
+
Reads every ``chunk_*.jsonl`` under ``output_dir``, sorts all records by
|
|
592
|
+
``input_index``, and writes them to the destination. A missing chunk
|
|
593
|
+
(gap in indices) is tolerated. If ``output_path`` is an existing
|
|
594
|
+
directory, the combined file is named ``{job_id}.jsonl`` inside it.
|
|
595
|
+
"""
|
|
596
|
+
if output_path.is_dir():
|
|
597
|
+
output_path = output_path / f"{job_id}.jsonl"
|
|
598
|
+
|
|
599
|
+
records: list[dict] = []
|
|
600
|
+
for chunk_file in sorted(output_dir.glob("chunk_*.jsonl")):
|
|
601
|
+
for line in chunk_file.read_text().splitlines():
|
|
602
|
+
if line.strip():
|
|
603
|
+
records.append(json.loads(line))
|
|
604
|
+
|
|
605
|
+
records.sort(key=lambda r: r["input_index"])
|
|
606
|
+
|
|
607
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
608
|
+
with open(output_path, "w") as f:
|
|
609
|
+
for rec in records:
|
|
610
|
+
f.write(json.dumps(rec) + "\n")
|
|
611
|
+
|
|
612
|
+
shutil.rmtree(output_dir, ignore_errors=True)
|
|
613
|
+
click.echo(f"Combined {len(records)} records into {output_path}")
|