Skip to content

Feat/offtick soc projection#2194

Merged
Flix6x merged 83 commits into
mainfrom
feat/offtick-soc-normalization
Jul 17, 2026
Merged

Feat/offtick soc projection#2194
Flix6x merged 83 commits into
mainfrom
feat/offtick-soc-normalization

Conversation

@BelhsanHmida

@BelhsanHmida BelhsanHmida commented May 22, 2026

Copy link
Copy Markdown
Contributor

Description

  • Project off-tick storage SoC constraints onto the scheduling ticks using charge/discharge capacity, instead of blindly flooring timed soc-targets, soc-minima, and soc-maxima.
  • Preserve original SoC event times during storage flex-model schema deserialization so scheduler logic can interpret the user’s intent.
  • Automatically enable relax-soc-constraints when off-tick SoC constraints are submitted, so generated/adjusted constraints can be softened safely.
  • Added tests for off-tick SoC target projection and automatic SoC constraint relaxation.
  • add changelog entry.

How to test

pytest flexmeasures/api/v3_0/tests/test_sensor_schedules.py flexmeasures/data/schemas/tests/test_scheduling.py flexmeasures/data/models/planning/tests/test_storage.py -q

Manual test:

  • Trigger a storage schedule with a 15-minute resolution and an off-tick soc-target, e.g. 17:12.
  • Use 0.04 MW charging capacity and target 1 MWh.
  • Verify the scheduler does not floor the target to 17:00, but projects it onto the scheduling ticks as an adjusted previous-tick bound and a next-tick target.
  • Verify off-tick SoC constraints enable relax-soc-constraints.

Projection policy

The scheduler can enforce SoC constraints only on its scheduling ticks. For point-like off-tick SoC events:

  • soc-targets are projected to an exact target on the next scheduling tick, with capacity-adjusted lower and upper bounds on the previous tick.
  • soc-minima are projected as lower bounds on the surrounding ticks, adjusted by reachable charge/discharge capacity.
  • soc-maxima are projected as upper bounds on the surrounding ticks, adjusted by reachable charge/discharge capacity.
  • If multiple projected lower bounds land on the same tick, the stricter lower bound is kept.
  • If multiple projected upper bounds land on the same tick, the stricter upper bound is kept.

Related Items

Follow-up to PR #2146 review discussion around off-tick storage SoC constraints.

Sign-off

  • I agree to license my contributions under the project’s license.
  • I signed off my commits.

Flix6x and others added 30 commits November 21, 2025 15:41
…ataFrames

Signed-off-by: F.N. Claessen <felix@seita.nl>
Signed-off-by: F.N. Claessen <felix@seita.nl>
Signed-off-by: F.N. Claessen <felix@seita.nl>
Signed-off-by: F.N. Claessen <felix@seita.nl>
Signed-off-by: F.N. Claessen <felix@seita.nl>
Signed-off-by: F.N. Claessen <felix@seita.nl>
# Conflicts:
#	flexmeasures/api/common/schemas/sensor_data.py
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
…nt-start

Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>

# Conflicts:
#	flexmeasures/api/v3_0/tests/test_sensor_schedules.py
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Signed-off-by: Mohamed Belhsan Hmida <149331360+BelhsanHmida@users.noreply.github.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…dule_floors_flex_model_datetimes

Signed-off-by: F.N. Claessen <claessen@seita.nl>
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>
Signed-off-by: Mohamed Belhsan Hmida <149331360+BelhsanHmida@users.noreply.github.com>
…nt-start

Signed-off-by: Mohamed Belhsan Hmida <mohamedbelhsanhmida@gmail.com>

# Conflicts:
#	flexmeasures/api/common/schemas/sensor_data.py
#	flexmeasures/api/v3_0/tests/test_sensor_data.py
#	flexmeasures/data/schemas/sensors.py
Flix6x and others added 16 commits July 17, 2026 14:09
Pure code move: extract the projection helpers, rule table and
project_off_tick_soc_constraints() from planning/storage.py into
planning/soc_projection.py, and update imports.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ions

The SocProjectionRule dataclass, policy table and five decoder helpers
abstracted just six fixed projection rules; writing the projections out
explicitly is shorter and easier to audit. Also simplify the reachable-energy
computation: the projection periods always lie within a single scheduling
tick, so no tick-walking loop is needed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ommitments

Previously the projection ran only in the hard-constraint path, after
soc-minima/soc-maxima with breach prices had already been converted to soft
StockCommitments (and nulled). Since off-tick SoC constraints auto-enable
relax-soc-constraints, off-tick minima/maxima would in practice reach the
commitment builder unprojected, while bounds projected from off-tick targets
were applied as hard constraints despite relaxation being enabled.

Now the projection runs right after the device capacities are known and
before the soft/hard split, so both paths consume on-tick events:
projected bounds are softened by breach prices exactly like user-given
bounds, and only fall back to hard constraints when no breach price is set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…lity

The reachable energy used to project off-tick SoC constraints now converts
grid power to stock change: charging at power P moves the stock at rate
P * charging_efficiency (which can exceed 1, e.g. a heat pump's COP), and
discharging at power P moves the stock at rate P / discharging_efficiency.

To that end, compute the (dis)charging efficiency series before the SoC
constraint handling in the device loop and pass them to the projection.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Off-tick soc-target datetimes are no longer floored during deserialization,
so a target that drives the schedule extension could set an off-tick schedule
end. Its projection onto the next scheduling tick would then fall beyond the
schedule end and be silently disregarded. Ceiling the extension keeps the
projected target within the schedule.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
… constraints

Auto-enabling SoC constraint relaxation used to silently override a user's
explicit choice to keep SoC constraints hard. Now the explicit False is
respected (with a logged warning that projection may add infeasible bounds),
and auto-enabling is logged as well.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
… dropped

Bounds projected from off-tick SoC events can only be merged into list-based
(or missing) soc-minima/soc-maxima specifications. When the counterpart field
is given as a sensor, series or fixed quantity, the projected bounds are
dropped; that is now logged instead of silent.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…alization

# Conflicts:
#	documentation/changelog.rst
Assigning new instance-level soc fields in __init__ never had any effect:
marshmallow resolves fields from the class-level declared fields, so the
flooring resolution (and timezone) passed to those instance fields was dead
code, contradicting this PR's preservation of off-tick SoC event datetimes.
Remove it, and instead realize the intended per-instance overrides (timezone
and default soc-unit) by setting attributes on the bound fields.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ction

When off-tick projection is disabled (floor_datetimes_to_resolution=False),
point-like SoC events between scheduling ticks match no entry in the
constraint series and were silently dropped. Log a warning instead, and
document the attribute's role as the projection opt-out, as well as the fact
that an explicit relax-soc-constraints: False is respected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…chema

The schema's pre_load detection used a guessed resolution (falling back to
the sensor's event resolution or 15 minutes even when the scheduler would
resolve a different scheduling resolution) and kept mutable per-load state on
the schema instance. Detect off-tick SoC events on the serialized flex-model
in the scheduler instead, using the scheduler's actual resolution, so the
relax decision matches what the projection will actually do.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…he soft path

- Pure test that reachable energy uses charging efficiency (incl. COP > 1)
  and discharging efficiency.
- A target beyond the schedule end extends it to the tick carrying the
  projected target (ceiled), instead of the target being disregarded.
- With a soc-minima breach price, projected off-tick minima feed the soft
  stock commitments rather than becoming hard constraints.
- Pin relax-constraints/relax-soc-constraints to False in the hard-path
  projection tests, since projected bounds are now softened by default.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
…ints

When relaxation is enabled purely because off-tick SoC constraints require
projection (i.e. the user's flex-context did not already soften SoC
constraints via relax flags or explicit breach prices), the softening now
applies only to the devices that actually use off-tick SoC constraints,
tracked by their power sensor. Other devices in a multi-device flex-model
keep their hard SoC constraints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Two devices via the serialized multi-sensor flex-model path: the off-tick
device's soc-minima are projected and softened into stock commitments, while
the on-tick device's soc-minima remain hard constraints.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Flix6x and others added 2 commits July 17, 2026 15:37
Following the original recipe in issue #10: when soc-at-start is resolved
from the state-of-charge field (an instantaneous sensor or time series) and
the underlying measurement was taken at an off-tick time within the first
scheduling interval, the SoC is assumed to hold from the schedule start until
that time, and the next scheduling tick receives an upper bound (value plus
chargeable energy since then) and a lower bound (value minus dischargeable
energy), efficiency-aware and clamped to the global soc-min/soc-max.

The measurement time is recorded (per power sensor) when resolving the
state-of-charge field, in both single-sensor and multi-device (shared stock)
modes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
A pure test for the next-tick bounds (efficiency-aware, no-op on ticks), and
an end-to-end test resolving the starting SoC from an off-tick
state-of-charge time series entry.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Flix6x added a commit that referenced this pull request Jul 17, 2026
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

Thanks @BelhsanHmida — the projection policy is sound and well-tested, and the target projection faithfully generalizes the original recipe in #10 (comment). I reviewed the branch in depth and, rather than a long list of change requests, pushed fixes for what I found directly onto your branch (17 commits, d255bd3c5..03fd551b0, plus a merge of main to pick up the OpenAPI hook fix). Summary of what was addressed:

Correctness issues found in review

  1. Projection ran only in the hard-constraint path — but with breach prices set (which the auto-enabled relaxation guarantees), soc-minima/soc-maxima were converted to soft StockCommitments (and nulled) before the projection ran. So off-tick minima/maxima reached the commitment builder unprojected, while target-derived bounds were applied as hard constraints despite relaxation. Fixed by projecting right after device capacities are known, before the soft/hard split (8ba7a524c).
  2. Off-tick targets near the schedule end were silently dropped: possibly_extend_end extended self.end to the raw off-tick time, so the projected next-tick target fell beyond the schedule end and was disregarded. The extension is now ceiled to the scheduling resolution (54c576bc4).
  3. An explicit relax-soc-constraints: False was silently overridden; it is now respected, with a logged warning (d2360b515).
  4. Projected bounds were silently dropped when the counterpart field is a sensor/series/quantity; now logged (dc57d1563). Same for off-tick events that match no tick when projection is opted out via floor_datetimes_to_resolution=False (3576a1ff3).
  5. The flooring machinery in StorageFlexModelSchema was inert: marshmallow resolves fields from class-level declared fields, so the instance-level soc field re-creations (carrying flooring_resolution and the timezone override) never took effect. Removed, with the intended timezone/soc-unit overrides realized via setattr on the bound fields (2e49f87eb).

Refinements

  • Projection logic moved to a dedicated module planning/soc_projection.py, with docstrings, and the rule-table indirection replaced by explicit projections (d255bd3c5, 289a5ccf2, ca83e9f82).
  • Reachable energy is now efficiency-aware: charging moves the stock at P × charging_efficiency (which can exceed 1, e.g. a heat pump's COP), discharging at P / discharging_efficiency (cd0b69ffb).
  • Off-tick detection moved from the schema's pre_load (guessed resolution, mutable schema state) into the scheduler, using its actual resolution (eafa169cf).
  • Auto-relaxation is now scoped to the devices that actually use off-tick constraints (tracked by power sensor) when the user's flex-context didn't already soften SoC constraints — other devices keep their hard constraints (f0a5cc203). Follow-up to key this on stock groups instead is parked on Share storage-efficiency per stock group and index solver stock by group #2325.
  • Off-tick soc-at-start support (per the issue API usage with datetimes that aren't at midnight or a multiple of the sensor resolution afterwards #10 recipe): when the starting SoC resolves from the state-of-charge field and the measurement time falls off-tick within the first scheduling interval, the next tick receives capacity/efficiency-adjusted bounds (db98b260d).
  • Tests for all of the above, including the multi-device flex-model path (d13e37b26, f5967625f, 03fd551b0). The pre-existing hard-path tests now pin relax-constraints/relax-soc-constraints: False, since projected bounds are softened by default after fix 1.

One policy note for future discussion (not changed on the branch): the capacity-loosened bounds for user-given soc-minima/soc-maxima preserve reachability but don't enforce the bound at the off-tick moment itself — since intra-tick SoC is linear, duplicating the bound unadjusted onto both surrounding ticks would guarantee it exactly, with the auto-enabled relaxation absorbing any infeasibility. Similarly, targets are guaranteed at the next tick (slightly late), which matches the original issue-10 recipe; exact attainment at the off-tick time would need an interpolated constraint in device_scheduler. Both could be follow-ups.
Personally, duplicating the bound unadjusted does not appeal to me, and I favour the approach we now implemented. For me, the follow-up should be adding interpolated constraints in device_scheduler, but I feel this might need an update to how we formulate a schedule in the API response, too, in which time slots would no longer share the same resolution. Definitively out of scope, and not even on our roadmap.

Please review the pushed commits — happy to adjust anything you disagree with.

@Flix6x Flix6x added this to the 1.0.0 milestone Jul 17, 2026
Both trackers previously keyed on the device's power sensor id and relied on
the positional convention that the first device of a stock group applies the
stock's constraints:

- off_tick_soc_sensor_ids becomes off_tick_stock_keys: entries are tracked by
  the stock key resolved from their serialized state-of-charge field, so
  scoping covers all devices sharing the stock (entries without a resolvable
  stock key fall back to a namespaced power-sensor key).
- soc_at_start_datetimes is keyed by stock key, threaded through the
  soc-at-start resolution chain (exact stock key in multi-device mode; a None
  key covers the single-sensor time-series case).

Adds a regression test for the shared-stock case: off-tick soc-minima on a
stock-only entry soften the whole group's constraints while an unrelated
device keeps its hard on-tick minima.

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

Flix6x commented Jul 17, 2026

Copy link
Copy Markdown
Member

Pushed 1a0b144 implementing the stock-key rekey requested on #2325:

  • off_tick_soc_sensor_idsoff_tick_stock_keys: entries are tracked by the stock key resolved from their serialized state-of-charge field (via _resolve_stock_key from Track devices via a typed device inventory in the StorageScheduler #2321), so relaxation scoping covers all devices sharing a stock — including the case where the SoC parameters live on a stock-only entry or on a member that isn't the group's first device. Entries without a resolvable stock key (no state-of-charge, or a time series) fall back to a namespaced ("sensor", power_sensor.id) key, preserving the previous per-device behavior for standalone devices.
  • soc_at_start_datetimes is now keyed by stock key, threaded through the soc-at-start resolution chain: exact stock key in multi-device mode (passed down from the stock-group loop in _prepare), the SoC sensor's id when resolving from a sensor, and a None key for the single-sensor time-series case (with a matching fallback at the projection site).
  • New regression test test_off_tick_soc_relaxation_covers_all_devices_of_a_shared_stock: off-tick soc-minima on a stock-only entry soften the shared group's constraints while an unrelated device keeps its hard on-tick minima.

Full planning suite passes on this branch (210 passed). Note: no dependency on #2325 — the rekey only uses #2321-level APIs, so the branches merge in either order (trivial textual overlap in _prepare's new device_stock_key lookup). Heads-up: your local fm-wt-pr2194 checkout is now one commit behind origin.

Flix6x and others added 2 commits July 17, 2026 16:27
…able

Reinstate a policy table, now minimal: with soc-at-start added there are
eight bound rules following one symmetric pattern, and each rule only needs
to state which bound lands on which surrounding tick. The capacity period,
charge-vs-discharge direction, efficiency treatment and loosening sign all
derive from those two choices (SocProjectionRule.uses_charging / .sign).
A small _SocProjection working state bundles the device's capacities,
efficiencies and SoC limits, and accumulates the projected bounds, removing
the four near-identical projection blocks and the long parameter threading.

Public signatures and behavior are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
On Python 3.10, module-level generic aliases such as list[dict[...]] pass
inspect.isclass (bpo-46080 changed this in 3.11) and then make issubclass
raise a TypeError in test_all_custom_fields_call_super_deserialize.
The new type aliases in soc_projection.py triggered exactly that.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YNPESudGKciPZq88nGJn3C
Signed-off-by: F.N. Claessen <claessen@seita.nl>
Flix6x added a commit that referenced this pull request Jul 17, 2026
…oup (#2325)

* Speed up device_scheduler with a recursive stock balance

`_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

* Reference PR #2282 in the changelog entry

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

* Address review: apply changelog suggestion and cache stock-group device lists

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

* Fix degenerate optimum in alignment regression test after merging main

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>

* Share storage-efficiency per stock group and index solver stock by group (#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>

* Add changelog entries for PR #2325

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

* Enable storage losses in the dynamic-capacity test and consolidate changelog

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>

* Attach soft SoC commitments to their stock, not to a device index

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>

* Remove dead device_stock_commitment_equalities

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>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…alization

# Conflicts:
#	flexmeasures/data/models/planning/storage.py
@Flix6x
Flix6x merged commit a5c4396 into main Jul 17, 2026
12 of 13 checks passed
@Flix6x
Flix6x deleted the feat/offtick-soc-normalization branch July 17, 2026 15:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants