Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
4ad215d
feat: add stock-id field in Storage and DB flex model schemas
Ahmad-Wahid Feb 25, 2026
65fc268
feat: build stock groups
Ahmad-Wahid Feb 25, 2026
ef7cf60
feat: get stock groups
Ahmad-Wahid Feb 25, 2026
55ded46
feat: add a test case for multi feed stock
Ahmad-Wahid Feb 25, 2026
d740c0d
Merge remote-tracking branch 'origin/feat/multi-commodity' into feat/…
Ahmad-Wahid Mar 4, 2026
8bd859f
feat: add support for shared storage
Ahmad-Wahid Mar 5, 2026
6658803
remove the breakpoint
Ahmad-Wahid Mar 5, 2026
d500052
feat: update the test case for two devices with shared stock
Ahmad-Wahid Mar 5, 2026
09e9780
feat: add assertions with clear reasons
Ahmad-Wahid Mar 5, 2026
d4a15eb
Add support for multi-device charging of shared storage
Ahmad-Wahid Mar 12, 2026
26a1993
fix: sum all devices soc contribution, and use individual device effi…
Ahmad-Wahid Mar 23, 2026
c98b178
update test case for multi feed stock
Ahmad-Wahid Mar 13, 2026
358afb8
expect to charge the battery early to see the effect of fully discharge
Ahmad-Wahid Mar 23, 2026
4932cf9
fix: update the assert statements according to the scheduler results
Ahmad-Wahid Mar 23, 2026
b8ff719
Merge remote-tracking branch 'origin/feat/multi-commodity' into feat/…
Flix6x Mar 23, 2026
29785fa
dev: first step in resolving merge conflicts
Flix6x Mar 23, 2026
cefe507
chore: code annotation
Flix6x Mar 23, 2026
118587b
fix: not all flex-models have sensors
Flix6x Mar 23, 2026
74b665f
fix: static method has no self
Flix6x Mar 31, 2026
fbcf2e5
delete: remove inapplicable fields for stock model
Flix6x Mar 31, 2026
4259ffa
fix: fix interpretation of test results
Flix6x Mar 31, 2026
bc3991a
fix: move initialization of ems_constraints
Flix6x Mar 31, 2026
aefaf0d
fix: resolve merge conflicts on _build_soc_schedule, copied from Ahmad
Flix6x Mar 31, 2026
63b6bd7
fix: remove redundant code block
Flix6x Mar 31, 2026
0be435f
dev: use "state-of-charge" key instead of "sensor" key for stock models
Flix6x Mar 31, 2026
123f543
fix: skip StockCommitment for device models that outsource their stoc…
Flix6x Mar 31, 2026
f02e2ee
fix: old flex models that describe a device that serves both as a fee…
Flix6x Mar 31, 2026
5fb576e
fix: model stock devices using the state-of-charge field instead of t…
Flix6x Mar 31, 2026
cb110a9
fix: identify asset to merge with db flex-model
Flix6x Mar 31, 2026
b5bb77e
fix: validation
Flix6x Mar 31, 2026
816eda7
fix: flex-model setup in test
Flix6x Mar 31, 2026
1d5433f
fix: create stock group
Ahmad-Wahid Apr 4, 2026
eeffbf3
use soc-sensor in case of missing power sensor and also correct stock…
Ahmad-Wahid Apr 4, 2026
d8cab12
fix: create stock model for a model which has itself stock
Ahmad-Wahid Apr 5, 2026
ba7b433
update the assert statements
Ahmad-Wahid Apr 5, 2026
ea96b53
fix: merge conflicts
Ahmad-Wahid Apr 5, 2026
a229502
remove stock-id field
Ahmad-Wahid Apr 5, 2026
8190044
fix: correct the stock groups
Ahmad-Wahid Apr 7, 2026
177154c
refactor: remove unneccessary test function
Ahmad-Wahid Apr 9, 2026
a110f0e
fix: shared soc-gain, soc-usage, soc-minima and soc-maxima
Flix6x Apr 9, 2026
c7679ef
fix: shared StockCommitment for preferring a full SoC
Flix6x Apr 9, 2026
53d27c7
dev: todo
Flix6x Apr 9, 2026
69b5e27
dev: add "test" test case
Flix6x Apr 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 47 additions & 3 deletions flexmeasures/data/models/planning/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

from collections import defaultdict
from collections.abc import Iterable
from dataclasses import dataclass, field
from datetime import datetime, timedelta
Expand Down Expand Up @@ -52,6 +53,7 @@ class Scheduler:

flex_model: list[dict] | dict | None = None
flex_context: dict | None = None
stock_groups: dict | None = None

fallback_scheduler_class: "Type[Scheduler] | None" = None
info: dict | None = None
Expand All @@ -64,6 +66,41 @@ class Scheduler:

return_multiple: bool = False

@staticmethod
def _build_stock_groups(flex_model: list[dict]) -> dict:
"""
Build stock groups where devices sharing the same state-of-charge sensor are grouped together.
"""
groups = defaultdict(list)
soc_usage = defaultdict(list)

for d, fm in enumerate(flex_model):
if fm.get("sensor") is None:
continue

soc = fm.get("state_of_charge")
if soc is not None:
if hasattr(soc, "id"):
soc_id = soc.id
elif isinstance(soc, dict) and "sensor" in soc:
sensor = soc["sensor"]
soc_id = sensor.id if hasattr(sensor, "id") else sensor
else:
soc_id = soc

soc_usage[soc_id].append(d)

for soc_id, device_list in soc_usage.items():
groups[soc_id] = device_list

missing_soc_sensor_i = -len(flex_model)
for d, fm in enumerate(flex_model):
if fm.get("sensor") is not None and fm.get("state_of_charge") is None:
groups[missing_soc_sensor_i].append(d)
missing_soc_sensor_i += 1

return dict(groups)

def __init__(
self,
sensor: Sensor | None = None, # deprecated
Expand Down Expand Up @@ -202,12 +239,19 @@ def collect_flex_config(self):
# Listify the flex-model for the next code block, which actually does the merging with the db_flex_model
flex_model = [flex_model]

# Find which asset is relevant for a given device model in the flex-model from the trigger message
for flex_model_d in flex_model:
asset_id = flex_model_d.get("asset")
if asset_id is None:
sensor_id = flex_model_d["sensor"]
sensor = db.session.get(Sensor, sensor_id)
asset_id = sensor.asset_id
sensor_id = flex_model_d.get("sensor")
if sensor_id is not None:
sensor = db.session.get(Sensor, sensor_id)
asset_id = sensor.asset_id
else:
soc_sensor_ref = flex_model_d.get("state-of-charge")
if soc_sensor_ref is not None:
soc_sensor = db.session.get(Sensor, soc_sensor_ref["sensor"])
asset_id = soc_sensor.asset_id
if asset_id in db_flex_model:
flex_model_d = {**db_flex_model[asset_id], **flex_model_d}
amended_flex_model.append(flex_model_d)
Expand Down
95 changes: 78 additions & 17 deletions flexmeasures/data/models/planning/linear_optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ def device_scheduler( # noqa C901
commitment_upwards_deviation_price: list[pd.Series] | list[float] | None = None,
commitments: list[pd.DataFrame] | list[Commitment] | None = None,
initial_stock: float | list[float] = 0,
stock_groups: dict[int, list[int]] | None = None,
) -> tuple[list[pd.Series], float, SolverResults, ConcreteModel]:
"""This generic device scheduler is able to handle an EMS with multiple devices,
with various types of constraints on the EMS level and on the device level,
Expand Down Expand Up @@ -100,6 +101,22 @@ def device_scheduler( # noqa C901
resolution = pd.to_timedelta(device_constraints[0].index.freq).to_pytimedelta()
end = device_constraints[0].index.to_pydatetime()[-1] + resolution

# map device → stock group
device_to_group = {}

if stock_groups:
for g, devices in stock_groups.items():
for d in devices:
device_to_group[d] = g
# For devices not in any stock group (e.g., inflexible devices),
# map them to themselves so they're treated as individual groups
for d in range(len(device_constraints)):
if d not in device_to_group:
device_to_group[d] = d
else:
for d in range(len(device_constraints)):
device_to_group[d] = d

# Move commitments from old structure to new
if commitments is None:
commitments = []
Expand Down Expand Up @@ -484,33 +501,77 @@ def grouped_commitment_equalities(m, c, j, g):
)
model.commitment_sign = Var(model.c, domain=Binary, initialize=0)

# def _get_stock_change(m, d, j):
# """Determine final stock change of device d until time j.
#
# Apply conversion efficiencies to conversion from flow to stock change and vice versa,
# and apply storage efficiencies to stock levels from one datetime to the next.
# """
# if isinstance(initial_stock, list):
# # No initial stock defined for inflexible device
# initial_stock_d = initial_stock[d] if d < len(initial_stock) else 0
# else:
# initial_stock_d = initial_stock
#
# stock_changes = [
# (
# m.device_power_down[d, k] / m.device_derivative_down_efficiency[d, k]
# + m.device_power_up[d, k] * m.device_derivative_up_efficiency[d, k]
# + m.stock_delta[d, k]
# )
# for k in range(0, j + 1)
# ]
# efficiencies = [m.device_efficiency[d, k] for k in range(0, j + 1)]
# final_stock_change = [
# stock - initial_stock_d
# for stock in apply_stock_changes_and_losses(
# initial_stock_d, stock_changes, efficiencies
# )
# ][-1]
# return final_stock_change

def _get_stock_change(m, d, j):
"""Determine final stock change of device d until time j.

Apply conversion efficiencies to conversion from flow to stock change and vice versa,
and apply storage efficiencies to stock levels from one datetime to the next.
"""
# determine the stock group of this device
group = device_to_group[d]

# all devices belonging to this stock
devices = [dev for dev, g in device_to_group.items() if g == group]

# initial stock
if isinstance(initial_stock, list):
# No initial stock defined for inflexible device
initial_stock_d = initial_stock[d] if d < len(initial_stock) else 0
initial_stock_g = initial_stock[d] if d < len(initial_stock) else 0
else:
initial_stock_d = initial_stock
initial_stock_g = initial_stock

stock_changes = []

for k in range(0, j + 1):

change = 0

for dev in devices:
change += (
m.device_power_down[dev, k]
/ m.device_derivative_down_efficiency[dev, k]
+ m.device_power_up[dev, k]
* m.device_derivative_up_efficiency[dev, k]
+ m.stock_delta[dev, k]
)

stock_changes.append(change)

stock_changes = [
(
m.device_power_down[d, k] / m.device_derivative_down_efficiency[d, k]
+ m.device_power_up[d, k] * m.device_derivative_up_efficiency[d, k]
+ m.stock_delta[d, k]
)
for k in range(0, j + 1)
]
efficiencies = [m.device_efficiency[d, k] for k in range(0, j + 1)]

final_stock_change = [
stock - initial_stock_d
stock - initial_stock_g
for stock in apply_stock_changes_and_losses(
initial_stock_d, stock_changes, efficiencies
initial_stock_g,
stock_changes,
efficiencies,
)
][-1]

return final_stock_change

# Add constraints as a tuple of (lower bound, value, upper bound)
Expand Down
Loading
Loading