process.py 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
  1. from __future__ import annotations
  2. from datetime import datetime
  3. import pytz
  4. import pandas as pd
  5. from marshmallow import (
  6. Schema,
  7. post_load,
  8. fields,
  9. pre_load,
  10. )
  11. from flexmeasures.data.models.time_series import Sensor
  12. from flexmeasures.data.schemas.times import (
  13. DurationField,
  14. TimeIntervalSchema,
  15. )
  16. from enum import Enum
  17. class ProcessType(Enum):
  18. INFLEXIBLE = "INFLEXIBLE"
  19. BREAKABLE = "BREAKABLE"
  20. SHIFTABLE = "SHIFTABLE"
  21. class OptimizationDirection(Enum):
  22. MAX = "MAX"
  23. MIN = "MIN"
  24. class ProcessSchedulerFlexModelSchema(Schema):
  25. # time that the process last.
  26. duration = DurationField(required=True)
  27. # nominal power of the process.
  28. power = fields.Float(required=True)
  29. # policy to schedule a process: INFLEXIBLE, SHIFTABLE, BREAKABLE
  30. process_type = fields.Enum(
  31. ProcessType, load_default=ProcessType.INFLEXIBLE, data_key="process-type"
  32. )
  33. # time_restrictions will be turned into a Series with Boolean values (where True means restricted for scheduling).
  34. time_restrictions = fields.List(
  35. fields.Nested(TimeIntervalSchema()),
  36. data_key="time-restrictions",
  37. load_default=[],
  38. )
  39. # objective of the scheduler, to maximize or minimize.
  40. optimization_direction = fields.Enum(
  41. OptimizationDirection,
  42. load_default=OptimizationDirection.MIN,
  43. data_key="optimization-sense",
  44. )
  45. def __init__(self, sensor: Sensor, start: datetime, end: datetime, *args, **kwargs):
  46. """Pass start and end to convert time_restrictions into a time series and sensor
  47. as a fallback mechanism for the process_type
  48. """
  49. self.start = start.astimezone(pytz.utc)
  50. self.end = end.astimezone(pytz.utc)
  51. self.sensor = sensor
  52. super().__init__(*args, **kwargs)
  53. def get_mask_from_events(self, events: list[dict[str, str]] | None) -> pd.Series:
  54. """Convert events to a mask of the time periods that are valid
  55. :param events: list of events defined as dictionaries with a start and duration
  56. :return: mask of the allowed time periods
  57. """
  58. series = pd.Series(
  59. index=pd.date_range(
  60. self.start,
  61. self.end,
  62. freq=self.sensor.event_resolution,
  63. inclusive="left",
  64. name="event_start",
  65. tz=self.start.tzinfo,
  66. ),
  67. data=False,
  68. )
  69. if events is None:
  70. return series
  71. for event in events:
  72. start = event["start"]
  73. duration = event["duration"]
  74. end = start + duration
  75. series[(series.index >= start) & (series.index < end)] = True
  76. return series
  77. @post_load
  78. def post_load_time_restrictions(self, data: dict, **kwargs) -> dict:
  79. """Convert events (list of [start, duration] pairs) into a mask (pandas Series)"""
  80. data["time_restrictions"] = self.get_mask_from_events(data["time_restrictions"])
  81. return data
  82. @pre_load
  83. def pre_load_process_type(self, data: dict, **kwargs) -> dict:
  84. """Fallback mechanism for the process_type variable. If not found in data,
  85. it tries to find it in among the sensor or asset attributes and, if it's not found
  86. there either, it defaults to "INFLEXIBLE".
  87. """
  88. if "process-type" not in data or data["process-type"] is None:
  89. process_type = self.sensor.get_attribute("process-type")
  90. if process_type is None:
  91. process_type = self.sensor.generic_asset.get_attribute("process-type")
  92. if process_type is None:
  93. process_type = "INFLEXIBLE"
  94. data["process-type"] = process_type
  95. return data