From 5d829a90a73f92a86ebf716b03dfe24e55e91a40 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 9 May 2023 16:59:54 +0200 Subject: [PATCH 01/23] StorageScheduler supports losses over time Signed-off-by: F.N. Claessen --- .../models/planning/linear_optimization.py | 36 ++++++-- flexmeasures/data/models/planning/storage.py | 18 ++++ .../data/models/planning/tests/test_solver.py | 68 ++++++++++++--- .../data/schemas/scheduling/storage.py | 15 ++-- flexmeasures/utils/calculations.py | 86 ++++++++++++++++--- 5 files changed, 188 insertions(+), 35 deletions(-) diff --git a/flexmeasures/data/models/planning/linear_optimization.py b/flexmeasures/data/models/planning/linear_optimization.py index 32d8b7456..48247ac79 100644 --- a/flexmeasures/data/models/planning/linear_optimization.py +++ b/flexmeasures/data/models/planning/linear_optimization.py @@ -21,6 +21,7 @@ from pyomo.opt import SolverFactory, SolverResults from flexmeasures.data.models.planning.utils import initialize_series +from flexmeasures.utils.calculations import apply_stock_changes_and_losses infinity = float("inf") @@ -31,6 +32,7 @@ def device_scheduler( # noqa C901 commitment_quantities: List[pd.Series], commitment_downwards_deviation_price: Union[List[pd.Series], List[float]], commitment_upwards_deviation_price: Union[List[pd.Series], List[float]], + initial_stock: float = 0, ) -> Tuple[List[pd.Series], float, SolverResults]: """This generic device scheduler is able to handle an EMS with multiple devices, with various types of constraints on the EMS level and on the device level, @@ -43,6 +45,7 @@ def device_scheduler( # noqa C901 max: maximum stock assuming an initial stock of zero (e.g. in MWh or boxes) min: minimum stock assuming an initial stock of zero equal: exact amount of stock (we do this by clamping min and max) + efficiency: amount of stock left at the next datetime (the rest is lost) 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) @@ -171,6 +174,16 @@ def ems_derivative_min_select(m, j): else: return v + def device_efficiency(m, d, j): + """Assume perfect efficiency if no efficiency information is available.""" + try: + eff = device_constraints[d]["efficiency"].iloc[j] + except KeyError: + return 1 + if np.isnan(eff): + return 1 + return eff + def device_derivative_down_efficiency(m, d, j): """Assume perfect efficiency if no efficiency information is available.""" try: @@ -206,6 +219,7 @@ def device_derivative_up_efficiency(m, d, j): ) model.ems_derivative_max = Param(model.j, initialize=ems_derivative_max_select) model.ems_derivative_min = Param(model.j, initialize=ems_derivative_min_select) + model.device_efficiency = Param(model.d, model.j, initialize=device_efficiency) model.device_derivative_down_efficiency = Param( model.d, model.j, initialize=device_derivative_down_efficiency ) @@ -228,14 +242,24 @@ 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( + """Apply conversion efficiencies to conversion from flow to stock change and vice versa, + and apply storage efficiencies to stock levels from one datetime to the next.""" + stock_changes = [ + ( 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) - ), + ) + for k in range(0, j + 1) + ] + efficiencies = [m.device_efficiency[d, k] for k in range(0, j + 1)] + return ( + m.device_min[d, j], + [ + stock - initial_stock + for stock in apply_stock_changes_and_losses( + initial_stock, stock_changes, efficiencies + ) + ][-1], m.device_max[d, j], ) diff --git a/flexmeasures/data/models/planning/storage.py b/flexmeasures/data/models/planning/storage.py index 13af6faa8..00cd8b560 100644 --- a/flexmeasures/data/models/planning/storage.py +++ b/flexmeasures/data/models/planning/storage.py @@ -56,6 +56,7 @@ def compute( soc_min = self.flex_model.get("soc_min") soc_max = self.flex_model.get("soc_max") roundtrip_efficiency = self.flex_model.get("roundtrip_efficiency") + storage_efficiency = self.flex_model.get("storage_efficiency") prefer_charging_sooner = self.flex_model.get("prefer_charging_sooner", True) consumption_price_sensor = self.flex_context.get("consumption_price_sensor") @@ -114,6 +115,7 @@ def compute( "equals", "max", "min", + "efficiency", "derivative equals", "derivative max", "derivative min", @@ -167,6 +169,9 @@ def compute( ) device_constraints[0]["derivative up efficiency"] = roundtrip_efficiency**0.5 + # Apply storage efficiency (accounts for losses over time) + device_constraints[0]["efficiency"] = storage_efficiency + # Set up EMS constraints columns = ["derivative max", "derivative min"] ems_constraints = initialize_df(columns, start, end, resolution) @@ -181,6 +186,7 @@ def compute( commitment_quantities, commitment_downwards_deviation_price, commitment_upwards_deviation_price, + initial_stock=soc_at_start * (timedelta(hours=1) / resolution), ) if scheduler_results.solver.termination_condition == "infeasible": # Fallback policy if the problem was unsolvable @@ -250,7 +256,19 @@ def deserialize_flex_config(self): elif self.sensor.unit in ("MW", "kW"): self.flex_model["soc-unit"] = self.sensor.unit + "h" + # Check for storage efficiency + # todo: simplify to: `if self.flex_model.get("storage-efficiency") is None:` + if ( + "storage-efficiency" not in self.flex_model + or self.flex_model["storage-efficiency"] is None + ): + # Get default from sensor, or use 100% otherwise + self.flex_model["storage-efficiency"] = self.sensor.get_attribute( + "storage_efficiency", 1 + ) + # Check for round-trip efficiency + # todo: simplify to: `if self.flex_model.get("roundtrip-efficiency") is None:` if ( "roundtrip-efficiency" not in self.flex_model or self.flex_model["roundtrip-efficiency"] is None diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index c353bec80..020a9a889 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -11,12 +11,46 @@ from flexmeasures.data.models.planning.utils import ( initialize_series, ) -from flexmeasures.utils.calculations import integrate_time_series +from flexmeasures.utils.calculations import ( + apply_stock_changes_and_losses, + integrate_time_series, +) TOLERANCE = 0.00001 +@pytest.mark.parametrize( + "initial_stock, stock_deltas, expected_stocks, storage_efficiency", + [ + ( + 1000, + [100, -100, -100, 100], + [1000, 1089, 979.11, 870.3189, 960.615711], + 0.99, + ), + ( + 2.5, + [-0.5, -0.5, -0.5, -0.5], + [2.5, 1.8, 1.17, 0.603, 0.0927], + 0.9, + ), + ], +) +def test_storage_loss_function( + initial_stock, stock_deltas, expected_stocks, storage_efficiency +): + stocks = apply_stock_changes_and_losses( + initial_stock, + stock_deltas, + storage_efficiency=storage_efficiency, + how="left", + decimal_precision=6, + ) + print(stocks) + assert all(a == b for a, b in zip(stocks, expected_stocks)) + + @pytest.mark.parametrize("use_inflexible_device", [False, True]) def test_battery_solver_day_1( add_battery_assets, add_inflexible_device_forecasts, use_inflexible_device @@ -60,14 +94,18 @@ def test_battery_solver_day_1( @pytest.mark.parametrize( - "roundtrip_efficiency", + "roundtrip_efficiency, storage_efficiency", [ - 1, - 0.99, - 0.01, + (1, 1), + (1, 0.999), + (1, 0.5), + (0.99, 1), + (0.01, 1), ], ) -def test_battery_solver_day_2(add_battery_assets, roundtrip_efficiency: float): +def test_battery_solver_day_2( + add_battery_assets, roundtrip_efficiency: float, storage_efficiency: float +): """Check battery scheduling results for day 2, which is set up with 8 expensive, then 8 cheap, then again 8 expensive hours. If efficiency losses aren't too bad, we expect the scheduler to: @@ -98,6 +136,7 @@ def test_battery_solver_day_2(add_battery_assets, roundtrip_efficiency: float): "soc-min": soc_min, "soc-max": soc_max, "roundtrip-efficiency": roundtrip_efficiency, + "storage-efficiency": storage_efficiency, }, ) schedule = scheduler.compute() @@ -106,6 +145,7 @@ def test_battery_solver_day_2(add_battery_assets, roundtrip_efficiency: float): soc_at_start, up_efficiency=roundtrip_efficiency**0.5, down_efficiency=roundtrip_efficiency**0.5, + storage_efficiency=storage_efficiency, decimal_precision=6, ) @@ -124,22 +164,30 @@ def test_battery_solver_day_2(add_battery_assets, roundtrip_efficiency: float): soc_min, battery.get_attribute("min_soc_in_mwh") ) # Battery sold out at the end of its planning horizon - # As long as the roundtrip efficiency isn't too bad (I haven't computed the actual switch point) - if roundtrip_efficiency > 0.9: + # As long as the efficiencies aren't too bad (I haven't computed the actual switch points) + if roundtrip_efficiency > 0.9 and storage_efficiency > 0.9: assert soc_schedule.loc[start + timedelta(hours=8)] == max( soc_min, battery.get_attribute("min_soc_in_mwh") ) # Sell what you begin with assert soc_schedule.loc[start + timedelta(hours=16)] == min( soc_max, battery.get_attribute("max_soc_in_mwh") ) # Buy what you can to sell later - else: - # If the roundtrip efficiency is poor, best to stand idle + elif storage_efficiency > 0.9: + # If only the roundtrip efficiency is poor, best to stand idle (keep a high SoC as long as possible) assert soc_schedule.loc[start + timedelta(hours=8)] == battery.get_attribute( "soc_in_mwh" ) assert soc_schedule.loc[start + timedelta(hours=16)] == battery.get_attribute( "soc_in_mwh" ) + else: + # If the storage efficiency is poor, regardless of whether the roundtrip efficiency is poor, best to sell asap + assert soc_schedule.loc[start + timedelta(hours=8)] == max( + soc_min, battery.get_attribute("min_soc_in_mwh") + ) + assert soc_schedule.loc[start + timedelta(hours=16)] == max( + soc_min, battery.get_attribute("min_soc_in_mwh") + ) @pytest.mark.parametrize( diff --git a/flexmeasures/data/schemas/scheduling/storage.py b/flexmeasures/data/schemas/scheduling/storage.py index f9ef3d83d..a92d01ebe 100644 --- a/flexmeasures/data/schemas/scheduling/storage.py +++ b/flexmeasures/data/schemas/scheduling/storage.py @@ -46,6 +46,11 @@ class StorageFlexModelSchema(Schema): validate=validate.Range(min=0, max=1, min_inclusive=False, max_inclusive=True), data_key="roundtrip-efficiency", ) + storage_efficiency = QuantityField( + "%", + validate=validate.Range(min=0, max=1, min_inclusive=False, max_inclusive=True), + data_key="storage-efficiency", + ) prefer_charging_sooner = fields.Bool(data_key="prefer-charging-sooner") def __init__(self, start: datetime, sensor: Sensor, *args, **kwargs): @@ -85,10 +90,10 @@ def post_load_sequence(self, data: dict, **kwargs) -> dict: target["value"] /= 1000.0 data["soc_unit"] = "MWh" - # Convert round-trip efficiency to dimensionless (to the (0,1] range) - if data.get("roundtrip_efficiency") is not None: - data["roundtrip_efficiency"] = ( - data["roundtrip_efficiency"].to(ur.Quantity("dimensionless")).magnitude - ) + # Convert efficiencies to dimensionless (to the (0,1] range) + efficiency_fields = ("storage_efficiency", "roundtrip_efficiency") + for field in efficiency_fields: + if data.get(field) is not None: + data[field] = data[field].to(ur.Quantity("dimensionless")).magnitude return data diff --git a/flexmeasures/utils/calculations.py b/flexmeasures/utils/calculations.py index f85813fa7..af67a9b2c 100644 --- a/flexmeasures/utils/calculations.py +++ b/flexmeasures/utils/calculations.py @@ -2,6 +2,7 @@ from __future__ import annotations from datetime import timedelta +import math import numpy as np import pandas as pd @@ -37,11 +38,51 @@ def drop_nan_rows(a, b): return d[:, 0], d[:, 1] +def apply_stock_changes_and_losses( + initial: float, + changes: list[float], + storage_efficiency: float | list[float], + how: str = "linear", + decimal_precision: int | None = None, +) -> list[float]: + """Assign stock changes and determine losses from storage efficiency. + + :param initial: initial stock + :param changes: stock change for each step + :param storage_efficiency: ratio of stock left after a step (constant ratio or one per step) + :param how: left, right or linear; how stock changes should be applied, which affects how losses are applied + :param decimal_precision: Optional decimal precision to round off results (useful for tests failing over machine precision) + """ + stocks = [initial] + if not isinstance(storage_efficiency, list): + storage_efficiency = [storage_efficiency] * len(changes) + for d, e in zip(changes, storage_efficiency): + s = stocks[-1] + if e == 1: + next_stock = s + d + elif how == "left": + # First apply the stock change, then apply the losses (i.e. the stock changes on the left side of the time interval in which the losses apply) + next_stock = (s + d) * e + elif how == "right": + # First apply the losses, then apply the stock change (i.e. the stock changes on the right side of the time interval in which the losses apply) + next_stock = s * e + d + elif how == "linear": + # Assume the change happens at a constant rate, leading to a linear stock change, and exponential decay + next_stock = s * e + d * (e - 1) / math.log(e) + else: + raise NotImplementedError(f"Missing implementation for how='{how}'.") + stocks.append(next_stock) + if decimal_precision is not None: + stocks = [round(s, decimal_precision) for s in stocks] + return stocks + + def integrate_time_series( series: pd.Series, initial_stock: float, up_efficiency: float | pd.Series = 1, down_efficiency: float | pd.Series = 1, + storage_efficiency: float | pd.Series = 1, decimal_precision: int | None = None, ) -> pd.Series: """Integrate time series of length n and inclusive="left" (representing a flow) @@ -69,25 +110,42 @@ def integrate_time_series( dtype: float64 """ resolution = pd.to_timedelta(series.index.freq) + storage_efficiency = ( + storage_efficiency + if isinstance(storage_efficiency, pd.Series) + else pd.Series(storage_efficiency, index=series.index) + ) + + # Convert from flow to stock change, applying conversion efficiencies 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] + * ( + up_efficiency[series > 0] + if isinstance(up_efficiency, pd.Series) + else up_efficiency + ) + * (resolution / timedelta(hours=1)) ) - stock_change.loc[series <= 0] = series[series <= 0] / ( - down_efficiency[series <= 0] - if isinstance(down_efficiency, pd.Series) - else down_efficiency + stock_change.loc[series <= 0] = ( + series[series <= 0] + / ( + down_efficiency[series <= 0] + if isinstance(down_efficiency, pd.Series) + else down_efficiency + ) + * (resolution / timedelta(hours=1)) + ) + + stocks = apply_stock_changes_and_losses( + initial_stock, stock_change.tolist(), storage_efficiency.tolist() ) - int_s = pd.concat( + stocks = pd.concat( [ pd.Series(initial_stock, index=pd.date_range(series.index[0], periods=1)), - stock_change.shift(1, freq=resolution).cumsum() - * (resolution / timedelta(hours=1)) - + initial_stock, + pd.Series(stocks[1:], index=series.index).shift(1, freq=resolution), ] ) if decimal_precision is not None: - int_s = int_s.round(decimal_precision) - return int_s + stocks = stocks.round(decimal_precision) + return stocks From dfc9563af465e8a39b1940f5dccb3eba54d55d3c Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 9 May 2023 17:18:41 +0200 Subject: [PATCH 02/23] Fix tests Signed-off-by: F.N. Claessen --- flexmeasures/data/models/planning/tests/test_solver.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/flexmeasures/data/models/planning/tests/test_solver.py b/flexmeasures/data/models/planning/tests/test_solver.py index 020a9a889..ea90ebda8 100644 --- a/flexmeasures/data/models/planning/tests/test_solver.py +++ b/flexmeasures/data/models/planning/tests/test_solver.py @@ -234,6 +234,9 @@ def test_charging_station_solver_day_2(target_soc, charging_station_name): "roundtrip_efficiency": charging_station.get_attribute( "roundtrip_efficiency", 1 ), + "storage_efficiency": charging_station.get_attribute( + "storage_efficiency", 1 + ), "soc_targets": soc_targets, }, ) @@ -307,6 +310,9 @@ def test_fallback_to_unsolvable_problem(target_soc, charging_station_name): "roundtrip_efficiency": charging_station.get_attribute( "roundtrip_efficiency", 1 ), + "storage_efficiency": charging_station.get_attribute( + "storage_efficiency", 1 + ), "soc_targets": soc_targets, }, ) @@ -397,6 +403,7 @@ def test_building_solver_day_2( "soc_min": soc_min, "soc_max": soc_max, "roundtrip_efficiency": battery.get_attribute("roundtrip_efficiency", 1), + "storage_efficiency": battery.get_attribute("storage_efficiency", 1), }, flex_context={ "inflexible_device_sensors": inflexible_devices.values(), From 764ce48acc9d870cd49d5df30f131f8a4724b260 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 11 May 2023 13:31:24 +0200 Subject: [PATCH 03/23] Expose storage-efficiency through the CLI Signed-off-by: F.N. Claessen --- flexmeasures/cli/data_add.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/flexmeasures/cli/data_add.py b/flexmeasures/cli/data_add.py index 4412c85f9..2fa4fe9ce 100755 --- a/flexmeasures/cli/data_add.py +++ b/flexmeasures/cli/data_add.py @@ -1010,6 +1010,17 @@ def create_schedule(ctx): default=1, help="Round-trip efficiency (e.g. 85% or 0.85) to use for the schedule. Defaults to 100% (no losses).", ) +@click.option( + "--storage-efficiency", + "storage_efficiency", + type=QuantityField("%", validate=validate.Range(min=0, max=1)), + required=False, + default=1, + help="Storage efficiency (e.g. 95% or 0.95) to use for the schedule," + " applied over each time step equal to the sensor resolution." + " For example, a storage efficiency of 99 percent per (absolute) day, for scheduling a 1-hour resolution sensor, should be passed as a storage efficiency of 0.99**(1/24)." + " Defaults to 100% (no losses).", +) @click.option( "--as-job", is_flag=True, @@ -1029,6 +1040,7 @@ def add_schedule_for_storage( soc_min: ur.Quantity | None = None, soc_max: ur.Quantity | None = None, roundtrip_efficiency: ur.Quantity | None = None, + storage_efficiency: ur.Quantity | None = None, as_job: bool = False, ): """Create a new schedule for a storage asset. @@ -1084,6 +1096,8 @@ def add_schedule_for_storage( soc_max = convert_units(soc_max.magnitude, str(soc_max.units), "MWh", capacity=capacity_str) # type: ignore if roundtrip_efficiency is not None: roundtrip_efficiency = roundtrip_efficiency.magnitude / 100.0 + if storage_efficiency is not None: + storage_efficiency = storage_efficiency.magnitude / 100.0 scheduling_kwargs = dict( start=start, @@ -1097,6 +1111,7 @@ def add_schedule_for_storage( "soc-max": soc_max, "soc-unit": "MWh", "roundtrip-efficiency": roundtrip_efficiency, + "storage-efficiency": storage_efficiency, }, flex_context={ "consumption-price-sensor": consumption_price_sensor.id, From 91ccd48af19ed117d8389bd30c99debbb37fd0e7 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 11 May 2023 13:53:38 +0200 Subject: [PATCH 04/23] Refactor so that both CLI and API use the same efficiency field definitions Signed-off-by: F.N. Claessen --- flexmeasures/cli/data_add.py | 5 ++-- .../data/schemas/scheduling/storage.py | 24 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/flexmeasures/cli/data_add.py b/flexmeasures/cli/data_add.py index 2fa4fe9ce..1f4f03aed 100755 --- a/flexmeasures/cli/data_add.py +++ b/flexmeasures/cli/data_add.py @@ -45,6 +45,7 @@ LongitudeField, SensorIdField, ) +from flexmeasures.data.schemas.scheduling.storage import EfficiencyField from flexmeasures.data.schemas.sensors import SensorSchema from flexmeasures.data.schemas.units import QuantityField from flexmeasures.data.schemas.generic_assets import ( @@ -1005,7 +1006,7 @@ def create_schedule(ctx): @click.option( "--roundtrip-efficiency", "roundtrip_efficiency", - type=QuantityField("%", validate=validate.Range(min=0, max=1)), + type=EfficiencyField(), required=False, default=1, help="Round-trip efficiency (e.g. 85% or 0.85) to use for the schedule. Defaults to 100% (no losses).", @@ -1013,7 +1014,7 @@ def create_schedule(ctx): @click.option( "--storage-efficiency", "storage_efficiency", - type=QuantityField("%", validate=validate.Range(min=0, max=1)), + type=EfficiencyField(), required=False, default=1, help="Storage efficiency (e.g. 95% or 0.95) to use for the schedule," diff --git a/flexmeasures/data/schemas/scheduling/storage.py b/flexmeasures/data/schemas/scheduling/storage.py index a92d01ebe..a4ea56223 100644 --- a/flexmeasures/data/schemas/scheduling/storage.py +++ b/flexmeasures/data/schemas/scheduling/storage.py @@ -12,6 +12,18 @@ from flexmeasures.utils.unit_utils import ur +class EfficiencyField(QuantityField): + def __init__(self, *args, **kwargs): + super().__init__( + "%", + validate=validate.Range( + min=0, max=1, min_inclusive=False, max_inclusive=True + ), + *args, + **kwargs, + ) + + class SOCTargetSchema(Schema): """ A point in time with a target value. @@ -41,16 +53,8 @@ class StorageFlexModelSchema(Schema): data_key="soc-unit", ) # todo: allow unit to be set per field, using QuantityField("%", validate=validate.Range(min=0, max=1)) soc_targets = fields.List(fields.Nested(SOCTargetSchema()), data_key="soc-targets") - roundtrip_efficiency = QuantityField( - "%", - validate=validate.Range(min=0, max=1, min_inclusive=False, max_inclusive=True), - data_key="roundtrip-efficiency", - ) - storage_efficiency = QuantityField( - "%", - validate=validate.Range(min=0, max=1, min_inclusive=False, max_inclusive=True), - data_key="storage-efficiency", - ) + roundtrip_efficiency = EfficiencyField(data_key="roundtrip-efficiency") + storage_efficiency = EfficiencyField(data_key="storage-efficiency") prefer_charging_sooner = fields.Bool(data_key="prefer-charging-sooner") def __init__(self, start: datetime, sensor: Sensor, *args, **kwargs): From 41442bb47e958600e390c9320df9422256e3d7e6 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 11 May 2023 14:03:39 +0200 Subject: [PATCH 05/23] Test deserialization of efficiency field Signed-off-by: F.N. Claessen --- flexmeasures/data/tests/test_scheduling_jobs_fresh_db.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/flexmeasures/data/tests/test_scheduling_jobs_fresh_db.py b/flexmeasures/data/tests/test_scheduling_jobs_fresh_db.py index ca125614f..02b966a62 100644 --- a/flexmeasures/data/tests/test_scheduling_jobs_fresh_db.py +++ b/flexmeasures/data/tests/test_scheduling_jobs_fresh_db.py @@ -42,7 +42,12 @@ def test_scheduling_a_charging_station( end=end, belief_time=start, resolution=resolution, - flex_model={"soc-at-start": soc_at_start, "soc-targets": soc_targets}, + flex_model={ + "soc-at-start": soc_at_start, + "soc-targets": soc_targets, + "roundtrip-efficiency": "100%", + "storage-efficiency": 1, + }, ) print("Job: %s" % job.id) From 2f3e5ced88b4e0639b9cf181234e1cf9e6efa50e Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 11 May 2023 14:19:36 +0200 Subject: [PATCH 06/23] Check for energy losses due to inefficiencies Signed-off-by: F.N. Claessen --- flexmeasures/data/tests/test_scheduling_jobs.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/flexmeasures/data/tests/test_scheduling_jobs.py b/flexmeasures/data/tests/test_scheduling_jobs.py index a488fb98c..cdd2f1a4a 100644 --- a/flexmeasures/data/tests/test_scheduling_jobs.py +++ b/flexmeasures/data/tests/test_scheduling_jobs.py @@ -37,6 +37,10 @@ def test_scheduling_a_battery(db, app, add_battery_assets, setup_test_data): end=end, belief_time=start, resolution=resolution, + flex_model={ + "roundtrip-efficiency": "98%", + "storage-efficiency": 0.999, + }, ) print("Job: %s" % job.id) @@ -57,6 +61,9 @@ def test_scheduling_a_battery(db, app, add_battery_assets, setup_test_data): ) print([v.event_value for v in power_values]) assert len(power_values) == 96 + assert ( + sum(v.event_value for v in power_values) < -0.5 + ), "some cycling should have occurred to make a profit, resulting in overall consumption due to losses" scheduler_specs = { From 1e49dd76c88d9f91a13f165f4ab1e7b532ab1ff0 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 11 May 2023 14:24:03 +0200 Subject: [PATCH 07/23] Add storage-efficiency to API tests Signed-off-by: F.N. Claessen --- flexmeasures/api/v3_0/tests/test_sensor_schedules.py | 7 +++++++ flexmeasures/api/v3_0/tests/utils.py | 1 + 2 files changed, 8 insertions(+) diff --git a/flexmeasures/api/v3_0/tests/test_sensor_schedules.py b/flexmeasures/api/v3_0/tests/test_sensor_schedules.py index 53891c677..b2ab198f0 100644 --- a/flexmeasures/api/v3_0/tests/test_sensor_schedules.py +++ b/flexmeasures/api/v3_0/tests/test_sensor_schedules.py @@ -221,6 +221,9 @@ def test_trigger_and_get_schedule( roundtrip_efficiency = ( float(message["roundtrip-efficiency"].replace("%", "")) / 100.0 ) + storage_efficiency = ( + float(message["storage-efficiency"].replace("%", "")) / 100.0 + ) soc_targets = message.get("soc-targets") else: start_soc = message["flex-model"]["soc-at-start"] / 1000 # in MWh @@ -228,6 +231,9 @@ def test_trigger_and_get_schedule( float(message["flex-model"]["roundtrip-efficiency"].replace("%", "")) / 100.0 ) + storage_efficiency = ( + float(message["flex-model"]["storage-efficiency"].replace("%", "")) / 100.0 + ) soc_targets = message["flex-model"].get("soc-targets") resolution = sensor.event_resolution if soc_targets: @@ -271,6 +277,7 @@ def test_trigger_and_get_schedule( start_soc, up_efficiency=roundtrip_efficiency**0.5, down_efficiency=roundtrip_efficiency**0.5, + storage_efficiency=storage_efficiency, decimal_precision=6, ) print(consumption_schedule) diff --git a/flexmeasures/api/v3_0/tests/utils.py b/flexmeasures/api/v3_0/tests/utils.py index 2ab606a4e..41d4e8e5a 100644 --- a/flexmeasures/api/v3_0/tests/utils.py +++ b/flexmeasures/api/v3_0/tests/utils.py @@ -61,6 +61,7 @@ def message_for_trigger_schedule( "soc-max": 40, # in kWh, according to soc-unit "soc-unit": "kWh", "roundtrip-efficiency": "98%", + "storage-efficiency": "99.99%", } if with_targets: if realistic_targets: From 1dcec70081799ca40493eb41be14a368b085057c Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 11 May 2023 14:27:24 +0200 Subject: [PATCH 08/23] Add endpoint documentation for the storage-efficiency field Signed-off-by: F.N. Claessen --- flexmeasures/api/v3_0/sensors.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/flexmeasures/api/v3_0/sensors.py b/flexmeasures/api/v3_0/sensors.py index 70d63ca87..515b5d0f5 100644 --- a/flexmeasures/api/v3_0/sensors.py +++ b/flexmeasures/api/v3_0/sensors.py @@ -271,6 +271,7 @@ def trigger_schedule( # noqa: C901 at which the state of charge (soc) is 12.1 kWh, with a target state of charge of 25 kWh at 4.00pm. The minimum and maximum soc are set to 10 and 25 kWh, respectively. Roundtrip efficiency for use in scheduling is set to 98%. + Storage efficiency is set to 99.99%, denoting the state of charge left after each time step equal to the sensor's resolution. Aggregate consumption (of all devices within this EMS) should be priced by sensor 9, and aggregate production should be priced by sensor 10, where the aggregate power flow in the EMS is described by the sum over sensors 13, 14 and 15 @@ -294,6 +295,7 @@ def trigger_schedule( # noqa: C901 "soc-min": 10, "soc-max": 25, "roundtrip-efficiency": 0.98, + "storage-efficiency": 0.9999, }, "flex-context": { "consumption-price-sensor": 9, From 8156ce059cb44b2a5157c6886283b9b8d93416bc Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 22 May 2023 13:33:30 +0200 Subject: [PATCH 09/23] Add docstring incl. doctest tests Signed-off-by: F.N. Claessen --- flexmeasures/data/schemas/scheduling/storage.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/flexmeasures/data/schemas/scheduling/storage.py b/flexmeasures/data/schemas/scheduling/storage.py index a4ea56223..d1fa74679 100644 --- a/flexmeasures/data/schemas/scheduling/storage.py +++ b/flexmeasures/data/schemas/scheduling/storage.py @@ -13,6 +13,20 @@ class EfficiencyField(QuantityField): + """Field that deserializes to a Quantity with % units. Must be greater than 0% and less than or equal to 100%. + + Examples: + + >>> ef = EfficiencyField() + >>> ef.deserialize(0.9) + + >>> ef.deserialize("90%") + + >>> ef.deserialize("0%") + Traceback (most recent call last): + ... + marshmallow.exceptions.ValidationError: ['Must be greater than 0 and less than or equal to 1.'] + """ def __init__(self, *args, **kwargs): super().__init__( "%", From ed89b16b892bbe6e93b6fb612ae279ae6f71dc63 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 22 May 2023 13:38:29 +0200 Subject: [PATCH 10/23] Add field to API flex model documentation Signed-off-by: F.N. Claessen --- documentation/api/notation.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/documentation/api/notation.rst b/documentation/api/notation.rst index 9b3de79d8..a3334e248 100644 --- a/documentation/api/notation.rst +++ b/documentation/api/notation.rst @@ -195,6 +195,7 @@ Here are the three types of flexibility models you can expect to be built-in: - ``soc-max`` (defaults to max soc target) - ``soc-targets`` (defaults to NaN values) - ``roundtrip-efficiency`` (defaults to 100%) + - ``storage-efficiency`` (defaults to 100%) - ``prefer-charging-sooner`` (defaults to True, also signals a preference to discharge later) For some examples, see the `[POST] /sensors/(id)/schedules/trigger <../api/v3_0.html#post--api-v3_0-sensors-(id)-schedules-trigger>`_ endpoint docs. From 548cdedfed0db259ff187ec5d3a9533c1f4bc2bb Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 6 Jun 2023 12:15:30 +0200 Subject: [PATCH 11/23] Update flex model docs Signed-off-by: F.N. Claessen --- documentation/api/notation.rst | 40 +++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/documentation/api/notation.rst b/documentation/api/notation.rst index a3334e248..767020a59 100644 --- a/documentation/api/notation.rst +++ b/documentation/api/notation.rst @@ -185,28 +185,32 @@ This means that API and CLI users don't have to send the whole flex model every Here are the three types of flexibility models you can expect to be built-in: -1) For storage devices (e.g. batteries, charge points, electric vehicle batteries connected to charge points), the schedule deals with the state of charge (SOC). +1) For **storage devices** (e.g. batteries, and :abbr:`EV (electric vehicle)` batteries connected to charge points), the schedule deals with the state of charge (SOC). - The possible flexibility parameters are: - - - ``soc-at-start`` (defaults to 0) - - ``soc-unit`` (kWh or MWh) - - ``soc-min`` (defaults to 0) - - ``soc-max`` (defaults to max soc target) - - ``soc-targets`` (defaults to NaN values) - - ``roundtrip-efficiency`` (defaults to 100%) - - ``storage-efficiency`` (defaults to 100%) - - ``prefer-charging-sooner`` (defaults to True, also signals a preference to discharge later) - - For some examples, see the `[POST] /sensors/(id)/schedules/trigger <../api/v3_0.html#post--api-v3_0-sensors-(id)-schedules-trigger>`_ endpoint docs. - -2) Shiftable process + The possible flexibility parameters are: + + - ``soc-at-start`` (defaults to 0) + - ``soc-unit`` (kWh or MWh) + - ``soc-min`` (defaults to 0) + - ``soc-max`` (defaults to max soc target) + - ``soc-minima`` (defaults to NaN values) + - ``soc-maxima`` (defaults to NaN values) + - ``soc-targets`` (defaults to NaN values) + - ``roundtrip-efficiency`` (defaults to 100%) + - ``storage-efficiency`` (defaults to 100%) + - ``prefer-charging-sooner`` (defaults to True, also signals a preference to discharge later) + + For some examples, see the `[POST] /sensors/(id)/schedules/trigger <../api/v3_0.html#post--api-v3_0-sensors-(id)-schedules-trigger>`_ endpoint docs. + +2) For **shiftable processes** .. todo:: A simple algorithm exists, needs integration into FlexMeasures and asset type clarified. -3) Heat pumps - - .. todo:: Also work in progress, needs model for heat loss compensation. +3) For **buffer devices** (e.g. thermal energy storage systems connected to heat pumps), use the same flexibility parameters described above for storage devices. Here are some tips to model a buffer with these parameters: + + - Describe the thermal energy content in kWh or MWh. + - Set ``soc-minima`` to the accumulative usage forecast. + - Set ``roundtrip-efficiency`` to the square of the conversion efficiency. In addition, folks who write their own custom scheduler (see :ref:`plugin_customization`) might also require their custom flexibility model. That's no problem, FlexMeasures will let the scheduler decide which flexibility model is relevant and how it should be validated. From d7349e39233ca2bb33a709b100ea1b07dfc400a2 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 6 Jun 2023 12:24:08 +0200 Subject: [PATCH 12/23] black Signed-off-by: F.N. Claessen --- flexmeasures/data/schemas/scheduling/storage.py | 1 + 1 file changed, 1 insertion(+) diff --git a/flexmeasures/data/schemas/scheduling/storage.py b/flexmeasures/data/schemas/scheduling/storage.py index 10e8fa532..25f840900 100644 --- a/flexmeasures/data/schemas/scheduling/storage.py +++ b/flexmeasures/data/schemas/scheduling/storage.py @@ -34,6 +34,7 @@ class EfficiencyField(QuantityField): ... marshmallow.exceptions.ValidationError: ['Must be greater than 0 and less than or equal to 1.'] """ + def __init__(self, *args, **kwargs): super().__init__( "%", From 0c91a609dc911f1580fa56566bf942fc2bb92126 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 6 Jun 2023 14:12:32 +0200 Subject: [PATCH 13/23] Clarify efficiencies using footnotes and math Signed-off-by: F.N. Claessen --- documentation/api/notation.rst | 8 ++++++-- documentation/conf.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/documentation/api/notation.rst b/documentation/api/notation.rst index 767020a59..801063093 100644 --- a/documentation/api/notation.rst +++ b/documentation/api/notation.rst @@ -197,9 +197,11 @@ Here are the three types of flexibility models you can expect to be built-in: - ``soc-maxima`` (defaults to NaN values) - ``soc-targets`` (defaults to NaN values) - ``roundtrip-efficiency`` (defaults to 100%) - - ``storage-efficiency`` (defaults to 100%) + - ``storage-efficiency`` (defaults to 100%) [#]_ - ``prefer-charging-sooner`` (defaults to True, also signals a preference to discharge later) + .. [#] The storage efficiency (e.g. 95% or 0.95) to use for the schedule is applied over each time step equal to the sensor resolution. For example, a storage efficiency of 95 percent per (absolute) day, for scheduling a 1-hour resolution sensor, should be passed as a storage efficiency of :math:`0.95^{1/24} = 0.997865`. + For some examples, see the `[POST] /sensors/(id)/schedules/trigger <../api/v3_0.html#post--api-v3_0-sensors-(id)-schedules-trigger>`_ endpoint docs. 2) For **shiftable processes** @@ -210,7 +212,9 @@ Here are the three types of flexibility models you can expect to be built-in: - Describe the thermal energy content in kWh or MWh. - Set ``soc-minima`` to the accumulative usage forecast. - - Set ``roundtrip-efficiency`` to the square of the conversion efficiency. + - Set ``roundtrip-efficiency`` to the square of the conversion efficiency. [#]_ + + .. [#] Setting a roundtrip efficiency of higher than 1 is not supported. We plan to implement a separate field for :abbr:`COP (coefficient of performance)` values. In addition, folks who write their own custom scheduler (see :ref:`plugin_customization`) might also require their custom flexibility model. That's no problem, FlexMeasures will let the scheduler decide which flexibility model is relevant and how it should be validated. diff --git a/documentation/conf.py b/documentation/conf.py index e6d727873..a1f48a76f 100644 --- a/documentation/conf.py +++ b/documentation/conf.py @@ -44,7 +44,7 @@ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.coverage", - "sphinx.ext.imgmath", + "sphinx.ext.mathjax", "sphinx.ext.ifconfig", "sphinx.ext.todo", "sphinx_copybutton", From 556c67a755c3bcf5c0fc30ec3f4a075b05916193 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 12 Jun 2023 20:17:41 +0200 Subject: [PATCH 14/23] docs: expand inline comment Signed-off-by: F.N. Claessen --- flexmeasures/utils/calculations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/utils/calculations.py b/flexmeasures/utils/calculations.py index af67a9b2c..e53325648 100644 --- a/flexmeasures/utils/calculations.py +++ b/flexmeasures/utils/calculations.py @@ -67,7 +67,7 @@ def apply_stock_changes_and_losses( # First apply the losses, then apply the stock change (i.e. the stock changes on the right side of the time interval in which the losses apply) next_stock = s * e + d elif how == "linear": - # Assume the change happens at a constant rate, leading to a linear stock change, and exponential decay + # Assume the change happens at a constant rate, leading to a linear stock change, and exponential decay, within the current interval next_stock = s * e + d * (e - 1) / math.log(e) else: raise NotImplementedError(f"Missing implementation for how='{how}'.") From f2f3ad1e6b22c14cbc940fc4f3e284b5f130b5d7 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 12 Jun 2023 21:24:05 +0200 Subject: [PATCH 15/23] docs: Add math explanation to docstring Signed-off-by: F.N. Claessen --- Makefile | 2 +- flexmeasures/utils/calculations.py | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index e0e115695..c9561011a 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ test: # ---- Documentation --- -gen_code_docs := False # by default code documentation is not generated +gen_code_docs := True # by default code documentation is not generated update-docs: @echo "Creating docs environment ..." diff --git a/flexmeasures/utils/calculations.py b/flexmeasures/utils/calculations.py index e53325648..b8f905ed6 100644 --- a/flexmeasures/utils/calculations.py +++ b/flexmeasures/utils/calculations.py @@ -47,6 +47,31 @@ def apply_stock_changes_and_losses( ) -> list[float]: """Assign stock changes and determine losses from storage efficiency. + The initial stock is exponentially decayed, as with each consecutive (constant-resolution) time step, + some constant percentage of the previous stock remains. For example: + + .. math:: + + 100 \\rightarrow 90 \\rightarrow 81 \\rightarrow 72.9 \\rightarrow ... + + For computing the decay of the changes, we make an assumption on how a delta :math:`d` is distributed within a given time step. + In case it happens at a constant rate, this leads to a linear stock change from one time step to the next. + + An :math:`e` is introduced when we apply exponential decay to that. + To see that, imagine we cut one time step in :math:`n` pieces (each with a stock change :math:`\\frac{d}{n}` ), + apply the efficiency to each piece :math:`k` (for the corresponding fraction of the time step :math:`k/n`), + and then take the limit :math:`n \\rightarrow \infty`: + + .. math:: + + \lim_{n \\rightarrow \infty} \sum_{k=0}^{n}{\\frac{d}{n} \eta^{k/n}} + + `which is `_: + + .. math:: + + d \cdot \\frac{\eta - 1}{e^{\eta}} + :param initial: initial stock :param changes: stock change for each step :param storage_efficiency: ratio of stock left after a step (constant ratio or one per step) From 580c16fd4289c1c1adb15f4f9d558cde40a87a63 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 12 Jun 2023 21:28:20 +0200 Subject: [PATCH 16/23] docs: add API changelog entry Signed-off-by: F.N. Claessen --- documentation/api/change_log.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/documentation/api/change_log.rst b/documentation/api/change_log.rst index 406d898cc..b19fdf993 100644 --- a/documentation/api/change_log.rst +++ b/documentation/api/change_log.rst @@ -5,6 +5,11 @@ API change log .. note:: The FlexMeasures API follows its own versioning scheme. This is also reflected in the URL, allowing developers to upgrade at their own pace. +v3.0-10 | 2023-06-12 +"""""""""""""""""""" + +- Introduced the ``storage-efficiency`` field to the ``flex-model``field for `/sensors//schedules/trigger` (POST). + v3.0-9 | 2023-04-26 """"""""""""""""""" From dacb6a39631bb7289e79ea5c676e249b6a208705 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 12 Jun 2023 21:37:11 +0200 Subject: [PATCH 17/23] fix: revert change to Makefile Signed-off-by: F.N. Claessen --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index c9561011a..e0e115695 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ test: # ---- Documentation --- -gen_code_docs := True # by default code documentation is not generated +gen_code_docs := False # by default code documentation is not generated update-docs: @echo "Creating docs environment ..." From c7eeafe2aeb46b8c601282c0ce6d8a7c4306cb4a Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 12 Jun 2023 21:38:07 +0200 Subject: [PATCH 18/23] docs: changelog entry, and update changelog entry for minima and maxima fields Signed-off-by: F.N. Claessen --- documentation/changelog.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index fbe7da151..1658fc137 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -9,7 +9,8 @@ v0.14.0 | June XX, 2023 New features ------------- -* Add multiple maxima and minima constraints into `StorageScheduler` [see `PR #680 `_] +* Allow setting a storage efficiency using the new ``storage-efficiency`` field when calling `/sensors//schedules/trigger` (POST) through the API (within the ``flex-model`` field), or when calling ``flexmeasures add schedule for-storage`` through the CLI [see `PR #679 `_] +* Allow setting multiple SoC maxima and minima constraints for the `StorageScheduler`, using the new ``soc-minima`` and ``soc-maxima``fields when calling `/sensors//schedules/trigger` (POST) through the API (within the ``flex-model`` field) [see `PR #680 `_] * Introduction of the classes `Reporter` and `PandasReporter` [see `PR #641 `_] * Add CLI command ``flexmeasures add report`` [see `PR #659 `_] * Add CLI command ``flexmeasures show reporters`` [see `PR #686 `_] From 8f0e72ccb3542b24ed385072d20756e174e86264 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 12 Jun 2023 21:41:06 +0200 Subject: [PATCH 19/23] docs: explain abbreviations Signed-off-by: F.N. Claessen --- documentation/changelog.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 1658fc137..8cfcdceb8 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -10,7 +10,7 @@ New features ------------- * Allow setting a storage efficiency using the new ``storage-efficiency`` field when calling `/sensors//schedules/trigger` (POST) through the API (within the ``flex-model`` field), or when calling ``flexmeasures add schedule for-storage`` through the CLI [see `PR #679 `_] -* Allow setting multiple SoC maxima and minima constraints for the `StorageScheduler`, using the new ``soc-minima`` and ``soc-maxima``fields when calling `/sensors//schedules/trigger` (POST) through the API (within the ``flex-model`` field) [see `PR #680 `_] +* Allow setting multiple :abbr:`SoC (state of charge)` maxima and minima constraints for the `StorageScheduler`, using the new ``soc-minima`` and ``soc-maxima``fields when calling `/sensors//schedules/trigger` (POST) through the API (within the ``flex-model`` field) [see `PR #680 `_] * Introduction of the classes `Reporter` and `PandasReporter` [see `PR #641 `_] * Add CLI command ``flexmeasures add report`` [see `PR #659 `_] * Add CLI command ``flexmeasures show reporters`` [see `PR #686 `_] @@ -146,7 +146,7 @@ Bugfixes * The CLI command ``flexmeasures show beliefs`` now supports plotting time series data that includes NaN values, and provides better support for plotting multiple sensors that do not share the same unit [see `PR #516 `_ and `PR #539 `_] * Fixed JSON wrapping of return message for `/sensors/data` (GET) [see `PR #543 `_] * Consistent CLI/UI support for asset lat/lng positions up to 7 decimal places (previously the UI rounded to 4 decimal places, whereas the CLI allowed more than 4) [see `PR #522 `_] -* Stop trimming the planning window in response to price availability, which is a problem when SoC targets occur outside of the available price window, by making a simplistic assumption about future prices [see `PR #538 `_] +* Stop trimming the planning window in response to price availability, which is a problem when :abbr:`SoC (state of charge)` targets occur outside of the available price window, by making a simplistic assumption about future prices [see `PR #538 `_] * Faster loading of initial charts and calendar date selection [see `PR #533 `_] Infrastructure / Support @@ -177,7 +177,7 @@ v0.11.3 | November 2, 2022 Bugfixes ----------- -* Fix scheduling with imperfect efficiencies, which resulted in exceeding the device's lower SoC limit. [see `PR #520 `_] +* Fix scheduling with imperfect efficiencies, which resulted in exceeding the device's lower :abbr:`SoC (state of charge)` limit. [see `PR #520 `_] * Fix scheduler for Charge Points when taking into account inflexible devices [see `PR #517 `_] * Prevent rounding asset lat/long positions to 4 decimal places when editing an asset in the UI [see `PR #522 `_] From df092f17f48c876009bf54895f99da0919222c95 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 12 Jun 2023 21:43:29 +0200 Subject: [PATCH 20/23] fix(docs): required space Signed-off-by: F.N. Claessen --- documentation/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 8cfcdceb8..24c0991fd 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -10,7 +10,7 @@ New features ------------- * Allow setting a storage efficiency using the new ``storage-efficiency`` field when calling `/sensors//schedules/trigger` (POST) through the API (within the ``flex-model`` field), or when calling ``flexmeasures add schedule for-storage`` through the CLI [see `PR #679 `_] -* Allow setting multiple :abbr:`SoC (state of charge)` maxima and minima constraints for the `StorageScheduler`, using the new ``soc-minima`` and ``soc-maxima``fields when calling `/sensors//schedules/trigger` (POST) through the API (within the ``flex-model`` field) [see `PR #680 `_] +* Allow setting multiple :abbr:`SoC (state of charge)` maxima and minima constraints for the `StorageScheduler`, using the new ``soc-minima`` and ``soc-maxima`` fields when calling `/sensors//schedules/trigger` (POST) through the API (within the ``flex-model`` field) [see `PR #680 `_] * Introduction of the classes `Reporter` and `PandasReporter` [see `PR #641 `_] * Add CLI command ``flexmeasures add report`` [see `PR #659 `_] * Add CLI command ``flexmeasures show reporters`` [see `PR #686 `_] From 665c1c4f2a34e7d1cdd855999b764678560745f4 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 12 Jun 2023 21:52:54 +0200 Subject: [PATCH 21/23] docs: expand changelog entry for `flexmeasures add report` cli command Signed-off-by: F.N. Claessen --- documentation/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 2b5c18da4..3d663760f 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -11,7 +11,7 @@ New features * Allow setting a storage efficiency using the new ``storage-efficiency`` field when calling `/sensors//schedules/trigger` (POST) through the API (within the ``flex-model`` field), or when calling ``flexmeasures add schedule for-storage`` through the CLI [see `PR #679 `_] * Allow setting multiple :abbr:`SoC (state of charge)` maxima and minima constraints for the `StorageScheduler`, using the new ``soc-minima`` and ``soc-maxima`` fields when calling `/sensors//schedules/trigger` (POST) through the API (within the ``flex-model`` field) [see `PR #680 `_] -* Add CLI command ``flexmeasures add report`` [see `PR #659 `_] +* New CLI command ``flexmeasures add report`` to calculate a custom report from sensor data and save the results to the database, with the option to export them to a CSV or Excel file [see `PR #659 `_] * Add CLI command ``flexmeasures show reporters`` [see `PR #686 `_] * Add CLI command ``flexmeasures show schedulers`` [see `PR #708 `_] From b57bcc3f29c853f0353868a9e104601d6d4dd62c Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 12 Jun 2023 21:59:54 +0200 Subject: [PATCH 22/23] docs: expand and combine changelog entries for `flexmeasures shows reports` and `flexmeasures show schedulers` CLI commands Signed-off-by: F.N. Claessen --- documentation/changelog.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/documentation/changelog.rst b/documentation/changelog.rst index 3d663760f..8f7441b15 100644 --- a/documentation/changelog.rst +++ b/documentation/changelog.rst @@ -12,8 +12,7 @@ New features * Allow setting a storage efficiency using the new ``storage-efficiency`` field when calling `/sensors//schedules/trigger` (POST) through the API (within the ``flex-model`` field), or when calling ``flexmeasures add schedule for-storage`` through the CLI [see `PR #679 `_] * Allow setting multiple :abbr:`SoC (state of charge)` maxima and minima constraints for the `StorageScheduler`, using the new ``soc-minima`` and ``soc-maxima`` fields when calling `/sensors//schedules/trigger` (POST) through the API (within the ``flex-model`` field) [see `PR #680 `_] * New CLI command ``flexmeasures add report`` to calculate a custom report from sensor data and save the results to the database, with the option to export them to a CSV or Excel file [see `PR #659 `_] -* Add CLI command ``flexmeasures show reporters`` [see `PR #686 `_] -* Add CLI command ``flexmeasures show schedulers`` [see `PR #708 `_] +* New CLI commands ``flexmeasures show reporters`` and ``flexmeasures show schedulers`` to list available reporters and schedulers, respectively, including any defined in registered plugins [see `PR #686 `_ and `PR #708 `_] Bugfixes ----------- From bcbfa8dc5217345f71f376801349e2af57188387 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 12 Jun 2023 22:31:00 +0200 Subject: [PATCH 23/23] flake8 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 b8f905ed6..e1dee05ad 100644 --- a/flexmeasures/utils/calculations.py +++ b/flexmeasures/utils/calculations.py @@ -45,32 +45,32 @@ def apply_stock_changes_and_losses( how: str = "linear", decimal_precision: int | None = None, ) -> list[float]: - """Assign stock changes and determine losses from storage efficiency. + r"""Assign stock changes and determine losses from storage efficiency. The initial stock is exponentially decayed, as with each consecutive (constant-resolution) time step, some constant percentage of the previous stock remains. For example: .. math:: - 100 \\rightarrow 90 \\rightarrow 81 \\rightarrow 72.9 \\rightarrow ... + 100 \rightarrow 90 \rightarrow 81 \rightarrow 72.9 \rightarrow ... For computing the decay of the changes, we make an assumption on how a delta :math:`d` is distributed within a given time step. In case it happens at a constant rate, this leads to a linear stock change from one time step to the next. An :math:`e` is introduced when we apply exponential decay to that. - To see that, imagine we cut one time step in :math:`n` pieces (each with a stock change :math:`\\frac{d}{n}` ), + To see that, imagine we cut one time step in :math:`n` pieces (each with a stock change :math:`\frac{d}{n}` ), apply the efficiency to each piece :math:`k` (for the corresponding fraction of the time step :math:`k/n`), - and then take the limit :math:`n \\rightarrow \infty`: + and then take the limit :math:`n \rightarrow \infty`: .. math:: - \lim_{n \\rightarrow \infty} \sum_{k=0}^{n}{\\frac{d}{n} \eta^{k/n}} + \lim_{n \rightarrow \infty} \sum_{k=0}^{n}{\frac{d}{n} \eta^{k/n}} `which is `_: .. math:: - d \cdot \\frac{\eta - 1}{e^{\eta}} + d \cdot \frac{\eta - 1}{e^{\eta}} :param initial: initial stock :param changes: stock change for each step