Skip to content

Commit 86e176e

Browse files
mschaepersclaude
andcommitted
docs(study_cases): correct SOC runtime semantics (independent fallback, no mirror)
Earlier commit 4290b24 still asserted that EMHASS auto-mirrors soc_final = soc_init when only one is passed. That is wrong for the real REST-API code path: - src/emhass/utils.py lines 925-983 parses naive-mpc-optim runtimeparams. Missing soc_init falls back to battery_target_state_of_charge (0.6) independently of soc_final, and vice versa. No mirroring. - The defensive mirror-block in src/emhass/optimization.py lines 2477-2487 only fires if perform_optimization is called directly with one None value — never reached through the normal /action/ REST entry point. - dayahead-optim and perfect-optim do not read soc_init/soc_final from runtimeparams at all (utils.py line 980 else branch); both always use battery_target_state_of_charge. Rewrote good_practices.md §4 and mpc.md note to describe the actual behavior per action, with file:line references into the code. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 4290b24 commit 86e176e

2 files changed

Lines changed: 6 additions & 6 deletions

File tree

docs/study_cases/good_practices.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -41,15 +41,15 @@ Source: `optimization.py` constraints `min_energy = battery_minimum_state_of_cha
4141

4242
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.
4343

44-
## 4. `soc_init` and `soc_final` defaults are forgiving
44+
## 4. `soc_init` and `soc_final` runtime semantics
4545

46-
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.
46+
The two SOC parameters behave differently across optimization actions, so it pays to know exactly what EMHASS does in each path.
4747

48-
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. The EMHASS code helps here for the simple case: if you pass only `soc_init` at runtime (and omit `soc_final`), EMHASS sets `soc_final = soc_init` for you, so passing just `soc_init` is enough for a basic rolling-MPC setup.
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.
4949

50-
If you pass both `soc_init` and `soc_final` explicitly with different values, EMHASS uses them as-is. That is appropriate when you have a hard end-of-horizon target (e.g. "must be at 60% by tomorrow 06:00 to absorb morning PV"), or when your runtime layer computes a target SOC dynamically. In that case you may also want to extend the horizon so the constraint sits at the actual deadline rather than at the trailing edge of a fixed 24 h window.
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.
5151

52-
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.
52+
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.
5353

5454
## 5. `optim_status: Infeasible` triage order
5555

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` defaults: if only one is passed at runtime, EMHASS sets the other equal to it; if neither is passed, both fall back to `battery_target_state_of_charge` from the static config. Passing both with different values is also valid: EMHASS uses them as-is. For a basic rolling-MPC setup, passing only `soc_init` is enough; for systems that compute a dynamic end-of-horizon target (e.g. "must be at 60% by tomorrow 06:00"), pass both explicitly.
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 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.
3434
```
3535

3636
## Run

0 commit comments

Comments
 (0)