diff --git a/README.md b/README.md index 94bfa48..fb362db 100644 --- a/README.md +++ b/README.md @@ -70,16 +70,15 @@ components/ Pluggable building blocks behind factories: tasks/ Async tasks (one per component) that poll hardware/sim state at a fixed interval and publish changes through the message dispatcher. + sud_log.py's SudLogTask additionally records + each Sud run's measured data and forecast to + logs/*.json - see "Logging & analysis" below. ws/ Minimal WebSocket pub/sub layer: server (single- and multi-user), client, and a keyed message dispatcher (e.g. "Sensor", "Heater", "TempCtrl", "Stirrer", "Pot", "Sud"). -tracer.py Logs traced variables to .mat files (for - offline analysis/tuning in MATLAB/Octave, - see results.m / results_tc.m). - scripts/demos/ Standalone, eyeballed-plot demos (matplotlib) exercising components/* in isolation — pid/, plant/, and a full mash-schedule run in sud/. @@ -194,8 +193,9 @@ environment (`$WORKON_HOME`/`$BREWPI_HOME`) on a Raspberry Pi-style deployment. ## Mash schedules Files under `sude/` describe a brew's mash schedule ("Sud"): `pot_mass`, -`pot_material` (fixed for the whole brew), and a `steps` list. Every step -ramps toward its `temperature`, then optionally holds: +`pot_material`, `L` (pot energy loss coefficient), `Td` (transport +propagation delay) - all fixed for the whole brew - and a `steps` list. +Every step ramps toward its `temperature`, then optionally holds: - ramping toward `temperature` at `"ramp": {"rate": ...}` (°C/min) is not opt-in - it happens for every step, for as long as the temperature @@ -225,13 +225,27 @@ add malt or check gravity, instead of advancing immediately. Each step also has its own `grain_mass`/`water_mass` (defaulted like everything else from `default.step`), since both change over the course of -a brew — e.g. malt going in partway through, or water boiling off. Pass a -step's `grain_mass`/`water_mass` to `Sud.derive_plant_params()` (together -with the brew-wide `pot_mass`/`pot_material`) to get that step's lumped -`Pot` `M`/`C`; `scripts/demos/sud/demo_sud.py` recomputes and re-applies -these (via `Pot.set_thermal_params()`/`TempController.set_model_params()`, -on both the real plant and the controller's Smith-predictor model) every -time the current step changes, instead of deriving them once at startup. +a brew — e.g. malt going in partway through, or water boiling off. +`Sud.derive_plant_params()` folds a step's `grain_mass`/`water_mass` +together with the brew-wide `pot_mass`/`pot_material`/`L`/`Td` into a full +`Pot` plant-params dict (`M`/`C`/`L`/`Td`) - `M`/`C` vary with the step, +`L`/`Td` come straight through unchanged. `tasks/sud.py`'s `SudTask. +apply_plant_params()` pushes the result onto both the real plant +(`Pot.set_plant_params()`) and the controller's Smith-predictor model +(`TempController.set_model_plant_params()`, where it applies) - once +immediately when a Sud is loaded (using its first step), and again on +every real step transition during a run, rather than deriving it once at +startup (`scripts/demos/sud/demo_sud.py` mirrors the same pattern +standalone). + +Neither the real plant nor a model-based controller (Smith) ever gets +generic/placeholder params - they're only ever set from an actually +loaded Sud's own doc. Until one is loaded, both stay inert +(`Pot.is_configured()`/`TempControllerBase.is_configured()` are `False`, +so `tasks/pot.py`'s `PotTask` and `tasks/tempctrl.py`'s `TcTask` simply +skip `process()` rather than running on bogus values) - the temperature +controller's "Normal" variant has no plant-model dependency at all, +though, so it stays active regardless of whether a Sud is loaded. The top-level `default.step` object gives every field above (including the nested `ramp`/`hold`/`stirrer` blocks) a default value; a step in `steps` @@ -286,9 +300,12 @@ control behavior, not just a progress display. The forecast is computed server-side (`components/sud_forecast.py`'s `SudForecastEstimator`) by actually running the schedule through a throwaway `Sud`/`Pot`/temperature-controller trio built from the same -params (`pid_type`, `TempCtrl` gains, plant params, ambient, heater max -power) the real server uses - a multi-hour brew simulates in well under a -second since it's pure CPU-bound iteration, no real time/IO involved. This +params (`pid_type`, `TempCtrl` gains, ambient, heater max power) the real +server uses - plant params (`M`/`C`/`L`/`Td`) aren't a constructor +argument at all, since they come from the doc being forecast itself +(`Sud.derive_plant_params()`, same as the real run), not from any generic +default. A multi-hour brew simulates in well under a second since it's +pure CPU-bound iteration, no real time/IO involved. This is deliberately *not* recomputed as a run progresses: a forecast that keeps re-anchoring itself to match whatever's actually happening would no longer be a meaningful baseline to compare reality against. It's computed once, @@ -337,12 +354,20 @@ like a control problem. ## Logging & analysis -`tracer.py` (and `tasks/tracer.py`) periodically dump traced signals -(temperatures, heat rates, heater power) to `logs/*.mat` files, which can be -loaded in MATLAB/Octave — see `results.m` and `results_tc.m` — to evaluate -and tune the controller. +`tasks/sud_log.py`'s `SudLogTask` records every Sud run's measured data - +the same six signals the GUI's Automatic tab plots live (`temp_ist`/ +`temp_soll`, `rate_ist`/`rate_soll`, `power_set`/`power_eff`), each sample +with both simulated-elapsed-seconds and a real wall-clock timestamp - to +`logs/log_T