Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 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
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
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 @@ -718,6 +717,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