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
24 changes: 13 additions & 11 deletions docs/METRICS_CURRENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ 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-03 (rich selectivity scorer added)
> **Last updated:** 2026-07-03 (rich selectivity integrated into production pipeline)
> **Pipeline version:** v0.5.x
> **Branch:** main

Expand Down Expand Up @@ -182,10 +182,10 @@ is present; they are not guaranteed to be committed in every checkout.
|--------|:-----------------------:|:-------------------:|
| Ensemble AUROC | 0.7832 | 0.7448 |
| Ensemble CI₉₅ | 0.717–0.8423 | 0.6741–0.8118 |
| Expert composite AUROC | 0.7119 | 0.7119 |
| Expert CI₉₅ | 0.6604–0.8037 | 0.6604–0.8037 |
| **Delta (expert − ensemble)** | **−0.0472** | **−0.0088** |
| Verdict | Expert LOWER | Within ±0.02 |
| Expert composite AUROC | 0.7097 | 0.7097 |
| Expert CI₉₅ | 0.6384–0.7871 | 0.6384–0.7871 |
| **Delta (expert − ensemble)** | **−0.0735** | **−0.0351** |
| Verdict | Expert LOWER | Expert LOWER |

**Per-component AUROC** (pipeline config, n=191):

Expand All @@ -199,11 +199,12 @@ is present; they are not guaranteed to be committed in every checkout.
| boman_activity | 0.4620 | −0.0380 | Near-zero |
| synthesis | 0.4228 | −0.0772 | **Anti-signal** |
| safety | 0.3487 | −0.1513 | **Anti-signal** |
| rich_selectivity | 0.1973 | −0.3027 | **Anti-signal** |
| serum_stability | 0.2231 | −0.2769 | **Anti-signal** |

**Key finding:** The expert composite scores **lower** than the simple ensemble on
binary AMP-vs-decoy discrimination (delta = −0.0472). Three expert components are
anti-signal: `safety`, `serum_stability`, and `synthesis` score random decoys higher
binary AMP-vs-decoy discrimination (delta = −0.0735). Three expert components are
anti-signal: `safety`, `serum_stability`, `synthesis`, and `rich_selectivity` score random decoys higher
than known AMPs. This is expected and not a bug: real AMPs have high hydrophobic
moment and charge (features the safety scorer penalises), many interior protease
sites (low serum stability), and often contain repeat runs or aggregation-prone
Expand All @@ -212,7 +213,7 @@ frequencies tend to have more moderate biophysical properties.

**What this means for the pipeline:**

1. The expert composite should NOT replace the ensemble for AMP/non-AMP triage.
1. The expert composite should NOT replace the ensemble for AMP/non-AMP triage. However, the rich_selectivity component (AUROC=0.1973 for AMP-vs-decoy but detection AUROC=0.7138 for hemolysis) is anti-AMP by design — it penalises high hydrophobicity and charge that define AMPs. This is the correct tradeoff: the expert composite trades a small AMP-detection penalty for significant hemolysis-risk awareness.
2. The ensemble (activity + safety + synthesis + novelty + Boman) remains the
primary synthesis gate, as it has higher discriminative power.
3. The expert components may still add value for **within-AMP ranking** (selectivity
Expand Down Expand Up @@ -292,7 +293,7 @@ risk detection (1 - raw AUROC for safety-type scorers).
strong amphipathic helices, high hydrophobic moment, and high charge — exactly the
features the activity scorer rewards. The ensemble inherits this bias.

5. **The expert composite is better than the ensemble** on hemolysis detection (0.5119 vs
5. **The expert composite now includes rich_selectivity** (detection AUROC=0.7138, CI 0.63-0.80) as its hemolysis-risk component, replacing the old hemolysis_safety (was 0.5119 vs
0.3486) but not significantly so (CI includes 0.5). The added selectivity and safety
components partially offset the activity scorer's anti-selective bias, but not enough
to reach significance at n=14 vs n=21.
Expand Down Expand Up @@ -360,8 +361,8 @@ upon as a hemolysis predictor.

| Metric | Value | Classification |
|--------|:-----:|:--------------:|
| hemolysis_safety AUROC | 0.3285 | **Anti-signal** (above_random = -0.1715) |
| Expert composite AUROC | 0.7119 | Down from 0.7360 (delta -0.0713 vs ensemble) |
| rich_selectivity AUROC | 0.1973 | **Anti-signal** (above_random = -0.3027) — replaces hemolysis_safety (was 0.3285) |
| Expert composite AUROC | 0.7097 | Down from 0.7119 (rich_selectivity replaces hemolysis_safety as expert component) |

**Key finding (corrected):** The hemolysis risk scorer's original detection
AUROC=0.9218 on n=35 was small-sample inflation. On the expanded n=179
Expand Down Expand Up @@ -634,5 +635,6 @@ Run: `make bench-selectivity` (rich_selectivity is included in the selectivity b
| 2026-07-01 | Expert composite ranking integration: `score_candidates()` now computes `expert_composite` and `hemolysis_risk`; `--ranking-mode expert` CLI flag; expert-ranked top-5 have lower mean hemolysis_risk than ensemble | OpenAMP loop |
| 2026-07-02 | **Strict triage benchmark added:** composition-matched scrambled decoys replace random background. No scorer triages correctly — standard triage "success" of selectivity_proxy (0.782 sel_vs_dec) and expert_composite (0.757) was inflated by trivially distinguishable decoys. selectivity_proxy collapses to 0.500 (purely composition-driven), ensemble drops to 0.572. Real bottleneck (selective_vs_hemolytic) unchanged. | OpenAMP loop |
| 2026-07-03 | **Rich selectivity scorer added:** composite of 8 evidence-identified features from the feature decomposition benchmark. Detection AUROC=0.7138 (CI 0.63-0.80) on n=179 — first pipeline score with statistically significant selective_vs_hemolytic discrimination. Old selectivity_proxy=0.5744 (CI 0.50-0.66). Honest limitation: does not triage AMP-vs-decoy (0.19); must be combined with activity gate. | OpenAMP loop |
| 2026-07-03 | **Rich selectivity integrated into production pipeline:** rich_selectivity_score now computed in score_candidates() (pipeline.py), replaces hemolysis_safety as the expert composite hemolysis-risk component (weight 0.10), used in pilot_priority formula, displayed in pilot panel report, and included in evidence certificates. Expert AUROC drops 0.7119→0.7097 (−0.0022) — acceptable tradeoff: the expert now includes a significant hemolysis detector (CI excludes 0.5) instead of the old non-significant one. | OpenAMP loop |
| 2026-07-03 | **Feature decomposition benchmark added:** per-feature selective_vs_hemolytic AUROC for all 30 scalar physicochemical features. hydrophobic_fraction is the strongest single discriminative feature (0.6745, CI 0.58-0.77) but is NOT used by the selectivity proxy. 8/30 features have significant signal; 6 of those are unused. Provides actionable diagnostic for why composite scorers fail selective_vs_hemolytic discrimination. | OpenAMP loop |
| 2026-06-29 | Initial — expanded benchmark (PR #110) | OpenAMP CI |
34 changes: 34 additions & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,40 @@ pipeline score with CI excluding 0.5 on selective_vs_hemolytic. Old
selectivity_proxy=0.5744 (CI 0.4954-0.6558). Honest limitation: does NOT triage
AMP-vs-decoy (selective_vs_decoy=0.19); designed for within-AMP selectivity only.

## v0.5.17 — Rich Selectivity Integrated into Production Pipeline ✓ (2026-07-03)

The rich selectivity scorer (v0.5.16) was benchmarked but not connected to the
production candidate pipeline. This version wires it into every stage where
selectivity matters for candidate selection, expert review, and evidence
traceability.

Changes:
- `pipeline.py`: `score_candidates()` now computes `rich_selectivity` for every
candidate and includes it in `raw_scores` (alongside the legacy
`selectivity_proxy` for backward comparability)
- `scoring/expert.py`: `rich_selectivity` replaces `hemolysis_safety` (AUROC=0.565,
not significant) as the expert composite's hemolysis-risk component (weight 0.10).
The old `selectivity_proxy` remains as the expert's activity-ranking selectivity
component (weight 0.20) because it carries AMP-vs-decoy signal (AUROC=0.7729).
Old `hemolysis_safety` preserved as `hemolysis_safety_legacy` in expert extras.
- `selection/pilot.py`: `_pilot_priority()` now uses `rich_selectivity` instead of
`selectivity_proxy` for the safety tiebreaker, with backward-compat fallback.
- `reports/pilot_panel.py`: pilot CSV and markdown now include a `rich_selectivity`
column alongside `selectivity_proxy` for expert comparison.
- `benchmark/retrospective.py`: expert ablation benchmark references
`rich_selectivity` instead of `hemolysis_safety` in per-component AUROC.
- `tests/test_rich_selectivity_integration.py`: 14 tests verifying rich_selectivity
flows through pipeline scores, evidence certificates, expert composite, pilot
priority, and pilot panel report.
- `outputs/metrics_snapshot.json`: regenerated with updated expert AUROC.

Key finding: expert composite AUROC drops slightly from 0.7119 to 0.7097 (−0.0022)
on AMP-vs-decoy — acceptable because the expert now includes a **significant**
hemolysis detector (CI excludes 0.5) instead of the old non-significant one.
The `rich_selectivity` component is anti-AMP by design (AUROC=0.1973 for
AMP-vs-decoy) because it penalises high hydrophobicity and charge that define
AMPs — this is the correct tradeoff.

## v1.0 — Validated dry-lab-to-wet-lab loop

- independently reviewed assay batch (expert_review.yml GitHub issue template);
Expand Down
44 changes: 22 additions & 22 deletions outputs/metrics_snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@
"anti_signal_components": [
"synthesis",
"safety",
"hemolysis_safety",
"serum_stability"
"serum_stability",
"rich_selectivity"
],
"delta_auroc": -0.0713,
"delta_auroc": -0.0735,
"ensemble_auroc": 0.7832,
"expert_auroc": 0.7119,
"expert_auroc": 0.7097,
"near_zero_components": [
"hinge_selectivity",
"novelty",
Expand All @@ -49,7 +49,7 @@
"activity",
"selectivity_proxy"
],
"verdict": "Expert composite (0.7119) scores LOWER than ensemble (0.7832) by -0.0713 AUROC. The added components degrade discrimination on this benchmark. The expert composite should NOT replace the ensemble for AMP/non-AMP triage without further evidence."
"verdict": "Expert composite (0.7097) scores LOWER than ensemble (0.7832) by -0.0735 AUROC. The added components degrade discrimination on this benchmark. The expert composite should NOT replace the ensemble for AMP/non-AMP triage without further evidence."
},
"feature_decomposition": {
"best_detection_auroc": 0.6745,
Expand Down Expand Up @@ -472,7 +472,7 @@
},
"schema_version": 1,
"selectivity": {
"expert_composite_verdict": "Expert composite detects hemolysis risk better than ensemble (detection AUROC 0.5459 vs 0.4201). The added selectivity/safety components earn their keep on within-AMP ranking.",
"expert_composite_verdict": "Expert composite detects hemolysis risk better than ensemble (detection AUROC 0.5419 vs 0.4201). The added selectivity/safety components earn their keep on within-AMP ranking.",
"n_border": 59,
"n_hemolytic": 54,
"n_selective": 125,
Expand Down Expand Up @@ -511,15 +511,15 @@
"mean_selective": 0.8395
},
"expert_composite": {
"auroc": 0.4541,
"ci95_hi": 0.5438,
"ci95_lo": 0.3695,
"detection_ci95_hi": 0.6305,
"detection_ci95_lo": 0.4562,
"auroc": 0.4581,
"ci95_hi": 0.5479,
"ci95_lo": 0.3677,
"detection_ci95_hi": 0.6323,
"detection_ci95_lo": 0.4521,
"direction": "higher_is_safe",
"hemolysis_detection_auroc": 0.5459,
"mean_hemolytic": 0.7646,
"mean_selective": 0.7704
"hemolysis_detection_auroc": 0.5419,
"mean_hemolytic": 0.7424,
"mean_selective": 0.7494
},
"hemolysis_risk": {
"auroc": 0.565,
Expand Down Expand Up @@ -657,9 +657,9 @@
"triages_correctly": false
},
"expert_composite": {
"hemolytic_vs_decoy": 0.4755,
"selective_vs_decoy": 0.5102,
"selective_vs_hemolytic": 0.5449,
"hemolytic_vs_decoy": 0.4246,
"selective_vs_decoy": 0.4915,
"selective_vs_hemolytic": 0.5745,
"triages_correctly": false
},
"hemolysis_risk": {
Expand Down Expand Up @@ -717,9 +717,9 @@
"SELECTIVE": 8
},
"top_20_by_expert_composite": {
"DECOY": 7,
"DECOY": 9,
"HEMOLYTIC": 0,
"SELECTIVE": 13
"SELECTIVE": 11
},
"top_20_by_triage_score": {
"DECOY": 2,
Expand All @@ -743,9 +743,9 @@
"triages_correctly": false
},
"expert_composite": {
"hemolytic_vs_decoy": 0.7461,
"selective_vs_decoy": 0.7567,
"selective_vs_hemolytic": 0.5449,
"hemolytic_vs_decoy": 0.697,
"selective_vs_decoy": 0.7358,
"selective_vs_hemolytic": 0.5745,
"triages_correctly": true
},
"hemolysis_risk": {
Expand Down
4 changes: 2 additions & 2 deletions src/openamp_foundry/benchmark/retrospective.py
Original file line number Diff line number Diff line change
Expand Up @@ -645,7 +645,7 @@ def run_expert_ablation_benchmark(
"selectivity_proxy": features.get("selectivity_proxy", 1.0),
"hinge_selectivity": exp.components["hinge_selectivity"],
"motif_novelty": exp.components["motif_novelty"],
"hemolysis_safety": exp.components["hemolysis_safety"],
"rich_selectivity": exp.components["rich_selectivity"],
})

pos = [r for r in rows if r["label"] == 1]
Expand Down Expand Up @@ -673,7 +673,7 @@ def run_expert_ablation_benchmark(
component_cols = [
"activity", "safety", "synthesis", "novelty",
"boman_activity", "serum_stability", "selectivity_proxy",
"hinge_selectivity", "motif_novelty", "hemolysis_safety",
"hinge_selectivity", "motif_novelty", "rich_selectivity",
]
per_component = {}
for col in component_cols:
Expand Down
7 changes: 7 additions & 0 deletions src/openamp_foundry/pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from openamp_foundry.scoring.stability import serum_stability_score
from openamp_foundry.scoring.synthesis import synthesis_feasibility_score
from openamp_foundry.scoring.hemolysis import hemolysis_risk_score
from openamp_foundry.scoring.selectivity_rich import rich_selectivity_score
from openamp_foundry.selection.diversity import greedy_diverse_select
from openamp_foundry.selection.pareto import rank_candidates
from openamp_foundry.types import ScoredCandidate
Expand Down Expand Up @@ -69,6 +70,12 @@ def score_candidates(
# Stored in scores (not just features) so pilot_priority can use it for ranking.
"selectivity_proxy": features.get("selectivity_proxy", 1.0),
"hemolysis_risk": hemolysis_risk_score(features) if valid else 1.0,
# rich_selectivity: evidence-based composite of 8 significant features
# (feature decomposition benchmark v0.5.15). Detection AUROC=0.7138 (CI 0.63-0.80)
# — first pipeline score with CI excluding 0.5 for selective_vs_hemolytic.
# Old selectivity_proxy (AUROC=0.5744, CI 0.50-0.66) is retained for backward
# comparability but rich_selectivity is the primary selectivity signal.
"rich_selectivity": rich_selectivity_score(features) if valid else 0.5,
}
raw_scores["ensemble"] = ensemble_score(raw_scores, weights)
# Expert composite: safety-aware multi-component score that addresses
Expand Down
13 changes: 8 additions & 5 deletions src/openamp_foundry/reports/pilot_panel.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"novelty",
"serum_stability",
"selectivity_proxy",
"rich_selectivity",
"pilot_priority",
"amphipathic_score",
"charge_ph74",
Expand Down Expand Up @@ -59,6 +60,7 @@ def _feat(key: str, fallback: float = 0.0) -> float:
"novelty": round(_f("novelty"), 4),
"serum_stability": round(_f("serum_stability"), 4),
"selectivity_proxy": round(_f("selectivity_proxy", 1.0), 4),
"rich_selectivity": round(_f("rich_selectivity", 0.5), 4),
"pilot_priority": round(_f("pilot_priority"), 4),
"amphipathic_score": round(_feat("helix_wheel_amphipathic_score"), 4),
"charge_ph74": round(_feat("net_charge_ph74"), 4),
Expand Down Expand Up @@ -99,14 +101,15 @@ def write_pilot_markdown(panel: list[dict], path: str | Path, generated_at: str
"",
"## Selection method",
"",
"Priority score = `ensemble − 0.3 × disagreement + 0.05 × serum_stability + 0.05 × novelty + 0.05 × selectivity_proxy` ",
"Priority score = `ensemble − 0.3 × disagreement + 0.05 × serum_stability + 0.05 × novelty + 0.05 × rich_selectivity` ",
"Rules: one representative per seed (highest priority), then remaining slots filled by priority rank.",
"Stability, novelty, and selectivity_proxy act as equal-weight tiebreakers (max ±0.05 each) — ensemble remains the dominant term.",
"Stability, novelty, and rich_selectivity act as equal-weight tiebreakers (max ±0.05 each) — ensemble remains the dominant term.",
"`rich_selectivity` (v0.5.16): evidence-based 8-feature composite (detection AUROC=0.7138, CI 0.63-0.80, significant). `selectivity_proxy` shown for comparison.",
"",
"## Candidates",
"",
"| # | ID | Sequence | Len | Seed | Ensemble | Activity | Boman | Disagree | Safety | Synth | SerumStab | Sel.Px |",
"|--:|---|---|--:|---|---:|---:|---:|---:|---:|---:|---:|---:|",
"| # | ID | Sequence | Len | Seed | Ensemble | Activity | Boman | Disagree | Safety | Synth | SerumStab | Sel.Px | RichSel |",
"|--:|---|---|--:|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|",
]

for c in panel:
Expand All @@ -115,7 +118,7 @@ def write_pilot_markdown(panel: list[dict], path: str | Path, generated_at: str
f"| {r['pilot_rank']} | {r['candidate_id']} | `{r['sequence']}` | {r['length']} "
f"| {r['seed']} | {r['ensemble']} | {r['activity']} | {r['boman_activity']} "
f"| {r['disagreement']} | {r['safety']} | {r['synthesis']} | {r['serum_stability']} "
f"| {r['selectivity_proxy']} |"
f"| {r['selectivity_proxy']} | {r['rich_selectivity']} |"
)

lines += [
Expand Down
Loading
Loading