|
| 1 | +# %% IMPORTS AND SCENARIO DEFINITION |
| 2 | +import pandas as pd |
| 3 | +from matplotlib import pyplot as plt |
| 4 | +from matplotlib.colors import LinearSegmentedColormap |
| 5 | +from matplotlib.ticker import PercentFormatter |
| 6 | + |
| 7 | +from switch_model.tools.graph.main import GraphTools |
| 8 | +from papers.Martin_Staadecker_Value_of_LDES_and_Factors.LDES_paper_graphs.util import ( |
| 9 | + get_scenario, |
| 10 | + set_style, save_figure, |
| 11 | +) |
| 12 | + |
| 13 | +custom_color_map = LinearSegmentedColormap.from_list( |
| 14 | + "custom_color_map", ["#E0A4AA", "#B9646B", "#4D0409"] |
| 15 | +) |
| 16 | + |
| 17 | +# GET DATA FOR W/S RATIO |
| 18 | +STORAGE_BINS = (float("-inf"), 10, 20, float("inf")) |
| 19 | +STORAGE_LABELS = GraphTools.create_bin_labels(STORAGE_BINS) |
| 20 | +for i in range(len(STORAGE_LABELS)): |
| 21 | + STORAGE_LABELS[i] = STORAGE_LABELS[i] + "h Storage (GW)" |
| 22 | + |
| 23 | +# Define tools for wind to solar ratio set |
| 24 | +baseline_ws_ratio = 0.187 |
| 25 | +# Get Graph tools |
| 26 | +tools_ws_ratio = GraphTools( |
| 27 | + scenarios=[ |
| 28 | + get_scenario("WS10", 0.0909), |
| 29 | + get_scenario("WS018", 0.15), |
| 30 | + get_scenario("1342", baseline_ws_ratio), |
| 31 | + get_scenario("WS027", 0.21), |
| 32 | + get_scenario("WS033", 0.25), |
| 33 | + get_scenario("WS043", 0.3), |
| 34 | + get_scenario("WS066", 0.4), |
| 35 | + get_scenario("WS100", 0.5), |
| 36 | + get_scenario("WS150", 0.6), |
| 37 | + # get_scenario("WS233", 0.7), # Removed since results are invalid |
| 38 | + # get_scenario("WS500", 0.833), # Removed since results are misleading |
| 39 | + ], set_style=False |
| 40 | +) |
| 41 | +tools_ws_ratio.pre_graphing(multi_scenario=True) |
| 42 | + |
| 43 | +# Define tools for hydro set |
| 44 | +tools_hydro = GraphTools( |
| 45 | + scenarios=[ |
| 46 | + get_scenario("H25", 0), |
| 47 | + get_scenario("H025", 0.25), |
| 48 | + get_scenario("H050", 0.5), |
| 49 | + get_scenario("H065", 0.65), |
| 50 | + get_scenario("H085", 0.85), |
| 51 | + get_scenario("1342", 1), |
| 52 | + ], set_style=False |
| 53 | +) |
| 54 | +tools_hydro.pre_graphing(multi_scenario=True) |
| 55 | + |
| 56 | +# GET TX DATA |
| 57 | +tools_tx = GraphTools( |
| 58 | + scenarios=[ |
| 59 | + get_scenario("T4", "No Tx Build Costs\n(No Tx Congestion)"), |
| 60 | + get_scenario("1342", "Baseline"), |
| 61 | + get_scenario("T5", "10x Tx\nBuild Costs"), |
| 62 | + ], set_style=False |
| 63 | +) |
| 64 | +tools_tx.pre_graphing(multi_scenario=True) |
| 65 | + |
| 66 | +baseline_energy_cost = 22.43 |
| 67 | + |
| 68 | +# GET COST DATA |
| 69 | +tools_cost = GraphTools( |
| 70 | + scenarios=[ |
| 71 | + get_scenario("C21", 0.5), |
| 72 | + get_scenario("C18", 1), |
| 73 | + get_scenario("C22", 2), |
| 74 | + get_scenario("C23", 5), |
| 75 | + get_scenario("C26", 7), |
| 76 | + get_scenario("C17", 10), |
| 77 | + get_scenario("C24", 15), |
| 78 | + get_scenario("1342", baseline_energy_cost), |
| 79 | + get_scenario("C25", 40), |
| 80 | + get_scenario("C19", 70), |
| 81 | + get_scenario("C20", 102) |
| 82 | + ], set_style=False |
| 83 | +) |
| 84 | +tools_cost.pre_graphing(multi_scenario=True) |
| 85 | + |
| 86 | + |
| 87 | +def get_data(tools, normalize_to_baseline=None): |
| 88 | + storage = tools.get_dataframe("storage_capacity.csv") |
| 89 | + duration = storage.copy() |
| 90 | + duration = duration[duration["OnlinePowerCapacityMW"] != 0] |
| 91 | + duration["duration"] = ( |
| 92 | + duration["OnlineEnergyCapacityMWh"] / duration["OnlinePowerCapacityMW"] |
| 93 | + ) |
| 94 | + duration = duration[["scenario_index", "duration", "OnlinePowerCapacityMW"]] |
| 95 | + duration["Duration (h)"] = pd.cut( |
| 96 | + duration.duration, bins=STORAGE_BINS, labels=STORAGE_LABELS |
| 97 | + ) |
| 98 | + duration = duration.groupby( |
| 99 | + ["scenario_index", "Duration (h)"] |
| 100 | + ).OnlinePowerCapacityMW.sum() |
| 101 | + duration /= 10 ** 3 |
| 102 | + duration = duration.unstack() |
| 103 | + duration.index = duration.index.map(tools.get_scenario_name) |
| 104 | + |
| 105 | + storage = storage[["scenario_index", "OnlineEnergyCapacityMWh"]] |
| 106 | + storage = storage.groupby("scenario_index").sum() |
| 107 | + storage.index = storage.index.map(tools.get_scenario_name) |
| 108 | + storage *= 1e-6 |
| 109 | + storage.columns = ["Storage Energy Capacity (TWh)"] |
| 110 | + |
| 111 | + # Calculate transmission |
| 112 | + tx = tools.get_dataframe( |
| 113 | + "transmission.csv", |
| 114 | + usecols=["BuildTx", "trans_length_km", "scenario_index"], |
| 115 | + convert_dot_to_na=True, |
| 116 | + ).fillna(0) |
| 117 | + tx["BuildTx"] *= tx["trans_length_km"] |
| 118 | + tx["BuildTx"] *= 1e-6 |
| 119 | + tx = ( |
| 120 | + tx.groupby("scenario_index", as_index=False)["BuildTx"] |
| 121 | + .sum() |
| 122 | + .set_index("scenario_index") |
| 123 | + ) |
| 124 | + |
| 125 | + tx = tx.rename({"BuildTx": "New Tx"}, axis=1) |
| 126 | + tx.index = tx.index.map(tools.get_scenario_name) |
| 127 | + |
| 128 | + cap = tools.get_dataframe("gen_cap.csv") |
| 129 | + cap = tools.transform.gen_type(cap) |
| 130 | + cap = cap.groupby(["scenario_index", "gen_type"], as_index=False)[ |
| 131 | + "GenCapacity" |
| 132 | + ].sum() |
| 133 | + cap = cap.pivot(columns="gen_type", index="scenario_index", values="GenCapacity") |
| 134 | + cap *= 1e-3 # Convert to GW |
| 135 | + cap = cap.rename_axis("Technology", axis=1).rename_axis(None) |
| 136 | + cap = cap[["Wind", "Solar"]] |
| 137 | + cap.index = cap.index.map(tools.get_scenario_name) |
| 138 | + |
| 139 | + # Make it a percent change compared to the baseline |
| 140 | + if normalize_to_baseline is not None: |
| 141 | + tx = (tx / tx.loc[normalize_to_baseline]) |
| 142 | + cap = (cap / cap.loc[normalize_to_baseline]) |
| 143 | + storage = (storage / storage.loc[normalize_to_baseline]) |
| 144 | + duration = (duration / duration.sum(axis=1).loc[normalize_to_baseline]) |
| 145 | + |
| 146 | + return duration, tx, cap, storage |
| 147 | + |
| 148 | + |
| 149 | +# %% DEFINE FIGURE AND PLOTTING FUNCTIONS |
| 150 | +set_style() |
| 151 | +plt.close() |
| 152 | +fig = plt.figure() |
| 153 | +fig.set_size_inches(6.850394, 6.850394) |
| 154 | + |
| 155 | +# Define axes |
| 156 | +ax_tl = fig.add_subplot(2, 2, 1) |
| 157 | +ax_tr = fig.add_subplot(2, 2, 2, sharey=ax_tl) |
| 158 | +ax_bl = fig.add_subplot(2, 2, 3) |
| 159 | +ax_br = fig.add_subplot(2, 2, 4, sharey=ax_bl) |
| 160 | +Y_LIM_BASE = 1.85 |
| 161 | +ax_tl.set_ylim(0, Y_LIM_BASE) |
| 162 | +ax_bl.set_ylim(0, Y_LIM_BASE) |
| 163 | +ax_tl.yaxis.set_major_formatter(PercentFormatter(xmax=1)) |
| 164 | +ax_bl.yaxis.set_major_formatter(PercentFormatter(xmax=1)) |
| 165 | + |
| 166 | + |
| 167 | +# def create_secondary_y_axis(ax, include_label, y_lim, y_label, color="grey", offset=-0.25): |
| 168 | +# rax = ax.twinx() |
| 169 | +# rax.set_ylim(0, y_lim) |
| 170 | +# if include_label: |
| 171 | +# rax.spines["left"].set_position(("axes", offset)) |
| 172 | +# rax.yaxis.set_label_position("left") |
| 173 | +# rax.yaxis.tick_left() |
| 174 | +# rax.tick_params(top=False, bottom=False, right=False, left=True, which="both") |
| 175 | +# rax.spines["left"].set_color(color) |
| 176 | +# rax.set_ylabel(y_label) |
| 177 | +# else: |
| 178 | +# rax.tick_params(top=False, bottom=False, right=False, left=False, which="both") |
| 179 | +# rax.set_yticklabels([]) |
| 180 | +# return rax |
| 181 | + |
| 182 | + |
| 183 | +# y_lim = (Y_LIM_BASE) * 200 |
| 184 | +# y_label = "Storage Power Capacity (GW)" |
| 185 | +# c = "tab:red" |
| 186 | +# rax_top_left = create_secondary_y_axis(ax_tl, True, y_lim, y_label, c) |
| 187 | +# rax_top_right = create_secondary_y_axis(ax_tr, False, y_lim, y_label, c) |
| 188 | +# rax_bottom_left = create_secondary_y_axis(ax_bl, True, y_lim, y_label, c) |
| 189 | +# rax_bottom_right = create_secondary_y_axis(ax_br, False, y_lim, y_label, c) |
| 190 | + |
| 191 | + |
| 192 | +# y_lim = Y_LIM_BASE / 50 |
| 193 | +# y_label = "Energy Capacity (TWh)" |
| 194 | +# c = "green" |
| 195 | +# rrax_tl = create_secondary_y_axis(ax_tl, True, y_lim, y_label, c, offset=-0.4) |
| 196 | +# rrax_tr = create_secondary_y_axis(ax_tr, False, y_lim, y_label, c, offset=-0.4) |
| 197 | +# rrax_bl = create_secondary_y_axis(ax_bl, True, y_lim, y_label, c, offset=-0.4) |
| 198 | +# rrax_br = create_secondary_y_axis(ax_br, False, y_lim, y_label, c, offset=-0.4) |
| 199 | +# |
| 200 | +# y_lim = Y_LIM_BASE * 2 |
| 201 | +# y_label = "Solar Power Capacity (GW)" |
| 202 | +# c = tools_ws_ratio.get_colors()["Solar"] |
| 203 | +# rrrax_tl = create_secondary_y_axis(ax_tl, True, y_lim, y_label, c, offset=-0.6) |
| 204 | +# rrrax_tr = create_secondary_y_axis(ax_tr, False, y_lim, y_label, c, offset=-0.6) |
| 205 | +# rrrax_bl = create_secondary_y_axis(ax_bl, True, y_lim, y_label, c, offset=-0.6) |
| 206 | +# rrrax_br = create_secondary_y_axis(ax_br, False, y_lim, y_label, c, offset=-0.6) |
| 207 | + |
| 208 | + |
| 209 | +# %% DEFINE PLOTTING CODE |
| 210 | +def plot_panel(ax, rax, rrax, rrrax, data, title=""): |
| 211 | + duration, tx, cap, storage = data |
| 212 | + lw = 1 |
| 213 | + s = 3 |
| 214 | + colors = tools_ws_ratio.get_colors() |
| 215 | + duration.index.name = None |
| 216 | + storage.index.name = None |
| 217 | + duration.plot(ax=ax, colormap=custom_color_map, legend=False, kind="area", zorder=1.5, alpha=0.5, linewidth=0) |
| 218 | + # duration.sum(axis=1).plot(ax=ax, marker=".", color="red", label="All Storage (GW)", legend=False, |
| 219 | + # linewidth=lw, |
| 220 | + # markersize=s) |
| 221 | + ax.plot(tx, marker=".", color="tab:red", label="Built Transmission", linewidth=lw, markersize=s) |
| 222 | + cap["Wind"].plot(ax=ax, marker=".", color=colors, legend=False, linewidth=lw, markersize=s) |
| 223 | + cap["Solar"].plot(ax=ax, marker=".", color=colors, legend=False, linewidth=lw, markersize=s) |
| 224 | + storage.plot(ax=ax, marker=".", color="green", linewidth=lw, markersize=s, |
| 225 | + legend=False) |
| 226 | + ax.set_ylabel("Percent of baseline capacity") |
| 227 | + ax.set_title(title) |
| 228 | + |
| 229 | + |
| 230 | +# %% PLOT WIND TO SOLAR PENETRATION |
| 231 | + |
| 232 | +data_ws = get_data(tools_ws_ratio, normalize_to_baseline=baseline_ws_ratio) |
| 233 | + |
| 234 | +ax = ax_tl |
| 235 | +# rax = rax_top_left |
| 236 | +ax.tick_params(top=False, bottom=True, right=False, left=True, which="both") |
| 237 | + |
| 238 | +plot_panel(ax, None, None, None, data_ws, "Set A: Varying Wind-vs-Solar Share") |
| 239 | + |
| 240 | +ax.set_xticks([0.1, 0.2, 0.3, 0.4, 0.5, 0.6]) |
| 241 | +ax.set_xticklabels(["90%-10%\nSolar-Wind", "", "70%-30%\nSolar-Wind", "", "50%-50%\nSolar-Wind", ""]) |
| 242 | +ax.axvline(baseline_ws_ratio, linestyle="dotted", color="dimgrey") |
| 243 | +ax.text(baseline_ws_ratio - 0.02, 0.1, "Baseline", rotation=90, color="dimgrey") |
| 244 | + |
| 245 | +fig.legend(loc="lower center", ncol=4) |
| 246 | + |
| 247 | +# %% PLOT HYDRO |
| 248 | +data_hy = get_data(tools_hydro, normalize_to_baseline=1) |
| 249 | +ax = ax_tr |
| 250 | +# rax = rax_top_right |
| 251 | +plot_panel(ax, None, None, None, data_hy, "Set B: Reducing Hydropower Generation") |
| 252 | +ax.tick_params(top=False, bottom=True, right=False, left=False, which="both") |
| 253 | +ax.set_xticks([0, 0.5, 1]) |
| 254 | +ax.set_xticks([0.1, 0.2, 0.3, 0.4, 0.6, 0.7, 0.8, 0.9], minor=True) |
| 255 | +ax.set_xticklabels(["No\nhydropower", "50%\nhydropower", "Baseline\nHydropower"]) |
| 256 | + |
| 257 | +# %% PLOT TX |
| 258 | +ax = ax_bl |
| 259 | +# rax = rax_bottom_left |
| 260 | +data_tx = get_data(tools_tx, normalize_to_baseline="Baseline") |
| 261 | +ax.set_xticks([0, 1, 2]) |
| 262 | +plot_panel(ax, None, None, None, data_tx, "Set C: Varying Transmission Build Costs") |
| 263 | +# %% PLOT COSTS |
| 264 | +ax = ax_br |
| 265 | +# rax = rax_bottom_right |
| 266 | +data_cost = get_data(tools_cost, normalize_to_baseline=baseline_energy_cost) |
| 267 | +plot_panel(ax, None, None, None, data_cost, "Set D: Varying Storage Energy Costs") |
| 268 | +ax.set_xscale("log") |
| 269 | +ax.tick_params(top=False, bottom=True, right=False, left=False, which="both") |
| 270 | +ax.set_xticks([1, 10, 100]) |
| 271 | +ax.set_xticks( |
| 272 | + [ |
| 273 | + 0.5, |
| 274 | + 0.6, |
| 275 | + 0.7, |
| 276 | + 0.8, |
| 277 | + 0.9, |
| 278 | + 1, |
| 279 | + 2, |
| 280 | + 3, |
| 281 | + 4, |
| 282 | + 5, |
| 283 | + 6, |
| 284 | + 7, |
| 285 | + 8, |
| 286 | + 9, |
| 287 | + 10, |
| 288 | + 20, |
| 289 | + 30, |
| 290 | + 40, |
| 291 | + 50, |
| 292 | + 60, |
| 293 | + 70, |
| 294 | + 80, |
| 295 | + 90, |
| 296 | + ], |
| 297 | + minor=True, |
| 298 | +) |
| 299 | +ax.set_xticklabels(["1\n$/kWh", "10\n$/kWh", "100\n$/kWh"]) |
| 300 | +ax.set_xlabel("(log scale)") |
| 301 | +ax.axvline(baseline_energy_cost, linestyle="dotted", color="dimgrey") |
| 302 | +ax.text(baseline_energy_cost - 4, 0.1, "Baseline", rotation=90, color="dimgrey") |
| 303 | + |
| 304 | +plt.subplots_adjust(left=0.08, right=0.97, top=0.95, wspace=0.05) |
| 305 | + |
| 306 | +# %% SAVE FIGURE |
| 307 | +save_figure("figure-2-analysis-of-4-factors.png") |
| 308 | +# %% MAX POWER DURATION |
| 309 | +df = tools_ws_ratio.get_dataframe("storage_capacity.csv") |
| 310 | +df = df[df.scenario_name == 0.833] |
| 311 | +df["duration"] = df["duration"] = ( |
| 312 | + df["OnlineEnergyCapacityMWh"] / df["OnlinePowerCapacityMW"] |
| 313 | +) |
| 314 | +df = df[["load_zone", "duration", "OnlinePowerCapacityMW"]] |
| 315 | +df.sort_values("duration") |
| 316 | +# %% HYDRO POWER impact |
| 317 | +en = data_hy[3].copy() * 1000 |
| 318 | +pw = data_hy[0].sum(axis=1).copy() |
| 319 | +pw, en |
| 320 | + |
| 321 | +# %% HYDRO power total capacity |
| 322 | +df = tools_hydro.get_dataframe("dispatch_annual_summary.csv") |
| 323 | +scenario = 1 |
| 324 | +# scenario = 0.5 |
| 325 | +df = df[df.scenario_name == scenario] |
| 326 | +df = tools_hydro.transform.gen_type(df) |
| 327 | +df = df[df.gen_type != "Storage"] |
| 328 | +df = df.groupby("gen_type").Energy_GWh_typical_yr.sum() |
| 329 | +df *= 1e-3 |
| 330 | +df |
| 331 | +df.loc["Hydro"] / df.sum() |
| 332 | +# %% |
| 333 | +data_tx |
| 334 | +# %% Storage duration in 50% hydro |
| 335 | +df = tools_hydro.get_dataframe("dispatch_zonal_annual_summary.csv") |
| 336 | +df = df[df.scenario_name == 1] |
| 337 | +df = tools_hydro.transform.gen_type(df) |
| 338 | +df = df[["gen_load_zone", "gen_type", "Energy_GWh_typical_yr"]].set_index("gen_load_zone") |
| 339 | +df_sum = df.groupby("gen_load_zone").Energy_GWh_typical_yr.sum() |
| 340 | +df_sum = df_sum.rename("total") |
| 341 | +df = df.join(df_sum) |
| 342 | +cutoff = 0.5 |
| 343 | +df["percent"] = df["Energy_GWh_typical_yr"] / df["total"] |
| 344 | +df = df[["percent", "gen_type"]].reset_index() |
| 345 | +df = df[df.gen_type == "Hydro"] |
| 346 | +df = df[df.percent > cutoff] |
| 347 | +valid_load_zones = df["gen_load_zone"] |
| 348 | + |
| 349 | +df = tools_hydro.get_dataframe("storage_capacity.csv") |
| 350 | +# df = df[df.scenario_name == 0.5] |
| 351 | +df = df[["load_zone", "scenario_name", "OnlinePowerCapacityMW", "OnlineEnergyCapacityMWh"]] |
| 352 | +df = df[df.load_zone.isin(valid_load_zones)] |
| 353 | +df = df.groupby("scenario_name").sum() |
| 354 | +df["OnlineEnergyCapacityMWh"] / df["OnlinePowerCapacityMW"] |
| 355 | +# valid_load_zones |
| 356 | + |
| 357 | +# %% TX Change |
| 358 | +df = data_tx[2] |
| 359 | +df |
| 360 | +# %% COSTS |
| 361 | +df = data_cost |
| 362 | +table = 1 |
| 363 | +df[table], (data_cost[table] / data_cost[table].loc[22.43] - 1) * 100 |
| 364 | +# %% |
| 365 | +df = tools_cost.get_dataframe("storage_capacity.csv") |
| 366 | +df["duration"] = df["OnlineEnergyCapacityMWh"] / df["OnlinePowerCapacityMW"] |
| 367 | +df = df.groupby("scenario_name").duration.max() |
| 368 | +df |
| 369 | +# %% |
| 370 | +df = tools_cost.get_dataframe("storage_capacity.csv") |
| 371 | +df["duration"] = df["OnlineEnergyCapacityMWh"] / df["OnlinePowerCapacityMW"] |
| 372 | +total_power = df.groupby("scenario_name").OnlinePowerCapacityMW.sum() |
| 373 | +total_energy = df.groupby("scenario_name").OnlineEnergyCapacityMWh.sum() |
| 374 | +total_energy / total_power |
0 commit comments