Skip to content

Commit

Permalink
split stock delta into stock gain and stock usage
Browse files Browse the repository at this point in the history
Signed-off-by: Victor Garcia Reolid <victor@seita.nl>
  • Loading branch information
victorgarcia98 committed Dec 13, 2023
1 parent 06c41ac commit bf11552
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 84 deletions.
5 changes: 2 additions & 3 deletions documentation/concepts/device_scheduler.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Symbol Variable in the Code
:math:`P^{ems}_{max}(j)` ems_derivative_max Maximum flow of the EMS during time period :math:`j`.
:math:`Commitment(c,j)` commitment_quantity Commitment c (at EMS level) over time step :math:`j`.
:math:`M` M Large constant number, upper bound of :math:`Power_{up}(d,j)` and :math:`|Power_{down}(d,j)|`.
:math:`G(d,j)` stock_gain Explicit energy gain or loss of device :math:`d` during time period :math:`j`.
:math:`D(d,j)` stock_delta Explicit energy gain or loss of device :math:`d` during time period :math:`j`.
================================ ================================================ ==============================================================================================================


Expand Down Expand Up @@ -87,8 +87,7 @@ change of :math:`Stock(d,j)`, taking into account conversion efficiencies but no
.. math::
:name: stock
\Delta Stock(d,j) = \frac{P_{down}(d,j)}{\eta_{down}(d,j) } + P_{up}(d,j) \cdot \eta_{up}(d,j) + G(d,j)
\Delta Stock(d,j) = \frac{P_{down}(d,j)}{\eta_{down}(d,j) } + P_{up}(d,j) \cdot \eta_{up}(d,j) + D(d,j)
.. math::
Expand Down
18 changes: 9 additions & 9 deletions flexmeasures/data/models/planning/linear_optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ def device_scheduler( # noqa C901
derivative equals: exact amount of flow (we do this by clamping derivative min and derivative max)
derivative down efficiency: conversion efficiency of flow out of a device (flow out : stock decrease)
derivative up efficiency: conversion efficiency of flow into a device (stock increase : flow in)
stock gain: predefined stock delta to apply to the storage device. Positive quantity cause an increase and negative decrease (stock increase : flow in)
stock delta: predefined stock delta to apply to the storage device. Positive values cause an increase and negative values a decrease
EMS constraints are on an EMS level. Handled constraints (listed by column name):
derivative max: maximum flow
derivative min: minimum flow
Expand Down Expand Up @@ -104,11 +104,11 @@ def device_scheduler( # noqa C901
M = max(M, 1)

for d in range(len(device_constraints)):
if "stock gain" not in device_constraints[d].columns:
device_constraints[d]["stock gain"] = 0
if "stock delta" not in device_constraints[d].columns:
device_constraints[d]["stock delta"] = 0
else:
device_constraints[d]["stock gain"] = device_constraints[d][
"stock gain"
device_constraints[d]["stock delta"] = device_constraints[d][
"stock delta"
].fillna(0)

# Turn prices per commitment into prices per commitment flow
Expand Down Expand Up @@ -232,8 +232,8 @@ def device_derivative_up_efficiency(m, d, j):
return 1
return eff

def device_stock_gain(m, d, j):
return device_constraints[d]["stock gain"].iloc[j]
def device_stock_delta(m, d, j):
return device_constraints[d]["stock delta"].iloc[j]

model.up_price = Param(model.c, model.j, initialize=price_up_select)
model.down_price = Param(model.c, model.j, initialize=price_down_select)
Expand All @@ -257,7 +257,7 @@ def device_stock_gain(m, d, j):
model.device_derivative_up_efficiency = Param(
model.d, model.j, initialize=device_derivative_up_efficiency
)
model.stock_gain = Param(model.d, model.j, initialize=device_stock_gain)
model.stock_delta = Param(model.d, model.j, initialize=device_stock_delta)

# Add variables
model.ems_power = Var(model.d, model.j, domain=Reals, initialize=0)
Expand All @@ -281,7 +281,7 @@ def device_bounds(m, d, j):
(
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_gain[d, k]
+ m.stock_delta[d, k]
)
for k in range(0, j + 1)
]
Expand Down
49 changes: 24 additions & 25 deletions flexmeasures/data/models/planning/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ class MetaStorageScheduler(Scheduler):
"derivative min",
"derivative down efficiency",
"derivative up efficiency",
"stock gain",
"stock delta",
]

def compute_schedule(self) -> pd.Series | None:
Expand Down Expand Up @@ -234,37 +234,36 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901
max_value=convert_units(power_capacity_in_mw, "MW", sensor.unit),
)

stock_gain = self.flex_model.get("stock_gain", [])
soc_gain = self.flex_model.get("soc_gain", [])
soc_usage = self.flex_model.get("soc_usage", [])

all_stock_gain = []
all_stock_delta = []

for component in stock_gain:
stock_gain = get_continuous_series_sensor_or_quantity(
quantity_or_sensor=component,
actuator=sensor,
unit="MWh",
query_window=(start, end),
resolution=resolution,
beliefs_before=belief_time,
)
for is_usage, soc_delta in zip([False, True], [soc_gain, soc_usage]):
for component in soc_delta:
stock_delta_series = get_continuous_series_sensor_or_quantity(
quantity_or_sensor=component,
actuator=sensor,
unit="MWh",
query_window=(start, end),
resolution=resolution,
beliefs_before=belief_time,
)

if isinstance(component, Sensor):
from_unit = component.event_resolution
# By default, positive values for the gain sensor represent a stock
# gain and negative values stock losses.
# Invert the meaning by setting consumption_is_positive to True.
if component.get_attribute("consumption_is_positive", False):
stock_gain *= -1
if isinstance(component, Sensor):
from_unit = component.event_resolution
stock_delta_series *= resolution / from_unit

stock_gain *= resolution / from_unit
if is_usage:
stock_delta_series *= -1

all_stock_gain.append(stock_gain)
all_stock_delta.append(stock_delta_series)

if len(all_stock_gain) > 0:
all_stock_gain = pd.concat(all_stock_gain, axis=1)
if len(all_stock_delta) > 0:
all_stock_delta = pd.concat(all_stock_delta, axis=1)

device_constraints[0]["stock gain"] = all_stock_gain.sum(1)
device_constraints[0]["stock gain"] *= timedelta(hours=1) / resolution
device_constraints[0]["stock delta"] = all_stock_delta.sum(1)
device_constraints[0]["stock delta"] *= timedelta(hours=1) / resolution

# Apply round-trip efficiency evenly to charging and discharging
device_constraints[0]["derivative down efficiency"] = (
Expand Down
23 changes: 10 additions & 13 deletions flexmeasures/data/models/planning/tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -220,9 +220,9 @@ def process(db, building, setup_sources) -> dict[str, Sensor]:


@pytest.fixture(scope="module")
def add_stock_gain(db, add_battery_assets, setup_sources) -> dict[str, Sensor]:
def add_stock_delta(db, add_battery_assets, setup_sources) -> dict[str, Sensor]:
"""
Set up the same constant gain (-capacity_in_mw) in different resolutions.
Set up the same constant delta (capacity_in_mw) in different resolutions.
In 15 min event resolution, the maximum energy that the battery can produce/consume in period
is 0.25 * capacity_in_mw
Expand All @@ -232,41 +232,38 @@ def add_stock_gain(db, add_battery_assets, setup_sources) -> dict[str, Sensor]:
capacity = battery.get_attribute("capacity_in_mw")
sensors = {}
sensor_specs = [
("gain fails", timedelta(minutes=15), capacity, True),
("gain", timedelta(minutes=15), capacity * 0.25, True),
("gain hourly", timedelta(hours=1), capacity, True),
("gain None", timedelta(hours=1), -capacity, None),
("gain consumption is negative", timedelta(hours=1), -capacity, False),
("delta fails", timedelta(minutes=15), capacity),
("delta", timedelta(minutes=15), capacity * 0.25),
("delta hourly", timedelta(hours=1), capacity),
]

for name, resolution, value, consumption_is_positive in sensor_specs:
for name, resolution, value in sensor_specs:
# 1 days of test data
time_slots = initialize_index(
start=pd.Timestamp("2015-01-01").tz_localize("Europe/Amsterdam"),
end=pd.Timestamp("2015-01-02").tz_localize("Europe/Amsterdam"),
resolution=resolution,
)

stock_gain_sensor = Sensor(
stock_delta_sensor = Sensor(
name=name,
unit="MWh",
event_resolution=resolution,
generic_asset=battery,
attributes={"consumption_is_positive": consumption_is_positive},
)
db.session.add(stock_gain_sensor)
db.session.add(stock_delta_sensor)
db.session.flush()

stock_gain = [value] * len(time_slots)

add_as_beliefs(
db,
stock_gain_sensor,
stock_delta_sensor,
stock_gain,
time_slots,
setup_sources["Seita"],
)
sensors[name] = stock_gain_sensor
sensors[name] = stock_delta_sensor

return sensors

Expand Down
64 changes: 32 additions & 32 deletions flexmeasures/data/models/planning/tests/test_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1451,28 +1451,27 @@ def test_battery_power_capacity_as_sensor(


@pytest.mark.parametrize(
"stock_gain_sensor",
["gain fails", "gain", "gain hourly", "gain None", "gain consumption is negative"],
"stock_delta_sensor",
["delta fails", "delta", "delta hourly"],
)
def test_battery_stock_gain_sensor(
add_battery_assets, add_stock_gain, stock_gain_sensor
def test_battery_stock_delta_sensor(
add_battery_assets, add_stock_delta, stock_delta_sensor
):
"""
Test the stock gain feature using sensors.
Test the soc delta feature using sensors.
An empty battery is made to fulfill a usage signal under a flat tariff.
The battery is only allowed to charge (production-capacity = 0).
We expect the storage to charge in every period to compensate for the usage.
"""

_, battery = get_sensors_from_db(add_battery_assets)
tz = pytz.timezone("Europe/Amsterdam")
start = tz.localize(datetime(2015, 1, 1))
end = tz.localize(datetime(2015, 1, 2))
resolution = timedelta(minutes=15)
stock_gain_sensor_obj = add_stock_gain[stock_gain_sensor]
capacity = stock_gain_sensor_obj.get_attribute("capacity_in_mw")
stock_delta_sensor_obj = add_stock_delta[stock_delta_sensor]
capacity = stock_delta_sensor_obj.get_attribute("capacity_in_mw")

scheduler: Scheduler = StorageScheduler(
battery,
Expand All @@ -1482,15 +1481,15 @@ def test_battery_stock_gain_sensor(
flex_model={
"soc-max": 2,
"soc-min": 0,
"stock-gain": [{"sensor": stock_gain_sensor_obj.id}],
"soc-usage": [{"sensor": stock_delta_sensor_obj.id}],
"roundtrip-efficiency": 1,
"storage-efficiency": 1,
"production-capacity": "0kW",
"soc-at-start": 0,
},
)

if "fails" in stock_gain_sensor:
if "fails" in stock_delta_sensor:
with pytest.raises(InfeasibleProblemException):
schedule = scheduler.compute()
else:
Expand All @@ -1499,16 +1498,16 @@ def test_battery_stock_gain_sensor(


@pytest.mark.parametrize(
"gain,expected_gain",
"gain,usage,expected_delta",
[
(["1 MWh", "-1MWh"], 0), # net zero stock gain
(["0.5 MWh", "0.5MWh"], 1), # 1 MWh stock gain in every 15 min period
(["100 kWh"], 0.1), # 100 kWh stock gain in every 15 min period
(["-100 kWh"], -0.1), # 100 kWh stock loss in every 15 min period
([], None), # no gain defined -> no gain or loss happens
(["1 MWh"], ["1MWh"], 0), # delta stock is 0 (1 MWh - 1 MWh)
(["0.5 MWh", "0.5MWh"], [], 1), # 1 MWh stock gain in every 15 min period
(["100 kWh"], None, 0.1), # 100 kWh stock gain in every 15 min period
(None, ["100 kWh"], -0.1), # 100 kWh stock loss in every 15 min period
([], [], None), # no gain defined -> no gain or loss happens
],
)
def test_battery_gain_quantity(add_battery_assets, gain, expected_gain):
def test_battery_stock_delta_quantity(add_battery_assets, gain, usage, expected_delta):
"""
Test the stock gain field when a constant value is provided.
Expand All @@ -1520,26 +1519,27 @@ def test_battery_gain_quantity(add_battery_assets, gain, expected_gain):
start = tz.localize(datetime(2015, 1, 1))
end = tz.localize(datetime(2015, 1, 2))
resolution = timedelta(minutes=15)
flex_model = {
"soc-max": 2,
"soc-min": 0,
"roundtrip-efficiency": 1,
"storage-efficiency": 1,
}

if gain is not None:
flex_model["soc-gain"] = gain
if usage is not None:
flex_model["soc-usage"] = usage

scheduler: Scheduler = StorageScheduler(
battery,
start,
end,
resolution,
flex_model={
"soc-max": 2,
"soc-min": 0,
"stock-gain": gain,
"roundtrip-efficiency": 1,
"storage-efficiency": 1,
},
battery, start, end, resolution, flex_model=flex_model
)
scheduler_info = scheduler._prepare()

if expected_gain is not None:
if expected_delta is not None:
assert all(
scheduler_info[5][0]["stock gain"]
== expected_gain * (timedelta(hours=1) / resolution)
scheduler_info[5][0]["stock delta"]
== expected_delta * (timedelta(hours=1) / resolution)
)
else:
assert all(scheduler_info[5][0]["stock gain"].isna())
assert all(scheduler_info[5][0]["stock delta"].isna())
5 changes: 3 additions & 2 deletions flexmeasures/data/schemas/scheduling/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,9 @@ class StorageFlexModelSchema(Schema):
storage_efficiency = EfficiencyField(data_key="storage-efficiency")
prefer_charging_sooner = fields.Bool(data_key="prefer-charging-sooner")

stock_gain = fields.List(
QuantityOrSensor("MWh"), data_key="stock-gain", required=False
soc_gain = fields.List(QuantityOrSensor("MWh"), data_key="soc-gain", required=False)
soc_usage = fields.List(
QuantityOrSensor("MWh"), data_key="soc-usage", required=False
)

def __init__(self, start: datetime, sensor: Sensor, *args, **kwargs):
Expand Down

0 comments on commit bf11552

Please sign in to comment.