docs: Day-5 close-out — AGENTS §8 decision layer + trainer CLI
Browse files- AGENTS.md +30 -0
- README.md +19 -0
- src/models/bbb_model.py +26 -0
AGENTS.md
CHANGED
|
@@ -50,6 +50,8 @@ All experiment runs are tracked in **MLflow**. All services ship as **Docker** i
|
|
| 50 |
│ │ ├── storage.py # Parquet read/write helpers (snappy, single-threaded, deterministic)
|
| 51 |
│ │ └── tracking.py # MLflow `track_pipeline_run` context manager (see §7)
|
| 52 |
│ ├── pipelines/ # One file per modality. Pure functions + a `run_pipeline()` entry.
|
|
|
|
|
|
|
| 53 |
│ └── frontend/
|
| 54 |
│ └── app.py # Streamlit dashboard (3 tabs, one per modality)
|
| 55 |
└── tests/
|
|
@@ -142,3 +144,31 @@ The tracking URI is read from `MLFLOW_TRACKING_URI` (defaults to `./mlruns/` whe
|
|
| 142 |
**Live-demo lifeline**: set `NEUROBRIDGE_DISABLE_MLFLOW=1` to skip tracking entirely — the helper yields `None` and emits no MLflow calls. Use this when the tracking server is unreachable (offline demo, network outage, or CI without an MLflow service). Pipelines complete normally; only the run metadata is lost.
|
| 143 |
|
| 144 |
The repo-wide `conftest.py` autouse fixture pins `MLFLOW_TRACKING_URI` to a tmp directory for the test session, so the production `mlruns/` directory is never written by the test suite. Tests that interact with MLflow (in `tests/core/test_tracking.py` and the per-pipeline `Test<Modality>PipelineMLflow` classes) all share this isolated store.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
│ │ ├── storage.py # Parquet read/write helpers (snappy, single-threaded, deterministic)
|
| 51 |
│ │ └── tracking.py # MLflow `track_pipeline_run` context manager (see §7)
|
| 52 |
│ ├── pipelines/ # One file per modality. Pure functions + a `run_pipeline()` entry.
|
| 53 |
+
│ ├── models/ # Downstream decision-layer models (consume processed features)
|
| 54 |
+
│ │ └── bbb_model.py # BBB-permeability classifier + SHAP explainer + trainer CLI
|
| 55 |
│ └── frontend/
|
| 56 |
│ └── app.py # Streamlit dashboard (3 tabs, one per modality)
|
| 57 |
└── tests/
|
|
|
|
| 144 |
**Live-demo lifeline**: set `NEUROBRIDGE_DISABLE_MLFLOW=1` to skip tracking entirely — the helper yields `None` and emits no MLflow calls. Use this when the tracking server is unreachable (offline demo, network outage, or CI without an MLflow service). Pipelines complete normally; only the run metadata is lost.
|
| 145 |
|
| 146 |
The repo-wide `conftest.py` autouse fixture pins `MLFLOW_TRACKING_URI` to a tmp directory for the test session, so the production `mlruns/` directory is never written by the test suite. Tests that interact with MLflow (in `tests/core/test_tracking.py` and the per-pipeline `Test<Modality>PipelineMLflow` classes) all share this isolated store.
|
| 147 |
+
|
| 148 |
+
## 8. Decision Layer (Downstream Models)
|
| 149 |
+
|
| 150 |
+
Pipelines produce features (`data/processed/<modality>_features.parquet`).
|
| 151 |
+
Downstream models live in `src/models/` and consume those features:
|
| 152 |
+
|
| 153 |
+
| Model | File | Output | Endpoint |
|
| 154 |
+
|---|---|---|---|
|
| 155 |
+
| BBB permeability | `src/models/bbb_model.py` | `data/processed/bbb_model.joblib` | `POST /predict/bbb` |
|
| 156 |
+
|
| 157 |
+
Each downstream model module exposes a uniform surface:
|
| 158 |
+
- `train(df, label_col, ...)` → fitted classifier
|
| 159 |
+
- `save(model, path)` / `load(path)` → joblib artifact I/O
|
| 160 |
+
- `predict_with_proba(model, smiles)` → `{label, confidence}` (confidence is the max-class probability)
|
| 161 |
+
- `explain_prediction(model, smiles, top_k)` → SHAP top-k attributions sorted by `|shap_value|` descending
|
| 162 |
+
|
| 163 |
+
The API loads the joblib artifact at request time. If the artifact is
|
| 164 |
+
missing, the endpoint returns **HTTP 503** with a remediation hint pointing
|
| 165 |
+
at the trainer CLI (`python -m src.models.<name>`). This keeps the API
|
| 166 |
+
process startup fast and lets operators retrain without redeploying — the
|
| 167 |
+
Day-5 analog of Day-4's `NEUROBRIDGE_DISABLE_MLFLOW` lifeline.
|
| 168 |
+
|
| 169 |
+
**Determinism**: all classifiers are seeded (`random_state=42` default),
|
| 170 |
+
`n_jobs=1` (no tree-parallelism races). Re-running the trainer on the same
|
| 171 |
+
Parquet produces identical predictions.
|
| 172 |
+
|
| 173 |
+
**Override `BBB_MODEL_PATH`** env var to point the API at a non-default
|
| 174 |
+
artifact location (used by tests for tmp_path isolation).
|
README.md
CHANGED
|
@@ -14,6 +14,7 @@ and Docker shipping.
|
|
| 14 |
| 2 | Signal (EEG) | [`eeg_pipeline.py`](src/pipelines/eeg_pipeline.py) | Shipped — 67 tests green |
|
| 15 |
| 3 | Image (MRI / fMRI) | [`mri_pipeline.py`](src/pipelines/mri_pipeline.py) | Shipped — 106 tests green |
|
| 16 |
| 4 | API + MLOps + Frontend | FastAPI + MLflow + Streamlit + Docker | Shipped — 142 tests green |
|
|
|
|
| 17 |
|
| 18 |
## Quick Start
|
| 19 |
|
|
@@ -59,6 +60,21 @@ Result lives at `data/processed/mri_features.parquet` (48 ROI features per subje
|
|
| 59 |
> [Kaggle](https://www.kaggle.com/datasets/priyanagda/bbbp) or
|
| 60 |
> [MoleculeNet](https://moleculenet.org/datasets-1); place as `data/raw/bbbp.csv`.
|
| 61 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 62 |
### Run the full stack with Docker
|
| 63 |
|
| 64 |
```bash
|
|
@@ -154,6 +170,7 @@ finishes in under 4 seconds on a 2024 laptop.
|
|
| 154 |
- **Day 2 (shipped):** `eeg_pipeline.py` — bandpass + MNE ICA artifact removal + PSD + statistical features → Parquet.
|
| 155 |
- **Day 3 (shipped):** `mri_pipeline.py` — NIfTI volume loading, brain masking, ROI feature extraction, ComBat harmonization (`neuroHarmonize`) for site-level domain shift → Parquet (48 features, 106 tests green).
|
| 156 |
- **Day 4 (shipped):** FastAPI surface in `src/api/` (POST `/pipeline/{bbb,eeg,mri}` + `/health`), MLflow experiment tracking via `src.core.tracking` (see AGENTS.md §7), Streamlit dashboard at `src/frontend/app.py`, and Docker / `docker-compose.yml` for the api + MLflow stack — 142 tests green.
|
|
|
|
| 157 |
|
| 158 |
## Where to Look
|
| 159 |
|
|
@@ -171,3 +188,5 @@ finishes in under 4 seconds on a 2024 laptop.
|
|
| 171 |
- **Streamlit dashboard:** [`src/frontend/app.py`](src/frontend/app.py)
|
| 172 |
- **Container stack:** [`Dockerfile`](Dockerfile), [`docker-compose.yml`](docker-compose.yml)
|
| 173 |
- **Day-4 tests:** [`tests/api/`](tests/api/), [`tests/frontend/`](tests/frontend/), [`tests/pipelines/test_cross_pipeline_smoke.py`](tests/pipelines/test_cross_pipeline_smoke.py)
|
|
|
|
|
|
|
|
|
| 14 |
| 2 | Signal (EEG) | [`eeg_pipeline.py`](src/pipelines/eeg_pipeline.py) | Shipped — 67 tests green |
|
| 15 |
| 3 | Image (MRI / fMRI) | [`mri_pipeline.py`](src/pipelines/mri_pipeline.py) | Shipped — 106 tests green |
|
| 16 |
| 4 | API + MLOps + Frontend | FastAPI + MLflow + Streamlit + Docker | Shipped — 142 tests green |
|
| 17 |
+
| 5 | Decision Layer (Model + XAI + Interactive UI) | [`bbb_model.py`](src/models/bbb_model.py) — RandomForest + SHAP + `POST /predict/bbb` | Shipped — 158 tests green |
|
| 18 |
|
| 19 |
## Quick Start
|
| 20 |
|
|
|
|
| 60 |
> [Kaggle](https://www.kaggle.com/datasets/priyanagda/bbbp) or
|
| 61 |
> [MoleculeNet](https://moleculenet.org/datasets-1); place as `data/raw/bbbp.csv`.
|
| 62 |
|
| 63 |
+
### Train the downstream BBB model (one-time)
|
| 64 |
+
|
| 65 |
+
```bash
|
| 66 |
+
python -m src.pipelines.bbb_pipeline # produces data/processed/bbbp_features.parquet
|
| 67 |
+
python -m src.models.bbb_model # produces data/processed/bbb_model.joblib
|
| 68 |
+
```
|
| 69 |
+
|
| 70 |
+
Then `POST /predict/bbb` (and the Streamlit BBB tab) become live. Try:
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
curl -s -X POST http://localhost:8000/predict/bbb \
|
| 74 |
+
-H 'Content-Type: application/json' \
|
| 75 |
+
-d '{"smiles": "CCO", "top_k": 5}' | python3 -m json.tool
|
| 76 |
+
```
|
| 77 |
+
|
| 78 |
### Run the full stack with Docker
|
| 79 |
|
| 80 |
```bash
|
|
|
|
| 170 |
- **Day 2 (shipped):** `eeg_pipeline.py` — bandpass + MNE ICA artifact removal + PSD + statistical features → Parquet.
|
| 171 |
- **Day 3 (shipped):** `mri_pipeline.py` — NIfTI volume loading, brain masking, ROI feature extraction, ComBat harmonization (`neuroHarmonize`) for site-level domain shift → Parquet (48 features, 106 tests green).
|
| 172 |
- **Day 4 (shipped):** FastAPI surface in `src/api/` (POST `/pipeline/{bbb,eeg,mri}` + `/health`), MLflow experiment tracking via `src.core.tracking` (see AGENTS.md §7), Streamlit dashboard at `src/frontend/app.py`, and Docker / `docker-compose.yml` for the api + MLflow stack — 142 tests green.
|
| 173 |
+
- **Day 5 (shipped):** Decision layer in `src/models/bbb_model.py` — RandomForest BBB classifier on Morgan fingerprints, SHAP top-k explanations, `POST /predict/bbb` endpoint, interactive Streamlit BBB tab with SMILES input + decision card + SHAP bar chart, and trainer CLI (`python -m src.models.bbb_model`). See AGENTS.md §8 — 158 tests green.
|
| 174 |
|
| 175 |
## Where to Look
|
| 176 |
|
|
|
|
| 188 |
- **Streamlit dashboard:** [`src/frontend/app.py`](src/frontend/app.py)
|
| 189 |
- **Container stack:** [`Dockerfile`](Dockerfile), [`docker-compose.yml`](docker-compose.yml)
|
| 190 |
- **Day-4 tests:** [`tests/api/`](tests/api/), [`tests/frontend/`](tests/frontend/), [`tests/pipelines/test_cross_pipeline_smoke.py`](tests/pipelines/test_cross_pipeline_smoke.py)
|
| 191 |
+
- **Day-5 plan (full TDD task breakdown):** [`docs/superpowers/plans/2026-05-03-day5-downstream-model-xai-interactive.md`](docs/superpowers/plans/2026-05-03-day5-downstream-model-xai-interactive.md)
|
| 192 |
+
- **BBB downstream model (classifier + SHAP explainer + trainer CLI):** [`src/models/bbb_model.py`](src/models/bbb_model.py) + [`tests/models/test_bbb_model.py`](tests/models/test_bbb_model.py) (12 tests)
|
src/models/bbb_model.py
CHANGED
|
@@ -205,3 +205,29 @@ def explain_prediction(
|
|
| 205 |
{"feature": str(name), "shap_value": float(value)}
|
| 206 |
for name, value in pairs[:top_k]
|
| 207 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
{"feature": str(name), "shap_value": float(value)}
|
| 206 |
for name, value in pairs[:top_k]
|
| 207 |
]
|
| 208 |
+
|
| 209 |
+
|
| 210 |
+
DEFAULT_FEATURES_PATH = Path("data/processed/bbbp_features.parquet")
|
| 211 |
+
DEFAULT_MODEL_PATH = Path("data/processed/bbb_model.joblib")
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
def main() -> None:
|
| 215 |
+
"""Train and persist the production BBB model from the Day-4 features Parquet.
|
| 216 |
+
|
| 217 |
+
Reads from `DEFAULT_FEATURES_PATH`, trains with default hyperparameters,
|
| 218 |
+
and writes the artifact to `DEFAULT_MODEL_PATH`. Re-runs are idempotent
|
| 219 |
+
(same random_state).
|
| 220 |
+
"""
|
| 221 |
+
if not DEFAULT_FEATURES_PATH.exists():
|
| 222 |
+
raise FileNotFoundError(
|
| 223 |
+
f"Features Parquet not found at {DEFAULT_FEATURES_PATH}. "
|
| 224 |
+
f"Run `python -m src.pipelines.bbb_pipeline` first."
|
| 225 |
+
)
|
| 226 |
+
df = pd.read_parquet(DEFAULT_FEATURES_PATH)
|
| 227 |
+
model = train(df, label_col="p_np")
|
| 228 |
+
save(model, DEFAULT_MODEL_PATH)
|
| 229 |
+
logger.info("BBB model artifact ready at %s", DEFAULT_MODEL_PATH)
|
| 230 |
+
|
| 231 |
+
|
| 232 |
+
if __name__ == "__main__":
|
| 233 |
+
main()
|