Skip to content

Commit f191925

Browse files
mschaepersclaude
andcommitted
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>
1 parent 86e176e commit f191925

2 files changed

Lines changed: 13 additions & 4 deletions

File tree

docs/study_cases/good_practices.md

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,20 @@ If your downstream automation or display does need a different convention (e.g.
4343

4444
## 4. `soc_init` and `soc_final` runtime semantics
4545

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.
4747

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 aloneit 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.
4949

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.
5160

5261
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.
5362

docs/study_cases/mpc.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ In addition to the parameters from [Basic — PV + Battery](basic_pv_battery.md)
3030
For the full list of runtime keys, see [Passing data](../passing_data.md).
3131

3232
```{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 valueit does **not** mirror the passed value onto the missing one. To avoid an unintended terminal-SOC constraint in rolling MPC, 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.
3434
```
3535

3636
## Run

0 commit comments

Comments
 (0)