Skip to content

Commit

Permalink
Merge 4dc808c into a9aaf21
Browse files Browse the repository at this point in the history
  • Loading branch information
victorgarcia98 committed Nov 22, 2023
2 parents a9aaf21 + 4dc808c commit a86ace2
Show file tree
Hide file tree
Showing 5 changed files with 128 additions and 0 deletions.
12 changes: 12 additions & 0 deletions flexmeasures/data/models/planning/linear_optimization.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +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)
usage forecast: predefined stock delta to apply to the storage device. Positive quantity cause an increase and negative decrease (stock increase : flow in)
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 @@ -225,6 +226,15 @@ def device_derivative_up_efficiency(m, d, j):
return 1
return eff

def device_usage_forecast(m, d, j):
try:
usage_forecast = device_constraints[d]["usage forecast"].iloc[j]
except KeyError:
return 0
if np.isnan(usage_forecast):
return 0
return usage_forecast

model.up_price = Param(model.c, model.j, initialize=price_up_select)
model.down_price = Param(model.c, model.j, initialize=price_down_select)
model.commitment_quantity = Param(
Expand All @@ -247,6 +257,7 @@ def device_derivative_up_efficiency(m, d, j):
model.device_derivative_up_efficiency = Param(
model.d, model.j, initialize=device_derivative_up_efficiency
)
model.usage_forecast = Param(model.d, model.j, initialize=device_usage_forecast)

# Add variables
model.ems_power = Var(model.d, model.j, domain=Reals, initialize=0)
Expand All @@ -270,6 +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.usage_forecast[d, k]
)
for k in range(0, j + 1)
]
Expand Down
21 changes: 21 additions & 0 deletions flexmeasures/data/models/planning/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class MetaStorageScheduler(Scheduler):
"derivative min",
"derivative down efficiency",
"derivative up efficiency",
"usage forecast",
]

def compute_schedule(self) -> pd.Series | None:
Expand Down Expand Up @@ -235,6 +236,26 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901
method="upper",
)

usage_forecast = self.flex_model.get("usage_forecast", [])

all_usage_forecast = []

for component in usage_forecast:
all_usage_forecast.append(
get_continous_series_sensor_or_quantity(
quantity_or_sensor=component,
actuator=sensor,
target_unit="MWh",
query_window=(start, end),
resolution=resolution,
beliefs_before=belief_time,
)
)
if len(all_usage_forecast) > 0:
all_usage_forecast = pd.concat(all_usage_forecast, axis=1)

device_constraints[0]["usage forecast"] = all_usage_forecast.sum(1)

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


@pytest.fixture(scope="module")
def add_usage_forecast(db, add_battery_assets, setup_sources) -> Sensor:
"""
Set up a constant usage forecast
"""

battery = add_battery_assets["Test battery"]

# 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="15T",
)

usage_forecast_sensor = Sensor(
name="usage forecast",
unit="MWh",
event_resolution=timedelta(minutes=15),
generic_asset=battery,
)
db.session.add(usage_forecast_sensor)
db.session.flush()

usage_forecast = [-battery.get_attribute("capacity_in_mw")] * len(time_slots)

add_as_beliefs(
db, usage_forecast_sensor, usage_forecast, time_slots, setup_sources["Seita"]
)

return usage_forecast_sensor


def add_as_beliefs(db, sensor, values, time_slots, source):
beliefs = [
TimedBelief(
Expand Down
58 changes: 58 additions & 0 deletions flexmeasures/data/models/planning/tests/test_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -1375,3 +1375,61 @@ def test_battery_power_capacity_as_sensor(

assert all(device_constraints["derivative min"].values == expected_production)
assert all(device_constraints["derivative max"].values == expected_consumption)


def test_battery_usage_forecast_sensor(add_battery_assets, add_usage_forecast):
_, 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)

scheduler: Scheduler = StorageScheduler(
battery,
start,
end,
resolution,
flex_model={
"soc-max": 2,
"soc-min": 0,
"usage-forecast": [{"sensor": add_usage_forecast.id}],
"roundtrip-efficiency": 1,
"storage-efficiency": 1,
},
)
schedule = scheduler.compute()
assert all(schedule == 2)


@pytest.mark.parametrize(
"usage_forecast,expected_usage_forecast",
[(["1 MWh", "-1MWh"], 0), (["1 MWh", "1MWh"], 2), (["100 kWh"], 0.1), ([], None)],
)
def test_battery_usage_forecast_quantity(
add_battery_assets, usage_forecast, expected_usage_forecast
):
_, 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)

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

if expected_usage_forecast is not None:
assert all(scheduler_info[5][0]["usage forecast"] == expected_usage_forecast)
else:
assert all(scheduler_info[5][0]["usage forecast"].isna())
4 changes: 4 additions & 0 deletions flexmeasures/data/schemas/scheduling/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ class StorageFlexModelSchema(Schema):
storage_efficiency = EfficiencyField(data_key="storage-efficiency")
prefer_charging_sooner = fields.Bool(data_key="prefer-charging-sooner")

usage_forecast = fields.List(
QuantityOrSensor("MWh"), data_key="usage-forecast", required=False
)

def __init__(self, start: datetime, sensor: Sensor, *args, **kwargs):
"""Pass the schedule's start, so we can use it to validate soc-target datetimes."""
self.start = start
Expand Down

0 comments on commit a86ace2

Please sign in to comment.