Skip to content

Commit

Permalink
Merge branch 'main' into feature/ui/breadcrumb
Browse files Browse the repository at this point in the history
  • Loading branch information
victorgarcia98 committed Dec 19, 2023
2 parents 2c9c802 + 060c190 commit 07732f8
Show file tree
Hide file tree
Showing 9 changed files with 532 additions and 8 deletions.
1 change: 1 addition & 0 deletions documentation/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Infrastructure / Support
* Remove obsolete database tables `price`, `power`, `market`, `market_type`, `weather`, `asset`, and `weather_sensor` [see `PR #921 <https://github.com/FlexMeasures/flexmeasures/pull/921>`_]
* New documentation section on constructing a flex model for :abbr:`V2G (vehicle-to-grid)` [see `PR #885 <https://github.com/FlexMeasures/flexmeasures/pull/885>`_]
* Allow charts in plugins to show currency codes (such as EUR) as currency symbols (€) [see `PR #922 <https://github.com/FlexMeasures/flexmeasures/pull/922>`_]
* Define device-level power constraints as sensors to create schedules with changing power limits. [see `PR #897 <https://github.com/FlexMeasures/flexmeasures/pull/897>`_]

Bugfixes
-----------
Expand Down
7 changes: 6 additions & 1 deletion flexmeasures/api/v3_0/sensors.py
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@ def trigger_schedule(
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
(plus the flexible sensor being optimized, of course).
The battery consumption power capacity is limited by sensor 42 and the production capacity is constant (30 kW).
Note that, if forecasts for sensors 13, 14 and 15 are not available, a schedule cannot be computed.
.. code-block:: json
Expand Down Expand Up @@ -325,7 +328,9 @@ def trigger_schedule(
"soc-max": 25,
"roundtrip-efficiency": 0.98,
"storage-efficiency": 0.9999,
"power-capacity": "25kW"
"power-capacity": "25kW",
"consumption-capacity" : {"sensor" : 42},
"production-capacity" : "30 kW"
},
"flex-context": {
"consumption-price-sensor": 9,
Expand Down
92 changes: 92 additions & 0 deletions flexmeasures/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -778,6 +778,33 @@ def create_test_battery_assets(
)
db.session.add(test_battery_sensor_no_prices)

test_battery_dynamic_power_capacity = GenericAsset(
name="Test battery with dynamic power capacity",
owner=setup_accounts["Prosumer"],
generic_asset_type=battery_type,
latitude=10,
longitude=100,
attributes=dict(
capacity_in_mw=10,
max_soc_in_mwh=20,
min_soc_in_mwh=0,
soc_in_mwh=2.0,
market_id=setup_markets["epex_da"].id,
),
)
test_battery_dynamic_capacity_power_sensor = Sensor(
name="power",
generic_asset=test_battery_dynamic_power_capacity,
event_resolution=timedelta(minutes=15),
unit="MW",
attributes=dict(
capacity_in_mw=10,
production_capacity="8 MW",
consumption_capacity="0.5 MW",
),
)
db.session.add(test_battery_dynamic_capacity_power_sensor)

test_small_battery = GenericAsset(
name="Test small battery",
owner=setup_accounts["Prosumer"],
Expand Down Expand Up @@ -817,6 +844,7 @@ def create_test_battery_assets(
"Test battery": test_battery,
"Test battery with no known prices": test_battery_no_prices,
"Test small battery": test_small_battery,
"Test battery with dynamic power capacity": test_battery_dynamic_power_capacity,
}


Expand Down Expand Up @@ -1075,3 +1103,67 @@ def error_generator():
@roles_accepted(ADMIN_ROLE)
def vips_only():
return jsonify({"message": "Nothing bad happened."}), 200


@pytest.fixture(scope="module")
def capacity_sensors(db, add_battery_assets, setup_sources):
battery = add_battery_assets["Test battery with dynamic power capacity"]
production_capacity_sensor = Sensor(
name="production capacity",
generic_asset=battery,
unit="kW",
event_resolution="PT15M",
attributes={"consumption_is_positive": True},
)
consumption_capacity_sensor = Sensor(
name="consumption capacity",
generic_asset=battery,
unit="kW",
event_resolution="PT15M",
attributes={"consumption_is_positive": True},
)

db.session.add_all([production_capacity_sensor, consumption_capacity_sensor])
db.session.flush()

time_slots = pd.date_range(
datetime(2015, 1, 2), datetime(2015, 1, 2, 7, 45), freq="15T"
).tz_localize("Europe/Amsterdam")

values = [200] * 4 * 4 + [300] * 4 * 4

beliefs = [
TimedBelief(
event_start=dt,
belief_horizon=parse_duration("PT0M"),
event_value=val,
sensor=production_capacity_sensor,
source=setup_sources["Seita"],
)
for dt, val in zip(time_slots, values)
]
db.session.add_all(beliefs)
db.session.commit()

time_slots = pd.date_range(
datetime(2015, 1, 2), datetime(2015, 1, 2, 7, 45), freq="15T"
).tz_localize("Europe/Amsterdam")

values = [250] * 4 * 4 + [150] * 4 * 4

beliefs = [
TimedBelief(
event_start=dt,
belief_horizon=parse_duration("PT0M"),
event_value=val,
sensor=consumption_capacity_sensor,
source=setup_sources["Seita"],
)
for dt, val in zip(time_slots, values)
]
db.session.add_all(beliefs)
db.session.commit()

yield dict(
production=production_capacity_sensor, consumption=consumption_capacity_sensor
)
32 changes: 29 additions & 3 deletions flexmeasures/data/models/planning/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,14 @@
initialize_df,
get_power_values,
fallback_charging_policy,
get_continuous_series_sensor_or_quantity,
)
from flexmeasures.data.models.planning.exceptions import InfeasibleProblemException
from flexmeasures.data.schemas.scheduling.storage import StorageFlexModelSchema
from flexmeasures.data.schemas.scheduling import FlexContextSchema
from flexmeasures.utils.time_utils import get_max_planning_horizon
from flexmeasures.utils.coding_utils import deprecated
from flexmeasures.utils.unit_utils import ur
from flexmeasures.utils.unit_utils import ur, convert_units


def check_and_convert_power_capacity(
Expand Down Expand Up @@ -198,14 +199,39 @@ def _prepare(self, skip_validation: bool = False) -> tuple: # noqa: C901
soc_min,
)

consumption_capacity = self.flex_model.get("consumption_capacity")
production_capacity = self.flex_model.get("production_capacity")

if sensor.get_attribute("is_strictly_non_positive"):
device_constraints[0]["derivative min"] = 0
else:
device_constraints[0]["derivative min"] = power_capacity_in_mw * -1
device_constraints[0]["derivative min"] = (
-1
) * get_continuous_series_sensor_or_quantity(
quantity_or_sensor=production_capacity,
actuator=sensor,
unit=sensor.unit,
query_window=(start, end),
resolution=resolution,
beliefs_before=belief_time,
fallback_attribute="production_capacity",
max_value=convert_units(power_capacity_in_mw, "MW", sensor.unit),
)
if sensor.get_attribute("is_strictly_non_negative"):
device_constraints[0]["derivative max"] = 0
else:
device_constraints[0]["derivative max"] = power_capacity_in_mw
device_constraints[0][
"derivative max"
] = get_continuous_series_sensor_or_quantity(
quantity_or_sensor=consumption_capacity,
actuator=sensor,
unit=sensor.unit,
query_window=(start, end),
resolution=resolution,
beliefs_before=belief_time,
fallback_attribute="consumption_capacity",
max_value=convert_units(power_capacity_in_mw, "MW", sensor.unit),
)

# Apply round-trip efficiency evenly to charging and discharging
device_constraints[0]["derivative down efficiency"] = (
Expand Down
155 changes: 153 additions & 2 deletions flexmeasures/data/models/planning/tests/test_solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,8 @@ def test_battery_solver_day_1(
s.id for s in add_inflexible_device_forecasts.keys()
]
if use_inflexible_device
else []
else [],
"site-power-capacity": "2 MW",
},
)
schedule = scheduler.compute()
Expand Down Expand Up @@ -1008,7 +1009,7 @@ def compute_schedule(flex_model):
def get_sensors_from_db(battery_assets, battery_name="Test battery"):
# get the sensors from the database
epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
battery = battery_assets["Test battery"].sensors[0]
battery = battery_assets[battery_name].sensors[0]
assert battery.get_attribute("market_id") == epex_da.id

return epex_da, battery
Expand Down Expand Up @@ -1297,3 +1298,153 @@ def test_build_device_soc_values(caplog, soc_values, log_message):
)
print(device_values)
assert log_message in caplog.text


@pytest.mark.parametrize(
"battery_name, production_sensor, consumption_sensor, production_quantity, consumption_quantity, expected_production, expected_consumption",
[
(
"Test battery with dynamic power capacity",
False,
False,
None,
None,
[-8] * 24 * 4, # from the power sensor attribute 'production_capacity'
[0.5] * 24 * 4, # from the power sensor attribute 'consumption_capacity'
),
(
"Test battery with dynamic power capacity",
True,
False,
None,
None,
# from the flex model field 'production-capacity' (a sensor),
# and when absent, defaulting to the max value from the power sensor attribute capacity_in_mw
[-0.2] * 4 * 4 + [-0.3] * 4 * 4 + [-10] * 16 * 4,
# from the power sensor attribute 'consumption_capacity'
[0.5] * 24 * 4,
),
(
"Test battery with dynamic power capacity",
False,
True,
None,
None,
# from the power sensor attribute 'consumption_capacity'
[-8] * 24 * 4,
# from the flex model field 'consumption-capacity' (a sensor),
# and when absent, defaulting to the max value from the power sensor attribute capacity_in_mw
[0.25] * 4 * 4 + [0.15] * 4 * 4 + [10] * 16 * 4,
),
(
"Test battery with dynamic power capacity",
False,
False,
"100 kW",
"200 kW",
# from the flex model field 'production-capacity' (a quantity)
[-0.1] * 24 * 4,
# from the flex model field 'consumption-capacity' (a quantity)
[0.2] * 24 * 4,
),
(
"Test battery with dynamic power capacity",
False,
False,
"1 MW",
"2 MW",
# from the flex model field 'production-capacity' (a quantity)
[-1] * 24 * 4,
# from the power sensor attribute 'consumption_capacity' (a quantity)
[2] * 24 * 4,
),
(
"Test battery",
False,
False,
None,
None,
# from the asset attribute 'capacity_in_mw'
[-2] * 24 * 4,
# from the asset attribute 'capacity_in_mw'
[2] * 24 * 4,
),
(
"Test battery",
False,
False,
"10 kW",
None,
# from the flex model field 'production-capacity' (a quantity)
[-0.01] * 24 * 4,
# from the asset attribute 'capacity_in_mw'
[2] * 24 * 4,
),
(
"Test battery",
False,
False,
"10 kW",
"100 kW",
# from the flex model field 'production-capacity' (a quantity)
[-0.01] * 24 * 4,
# from the flex model field 'consumption-capacity' (a quantity)
[0.1] * 24 * 4,
),
],
)
def test_battery_power_capacity_as_sensor(
db,
add_battery_assets,
add_inflexible_device_forecasts,
capacity_sensors,
battery_name,
production_sensor,
consumption_sensor,
production_quantity,
consumption_quantity,
expected_production,
expected_consumption,
):

epex_da, battery = get_sensors_from_db(
add_battery_assets, battery_name=battery_name
)

tz = pytz.timezone("Europe/Amsterdam")
start = tz.localize(datetime(2015, 1, 2))
end = tz.localize(datetime(2015, 1, 3))
resolution = timedelta(minutes=15)
soc_at_start = 10

flex_model = {
"soc-at-start": soc_at_start,
"roundtrip-efficiency": "100%",
"prefer-charging-sooner": False,
}

if production_sensor:
flex_model["production-capacity"] = {
"sensor": capacity_sensors["production"].id
}

if consumption_sensor:
flex_model["consumption-capacity"] = {
"sensor": capacity_sensors["consumption"].id
}

if production_quantity is not None:
flex_model["production-capacity"] = production_quantity

if consumption_quantity is not None:
flex_model["consumption-capacity"] = consumption_quantity

scheduler: Scheduler = StorageScheduler(
battery, start, end, resolution, flex_model=flex_model
)

data_to_solver = scheduler._prepare()
device_constraints = data_to_solver[5][0]

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

0 comments on commit 07732f8

Please sign in to comment.