consumption.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import pandas as pd
  2. import numpy as np
  3. import cvxpy as cp
  4. from statsmodels.formula.api import ols
  5. from typing import Union
  6. class ConsumptionModel:
  7. def __init__(self):
  8. pass
  9. def get(self):
  10. pass
  11. class ThermoModel(ConsumptionModel):
  12. def __init__(self):
  13. pass
  14. def get(temperatures: np.ndarray):
  15. pass
  16. class FittedConsumptionModel(ConsumptionModel):
  17. def __init__(self, yearly_total: float):
  18. """Consumption Model fitted against observations of French electricity consumption (2012-2021).
  19. The model captures seasonal, weekly and diurnal variations. It has only 1 parameter, the total annual consumption.
  20. :param yearly_total: Total annual consumption in GWh.
  21. :type yearly_total: float
  22. """
  23. self.yearly_total = yearly_total
  24. self.fit()
  25. def get(self, times: Union[pd.Series, np.ndarray]) -> np.ndarray:
  26. """Retrieve the consumption for each timestamp from the input array.
  27. :param times: 1D array containing the input timestamps
  28. :type times: Union[pd.Series, np.ndarray]
  29. :return: 1D array of floats (consumption in GW) with the same length as the input.
  30. :rtype: np.ndarray
  31. """
  32. # compute the load for the desired timestamps
  33. times = pd.DataFrame({"time": times}).set_index("time")
  34. times.index = pd.to_datetime(times.index, utc=True)
  35. times["h"] = (
  36. (times.index - self.time_reference).total_seconds() / 3600
  37. ).astype(int)
  38. times["Y"] = 1
  39. for i, f in enumerate(self.frequencies):
  40. times[f"c_{i+1}"] = (times["h"] * f * 2 * np.pi).apply(np.cos)
  41. times[f"s_{i+1}"] = (times["h"] * f * 2 * np.pi).apply(np.sin)
  42. curve = self.fit_results.predict(times).values
  43. return curve
  44. def fit(self):
  45. consumption = pd.read_csv("data/consommation-quotidienne-brute.csv", sep=";")
  46. consumption["time"] = pd.to_datetime(
  47. consumption["Date - Heure"].str.replace(":", ""),
  48. format="%Y-%m-%dT%H%M%S%z",
  49. utc=True,
  50. )
  51. consumption.set_index("time", inplace=True)
  52. hourly = consumption.resample("1H").mean()
  53. self.time_reference = hourly.index.min()
  54. hourly["h"] = (
  55. (hourly.index - self.time_reference).total_seconds() / 3600
  56. ).astype(int)
  57. hourly["Y"] = hourly.index.year - hourly.index.year.min()
  58. # generate fourier components
  59. self.frequencies = (
  60. list((1 / (365.25 * 24)) * np.arange(1, 13))
  61. + list((1 / (24 * 7)) * np.arange(1, 7))
  62. + list((1 / 24) * np.arange(1, 12))
  63. )
  64. components = []
  65. for i, f in enumerate(self.frequencies):
  66. hourly[f"c_{i+1}"] = (hourly["h"] * f * 2 * np.pi).apply(np.cos)
  67. hourly[f"s_{i+1}"] = (hourly["h"] * f * 2 * np.pi).apply(np.sin)
  68. components += [f"c_{i+1}", f"s_{i+1}"]
  69. hourly.rename(
  70. columns={"Consommation brute électricité (MW) - RTE": "conso"}, inplace=True
  71. )
  72. hourly["conso"] /= 1000
  73. # fit load curve to fourier components
  74. model = ols("conso ~ " + " + ".join(components) + " + C(Y)", data=hourly)
  75. self.fit_results = model.fit()
  76. # normalize according to the desired total yearly consumption
  77. intercept = self.fit_results.params[0] + self.fit_results.params[1]
  78. self.fit_results.params *= self.yearly_total / (intercept * 365.25 * 24)
  79. class ConsumptionFlexibilityModel:
  80. def __init__(self, flexibility_power: float, flexibility_time: int):
  81. """Consumption flexibility model.
  82. Adapts consumption to supply under constraints.
  83. :param flexibility_power: Maximum power that can be postponed at any time, in GW
  84. :type flexibility_power: float
  85. :param flexibility_time: Maximum consumption delay in hours
  86. :type flexibility_time: int
  87. """
  88. self.flexibility_power = flexibility_power
  89. self.flexibility_time = flexibility_time
  90. def run(self, load: np.ndarray, supply: np.ndarray) -> np.ndarray:
  91. """Runs the model.
  92. Given initial load and supply, the model returns an adjusted load optimized under constraints.
  93. :param load: 1D array containing the initial load at each timestep, in GW
  94. :type load: np.ndarray
  95. :param supply: 1D array containing the power supply at each timestep, in GW
  96. :type supply: np.ndarray
  97. :return: 1D array containing the adjusted load at each timestep, in GW
  98. :rtype: np.ndarray
  99. """
  100. from functools import reduce
  101. T = len(supply)
  102. tau = self.flexibility_time
  103. h = cp.Variable((T, tau + 1))
  104. constraints = [
  105. h >= 0,
  106. h <= 1,
  107. # bound on the fraction of the demand at any time that can be postponed
  108. h[:, 0] >= 1 - self.flexibility_power / load,
  109. cp.multiply(load, cp.sum(h, axis=1)) - load <= self.flexibility_power,
  110. ] + [
  111. # total demand conservation
  112. reduce(lambda x, y: x + y, [h[t - l, l] for l in range(tau)]) == 1
  113. for t in np.arange(tau, T)
  114. ]
  115. prob = cp.Problem(
  116. cp.Minimize(cp.sum(cp.pos(cp.multiply(load, cp.sum(h, axis=1)) - supply))),
  117. constraints,
  118. )
  119. prob.solve(verbose=True, solver=cp.ECOS, max_iters=300)
  120. hb = np.array(h.value)
  121. return load * np.sum(hb, axis=1)