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 482 add scheduling test for maximizing self consumption #532

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
ce8169c
Better documentation of flexibility model for storage in endpoint; re…
nhoening Oct 1, 2022
3bed7c0
add changelog entry
nhoening Oct 1, 2022
1fba29e
make tests work, include updating older API versions, make prefer_cha…
nhoening Oct 1, 2022
0e9259d
use storage_specs in CLI command, as well
nhoening Oct 1, 2022
c2b2787
remove default resolution of 15M, for now pass in what you want
nhoening Oct 2, 2022
c0400ae
various review comments
nhoening Oct 28, 2022
4718a8a
black
nhoening Oct 29, 2022
2eef27d
fix tests
nhoening Oct 29, 2022
1f68659
always load sensor when checking storage specs
nhoening Oct 31, 2022
2beb928
begin to handle source model and version during scheduling
nhoening Oct 31, 2022
378caba
we can get multiple sources from our query (in the old setting, when …
nhoening Oct 31, 2022
5b48baf
give our two in-built schedulers an official __author__ and __version__
nhoening Oct 31, 2022
b664910
review comments
nhoening Oct 31, 2022
e9ff60b
refactor getting data source for a job to util function; use the actu…
nhoening Nov 2, 2022
5fe72dc
pass sensor to check_storage_specs, as we always have it already
nhoening Nov 2, 2022
f80171d
wrap Scheduler in classes, unify data source handling a bit more
nhoening Nov 3, 2022
c04f0d7
Merge branch 'main' into refactor-scheduling-storage-specs
nhoening Nov 4, 2022
22cb852
Support pandas 1.4 (#525)
Flix6x Nov 10, 2022
dd47dab
Stop requiring min/max SoC attributes, which have defaults:
Flix6x Nov 10, 2022
add377f
Set up device constraint columns for efficiencies in Charge Point sch…
Flix6x Nov 10, 2022
ccab2ee
Derive flow constraints for battery scheduling, too (copied from Char…
Flix6x Nov 10, 2022
6344cb0
Refactor: rename BatteryScheduler to StorageScheduler
Flix6x Nov 10, 2022
7f9eced
Warn for deprecation of
Flix6x Nov 10, 2022
4bc593c
Use StorageScheduler instead of ChargingStationScheduler
Flix6x Nov 10, 2022
ec40bc0
Deprecate ChargingStationScheduler
Flix6x Nov 10, 2022
6914a4b
Refactor: move StorageScheduler to dedicated module
Flix6x Nov 10, 2022
5e6bd4e
Update docstring
Flix6x Nov 10, 2022
beb2770
fix test
Flix6x Nov 10, 2022
3bf1b97
flake8
Flix6x Nov 10, 2022
ed3284d
Merge remote-tracking branch 'origin/main' into refactor-scheduling-s…
Flix6x Nov 10, 2022
a9899a2
Lose the v in version strings; prefer versions showing up as 'version…
Flix6x Nov 10, 2022
ad22c35
Refactor: rename module
Flix6x Nov 11, 2022
4efe883
Deal with empty SoC targets
Flix6x Nov 11, 2022
09cf700
Stop wrapping DataFrame representations in logging
Flix6x Oct 9, 2022
5a3845d
Log warning instead of raising UnknownForecastException, and assume z…
Flix6x Oct 9, 2022
4713de1
Workaround for GH #484
Flix6x Nov 10, 2022
0780d53
Test maximizing self-consumption
Flix6x Nov 11, 2022
51e5809
Refactor: a single parameterized test instead of 2 tests with a lot o…
Flix6x Nov 11, 2022
ffec586
Remove debug statements
Flix6x Nov 11, 2022
66e84cc
black
Flix6x Nov 11, 2022
5bb7751
Correct mistake while refactoring
Flix6x Nov 11, 2022
3dda0e8
Check default price sensor in both scenarios
Flix6x Nov 17, 2022
2fac4f5
Expand explanation of optimization context
Flix6x Nov 17, 2022
8343eba
Reorder assertions and add more comments
Flix6x Nov 17, 2022
7790492
Merge remote-tracking branch 'origin/main' into issue-482_Add_schedul…
Flix6x Nov 21, 2022
17148d6
Upgrade timely-beliefs to partly resolve workaround
Flix6x Nov 21, 2022
f84c200
changelog entry
Flix6x Nov 21, 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 @@ -32,6 +32,7 @@ Infrastructure / Support
* Plugins can save BeliefsSeries, too, instead of just BeliefsDataFrames [see `PR #523 <http://www.github.com/FlexMeasures/flexmeasures/pull/523>`_]
* Improve documentation and code w.r.t. storage flexibility modelling ― prepare for handling other schedulers & merge battery and car charging schedulers [see `PR #511 <http://www.github.com/FlexMeasures/flexmeasures/pull/511>`_]
* Revised strategy for removing unchanged beliefs when saving data: retain the oldest measurement (ex-post belief), too [see `PR #518 <http://www.github.com/FlexMeasures/flexmeasures/pull/518>`_]
* Scheduling test for maximizing self-consumption, and improved time series db queries for fixed tariffs (and other long-term constants) [see `PR #532 <http://www.github.com/FlexMeasures/flexmeasures/pull/532>`_]


v0.11.3 | November 2, 2022
Expand Down
56 changes: 56 additions & 0 deletions flexmeasures/data/models/planning/tests/conftest.py
Expand Up @@ -3,6 +3,7 @@
from datetime import timedelta
import pytest

from timely_beliefs.sensors.func_store.knowledge_horizons import at_date
import pandas as pd

from flexmeasures.data.models.generic_assets import GenericAsset, GenericAssetType
Expand All @@ -18,6 +19,61 @@ def setup_planning_test_data(db, add_market_prices, add_charging_station_assets)
print("Setting up data for planning tests on %s" % db.engine)


@pytest.fixture(scope="module")
def create_test_tariffs(db, setup_accounts, setup_sources) -> dict[str, Sensor]:
"""Create a fixed consumption tariff and a fixed feed-in tariff that is lower."""

market_type = GenericAssetType(
name="tariff market",
)
db.session.add(market_type)
contract = GenericAsset(
name="supply contract",
generic_asset_type=market_type,
owner=setup_accounts["Supplier"],
)
db.session.add(contract)
consumption_price_sensor = Sensor(
name="fixed consumption tariff",
generic_asset=contract,
event_resolution=timedelta(hours=24 * 365),
unit="EUR/MWh",
knowledge_horizon=(at_date, {"knowledge_time": "2014-11-01T00:00+01:00"}),
)
db.session.add(consumption_price_sensor)
production_price_sensor = Sensor(
name="fixed feed-in tariff",
generic_asset=contract,
event_resolution=timedelta(hours=24 * 365),
unit="EUR/MWh",
knowledge_horizon=(at_date, {"knowledge_time": "2014-11-01T00:00+01:00"}),
)
db.session.add(production_price_sensor)

# Add prices
consumption_price = TimedBelief(
event_start="2015-01-01T00:00+01:00",
belief_time="2014-11-01T00:00+01:00", # publication date
event_value=300 * 1.21,
source=setup_sources["Seita"],
sensor=consumption_price_sensor,
)
db.session.add(consumption_price)
production_price = TimedBelief(
event_start="2015-01-01T00:00+01:00",
belief_time="2014-11-01T00:00+01:00", # publication date
event_value=300,
source=setup_sources["Seita"],
sensor=production_price_sensor,
)
db.session.add(production_price)
db.session.flush() # make sure that prices are assigned to price sensors
return {
"consumption_price_sensor": consumption_price_sensor,
"production_price_sensor": production_price_sensor,
}


@pytest.fixture(scope="module")
def building(db, setup_accounts, setup_markets) -> GenericAsset:
"""
Expand Down
71 changes: 57 additions & 14 deletions flexmeasures/data/models/planning/tests/test_solver.py
Expand Up @@ -272,25 +272,52 @@ def test_fallback_to_unsolvable_problem(target_soc, charging_station_name):
)


@pytest.mark.parametrize(
"market_scenario",
[
"dynamic contract",
"fixed contract",
],
)
def test_building_solver_day_2(
db,
add_battery_assets,
add_market_prices,
create_test_tariffs,
add_inflexible_device_forecasts,
inflexible_devices,
flexible_devices,
market_scenario: str,
):
"""Check battery scheduling results within the context of a building with PV, for day 2,
which is set up with 8 expensive, then 8 cheap, then again 8 expensive hours.
We expect the scheduler to:
- completely discharge within the first 8 hours
- completely charge within the next 8 hours
- completely discharge within the last 8 hours
"""Check battery scheduling results within the context of a building with PV, for day 2, against the following market scenarios:
1) a dynamic tariff with equal consumption and feed-in tariffs, that is set up with 8 expensive, then 8 cheap, then again 8 expensive hours.
2) a fixed consumption tariff and a fixed feed-in tariff that is lower, which incentives to maximize self-consumption of PV power into the battery.
In the test data:
- Hours with net production coincide with low dynamic market prices.
- Hours with net consumption coincide with high dynamic market prices.
So when the prices are low (in scenario 1), we have net production, and when they are high, net consumption.
That means we have first net consumption, then net production, and then net consumption again.
In either scenario, we expect the scheduler to:
- completely discharge within the first 8 hours (either due to 1) high prices, or 2) net consumption)
Flix6x marked this conversation as resolved.
Show resolved Hide resolved
- completely charge within the next 8 hours (either due to 1) low prices, or 2) net production)
- completely discharge within the last 8 hours (either due to 1) high prices, or 2) net consumption)
"""
epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none()
battery = flexible_devices["battery power sensor"]
building = battery.generic_asset
assert battery.get_attribute("market_id") == epex_da.id
default_consumption_price_sensor = Sensor.query.filter(
Sensor.name == "epex_da"
).one_or_none()
assert battery.get_attribute("market_id") == default_consumption_price_sensor.id
if market_scenario == "dynamic contract":
consumption_price_sensor = default_consumption_price_sensor
production_price_sensor = consumption_price_sensor
elif market_scenario == "fixed contract":
consumption_price_sensor = create_test_tariffs["consumption_price_sensor"]
production_price_sensor = create_test_tariffs["production_price_sensor"]
else:
raise NotImplementedError(
f"Missing test case for market conditions '{market_scenario}'"
)
tz = pytz.timezone("Europe/Amsterdam")
start = tz.localize(datetime(2015, 1, 2))
end = tz.localize(datetime(2015, 1, 3))
Expand All @@ -315,6 +342,8 @@ def test_building_solver_day_2(
end,
resolution,
storage_specs=storage_specs,
consumption_price_sensor=consumption_price_sensor,
production_price_sensor=production_price_sensor,
inflexible_device_sensors=inflexible_devices.values(),
)
soc_schedule = integrate_time_series(schedule, soc_at_start, decimal_precision=6)
Expand Down Expand Up @@ -353,14 +382,28 @@ def test_building_solver_day_2(
assert soc >= max(soc_min, battery.get_attribute("min_soc_in_mwh"))
assert soc <= battery.get_attribute("max_soc_in_mwh")

# Check whether the resulting soc schedule follows our expectations for 8 expensive, 8 cheap and 8 expensive hours
assert soc_schedule.iloc[-1] == max(
soc_min, battery.get_attribute("min_soc_in_mwh")
) # Battery sold out at the end of its planning horizon
# Check whether the resulting soc schedule follows our expectations for.
# To recap, in scenario 1 and 2, the schedule should mainly be influenced by:
# 1) 8 expensive, 8 cheap and 8 expensive hours
# 2) 8 net-consumption, 8 net-production and 8 net-consumption hours

# Result after 8 hours
# 1) Sell what you begin with
# 2) The battery discharged as far as it could during the first 8 net-consumption hours
assert soc_schedule.loc[start + timedelta(hours=8)] == max(
soc_min, battery.get_attribute("min_soc_in_mwh")
) # Sell what you begin with
)

# Result after second 8 hour-interval
# 1) Buy what you can to sell later, when prices will be high again
# 2) The battery charged with PV power as far as it could during the middle 8 net-production hours
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
)

# Result at end of day
# 1) The battery sold out at the end of its planning horizon
# 2) The battery discharged as far as it could during the last 8 net-consumption hours
assert soc_schedule.iloc[-1] == max(
soc_min, battery.get_attribute("min_soc_in_mwh")
)
8 changes: 6 additions & 2 deletions flexmeasures/data/models/time_series.py
Expand Up @@ -652,8 +652,9 @@ def search(
bdf = cls.search_session(
session=db.session,
sensor=sensor,
event_starts_after=event_starts_after,
event_ends_before=event_ends_before,
# Workaround (1st half) for https://github.com/FlexMeasures/flexmeasures/issues/484
event_ends_after=event_starts_after,
event_starts_before=event_ends_before,
beliefs_after=beliefs_after,
beliefs_before=beliefs_before,
horizons_at_least=horizons_at_least,
Expand Down Expand Up @@ -690,6 +691,9 @@ def search(
bdf = bdf.resample_events(
resolution, keep_only_most_recent_belief=most_recent_beliefs_only
)
# Workaround (2nd half) for https://github.com/FlexMeasures/flexmeasures/issues/484
bdf = bdf[bdf.event_starts >= event_starts_after]
bdf = bdf[bdf.event_ends <= event_ends_before]
bdf_dict[bdf.sensor.name] = bdf

if sum_multiple:
Expand Down
2 changes: 1 addition & 1 deletion requirements/app.in
Expand Up @@ -28,7 +28,7 @@ tldextract
pyomo>=5.6
tabulate
timetomodel>=0.7.1
timely-beliefs[forecast]>=1.13
timely-beliefs[forecast]>=1.14
python-dotenv
# a backport, not needed in Python3.8
importlib_metadata
Expand Down
2 changes: 1 addition & 1 deletion requirements/app.txt
Expand Up @@ -314,7 +314,7 @@ tabulate==0.8.10
# via -r requirements/app.in
threadpoolctl==3.1.0
# via scikit-learn
timely-beliefs[forecast]==1.13.0
timely-beliefs[forecast]==1.14.0
# via -r requirements/app.in
timetomodel==0.7.1
# via -r requirements/app.in
Expand Down