Skip to content

Share storage-efficiency per stock group and index solver stock by group#2325

Merged
Flix6x merged 12 commits into
mainfrom
feat/shared-storage-efficiency
Jul 17, 2026
Merged

Share storage-efficiency per stock group and index solver stock by group#2325
Flix6x merged 12 commits into
mainfrom
feat/shared-storage-efficiency

Conversation

@Flix6x

@Flix6x Flix6x commented Jul 17, 2026

Copy link
Copy Markdown
Member

Description

Closes #2324. Originally stacked on #2282; now that #2282 has merged, this PR targets main directly.

Since #2321, a shared stock's SoC parameters live on a single flex-model entry and all group members receive the group's initial stock — but storage-efficiency remained a per-device field, silently allowing members of one stock group to declare different loss rates for the same physical stock. This PR:

  • Treats storage-efficiency as a property of the stock: it may be defined on the entry holding the stock's SoC parameters (including stock-only entries, where it was previously ignored) or on exactly one member device, and applies to all members. Conflicting definitions fail fast, mirroring Track devices via a typed device inventory in the StorageScheduler #2321's handling of the other SoC parameters. This resolves the long-standing # todo: move storage-efficiency into a shared parameter… in storage.py.
  • Indexes the solver's stock variable and balance recursion by stock group instead of by device (follow-up to Speed up device_scheduler with a recursive stock balance #2282, as suggested in its review): one recursion per shared stock instead of one per member, dropping the redundant secondary stock variables/constraints. device_scheduler validates that grouped devices share their storage efficiency and initial stock, so direct callers get a clear error instead of silently ambiguous semantics.

Bugfix

While wiring this up, a latent key collision surfaced in device_to_group: declared stock groups are keyed by state-of-charge sensor id, while ungrouped devices used their own device index as group key. An ungrouped device whose index equalled a SoC sensor id was silently merged into that stock group, feeding its flows into the wrong stock — test_simulation_with_dynamic_consumption_capacity actually exercised this (the building's raw electricity load was counted as heat inflow into the buffer). Group keys are now namespaced.

How to test

pytest flexmeasures/data/models/planning/tests/test_commitments.py -k storage_efficiency
pytest flexmeasures/data/models/planning/tests/

🤖 Generated with Claude Code

Flix6x and others added 7 commits July 10, 2026 21:38
`_get_stock_change` expressed a device's stock at step j as a running sum
over all earlier steps, so the number of model nonzeros grew quadratically
with the scheduling horizon (e.g. ~12k nonzeros at 12h, ~349k at 3 days).
That dominated solve time on longer horizons.

Introduce an explicit `device_stock` variable coupled by a recursive
`device_stock_balance` constraint (stock[j] = a*stock[j-1] + b*change[j],
with a, b the per-step loss coefficients that apply_stock_changes_and_losses
computes). This is mathematically equivalent -- verified identical
objectives across horizons and against the planning test suite -- and keeps
the number of nonzeros linear in the horizon. Solve time drops ~10x at the
2-day default horizon and ~23x at 3 days.

Found while investigating SeitaBV/ems#172.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqGFrHfGrBHAJyjdAyr3y
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqGFrHfGrBHAJyjdAyr3y
…ce lists

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The recursion formulation lands on a cost-equal alternative vertex (A charges 3,
B discharges 1 for free under net site metering). Make device B lossy in both
directions so the optimum is unique again.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…oup (#2324)

- storage-efficiency may now be defined on the entry holding a shared stock's
  SoC parameters (or on exactly one member device) and applies to all members;
  conflicting definitions fail fast, mirroring how #2321 treats the other SoC
  parameters.
- device_scheduler now models one stock variable and one balance recursion per
  stock group instead of per device, dropping the redundant secondary
  recursions, and validates that grouped devices share their storage
  efficiency and initial stock.
- Fix a latent key collision in device_to_group: an ungrouped device whose
  index equalled a state-of-charge sensor id was silently merged into that
  stock group, feeding its flows into the wrong stock.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@read-the-docs-community

read-the-docs-community Bot commented Jul 17, 2026

Copy link
Copy Markdown

@Flix6x Flix6x added this to the 1.0.0 milestone Jul 17, 2026
@Flix6x Flix6x added bug Something isn't working Scheduling labels Jul 17, 2026
@Flix6x

Flix6x commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

Requirement from the off-tick SoC projection work (#2194): when off-tick SoC constraints auto-enable relax-soc-constraints, we want the softening scoped to the device(s) actually using off-tick constraints — other devices in a multi-device flex-model should keep their hard SoC constraints.

The tricky case is a shared stock: since #2321, its SoC parameters (incl. soc-targets/soc-minima/soc-maxima) live on a single flex-model entry — possibly a stock-only entry without a power sensor — while the constraints are applied (and softened into StockCommitments) on the first device of the stock group. So scoping cannot key on the entry's own power sensor; it needs the mapping entry → stock key → the device that applies the stock's constraints.

Since this PR already treats the stock group as the canonical unit in the solver (group-indexed stock recursion, shared storage-efficiency), it would be good to tackle that mapping here:

  1. Expose a canonical stock_key → member devices / stock_key → applying device lookup (e.g. on DeviceInventory), so deserialization-time logic can resolve which device a stock entry's SoC constraints land on.
  2. Ideally, go one step further and attach the soft SoC StockCommitments to the stock group rather than to a device index — matching the new group-indexed stock variable — so per-stock scoping (and result mapping) becomes structural instead of positional.

With that in place, #2194's relaxation scoping can track stock keys instead of power sensor ids, which also fixes the case where a member device entry carries the SoC parameters of a stock whose first device is a different entry.

…angelog

The buffer's storage-efficiency (previously ignored, with a 'does not work yet'
todo) is now honored, so enable it at 99% per 15 minutes and update the
expected schedules: the heater covers the loss-induced remainder beyond the
maxed-out boiler around the clock, and the boiler tops up the buffer in the
final hour of the cheap-electricity window.

Also fold the two changelog entries into the existing shared-storage feature
entry for v1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Base automatically changed from perf/linear-stock-recursion to main July 17, 2026 13:37
Enabler for scoping #2194's SoC-constraint relaxation per stock:

- StockCommitment gains a 'stock' attribute holding the stock key; the
  StorageScheduler stamps it on all soft SoC commitments (soc-minima,
  soc-maxima, prefer-full).
- The solver couples stock-scoped commitments to the stock group as a whole
  (via the group's first device, whose stock is the group's stock), regardless
  of which device index the commitment names.
- DeviceInventory.stock_constraint_device(stock_key) exposes the device that
  applies a stock's SoC constraints, complementing stock_groups for
  deserialization-time scoping logic.
- The solver's grouped-device efficiency guard now tolerates a missing
  efficiency column (the default, lossless case), which direct callers of
  device_scheduler commonly omit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Flix6x

Flix6x commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

Done in 239c6da — both parts:

  1. Lookups: DeviceInventory.stock_groups was already the canonical stock_key → member devices map; added DeviceInventory.stock_constraint_device(stock_key) for the device that applies a stock's SoC constraints (the group's first member). Together these give Feat/offtick soc projection #2194 the entry → stock key → applying device resolution at deserialization time.

  2. Structural attachment: StockCommitment gains a scalar stock attribute (the stock key), stamped by the StorageScheduler on all soft SoC commitments (soc-minima, soc-maxima, prefer-full-sooner). In the solver, a stock-scoped commitment resolves its device group from the stock key — coupling to the group's stock via the group's first device — so the device index it names no longer matters. A new parametrized test proves the commitment binds the group stock identically whether it names the group's first or second device (the previously broken case you mentioned). With this, Feat/offtick soc projection #2194's relaxation scoping can track stock keys throughout.

Two observations from the trenches, both out of scope here:

  • device_stock_commitment_equalities in linear_optimization.py is dead code — it's never registered as a model constraint; all device-level commitments route through grouped_commitment_equalities. Could be removed.
  • ems_flow_commitment_equalities returns (None, center, None) bounds, i.e. EMS-level FlowCommitments (no device, no commodity filter) generate no coupling constraint at all, leaving their deviation variables free. The StorageScheduler always sets device/commodity so production paths are unaffected, but direct device_scheduler callers passing an EMS-level FlowCommitment get an unbounded model (I hit this writing the new test). Worth a follow-up issue?

@Flix6x

Flix6x commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

Follow-up to my earlier comment, from the #2194 work just pushed: besides the relaxation scoping, #2194 now also records at which time the starting state of charge is actually known (when soc-at-start is resolved from the state-of-charge field), keyed by the device's power sensor id (soc_at_start_datetimes), to project an off-tick starting SoC onto the next scheduling tick.

For a shared stock, that timing is a property of the stock (the measurement pertains to the stock's SoC, resolved on the entry holding the SoC parameters), not of any particular member device. So when this PR introduces the canonical stock_key → devices mapping, both trackers from #2194 should move to stock keys in the same sweep:

  • off_tick_soc_sensor_ids (scoping of auto-enabled SoC relaxation), and
  • soc_at_start_datetimes (off-tick starting-SoC projection).

Both currently key on the power sensor id and rely on "the first device of the stock group applies the stock's constraints", which is exactly the positional convention this PR is in a good place to replace.

@Flix6x Flix6x mentioned this pull request Jul 17, 2026
7 tasks
It was never registered as a model constraint; all device-level commitments
route through grouped_commitment_equalities.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Flix6x

Flix6x commented Jul 17, 2026

Copy link
Copy Markdown
Member Author

All three follow-ups handled:

Flix6x added 2 commits July 17, 2026 16:33
…fficiency

# Conflicts:
#	flexmeasures/data/models/planning/linear_optimization.py
…fficiency

# Conflicts:
#	flexmeasures/data/models/planning/tests/test_commitments.py
@Flix6x
Flix6x merged commit 209b518 into main Jul 17, 2026
13 checks passed
@Flix6x
Flix6x deleted the feat/shared-storage-efficiency branch July 17, 2026 15:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working refactoring Scheduling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Share storage-efficiency per stock, then index solver stock variables by stock group

1 participant