# WashData ML subsystem (experimental, gated) Compact, **NumPy-only** models plus the runtime that trains and consumes them. No new dependencies (NumPy is already in `manifest.json`). Everything here is gated by flags in `const.py` and is inert until enabled, so the proven detection/matching/ETA code paths are unchanged by default. ## Feature flags (const.py) - `SHOW_ML_LAB` - show the ML Lab panel tab (shadow-mode comparison + review). - `ENABLE_ML_SUGGESTIONS` - surface ML-calibrated setting suggestions alongside the classic ones (`MLSuggestionEngine`). - `ENABLE_ML_TRAINING` - allow the scheduled/manual on-device training loop. - `CONF_ENABLE_ML_MODELS` (per-device option) - opt-in gate (via `ml_models_enabled(options)`) for feeding ML signals into runtime decisions; default off so callers keep existing behavior. (No runtime consumer wires this yet - the live ML paths below run under their own flags.) ## What ships here - `promoted_manifest.json` + `_model.py` - the embedded **baseline** models (the broad-corpus models trained offline in `/root/ml_washdata`). Each module is self-contained and exposes `score()`, `predict()`, `FEATURE_COLUMNS`, `THRESHOLD`, `MODEL_METRICS`. `_feature_contract.json` documents the live data each feature comes from; `_parity.json` are golden feature→score cases the tests assert against. - `feature_extraction.py` - NumPy-only runtime feature extractors (`latest_end_event_features`, `live_match_features`, `quality_features`, `profile_expectation`, energy integration) matching the models' `FEATURE_COLUMNS`. - `engine.py` - `resolve_scorer(capability, store)`, the single bridge that returns a **classifier** scoring callable preferring an on-device trained spec over the embedded baseline (`"on_device"` vs `"baseline"`); `resolve_regressor( capability, store)` is its **regression** twin for `standardized_linear` heads that have no shipped baseline (returns `(None, None)` until one is promoted), plus `ml_models_enabled` (opt-in gate) and `available_models` (manifest provenance). - `trainer.py` - NumPy-only training for two spec kinds: logistic classifiers (`fit_logistic`, `select_threshold`, `binary_metrics`, `auc`, `build_spec`/ `score_spec` - byte-compatible with the embedded `score()` math) and ridge **regressors** (`fit_ridge`, `regression_metrics`, `build_regression_spec`/ `predict_value_spec` - standardized features + standardized target). - `training_task.py` - on-device orchestration: derives labels from the device's own cycles (end events from trace geometry; quality from status + ML-Lab review labels; live_match from match-ranking-history snapshots), synthesises completion-fraction examples for the regression capabilities, trains, and promotes a classifier only when its held-out AUC is within margin of the baseline (a regressor only when its held-out MAE beats the naive elapsed/ expected projection). - `matching_tuner.py` - `tune_matching_config(cycles)`: NumPy-only, executor-safe leave-one-out tuning of the matcher's bounded scoring weights (`corr_weight`, `duration_weight`, `energy_weight`, `dtw_ensemble_w`) over the device's own labelled cycles. Same promotion discipline as the models (gate on a held-out split by a margin); it only ever changes the emphasis between shape/level/energy, never structural matching behaviour. Models (all standardized-logistic; only models that beat their baseline are shipped): - `hybrid_curve_quality_model` - P(finished cycle is a problem). - `live_match_commit_model` - P(top-1 live program match is correct). - `cycle_end_detector_model` - P(a low-power event is the true end vs a pause). (No regression baseline is shipped: the `remaining_time` and `total_energy` completion-fraction regressors did not beat the `expected_duration - elapsed` heuristic on the broad corpus, so they stay inert until on-device training promotes a per-device spec that beats that naive projection.) ## How trained models reach inference `resolve_scorer(capability, store)` is used by the ML Lab shadow comparison (`ws_api._compute_ml_comparison`) and by `MLSuggestionEngine`. If the profile store holds an on-device spec for that capability (trained by `training_task` and persisted under `ml_model_versions`), it is used; otherwise the embedded baseline module is used. The shipped baseline is a broad-corpus model - per-user accuracy gains come from on-device training, not from replacing the baseline. ## Regenerating the embedded baseline (offline lab only) ```bash cd /root/ml_washdata ./ml.sh experiment # retrain + verify the determinism gate python promote_to_integration.py --target # reads output/promoted/ ``` `promote_to_integration.py` refuses to copy any model whose encode/decode round trip is not deterministic. On-device training never touches these baseline files; it writes trained specs into the profile store instead.