-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
435 additions
and
10 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
from __future__ import annotations | ||
|
||
from datetime import datetime, timedelta | ||
from typing import Any, List, Dict | ||
|
||
|
||
from flexmeasures.data.models.reporting import Reporter | ||
from flexmeasures.data.schemas.reporting.cost import ( | ||
CostReporterConfigSchema, | ||
CostReporterParametersSchema, | ||
) | ||
from flexmeasures.data.models.time_series import Sensor | ||
from flexmeasures.utils.time_utils import server_now | ||
from flexmeasures.data.queries.utils import simplify_index | ||
|
||
|
||
class CostReporter(Reporter): | ||
"""Compute the cashflow due to energy/power flow. | ||
Given power/energy and price sensors, this reporter computes the cost/profit | ||
of a power/energy flow under a certain tariff. | ||
Sign convention | ||
---------------- | ||
Power flows: | ||
(+) production | ||
(-) consumption | ||
Cash flows: | ||
(+) to charge | ||
(-) to pay | ||
""" | ||
|
||
__version__ = "1" | ||
__author__ = "Seita" | ||
|
||
_config_schema = CostReporterConfigSchema() | ||
_parameters_schema = CostReporterParametersSchema() | ||
|
||
weights: dict | ||
method: str | ||
|
||
def _compute_report( | ||
self, | ||
start: datetime, | ||
end: datetime, | ||
input: List[Dict[str, Any]], | ||
output: List[Dict[str, Any]], | ||
resolution: timedelta | None = None, | ||
belief_time: datetime | None = None, | ||
) -> List[Dict[str, Any]]: | ||
""" | ||
:param start: start time of the report | ||
:param end: end time of the report | ||
:param input: power/energy sensor to consider | ||
:param output: sensor where to save the report to. Specify multiple | ||
output sensors with different resolutions to save | ||
the results in multiple time frames (e.g. hourly, daily). | ||
:param resolution: _description_, defaults to None | ||
:param belief_time: time where the information is available. | ||
""" | ||
|
||
production_price_sensor: Sensor = self._config.get("production_price_sensor") | ||
consumption_price_sensor: Sensor = self._config.get("consumption_price_sensor") | ||
|
||
input_sensor: Sensor = input[0]["sensor"] # power or energy sensor | ||
timezone = input_sensor.timezone | ||
|
||
if belief_time is None: | ||
belief_time = server_now() | ||
|
||
# get prices | ||
production_price = simplify_index( | ||
production_price_sensor.search_beliefs( | ||
event_starts_after=start, | ||
event_ends_before=end, | ||
beliefs_before=belief_time, | ||
resolution=input_sensor.event_resolution, | ||
) | ||
) | ||
production_price = production_price.tz_convert(timezone) | ||
consumption_price = simplify_index( | ||
consumption_price_sensor.search_beliefs( | ||
event_starts_after=start, | ||
event_ends_before=end, | ||
beliefs_before=belief_time, | ||
resolution=input_sensor.event_resolution, | ||
) | ||
) | ||
consumption_price = consumption_price.tz_convert(timezone) | ||
|
||
# get power/energy time series | ||
power_energy_data = simplify_index( | ||
input_sensor.search_beliefs( | ||
event_starts_after=start, | ||
event_ends_before=end, | ||
resolution=resolution, | ||
beliefs_before=belief_time, | ||
) | ||
) | ||
|
||
# compute energy flow from power flow | ||
if input_sensor.measures_power: | ||
power_energy_data *= input_sensor.event_resolution / timedelta(hours=1) | ||
|
||
# compute cashflow | ||
# this step assumes that positive flows represent production and negative flows consumption | ||
result = ( | ||
power_energy_data.clip(lower=0) * production_price | ||
+ power_energy_data.clip(upper=0) * consumption_price | ||
) | ||
|
||
results = [] | ||
|
||
for output_description in output: | ||
output_sensor = output_description["sensor"] | ||
_result = result.copy() | ||
|
||
# resample result to the event_resolution of the output sensor | ||
_result = _result.resample(output_sensor.event_resolution).sum() | ||
|
||
# convert BeliefsSeries into a BeliefsDataFrame | ||
_result["belief_time"] = belief_time | ||
_result["cumulative_probability"] = 0.5 | ||
_result["source"] = self.data_source | ||
_result.event_resolution = output_sensor.event_resolution | ||
|
||
_result = _result.set_index( | ||
["belief_time", "source", "cumulative_probability"], append=True | ||
) | ||
|
||
results.append( | ||
{ | ||
"name": "cashflow", | ||
"column": "event_value", | ||
"sensor": output_description["sensor"], | ||
"data": _result, | ||
} | ||
) | ||
|
||
return results |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
72 changes: 72 additions & 0 deletions
72
flexmeasures/data/models/reporting/tests/test_cost_reporter.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import pytest | ||
|
||
from datetime import timedelta | ||
from flexmeasures.data.models.reporting.cost import CostReporter | ||
from flexmeasures.data.models.time_series import Sensor | ||
from datetime import datetime | ||
from pytz import timezone | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"use_power_sensor", | ||
[True, False], | ||
) | ||
def test_cost_reporter(app, db, cost_report, use_power_sensor): | ||
( | ||
cashflow_sensor_hourly, | ||
cashflow_sensor_daily, | ||
power_sensor, | ||
energy_sensor, | ||
) = cost_report | ||
output_sensor = energy_sensor | ||
|
||
if use_power_sensor: | ||
output_sensor = power_sensor | ||
|
||
epex_da = Sensor.query.filter(Sensor.name == "epex_da").one_or_none() | ||
epex_da_production = Sensor.query.filter( | ||
Sensor.name == "epex_da_production" | ||
).one_or_none() | ||
|
||
cost_reporter = CostReporter( | ||
consumption_price_sensor=epex_da, production_price_sensor=epex_da_production | ||
) | ||
|
||
tz = timezone("Europe/Amsterdam") | ||
|
||
result = cost_reporter.compute( | ||
start=tz.localize(datetime(2015, 1, 3)), | ||
end=tz.localize(datetime(2015, 1, 4)), | ||
input=[dict(sensor=output_sensor)], | ||
output=[ | ||
dict(sensor=cashflow_sensor_hourly), | ||
dict(sensor=cashflow_sensor_daily), | ||
], | ||
) | ||
|
||
result_hourly = result[0]["data"] | ||
result_daily = result[1]["data"] | ||
|
||
assert result_hourly.event_resolution == timedelta(hours=1) | ||
|
||
# period of negative prices | ||
|
||
# in the period from 00:00 to 03:00 | ||
# the device produces 100kWh hourly at a -50 EUR/MWh price | ||
assert (result_hourly[0:4] == -5).event_value.all() | ||
|
||
# in the period from 04:00 to 08:00 | ||
# the device consumes 100kWh hourly at a 10 EUR/MWh price | ||
assert (result_hourly[4:8] == 1).event_value.all() | ||
|
||
# period of positive prices | ||
|
||
# in the period from 08:00 to 12:00 | ||
# the device produces 100kWh hourly at a 60 EUR/MWh price | ||
assert (result_hourly[8:12] == 6).event_value.all() | ||
|
||
# in the period from 12:00 to 16:00 | ||
# the device produces 100kWh hourly at a 100 EUR/MWh price | ||
assert (result_hourly[12:16] == -10).event_value.all() | ||
|
||
assert result_daily.event_value.iloc[0] == result_hourly.sum().iloc[0] |
Oops, something went wrong.