mekosotto commited on
Commit
53256ed
·
1 Parent(s): b6f1745

docs: Day-5 close-out — AGENTS §8 decision layer + trainer CLI

Browse files
Files changed (3) hide show
  1. AGENTS.md +30 -0
  2. README.md +19 -0
  3. 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()