diff --git a/docs/study_cases/basic_pv.md b/docs/study_cases/basic_pv.md index 0e676628..caf8b65f 100644 --- a/docs/study_cases/basic_pv.md +++ b/docs/study_cases/basic_pv.md @@ -15,7 +15,7 @@ This scenario adds a 5 kWp PV installation to the previous tutorial. No battery | Optimization modes | perfect-optim (backtest), dayahead-optim | | Cost function | profit | -This matches the default configuration shipped in `config_defaults.json` — no parameter changes required. +To enable PV in EMHASS, set `set_use_pv: true` (default is `false`) and configure your PV plant via `solar_forecast_kwp` (for the `solar.forecast` method) or one of the other `weather_forecast_method` options. The two deferrable loads use the default `nominal_power_of_deferrable_loads: [3000.0, 750.0]` from `config_defaults.json`. ## Perfect optimization (7-day historical backtest) diff --git a/docs/study_cases/dhw_walkthrough.md b/docs/study_cases/dhw_walkthrough.md index 8b85cc61..df2ddf15 100644 --- a/docs/study_cases/dhw_walkthrough.md +++ b/docs/study_cases/dhw_walkthrough.md @@ -96,7 +96,7 @@ rest_command: {}, { "thermal_config": { - "heating_rate": 4.0, + "heating_rate": 5.0, "cooling_constant": 0.02, "start_temperature": {{ states('sensor.dhw_tank_temperature') | float }}, "sense": "heat", diff --git a/docs/study_cases/good_practices.md b/docs/study_cases/good_practices.md index 7f8e2bdf..e1e7f995 100644 --- a/docs/study_cases/good_practices.md +++ b/docs/study_cases/good_practices.md @@ -41,15 +41,24 @@ Source: `optimization.py` constraints `min_energy = battery_minimum_state_of_cha If your downstream automation or display does need a different convention (e.g. percentage of *usable* range), apply the transform yourself in a HA template. Don't assume EMHASS already did it. -## 4. `soc_init` and `soc_final` defaults are forgiving +## 4. `soc_init` and `soc_final` runtime semantics -For day-ahead optimization, setting `soc_final` to a desired end-of-day SOC (e.g. 0.6) ensures you don't end the day empty. EMHASS uses `battery_target_state_of_charge` (default 0.6) as the fallback when neither `soc_init` nor `soc_final` is passed. +The two SOC parameters behave differently across optimization actions, so it pays to know exactly what EMHASS does with each. -For rolling MPC, the often-cited concern is that a fixed `soc_final` reserves capacity at the trailing edge of every horizon and biases the optimizer toward conservative mid-day behavior. This is real, but the EMHASS code already handles the common case: when you pass only `soc_init` at runtime, EMHASS auto-sets `soc_final = soc_init`. So **passing only `soc_init`** in your MPC payload is the standard rolling-MPC recipe. +**`naive-mpc-optim`** reads `soc_init` and `soc_final` from `runtimeparams` independently. If one is missing, EMHASS substitutes `battery_target_state_of_charge` (default `0.6`) for that value alone; it does **not** mirror the passed value onto the missing one. So passing only `soc_init = 0.45` yields `soc_init = 0.45, soc_final = 0.6`, which still imposes a terminal-SOC constraint at every solve. Always pass both explicitly. -Pass `soc_final` explicitly only when you have a hard end-of-horizon target (e.g. "must be at 60% by tomorrow 06:00 to absorb morning PV"). In that case you may also want to extend the horizon so the constraint sits at the actual deadline, not 24 h after the current re-run. +**`dayahead-optim`** and **`perfect-optim`** do not read `soc_init` or `soc_final` from `runtimeparams` at all. Both fall back to `battery_target_state_of_charge`. Use day-ahead when that single value is the right answer for both ends of the horizon, and switch to MPC when you need runtime control of either. -A common new-user trap is the opposite: starting with very low actual SOC where `soc_init` is below `battery_minimum_state_of_charge`. The optimization becomes infeasible because the initial state already violates a constraint. See Section 5 below (item 4) for the full triage and [discussion #359](https://github.com/davidusb-geek/emhass/discussions/359) for the canonical thread. +### Practical recipes for rolling MPC + +Two patterns work well in production. Both pass *both* values explicitly per MPC call, just with different `soc_final`: + +- **`soc_final = soc_init`** (pass current measured SOC for both). The trailing-edge constraint becomes neutral: the battery's end-of-horizon SOC is allowed to land wherever it started, so the optimizer is free to use it inside the horizon. Good for systems with no hard end-of-day target. +- **`soc_final = 0`** (or `battery_minimum_state_of_charge` if you prefer to stay above the floor). With a 48-step (24 h) rolling horizon and re-runs every 30 min, the deadline at step 48 is always 24 h ahead. Each run replaces the schedule before that deadline ever arrives, so the trailing target is never actually reached. In practice the optimizer behaves the same as the neutral-edge case, just expressed differently. Useful when your runtime layer wants a single static `soc_final` value rather than tracking the live sensor. + +If you do have a real end-of-horizon deadline (for example "must be at 60% before tomorrow 06:00 to absorb morning PV"), pass that target as `soc_final` and extend `prediction_horizon` so the deadline sits at the actual point in time, not at the trailing edge of a fixed 24 h window. + +A common new-user trap is starting with very low actual SOC where `soc_init` is below `battery_minimum_state_of_charge`. The optimization becomes infeasible because the initial state already violates a constraint. See Section 5 below (item 4) for the full triage and [discussion #359](https://github.com/davidusb-geek/emhass/discussions/359) for the canonical thread. ## 5. `optim_status: Infeasible` triage order diff --git a/docs/study_cases/mpc.md b/docs/study_cases/mpc.md index 072e0391..22f5656f 100644 --- a/docs/study_cases/mpc.md +++ b/docs/study_cases/mpc.md @@ -30,7 +30,7 @@ In addition to the parameters from [Basic — PV + Battery](basic_pv_battery.md) For the full list of runtime keys, see [Passing data](../passing_data.md). ```{note} -`soc_init` and `soc_final` defaults: EMHASS already defaults `soc_final` to `soc_init` (and vice versa) when only one is passed at runtime; if neither is passed, both fall back to `battery_target_state_of_charge` from the static config. So for typical rolling MPC, passing only `soc_init` is sufficient: the optimizer will not impose a terminal-SOC bias inside the horizon. Pass `soc_final` explicitly only when you have a hard end-of-horizon target (e.g. ensure 60% before tomorrow morning). +`soc_init` and `soc_final` are read from `runtimeparams` independently. If one is omitted, EMHASS substitutes `battery_target_state_of_charge` (default `0.6`) for that single value; it does **not** mirror the passed value onto the missing one. Always pass both explicitly. Two production-tested rolling-MPC patterns: `soc_final = soc_init` (current SOC for both, neutral trailing edge) or `soc_final = 0` (with a 24 h horizon re-run every 30 min, the deadline is always 24 h away and never reached, so this behaves the same in practice). For a hard end-of-horizon target, pass that value and extend `prediction_horizon` so the deadline sits at the real point in time. ``` ## Run diff --git a/docs/study_cases/reference_configs.md b/docs/study_cases/reference_configs.md index a6d9e85d..e3ba1a30 100644 --- a/docs/study_cases/reference_configs.md +++ b/docs/study_cases/reference_configs.md @@ -17,7 +17,8 @@ The most common modern setup: ~150 m² home, 8–12 kWp PV, 10–15 kWh battery, set_use_pv: true solar_forecast_kwp: 10 optimization_time_step: 30 -prediction_horizon: 48 # used by MPC +# Note: prediction_horizon is a runtime parameter (passed in the MPC payload), +# not a static config field. With this 30-min step a 24 h horizon is 48 timesteps. set_use_battery: true battery_nominal_energy_capacity: 12000 # Wh (= 12 kWh)