Forward Heating Mode¶
Increment 1 of the equipment-realism milestone: a forward/predictive simulation mode where a capacity-limited, PI-controlled boiler heats the greenhouse toward a temperature setpoint and reports realistic fuel consumption. The byte-stable measured-replay path and both existing integrators are untouched.
What it is (vs replay)¶
Replay (measured_replay*) |
Forward mode | |
|---|---|---|
| Heating input | replayed measured pipe-temperature proxy flux | PI controller → boiler with rated capacity + efficiency |
| Control | none (open-loop replay of measured actuators) | closed-loop on a temperature setpoint |
| Fuel accounting | none | gas = delivered / efficiency, per step |
| Capacity limit | none (unbounded flux) | rated capacity; saturation reported |
| Use | reproduce a measured dataset | design studies (size a boiler, test a setpoint schedule) |
The forward mode controls only heating in closed loop; every other input (weather, lighting, CO2, ventilation, screens) comes from the same forcing the replay uses.
Architecture¶
greenflux.control— new layer:PIControllerParameters/PIControllerState/pi_control_step— generic PI with conditional-integration anti-windup.HeatingActuator— wraps the existingequipment.heating.Boiler(per-m² capacity + efficiency + unmet), driven by the PI controller, optional ramp limit.ConstantSetpoint/DayNightSetpoint/MeasuredSetpoint— setpoint providers.greenflux.simulators.forward.simulate_forward(...)— outer control loop. Each control interval computes the heating flux once from the interval-start state, then advances the sameGreenhouseUnitphysics over the interval with that flux held constant (zero-order hold) viasolver="ivp"(default) orsolver="fixed". The PI integral lives in the driver, never in the frozen ODE state vector, so the mode composes with both integrators without touching parity. Returns aForwardResultwith a state trajectory, aForwardConsumptionLedger(heat_delivered_kwh_m2,fuel_gas_kwh_m2,hours_at_capacity), andsaturated_step_count.projects.schema.HeatingActuatorDefinitiononEquipmentDefinition— declarative (rated_capacity_w_m2,efficiency,proportional_gain_w_m2_per_k,integral_gain_w_m2_per_k_s,ramp_limit_w_m2_s). Optional with a default, so existingproject.jsonfiles load unchanged..to_actuator()builds aHeatingActuator.
Under-capacity is reported via saturation (hours_at_capacity, saturated_step_count)
and shows up as a persistent setpoint-tracking error. The boiler-level "unmet" is not a
separate ledger field: because the PI anti-windup bound equals the rated capacity, the
boiler never receives a request above capacity, so its unmet term is always zero (a real
kWh-deficit measure can return with a multi-actuator / heat-pump path).
Running the validation harness¶
It drives the forward mode with the measured indoor temperature as the setpoint over
the 12 strided HPS windows, sizes the boiler from the replay's peak delivered heat (×1.2),
and prints a comparison table (also written to
outputs/forward/validate_forward_heating.json).
Validation results and honest caveats¶
Measured run (HPS, 12 strided windows, measured_replay_physical):
| metric | value |
|---|---|
| replay air-temp RMSE | 2.30 °C |
| forward air-temp RMSE | 0.79 °C (tracks the measured setpoint tightly) |
| replay heat (1 day) | 0.64 kWh/m² |
| forward heat (1 day) | 0.93 kWh/m² (+46 %) |
| forward gas (~200-day season) | ~207 kWh/m² |
| avg hours at capacity (of 24 h) | 12.7 |
The forward driver, controller, actuator, ledger and schema are correct and unit-tested. The over-delivery and heavy saturation above are limitations of this particular validation setup, not of the mode:
- Heating-only actuator chasing the full measured temperature. The measured trajectory is the result of all fluxes (heating + solar + ventilation). A heating-only boiler forced to follow it can only add heat, never remove it, so it over-heats on every solar/ventilation-driven rise and saturates. Tracking to 0.79 °C therefore comes at the cost of ~46 % excess heat.
- Two calibration-application paths. The harness builds the unit via the builder path
(
_apply_profile_to_project), while the replay applies the profile through the analysis path (per-input multipliers). The two units are not identical, so the heat comparison is not perfectly apples-to-apples. - Auto-sized aggressive PI.
kp = rated/2saturates on any error > ~2 °C; morning warm-up and cold nights exceed that often.
For its actual purpose — forward design studies driven by a real setpoint schedule
(e.g. DayNightSetpoint(19 °C / 17 °C)) — the mode behaves correctly: temperature
converges to the setpoint within the boiler's capacity, and fuel/hours-at-capacity are
accounted against the rated equipment. Reproducing a measured dataset's exact energy is the
replay's job, not the forward mode's.
Next increments (same pattern)¶
CO2 / lighting / ventilation / heat-pump as closed-loop capacity-limited actuators; a
project-run --mode forward CLI flag; on/off-hysteresis control; active cooling. Each
reuses the actuator + controller + ledger pattern established here.