storage.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. import numpy as np
  2. import numba
  3. from typing import Tuple
  4. @numba.njit
  5. def storage_iterate(dE, capacity, efficiency, n):
  6. storage = np.zeros(n)
  7. for i in np.arange(1, n):
  8. if dE[i] >= 0:
  9. dE[i] *= efficiency
  10. storage[i] = np.maximum(0, np.minimum(capacity, storage[i - 1] + dE[i - 1]))
  11. return storage
  12. class StorageModel:
  13. def __int__(self):
  14. pass
  15. class MultiStorageModel(StorageModel):
  16. def __init__(
  17. self,
  18. storage_capacities: np.ndarray,
  19. storage_max_loads: np.ndarray,
  20. storage_max_deliveries: np.ndarray,
  21. storage_efficiencies: np.ndarray,
  22. ):
  23. """Multiple storage model.
  24. The model fills each storage capacity by order of priority when the power supply exceeds demand.
  25. It then drains each storage capacity according to the same order of priority when the demand exceeds the supply.
  26. :param storage_capacities: 1D array containing the storage capacity of each storage in GWh. The order of the values define the priority order (lower indices = higher priority, typically batteries which absorb diurnal fluctuations).
  27. :type storage_capacities: np.ndarray
  28. :param storage_max_loads: 1D array containing the storage load capacity of each storage in GW.
  29. :type storage_max_loads: np.ndarray, optional
  30. :param storage_max_deliveries: 1D array containing the storage output capacity of each storage in GW.
  31. :type storage_max_deliveries: np.ndarray, optional
  32. :param storage_efficiencies: 1D array containing the yield of each storage (between 0 and 1).
  33. :type storage_efficiencies: np.ndarray, optional
  34. """
  35. self.storage_max_loads = np.array(storage_max_loads)
  36. self.storage_max_deliveries = np.array(storage_max_deliveries)
  37. self.storage_capacities = np.array(storage_capacities)
  38. self.storage_efficiencies = np.array(storage_efficiencies)
  39. self.n_storages = len(self.storage_capacities)
  40. assert len(storage_max_loads) == self.n_storages
  41. assert len(storage_max_deliveries) == self.n_storages
  42. assert len(storage_efficiencies) == self.n_storages
  43. assert (
  44. (self.storage_efficiencies >= 0) & (self.storage_efficiencies <= 1)
  45. ).all(), "The efficiency of each storage type must be comprised between 0 and 1"
  46. def run(self, power_delta: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
  47. """Run the storage model
  48. :param power_delta: 1D array containing the supply-demand difference in GW for each timestep. Positive values mean supply is higher than demand.
  49. :type power_delta: np.ndarray
  50. :return: Tuple of two 2D arrays (each of shape :math:`S\times T`), where S is the amount of storage types and T the amount of timesteps. The first array contains the amount of energy stored in each storage capacity, in GWh. The second array contains the power impact on the grid (negative when storage is loading, positive when storage delivers power).
  51. :rtype: Tuple[np.ndarray, np.ndarray]
  52. """
  53. T = len(power_delta)
  54. available_power = power_delta
  55. excess_power = np.maximum(0, available_power)
  56. deficit_power = np.maximum(0, -available_power)
  57. storage_try_load = np.zeros((self.n_storages, T))
  58. storage_try_delivery = np.zeros((self.n_storages, T))
  59. storage = np.zeros((self.n_storages, T))
  60. storage_impact = np.zeros((self.n_storages, T))
  61. dE_storage = np.zeros((self.n_storages, T))
  62. for i in range(self.n_storages):
  63. storage_try_load[i] = np.minimum(excess_power, self.storage_max_loads[i])
  64. storage_try_delivery[i] = np.minimum(
  65. deficit_power, self.storage_max_deliveries[i]
  66. )
  67. dE_storage[i] = storage_try_load[i] - storage_try_delivery[i]
  68. storage[i] = storage_iterate(
  69. dE_storage[i],
  70. self.storage_capacities[i],
  71. self.storage_efficiencies[i],
  72. T,
  73. )
  74. # impact of storage on the available power
  75. storage_impact[i] = -np.diff(storage[i], append=0)
  76. storage_impact[i] = np.multiply(
  77. storage_impact[i],
  78. np.where(storage_impact[i] < 0, 1 / self.storage_efficiencies[i], 1),
  79. )
  80. available_power += storage_impact[i]
  81. excess_power = np.maximum(0, available_power)
  82. deficit_power = np.maximum(0, -available_power)
  83. return storage, storage_impact