Lucas Gautheron 1 year ago
parent
commit
b4d55e65b5

+ 1 - 0
.DS_Store

@@ -0,0 +1 @@
+.git/annex/objects/zJ/17/MD5E-s6148--860827d44adcd48ca8d64f07be987e9f/MD5E-s6148--860827d44adcd48ca8d64f07be987e9f

+ 0 - 1
consommation-quotidienne-brute.csv

@@ -1 +0,0 @@
-.git/annex/objects/V7/k6/MD5E-s16583374--80c366e542620e3cd4dcd68d130fdf34.csv/MD5E-s16583374--80c366e542620e3cd4dcd68d130fdf34.csv

+ 1 - 0
data/consommation-quotidienne-brute.csv

@@ -0,0 +1 @@
+../.git/annex/objects/V7/k6/MD5E-s16583374--80c366e542620e3cd4dcd68d130fdf34.csv/MD5E-s16583374--80c366e542620e3cd4dcd68d130fdf34.csv

+ 1 - 0
data/eco2mix-national-cons-def.csv

@@ -0,0 +1 @@
+../.git/annex/objects/G2/q0/MD5E-s60226207--d18cd3692db2b5cb801e6e6abfb5344a.csv/MD5E-s60226207--d18cd3692db2b5cb801e6e6abfb5344a.csv

+ 1 - 0
data/ninja_pv_europe_v1.1_merra2.csv

@@ -0,0 +1 @@
+../.git/annex/objects/1w/X7/MD5E-s66200945--1fe701ba91de4f086f26957bc6b0cc21.csv/MD5E-s66200945--1fe701ba91de4f086f26957bc6b0cc21.csv

+ 1 - 0
data/ninja_wind_europe_v1.1_current_on-offshore.csv

@@ -0,0 +1 @@
+../.git/annex/objects/gV/pW/MD5E-s122422673--a479250c6523a160c62deafd463b6475.csv/MD5E-s122422673--a479250c6523a160c62deafd463b6475.csv

+ 1 - 0
data/potential.parquet

@@ -0,0 +1 @@
+../.git/annex/objects/0V/pF/MD5E-s121425233--7ffca57425a772ab6fb1a23da0cb6486/MD5E-s121425233--7ffca57425a772ab6fb1a23da0cb6486

+ 1 - 0
data/registre-national-installation-production-stockage-electricite-agrege.csv

@@ -0,0 +1 @@
+../.git/annex/objects/v1/8f/MD5E-s19748506--c8d309c0e9d300448fe594d6159d4298.csv/MD5E-s19748506--c8d309c0e9d300448fe594d6159d4298.csv

+ 0 - 1
eco2mix-national-cons-def.csv

@@ -1 +0,0 @@
-.git/annex/objects/G2/q0/MD5E-s60226207--d18cd3692db2b5cb801e6e6abfb5344a.csv/MD5E-s60226207--d18cd3692db2b5cb801e6e6abfb5344a.csv

+ 3 - 3
generate_load_factors.py

@@ -1,5 +1,5 @@
-wind_data = pd.read_csv("ninja_wind_europe_v1.1_current_on-offshore.csv")
-solar_data = pd.read_csv("ninja_pv_europe_v1.1_merra2.csv")
+wind_data = pd.read_csv("data/ninja_wind_europe_v1.1_current_on-offshore.csv")
+solar_data = pd.read_csv("data/ninja_pv_europe_v1.1_merra2.csv")
 
 wind_data['time'] = pd.to_datetime(wind_data['time'], format="%Y-%m-%d %H:%M:%S")
 solar_data['time'] = pd.to_datetime(solar_data['time'], format="%Y-%m-%d %H:%M:%S")
@@ -40,4 +40,4 @@ potential = potential.merge(pd.DataFrame(solar), left_index=True, right_index=Tr
 print(potential)
 potential.sort_values(["time", "region"], inplace=True)
 
-potential.to_parquet("potential.parquet")
+potential.to_parquet("data/potential.parquet")

+ 1 - 0
mix_simul/__pycache__/consumption.cpython-38.pyc

@@ -0,0 +1 @@
+../../.git/annex/objects/9j/jJ/MD5E-s4909--1057e2848548823c046604954baf10cf.pyc/MD5E-s4909--1057e2848548823c046604954baf10cf.pyc

+ 1 - 0
mix_simul/__pycache__/production.cpython-38.pyc

@@ -0,0 +1 @@
+../../.git/annex/objects/4Q/fq/MD5E-s2360--7294fc473c5a5a8e4df1e957a715fd8f.pyc/MD5E-s2360--7294fc473c5a5a8e4df1e957a715fd8f.pyc

+ 1 - 0
mix_simul/__pycache__/scenarios.cpython-38.pyc

@@ -0,0 +1 @@
+../../.git/annex/objects/Kq/7W/MD5E-s1578--a135abb705a5c755dea0a4e64378868e.pyc/MD5E-s1578--a135abb705a5c755dea0a4e64378868e.pyc

+ 1 - 0
mix_simul/__pycache__/storage.cpython-38.pyc

@@ -0,0 +1 @@
+../../.git/annex/objects/Kg/29/MD5E-s2254--8214c9f4a246799d8bb719439dee5438.pyc/MD5E-s2254--8214c9f4a246799d8bb719439dee5438.pyc

+ 114 - 0
mix_simul/consumption.py

@@ -0,0 +1,114 @@
+import pandas as pd
+import numpy as np
+import cvxpy as cp
+
+from statsmodels.formula.api import ols
+
+from typing import Union
+
+class ConsumptionModel:
+    def __init__(self):
+        pass
+
+    def get(self):
+        pass
+
+class ThermoModel(ConsumptionModel):
+    def __init__(self):
+        pass
+
+    def get(temperatures: np.ndarray):
+        pass
+
+class FittedConsumptionModel(ConsumptionModel):
+    def __init__(self, yearly_total: float):
+        self.yearly_total = yearly_total
+        self.fit()
+
+    def get(self, times: Union[pd.Series, 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)
+        times['h'] = ((times.index - self.time_reference
+                    ).total_seconds()/3600).astype(int)
+        times['Y'] = 1
+        for i, f in enumerate(self.frequencies):
+            times[f'c_{i+1}'] = (times['h']*f*2*np.pi).apply(np.cos)
+            times[f's_{i+1}'] = (times['h']*f*2*np.pi).apply(np.sin)
+
+        curve = self.fit_results.predict(times).values
+        return curve
+
+    def fit(self):
+        consumption = pd.read_csv("data/consommation-quotidienne-brute.csv", sep=";")
+        consumption['time'] = pd.to_datetime(
+        consumption["Date - Heure"].str.replace(":", ""), format="%Y-%m-%dT%H%M%S%z", utc=True)
+        consumption.set_index("time", inplace=True)
+        hourly = consumption.resample('1H').mean()
+
+        self.time_reference = hourly.index.min()
+
+        hourly['h'] = ((hourly.index - self.time_reference
+                        ).total_seconds()/3600).astype(int)
+
+        hourly['Y'] = hourly.index.year-hourly.index.year.min()
+
+        # generate fourier components
+        self.frequencies = list((1/(365.25*24))*np.arange(1, 13)) + \
+            list((1/(24*7)) * np.arange(1, 7)) + \
+            list((1/24) * np.arange(1, 12))
+        components = []
+
+        for i, f in enumerate(self.frequencies):
+            hourly[f'c_{i+1}'] = (hourly['h']*f*2*np.pi).apply(np.cos)
+            hourly[f's_{i+1}'] = (hourly['h']*f*2*np.pi).apply(np.sin)
+
+            components += [f'c_{i+1}', f's_{i+1}']
+
+        hourly.rename(
+            columns={'Consommation brute électricité (MW) - RTE': 'conso'}, inplace=True)
+        hourly["conso"] /= 1000
+
+        # fit load curve to fourier components
+        model = ols("conso ~ " + " + ".join(components)+" + C(Y)", data=hourly)
+        self.fit_results = model.fit()
+
+        # normalize according to the desired total yearly consumption
+        intercept = self.fit_results.params[0]+self.fit_results.params[1]
+        self.fit_results.params *= self.yearly_total/(intercept*365.25*24)
+
+class ConsumptionFlexibilityModel:
+
+    def __init__(self, flexibility_power: float, flexibility_time: int):
+        self.flexibility_power = flexibility_power
+        self.flexibility_time = flexibility_time
+
+    def run(self, load: np.ndarray, supply: np.ndarray):
+        from functools import reduce
+
+        T = len(supply)
+        tau = self.flexibility_time    
+
+        h = cp.Variable((T, tau+1))
+
+        constraints = [
+            h >= 0,
+            h <= 1,
+            h[:, 0] >= 1-flexibility_power/load, # bound on the fraction of the demand at any time that can be postponed
+            cp.multiply(load, cp.sum(h, axis=1))-load <= flexibility_power, # total demand conservation
+        ] + [
+            reduce(lambda x, y: x+y, [h[t-l, l] for l in range(tau)]) == 1
+            for t in np.arange(flexibility_time, T)
+        ]
+
+        prob = cp.Problem(
+            cp.Minimize(
+                cp.sum(cp.pos(cp.multiply(load, cp.sum(h, axis=1))-supply))
+            ),
+            constraints
+        )
+
+        prob.solve(verbose=True, solver=cp.ECOS, max_iters=300)
+
+        hb = np.array(h.value)
+        return load*np.sum(hb, axis=1)

+ 58 - 0
mix_simul/production.py

@@ -0,0 +1,58 @@
+import numpy as np
+import cvxpy as cp
+
+class PowerSupply:
+    def __init__(self):
+        pass
+
+    def power(self):
+        pass
+
+
+class IntermittentArray(PowerSupply):
+    def __init__(self,
+        potential: np.ndarray,
+        units_per_region: np.ndarray
+    ):
+
+        self.potential = potential
+        self.units_per_region = units_per_region
+
+    def power(self):
+        return np.einsum('ijk,ik', self.potential, self.units_per_region)    
+
+
+class DispatchableArray(PowerSupply):
+    def __init__(self,
+        dispatchable: np.ndarray
+    ):
+        self.dispatchable = np.array(dispatchable)
+        self.n_dispatchable_sources = self.dispatchable.shape[0]
+
+    def power(self, gap: np.ndarray) -> np.ndarray:
+        # optimize dispatch
+        T = len(gap)
+        dispatch_power = cp.Variable((self.n_dispatchable_sources, T))
+
+        constraints = [
+            dispatch_power >= 0,
+            cp.sum(dispatch_power, axis=0) <= np.maximum(gap, 0)
+        ] + [
+            dispatch_power[i,:] <= self.dispatchable[i,0]
+            for i in range(self.n_dispatchable_sources)
+        ] + [
+            cp.sum(dispatch_power[i]) <= self.dispatchable[i,1]*T/(365.25*24)
+            for i in range(self.n_dispatchable_sources)
+        ]
+
+        prob = cp.Problem(
+            cp.Minimize(
+                cp.sum(cp.pos(gap-cp.sum(dispatch_power, axis=0)))
+            ),
+            constraints
+        )
+
+        prob.solve(solver=cp.ECOS, max_iters=300)
+        dp = dispatch_power.value
+
+        return dp

+ 61 - 0
mix_simul/scenarios.py

@@ -0,0 +1,61 @@
+from .consumption import *
+from .production import *
+from .storage import *
+
+import yaml
+
+
+class Scenario:
+    def __init__(self,
+        yearly_total = 645*1000,
+        sources: dict = {},
+        multistorage: dict = {},
+        flexibility_power=0,
+        flexibility_time=8
+    ):
+        self.yearly_total = yearly_total
+        self.sources = sources
+        self.multistorage = multistorage
+        self.flexibility_power = flexibility_power
+        self.flexibility_time = flexibility_time
+        pass
+
+    def run(self, times, intermittent_load_factors):
+        # consumption
+        consumption_model = FittedConsumptionModel(self.yearly_total)
+        load = consumption_model.get(times)
+
+        # intermittent sources (or sources with fixed load factors)
+        intermittent_array = IntermittentArray(
+            intermittent_load_factors,
+            np.transpose([self.sources["intermittent"]])
+        )
+        power = intermittent_array.power()
+
+        if self.flexibility_power > 0:
+            flexibility_model = ConsumptionFlexibilityModel(
+                self.flexibility_power, self.flexibility_time
+            )
+            load = flexibility_model.run(load, power)
+
+        power_delta = power-load
+
+        # adjust power to load with storage
+        storage_model = MultiStorageModel(
+            self.multistorage["capacity"],
+            self.multistorage["power"],
+            self.multistorage["power"],
+            self.multistorage["efficiency"]
+        )
+
+        storage, storage_impact = storage_model.run(power_delta)
+        gap = load-power-storage_impact.sum(axis=0)
+
+        # further adjust power to load with dispatchable power sources
+        dispatchable_model = DispatchableArray(self.sources["dispatchable"])
+        dp = dispatchable_model.power(gap)
+
+        gap -= dp.sum(axis=0)
+        S = np.maximum(gap, 0).mean()
+
+        return S, load, power, gap, storage, dp

+ 77 - 0
mix_simul/storage.py

@@ -0,0 +1,77 @@
+import numpy as np 
+import numba
+from typing import Tuple
+
+@numba.njit
+def storage_iterate(dE, capacity, efficiency, n):
+    storage = np.zeros(n)
+
+    for i in np.arange(1, n):
+        if dE[i] >= 0:
+            dE[i] *= efficiency
+
+        storage[i] = np.maximum(0, np.minimum(capacity, storage[i-1]+dE[i-1]))
+
+    return storage
+
+class StorageModel:
+    def __int__(self):
+        pass
+
+
+class MultiStorageModel(StorageModel):
+    def __init__(self,
+        storage_capacities: np.ndarray,
+        storage_max_loads=np.ndarray,
+        storage_max_deliveries=np.ndarray,
+        storage_efficiencies=np.ndarray
+    ):
+
+        self.storage_max_loads = np.array(storage_max_loads)
+        self.storage_max_deliveries = np.array(storage_max_deliveries)
+        self.storage_capacities = np.array(storage_capacities)*self.storage_max_loads
+        self.storage_efficiencies = np.array(storage_efficiencies)
+
+        self.n_storages = len(self.storage_capacities)
+
+        assert len(storage_max_loads) == self.n_storages
+        assert len(storage_max_deliveries) == self.n_storages
+        assert len(storage_efficiencies) == self.n_storages
+
+    
+    def run(self, power_delta: np.ndarray) -> Tuple[np.ndarray, np.ndarray]:
+        T = len(power_delta)
+
+        available_power = power_delta
+        excess_power = np.maximum(0, available_power)
+        deficit_power = np.maximum(0, -available_power)
+
+        storage_try_load = np.zeros((self.n_storages, T))
+        storage_try_delivery = np.zeros((self.n_storages, T))
+
+        storage = np.zeros((self.n_storages, T))
+        storage_impact = np.zeros((self.n_storages, T))
+        dE_storage = np.zeros((self.n_storages, T))
+
+        for i in range(self.n_storages):
+            storage_try_load[i] = np.minimum(excess_power, self.storage_max_loads[i])
+            storage_try_delivery[i] = np.minimum(deficit_power, self.storage_max_deliveries[i])
+
+            dE_storage[i] = storage_try_load[i]-storage_try_delivery[i]
+
+            storage[i] = storage_iterate(
+                dE_storage[i], self.storage_capacities[i], self.storage_efficiencies[i], T
+            )
+
+            # impact of storage on the available power        
+            storage_impact[i] = -np.diff(storage[i], append=0)
+            storage_impact[i] = np.multiply(
+                storage_impact[i],
+                np.where(storage_impact[i] < 0, 1/self.storage_efficiencies[i], 1)
+            )
+
+            available_power += storage_impact[i]
+            excess_power = np.maximum(0, available_power)
+            deficit_power = np.maximum(0, -available_power)
+
+        return storage, storage_impact

+ 0 - 1
ninja_pv_europe_v1.1_merra2.csv

@@ -1 +0,0 @@
-.git/annex/objects/1w/X7/MD5E-s66200945--1fe701ba91de4f086f26957bc6b0cc21.csv/MD5E-s66200945--1fe701ba91de4f086f26957bc6b0cc21.csv

+ 0 - 1
ninja_wind_europe_v1.1_current_on-offshore.csv

@@ -1 +0,0 @@
-.git/annex/objects/gV/pW/MD5E-s122422673--a479250c6523a160c62deafd463b6475.csv/MD5E-s122422673--a479250c6523a160c62deafd463b6475.csv

+ 1 - 1
output.png

@@ -1 +1 @@
-.git/annex/objects/Q4/gx/MD5E-s718471--81f768fdd51b9afea5b8d0f49b740718.png/MD5E-s718471--81f768fdd51b9afea5b8d0f49b740718.png
+.git/annex/objects/QG/7v/MD5E-s717267--e3338628dac2522575cab1f4460e7c35.png/MD5E-s717267--e3338628dac2522575cab1f4460e7c35.png

+ 1 - 1
output_dispatch.png

@@ -1 +1 @@
-.git/annex/objects/J2/Ff/MD5E-s601016--7efdc3d552f22a5110d82b3fdab9d6cc.png/MD5E-s601016--7efdc3d552f22a5110d82b3fdab9d6cc.png
+.git/annex/objects/Vw/56/MD5E-s499865--e81f755d4e5f8d2168b9b41af8485266.png/MD5E-s499865--e81f755d4e5f8d2168b9b41af8485266.png

+ 1 - 1
output_storage.png

@@ -1 +1 @@
-.git/annex/objects/JK/65/MD5E-s307265--4ccb4b1ba75985d1d29ead84495904b7.png/MD5E-s307265--4ccb4b1ba75985d1d29ead84495904b7.png
+.git/annex/objects/Jk/GX/MD5E-s310746--c6850ddf25418072833dd95c8e793f2d.png/MD5E-s310746--c6850ddf25418072833dd95c8e793f2d.png

+ 0 - 1
potential.parquet

@@ -1 +0,0 @@
-.git/annex/objects/0V/pF/MD5E-s121425233--7ffca57425a772ab6fb1a23da0cb6486/MD5E-s121425233--7ffca57425a772ab6fb1a23da0cb6486

+ 0 - 1
registre-national-installation-production-stockage-electricite-agrege.csv

@@ -1 +0,0 @@
-.git/annex/objects/v1/8f/MD5E-s19748506--c8d309c0e9d300448fe594d6159d4298.csv/MD5E-s19748506--c8d309c0e9d300448fe594d6159d4298.csv

+ 194 - 0
run.py

@@ -0,0 +1,194 @@
+import pandas as pd 
+import numpy as np
+
+from matplotlib import pyplot as plt 
+import matplotlib.dates as mdates
+
+import yaml
+
+from mix_simul.scenarios import Scenario 
+
+with open("scenarios/rte.yml", 'r') as f:
+    rte = yaml.load(f, Loader=yaml.FullLoader)
+
+potential = pd.read_parquet("data/potential.parquet")
+potential = potential.loc['1985-01-01 00:00:00':'2015-01-01 00:00:00', :]
+potential.fillna(0, inplace=True)
+
+begin = "2012-01-01"
+end = "2015-01-01"
+flexibility = False
+
+potential = potential.loc[(
+    slice(f'{begin} 00:00:00', f'{end} 00:00:00'), 'FR'), :]
+
+# intermittent sources potential
+p = potential[["onshore", "offshore", "solar"]].to_xarray().to_array()
+p = np.insert(p, 3, 0.68, axis=0)  # nuclear power-like
+
+times = potential.index.get_level_values(0)
+
+fig, axes = plt.subplots(nrows=6, ncols=2, sharex="col", sharey=True)
+w, h = fig.get_size_inches()
+fig.set_figwidth(w*1.5)
+fig.set_figheight(h*1.5)
+
+fig_storage, axes_storage = plt.subplots(nrows=6, ncols=2, sharex="col", sharey=True)
+fig_storage.set_figwidth(w*1.5)
+fig_storage.set_figheight(h*1.5)
+
+fig_dispatch, axes_dispatch = plt.subplots(nrows=6, ncols=2, sharex="col", sharey=True)
+fig_dispatch.set_figwidth(w*1.5)
+fig_dispatch.set_figheight(h*1.5)
+
+date_fmt = mdates.DateFormatter('%d/%m')
+
+
+# for step in np.linspace(start, stop, 2050-2022, True)[::-1]:
+row = 0
+for scenario in rte:
+    rte[scenario]["flexibility_power"] = 0
+    scenario_model = Scenario(**rte[scenario])
+    S, load, production, gap, storage, dp = scenario_model.run(times, p)
+
+    print(f"{scenario}:", S, gap.max(), np.quantile(gap, 0.95))
+    print(f"exports: {np.minimum(np.maximum(-gap, 0), 39).sum()/1000} TWh; imports: {np.minimum(np.maximum(gap, 0), 39).sum()/1000} TWh")
+    print(f"dispatchable: " + ", ".join([f"{dp[i].sum()/1000:.2f} TWh" for i in range(dp.shape[0])]))
+
+    potential['load'] = load
+    potential['production'] = production
+    potential['available'] = production-np.diff(storage.sum(axis=0), append=0)
+
+    for i in range(3):
+        potential[f"storage_{i}"] = np.diff(storage[i,:], append=0)#storage[i,:]/1000
+        potential[f"storage_{i}"] = storage[i,:]/1000
+
+    for i in range(dp.shape[0]):
+        potential[f"dispatch_{i}"] = dp[i,:]
+
+    potential["dispatch"] = dp.sum(axis=0)
+
+    data = [
+        potential.loc[(slice('2013-12-01 00:00:00',
+                       '2014-01-01 00:00:00'), 'FR'), :],
+        potential.loc[(slice('2013-06-01 00:00:00',
+                       '2013-07-01 00:00:00'), 'FR'), :]
+    ]
+
+    months = [
+        "Février",
+        "Juin"
+    ]
+
+    labels = [
+        "load (GW)",
+        "production (GW)",
+        "available power (production-storage) (GW)",
+        "power deficit"
+    ]
+
+    labels_storage = [
+        "Batteries (TWh)",
+        "STEP (TWh)",
+        "P2G (TWh)"
+    ]
+
+    labels_dispatch = [
+        "Hydro (GW)",
+        "Biomass (GW)",
+        "Thermal (GW)"
+    ]
+
+    for col in range(2):
+        ax = axes[row, col]
+        ax.plot(data[col].index.get_level_values(0), data[col]
+                ["load"], label="adjusted load (GW)", lw=1)
+        ax.plot(data[col].index.get_level_values(0), data[col]
+                ["production"], label="production (GW)", ls="dotted", lw=1)
+        ax.plot(data[col].index.get_level_values(0), data[col]["available"],
+                label="available power (production-d(storage)/dt) (GW)", lw=1)
+
+        ax.fill_between(
+            data[col].index.get_level_values(0),
+            data[col]["available"],
+            data[col]["load"],
+            where=data[col]["load"] > data[col]["available"],
+            color='red',
+            alpha=0.15
+        )
+
+        ax.xaxis.set_major_formatter(date_fmt)
+        ax.text(
+            0.5, 0.87, f"Scénario {scenario} ({months[col]})", ha='center', transform=ax.transAxes)
+
+        ax.set_ylim(25, 225)
+
+        ax = axes_storage[row, col]
+        for i in np.arange(3):
+            if i == 2:
+                base = 0
+            else:
+                base = np.sum([data[col][f"storage_{j}"] for j in np.arange(i+1,3)], axis=0)
+            
+            ax.fill_between(data[col].index.get_level_values(0), base, base+data[col]
+                [f"storage_{i}"], label=f"storage {i}", alpha=0.5)
+
+            ax.plot(data[col].index.get_level_values(0), base+data[col]
+                [f"storage_{i}"], label=f"storage {i}", lw=0.25)
+
+        ax.xaxis.set_major_formatter(date_fmt)
+        ax.text(
+            0.5, 0.87, f"Scénario {scenario} ({months[col]})", ha='center', transform=ax.transAxes)
+
+        ax = axes_dispatch[row, col]
+        for i in range(dp.shape[0]):
+            if i == 0:
+                base = 0
+            else:
+                base = np.sum([data[col][f"dispatch_{j}"] for j in np.arange(i)], axis=0)
+            
+            ax.fill_between(data[col].index.get_level_values(0), base, base+data[col]
+                [f"dispatch_{i}"], label=f"dispatch {i}", alpha=0.5)
+
+            ax.plot(data[col].index.get_level_values(0), base+data[col]
+                [f"dispatch_{i}"], label=f"dispatch {i}", lw=0.25)
+
+        ax.xaxis.set_major_formatter(date_fmt)
+        ax.text(
+            0.5, 0.87, f"Scénario {scenario} ({months[col]})", ha='center', transform=ax.transAxes)
+
+    row += 1
+
+for label in axes[-1, 0].get_xmajorticklabels() + axes[-1, 1].get_xmajorticklabels():
+    label.set_rotation(30)
+    label.set_horizontalalignment("right")
+
+for label in axes_storage[-1, 0].get_xmajorticklabels() + axes_storage[-1, 1].get_xmajorticklabels():
+    label.set_rotation(30)
+    label.set_horizontalalignment("right")
+
+for label in axes_dispatch[-1, 0].get_xmajorticklabels() + axes_dispatch[-1, 1].get_xmajorticklabels():
+    label.set_rotation(30)
+    label.set_horizontalalignment("right")
+
+flex = "With" if flexibility else "Without"
+
+plt.subplots_adjust(wspace=0, hspace=0)
+fig.suptitle(f"Simulations based on {begin}--{end} weather data.\n{flex} consumption flexibility; no nuclear seasonality (unrealistic)")
+fig.text(1, 0, 'Lucas Gautheron', ha="right")
+fig.legend(labels, loc='lower right', bbox_to_anchor=(1, -0.1),
+           ncol=len(labels), bbox_transform=fig.transFigure)
+fig.savefig("output.png", bbox_inches="tight", dpi=200)
+
+fig_storage.suptitle(f"Simulations based on {begin}--{end} weather data.\n{flex} consumption flexibility; no nuclear seasonality (unrealistic)")
+fig_storage.text(1, 0, 'Lucas Gautheron', ha="right")
+fig_storage.legend(labels_storage, loc='lower right', bbox_to_anchor=(1, -0.1),
+           ncol=len(labels_storage), bbox_transform=fig_storage.transFigure)
+fig_storage.savefig("output_storage.png", bbox_inches="tight", dpi=200)
+
+fig_dispatch.suptitle(f"Simulations based on {begin}--{end} weather data.\n{flex} consumption flexibility; no nuclear seasonality (unrealistic)")
+fig_dispatch.text(1, 0, 'Lucas Gautheron', ha="right")
+fig_dispatch.legend(labels_dispatch, loc='lower right', bbox_to_anchor=(1, -0.1),
+           ncol=len(labels_dispatch), bbox_transform=fig_dispatch.transFigure)
+fig_dispatch.savefig("output_dispatch.png", bbox_inches="tight", dpi=200)
+plt.show()

+ 65 - 0
scenarios/rte.yml

@@ -0,0 +1,65 @@
+M0:
+  yearly_total: 645000
+  flexibility_power: 17
+  sources:
+    intermittent: [74, 62, 208, 0]
+    dispatchable: [[22, 63000], [2, 12000], [0.5, 1000000000]]
+  multistorage:
+    power: [26, 8, 29]
+    capacity: [4, 24, 336]
+    efficiency: [0.8, 0.8, 0.3]
+
+M1:
+  yearly_total: 645000
+  flexibility_power: 17
+  sources:
+    intermittent: [59, 45, 214, 16]
+    dispatchable: [[22, 63000], [2, 12000], [0.5, 1000000000]]
+  multistorage:
+    power: [21, 8, 20]
+    capacity: [4, 24, 336]
+    efficiency: [0.8, 0.8, 0.3]
+
+M23:
+  yearly_total: 645000
+  flexibility_power: 17
+  sources:
+    intermittent: [72, 60, 125, 16]
+    dispatchable: [[22, 63000], [2, 12000], [0.5, 1000000000]]
+  multistorage:
+    power: [13, 8, 20]
+    capacity: [4, 24, 336]
+    efficiency: [0.8, 0.8, 0.3]
+
+N1:
+  yearly_total: 645000
+  flexibility_power: 17
+  sources:
+    intermittent: [58, 45, 118, 29]
+    dispatchable: [[22, 63000], [2, 12000], [0.5, 1000000000]]
+  multistorage:
+    power: [9, 8, 11]
+    capacity: [4, 24, 336]
+    efficiency: [0.8, 0.8, 0.3]
+
+N2:
+  yearly_total: 645000
+  flexibility_power: 17
+  sources:
+    intermittent: [52, 36, 90, 39]
+    dispatchable: [[22, 63000], [2, 12000], [0.5, 1000000000]]
+  multistorage:
+    power: [2, 8, 5]
+    capacity: [4, 24, 336]
+    efficiency: [0.8, 0.8, 0.3]
+
+N03:
+  yearly_total: 645000
+  flexibility_power: 17
+  sources:
+    intermittent: [43, 22, 70, 51]
+    dispatchable: [[22, 63000], [2, 12000], [0.5, 1000000000]]
+  multistorage:
+    power: [1, 8, 0]
+    capacity: [4, 24, 336]
+    efficiency: [0.8, 0.8, 0.3]