profit.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. from __future__ import annotations
  2. from datetime import datetime, timedelta
  3. from typing import Any
  4. from flexmeasures.data.models.reporting import Reporter
  5. from flexmeasures.data.schemas.reporting.profit import (
  6. ProfitOrLossReporterConfigSchema,
  7. ProfitOrLossReporterParametersSchema,
  8. )
  9. from flexmeasures.data.models.time_series import Sensor
  10. from flexmeasures.utils.time_utils import server_now
  11. from flexmeasures.data.queries.utils import simplify_index
  12. from flexmeasures.utils.unit_utils import ur, determine_stock_unit, is_currency_unit
  13. class ProfitOrLossReporter(Reporter):
  14. """Compute the profit or loss due to energy/power flow.
  15. Given power/energy and price sensors, this reporter computes the profit (revenue - cost)
  16. or losses (cost - revenue) of a power/energy flow under a certain tariff.
  17. Sign convention (by default)
  18. ----------------
  19. Power flows:
  20. (+) production
  21. (-) consumption
  22. Profit:
  23. (+) gains
  24. (-) losses
  25. This sign convention can be adapted to your needs:
  26. - The power/energy convention can be inverted by setting the sensor attribute `consumption_is_positive` to True.
  27. - The output (gains/losses) sign can be inverted by setting the reporter config attribute `loss_is_positive` to False.
  28. """
  29. __version__ = "1"
  30. __author__ = "Seita"
  31. _config_schema = ProfitOrLossReporterConfigSchema()
  32. _parameters_schema = ProfitOrLossReporterParametersSchema()
  33. weights: dict
  34. method: str
  35. def _compute_report(
  36. self,
  37. start: datetime,
  38. end: datetime,
  39. input: list[dict[str, Any]],
  40. output: list[dict[str, Any]],
  41. belief_time: datetime | None = None,
  42. ) -> list[dict[str, Any]]:
  43. """
  44. :param start: start time of the report
  45. :param end: end time of the report
  46. :param input: description of the power/energy sensor, e.g. `input=[{"sensor": 42}]`
  47. :param output: description of the output sensors where to save the report to.
  48. Specify multiple output sensors with different resolutions to save
  49. the results in multiple time frames (e.g. hourly, daily),
  50. e.g. `output = [{"sensor" : 43}, {"sensor" : 44]}]`
  51. :param belief_time: datetime used to indicate we are interested in the state of knowledge at that time.
  52. It is used to filter input, and to assign a recording time to output.
  53. """
  54. production_price_sensor: Sensor = self._config.get("production_price_sensor")
  55. consumption_price_sensor: Sensor = self._config.get("consumption_price_sensor")
  56. loss_is_positive: bool = self._config.get("loss_is_positive", False)
  57. input_sensor: Sensor = input[0]["sensor"] # power or energy sensor
  58. input_source: Sensor = input[0].get("sources", None)
  59. timezone = input_sensor.timezone
  60. if belief_time is None:
  61. belief_time = server_now()
  62. # get prices
  63. production_price = simplify_index(
  64. production_price_sensor.search_beliefs(
  65. event_starts_after=start,
  66. event_ends_before=end,
  67. beliefs_before=belief_time,
  68. resolution=input_sensor.event_resolution,
  69. most_recent_beliefs_only=True,
  70. one_deterministic_belief_per_event=True,
  71. )
  72. )
  73. production_price = production_price.tz_convert(timezone)
  74. consumption_price = simplify_index(
  75. consumption_price_sensor.search_beliefs(
  76. event_starts_after=start,
  77. event_ends_before=end,
  78. beliefs_before=belief_time,
  79. resolution=input_sensor.event_resolution,
  80. most_recent_beliefs_only=True,
  81. one_deterministic_belief_per_event=True,
  82. )
  83. )
  84. consumption_price = consumption_price.tz_convert(timezone)
  85. # get power/energy time series
  86. power_energy_data = simplify_index(
  87. input_sensor.search_beliefs(
  88. event_starts_after=start,
  89. event_ends_before=end,
  90. beliefs_before=belief_time,
  91. source=input_source,
  92. most_recent_beliefs_only=True,
  93. one_deterministic_belief_per_event=True,
  94. )
  95. )
  96. unit_consumption_price = ur.Unit(consumption_price_sensor.unit)
  97. unit_production_price = ur.Unit(production_price_sensor.unit)
  98. # compute energy flow from power flow
  99. if input_sensor.measures_power:
  100. power_energy_data *= input_sensor.event_resolution / timedelta(hours=1)
  101. power_energy_unit = ur.Unit(
  102. determine_stock_unit(input_sensor.unit, time_unit="h")
  103. )
  104. else:
  105. power_energy_unit = ur.Unit(input_sensor.unit)
  106. # check that the unit of the results are a currency
  107. cost_unit = unit_consumption_price * power_energy_unit
  108. revenue_unit = unit_production_price * power_energy_unit
  109. assert is_currency_unit(cost_unit)
  110. assert is_currency_unit(revenue_unit)
  111. # transform time series as to get positive values for production and negative for consumption
  112. if input_sensor.get_attribute("consumption_is_positive", False):
  113. power_energy_data *= -1.0
  114. # compute profit
  115. # this step assumes that positive flows represent production and negative flows consumption
  116. result = (
  117. power_energy_data.clip(lower=0) * production_price
  118. + power_energy_data.clip(upper=0) * consumption_price
  119. )
  120. # transform a losses in negative to positive
  121. if loss_is_positive:
  122. result *= -1.0
  123. results = []
  124. output_name = "profit"
  125. if loss_is_positive:
  126. output_name = "loss"
  127. for output_description in output:
  128. output_sensor = output_description["sensor"]
  129. _result = result.copy()
  130. # resample result to the event_resolution of the output sensor
  131. _result = _result.resample(output_sensor.event_resolution).sum()
  132. # convert BeliefsSeries into a BeliefsDataFrame
  133. _result["belief_time"] = belief_time
  134. _result["cumulative_probability"] = 0.5
  135. _result["source"] = self.data_source
  136. _result.sensor = output_sensor
  137. _result.event_resolution = output_sensor.event_resolution
  138. # check output sensor unit coincides with the units of the result
  139. assert str(cost_unit) == output_sensor.unit
  140. assert str(revenue_unit) == output_sensor.unit
  141. _result = _result.set_index(
  142. ["belief_time", "source", "cumulative_probability"], append=True
  143. )
  144. results.append(
  145. {
  146. "name": output_name,
  147. "column": "event_value",
  148. "sensor": output_description["sensor"],
  149. "data": _result,
  150. }
  151. )
  152. return results