From 0603559ebf10274c8126b81d23202be34a511ac2 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 1 Feb 2021 18:06:11 +0100 Subject: [PATCH 01/12] Mix in timely beliefs Sensor. Add custom database revision. --- flexmeasures/data/models/assets.py | 4 +- flexmeasures/data/models/markets.py | 3 +- flexmeasures/data/models/weather.py | 3 +- flexmeasures/data/services/time_series.py | 6 +- ...or_with_asset_market_and_weather_sensor.py | 123 ++++++++++++++++++ 5 files changed, 130 insertions(+), 9 deletions(-) create mode 100644 migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py diff --git a/flexmeasures/data/models/assets.py b/flexmeasures/data/models/assets.py index 0c9d3150c..5d66b4dc4 100644 --- a/flexmeasures/data/models/assets.py +++ b/flexmeasures/data/models/assets.py @@ -2,7 +2,7 @@ from datetime import timedelta import isodate - +import timely_beliefs as tb from sqlalchemy.orm import Query from flexmeasures.data.config import db @@ -68,7 +68,7 @@ def __repr__(self): return "" % self.name -class Asset(db.Model): +class Asset(db.Model, tb.SensorDBMixin): """Each asset is an energy- consuming or producing hardware. """ id = db.Column(db.Integer, primary_key=True) diff --git a/flexmeasures/data/models/markets.py b/flexmeasures/data/models/markets.py index bc56e4550..a34ba90b3 100644 --- a/flexmeasures/data/models/markets.py +++ b/flexmeasures/data/models/markets.py @@ -1,6 +1,7 @@ from typing import Dict from datetime import timedelta +import timely_beliefs as tb from sqlalchemy.orm import Query from flexmeasures.data.config import db @@ -40,7 +41,7 @@ def __repr__(self): return "" % self.name -class Market(db.Model): +class Market(db.Model, tb.SensorDBMixin): """Each market is a pricing service.""" id = db.Column(db.Integer, primary_key=True) diff --git a/flexmeasures/data/models/weather.py b/flexmeasures/data/models/weather.py index 5cc68cd07..9eeca6fed 100644 --- a/flexmeasures/data/models/weather.py +++ b/flexmeasures/data/models/weather.py @@ -2,6 +2,7 @@ from datetime import timedelta import math +import timely_beliefs as tb from sqlalchemy.orm import Query from sqlalchemy.ext.hybrid import hybrid_method, hybrid_property from sqlalchemy.sql.expression import func @@ -35,7 +36,7 @@ def __repr__(self): return "" % self.name -class WeatherSensor(db.Model): +class WeatherSensor(db.Model, tb.SensorDBMixin): """A weather sensor has a location on Earth and measures weather values of a certain weather sensor type, such as temperature, wind speed and radiation.""" diff --git a/flexmeasures/data/services/time_series.py b/flexmeasures/data/services/time_series.py index b1c60df01..299e06476 100644 --- a/flexmeasures/data/services/time_series.py +++ b/flexmeasures/data/services/time_series.py @@ -181,11 +181,7 @@ def query_time_series_data( if current_app.config.get("FLEXMEASURES_MODE", "") == "demo": df.index = df.index.map(lambda t: t.replace(year=datetime.now().year)) - flexmeasures_sensor = find_sensor_by_name(name=generic_asset_name) - sensor = tb.Sensor( - name=generic_asset_name, - event_resolution=flexmeasures_sensor.event_resolution, - ) + sensor = find_sensor_by_name(name=generic_asset_name) bdf = tb.BeliefsDataFrame(df.reset_index(), sensor=sensor) # re-sample data to the resolution we need to serve diff --git a/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py b/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py new file mode 100644 index 000000000..63a8bbe05 --- /dev/null +++ b/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py @@ -0,0 +1,123 @@ +"""mix in timely beliefs sensor with asset, market and weather sensor; introduce knowledge horizons + +Revision ID: 22ce09690d23 +Revises: 564e8df4e3a9 +Create Date: 2021-01-31 14:31:16.370110 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "22ce09690d23" +down_revision = "564e8df4e3a9" +branch_labels = None +depends_on = None + + +def upgrade(): + + # Mix in timely_beliefs.Sensor with flexmeasures.Asset + op.add_column( + "asset", sa.Column("knowledge_horizon_fnc", sa.String(length=80), nullable=True) + ) + op.execute( + "update asset set knowledge_horizon_fnc = 'determine_ex_post_knowledge_horizon';" + ) # default assumption that power measurements are known right after the fact + op.alter_column("asset", "knowledge_horizon_fnc", nullable=False) + + op.add_column("asset", sa.Column("knowledge_horizon_par", sa.JSON(), nullable=True)) + op.execute( + """update asset set knowledge_horizon_par = '{"ex_post_horizon": "PT0H"}';""" + ) + op.alter_column("asset", "knowledge_horizon_par", nullable=False) + + op.add_column("asset", sa.Column("timezone", sa.String(length=80), nullable=True)) + op.execute("update asset set timezone = 'Asia/Seoul';") + op.alter_column("asset", "timezone", nullable=False) + + # Mix in timely_beliefs.Sensor with flexmeasures.Market + op.add_column( + "market", + sa.Column("knowledge_horizon_fnc", sa.String(length=80), nullable=True), + ) + op.execute( + "update market set knowledge_horizon_fnc = 'determine_ex_ante_knowledge_horizon';" + ) # default assumption that prices are known before a transaction + op.execute( + "update market set knowledge_horizon_fnc = 'determine_ex_ante_knowledge_horizon_for_x_days_ago_at_y_oclock' where name='epex_da';" + ) + op.execute( + "update market set knowledge_horizon_fnc = 'determine_ex_ante_knowledge_horizon_for_x_days_ago_at_y_oclock' where name='kpx_da';" + ) + op.execute( + "update market set knowledge_horizon_fnc = 'determine_knowledge_horizon_for_fixed_knowledge_time' where name in ('kepco_cs_fast', 'kepco_cs_slow', 'kepco_cs_smart');" + ) + op.alter_column("market", "knowledge_horizon_fnc", nullable=False) + + op.add_column( + "market", sa.Column("knowledge_horizon_par", sa.JSON(), nullable=True) + ) + op.execute( + """update market set knowledge_horizon_par = '{"ex_ante_horizon": "PT0H"}';""" + ) + op.execute( + """update market set knowledge_horizon_par = '{"x": 1, "y": 12, "z": "Europe/Paris"}' where name='epex_da';""" + ) # gate closure at 12:00 on the preceding day, with expected price publication at 12.42 and 12.55 (from EPEX Spot Day-Ahead Multi-Regional Coupling, https://www.epexspot.com/en/downloads#rules-fees-processes ) + op.execute( + """update market set knowledge_horizon_par = '{"x": 1, "y": 10, "z": "Asia/Seoul"}' where name='kpx_da';""" + ) # gate closure at 10.00 on the preceding day, with expected price publication at 15.00 (from KPX Power Market Operation, https://www.slideshare.net/sjchung0/power-market-operation ) + op.execute( + """update market set knowledge_horizon_par = '{"knowledge_time": "2014-12-31 00:00:00+00:00"}' where name in ('kepco_cs_fast', 'kepco_cs_slow', 'kepco_cs_smart');""" + ) # tariff publication date (unofficial) + op.alter_column("market", "knowledge_horizon_par", nullable=False) + op.execute( + "update price set horizon = interval '0 hours' from market where market_id = market.id and market.name in ('kepco_cs_fast', 'kepco_cs_slow', 'kepco_cs_smart');" + ) # 0 hours after fixed knowledge time (i.e. at publication date) + + op.add_column("market", sa.Column("timezone", sa.String(length=80), nullable=True)) + op.execute("update market set timezone = 'UTC';") + op.execute("update market set timezone = 'Europe/Paris' where name='epex_da';") + op.execute("update market set timezone = 'Asia/Seoul' where unit='KRW/kWh';") + op.alter_column("market", "timezone", nullable=False) + + # Mix in timely_beliefs.Sensor with flexmeasures.WeatherSensor + op.add_column( + "weather_sensor", + sa.Column("knowledge_horizon_fnc", sa.String(length=80), nullable=True), + ) + op.execute( + "update weather_sensor set knowledge_horizon_fnc = 'determine_ex_post_knowledge_horizon';" + ) # default assumption that weather measurements are known right after the fact + op.alter_column("weather_sensor", "knowledge_horizon_fnc", nullable=False) + + op.add_column( + "weather_sensor", sa.Column("knowledge_horizon_par", sa.JSON(), nullable=True) + ) + op.execute( + """update weather_sensor set knowledge_horizon_par = '{"ex_post_horizon": "PT0H"}';""" + ) + op.alter_column("weather_sensor", "knowledge_horizon_par", nullable=False) + + op.add_column( + "weather_sensor", sa.Column("timezone", sa.String(length=80), nullable=True) + ) + op.execute("update weather_sensor set timezone = 'Asia/Seoul';") + op.alter_column("weather_sensor", "timezone", nullable=False) + + +def downgrade(): + # Drop mixed in columns + op.drop_column("asset", "timezone") + op.drop_column("asset", "knowledge_horizon_par") + op.drop_column("asset", "knowledge_horizon_fnc") + op.drop_column("market", "timezone") + op.drop_column("market", "knowledge_horizon_par") + op.drop_column("market", "knowledge_horizon_fnc") + op.drop_column("weather_sensor", "timezone") + op.drop_column("weather_sensor", "knowledge_horizon_par") + op.drop_column("weather_sensor", "knowledge_horizon_fnc") + op.execute( + "update price set horizon = ((datetime + market.event_resolution) - '2014-12-31 00:00:00+00:00') from market where market_id = market.id and name in ('kepco_cs_fast', 'kepco_cs_slow', 'kepco_cs_smart');" + ) # rolling horizon before end of event (i.e. at publication date) From 3aa5322db8b248446897bf26f1702cfc0f2345b0 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 2 Feb 2021 15:49:43 +0100 Subject: [PATCH 02/12] Set default knowledge horizon functions for physical and economic sensors. Fix tests. --- flexmeasures/conftest.py | 2 ++ flexmeasures/data/models/assets.py | 5 +++++ flexmeasures/data/models/markets.py | 5 +++++ flexmeasures/data/models/weather.py | 5 +++++ 4 files changed, 17 insertions(+) diff --git a/flexmeasures/conftest.py b/flexmeasures/conftest.py index 38770e144..d11dc50df 100644 --- a/flexmeasures/conftest.py +++ b/flexmeasures/conftest.py @@ -107,6 +107,8 @@ def setup_markets(db): market_type=day_ahead, event_resolution=timedelta(hours=1), unit="EUR/MWh", + knowledge_horizon_fnc="determine_ex_ante_knowledge_horizon_for_x_days_ago_at_y_oclock", + knowledge_horizon_par={"x": 1, "y": 12, "z": "Europe/Paris"}, ) db.session.add(epex_da) diff --git a/flexmeasures/data/models/assets.py b/flexmeasures/data/models/assets.py index 5d66b4dc4..d87c32e92 100644 --- a/flexmeasures/data/models/assets.py +++ b/flexmeasures/data/models/assets.py @@ -105,6 +105,11 @@ class Asset(db.Model, tb.SensorDBMixin): market_id = db.Column(db.Integer, db.ForeignKey("market.id"), nullable=True) def __init__(self, **kwargs): + # Set default knowledge horizon function for a physical sensor + if "knowledge_horizon_fnc" not in kwargs: + kwargs["knowledge_horizon_fnc"] = "determine_ex_post_knowledge_horizon" + if "knowledge_horizon_par" not in kwargs: + kwargs["knowledge_horizon_par"] = {"ex_post_horizon": "PT0H"} super(Asset, self).__init__(**kwargs) self.name = self.name.replace(" (MW)", "") if "display_name" not in kwargs: diff --git a/flexmeasures/data/models/markets.py b/flexmeasures/data/models/markets.py index a34ba90b3..63e63b820 100644 --- a/flexmeasures/data/models/markets.py +++ b/flexmeasures/data/models/markets.py @@ -56,6 +56,11 @@ class Market(db.Model, tb.SensorDBMixin): ) def __init__(self, **kwargs): + # Set default knowledge horizon function for an economic sensor + if "knowledge_horizon_fnc" not in kwargs: + kwargs["knowledge_horizon_fnc"] = "determine_ex_ante_knowledge_horizon" + if "knowledge_horizon_par" not in kwargs: + kwargs["knowledge_horizon_par"] = {"ex_post_horizon": "PT0H"} super(Market, self).__init__(**kwargs) self.name = self.name.replace(" ", "_").lower() if "display_name" not in kwargs: diff --git a/flexmeasures/data/models/weather.py b/flexmeasures/data/models/weather.py index 9eeca6fed..6ac051f13 100644 --- a/flexmeasures/data/models/weather.py +++ b/flexmeasures/data/models/weather.py @@ -66,6 +66,11 @@ class WeatherSensor(db.Model, tb.SensorDBMixin): ) def __init__(self, **kwargs): + # Set default knowledge horizon function for a physical sensor + if "knowledge_horizon_fnc" not in kwargs: + kwargs["knowledge_horizon_fnc"] = "determine_ex_post_knowledge_horizon" + if "knowledge_horizon_par" not in kwargs: + kwargs["knowledge_horizon_par"] = {"ex_post_horizon": "PT0H"} super(WeatherSensor, self).__init__(**kwargs) self.name = self.name.replace(" ", "_").lower() From 1cbf7e2482094d665bca220a3fdc8df3362f2100 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Tue, 2 Feb 2021 16:49:34 +0100 Subject: [PATCH 03/12] Clean up redundant columns with respect to tb.SensorDBMixin. --- flexmeasures/data/models/assets.py | 8 -------- flexmeasures/data/models/markets.py | 6 ------ flexmeasures/data/models/weather.py | 6 ------ 3 files changed, 20 deletions(-) diff --git a/flexmeasures/data/models/assets.py b/flexmeasures/data/models/assets.py index d87c32e92..a9a6de24a 100644 --- a/flexmeasures/data/models/assets.py +++ b/flexmeasures/data/models/assets.py @@ -1,5 +1,4 @@ from typing import Dict, List, Tuple, Union -from datetime import timedelta import isodate import timely_beliefs as tb @@ -80,13 +79,6 @@ class Asset(db.Model, tb.SensorDBMixin): asset_type_name = db.Column( db.String(80), db.ForeignKey("asset_type.name"), nullable=False ) - unit = db.Column(db.String(80), default="", nullable=False) - # Expected resolution of time series for this sensor. - # Defaults to zero, as it can't be None (used for calculations during - # query building). You should set this to a realistic value! - event_resolution = db.Column( - db.Interval(), nullable=False, default=timedelta(minutes=0) - ) # How many MW at peak usage capacity_in_mw = db.Column(db.Float, nullable=False) # State of charge in MWh and its datetime and udi event diff --git a/flexmeasures/data/models/markets.py b/flexmeasures/data/models/markets.py index 63e63b820..e35c2e3a6 100644 --- a/flexmeasures/data/models/markets.py +++ b/flexmeasures/data/models/markets.py @@ -1,5 +1,4 @@ from typing import Dict -from datetime import timedelta import timely_beliefs as tb from sqlalchemy.orm import Query @@ -44,16 +43,11 @@ def __repr__(self): class Market(db.Model, tb.SensorDBMixin): """Each market is a pricing service.""" - id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80), unique=True) display_name = db.Column(db.String(80), default="", unique=True) market_type_name = db.Column( db.String(80), db.ForeignKey("market_type.name"), nullable=False ) - unit = db.Column(db.String(80), default="", nullable=False) - event_resolution = db.Column( - db.Interval(), nullable=False, default=timedelta(minutes=0) - ) def __init__(self, **kwargs): # Set default knowledge horizon function for an economic sensor diff --git a/flexmeasures/data/models/weather.py b/flexmeasures/data/models/weather.py index 6ac051f13..72cb3d5c0 100644 --- a/flexmeasures/data/models/weather.py +++ b/flexmeasures/data/models/weather.py @@ -1,5 +1,4 @@ from typing import Dict, Tuple -from datetime import timedelta import math import timely_beliefs as tb @@ -40,16 +39,11 @@ class WeatherSensor(db.Model, tb.SensorDBMixin): """A weather sensor has a location on Earth and measures weather values of a certain weather sensor type, such as temperature, wind speed and radiation.""" - id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(80), unique=True) display_name = db.Column(db.String(80), default="", unique=False) weather_sensor_type_name = db.Column( db.String(80), db.ForeignKey("weather_sensor_type.name"), nullable=False ) - unit = db.Column(db.String(80), default="", nullable=False) - event_resolution = db.Column( - db.Interval(), nullable=False, default=timedelta(minutes=0) - ) # latitude is the North/South coordinate latitude = db.Column(db.Float, nullable=False) # longitude is the East/West coordinate From 7463985057b5df17704cbdef7a42a4631e2372a1 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Wed, 3 Feb 2021 12:29:00 +0100 Subject: [PATCH 04/12] Missed one redundant column. --- flexmeasures/data/models/assets.py | 1 - 1 file changed, 1 deletion(-) diff --git a/flexmeasures/data/models/assets.py b/flexmeasures/data/models/assets.py index a9a6de24a..da0edc5b4 100644 --- a/flexmeasures/data/models/assets.py +++ b/flexmeasures/data/models/assets.py @@ -70,7 +70,6 @@ def __repr__(self): class Asset(db.Model, tb.SensorDBMixin): """Each asset is an energy- consuming or producing hardware. """ - id = db.Column(db.Integer, primary_key=True) # The name name = db.Column(db.String(80), default="", unique=True) # The name we want to see (don't unnecessarily capitalize, so it can be used in a sentence) From f236fefafe14943a8a738fe24802a8c52c264aef Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 4 Feb 2021 16:40:16 +0100 Subject: [PATCH 05/12] Let timely-beliefs set the default knowledge horizon for physical sensors. --- flexmeasures/data/models/assets.py | 5 ----- flexmeasures/data/models/weather.py | 5 ----- 2 files changed, 10 deletions(-) diff --git a/flexmeasures/data/models/assets.py b/flexmeasures/data/models/assets.py index da0edc5b4..2f3a35a8e 100644 --- a/flexmeasures/data/models/assets.py +++ b/flexmeasures/data/models/assets.py @@ -96,11 +96,6 @@ class Asset(db.Model, tb.SensorDBMixin): market_id = db.Column(db.Integer, db.ForeignKey("market.id"), nullable=True) def __init__(self, **kwargs): - # Set default knowledge horizon function for a physical sensor - if "knowledge_horizon_fnc" not in kwargs: - kwargs["knowledge_horizon_fnc"] = "determine_ex_post_knowledge_horizon" - if "knowledge_horizon_par" not in kwargs: - kwargs["knowledge_horizon_par"] = {"ex_post_horizon": "PT0H"} super(Asset, self).__init__(**kwargs) self.name = self.name.replace(" (MW)", "") if "display_name" not in kwargs: diff --git a/flexmeasures/data/models/weather.py b/flexmeasures/data/models/weather.py index 72cb3d5c0..8d88eee85 100644 --- a/flexmeasures/data/models/weather.py +++ b/flexmeasures/data/models/weather.py @@ -60,11 +60,6 @@ class WeatherSensor(db.Model, tb.SensorDBMixin): ) def __init__(self, **kwargs): - # Set default knowledge horizon function for a physical sensor - if "knowledge_horizon_fnc" not in kwargs: - kwargs["knowledge_horizon_fnc"] = "determine_ex_post_knowledge_horizon" - if "knowledge_horizon_par" not in kwargs: - kwargs["knowledge_horizon_par"] = {"ex_post_horizon": "PT0H"} super(WeatherSensor, self).__init__(**kwargs) self.name = self.name.replace(" ", "_").lower() From 39b814e67e88520b029d5e556b86acee88895673 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 4 Feb 2021 16:41:06 +0100 Subject: [PATCH 06/12] Textual change. --- flexmeasures/data/services/forecasting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/data/services/forecasting.py b/flexmeasures/data/services/forecasting.py index a79d4f542..6042a7252 100644 --- a/flexmeasures/data/services/forecasting.py +++ b/flexmeasures/data/services/forecasting.py @@ -72,7 +72,7 @@ def create_forecasting_jobs( 2) forecast each quarter-hour from 9pm to 11pm, i.e. the 6h forecast 3) forecast each quarter-hour from 3pm to 5pm the next day, i.e. the 1d forecast - If not given, relevant horizons are deduced from the resolution of the posted data. + If not given, relevant horizons are derived from the resolution of the posted data. The job needs a model configurator, for which you can supply a model search term. If ommited, the current default model configuration will be used. From e449473ee794151084088289519f010250e2dbdc Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Thu, 4 Feb 2021 18:40:07 +0100 Subject: [PATCH 07/12] Use shorthands for knowledge horizon functions. --- flexmeasures/data/models/markets.py | 7 +- ...or_with_asset_market_and_weather_sensor.py | 83 +++++++++++++++---- 2 files changed, 71 insertions(+), 19 deletions(-) diff --git a/flexmeasures/data/models/markets.py b/flexmeasures/data/models/markets.py index e35c2e3a6..2094ef3fc 100644 --- a/flexmeasures/data/models/markets.py +++ b/flexmeasures/data/models/markets.py @@ -1,6 +1,7 @@ from typing import Dict import timely_beliefs as tb +from timely_beliefs.sensors.func_store import knowledge_horizons from sqlalchemy.orm import Query from flexmeasures.data.config import db @@ -52,9 +53,11 @@ class Market(db.Model, tb.SensorDBMixin): def __init__(self, **kwargs): # Set default knowledge horizon function for an economic sensor if "knowledge_horizon_fnc" not in kwargs: - kwargs["knowledge_horizon_fnc"] = "determine_ex_ante_knowledge_horizon" + kwargs["knowledge_horizon_fnc"] = "EX_ANTE" if "knowledge_horizon_par" not in kwargs: - kwargs["knowledge_horizon_par"] = {"ex_post_horizon": "PT0H"} + kwargs["knowledge_horizon_par"] = { + knowledge_horizons.shorthands["EX_ANTE"].__code__.co_varnames[1]: "PT0H" + } super(Market, self).__init__(**kwargs) self.name = self.name.replace(" ", "_").lower() if "display_name" not in kwargs: diff --git a/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py b/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py index 63a8bbe05..3cec1b0cb 100644 --- a/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py +++ b/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py @@ -6,7 +6,9 @@ """ from alembic import op +import json import sqlalchemy as sa +from timely_beliefs.sensors.func_store import knowledge_horizons # revision identifiers, used by Alembic. @@ -15,21 +17,45 @@ branch_labels = None depends_on = None +# set default parameters for the two default knowledge horizon functions +ex_ante_default_par = { + knowledge_horizons.shorthands["EX_ANTE"].__code__.co_varnames[1]: "PT0H" +} +ex_post_default_par = { + knowledge_horizons.shorthands["EX_POST"].__code__.co_varnames[1]: "PT0H" +} + def upgrade(): # Mix in timely_beliefs.Sensor with flexmeasures.Asset op.add_column( - "asset", sa.Column("knowledge_horizon_fnc", sa.String(length=80), nullable=True) + "asset", + sa.Column( + "knowledge_horizon_fnc", + sa.String(length=80), + nullable=True, + default="EX_POST", + ), ) op.execute( - "update asset set knowledge_horizon_fnc = 'determine_ex_post_knowledge_horizon';" + "update asset set knowledge_horizon_fnc = 'EX_POST';" ) # default assumption that power measurements are known right after the fact op.alter_column("asset", "knowledge_horizon_fnc", nullable=False) - op.add_column("asset", sa.Column("knowledge_horizon_par", sa.JSON(), nullable=True)) + op.add_column( + "asset", + sa.Column( + "knowledge_horizon_par", + sa.JSON(), + nullable=True, + default={ + knowledge_horizons.shorthands["EX_POST"].__code__.co_varnames[1]: "PT0H" + }, + ), + ) op.execute( - """update asset set knowledge_horizon_par = '{"ex_post_horizon": "PT0H"}';""" + f"""update asset set knowledge_horizon_par = '{json.dumps(ex_post_default_par)}';""" ) op.alter_column("asset", "knowledge_horizon_par", nullable=False) @@ -40,27 +66,37 @@ def upgrade(): # Mix in timely_beliefs.Sensor with flexmeasures.Market op.add_column( "market", - sa.Column("knowledge_horizon_fnc", sa.String(length=80), nullable=True), + sa.Column( + "knowledge_horizon_fnc", + sa.String(length=80), + nullable=True, + default="EX_ANTE", + ), ) op.execute( - "update market set knowledge_horizon_fnc = 'determine_ex_ante_knowledge_horizon';" + "update market set knowledge_horizon_fnc = 'EX_ANTE';" ) # default assumption that prices are known before a transaction op.execute( - "update market set knowledge_horizon_fnc = 'determine_ex_ante_knowledge_horizon_for_x_days_ago_at_y_oclock' where name='epex_da';" + "update market set knowledge_horizon_fnc = 'EX_ANTE_X_DAYS_AT_Y_OCLOCK' where name in ('epex_da', 'kpx_da');" ) op.execute( - "update market set knowledge_horizon_fnc = 'determine_ex_ante_knowledge_horizon_for_x_days_ago_at_y_oclock' where name='kpx_da';" - ) - op.execute( - "update market set knowledge_horizon_fnc = 'determine_knowledge_horizon_for_fixed_knowledge_time' where name in ('kepco_cs_fast', 'kepco_cs_slow', 'kepco_cs_smart');" + "update market set knowledge_horizon_fnc = 'AT_DATE' where name in ('kepco_cs_fast', 'kepco_cs_slow', 'kepco_cs_smart');" ) op.alter_column("market", "knowledge_horizon_fnc", nullable=False) op.add_column( - "market", sa.Column("knowledge_horizon_par", sa.JSON(), nullable=True) + "market", + sa.Column( + "knowledge_horizon_par", + sa.JSON(), + nullable=True, + default={ + knowledge_horizons.shorthands["EX_ANTE"].__code__.co_varnames[1]: "PT0H" + }, + ), ) op.execute( - """update market set knowledge_horizon_par = '{"ex_ante_horizon": "PT0H"}';""" + f"""update market set knowledge_horizon_par = '{json.dumps(ex_ante_default_par)}';""" ) op.execute( """update market set knowledge_horizon_par = '{"x": 1, "y": 12, "z": "Europe/Paris"}' where name='epex_da';""" @@ -85,18 +121,31 @@ def upgrade(): # Mix in timely_beliefs.Sensor with flexmeasures.WeatherSensor op.add_column( "weather_sensor", - sa.Column("knowledge_horizon_fnc", sa.String(length=80), nullable=True), + sa.Column( + "knowledge_horizon_fnc", + sa.String(length=80), + nullable=True, + default="EX_POST", + ), ) op.execute( - "update weather_sensor set knowledge_horizon_fnc = 'determine_ex_post_knowledge_horizon';" + "update weather_sensor set knowledge_horizon_fnc = 'EX_POST';" ) # default assumption that weather measurements are known right after the fact op.alter_column("weather_sensor", "knowledge_horizon_fnc", nullable=False) op.add_column( - "weather_sensor", sa.Column("knowledge_horizon_par", sa.JSON(), nullable=True) + "weather_sensor", + sa.Column( + "knowledge_horizon_par", + sa.JSON(), + nullable=True, + default={ + knowledge_horizons.shorthands["EX_POST"].__code__.co_varnames[1]: "PT0H" + }, + ), ) op.execute( - """update weather_sensor set knowledge_horizon_par = '{"ex_post_horizon": "PT0H"}';""" + f"""update weather_sensor set knowledge_horizon_par = '{json.dumps(ex_post_default_par)}';""" ) op.alter_column("weather_sensor", "knowledge_horizon_par", nullable=False) From acdd09a2dd14b062e8d2e61e6d5fd361bdba260c Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Sat, 6 Feb 2021 16:01:40 +0100 Subject: [PATCH 08/12] Switch to shorthand functions and dynamically link them. --- flexmeasures/data/models/markets.py | 4 +-- ...or_with_asset_market_and_weather_sensor.py | 36 +++++++------------ 2 files changed, 15 insertions(+), 25 deletions(-) diff --git a/flexmeasures/data/models/markets.py b/flexmeasures/data/models/markets.py index 2094ef3fc..41afa907e 100644 --- a/flexmeasures/data/models/markets.py +++ b/flexmeasures/data/models/markets.py @@ -53,10 +53,10 @@ class Market(db.Model, tb.SensorDBMixin): def __init__(self, **kwargs): # Set default knowledge horizon function for an economic sensor if "knowledge_horizon_fnc" not in kwargs: - kwargs["knowledge_horizon_fnc"] = "EX_ANTE" + kwargs["knowledge_horizon_fnc"] = knowledge_horizons.ex_ante.__name__ if "knowledge_horizon_par" not in kwargs: kwargs["knowledge_horizon_par"] = { - knowledge_horizons.shorthands["EX_ANTE"].__code__.co_varnames[1]: "PT0H" + knowledge_horizons.ex_ante.__code__.co_varnames[1]: "PT0H" } super(Market, self).__init__(**kwargs) self.name = self.name.replace(" ", "_").lower() diff --git a/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py b/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py index 3cec1b0cb..be0278eb0 100644 --- a/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py +++ b/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py @@ -18,12 +18,8 @@ depends_on = None # set default parameters for the two default knowledge horizon functions -ex_ante_default_par = { - knowledge_horizons.shorthands["EX_ANTE"].__code__.co_varnames[1]: "PT0H" -} -ex_post_default_par = { - knowledge_horizons.shorthands["EX_POST"].__code__.co_varnames[1]: "PT0H" -} +ex_ante_default_par = {knowledge_horizons.ex_ante.__code__.co_varnames[1]: "PT0H"} +ex_post_default_par = {knowledge_horizons.ex_post.__code__.co_varnames[1]: "PT0H"} def upgrade(): @@ -35,11 +31,11 @@ def upgrade(): "knowledge_horizon_fnc", sa.String(length=80), nullable=True, - default="EX_POST", + default=knowledge_horizons.ex_post.__name__, ), ) op.execute( - "update asset set knowledge_horizon_fnc = 'EX_POST';" + f"update asset set knowledge_horizon_fnc = '{knowledge_horizons.ex_post.__name__}';" ) # default assumption that power measurements are known right after the fact op.alter_column("asset", "knowledge_horizon_fnc", nullable=False) @@ -49,9 +45,7 @@ def upgrade(): "knowledge_horizon_par", sa.JSON(), nullable=True, - default={ - knowledge_horizons.shorthands["EX_POST"].__code__.co_varnames[1]: "PT0H" - }, + default={knowledge_horizons.ex_post.__code__.co_varnames[1]: "PT0H"}, ), ) op.execute( @@ -70,17 +64,17 @@ def upgrade(): "knowledge_horizon_fnc", sa.String(length=80), nullable=True, - default="EX_ANTE", + default=knowledge_horizons.ex_ante.__name__, ), ) op.execute( - "update market set knowledge_horizon_fnc = 'EX_ANTE';" + f"update market set knowledge_horizon_fnc = '{knowledge_horizons.ex_ante.__name__}';" ) # default assumption that prices are known before a transaction op.execute( - "update market set knowledge_horizon_fnc = 'EX_ANTE_X_DAYS_AT_Y_OCLOCK' where name in ('epex_da', 'kpx_da');" + f"update market set knowledge_horizon_fnc = '{knowledge_horizons.x_days_ago_at_y_oclock.__name__}' where name in ('epex_da', 'kpx_da');" ) op.execute( - "update market set knowledge_horizon_fnc = 'AT_DATE' where name in ('kepco_cs_fast', 'kepco_cs_slow', 'kepco_cs_smart');" + f"update market set knowledge_horizon_fnc = '{knowledge_horizons.at_date.__name__}' where name in ('kepco_cs_fast', 'kepco_cs_slow', 'kepco_cs_smart');" ) op.alter_column("market", "knowledge_horizon_fnc", nullable=False) @@ -90,9 +84,7 @@ def upgrade(): "knowledge_horizon_par", sa.JSON(), nullable=True, - default={ - knowledge_horizons.shorthands["EX_ANTE"].__code__.co_varnames[1]: "PT0H" - }, + default={knowledge_horizons.ex_ante.__code__.co_varnames[1]: "PT0H"}, ), ) op.execute( @@ -125,11 +117,11 @@ def upgrade(): "knowledge_horizon_fnc", sa.String(length=80), nullable=True, - default="EX_POST", + default=knowledge_horizons.ex_post.__name__, ), ) op.execute( - "update weather_sensor set knowledge_horizon_fnc = 'EX_POST';" + f"update weather_sensor set knowledge_horizon_fnc = '{knowledge_horizons.ex_post.__name__}';" ) # default assumption that weather measurements are known right after the fact op.alter_column("weather_sensor", "knowledge_horizon_fnc", nullable=False) @@ -139,9 +131,7 @@ def upgrade(): "knowledge_horizon_par", sa.JSON(), nullable=True, - default={ - knowledge_horizons.shorthands["EX_POST"].__code__.co_varnames[1]: "PT0H" - }, + default={knowledge_horizons.ex_post.__code__.co_varnames[1]: "PT0H"}, ), ) op.execute( From 5e429cfe5886b2f154efeb7507ac4bbdb9903003 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 8 Feb 2021 11:16:33 +0100 Subject: [PATCH 09/12] Upgrade dependency. --- requirements/app.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements/app.in b/requirements/app.in index 01a6c03fa..c2c24d39a 100644 --- a/requirements/app.in +++ b/requirements/app.in @@ -27,7 +27,7 @@ pyomo>=5.6 forecastiopy pysolar timetomodel>=0.6.8 -timely-beliefs>=1.0.0 +timely-beliefs>=1.2.0 python-dotenv Flask-SSLify Flask_JSON From ca6a607119d3b5b7cc62c46735ec1d58dbb52d21 Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 8 Feb 2021 17:58:19 +0100 Subject: [PATCH 10/12] Typo. --- flexmeasures/data/services/forecasting.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flexmeasures/data/services/forecasting.py b/flexmeasures/data/services/forecasting.py index 6042a7252..12035d8b2 100644 --- a/flexmeasures/data/services/forecasting.py +++ b/flexmeasures/data/services/forecasting.py @@ -74,7 +74,7 @@ def create_forecasting_jobs( If not given, relevant horizons are derived from the resolution of the posted data. - The job needs a model configurator, for which you can supply a model search term. If ommited, the + The job needs a model configurator, for which you can supply a model search term. If omitted, the current default model configuration will be used. It's possible to customize model parameters, but this feature is (currently) meant to only From 62d7acedcb37e7bcb8642e97a9d3f66ff4d22c5b Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 8 Feb 2021 19:44:18 +0100 Subject: [PATCH 11/12] Fix ex_ante parameter. --- ...ely_beliefs_sensor_with_asset_market_and_weather_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py b/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py index be0278eb0..cad8037ee 100644 --- a/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py +++ b/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py @@ -18,7 +18,7 @@ depends_on = None # set default parameters for the two default knowledge horizon functions -ex_ante_default_par = {knowledge_horizons.ex_ante.__code__.co_varnames[1]: "PT0H"} +ex_ante_default_par = {knowledge_horizons.ex_ante.__code__.co_varnames[0]: "PT0H"} ex_post_default_par = {knowledge_horizons.ex_post.__code__.co_varnames[1]: "PT0H"} @@ -84,7 +84,7 @@ def upgrade(): "knowledge_horizon_par", sa.JSON(), nullable=True, - default={knowledge_horizons.ex_ante.__code__.co_varnames[1]: "PT0H"}, + default=ex_ante_default_par, ), ) op.execute( From cb78ad3718faffa33bd4b8f95e2c1ea66d1364fa Mon Sep 17 00:00:00 2001 From: "F.N. Claessen" Date: Mon, 8 Feb 2021 19:45:21 +0100 Subject: [PATCH 12/12] Delay the introduction of knowledge horizon function for day-ahead markets. --- ...or_with_asset_market_and_weather_sensor.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py b/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py index cad8037ee..e99ac01c2 100644 --- a/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py +++ b/migrations/versions/22ce09690d23_mix_in_timely_beliefs_sensor_with_asset_market_and_weather_sensor.py @@ -70,9 +70,6 @@ def upgrade(): op.execute( f"update market set knowledge_horizon_fnc = '{knowledge_horizons.ex_ante.__name__}';" ) # default assumption that prices are known before a transaction - op.execute( - f"update market set knowledge_horizon_fnc = '{knowledge_horizons.x_days_ago_at_y_oclock.__name__}' where name in ('epex_da', 'kpx_da');" - ) op.execute( f"update market set knowledge_horizon_fnc = '{knowledge_horizons.at_date.__name__}' where name in ('kepco_cs_fast', 'kepco_cs_slow', 'kepco_cs_smart');" ) @@ -90,12 +87,6 @@ def upgrade(): op.execute( f"""update market set knowledge_horizon_par = '{json.dumps(ex_ante_default_par)}';""" ) - op.execute( - """update market set knowledge_horizon_par = '{"x": 1, "y": 12, "z": "Europe/Paris"}' where name='epex_da';""" - ) # gate closure at 12:00 on the preceding day, with expected price publication at 12.42 and 12.55 (from EPEX Spot Day-Ahead Multi-Regional Coupling, https://www.epexspot.com/en/downloads#rules-fees-processes ) - op.execute( - """update market set knowledge_horizon_par = '{"x": 1, "y": 10, "z": "Asia/Seoul"}' where name='kpx_da';""" - ) # gate closure at 10.00 on the preceding day, with expected price publication at 15.00 (from KPX Power Market Operation, https://www.slideshare.net/sjchung0/power-market-operation ) op.execute( """update market set knowledge_horizon_par = '{"knowledge_time": "2014-12-31 00:00:00+00:00"}' where name in ('kepco_cs_fast', 'kepco_cs_slow', 'kepco_cs_smart');""" ) # tariff publication date (unofficial) @@ -145,6 +136,18 @@ def upgrade(): op.execute("update weather_sensor set timezone = 'Asia/Seoul';") op.alter_column("weather_sensor", "timezone", nullable=False) + # todo: execute after adding relevant tests and updating our strategy to create forecasting jobs when new information arrives + # op.execute( + # f"update market set knowledge_horizon_fnc = '{knowledge_horizons.x_days_ago_at_y_oclock.__name__}' where name in ('epex_da', 'kpx_da');" + # ) + # op.execute( + # """update market set knowledge_horizon_par = '{"x": 1, "y": 12, "z": "Europe/Paris"}' where name='epex_da';""" + # ) # gate closure at 12:00 on the preceding day, with expected price publication at 12.42 and 12.55 (from EPEX Spot Day-Ahead Multi-Regional Coupling, https://www.epexspot.com/en/downloads#rules-fees-processes ) + # op.execute( + # """update market set knowledge_horizon_par = '{"x": 1, "y": 10, "z": "Asia/Seoul"}' where name='kpx_da';""" + # ) # gate closure at 10.00 on the preceding day, with expected price publication at 15.00 (from KPX Power Market Operation, https://www.slideshare.net/sjchung0/power-market-operation ) + # todo: add statement to update the horizon for prices on these markets, in accordance with their new knowledge horizon function + def downgrade(): # Drop mixed in columns