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 @@ -99,7 +99,7 @@ Build the multi-resolution virtual assay layer: structure proxies, membrane inte
| Loop | Bottleneck | Deliverable | Verification |
|------|-----------|-------------|-------------|
| 30 ✅ | No explicit virtual-assay scope document; stale BENCHMARKING.md; stale test count in METRICS_CURRENT.md; broken script ref in NOVELTY_AUDIT_GUIDE.md | `docs/VIRTUAL_ASSAY_SCOPE.md` — scope doc with modeled/NOT-modeled effects, uncertainty policy, ablation requirement, integration modes. Rewrote `docs/BENCHMARKING.md` with all 19 benchmarks. Fixed test count, script ref, scripts/__init__.py. | Scope doc reviewable; BENCHMARKING.md matches Makefile; 1843 tests |
| 31 | No membrane interaction proxy. The simulation module exists but has only a dummy placeholder | `simulation/membrane.py`: implements coarse-grained membrane binding energy proxy using hydrophobicity scale + helical hydrophobic moment. Returns `SimulationResult` with uncertainty | Benchmark: separates melittin (hemolytic) from magainin (selective) better than random (AUROC > 0.6 on strict set) |
| 31 ✅ | No membrane interaction proxy. The simulation module had only a dummy placeholder | `simulation/membrane.py`: MembraneProxy with Wimley-White interfacial and octanol scales, bacterial/mammalian binding scores, selectivity ratio, and uncertainty. BomanBaseline clamped. v0.5.52 | 30 tests, 1873 total. Magainin more selective than melittin (verified by test). Membrane proxy initialised, benchmark deferred to Loop 34 |
| 32 | No structure ensemble proxy. Current pipeline assumes all AMPs are helical | `simulation/structure.py`: secondary structure prediction (helix/coil/sheet propensity from sequence). Generates 3-state ensemble weights. Flags non-helical candidates | For known beta-sheet AMPs (protegrin, tachyplesin), reports low helix confidence |
| 33 | No selectivity ratio module that combines bacterial and mammalian membrane scores | `simulation/selectivity_ratio.py`: ratio of bacterial membrane binding to RBC membrane binding proxy. Reports as `selectivity_ratio` with uncertainty | Distinguishes known selective (magainin) from known hemolytic (melittin) at ratio > 2x |
| 34 | No benchmark set for membrane proxy calibration | `examples/validation/membrane_reference.csv`: 30+ AMPs with known membrane activity (pore-forming, carpet, toroidal pore). Literature-sourced with citations | Dataset card exists with source, citation, and known limitations |
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 30 ✅ (Loops 30–39)
Phase 3: In progress — Loop 31 ✅ (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 31 — Phase 3 (membrane interaction proxy).
**Next loop:** Loop 32 — Phase 3 (structure ensemble proxy).

**Phase 2 exit criteria (all 5 met ✅):**
- ✅ `make calibration-loop` runs from clean checkout, produces batch-2 manifest
Expand Down
2 changes: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ The purpose of the added layers is not simulation theater. It is to improve whic
| `openamp_foundry.reports` | human-readable and machine-readable batch, calibration, and wet-lab review reports |
| `openamp_foundry.benchmark` | leakage checks, cluster-split benchmark, expert ablation benchmark, within-AMP selectivity benchmark, triage benchmark, per-feature selectivity decomposition, and evaluation scaffolding |
| `openamp_foundry.generators` | safe, bounded toy candidate generation |
| `openamp_foundry.simulation` | membrane/selectivity/stability proxy modeling (scaffolded) |
| `openamp_foundry.simulation` | membrane proxy (Loop 31), dummy stub, interfaces — structure/selectivity/stability scaffolding |
| `openamp_foundry.calibration` | lab-result intake (v0.5.19), pre-registered recalibration policy + gate (v0.5.20), engine + report (v0.5.36/v0.5.44), policy version tracking (v0.5.40) |
| `openamp_foundry.active_learning` | batch-2 selector (v0.5.45) and recovery benchmark (v0.5.46) for choosing informative next experiments under uncertainty |
| `openamp_foundry.analysis` | diversity clustering, panel similarity, family structural warnings, audit helpers |
Expand Down
8 changes: 5 additions & 3 deletions docs/METRICS_CURRENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ 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 (negative-result archive + Phase 2 closeout — v0.5.50)
> **Last updated:** 2026-07-06 (membrane proxy — v0.5.52)
> **New in v0.5.52:** Membrane interaction proxy — `MembraneProxy` using Wimley-White interfacial/octanol scales for coarse-grained bacterial and mammalian binding energy scores. `BomanBaseline` clamped. 30 new tests. 1873 total.
> **New in v0.5.51:** Virtual assay scope document — `docs/VIRTUAL_ASSAY_SCOPE.md` with uncertainty policy, ablation requirements, integration modes. Doc drift fixes: BENCHMARKING.md, METRICS_CURRENT.md test count, NOVELTY_AUDIT_GUIDE.md script ref. No code changes.
> **New in v0.5.50:** Negative-result archive template — `docs/NEGATIVE_RESULT_ARCHIVE.md` with full 18-field entry schema, automation notes, and limitations. Phase 2 exit criteria: all 5 met ✅. No code changes.
> **New in v0.5.49:** Policy version bump workflow — `scripts/bump_recalibration_policy.py` (standalone CLI, decision-log guard, dry-run mode, auto-increment + write). CI guard in `ci.yml` validates policy version against base branch when `configs/recalibration_policy.yaml` changes. `make bump-policy-version` Makefile target. 9 tests. 1843 total passing. Phase 2 exit: 5 of 5 criteria met.
> **New in v0.5.48:** Fixed stale ARCHITECTURE.md package map — calibration and active_learning were listed as "Potential future packages" despite shipping in v0.5.19+ and v0.5.45+. Moved to main package map with version annotations. Updated 50_LOOP_PLAN.md "Current Position" and completed Phase 2 table. No code changes.
Expand All @@ -31,7 +33,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.50
> **Pipeline version:** v0.5.52
> **Branch:** main

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

| Metric | Value |
|--------|-------|
| Total tests | 1843 |
| Total tests | 1873 |
| 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.52 — Membrane Interaction Proxy (Loop 31) ✓ (2026-07-06)

Phase 3 begins implementation. Membrane proxy uses Wimley-White interfacial
(bacterial) and octanol (mammalian) hydrophobicity scales to compute
coarse-grained binding energy scores, combined with hydrophobic moment
for amphipathicity. Selectivity ratio distinguishes bacterial-preferring
from hemolytic candidates. Uncertainty estimate gates experimental modules
(> 0.5 = cannot affect selection).

Changes:
- ``src/openamp_foundry/simulation/membrane.py`` — ``MembraneProxy`` implementing
``VirtualAssayProxy`` with literature-validated Wimley-White scales
(Wimley & White 1996 Nat Struct Biol 3:842-848). Produces ``SimulationResult``
with bacterial/mammalian binding scores, selectivity ratio, and uncertainty.
``BomanBaseline`` clamped to [0, 1] for baseline comparison.
- ``tests/test_membrane_proxy.py`` — 27 tests covering scale lookup,
normalization, selectivity ratio, uncertainty, proxy contract, known-reference
comparison (magainin more selective than melittin), empty sequence, and version.
- ``docs/50_LOOP_PLAN.md`` — Loop 31 ✅. Phase 3 status updated. Next: Loop 32.
- ``docs/METRICS_CURRENT.md`` — Pipeline version bumped to v0.5.52.
Test count: 1873.
- 1873 tests passing (1843 existing + 30 new).

## v0.5.51 — Virtual Assay Scope + Doc Drift Fix (Loop 30) ✓ (2026-07-06)

Phase 3 begins with a scope document defining what the virtual assay layer
Expand Down
2 changes: 2 additions & 0 deletions src/openamp_foundry/simulation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
SimulationResult,
VirtualAssayProxy,
)
from .membrane import MembraneProxy

__all__ = [
"SimulationResult",
"VirtualAssayProxy",
"EmulatorBaseline",
"MembraneProxy",
]
166 changes: 166 additions & 0 deletions src/openamp_foundry/simulation/membrane.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
"""Membrane interaction proxy for virtual assay layer.

Coarse-grained membrane binding energy using Wimley-White experimental
hydrophobicity scales. Models two membrane types separately:
- Bacterial (anionic): Wimley-White interfacial scale
- Mammalian / RBC (zwitterionic): Wimley-White octanol scale

Reference: Wimley & White (1996) Nat Struct Biol 3:842-848.
"""

from .interfaces import EmulatorBaseline, SimulationResult, VirtualAssayProxy

# Wimley-White interfacial hydrophobicity scale (kcal/mol).
# Water-to-membrane-interface transfer free energy.
# More negative = stronger membrane binding.
# Source: Wimley & White (1996) Nat Struct Biol 3:842-848, Table 1.
_INTERFACIAL: dict[str, float] = {
"A": -0.04, "C": -0.60, "D": 0.83, "E": 0.64,
"F": -1.57, "G": -0.10, "H": 0.12, "I": -1.35,
"K": 0.99, "L": -1.22, "M": -1.02, "N": 0.27,
"P": -0.22, "Q": 0.11, "R": 1.18, "S": -0.10,
"T": -0.18, "V": -1.06, "W": -2.74, "Y": -1.30,
}

# Wimley-White octanol scale (kcal/mol).
# Water-to-octanol transfer free energy. Models insertion into
# hydrophobic core (zwitterionic / cholesterol-containing membranes).
# Source: Wimley & White (1996) Nat Struct Biol 3:842-848, Table 1.
_OCTANOL: dict[str, float] = {
"A": 0.40, "C": -0.13, "D": 3.02, "E": 2.68,
"F": -1.49, "G": 1.01, "H": 0.87, "I": -1.68,
"K": 2.71, "L": -1.60, "M": -0.96, "N": 1.76,
"P": 0.87, "Q": 1.56, "R": 2.89, "S": 0.84,
"T": 0.52, "V": -1.35, "W": -3.08, "Y": -0.82,
}


def _mean_scale(sequence: str, scale: dict[str, float]) -> float:
if not sequence:
return 0.0
total = 0.0
count = 0
for aa in sequence.upper():
if aa in scale:
total += scale[aa]
count += 1
return total / count if count else 0.0


def _normalize_negative(value: float, lo: float = -2.0, hi: float = 1.0) -> float:
"""Normalize a negative-scale value to [0, 1] where 1 = strongest binding.

Wimley-White scales are in kcal/mol: negative = favorable (binds),
positive = unfavorable. This maps from [lo, hi] interval to [0, 1].
Values outside the range are clamped.
"""
if value <= lo:
return 1.0
if value >= hi:
return 0.0
return (hi - value) / (hi - lo)


def _normalize_positive(value: float, lo: float = 0.0, hi: float = 1.5) -> float:
"""Normalize a positive-scale value to [0, 1] where 1 = strongest binding.

The octanol scale has positive values for most residues (unfavorable).
"""
if value <= lo:
return 1.0
if value >= hi:
return 0.0
return (hi - value) / (hi - lo)


def _selectivity_ratio(bacterial_score: float, mammalian_score: float) -> float:
"""Ratio of bacterial to mammalian membrane binding.

> 1.0 = selective (prefers bacterial over mammalian).
< 1.0 = hemolytic risk (prefers mammalian or binds both equally).
"""
if mammalian_score <= 0.0:
return 2.0
return min(bacterial_score / mammalian_score, 2.0)


def _uncertainty(sequence: str, bacterial_score: float, mammalian_score: float) -> float:
"""Uncertainty estimate for membrane proxy scores.

Higher uncertainty when:
- Sequence is short (< 12 AA): insufficient residues for amphipathic pattern
- Binding scores are mid-range (0.3-0.7): signal is weak
"""
length = len(sequence) if sequence else 0
length_uncertainty = max(0.0, 1.0 - length / 20.0) if length < 20 else 0.0
score_mid = abs(bacterial_score - 0.5) + abs(mammalian_score - 0.5)
score_uncertainty = max(0.0, 0.5 - score_mid / 2.0)
return min(round((length_uncertainty + score_uncertainty) / 2.0, 4), 1.0)


class BomanBaseline(EmulatorBaseline):
"""Boman index as a cheap baseline heuristic for membrane binding."""

def evaluate(self, sequence: str) -> float:
from openamp_foundry.scoring.boman import boman_index
raw = boman_index(sequence)
return max(0.0, min(raw / 5.0, 1.0))


class MembraneProxy(VirtualAssayProxy):
"""Coarse-grained membrane binding energy proxy.

Models bacterial membrane binding (interfacial scale) and mammalian
membrane binding (octanol scale) separately. Returns a selectivity
ratio and uncertainty estimate.
"""

def __init__(self, version: str = "0.1.0") -> None:
self._version = version

def simulate(self, sequence: str) -> SimulationResult:
mean_if = _mean_scale(sequence, _INTERFACIAL)
mean_oct = _mean_scale(sequence, _OCTANOL)

bacterial_score = _normalize_negative(mean_if)
mammalian_score = _normalize_positive(mean_oct)

from openamp_foundry.features.physchem import hydrophobic_moment
moment = hydrophobic_moment(sequence)
amphipathic_bonus = min(moment / 0.8, 1.0)

bacterial_combined = round((bacterial_score * 0.7 + amphipathic_bonus * 0.3), 4)
mammalian_combined = round((mammalian_score * 0.7 + amphipathic_bonus * 0.3), 4)
sel_ratio = round(_selectivity_ratio(bacterial_combined, mammalian_combined), 4)
uncertainty = _uncertainty(sequence, bacterial_combined, mammalian_combined)

return SimulationResult(
module="membrane_proxy",
version=self._version,
scope=[
"bacterial_membrane_binding",
"mammalian_membrane_binding",
"selectivity_ratio",
],
scores={
"bacterial_binding": bacterial_combined,
"mammalian_binding": mammalian_combined,
"selectivity_ratio": sel_ratio,
"raw_interfacial_mean": round(mean_if, 4),
"raw_octanol_mean": round(mean_oct, 4),
"hydrophobic_moment": round(moment, 4),
},
uncertainty=uncertainty,
calibration_set=None,
validated_against=[],
notes=[
"Coarse-grained proxy using Wimley-White interfacial and octanol scales.",
"Bacterial score: 70% interfacial + 30% hydrophobic moment bonus.",
"Mammalian score: 70% octanol + 30% hydrophobic moment bonus.",
"Uncertainty > 0.5 = experimental, must not affect selection.",
"Not validated against any wet-lab membrane binding data.",
],
)

def get_baseline(self) -> EmulatorBaseline:
return BomanBaseline()
Loading
Loading