physistool 0.1.2__tar.gz → 0.1.4__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.
- {physistool-0.1.2 → physistool-0.1.4}/PKG-INFO +1 -1
- {physistool-0.1.2 → physistool-0.1.4}/physis/__init__.py +1 -1
- {physistool-0.1.2 → physistool-0.1.4}/physis/metering.py +40 -8
- {physistool-0.1.2 → physistool-0.1.4}/physis/physis_client.py +139 -37
- {physistool-0.1.2 → physistool-0.1.4}/physistool.egg-info/PKG-INFO +1 -1
- {physistool-0.1.2 → physistool-0.1.4}/pyproject.toml +1 -1
- {physistool-0.1.2 → physistool-0.1.4}/LICENSE +0 -0
- {physistool-0.1.2 → physistool-0.1.4}/README.md +0 -0
- {physistool-0.1.2 → physistool-0.1.4}/physis/attestation.py +0 -0
- {physistool-0.1.2 → physistool-0.1.4}/physis/dispatcher.py +0 -0
- {physistool-0.1.2 → physistool-0.1.4}/physis/envelope.py +0 -0
- {physistool-0.1.2 → physistool-0.1.4}/physis/licensing.py +0 -0
- {physistool-0.1.2 → physistool-0.1.4}/physistool.egg-info/SOURCES.txt +0 -0
- {physistool-0.1.2 → physistool-0.1.4}/physistool.egg-info/dependency_links.txt +0 -0
- {physistool-0.1.2 → physistool-0.1.4}/physistool.egg-info/entry_points.txt +0 -0
- {physistool-0.1.2 → physistool-0.1.4}/physistool.egg-info/requires.txt +0 -0
- {physistool-0.1.2 → physistool-0.1.4}/physistool.egg-info/top_level.txt +0 -0
- {physistool-0.1.2 → physistool-0.1.4}/setup.cfg +0 -0
|
@@ -36,7 +36,7 @@ _TIMEOUT = 15 # seconds
|
|
|
36
36
|
# in sync with ``build/package.py`` VERSION — this is the version of the shipped
|
|
37
37
|
# wheel. It is *spoofable* by design: the gate is a forced-upgrade lever, not an
|
|
38
38
|
# anti-tamper check — the real enforcement lives server-side.
|
|
39
|
-
CLIENT_VERSION = "0.1.
|
|
39
|
+
CLIENT_VERSION = "0.1.3"
|
|
40
40
|
|
|
41
41
|
|
|
42
42
|
class MeteringError(Exception):
|
|
@@ -52,6 +52,25 @@ class MeteringError(Exception):
|
|
|
52
52
|
self.detail = detail or message
|
|
53
53
|
|
|
54
54
|
|
|
55
|
+
class UpgradeRequiredError(MeteringError):
|
|
56
|
+
"""The server's MIN_CLIENT_VERSION gate refused this build (HTTP 403).
|
|
57
|
+
|
|
58
|
+
Enforced server-side on the charge path (reserve) BEFORE any credit moves, so
|
|
59
|
+
an old or cracked client cannot fold. Carries the required + current versions
|
|
60
|
+
and a ready-to-print ``pip install -U`` message. Subclasses MeteringError so
|
|
61
|
+
existing ``except MeteringError`` sites still catch it, but callers may catch
|
|
62
|
+
this specifically to show a clean forced-upgrade prompt.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, min_client_version: str, your_version: str):
|
|
66
|
+
self.min_client_version = min_client_version
|
|
67
|
+
self.your_version = your_version
|
|
68
|
+
msg = (f"this physistool client ({your_version}) is below the minimum "
|
|
69
|
+
f"supported version ({min_client_version}). "
|
|
70
|
+
f"Upgrade with: pip install -U physistool")
|
|
71
|
+
super().__init__(msg, status=403, detail=msg)
|
|
72
|
+
|
|
73
|
+
|
|
55
74
|
def _post(base_url: str, path: str, payload: dict) -> dict:
|
|
56
75
|
url = f"{base_url}{path}"
|
|
57
76
|
# client_version rides along on every call (see CLIENT_VERSION); an explicit
|
|
@@ -66,7 +85,16 @@ def _post(base_url: str, path: str, payload: dict) -> dict:
|
|
|
66
85
|
body = resp.read().decode("utf-8")
|
|
67
86
|
return json.loads(body) if body else {}
|
|
68
87
|
except urllib.error.HTTPError as e:
|
|
69
|
-
|
|
88
|
+
raw, obj = _read_error_body(e)
|
|
89
|
+
# Forced-upgrade gate: a 403 carrying min_client_version means this build
|
|
90
|
+
# is below the server's floor. Surface it as a dedicated, actionable error
|
|
91
|
+
# (the client prints "pip install -U") instead of a generic reserve failure.
|
|
92
|
+
if e.code == 403 and isinstance(obj, dict) and obj.get("min_client_version"):
|
|
93
|
+
raise UpgradeRequiredError(
|
|
94
|
+
str(obj["min_client_version"]),
|
|
95
|
+
str(obj.get("your_version") or CLIENT_VERSION)) from e
|
|
96
|
+
detail = (obj.get("error") or obj.get("detail") or raw) if isinstance(obj, dict) \
|
|
97
|
+
else (raw or e.reason or "unknown error")
|
|
70
98
|
raise MeteringError(f"backend returned HTTP {e.code}: {detail}",
|
|
71
99
|
status=e.code, detail=detail) from e
|
|
72
100
|
except urllib.error.URLError as e:
|
|
@@ -79,13 +107,17 @@ def _post(base_url: str, path: str, payload: dict) -> dict:
|
|
|
79
107
|
status=0, detail=str(e)) from e
|
|
80
108
|
|
|
81
109
|
|
|
82
|
-
def
|
|
110
|
+
def _read_error_body(e: "urllib.error.HTTPError") -> "tuple[str, object]":
|
|
111
|
+
"""Read an HTTPError body once, returning (raw_text, parsed_json_or_None)."""
|
|
112
|
+
raw = ""
|
|
113
|
+
try:
|
|
114
|
+
raw = e.read().decode("utf-8", "replace")
|
|
115
|
+
except OSError:
|
|
116
|
+
return "", None
|
|
83
117
|
try:
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return
|
|
87
|
-
except (ValueError, OSError):
|
|
88
|
-
return e.reason or "unknown error"
|
|
118
|
+
return raw, json.loads(raw)
|
|
119
|
+
except ValueError:
|
|
120
|
+
return raw, None
|
|
89
121
|
|
|
90
122
|
|
|
91
123
|
# ── lifecycle calls ─────────────────────────────────────────────────────────
|
|
@@ -668,6 +668,10 @@ def fold(
|
|
|
668
668
|
backend, api_key, credits=credits,
|
|
669
669
|
idempotency_key=str(uuid.uuid4()), input_hash=input_hash,
|
|
670
670
|
)
|
|
671
|
+
except metering_mod.UpgradeRequiredError as exc:
|
|
672
|
+
# Forced-upgrade gate (server MIN_CLIENT_VERSION). Not a reserve/credit
|
|
673
|
+
# problem — the build is too old to fold. Surface the pip command as-is.
|
|
674
|
+
raise FoldError(exc.detail, status=exc.status, error="please_upgrade") from exc
|
|
671
675
|
except metering_mod.MeteringError as exc:
|
|
672
676
|
raise FoldError(
|
|
673
677
|
f"could not reserve {credits} credit(s): {exc.detail}",
|
|
@@ -825,10 +829,17 @@ def fold(
|
|
|
825
829
|
if not pdb_b64:
|
|
826
830
|
raise FoldError("server reported ok but returned no pdb_b64", status=status)
|
|
827
831
|
pdb_bytes = base64.b64decode(pdb_b64)
|
|
832
|
+
# Create the --out directory if the customer pointed at a new subfolder
|
|
833
|
+
# (e.g. --out run1/fold.pdb). open() alone would raise on a missing dir;
|
|
834
|
+
# batch mode already does this per-job, so match that behaviour here.
|
|
835
|
+
os.makedirs(os.path.dirname(os.path.abspath(out_path)) or ".", exist_ok=True)
|
|
828
836
|
with open(out_path, "wb") as fh:
|
|
829
837
|
fh.write(pdb_bytes)
|
|
830
838
|
if report_path is None:
|
|
831
839
|
report_path = os.path.splitext(out_path)[0] + ".json"
|
|
840
|
+
# --report-json may point at its own (possibly missing) subfolder; create it
|
|
841
|
+
# too. Harmless when it is the same dir as --out (already made above).
|
|
842
|
+
os.makedirs(os.path.dirname(os.path.abspath(report_path)) or ".", exist_ok=True)
|
|
832
843
|
with open(report_path, "w", encoding="utf-8") as fh:
|
|
833
844
|
json.dump(report_json, fh, indent=2)
|
|
834
845
|
# Value layer (§2.3): interactive .html + .md/.pml/CSVs/trajectory/ensemble/
|
|
@@ -952,6 +963,41 @@ _MANIFEST_EVIDENCE = {dest: kwarg for _f, dest, kwarg, *_ in _EVIDENCE_FLAGS}
|
|
|
952
963
|
_MANIFEST_EVIDENCE["input_af2"] = _MANIFEST_EVIDENCE["input_ai"]
|
|
953
964
|
|
|
954
965
|
|
|
966
|
+
def _job_override(spec: "dict", key: str, default):
|
|
967
|
+
"""Per-job manifest override: the job's own value when present (non-null),
|
|
968
|
+
else the batch-global ``default``.
|
|
969
|
+
|
|
970
|
+
Lets a manifest set ``best_of`` / ``seed`` / ``helix_prior`` / evidence knobs /
|
|
971
|
+
``out`` per job instead of one value shared by every job. A ``null`` (or absent)
|
|
972
|
+
field falls back to the global flag, so mixed manifests stay concise."""
|
|
973
|
+
if not isinstance(spec, dict):
|
|
974
|
+
return default
|
|
975
|
+
v = spec.get(key)
|
|
976
|
+
return default if v is None else v
|
|
977
|
+
|
|
978
|
+
|
|
979
|
+
def _job_best_of(spec: "dict", global_best_of) -> int:
|
|
980
|
+
"""A job's effective ``best_of`` (its own override, else the global), ≥ 1.
|
|
981
|
+
Used for the credit total and worker sizing as well as the fold itself."""
|
|
982
|
+
return max(1, int(_job_override(spec, "best_of", global_best_of)))
|
|
983
|
+
|
|
984
|
+
|
|
985
|
+
def _job_out_path(override: "str | None", out_dir: str, jid: str) -> str:
|
|
986
|
+
"""Resolve a job's output PDB path.
|
|
987
|
+
|
|
988
|
+
Default (no manifest ``out``): ``out_dir/<jid>/<jid>.pdb``. A manifest ``out``
|
|
989
|
+
field overrides it, with the same feel as the single-fold ``--out``: a *file*
|
|
990
|
+
path (``run1/my.pdb``) sets the directory + stem for the whole bundle (report,
|
|
991
|
+
HTML, CSVs re-stem to it); a *directory* (trailing separator or no extension)
|
|
992
|
+
drops ``<jid>.pdb`` inside. Relative paths resolve against the batch ``out_dir``."""
|
|
993
|
+
if not override:
|
|
994
|
+
return os.path.join(out_dir, jid, f"{jid}.pdb")
|
|
995
|
+
p = override if os.path.isabs(override) else os.path.join(out_dir, override)
|
|
996
|
+
if override.endswith(("/", "\\")) or not os.path.splitext(p)[1]:
|
|
997
|
+
return os.path.join(p, f"{jid}.pdb")
|
|
998
|
+
return p
|
|
999
|
+
|
|
1000
|
+
|
|
955
1001
|
def fold_batch(
|
|
956
1002
|
manifest_path: str,
|
|
957
1003
|
out_dir: str,
|
|
@@ -979,8 +1025,14 @@ def fold_batch(
|
|
|
979
1025
|
an optional ``distogram_artifact``, any ``input_<name>`` evidence fields
|
|
980
1026
|
(path or list; relative paths resolve against the manifest dir), and an optional
|
|
981
1027
|
``held_out_input`` path (independent contacts scored post-fold into that job's
|
|
982
|
-
report — §C5).
|
|
983
|
-
evidence params / held-out threshold+neff / ``encrypt``)
|
|
1028
|
+
report — §C5). The function arguments (``best_of`` / ``seed`` / ``disto_max_bin`` /
|
|
1029
|
+
helix / evidence params / held-out threshold+neff / ``encrypt``) are the batch-wide
|
|
1030
|
+
DEFAULTS; **any job may override them per-id** by carrying the same-named field
|
|
1031
|
+
(``best_of``, ``seed``, ``helix_prior``, ``helix_prior_ensemble``, ``disto_max_bin``,
|
|
1032
|
+
``template_sigma``, ``noe_mode``, ``soi_fill``, ``held_out_threshold``,
|
|
1033
|
+
``held_out_msa_neff``, ``encrypt``). A per-job ``out`` field further overrides where
|
|
1034
|
+
that job writes (a file path sets the whole bundle's dir+stem; a directory drops
|
|
1035
|
+
``<id>.pdb`` inside; relative to ``out_dir``). Absent overrides default to
|
|
984
1036
|
``out_dir/<id>/<id>.{pdb,json}``; the batch aggregate is returned.
|
|
985
1037
|
|
|
986
1038
|
Metering mirrors the single fold, per job: each sequence reserves ``best_of``
|
|
@@ -1034,9 +1086,14 @@ def fold_batch(
|
|
|
1034
1086
|
if held_out_msa_neff is not None:
|
|
1035
1087
|
held_out_params["msa_neff"] = float(held_out_msa_neff)
|
|
1036
1088
|
|
|
1037
|
-
|
|
1089
|
+
# Encryption may be global (``--encrypt``) OR requested by any single job (a
|
|
1090
|
+
# manifest ``encrypt: true`` field). Load the envelope + enclave key if EITHER
|
|
1091
|
+
# asks for it; each job then seals (or not) per its own effective flag.
|
|
1092
|
+
any_encrypt = encrypt or any(
|
|
1093
|
+
isinstance(s, dict) and bool(s.get("encrypt")) for s in jobs_spec)
|
|
1094
|
+
env = _import_envelope() if any_encrypt else None
|
|
1038
1095
|
enclave_pub = None
|
|
1039
|
-
if
|
|
1096
|
+
if any_encrypt:
|
|
1040
1097
|
if not trusted_pub:
|
|
1041
1098
|
raise FoldError("attestation is missing enclave_pubkey; cannot encrypt")
|
|
1042
1099
|
enclave_pub = env.load_public_key_b64(trusted_pub)
|
|
@@ -1044,6 +1101,7 @@ def fold_batch(
|
|
|
1044
1101
|
batch: list[dict] = []
|
|
1045
1102
|
cust_keys: dict[str, object] = {} # jid -> customer private key (encrypt)
|
|
1046
1103
|
reservations: dict[str, str] = {} # jid -> reservation_id (metering)
|
|
1104
|
+
job_out_paths: dict[str, str] = {} # jid -> resolved output PDB path
|
|
1047
1105
|
try:
|
|
1048
1106
|
for idx, spec in enumerate(jobs_spec):
|
|
1049
1107
|
if not isinstance(spec, dict) or not spec.get("sequence"):
|
|
@@ -1078,30 +1136,55 @@ def fold_batch(
|
|
|
1078
1136
|
f"job '{jid}': blind (FASTA) input needs evidence "
|
|
1079
1137
|
"(an input_<name> field) or a distogram_artifact")
|
|
1080
1138
|
|
|
1139
|
+
# ── Per-job overrides (a manifest field beats the batch-global flag) ──
|
|
1140
|
+
job_best_of = _job_best_of(spec, best_of)
|
|
1141
|
+
job_seed = _job_override(spec, "seed", seed)
|
|
1142
|
+
job_helix = bool(_job_override(spec, "helix_prior", helix_prior))
|
|
1143
|
+
job_helix_ens = bool(_job_override(
|
|
1144
|
+
spec, "helix_prior_ensemble", helix_prior_ensemble))
|
|
1145
|
+
job_disto_max_bin = _job_override(spec, "disto_max_bin", disto_max_bin)
|
|
1146
|
+
job_encrypt = bool(_job_override(spec, "encrypt", encrypt))
|
|
1147
|
+
# Evidence / held-out knobs: start from the batch-global dict, then let
|
|
1148
|
+
# the job override individual keys.
|
|
1149
|
+
job_ev_params = dict(evidence_params)
|
|
1150
|
+
if spec.get("template_sigma") is not None:
|
|
1151
|
+
job_ev_params["template_sigma"] = float(spec["template_sigma"])
|
|
1152
|
+
if spec.get("noe_mode") is not None:
|
|
1153
|
+
job_ev_params["noe_mode"] = spec["noe_mode"]
|
|
1154
|
+
if spec.get("soi_fill") is not None:
|
|
1155
|
+
job_ev_params["soi_fill"] = spec["soi_fill"]
|
|
1156
|
+
job_ho_params = dict(held_out_params)
|
|
1157
|
+
if spec.get("held_out_threshold") is not None:
|
|
1158
|
+
job_ho_params["threshold"] = float(spec["held_out_threshold"])
|
|
1159
|
+
if spec.get("held_out_msa_neff") is not None:
|
|
1160
|
+
job_ho_params["msa_neff"] = float(spec["held_out_msa_neff"])
|
|
1161
|
+
job_out_paths[jid] = _job_out_path(
|
|
1162
|
+
_job_override(spec, "out", None), out_dir, jid)
|
|
1163
|
+
|
|
1081
1164
|
args_obj = {
|
|
1082
|
-
"best_of":
|
|
1165
|
+
"best_of": job_best_of,
|
|
1083
1166
|
"distogram_artifact_b64": distogram_b64,
|
|
1084
|
-
"helix_prior":
|
|
1085
|
-
"helix_prior_ensemble": bool(
|
|
1167
|
+
"helix_prior": job_helix,
|
|
1168
|
+
"helix_prior_ensemble": bool(job_helix and job_helix_ens),
|
|
1086
1169
|
}
|
|
1087
|
-
if
|
|
1088
|
-
args_obj["seed"] = int(
|
|
1089
|
-
if
|
|
1090
|
-
args_obj["disto_max_bin"] = str(
|
|
1091
|
-
if
|
|
1092
|
-
args_obj["evidence_params"] =
|
|
1093
|
-
if
|
|
1094
|
-
args_obj["held_out_params"] =
|
|
1170
|
+
if job_seed is not None:
|
|
1171
|
+
args_obj["seed"] = int(job_seed)
|
|
1172
|
+
if job_disto_max_bin is not None:
|
|
1173
|
+
args_obj["disto_max_bin"] = str(job_disto_max_bin)
|
|
1174
|
+
if job_ev_params:
|
|
1175
|
+
args_obj["evidence_params"] = job_ev_params
|
|
1176
|
+
if job_ho_params:
|
|
1177
|
+
args_obj["held_out_params"] = job_ho_params
|
|
1095
1178
|
|
|
1096
1179
|
fold_token = None
|
|
1097
1180
|
if metering_on:
|
|
1098
1181
|
try:
|
|
1099
1182
|
res = metering_mod.reserve(
|
|
1100
|
-
backend, api_key, credits=
|
|
1183
|
+
backend, api_key, credits=job_best_of,
|
|
1101
1184
|
idempotency_key=str(uuid.uuid4()), input_hash=input_hash)
|
|
1102
1185
|
except metering_mod.MeteringError as exc:
|
|
1103
1186
|
raise FoldError(
|
|
1104
|
-
f"job '{jid}': could not reserve {
|
|
1187
|
+
f"job '{jid}': could not reserve {job_best_of} credit(s): "
|
|
1105
1188
|
f"{exc.detail}", status=exc.status, error=str(exc)) from exc
|
|
1106
1189
|
fold_token = res.get("fold_token")
|
|
1107
1190
|
rid = res.get("reservation_id")
|
|
@@ -1112,7 +1195,7 @@ def fold_batch(
|
|
|
1112
1195
|
reservations[jid] = rid
|
|
1113
1196
|
|
|
1114
1197
|
job_el: dict = {"id": jid, "fold_token": fold_token}
|
|
1115
|
-
if
|
|
1198
|
+
if job_encrypt:
|
|
1116
1199
|
cust_priv, _pub = env.generate_keypair()
|
|
1117
1200
|
cust_keys[jid] = cust_priv
|
|
1118
1201
|
job_el["customer_pubkey"] = env.public_b64(cust_priv)
|
|
@@ -1157,7 +1240,8 @@ def fold_batch(
|
|
|
1157
1240
|
jid = r.get("id") or "job"
|
|
1158
1241
|
st = r.get("status")
|
|
1159
1242
|
if st == "ok":
|
|
1160
|
-
|
|
1243
|
+
# Decrypt per job: only jobs that sealed their input carry a private key.
|
|
1244
|
+
if r.get("result_envelope") is not None and jid in cust_keys:
|
|
1161
1245
|
blob = _join_envelope(r["result_envelope"])
|
|
1162
1246
|
inner = json.loads(env.decrypt(blob, cust_keys[jid]).decode("utf-8"))
|
|
1163
1247
|
pdb_b64 = inner.get("pdb_b64")
|
|
@@ -1173,14 +1257,15 @@ def fold_batch(
|
|
|
1173
1257
|
n_failed += 1
|
|
1174
1258
|
print(f"[batch] {jid}: ok but no PDB — skipping write", file=sys.stderr)
|
|
1175
1259
|
continue
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
job_pdb = os.path.join(
|
|
1260
|
+
# Per-job output path (manifest ``out`` field) or the default subdir;
|
|
1261
|
+
# its directory is created if it does not exist (like the single fold).
|
|
1262
|
+
job_pdb = job_out_paths.get(jid) or os.path.join(out_dir, jid, f"{jid}.pdb")
|
|
1263
|
+
os.makedirs(os.path.dirname(os.path.abspath(job_pdb)) or ".", exist_ok=True)
|
|
1179
1264
|
with open(job_pdb, "wb") as fh:
|
|
1180
1265
|
fh.write(base64.b64decode(pdb_b64))
|
|
1181
|
-
with open(os.path.
|
|
1266
|
+
with open(os.path.splitext(job_pdb)[0] + ".json", "w", encoding="utf-8") as fh:
|
|
1182
1267
|
json.dump(report_json, fh, indent=2)
|
|
1183
|
-
# Value layer for this job (re-stemmed to
|
|
1268
|
+
# Value layer for this job (re-stemmed to the job PDB name, same dir).
|
|
1184
1269
|
_write_artifacts(artifacts, artifacts_stem, job_pdb)
|
|
1185
1270
|
n_ok += 1
|
|
1186
1271
|
else:
|
|
@@ -1267,10 +1352,14 @@ TYPED EVIDENCE (all repeatable; different types combine freely)
|
|
|
1267
1352
|
(a pre-built --distogram-artifact is used as-is and skips all of the above.)
|
|
1268
1353
|
|
|
1269
1354
|
BATCH MANIFEST (jobs.json) - a JSON array; paths resolve against the manifest dir:
|
|
1270
|
-
[ { "id": "P1", "sequence": "p1.pdb", "input_template": ["h.pdb"]
|
|
1355
|
+
[ { "id": "P1", "sequence": "p1.pdb", "input_template": ["h.pdb"],
|
|
1356
|
+
"best_of": 8, "helix_prior": true, "out": "hard/P1.pdb" },
|
|
1271
1357
|
{ "sequence": "p2.fasta", "distogram_artifact": "p2.distogram.json" } ]
|
|
1272
|
-
|
|
1273
|
-
|
|
1358
|
+
The CLI flags (--best-of/--seed/--helix-prior/evidence knobs/--encrypt) are the
|
|
1359
|
+
batch-wide DEFAULTS; any job may override them per-id with a same-named field
|
|
1360
|
+
(best_of, seed, helix_prior, helix_prior_ensemble, disto_max_bin, template_sigma,
|
|
1361
|
+
noe_mode, soi_fill, held_out_threshold, held_out_msa_neff, encrypt). A per-job
|
|
1362
|
+
"out" also overrides where that job writes. Default output: <--out>/<id>/<id>.{pdb,json}.
|
|
1274
1363
|
|
|
1275
1364
|
GOOD TO KNOW
|
|
1276
1365
|
- Billing is enforced server-side: the enclave refuses any fold without a
|
|
@@ -1339,11 +1428,18 @@ BATCH (many sequences, one request)
|
|
|
1339
1428
|
|
|
1340
1429
|
fold_batch(
|
|
1341
1430
|
"jobs.json", "results/", # manifest + results root
|
|
1342
|
-
best_of=2, encrypt=False, #
|
|
1431
|
+
best_of=2, encrypt=False, # batch-wide DEFAULTS (any job may override)
|
|
1343
1432
|
)
|
|
1344
1433
|
# Manifest is a JSON array; per-job keys: sequence, id, distogram_artifact,
|
|
1345
1434
|
# input_<name> evidence fields, held_out_input. Paths resolve against the
|
|
1346
1435
|
# manifest dir. Outputs go to results/<id>/<id>.{pdb,json}.
|
|
1436
|
+
#
|
|
1437
|
+
# PER-JOB OVERRIDES — a job may carry any of these to beat the batch default:
|
|
1438
|
+
# best_of, seed, helix_prior, helix_prior_ensemble, disto_max_bin,
|
|
1439
|
+
# template_sigma, noe_mode, soi_fill, held_out_threshold, held_out_msa_neff,
|
|
1440
|
+
# encrypt, and `out` (a file path -> that job's dir+stem, or a directory).
|
|
1441
|
+
# e.g. {"id":"hard","sequence":"h.fasta","input_msa":"h.a3m",
|
|
1442
|
+
# "best_of":8,"helix_prior":true,"out":"hard/h.pdb"}
|
|
1347
1443
|
|
|
1348
1444
|
NOTES
|
|
1349
1445
|
- The dispatcher (default-on for the CLI) is a CLI-layer convenience; the SDK
|
|
@@ -1717,18 +1813,22 @@ def _print_batch_preview(manifest_path: str, args) -> None:
|
|
|
1717
1813
|
except (OSError, ValueError) as exc:
|
|
1718
1814
|
print(f"[error] cannot read manifest {manifest_path}: {exc}", file=sys.stderr)
|
|
1719
1815
|
return
|
|
1720
|
-
|
|
1721
|
-
|
|
1816
|
+
job_list = jobs if isinstance(jobs, list) else []
|
|
1817
|
+
n = len(job_list)
|
|
1818
|
+
credits = sum(_job_best_of(s, args.best_of) for s in job_list)
|
|
1722
1819
|
metered = not args.no_metering
|
|
1723
1820
|
print("Physis Fold — BATCH DRY RUN (nothing folded, no credits charged)")
|
|
1724
1821
|
print(f" manifest : {manifest_path} ({n} job(s))")
|
|
1725
|
-
for i, spec in enumerate(
|
|
1822
|
+
for i, spec in enumerate(job_list):
|
|
1726
1823
|
jid = (spec.get("id") if isinstance(spec, dict) else None) or f"job_{i}"
|
|
1727
1824
|
seq = spec.get("sequence", "?") if isinstance(spec, dict) else "?"
|
|
1728
|
-
|
|
1825
|
+
bo = _job_best_of(spec, args.best_of)
|
|
1826
|
+
out_ov = spec.get("out") if isinstance(spec, dict) else None
|
|
1827
|
+
extra = f"best-of {bo}" + (f", out {out_ov}" if out_ov else "")
|
|
1828
|
+
print(f" - {jid:<14} {seq:<28} ({extra})")
|
|
1729
1829
|
print(f" out (root) : {args.out or '<--out required to run>'}")
|
|
1730
|
-
print(f" best-of : {args.best_of} seed(s)")
|
|
1731
|
-
print(f" credit cost : {credits} credit(s)
|
|
1830
|
+
print(f" best-of : {args.best_of} seed(s) (global default; per-job overrides apply)")
|
|
1831
|
+
print(f" credit cost : {credits} credit(s) (sum of per-job best-of)"
|
|
1732
1832
|
+ ("" if metered else " [metering off - no charge]"))
|
|
1733
1833
|
print(" --> Re-run with --run to execute for real.")
|
|
1734
1834
|
|
|
@@ -1745,10 +1845,12 @@ def _command_foldings(args) -> int:
|
|
|
1745
1845
|
try:
|
|
1746
1846
|
with open(args.batch, encoding="utf-8") as fh:
|
|
1747
1847
|
jobs = json.load(fh)
|
|
1748
|
-
n = len(jobs) if isinstance(jobs, list) else 1
|
|
1749
1848
|
except (OSError, ValueError):
|
|
1750
|
-
|
|
1751
|
-
|
|
1849
|
+
return best_of
|
|
1850
|
+
if not isinstance(jobs, list) or not jobs:
|
|
1851
|
+
return best_of
|
|
1852
|
+
# Per-job best_of may differ; sum the effective foldings across all jobs.
|
|
1853
|
+
return sum(_job_best_of(s, best_of) for s in jobs)
|
|
1752
1854
|
return best_of
|
|
1753
1855
|
|
|
1754
1856
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "physistool"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.4"
|
|
8
8
|
description = "Open-source PhysisFold client — CLI + SDK for constraint-guided all-atom protein folding via a remote (Nitro Enclave) endpoint."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.9"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|