From 1c85e3825bd9ab53807723605a33f2c0f76b184c Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 31 Oct 2022 15:52:49 +0100 Subject: [PATCH 1/5] Apply efficiencies to conversion from flow to stock change and vice versa Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/solver.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/flexmeasures/data/models/planning/solver.py b/flexmeasures/data/models/planning/solver.py index e16af9488..86325aff1 100644 --- a/flexmeasures/data/models/planning/solver.py +++ b/flexmeasures/data/models/planning/solver.py @@ -46,8 +46,8 @@ def device_scheduler( # noqa C901 derivative max: maximum flow (e.g. in MW or boxes/h) derivative min: minimum flow derivative equals: exact amount of flow (we do this by clamping derivative min and derivative max) - derivative down efficiency: ratio of downwards flows (flow into EMS : flow out of device) - derivative up efficiency: ratio of upwards flows (flow into device : flow out of EMS) + 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) EMS constraints are on an EMS level. Handled constraints (listed by column name): derivative max: maximum flow derivative min: minimum flow @@ -228,10 +228,12 @@ def device_derivative_up_efficiency(m, d, j): # Add constraints as a tuple of (lower bound, value, upper bound) def device_bounds(m, d, j): + """Apply efficiencies to conversion from flow to stock change and vice versa.""" return ( m.device_min[d, j], sum( - m.device_power_down[d, k] + m.device_power_up[d, k] + 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] for k in range(0, j + 1) ), m.device_max[d, j], @@ -275,12 +277,10 @@ def ems_flow_commitment_equalities(m, j): ) def device_derivative_equalities(m, d, j): - """Couple device flows to EMS flows per device, applying efficiencies.""" + """Couple device flows to EMS flows per device.""" return ( 0, - m.device_power_up[d, j] / m.device_derivative_up_efficiency[d, j] - + m.device_power_down[d, j] * m.device_derivative_down_efficiency[d, j] - - m.ems_power[d, j], + m.device_power_up[d, j] + m.device_power_down[d, j] - m.ems_power[d, j], 0, ) @@ -321,10 +321,7 @@ def cost_function(m): planned_costs = value(model.costs) planned_power_per_device = [] for d in model.d: - planned_device_power = [ - model.device_power_down[d, j].value + model.device_power_up[d, j].value - for j in model.j - ] + planned_device_power = [model.ems_power[d, j].value for j in model.j] planned_power_per_device.append( pd.Series( index=pd.date_range( From 5ec54216f65e205d59d0ff8f9d78fbe494a508ce Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 31 Oct 2022 16:17:04 +0100 Subject: [PATCH 2/5] Reorder print statements for more convenient uncommenting Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/solver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/data/models/planning/solver.py b/flexmeasures/data/models/planning/solver.py index 86325aff1..42c8acf29 100644 --- a/flexmeasures/data/models/planning/solver.py +++ b/flexmeasures/data/models/planning/solver.py @@ -332,7 +332,7 @@ def cost_function(m): ) # model.pprint() + # model.display() # print(results.solver.termination_condition) # print(planned_costs) - # model.display() return planned_power_per_device, planned_costs, results From 4361001e7dde7ed17d698e17a7fd88f68508e769 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 31 Oct 2022 16:35:59 +0100 Subject: [PATCH 3/5] Fix test Signed-off-by: F.N. Claessen --- flexmeasures/api/v1_3/tests/test_api_v1_3.py | 4 ++- .../api/v3_0/tests/test_sensor_schedules.py | 4 ++- .../data/models/planning/tests/test_solver.py | 8 ++++- flexmeasures/utils/calculations.py | 31 ++++++++++++++----- 4 files changed, 37 insertions(+), 10 deletions(-) diff --git a/flexmeasures/api/v1_3/tests/test_api_v1_3.py b/flexmeasures/api/v1_3/tests/test_api_v1_3.py index 54429774a..0ab3221e9 100644 --- a/flexmeasures/api/v1_3/tests/test_api_v1_3.py +++ b/flexmeasures/api/v1_3/tests/test_api_v1_3.py @@ -112,7 +112,9 @@ def test_post_udi_event_and_get_device_message( # check targets, if applicable if "targets" in message: start_soc = message["value"] / 1000 # in MWh - soc_schedule = integrate_time_series(consumption_schedule, start_soc, 6) + soc_schedule = integrate_time_series( + consumption_schedule, start_soc, decimal_precision=6 + ) print(consumption_schedule) print(soc_schedule) for target in message["targets"]: diff --git a/flexmeasures/api/v3_0/tests/test_sensor_schedules.py b/flexmeasures/api/v3_0/tests/test_sensor_schedules.py index 9c9ac912a..6d9a4516d 100644 --- a/flexmeasures/api/v3_0/tests/test_sensor_schedules.py +++ b/flexmeasures/api/v3_0/tests/test_sensor_schedules.py @@ -90,7 +90,9 @@ def test_trigger_and_get_schedule( # check targets, if applicable if "targets" in message: start_soc = message["soc-at-start"] / 1000 # in MWh - soc_schedule = integrate_time_series(consumption_schedule, start_soc, 6) + soc_schedule = integrate_time_series( + consumption_schedule, start_soc, decimal_precision=6 + ) print(consumption_schedule) print(soc_schedule) for target in message["targets"]: diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index 15396e66b..0295079eb 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -90,7 +90,13 @@ def test_battery_solver_day_2(add_battery_assets, roundtrip_efficiency: float): soc_max=soc_max, roundtrip_efficiency=roundtrip_efficiency, ) - soc_schedule = integrate_time_series(schedule, soc_at_start, decimal_precision=6) + soc_schedule = integrate_time_series( + schedule, + soc_at_start, + up_efficiency=roundtrip_efficiency**0.5, + down_efficiency=roundtrip_efficiency**0.5, + decimal_precision=6, + ) with pd.option_context("display.max_rows", None, "display.max_columns", 3): print(soc_schedule) diff --git a/flexmeasures/utils/calculations.py b/flexmeasures/utils/calculations.py index fef972f82..f7eb0e66a 100644 --- a/flexmeasures/utils/calculations.py +++ b/flexmeasures/utils/calculations.py @@ -1,6 +1,7 @@ """ Calculations """ +from __future__ import annotations + from datetime import timedelta -from typing import Optional import numpy as np import pandas as pd @@ -37,7 +38,11 @@ def drop_nan_rows(a, b): def integrate_time_series( - s: pd.Series, s0: float, decimal_precision: Optional[int] = None + series: pd.Series, + s0: float, + up_efficiency: float | pd.Series = 1, + down_efficiency: float | pd.Series = 1, + decimal_precision: int | None = None, ) -> pd.Series: """Integrate time series of length n and closed="left" (representing a flow) to a time series of length n+1 and closed="both" (representing a stock), @@ -46,7 +51,7 @@ def integrate_time_series( Optionally, set a decimal precision to round off the results (useful for tests failing over machine precision). >>> s = pd.Series([1, 2, 3, 4], index=pd.date_range(datetime(2001, 1, 1, 5), datetime(2001, 1, 1, 6), freq=timedelta(minutes=15), closed="left")) - >>> integrate_time_series(s, 10) + >>> integrate_time_series(series, 10) 2001-01-01 05:00:00 10.00 2001-01-01 05:15:00 10.25 2001-01-01 05:30:00 10.75 @@ -55,7 +60,7 @@ def integrate_time_series( Freq: D, dtype: float64 >>> s = pd.Series([1, 2, 3, 4], index=pd.date_range(datetime(2001, 1, 1, 5), datetime(2001, 1, 1, 7), freq=timedelta(minutes=30), closed="left")) - >>> integrate_time_series(s, 10) + >>> integrate_time_series(series, 10) 2001-01-01 05:00:00 10.0 2001-01-01 05:30:00 10.5 2001-01-01 06:00:00 11.5 @@ -63,11 +68,23 @@ def integrate_time_series( 2001-01-01 07:00:00 15.0 dtype: float64 """ - resolution = pd.to_timedelta(s.index.freq) + resolution = pd.to_timedelta(series.index.freq) + stock_change = pd.Series(data=np.NaN, index=series.index) + stock_change.loc[series > 0] = series[series > 0] * ( + up_efficiency[series > 0] + if isinstance(up_efficiency, pd.Series) + else up_efficiency + ) + stock_change.loc[series <= 0] = series[series <= 0] / ( + down_efficiency[series <= 0] + if isinstance(down_efficiency, pd.Series) + else down_efficiency + ) int_s = pd.concat( [ - pd.Series(s0, index=pd.date_range(s.index[0], periods=1)), - s.shift(1, freq=resolution).cumsum() * (resolution / timedelta(hours=1)) + pd.Series(s0, index=pd.date_range(series.index[0], periods=1)), + stock_change.shift(1, freq=resolution).cumsum() + * (resolution / timedelta(hours=1)) + s0, ] ) From 64d7e0ac72770db9589183a5d56706818c008d71 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 2 Nov 2022 20:48:31 +0100 Subject: [PATCH 4/5] Refactor: rename variables Signed-off-by: F.N. Claessen --- flexmeasures/utils/calculations.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flexmeasures/utils/calculations.py b/flexmeasures/utils/calculations.py index f7eb0e66a..84cb8dd79 100644 --- a/flexmeasures/utils/calculations.py +++ b/flexmeasures/utils/calculations.py @@ -39,19 +39,19 @@ def drop_nan_rows(a, b): def integrate_time_series( series: pd.Series, - s0: float, + initial_stock: float, up_efficiency: float | pd.Series = 1, down_efficiency: float | pd.Series = 1, decimal_precision: int | None = None, ) -> pd.Series: """Integrate time series of length n and closed="left" (representing a flow) to a time series of length n+1 and closed="both" (representing a stock), - given a starting stock s0. + given an initial stock (i.e. the constant of integration). The unit of time is hours: i.e. the stock unit is flow unit times hours (e.g. a flow in kW becomes a stock in kWh). Optionally, set a decimal precision to round off the results (useful for tests failing over machine precision). >>> s = pd.Series([1, 2, 3, 4], index=pd.date_range(datetime(2001, 1, 1, 5), datetime(2001, 1, 1, 6), freq=timedelta(minutes=15), closed="left")) - >>> integrate_time_series(series, 10) + >>> integrate_time_series(s, 10) 2001-01-01 05:00:00 10.00 2001-01-01 05:15:00 10.25 2001-01-01 05:30:00 10.75 @@ -60,7 +60,7 @@ def integrate_time_series( Freq: D, dtype: float64 >>> s = pd.Series([1, 2, 3, 4], index=pd.date_range(datetime(2001, 1, 1, 5), datetime(2001, 1, 1, 7), freq=timedelta(minutes=30), closed="left")) - >>> integrate_time_series(series, 10) + >>> integrate_time_series(s, 10) 2001-01-01 05:00:00 10.0 2001-01-01 05:30:00 10.5 2001-01-01 06:00:00 11.5 @@ -82,10 +82,10 @@ def integrate_time_series( ) int_s = pd.concat( [ - pd.Series(s0, index=pd.date_range(series.index[0], periods=1)), + pd.Series(initial_stock, index=pd.date_range(series.index[0], periods=1)), stock_change.shift(1, freq=resolution).cumsum() * (resolution / timedelta(hours=1)) - + s0, + + initial_stock, ] ) if decimal_precision is not None: From 890ee11a1caefa2583e88a65b7f39ef3e9a6ad73 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 2 Nov 2022 20:52:16 +0100 Subject: [PATCH 5/5] changelog entry Signed-off-by: F.N. Claessen --- documentation/changelog.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 2fd2defba..ca5e43509 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -30,6 +30,7 @@ v0.11.3 | November XX, 2022 Bugfixes ----------- +* Fix scheduling with imperfect efficiencies, which resulted in exceeding the device's lower SoC limit. [see `PR #520 `_] * Fix scheduler for Charge Points when taking into account inflexible devices [see `PR #517 `_]