Lucas Gautheron 1 gadu atpakaļ
vecāks
revīzija
c6f2f2110b

+ 1 - 0
consommation-quotidienne-brute.csv

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

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

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

+ 43 - 0
generate_load_factors.py

@@ -0,0 +1,43 @@
+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['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")
+
+regions_labels = sorted(list(solar_data.columns))
+regions_labels.remove("time")
+
+offshore_labels = {x for x in wind_data.columns if '_OFF' in x}
+missing_offshore = {f"{x}_OFF" for x in regions_labels} - offshore_labels
+
+for x in missing_offshore:
+    wind_data[x] = 0
+
+offshore_labels = [x for x in wind_data.columns if '_OFF' in x]
+onshore_labels = [x for x in wind_data.columns if '_ON' in x]
+
+offshore = wind_data[["time"] + offshore_labels]
+onshore = wind_data[["time"] + onshore_labels]
+solar = solar_data.set_index("time").stack().rename("solar")
+print(solar.index.names)
+solar.index.names = ["time", "region"]
+
+offshore.set_index("time", inplace=True)
+offshore.columns = map(lambda x: x.replace("_OFF", ""), offshore.columns)
+offshore = offshore.stack().rename("offshore")
+offshore.index.names = ["time", "region"]
+
+onshore.set_index("time", inplace=True)
+onshore.columns = map(lambda x: x.replace("_ON", ""), onshore.columns)
+onshore = onshore.stack().rename("onshore")
+onshore.index.names = ["time", "region"]
+
+potential = pd.DataFrame(offshore)
+print(potential)
+potential = potential.merge(pd.DataFrame(onshore), left_index=True, right_index=True, how="outer")
+print(potential)
+potential = potential.merge(pd.DataFrame(solar), left_index=True, right_index=True, how="outer")
+print(potential)
+potential.sort_values(["time", "region"], inplace=True)
+
+potential.to_parquet("potential.parquet")

+ 1 - 0
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
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

+ 596 - 0
optimize.py

@@ -0,0 +1,596 @@
+import pandas as pd
+import numpy as np
+import numba
+
+from numpy.random import dirichlet
+from scipy.optimize import linprog
+from scipy.signal import square
+from statsmodels.formula.api import ols
+
+import cvxpy as cp
+
+from matplotlib import pyplot as plt
+import matplotlib.dates as mdates
+
+import argparse
+
+parser = argparse.ArgumentParser()
+parser.add_argument("--begin")
+parser.add_argument("--end")
+parser.add_argument("--flexibility", action="store_true")
+args = parser.parse_args()
+
+
+def achieved_load_factors(sources=[]):
+    # installed capacity
+    infra = pd.read_csv(
+        "registre-national-installation-production-stockage-electricite-agrege.csv", sep=";")
+    infra["Year"] = infra["dateMiseEnService"].str[-4:].astype(str)
+    infra.dropna(subset=["Year", "puisMaxInstallee"], inplace=True)
+    infra = infra[infra["Year"] != 'nan']
+    infra = infra.groupby(["filiere", "Year"]).agg(
+        pi=('puisMaxInstallee', 'sum')
+    ).reset_index().sort_values(["filiere", "Year"])
+
+    infra['pi'] = infra.groupby(
+        'filiere')['pi'].transform(pd.Series.cumsum)/1000
+    infra["Year"] = infra["Year"].astype(int)
+    infra.set_index(["filiere", "Year"], inplace=True)
+    infra = infra.reindex(
+        index=[
+            (filiere, year) for filiere in set(infra.index.get_level_values(0)) for year in np.arange(2000, 2022)
+        ]
+    )
+    infra = infra.sort_index(level=0).ffill().reindex(infra.index)
+    infra.reset_index(inplace=True)
+    infra = infra.pivot(index="Year", columns="filiere", values="pi")
+
+    # production
+    prod = pd.read_csv("eco2mix-national-cons-def.csv", sep=";")
+    prod["time"] = pd.to_datetime(
+        prod["Date et Heure"].str.replace(":", ""), format="%Y-%m-%dT%H%M%S%z", utc=True
+    )
+    prod["Year"] = prod["time"].dt.year
+    prod = prod.merge(infra, left_on="Year", right_on="Year", how="left").sort_values("time").set_index("time")
+
+    # consumption
+    consumption = pd.read_csv("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)
+
+    print(consumption["Consommation brute électricité (MW) - RTE"])
+
+    for source in sources:
+        fig, ax1 = plt.subplots()
+        ax2 = ax1.twinx()
+        prod[f"{source} (load factor)"] = prod[f"{source} (MW)"]/prod[source]
+        print(prod[f"{source} (load factor)"])
+        ax1.scatter(prod.index, prod[f"{source} (load factor)"], s=0.5, color="red")
+        ax2.plot(consumption.index, consumption["Consommation brute électricité (MW) - RTE"], lw=0.5, color="blue")
+        plt.show()
+        fig.savefig(f"test_{source}.png", dpi=200)
+        plt.clf()
+        plt.cla()
+
+    # plt.show()
+
+
+# achieved_load_factors(["Nucléaire", "Hydraulique"])
+
+
+def generate_load_curve(times, yearly_total=645*1000):
+    # load observed load curve
+    consumption = pd.read_csv("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()
+    hourly['h'] = ((hourly.index - hourly.index.min()
+                    ).total_seconds()/3600).astype(int)
+
+    hourly['Y'] = hourly.index.year-hourly.index.year.min()
+
+    # generate fourier components
+    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(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)
+    results = model.fit()
+
+    # normalize according to the desired total yearly consumption
+    intercept = results.params[0]+results.params[1]
+    results.params *= yearly_total/(intercept*365.25*24)
+
+    # 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 - hourly.index.min()
+                   ).total_seconds()/3600).astype(int)
+    times['Y'] = 1
+    for i, f in enumerate(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 = results.predict(times)
+    return np.array(curve)
+
+
+@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
+
+
+def objective_with_storages(
+    potential,
+    units_per_region,
+    dispatchable,
+    p_load=2664*1000/(365*24),
+    storage_capacities=5*250,
+    storage_max_loads=5*30,
+    storage_max_deliveries=5*30,
+    storage_efficiencies=0.67
+):
+
+    power = np.einsum('ijk,ik', potential, units_per_region)
+    power_delta = power-p_load
+
+    T = len(power)
+
+    available_power = np.array(power_delta)
+    excess_power = np.maximum(0, available_power)
+    deficit_power = np.maximum(0, -available_power)
+
+    n_storages = len(storage_capacities)
+    storage_try_load = np.zeros((n_storages, T))
+    storage_try_delivery = np.zeros((n_storages, T))
+
+    storage = np.zeros((n_storages, T))
+    storage_impact = np.zeros((n_storages, T))
+    dE_storage = np.zeros((n_storages, T))
+
+    for i in range(n_storages):
+        storage_try_load[i] = np.minimum(excess_power, storage_max_loads[i])
+        storage_try_delivery[i] = np.minimum(deficit_power, storage_max_deliveries[i])
+
+        dE_storage[i] = storage_try_load[i]-storage_try_delivery[i]
+
+        storage[i] = storage_iterate(
+            dE_storage[i], storage_capacities[i], 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/storage_efficiencies[i], 1)
+        )
+
+        available_power += storage_impact[i]
+        excess_power = np.maximum(0, available_power)
+        deficit_power = np.maximum(0, -available_power)
+
+    gap = p_load-power-storage_impact.sum(axis=0)
+
+    # optimize dispatch
+    n_dispatchable_sources = dispatchable.shape[0]
+    dispatch_power = cp.Variable((n_dispatchable_sources, T))
+
+    constraints = [
+        dispatch_power >= 0,
+        cp.sum(dispatch_power, axis=0) <= np.maximum(gap, 0)
+    ] + [
+        dispatch_power[i,:] <= dispatchable[i,:,0].sum()
+        for i in range(n_dispatchable_sources)
+    ] + [
+        cp.sum(dispatch_power[i]) <= dispatchable[i,:,1].sum()
+        for i in range(n_dispatchable_sources)
+    ]
+
+    prob = cp.Problem(
+        cp.Minimize(
+            cp.sum(cp.pos(gap-cp.sum(dispatch_power, axis=0))) + cp.max(gap-cp.sum(dispatch_power, axis=0))
+        ),
+        constraints
+    )
+
+    prob.solve(solver=cp.ECOS, max_iters=300)
+    dp = dispatch_power.value
+
+    gap -= dp.sum(axis=0)
+    S = np.maximum(gap, 0).mean()
+
+    return S, power, gap, storage, dp
+
+def objective_with_storage_and_flexibility(
+    potential,
+    units_per_region,
+    dispatchable,
+    p_load=2664*1000/(365*24),
+    storage_capacities=5*250,
+    storage_max_loads=5*30,
+    storage_max_deliveries=5*30,
+    storage_efficiencies=0.67,
+    flexibility_power=17,
+    flexibility_time=8
+):
+
+    power = np.einsum('ijk,ik', potential, units_per_region)    
+
+    T = len(power)
+    tau = flexibility_time    
+
+    h = cp.Variable((T, tau+1))
+
+    constraints = [
+        h >= 0,
+        h <= 1,
+        h[:, 0] >= 1-flexibility_power/p_load,
+        cp.multiply(p_load, cp.sum(h, axis=1))-p_load <= flexibility_power,
+    ] + [
+        h[t, 0]+h[t-1, 1]+h[t-2, 2]+h[t-3, 3]+h[t-4, 4] +
+        h[t-5, 5]+h[t-6, 6]+h[t-7, 7]+h[t-8, 8] == 1
+        for t in np.arange(flexibility_time, T)
+    ]
+
+    prob = cp.Problem(
+        cp.Minimize(
+            cp.sum(cp.pos(cp.multiply(p_load, cp.sum(h, axis=1))-power))
+        ),
+        constraints
+    )
+
+    prob.solve(verbose=True, solver=cp.ECOS, max_iters=300)
+
+    hb = np.array(h.value)
+    p_load = p_load*np.sum(hb, axis=1)
+
+    power_delta = power-p_load
+    available_power = np.array(power_delta)
+    excess_power = np.maximum(0, available_power)
+    deficit_power = np.maximum(0, -available_power)
+
+    n_storages = len(storage_capacities)
+    storage_try_load = np.zeros((n_storages, T))
+    storage_try_delivery = np.zeros((n_storages, T))
+
+    storage = np.zeros((n_storages, T))
+    storage_impact = np.zeros((n_storages, T))
+    dE_storage = np.zeros((n_storages, T))
+
+    for i in range(n_storages):
+        storage_try_load[i] = np.minimum(excess_power, storage_max_loads[i])
+        storage_try_delivery[i] = np.minimum(deficit_power, storage_max_deliveries[i])
+
+        dE_storage[i] = storage_try_load[i]-storage_try_delivery[i]
+
+        storage[i] = storage_iterate(
+            dE_storage[i], storage_capacities[i], 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/storage_efficiencies[i], 1)
+        )
+
+        available_power += storage_impact[i]
+        excess_power = np.maximum(0, available_power)
+        deficit_power = np.maximum(0, -available_power)
+
+    # power missing to meet demand
+    gap = p_load-power-storage_impact.sum(axis=0)
+
+    # optimize dispatch
+    n_dispatchable_sources = dispatchable.shape[0]
+    dispatch_power = cp.Variable((n_dispatchable_sources, T))
+
+    constraints = [
+        dispatch_power >= 0,
+        cp.sum(dispatch_power, axis=0) <= np.maximum(gap, 0)
+    ] + [
+        dispatch_power[i,:] <= dispatchable[i,:,0].sum()
+        for i in range(n_dispatchable_sources)
+    ] + [
+        cp.sum(dispatch_power[i]) <= dispatchable[i,:,1].sum()
+        for i in range(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
+
+    gap -= dp.sum(axis=0)
+    S = np.maximum(gap, 0).mean()
+
+    return S, power, gap, storage, p_load, dp
+
+
+potential = pd.read_parquet("potential.parquet")
+potential = potential.loc['1985-01-01 00:00:00':'2015-01-01 00:00:00', :]
+potential.fillna(0, inplace=True)
+
+potential = potential.loc[(
+    slice(f'{args.begin} 00:00:00', f'{args.end} 00:00:00'), 'FR'), :]
+
+load = generate_load_curve(potential.index.get_level_values(0))
+
+p = potential[["onshore", "offshore", "solar"]].to_xarray().to_array()
+p = np.insert(p, 3, 0.68, axis=0)  # nuclear power-like
+# p[-1,:,0] = 0.68+0.25*np.cos(2*np.pi*np.arange(len(potential))/(365.25*24)) # nuclear seasonality ersatz
+
+dispatchable = np.zeros((3, 1, 2))
+
+# hydro power
+dispatchable[0][0][0] = 22
+dispatchable[0][0][1] = 63*1000*len(potential)/(365.25*24)
+
+# biomass
+dispatchable[1][0][0] = 2
+dispatchable[1][0][1] = 12*1000*len(potential)/(365.25*24)
+
+# thermique tradi
+dispatchable[2][0][0] = 0.5
+dispatchable[2][0][1] = 1e8
+
+n_sources = p.shape[0]
+n_dt = p.shape[1]
+n_regions = p.shape[2]
+
+# start = np.array([18, 0, 10, 61.4, 3])  # current mix
+# # stop = np.array([65, 40, 143, 0, 3]) # negawatt like mix
+# stop = np.array([74, 62, 208, 0, 3])  # RTE 100% EnR
+
+scenarios = {
+    'M0': np.array([74, 62, 208, 0]),
+    'M1': np.array([59, 45, 214, 16]),
+    'M23': np.array([72, 60, 125, 16]),
+    'N1': np.array([58, 45, 118, 16+13]),
+    'N2': np.array([52, 36, 90, 16+23]),
+    'N03': np.array([43, 22, 70, 24+27]),
+}
+
+BATTERY_CAPACITY=4 # 4 hour capacity
+STEP_CAPACITY=24
+P2G_CAPACITY=24*7*2
+
+battery_power = {
+    'M0': 26,
+    'M1': 21,
+    'M23': 13,
+    'N1': 9,
+    'N2': 2,
+    'N03': 1
+}
+
+p2g_power = {
+    'M0': 29,
+    'M1': 20,
+    'M23': 20,
+    'N1': 11,
+    'N2': 5,
+    'N03': 0
+}
+
+step_power = 8
+
+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)
+
+
+# for step in np.linspace(start, stop, 2050-2022, True)[::-1]:
+row = 0
+for scenario in scenarios:
+    units = np.transpose([scenarios[scenario]])
+
+    # 8 GW STEP capacity common to all scenarios
+    storage_power = battery_power[scenario] + p2g_power[scenario] + step_power
+    storage_max_load = battery_power[scenario]*BATTERY_CAPACITY + p2g_power[scenario]*24*7 + step_power*24
+
+    # nuclear_load_model(p[:,:3], units, load, 20)
+
+    if args.flexibility:
+        S, production, gap, storage, adjusted_load, dp = objective_with_storage_and_flexibility(
+            p, units,
+            dispatchable,
+            p_load=load,
+            storage_capacities=[battery_power[scenario]*BATTERY_CAPACITY, step_power*STEP_CAPACITY, p2g_power[scenario]*P2G_CAPACITY],
+            storage_max_deliveries=[battery_power[scenario], step_power, p2g_power[scenario]],
+            storage_max_loads=[battery_power[scenario], step_power, p2g_power[scenario]],
+            storage_efficiencies=[0.8, 0.8, 0.3]
+        )
+
+        print(f"{scenario}:", S, gap.max(), np.quantile(gap, 0.95))
+    else:
+        S, production, gap, storage, dp = objective_with_storages(
+            p,
+            units,
+            dispatchable,
+            p_load = load,
+            storage_capacities=[battery_power[scenario]*BATTERY_CAPACITY, step_power*STEP_CAPACITY, p2g_power[scenario]*P2G_CAPACITY],
+            storage_max_deliveries=[battery_power[scenario], step_power, p2g_power[scenario]],
+            storage_max_loads=[battery_power[scenario], step_power, p2g_power[scenario]],
+            storage_efficiencies=[0.8, 0.8, 0.3]
+        )
+        adjusted_load = load
+        print(f"{scenario} w/o flexibility:", 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['adjusted_load'] = adjusted_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-02-01 00:00:00',
+                       '2013-03-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 = [
+        "adjusted load (GW)",
+        "production (GW)",
+        "available power (production-storage) (GW)",
+        "power deficit"
+    ]
+
+    labels_storage = [
+        "Batterie (TWh)",
+        "STEP (TWh)",
+        "P2G (TWh)"
+    ]
+
+    labels_dispatch = [
+        "Hydraulique (GW)",
+        "Biomasse (GW)",
+        "Thermique gaz (GW)"
+    ]
+
+    for col in range(2):
+        ax = axes[row, col]
+        ax.plot(data[col].index.get_level_values(0), data[col]
+                ["adjusted_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]["adjusted_load"],
+            where=data[col]["adjusted_load"] > data[col]["available"],
+            color='red',
+            alpha=0.15
+        )
+
+        fmt = mdates.DateFormatter('%d/%m')
+        ax.xaxis.set_major_formatter(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(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(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 args.flexibility else "Without"
+
+plt.subplots_adjust(wspace=0, hspace=0)
+fig.suptitle(f"Simulations based on {args.begin}--{args.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 {args.begin}--{args.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 {args.begin}--{args.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()

+ 1 - 0
output.png

@@ -0,0 +1 @@
+.git/annex/objects/Q4/gx/MD5E-s718471--81f768fdd51b9afea5b8d0f49b740718.png/MD5E-s718471--81f768fdd51b9afea5b8d0f49b740718.png

+ 1 - 0
output_dispatch.png

@@ -0,0 +1 @@
+.git/annex/objects/J2/Ff/MD5E-s601016--7efdc3d552f22a5110d82b3fdab9d6cc.png/MD5E-s601016--7efdc3d552f22a5110d82b3fdab9d6cc.png

+ 1 - 0
output_storage.png

@@ -0,0 +1 @@
+.git/annex/objects/JK/65/MD5E-s307265--4ccb4b1ba75985d1d29ead84495904b7.png/MD5E-s307265--4ccb4b1ba75985d1d29ead84495904b7.png

+ 1 - 0
potential.parquet

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

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

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