You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
docs(study_cases): drop utils.py line refs, add rolling-MPC SOC recipes
Two improvements to the SOC runtime semantics description:
1. Removed the explicit "src/emhass/utils.py lines 925-983" pointer.
Line numbers drift with future code changes and shouldn't be hard-coded
in user-facing docs.
2. Added two production-tested rolling-MPC recipes:
- soc_final = soc_init (current SOC for both, neutral trailing edge)
- soc_final = 0 (target is never reached because each 24 h horizon
is replaced before the deadline arrives; behaves the same in practice)
Both patterns require passing both values explicitly per call, since
omitting one falls back to battery_target_state_of_charge for that
value alone (no mirroring).
Same edits applied to good_practices.md §4 and the mpc.md note for
consistency.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: docs/study_cases/good_practices.md
+12-3Lines changed: 12 additions & 3 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -43,11 +43,20 @@ If your downstream automation or display does need a different convention (e.g.
43
43
44
44
## 4. `soc_init` and `soc_final` runtime semantics
45
45
46
-
The two SOC parameters behave differently across optimization actions, so it pays to know exactly what EMHASS does in each path.
46
+
The two SOC parameters behave differently across optimization actions, so it pays to know exactly what EMHASS does with each.
47
47
48
-
**`naive-mpc-optim`:**EMHASS reads `soc_init` and `soc_final` from `runtimeparams` independently. If a value is missing from `runtimeparams`, EMHASS falls back to `battery_target_state_of_charge` (default `0.6`) for that value alone — it does **not** mirror one to the other. So passing only `soc_init = 0.45`and omitting `soc_final`yields `soc_init = 0.45, soc_final = 0.6`, which still imposes a terminal-SOC constraint. To avoid trailing-edge bias in rolling MPC, pass both explicitly (typically `soc_init` from your battery sensor, `soc_final` to whatever target your runtime layer computes — equal to `soc_init` for a neutral trailing edge, or a fixed end-of-horizon target like `0.6` for an end-of-day reserve). See `src/emhass/utils.py` lines 925-983 for the parsing logic.
48
+
**`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.
49
49
50
-
**`dayahead-optim` and `perfect-optim`:** runtime `soc_init` and `soc_final` are **not** read from `runtimeparams`; both fall back to `battery_target_state_of_charge` unconditionally. Use day-ahead when `battery_target_state_of_charge` is the right answer for both ends of the horizon, and switch to MPC when you need runtime control of either value.
50
+
**`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.
51
+
52
+
### Practical recipes for rolling MPC
53
+
54
+
Two patterns work well in production. Both pass *both* values explicitly per MPC call, just with different `soc_final`:
55
+
56
+
-**`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.
57
+
-**`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.
58
+
59
+
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.
51
60
52
61
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.
Copy file name to clipboardExpand all lines: docs/study_cases/mpc.md
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -30,7 +30,7 @@ In addition to the parameters from [Basic — PV + Battery](basic_pv_battery.md)
30
30
For the full list of runtime keys, see [Passing data](../passing_data.md).
31
31
32
32
```{note}
33
-
`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 one value — it does **not** mirror the passed value onto the missing one. To avoid an unintended terminal-SOC constraint in rollingMPC, pass both explicitly: typically `soc_init` from your battery sensor and `soc_final` to whatever target your runtime layer computes (equal to `soc_init` for a neutral trailing edge, or a fixed end-of-horizon target). See `src/emhass/utils.py` lines 925-983 for the parsing logic.
33
+
`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.
0 commit comments