Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue 450 api option to customize which price and power sensors to take into account for scheduling #451

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
9cd08cd
Add optional API fields to specify which price sensors to use for con…
Flix6x Jun 24, 2022
7acb199
Rename API fields
Flix6x Jun 24, 2022
51551c0
Add optional API field to specify which inflexible device sensors to …
Flix6x Jun 25, 2022
cdf2055
Add test data for inflexible device
Flix6x Jun 25, 2022
7fd16c3
Add test case for scheduling a battery including an inflexible device
Flix6x Jun 25, 2022
dd2c454
Fix boundary conditions for including inflexible devices
Flix6x Jun 28, 2022
c17bf95
Assume perfect efficiency if no efficiency information is available
Flix6x Jun 28, 2022
dde07db
Switch from random periodic data to a step function, leaving a little…
Flix6x Jun 28, 2022
973bf87
Return test data with conftest setup
Flix6x Jun 28, 2022
96981de
Skip redundant sensor queries in tests
Flix6x Jun 28, 2022
418c186
Refactor: rename variable for clarity
Flix6x Jun 28, 2022
fc7f08a
Take into account the nominal capacity of the asset, if given
Flix6x Jun 28, 2022
ef6d5c0
Refactor: rename variable in test
Flix6x Jun 28, 2022
170be64
Convert consumption/production power sign for our scheduler
Flix6x Jun 30, 2022
116d422
Split fixtures, set up the building with a flexible device and two in…
Flix6x Jun 30, 2022
b04401d
Test building solver
Flix6x Jun 30, 2022
e9b3338
Merge branch 'main' into Issue-450_API_option_to_customize_which_pric…
Flix6x Jul 19, 2022
4e90f39
Merge branch 'main' into Issue-450_API_option_to_customize_which_pric…
Flix6x Jul 19, 2022
8c343dc
Fix passing along list of inflexible device sensors for battery sched…
Flix6x Aug 8, 2022
bb01440
Restrict data used for scheduling to what was known at the time of sc…
Flix6x Aug 8, 2022
38ceee6
Fix tests, given that the values for solar PV and residual demand (de…
Flix6x Aug 8, 2022
0c9e886
Let test schedule the future instead of the past, which requires fore…
Flix6x Aug 8, 2022
3a95f86
Set up test data for inflexible devices in the Europe/Amsterdam timez…
Flix6x Aug 8, 2022
38e87c0
Set up test data for EPEX market prices in the Europe/Amsterdam timez…
Flix6x Aug 8, 2022
e0a1507
Fix scheduling tests corresponding to having market prices available …
Flix6x Aug 8, 2022
46969c7
black
Flix6x Aug 8, 2022
9151e94
Add comment from earlier commit
Flix6x Aug 8, 2022
b6bb9ae
Clear up inline comment
Flix6x Aug 8, 2022
e41c0ae
Rename variable internally
Flix6x Aug 8, 2022
fdaaccc
Rename API field
Flix6x Aug 8, 2022
bd54acf
Rename CLI field with future deprecation warning
Flix6x Aug 8, 2022
67aadbd
Clarify that the consumption price is being applied to the aggregate …
Flix6x Aug 8, 2022
be58a3f
Clarify description of aggregate power flow
Flix6x Aug 24, 2022
fcbbc7d
Add return type annotation
Flix6x Aug 24, 2022
d429139
Expand docstring
Flix6x Aug 24, 2022
dd96b66
Fix use of wrong exception
Flix6x Aug 24, 2022
d8b7f57
Refactor: rename util function
Flix6x Aug 24, 2022
5cc79da
Changelog entry
Flix6x Aug 24, 2022
d101e1d
Implement recommended docstring revision
Flix6x Aug 24, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions documentation/changelog.rst
Expand Up @@ -13,6 +13,7 @@ New features
* Collapsible side-panel (hover/swipe) used for date selection on sensor charts, and various styling improvements [see `PR #447 <http://www.github.com/FlexMeasures/flexmeasures/pull/447>`_ and `PR #448 <http://www.github.com/FlexMeasures/flexmeasures/pull/448>`_]
* Switched from 12-hour AM/PM to 24-hour clock notation for time series chart axis labels [see `PR #446 <http://www.github.com/FlexMeasures/flexmeasures/pull/446>`_]
* Get data in a given resolution [see `PR #458 <http://www.github.com/FlexMeasures/flexmeasures/pull/458>`_]
* New API options to further customize the optimization context for scheduling, including the ability to use different prices for consumption and production (feed-in) [see `PR #451 <http://www.github.com/FlexMeasures/flexmeasures/pull/451>`_]

Bugfixes
-----------
Expand Down
2 changes: 1 addition & 1 deletion documentation/index.rst
Expand Up @@ -41,7 +41,7 @@ A tiny, but complete example: Let's install FlexMeasures from scratch. Then, usi
$ flexmeasures db upgrade # create tables
$ flexmeasures add toy-account --kind battery # setup account & a user, a battery (Id 2) and a market (Id 3)
$ flexmeasures add beliefs --sensor-id 3 --source toy-user prices-tomorrow.csv # load prices, also possible per API
$ flexmeasures add schedule --sensor-id 2 --optimization-context-id 3 \
$ flexmeasures add schedule --sensor-id 2 --consumption-price-sensor 3 \
--start ${TOMORROW}T07:00+01:00 --duration PT12H \
--soc-at-start 50% --roundtrip-efficiency 90% # this is also possible per API
$ flexmeasures show beliefs --sensor-id 2 --start ${TOMORROW}T07:00:00+01:00 --duration PT12H # also visible per UI, of course
Expand Down
4 changes: 2 additions & 2 deletions documentation/tut/toy-example-from-scratch.rst
Expand Up @@ -21,7 +21,7 @@ Below are the ``flexmeasures`` CLI commands we'll run, and which we'll explain s
# load prices to optimise the schedule against
$ flexmeasures add beliefs --sensor-id 3 --source toy-user prices-tomorrow.csv
# make the schedule
$ flexmeasures add schedule --sensor-id 2 --optimization-context-id 3 \
$ flexmeasures add schedule --sensor-id 2 --consumption-price-sensor 3 \
--start ${TOMORROW}T07:00+01:00 --duration PT12H \
--soc-at-start 50% --roundtrip-efficiency 90%

Expand Down Expand Up @@ -266,7 +266,7 @@ To keep it short, we'll only ask for a 12-hour window starting at 7am. Finally,

.. code-block:: console

$ flexmeasures add schedule --sensor-id 2 --optimization-context-id 3 \
$ flexmeasures add schedule --sensor-id 2 --consumption-price-sensor 3 \
--start ${TOMORROW}T07:00+01:00 --duration PT12H \
--soc-at-start 50% --roundtrip-efficiency 90%
New schedule is stored.
Expand Down
6 changes: 3 additions & 3 deletions flexmeasures/api/v1_3/tests/utils.py
Expand Up @@ -29,15 +29,15 @@ def message_for_post_udi_event(
message = {
"type": "PostUdiEventRequest",
"event": "ea1.2018-06.localhost:%s:204:soc",
"datetime": "2015-01-01T00:00:00+00:00",
"datetime": "2015-01-01T00:00:00+01:00",
"value": 12.1,
"unit": "kWh",
}
if targets:
message["event"] = message["event"] + "-with-targets"
message["targets"] = [{"value": 25, "datetime": "2015-01-02T23:00:00+00:00"}]
message["targets"] = [{"value": 25, "datetime": "2015-01-02T23:00:00+01:00"}]
if unknown_prices:
message[
"datetime"
] = "2040-01-01T00:00:00+00:00" # We have no beliefs in our test database about 2040 prices
] = "2040-01-01T00:00:00+01:00" # We have no beliefs in our test database about 2040 prices
return message
2 changes: 1 addition & 1 deletion flexmeasures/api/v2_0/tests/test_api_v2_0_assets.py
Expand Up @@ -100,7 +100,7 @@ def test_get_assets(client, add_charging_station_assets, use_owner_id, num_asset
battery = asset
assert battery
assert pd.Timestamp(battery["soc_datetime"]) == pd.Timestamp(
"2015-01-01T00:00:00+00:00"
"2015-01-01T00:00:00+01:00"
)
assert battery["owner_id"] == test_prosumer2_id
assert battery["capacity_in_mw"] == 2
Expand Down
27 changes: 25 additions & 2 deletions flexmeasures/api/v3_0/sensors.py
@@ -1,6 +1,6 @@
from datetime import datetime, timedelta
import json
from typing import Optional
from typing import List, Optional

from flask import current_app
from flask_classful import FlaskView, route
Expand Down Expand Up @@ -220,6 +220,15 @@ def get_data(self, response: dict):
), # todo: allow unit to be set per field, using QuantityField("%", validate=validate.Range(min=0, max=1))
"targets": fields.List(fields.Nested(TargetSchema), data_key="soc-targets"),
# todo: add a duration parameter, instead of falling back to FLEXMEASURES_PLANNING_HORIZON
"consumption_price_sensor": SensorIdField(
data_key="consumption-price-sensor", required=False
),
"production_price_sensor": SensorIdField(
data_key="production-price-sensor", required=False
),
"inflexible_device_sensors": fields.List(
SensorIdField, data_key="inflexible-device-sensors", required=False
),
},
location="json",
)
Expand All @@ -231,6 +240,9 @@ def trigger_schedule( # noqa: C901
unit: str,
prior: datetime,
roundtrip_efficiency: Optional[ur.Quantity] = None,
consumption_price_sensor: Optional[Sensor] = None,
production_price_sensor: Optional[Sensor] = None,
inflexible_device_sensors: Optional[List[Sensor]] = None,
**kwargs,
):
"""
Expand Down Expand Up @@ -258,6 +270,11 @@ def trigger_schedule( # noqa: C901
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%.
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
(plus the flexible sensor being optimized, of course).
Note that, if forecasts for sensors 13, 14 and 15 are not available, a schedule cannot be computed.

.. code-block:: json

Expand All @@ -273,7 +290,10 @@ def trigger_schedule( # noqa: C901
],
"soc-min": 10,
"soc-max": 25,
"roundtrip-efficiency": 0.98
"roundtrip-efficiency": 0.98,
"consumption-price-sensor": 9,
"production-price-sensor": 10,
"inflexible-device-sensors": [13, 14, 15]
}

**Example response**
Expand Down Expand Up @@ -395,6 +415,9 @@ def trigger_schedule( # noqa: C901
soc_min=soc_min,
soc_max=soc_max,
roundtrip_efficiency=roundtrip_efficiency,
consumption_price_sensor=consumption_price_sensor,
production_price_sensor=production_price_sensor,
inflexible_device_sensors=inflexible_device_sensors,
enqueue=True,
)

Expand Down
6 changes: 3 additions & 3 deletions flexmeasures/api/v3_0/tests/utils.py
Expand Up @@ -39,16 +39,16 @@ def message_for_post_udi_event(
targets: bool = False,
) -> dict:
message = {
"start": "2015-01-01T00:00:00+00:00",
"start": "2015-01-01T00:00:00+01:00",
"soc-at-start": 12.1,
"soc-unit": "kWh",
}
if targets:
message["soc-targets"] = [
{"value": 25, "datetime": "2015-01-02T23:00:00+00:00"}
{"value": 25, "datetime": "2015-01-02T23:00:00+01:00"}
]
if unknown_prices:
message[
"start"
] = "2040-01-01T00:00:00+00:00" # We have no beliefs in our test database about 2040 prices
] = "2040-01-01T00:00:00+01:00" # We have no beliefs in our test database about 2040 prices
return message
35 changes: 32 additions & 3 deletions flexmeasures/cli/data_add.py
Expand Up @@ -15,6 +15,7 @@
from sqlalchemy.exc import IntegrityError
from timely_beliefs.sensors.func_store.knowledge_horizons import x_days_ago_at_y_oclock
import timely_beliefs as tb
import timely_beliefs.utils as tb_utils
from workalendar.registry import registry as workalendar_registry

from flexmeasures.data import db
Expand Down Expand Up @@ -777,12 +778,26 @@ def create_forecasts(
required=True,
help="Create schedule for this sensor. Follow up with the sensor's ID.",
)
@click.option(
"--consumption-price-sensor",
"consumption_price_sensor",
type=SensorIdField(),
required=False,
help="Optimize consumption against this sensor. The sensor typically records an electricity price (e.g. in EUR/kWh), but this field can also be used to optimize against some emission intensity factor (e.g. in kg CO₂ eq./kWh). Follow up with the sensor's ID.",
)
@click.option(
"--production-price-sensor",
"production_price_sensor",
type=SensorIdField(),
required=False,
help="Optimize production against this sensor. Defaults to the consumption price sensor. The sensor typically records an electricity price (e.g. in EUR/kWh), but this field can also be used to optimize against some emission intensity factor (e.g. in kg CO₂ eq./kWh). Follow up with the sensor's ID.",
)
@click.option(
"--optimization-context-id",
"optimization_context_sensor",
type=SensorIdField(),
required=True,
help="Optimize against this sensor, which measures a price factor or CO₂ intensity factor. Follow up with the sensor's ID.",
help="To be deprecated. Use consumption-price-sensor instead.",
)
@click.option(
"--start",
Expand Down Expand Up @@ -847,6 +862,8 @@ def create_forecasts(
)
def create_schedule(
power_sensor: Sensor,
consumption_price_sensor: Sensor,
production_price_sensor: Sensor,
optimization_context_sensor: Sensor,
start: datetime,
duration: timedelta,
Expand All @@ -865,10 +882,20 @@ def create_schedule(
- only supports datetimes on the hour or a multiple of the sensor resolution thereafter
"""

# todo: deprecate the 'optimization-context-id' argument in favor of 'consumption-price-sensor' (announced v0.11.0)
tb_utils.replace_deprecated_argument(
"optimization-context-id",
optimization_context_sensor,
"consumption-price-sensor",
consumption_price_sensor,
)

# Parse input
if not power_sensor.measures_power:
click.echo(f"Sensor with ID {power_sensor.id} is not a power sensor.")
raise click.Abort()
if production_price_sensor is None:
production_price_sensor = consumption_price_sensor
end = start + duration
for attribute in ("min_soc_in_mwh", "max_soc_in_mwh"):
try:
Expand Down Expand Up @@ -922,7 +949,8 @@ def create_schedule(
soc_min=soc_min,
soc_max=soc_max,
roundtrip_efficiency=roundtrip_efficiency,
price_sensor=optimization_context_sensor,
consumption_price_sensor=consumption_price_sensor,
production_price_sensor=production_price_sensor,
)
if job:
print(f"New scheduling job {job.id} has been added to the queue.")
Expand All @@ -938,7 +966,8 @@ def create_schedule(
soc_min=soc_min,
soc_max=soc_max,
roundtrip_efficiency=roundtrip_efficiency,
price_sensor=optimization_context_sensor,
consumption_price_sensor=consumption_price_sensor,
production_price_sensor=production_price_sensor,
)
if success:
print("New schedule is stored.")
Expand Down
25 changes: 17 additions & 8 deletions flexmeasures/conftest.py
Expand Up @@ -3,6 +3,7 @@
from random import random
from datetime import datetime, timedelta
from typing import List, Dict
import pytz

from isodate import parse_duration
import pandas as pd
Expand Down Expand Up @@ -494,14 +495,18 @@ def add_market_prices(db: SQLAlchemy, setup_assets, setup_markets, setup_sources

# one day of test data (one complete sine curve)
time_slots = pd.date_range(
datetime(2015, 1, 1), datetime(2015, 1, 2), freq="1H", closed="left"
datetime(2015, 1, 1),
datetime(2015, 1, 2),
freq="1H",
closed="left",
tz="Europe/Amsterdam",
)
values = [
random() * (1 + np.sin(x * 2 * np.pi / 24)) for x in range(len(time_slots))
]
day1_beliefs = [
TimedBelief(
event_start=as_server_time(dt),
event_start=dt,
belief_horizon=timedelta(hours=0),
event_value=val,
source=setup_sources["Seita"],
Expand All @@ -513,12 +518,16 @@ def add_market_prices(db: SQLAlchemy, setup_assets, setup_markets, setup_sources

# another day of test data (8 expensive hours, 8 cheap hours, and again 8 expensive hours)
time_slots = pd.date_range(
datetime(2015, 1, 2), datetime(2015, 1, 3), freq="1H", closed="left"
datetime(2015, 1, 2),
datetime(2015, 1, 3),
freq="1H",
closed="left",
tz="Europe/Amsterdam",
)
values = [100] * 8 + [90] * 8 + [100] * 8
day2_beliefs = [
TimedBelief(
event_start=as_server_time(dt),
event_start=dt,
belief_horizon=timedelta(hours=0),
event_value=val,
source=setup_sources["Seita"],
Expand Down Expand Up @@ -573,7 +582,7 @@ def create_test_battery_assets(
max_soc_in_mwh=5,
min_soc_in_mwh=0,
soc_in_mwh=2.5,
soc_datetime=as_server_time(datetime(2015, 1, 1)),
soc_datetime=pytz.timezone("Europe/Amsterdam").localize(datetime(2015, 1, 1)),
soc_udi_event_id=203,
latitude=10,
longitude=100,
Expand All @@ -591,7 +600,7 @@ def create_test_battery_assets(
max_soc_in_mwh=5,
min_soc_in_mwh=0,
soc_in_mwh=2.5,
soc_datetime=as_server_time(datetime(2040, 1, 1)),
soc_datetime=pytz.timezone("Europe/Amsterdam").localize(datetime(2040, 1, 1)),
soc_udi_event_id=203,
latitude=10,
longitude=100,
Expand Down Expand Up @@ -644,7 +653,7 @@ def add_charging_station_assets(
max_soc_in_mwh=5,
min_soc_in_mwh=0,
soc_in_mwh=2.5,
soc_datetime=as_server_time(datetime(2015, 1, 1)),
soc_datetime=pytz.timezone("Europe/Amsterdam").localize(datetime(2015, 1, 1)),
soc_udi_event_id=203,
latitude=10,
longitude=100,
Expand All @@ -662,7 +671,7 @@ def add_charging_station_assets(
max_soc_in_mwh=5,
min_soc_in_mwh=0,
soc_in_mwh=2.5,
soc_datetime=as_server_time(datetime(2015, 1, 1)),
soc_datetime=pytz.timezone("Europe/Amsterdam").localize(datetime(2015, 1, 1)),
soc_udi_event_id=203,
latitude=10,
longitude=100,
Expand Down