Lucas Gautheron 1 rok pred
rodič
commit
e07b38be01
3 zmenil súbory, kde vykonal 88 pridanie a 9 odobranie
  1. 15 1
      mix_simul/consumption.py
  2. 45 5
      mix_simul/production.py
  3. 28 3
      mix_simul/storage.py

+ 15 - 1
mix_simul/consumption.py

@@ -25,10 +25,24 @@ class ThermoModel(ConsumptionModel):
 
 class FittedConsumptionModel(ConsumptionModel):
     def __init__(self, yearly_total: float):
+        """Consumption Model fitted against observations of French electricity consumption (2012-2021).
+
+        The model captures seasonal, weekly and diurnal variations. It has only 1 parameter, the total annual consumption.
+
+        :param yearly_total: Total annual consumption in GWh.
+        :type yearly_total: float
+        """
         self.yearly_total = yearly_total
         self.fit()
 
-    def get(self, times: Union[pd.Series, np.ndarray]):
+    def get(self, times: Union[pd.Series, np.ndarray]) -> np.ndarray:
+        """Retrieve the consumption for each timestamp from the input array.
+
+        :param times: 1D array containing the input timestamps 
+        :type times: Union[pd.Series, np.ndarray]
+        :return: 1D array of floats (consumption in GW) with the same length as the input.
+        :rtype: np.ndarray
+        """
         # compute the load for the desired timestamps
         times = pd.DataFrame({"time": times}).set_index("time")
         times.index = pd.to_datetime(times.index, utc=True)

+ 45 - 5
mix_simul/production.py

@@ -11,21 +11,61 @@ class PowerSupply:
 
 
 class IntermittentArray(PowerSupply):
-    def __init__(self, potential: np.ndarray, units_per_region: np.ndarray):
+    def __init__(self, load_factors: np.ndarray, capacity_per_region: np.ndarray):
+        """Intermittent source model.
 
-        self.potential = potential
-        self.units_per_region = units_per_region
+        This model is suitable for sources with a fixed load factor,
+        typically intermittent renewables.
 
-    def power(self):
-        return np.einsum("ijk,ik", self.potential, self.units_per_region)
+        :param load_factors: 3D array of shape :math:`S\times T \times R` containing the input load factors.
+
+        S is the amount of sources (solar, onshore and offshore wind, etc.)
+        T is the amount of time steps.
+        R is the amount of regions.
+
+        :type load_factors: np.ndarray
+        :param capacity_per_region: 2D array of shape :math:`S\times R` containing the installed capacity of each type of source within each region, in GW.
+        :type capacity_per_region: np.ndarray
+        """
+
+        self.load_factors = load_factors
+        self.capacity_per_region = capacity_per_region
+
+        assert (
+            (self.load_factors >= 0) & (self.load_factors <= 1)
+        ).all(), "load factors must be comprised between 0 and 1"
+
+    def power(self) -> np.ndarray:
+        """Power production according to the load factors and installed capacity.
+
+        :return: 1D array of size T, containing the power production in GW at each time step.
+        :rtype: np.ndarray
+        """
+        return np.einsum("ijk,ik", self.load_factors, self.capacity_per_region)
 
 
 class DispatchableArray(PowerSupply):
     def __init__(self, dispatchable: np.ndarray):
+        """Dispatchable power supply.
+
+        The model assumes that these sources can be triggered at any time to deliver up to a certain amount of power (the maximum capacity) and up to a certain amount of energy per year.
+        Their total power output never exceeds the amount of power needed to meet the demand (no waste).
+        The output is determined by minimizing the deficit of power (demand-supply) over the considered time-range.
+
+        :param dispatchable: 2D array of shape :math:`S\times 2` containing the power capacity (in GW) and energy capacity (in GWh) of each of the S sources.
+        :type dispatchable: np.ndarray
+        """
         self.dispatchable = np.array(dispatchable)
         self.n_dispatchable_sources = self.dispatchable.shape[0]
 
     def power(self, gap: np.ndarray) -> np.ndarray:
+        """Power production optimized for compensating the input gap.
+
+        :param gap: 1D array containing the power gap to compensate in GW at each timestep.
+        :type gap: np.ndarray
+        :return: 1D array of size T, containing the power production in GW at each time step.
+        :rtype: np.ndarray
+        """
         # optimize dispatch
         T = len(gap)
         dispatch_power = cp.Variable((self.n_dispatchable_sources, T))

+ 28 - 3
mix_simul/storage.py

@@ -25,10 +25,24 @@ class MultiStorageModel(StorageModel):
     def __init__(
         self,
         storage_capacities: np.ndarray,
-        storage_max_loads=np.ndarray,
-        storage_max_deliveries=np.ndarray,
-        storage_efficiencies=np.ndarray,
+        storage_max_loads: np.ndarray,
+        storage_max_deliveries: np.ndarray,
+        storage_efficiencies: np.ndarray,
     ):
+        """Multiple storage model.
+
+        The model fills each storage capacity by order of priority when the power supply exceeds demand.
+        It then drains each storage capacity according to the same order of priority when the demand exceeds the supply.
+
+        :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).
+        :type storage_capacities: np.ndarray
+        :param storage_max_loads: 1D array containing the storage load capacity of each storage in GW.
+        :type storage_max_loads: np.ndarray, optional
+        :param storage_max_deliveries: 1D array containing the storage output capacity of each storage in GW.
+        :type storage_max_deliveries: np.ndarray, optional
+        :param storage_efficiencies: 1D array containing the yield of each storage (between 0 and 1).
+        :type storage_efficiencies: np.ndarray, optional
+        """
 
         self.storage_max_loads = np.array(storage_max_loads)
         self.storage_max_deliveries = np.array(storage_max_deliveries)
@@ -41,7 +55,18 @@ class MultiStorageModel(StorageModel):
         assert len(storage_max_deliveries) == self.n_storages
         assert len(storage_efficiencies) == self.n_storages
 
+        assert (
+            (self.storage_efficiencies >= 0) & (self.storage_efficiencies <= 1)
+        ).all(), "The efficiency of each storage type must be comprised between 0 and 1"
+
     def run(self, power_delta: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
+        """Run the storage model
+
+        :param power_delta: 1D array containing the supply-demand difference in GW for each timestep. Positive values mean supply is higher than demand.
+        :type power_delta: np.ndarray
+        :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).
+        :rtype: Tuple[np.ndarray, np.ndarray]
+        """
         T = len(power_delta)
 
         available_power = power_delta