Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
5be555b
support commodity based EMS flow commitments and grouped devices
Ahmad-Wahid Feb 3, 2026
6f2d745
Add commodity field and support multi-device commitments
Ahmad-Wahid Feb 3, 2026
1d63893
test shared buffer and multi-comodity commitments
Ahmad-Wahid Feb 3, 2026
8c9b1b5
remove hard check for commodity to make backward compatible
Ahmad-Wahid Feb 3, 2026
a8f3b89
update the function to support backward compatibility
Ahmad-Wahid Feb 3, 2026
be09c4d
feat: add commodity field to the flexmodel and DBstorage-flex-model s…
Ahmad-Wahid Feb 10, 2026
07822eb
fix: use devices as index rather than time series
Flix6x Feb 10, 2026
92eabc7
fix: exclude gas-power devices from electricity commitments
Flix6x Feb 10, 2026
f9df705
Merge branch 'feat/switching-between-gas-and-electricity' into feat/m…
Flix6x Feb 10, 2026
325bfe8
feat: add gas-price field to the Flex-context schema
Ahmad-Wahid Feb 10, 2026
9261629
apply black
Ahmad-Wahid Feb 10, 2026
9dd5802
feat: add a test case for two flexible devices with commodity
Ahmad-Wahid Feb 10, 2026
b22c6d7
use expected datatypes
Ahmad-Wahid Feb 16, 2026
c88af5c
feat: split commitments per commodity
Ahmad-Wahid Feb 16, 2026
c2341f1
feat: split commitments per commodity
Ahmad-Wahid Feb 16, 2026
88d4be0
Merge remote-tracking branch 'origin/feat/multi-commodity' into feat/…
Ahmad-Wahid Feb 16, 2026
be05a19
Revert "use expected datatypes"
Ahmad-Wahid Feb 16, 2026
f4ffd8a
feat: add a test case for different commodities
Ahmad-Wahid Feb 16, 2026
c43d2ad
fix: do not produce gas
Flix6x Feb 20, 2026
0aa5b2b
feat: create a flow commitment for prefering to charge sooner devices
Ahmad-Wahid Feb 26, 2026
a48c6ba
add soc constraints for boiler
Ahmad-Wahid Feb 26, 2026
c58965f
add some assert statments
Ahmad-Wahid Feb 26, 2026
515f34a
update and add new assertions with clear explanation
Ahmad-Wahid Mar 4, 2026
4d77344
update the docstring
Ahmad-Wahid Mar 4, 2026
2becd02
refactor: move tiny-price-slope decleration out of the for loop
Ahmad-Wahid Mar 6, 2026
d9d4f94
Revert "refactor: move tiny-price-slope decleration out of the for loop"
Ahmad-Wahid Mar 6, 2026
2a22fae
refactor: move tiny-price-slope decleration out of the for loop
Ahmad-Wahid Mar 6, 2026
f38d6d8
fix: add data_key attr
Ahmad-Wahid Mar 6, 2026
007e514
add missing commodity description and it's field in ui flexmodel schema
Ahmad-Wahid Mar 6, 2026
0bff8bc
fix: add missing gas-price field in UI Flexcontext schema
Ahmad-Wahid Mar 6, 2026
82f807e
fix: wrong timezone; the test relied on the preference to charge soon…
Flix6x Mar 13, 2026
128550f
feat: move preference to charge sooner and discharge later into a Sto…
Flix6x Mar 13, 2026
130b9dd
fix: test case no longer relies on arbitrage opportunity coming from …
Flix6x Mar 13, 2026
1eab828
feat: check for optimal schedule
Flix6x Mar 13, 2026
b9bc4a9
feat: prefer a full storage earlier over later
Flix6x Mar 13, 2026
57df5c3
docs: update commitment name and inline comments
Flix6x Mar 13, 2026
ce71637
docs: touch up test explanation
Flix6x Mar 14, 2026
f6183df
fix: update test case given preference for a full battery
Flix6x Mar 14, 2026
ed471a8
delete: clean up comment
Flix6x Mar 14, 2026
7611fb9
feat: model the preference to curtail later within the same StockComm…
Flix6x Mar 14, 2026
bf16e63
fix: reduce tiny price slope
Flix6x Mar 14, 2026
06c30dc
docs: delete duplicate changelog entry
Flix6x Mar 16, 2026
0125e28
Merge remote-tracking branch 'origin/main' into feat/full-soc-preference
Flix6x Mar 18, 2026
d99089b
docs: fix broken link
Flix6x Mar 18, 2026
cf01f1d
Revert "fix: reduce tiny price slope"
Flix6x Mar 18, 2026
bdbdead
fix: soc unit conversion
Flix6x Mar 18, 2026
f987706
fix: adapt test to check for 1 hour of free energy at 15-min scheduli…
Flix6x Mar 18, 2026
7acbc99
Merge remote-tracking branch 'origin/feat/full-soc-preference' into f…
Flix6x Mar 19, 2026
c09371a
Merge branch 'feat/switching-between-gas-and-electricity' into feat/m…
Flix6x Mar 19, 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
2 changes: 1 addition & 1 deletion documentation/dev/setup-and-guidelines.rst
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ On Linux and Windows, everything will be installed using Python packages.
On MacOS, this will install all test dependencies, and locally install the HiGHS solver.
For this to work, make sure you have `Homebrew <https://brew.sh/>`_ installed.

Besides the HiGHS solver (as the current default), the CBC solver is required for tests as well. See `The install instructions <https://github.com/coin-or/Cbc?tab=readme-ov-file#binaries`_ for more information.
Besides the HiGHS solver (as the current default), the CBC solver is required for tests as well. See `The install instructions <https://github.com/coin-or/Cbc?tab=readme-ov-file#binaries>`_ for more information.

Configuration
^^^^^^^^^^^^^
Expand Down
34 changes: 32 additions & 2 deletions flexmeasures/data/models/planning/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,12 +288,25 @@ class Commitment:
quantity: pd.Series = 0
upwards_deviation_price: pd.Series = 0
downwards_deviation_price: pd.Series = 0
commodity: str | pd.Series | None = None

def __post_init__(self):
if (
isinstance(self, FlowCommitment)
and isinstance(self.commodity, pd.Series)
and self.device is not None
):
devices = extract_devices(self.device)
missing = set(devices) - set(self.commodity.index)
if missing:
raise ValueError(f"commodity mapping missing for devices: {missing}")

series_attributes = [
attr
for attr, _type in self.__annotations__.items()
if _type == "pd.Series" and hasattr(self, attr)
if _type == "pd.Series"
and hasattr(self, attr)
and attr not in ("device_group", "commodity")
]
for series_attr in series_attributes:
val = getattr(self, series_attr)
Expand Down Expand Up @@ -386,6 +399,10 @@ def _init_device_group(self):
range(len(devices)), index=devices, name="device_group"
)
else:
if not isinstance(self.device_group, pd.Series):
self.device_group = pd.Series(
self.device_group, index=devices, name="device_group"
)
# Validate custom grouping
missing = set(devices) - set(self.device_group.index)
if missing:
Expand Down Expand Up @@ -435,12 +452,25 @@ def to_frame(self) -> pd.DataFrame:
],
axis=1,
)
# map device → device_group
# device_group
if self.device is not None:
df["device_group"] = map_device_to_group(self.device, self.device_group)
else:
df["device_group"] = 0

# commodity
if getattr(self, "commodity", None) is None:
df["commodity"] = None
elif isinstance(self.commodity, pd.Series):
# commodity is a device→commodity mapping, like device_group
if self.device is None:
df["commodity"] = None
else:
df["commodity"] = map_device_to_group(self.device, self.commodity)
else:
# scalar commodity
df["commodity"] = self.commodity

return df


Expand Down
81 changes: 48 additions & 33 deletions flexmeasures/data/models/planning/linear_optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,20 @@ def device_scheduler( # noqa C901
df["group"] = group
commitments.append(df)

# commodity → set(device indices)
commodity_devices = {}

for df in commitments:
if "commodity" not in df.columns or "device" not in df.columns:
continue

for _, row in df[["commodity", "device"]].dropna().iterrows():
devices = row["device"]
if not isinstance(devices, (list, tuple, set)):
devices = [devices]

commodity_devices.setdefault(row["commodity"], set()).update(devices)

# Check if commitments have the same time window and resolution as the constraints
for commitment in commitments:
start_c = commitment.index.to_pydatetime()[0]
Expand Down Expand Up @@ -579,45 +593,30 @@ def device_stock_commitment_equalities(m, c, j, d):
)

def ems_flow_commitment_equalities(m, c, j):
"""Couple EMS flows (sum over devices) to each commitment.
"""Couple EMS flow commitments to device flows, optionally filtered by commodity."""

- Creates an inequality for one-sided commitments.
- Creates an equality for two-sided commitments and for groups of size 1.
"""
if (
"device" in commitments[c].columns
and not pd.isnull(commitments[c]["device"]).all()
) or m.commitment_quantity[c, j] == -infinity:
# Commitment c does not concern EMS
if commitments[c]["class"].iloc[0] != FlowCommitment:
return Constraint.Skip
if (
"class" in commitments[c].columns
and not (
commitments[c]["class"].apply(lambda cl: cl == FlowCommitment)
).all()
):
raise NotImplementedError(
"StockCommitment on an EMS level has not been implemented. Please file a GitHub ticket explaining your use case."
)

# Legacy behavior: no commodity → sum over all devices
if "commodity" not in commitments[c].columns:
devices = m.d
else:
commodity = commitments[c]["commodity"].iloc[0]
if pd.isna(commodity):
devices = m.d
else:
devices = commodity_devices.get(commodity, set())
if not devices:
return Constraint.Skip

return (
(
0
if len(commitments[c]) == 1
or "upwards deviation price" in commitments[c].columns
else None
),
# 0 if "upwards deviation price" in commitments[c].columns else None, # todo: possible simplification
None,
m.commitment_quantity[c, j]
+ m.commitment_downwards_deviation[c]
+ m.commitment_upwards_deviation[c]
- sum(m.ems_power[:, j]),
(
0
if len(commitments[c]) == 1
or "downwards deviation price" in commitments[c].columns
else None
),
# 0 if "downwards deviation price" in commitments[c].columns else None, # todo: possible simplification
- sum(m.ems_power[d, j] for d in devices),
None,
)

def device_derivative_equalities(m, d, j):
Expand Down Expand Up @@ -726,6 +725,22 @@ def cost_function(m):
)

model.commitment_costs = commitment_costs
commodity_costs = {}
for c in model.c:
commodity = None
if "commodity" in commitments[c].columns:
commodity = commitments[c]["commodity"].iloc[0]
if commodity is None or (isinstance(commodity, float) and np.isnan(commodity)):
continue

cost = value(
model.commitment_downwards_deviation[c] * model.down_price[c]
+ model.commitment_upwards_deviation[c] * model.up_price[c]
)
commodity_costs[commodity] = commodity_costs.get(commodity, 0) + cost

model.commodity_costs = commodity_costs

# model.pprint()
# model.display()
# print(results.solver.termination_condition)
Expand Down
Loading
Loading