Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/50_LOOP_PLAN.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ Build the multi-resolution virtual assay layer: structure proxies, membrane inte
| 35 ✅ | No executable brake prevented future agents from enabling weighted simulation despite negative ablation results | `simulation/gate.py` + `openamp-foundry bench simulation-gate`: fail-closed permission gate consuming both current ablation artifacts. `make simulation-gate` writes a machine-readable verdict. | 6 tests added; current suite 1927 passing, 7 skipped. Current verdict blocks weighted mode and downgrades to info-only |
| 36 ✅ | No per-module cheap-baseline benchmark for within-domain simulation value | `scripts/benchmark_simulation_baselines.py`: compares each simulation signal (bacterial_binding, selectivity_ratio, helix_weight, non_helical) against its cheapest heuristic baseline (mean_eisenberg, selectivity_proxy, helix_propensity, proline_fraction). v0.5.58 | 13 tests, 1953 total. Honest finding: 0/4 signals beat baselines. All simulation modules remain permanently experimental. Weighted mode remains blocked |
| 37 ✅ | No `--simulation-mode` CLI flag. Simulation modules exist but users cannot access them | Added `--simulation-mode` to `rank` CLI: `off` (default) and `info` (runs MembraneProxy + StructureProxy, adds `sim_*` keys to scores, includes in JSONL/report). Weighted blocked by gate (Loop 35). v0.5.57 | 6 tests, 1940 total. Demo pipeline with `--simulation-mode info` produces sim scores for all 10 candidates. Report includes sim columns |
| 38 | No API contract for third-party simulation modules (e.g. Martini MD, AlphaFold) | `simulation/interfaces.py` extended with `ExternalSimulationAdapter` protocol. Documented in ARCHITECTURE.md extension points | Third-party can implement the protocol without reading pipeline internals |
| 38 ✅ | No API contract for third-party simulation modules (e.g. Martini MD, AlphaFold) | `simulation/interfaces.py` extended with `ExternalSimulationAdapter` protocol. ARCHITECTURE.md extension points documented. v0.5.59 | 13 tests, 1966 total. Adapter wraps any callable, checks availability, handles errors, propagates metadata |
| 39 | Virtual-assay benchmark report comparing simulation-augmented vs simulation-free ranking on the strict triage set | `docs/SIMULATION_BENCHMARK.md`: head-to-head comparison of sel_vs_dec, hem_vs_dec, sel_vs_hem AUROCs with and without simulation. If simulation doesn't improve strict triage, say so explicitly | Honest: if delta ≤ 0, report "simulation did not improve triage" |

**Phase 3 exit criteria:**
Expand Down Expand Up @@ -180,7 +180,7 @@ If no data arrives, virtual assay scaffolding continues independently.
Phase 0: ✅ Complete (Loops 1–8)
Phase 1: ✅ Complete (Loops 9–17)
Phase 2: ✅ Complete (Loops 18–29)
Phase 3: In progress — Loop 37 ✅ (Loops 30–39)
Phase 3: In progress — Loop 38 ✅ (Loops 30–39)
Phase 4: Not started (Loops 40–49)
```

Expand Down Expand Up @@ -241,7 +241,7 @@ Phase 4: Not started (Loops 40–49)
| 28 ✅ | Policy version bump workflow for when real data arrives | `scripts/bump_recalibration_policy.py`: standalone script with `--dry-run`, decision-log guard, auto-increment + write. CI guard in `ci.yml` validates policy version changes against base branch. v0.5.49. 9 tests. | CI rejects policy PRs without valid decision log; `make bump-policy-version` and `make bump-policy-version-dry-run` available |
| 29 ✅ | No public negative-result archive format. If Wave 1 yields all negatives, where do they go? | `docs/NEGATIVE_RESULT_ARCHIVE.md`: full template with entry schema, procedures, automation notes, and limitations. Covers pre-selection rejects, selected-untested, lab inactives, lab toxic, control failures. v0.5.50. | Template complete enough for a lab partner to fill; schema defines 18 fields with required/conditional markers |

**Next loop:** Loop 38 — Phase 3 (external adapter protocol).
**Next loop:** Loop 39 — Phase 3 (virtual-assay benchmark report).

**Phase 2 exit criteria (all 5 met ✅):**
- ✅ `make calibration-loop` runs from clean checkout, produces batch-2 manifest
Expand Down
34 changes: 33 additions & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@ Later external predictors should be added as adapters. Each adapter must return:

Adapters must not silently download model weights or send sequences to third-party services without explicit user consent.

Any future simulation or emulator module must implement the `VirtualAssayProxy` interface (in `openamp_foundry.simulation`) and return a `SimulationResult` object matching this schema:
### Internal simulation modules

Any future simulation or emulator module must implement `VirtualAssayProxy`
(in `openamp_foundry.simulation`) and return a `SimulationResult`:

```python
@dataclass
Expand All @@ -131,3 +134,32 @@ class SimulationResult:
```

If calibration data is absent or weak, the `uncertainty` field must surface that directly.

### External simulation adapters

Third-party tools (Martini MD, AlphaFold, REST APIs) can be integrated
via `ExternalSimulationAdapter`:

```python
from openamp_foundry.simulation import ExternalSimulationAdapter, SimulationResult

def run_martini(sequence: str) -> SimulationResult:
# ... call external tool, map output to SimulationResult ...
...

adapter = ExternalSimulationAdapter(
name="martini_membrane",
version="0.1.0",
simulate_fn=run_martini,
required_module="martini", # optional: fail gracefully if missing
scope=["membrane_binding"],
)
result = adapter.simulate("GIGKFLHSAKKFGKAFVGEIMNS")
```

The adapter:
- Wraps any callable `(str) -> SimulationResult` into the standard interface.
- Checks `is_available()` before running (avoids ImportError crashes).
- Returns `uncertainty=1.0` error result if the wrapped function raises.
- Propagates the adapter's `name`, `version`, and `scope` into the result.
- Falls back to `EmulatorBaseline` returning 0.5 for cheap-baseline comparison.
7 changes: 4 additions & 3 deletions docs/METRICS_CURRENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ Machine-readable snapshot: `outputs/metrics_snapshot.json` regenerated with `mak
> **Purpose:** One authoritative table of current pipeline metrics. If any doc disagrees
> with this file, this file wins. Updated whenever benchmark/benchmark config changes.
>
> **Last updated:** 2026-07-06 (simulation cheap-baseline benchmark — v0.5.58)
> **Last updated:** 2026-07-06 (external simulation adapter — v0.5.59)
> **New in v0.5.59:** `ExternalSimulationAdapter` protocol — wraps third-party callables into `VirtualAssayProxy`. Availability check, graceful error handling, metadata override. ARCHITECTURE.md docs updated. 13 tests. 1966 total.
> **New in v0.5.58:** Per-signal cheap-baseline comparison: 0/4 simulation signals beat their cheapest heuristic. All simulation modules remain permanently experimental. `make bench-simulation-baselines`. 13 tests. 1953 total.
> **New in v0.5.57:** `rank --simulation-mode info` runs MembraneProxy + StructureProxy on every candidate, adds `sim_*` scores to JSONL output and Markdown report. 6 new CLI tests. 1940 total.
> **New in v0.5.56:** `openamp-foundry bench simulation-gate` converts both
Expand Down Expand Up @@ -43,7 +44,7 @@ Machine-readable snapshot: `outputs/metrics_snapshot.json` regenerated with `mak
> **New in v0.5.31:** Added dipeptide-order features for sequence-order awareness. `dipeptide_order_score` achieves AUROC 0.7861 on AMP-vs-scrambled discrimination — the strongest order-dependent feature in the pipeline. Only 7/31 features survive scrambling (amphipathicity/helix-wheel + dipeptide). All composition features are purely position-independent (exactly 0.5000 AUROC on scrambled test).
> **New in v0.5.30:** Easy baseline benchmark added — charge density alone (AUROC 0.8166) outperforms the full pipeline ensemble (0.7792) on AMP-vs-Swiss-Prot-decoy discrimination. Honest finding documented: expected because pipeline optimizes for safety, not raw discrimination.
> **New in v0.5.29:** Expanded benchmark to 500 AMPs + 500 composition-matched decoys (n=1000). AUROC 0.7792 (CI₉₅: 0.7505–0.8065) confirms signal generalizes. Cluster-aware CI: 0.746–0.8102. Representative AUROC: 0.778. Standard benchmark (n=191) retained for backward comparison.
> **Pipeline version:** v0.5.58
> **Pipeline version:** v0.5.59
> **Branch:** main

---
Expand Down Expand Up @@ -1113,7 +1114,7 @@ Decoys score low on activity. Selective AMPs score moderately on both.

| Metric | Value |
|--------|-------|
| Total tests | 1953 |
| Total tests | 1966 |
| Coverage (branch) | 99% (6 CLI guard lines only) |
| Source modules at 100% | All pipeline, QC, scoring modules |

Expand Down
23 changes: 23 additions & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,28 @@
# Roadmap

## v0.5.59 — External Simulation Adapter Protocol (Loop 38) ✓ (2026-07-06)

API contract for third-party simulation modules (Martini MD, AlphaFold,
REST APIs). Completes Phase 3 exit criterion 5 (external adapter protocol).

Changes:
- ``src/openamp_foundry/simulation/interfaces.py`` — Added
``ExternalSimulationAdapter`` class wrapping any callable ``(str) ->
SimulationResult`` into the ``VirtualAssayProxy`` interface. Features:
optional ``required_module`` availability check, graceful error handling
(returns ``uncertainty=1.0`` on failure), metadata override (adapter's
name/version/scope applied to result), ``is_available()`` method,
``_error_result()`` helper.
- ``tests/test_external_adapter.py`` — 13 tests covering: availability
check, successful simulation, missing module, graceful failure,
exception handling, baseline fallback, metadata propagation, custom
scope, error result schema.
- ``docs/ARCHITECTURE.md`` — Extension points section expanded with
``ExternalSimulationAdapter`` usage example and documentation.
- ``docs/50_LOOP_PLAN.md`` — Loop 38 ✅. Next: Loop 39.
- ``docs/METRICS_CURRENT.md`` — Pipeline version v0.5.59. Test count: 1966.
- 1966 tests passing (1953 existing + 13 new).

## v0.5.58 — Simulation Cheap-Baseline Benchmark (Loop 36) ✓ (2026-07-06)

Per-signal comparison: does each simulation output beat its cheapest
Expand Down
4 changes: 2 additions & 2 deletions src/openamp_foundry/simulation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from .gate import SimulationGateVerdict, evaluate_simulation_gate
from .interfaces import (
EmulatorBaseline,
ExternalSimulationAdapter,
SimulationResult,
VirtualAssayProxy,
)
Expand All @@ -12,9 +13,8 @@
__all__ = [
"SimulationResult",
"VirtualAssayProxy",
"ExternalSimulationAdapter",
"EmulatorBaseline",
"MembraneProxy",
"StructureProxy",
"SimulationGateVerdict",
"evaluate_simulation_gate",
]
111 changes: 110 additions & 1 deletion src/openamp_foundry/simulation/interfaces.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
"""Interfaces and schemas for the OpenAMP virtual assay layer."""

import abc
import importlib
import sys
from dataclasses import dataclass, field
from typing import Dict, List, Optional
from typing import Callable, Dict, List, Optional


@dataclass
Expand Down Expand Up @@ -54,3 +56,110 @@ def simulate(self, sequence: str) -> SimulationResult:
def get_baseline(self) -> EmulatorBaseline:
"""Return the baseline heuristic this simulation must beat."""
pass


def _error_result(
module: str,
version: str,
error_msg: str,
) -> SimulationResult:
"""Create a SimulationResult for a failed external simulation."""
return SimulationResult(
module=module,
version=version,
scope=[],
scores={"error": -1.0},
uncertainty=1.0,
calibration_set=None,
validated_against=[],
notes=[f"External simulation failed: {error_msg}"],
)


class ExternalSimulationAdapter(VirtualAssayProxy):
"""Adapter for third-party external simulation modules.

Wraps an external simulation function (e.g., calling Martini MD,
AlphaFold, or a REST API) into the VirtualAssayProxy interface.

The wrapped function must accept (sequence: str) -> SimulationResult.
Fails gracefully with uncertainty=1.0 if the module errors.

Example::

def run_martini(seq: str) -> SimulationResult:
...

adapter = ExternalSimulationAdapter(
name="martini_membrane",
version="0.1.0",
simulate_fn=run_martini,
required_module="martini",
)
result = adapter.simulate("GIGKFLHSAKKFGKAFVGEIMNS")
"""

def __init__(
self,
name: str,
version: str,
simulate_fn: Callable[[str], SimulationResult],
required_module: str | None = None,
scope: list[str] | None = None,
) -> None:
self._name = name
self._version = version
self._simulate_fn = simulate_fn
self._required_module = required_module
self._scope = scope or ["external_simulation"]

def is_available(self) -> bool:
"""Check whether the external module can be imported.

Returns True if no module requirement is specified or the
module can be imported. This lets callers fail gracefully
when an external dependency is not installed.
"""
if self._required_module is None:
return True
try:
importlib.import_module(self._required_module)
return True
except ImportError:
return False

def simulate(self, sequence: str) -> SimulationResult:
if not self.is_available():
return _error_result(
self._name, self._version,
f"Required module '{self._required_module}' is not installed. "
f"Install it with: pip install {self._required_module}",
)
try:
result = self._simulate_fn(sequence)
result.module = self._name
result.version = self._version
result.scope = self._scope
return result
except Exception as exc:
return _error_result(
self._name, self._version,
f"Simulation raised {type(exc).__name__}: {exc}",
)

def get_baseline(self) -> EmulatorBaseline:
return _ExternalFallbackBaseline(self._name)

@property
def module_name(self) -> str:
return self._name


class _ExternalFallbackBaseline(EmulatorBaseline):
"""Fallback baseline for external modules: returns 0.5 (no signal)."""

def __init__(self, name: str) -> None:
self._name = name

def evaluate(self, sequence: str) -> float:
return 0.5
Loading
Loading