Skip to content

Commit e240f28

Browse files
committed
[feat.motif.Omega.D9+] make target-bound physics and result-driven design executable
WHY: Dirac could schedule GPU work and model a DMTA loop, but it could not prove a real OpenFE edge or turn governed experimental measurements into a durable, recoverable next-round portfolio without crossing its existing Job and Artifact boundaries.
1 parent e9f65cd commit e240f28

26 files changed

Lines changed: 1615 additions & 21 deletions
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
-- 034 · Durable experimental-result trigger and stage ledger for Motif DMTA loops.
2+
3+
BEGIN;
4+
5+
CREATE TABLE design.motif_closed_loop_run (
6+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
7+
request_key text NOT NULL UNIQUE,
8+
specification_digest bytea NOT NULL UNIQUE CHECK (octet_length(specification_digest)=32),
9+
program_id uuid NOT NULL REFERENCES design.project(id) ON DELETE RESTRICT,
10+
campaign_id uuid NOT NULL REFERENCES design.campaign(id) ON DELETE RESTRICT,
11+
endpoint_key text NOT NULL,
12+
measurement_ids uuid[] NOT NULL CHECK (cardinality(measurement_ids) > 0),
13+
specification jsonb NOT NULL,
14+
state text NOT NULL DEFAULT 'pending'
15+
CHECK (state IN ('pending','running','blocked','completed','cancelled')),
16+
stage text NOT NULL DEFAULT 'snapshot'
17+
CHECK (stage IN ('snapshot','train','predict','acquire','completed')),
18+
stage_jobs jsonb NOT NULL DEFAULT '{}'::jsonb,
19+
outputs jsonb NOT NULL DEFAULT '{}'::jsonb,
20+
attention jsonb NOT NULL DEFAULT '{}'::jsonb,
21+
actor_kind app.actor_kind NOT NULL,
22+
actor_id text NOT NULL,
23+
created_at timestamptz NOT NULL DEFAULT now(),
24+
updated_at timestamptz NOT NULL DEFAULT now(),
25+
finished_at timestamptz,
26+
CHECK (finished_at IS NULL OR finished_at >= created_at)
27+
);
28+
29+
CREATE INDEX motif_closed_loop_claim_idx
30+
ON design.motif_closed_loop_run(state,updated_at)
31+
WHERE state IN ('pending','running');
32+
CREATE INDEX motif_closed_loop_campaign_idx
33+
ON design.motif_closed_loop_run(campaign_id,created_at DESC);
34+
35+
COMMENT ON TABLE design.motif_closed_loop_run IS
36+
'Durable, replayable result->snapshot->train->predict->acquire controller state. '
37+
'Each scientific stage remains an ordinary Dirac Job; this table owns only orchestration.';
38+
39+
INSERT INTO meta.migration (filename,sha256,content_sha256,hash_source)
40+
VALUES ('034_motif_closed_loop_controller.sql','\x17feb7ca24780b808b1bea2d34bd1daf6c8bd5fe9a64d9cf02b562990916cfa5'::bytea,
41+
'\x17feb7ca24780b808b1bea2d34bd1daf6c8bd5fe9a64d9cf02b562990916cfa5'::bytea,'content')
42+
ON CONFLICT (filename) DO NOTHING;
43+
44+
COMMIT;
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
-- 035 · Make blocked Motif closed loops explicitly and durably retryable.
2+
3+
BEGIN;
4+
5+
ALTER TABLE design.motif_closed_loop_run
6+
ADD COLUMN stage_attempts jsonb NOT NULL DEFAULT '{}'::jsonb;
7+
8+
INSERT INTO meta.migration (filename,sha256,content_sha256,hash_source)
9+
VALUES ('035_motif_closed_loop_retry.sql','\x01da2d20120ea2601c05090bad6d2371a520aca71d8beca06eaa9bbb422e76a0'::bytea,
10+
'\x01da2d20120ea2601c05090bad6d2371a520aca71d8beca06eaa9bbb422e76a0'::bytea,'content')
11+
ON CONFLICT (filename) DO NOTHING;
12+
13+
COMMIT;

‎backend/dirac_app/handlers.py‎

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ def physics_openmm_md(input: dict, ctx) -> dict:
181181
actor=ctx.actor, command_id=ctx.command_id)
182182

183183

184+
def physics_openfe_edge(input: dict, ctx) -> dict:
185+
return ctx.kernel.submit(
186+
'physics.motif.openfe_edge', input, request_id=ctx.request_id,
187+
actor=ctx.actor, command_id=ctx.command_id)
188+
189+
184190
def physics_rbfe_network(input: dict, ctx) -> dict:
185191
return ctx.kernel.submit(
186192
'physics.motif.rbfe_network', input, request_id=ctx.request_id,
@@ -228,7 +234,37 @@ def policy_release_register(input: dict, ctx) -> dict:
228234

229235

230236
def result_ingest(input: dict, ctx) -> dict:
231-
return _motif_governance(ctx).ingest_measurements(input["measurements"], ctx.actor)
237+
controller = getattr(ctx.kernel, "closed_loop_controller", None)
238+
loop_spec = input.get("closed_loop")
239+
if loop_spec is not None:
240+
if controller is None:
241+
raise failures.DiracFailure(
242+
"DB_UNAVAILABLE", "closed-loop controller is unavailable")
243+
controller.validate_spec(loop_spec, input["measurements"])
244+
controller.validate_context(loop_spec)
245+
result = _motif_governance(ctx).ingest_measurements(
246+
input["measurements"], ctx.actor)
247+
if loop_spec is not None:
248+
result["closed_loop"] = controller.enqueue(
249+
spec=loop_spec, measurements=input["measurements"],
250+
ingest_result=result, actor=ctx.actor)
251+
return result
252+
253+
254+
def campaign_closed_loop_get(input: dict, ctx) -> dict:
255+
controller = getattr(ctx.kernel, "closed_loop_controller", None)
256+
if controller is None:
257+
raise failures.DiracFailure(
258+
"DB_UNAVAILABLE", "closed-loop controller is unavailable")
259+
return controller.get(input["run_ref"]["id"])
260+
261+
262+
def campaign_closed_loop_retry(input: dict, ctx) -> dict:
263+
controller = getattr(ctx.kernel, "closed_loop_controller", None)
264+
if controller is None:
265+
raise failures.DiracFailure(
266+
"DB_UNAVAILABLE", "closed-loop controller is unavailable")
267+
return controller.retry(input["run_ref"]["id"])
232268

233269

234270
def _programs(ctx):

‎backend/executors/kubernetes_invocation.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,8 @@ def _request(self, ctx: InvocationContext, payload: dict, *, attempt_id: str,
160160
seed = int(payload.get("seed", 0))
161161
output_digest = sha256_digest(json.dumps(
162162
spec.output_schema, sort_keys=True, separators=(",", ":")))
163+
scale_profile = spec.execution.get("scale_profile") or {}
164+
cpu_cores = float(scale_profile.get("cpu_cores", 4))
163165
return {
164166
"schema_version": "1.0",
165167
"execution_id": str(uuid4()),
@@ -175,7 +177,7 @@ def _request(self, ctx: InvocationContext, payload: dict, *, attempt_id: str,
175177
"input_manifest_artifact_id": input_id,
176178
"output_contract_digest": output_digest,
177179
"resource_request": {
178-
"cpu_cores": 4,
180+
"cpu_cores": cpu_cores,
179181
"memory_bytes": 8 << 30,
180182
"gpus": 1,
181183
"gpu_arch": ["blackwell"],

‎backend/executors/kubernetes_kueue.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ class StaticPvcMount:
7979
claim_name: str
8080
mount_path: str
8181
read_only: bool = True
82+
sub_path: str | None = None
8283

8384
def __post_init__(self) -> None:
8485
for field in ("name", "claim_name"):
@@ -88,6 +89,11 @@ def __post_init__(self) -> None:
8889
path = PurePosixPath(self.mount_path)
8990
if not self.mount_path.startswith("/") or ".." in path.parts:
9091
raise ValueError("mount_path must be an absolute normalized path")
92+
if self.sub_path is not None:
93+
child = PurePosixPath(self.sub_path)
94+
if (not self.sub_path or self.sub_path.startswith("/")
95+
or ".." in child.parts or str(child) != self.sub_path):
96+
raise ValueError("sub_path must be a relative normalized path")
9197

9298

9399
class KubernetesKueueAdapter:
@@ -416,6 +422,8 @@ def _job_manifest(self, name: str, config_map: str,
416422
"name": mount.name,
417423
"mountPath": mount.mount_path,
418424
"readOnly": mount.read_only,
425+
**({"subPath": getattr(mount, "sub_path")}
426+
if getattr(mount, "sub_path", None) is not None else {}),
419427
} for mount in (*self.static_host_mounts,
420428
*self.static_pvc_mounts)],
421429
],

‎backend/kernel.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,9 @@ def default_executor():
8787
static_pvc_mounts=[
8888
StaticPvcMount('dirac-runtime', 'dirac-motif-runtime',
8989
str(worker_repository), True),
90+
StaticPvcMount(
91+
'dirac-posix-shell', 'dirac-motif-runtime', '/bin/sh', True,
92+
sub_path='runtime-bin/dash'),
9093
StaticPvcMount('dirac-exchange', 'dirac-motif-exchange',
9194
str(worker_exchange), False),
9295
])
@@ -340,4 +343,14 @@ def build(*, dsn: str = DEFAULT_DSN, with_versions: bool = True,
340343
svc.job_store_kind = getattr(js, 'kind', 'injected') # type: ignore[attr-defined]
341344
svc.job_durability = getattr(js, 'durability', 'unknown') # type: ignore[attr-defined]
342345
svc.executor_kind = getattr(ex, 'kind', 'injected') # type: ignore[attr-defined]
346+
try:
347+
import psycopg
348+
from motif.closed_loop import ClosedLoopController
349+
controller = ClosedLoopController(
350+
svc, lambda: psycopg.connect(dsn, autocommit=True))
351+
except Exception as error: # noqa: BLE001
352+
print(f'[kernel] closed-loop controller unavailable '
353+
f'({type(error).__name__}: {error})', file=sys.stderr, flush=True)
354+
controller = None
355+
svc.closed_loop_controller = controller # type: ignore[attr-defined]
343356
return svc

‎backend/method_registry.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,8 @@ def _qm_unit(kind: str, quantity: str, units: str) -> dict:
223223
['dock_vina', '_ligand_pdbqt', '_digest']),
224224
'physics.motif.openmm_md': ('motif.physics',
225225
['run_openmm_md', '_sha', '_digest']),
226+
'physics.motif.openfe_edge': ('motif.openfe_runner',
227+
['execute_openfe_edge', '_digest']),
226228
'physics.motif.rbfe_network': ('motif.rbfe',
227229
['plan_rbfe_network', '_chemistry', '_digest']),
228230
'physics.motif.rbfe_aggregate': ('motif.rbfe',
@@ -234,9 +236,7 @@ def _qm_unit(kind: str, quantity: str, units: str) -> dict:
234236
UNITS[_method_id] = {
235237
'module': _module,
236238
'fns': _functions,
237-
'consts': (['PREDICTOR_SOURCE_DIGESTS'] if _module == 'motif.mesh' else
238-
['ACQUISITION_SOURCE_DIGESTS']
239-
if _module == 'motif.advanced_acquisition' else []),
239+
'consts': list((_descriptor.get('implementation') or {}).get('constants', [])),
240240
'descriptor_path': str(_descriptor_path),
241241
'exec_class': 'job',
242242
'in_schema': _descriptor['input']['schema'],

0 commit comments

Comments
 (0)