|
7 | 7 | storage, when to charge, energy accounting, etc. |
8 | 8 | """ |
9 | 9 | import math |
| 10 | + |
| 11 | +import pandas as pd |
10 | 12 | from scipy import fft |
11 | 13 |
|
12 | 14 | from pyomo.environ import * |
@@ -444,68 +446,72 @@ def post_solve(instance, outdir): |
444 | 446 | @graph( |
445 | 447 | "state_of_charge", |
446 | 448 | title="State of Charge Throughout the Year", |
447 | | - supports_multi_scenario=True |
| 449 | + supports_multi_scenario=True, |
| 450 | + note="The daily charge/discharge amount is calculated as" |
| 451 | + " the difference between the maximum and minimum" |
| 452 | + " state of charge in a 1-day rolling window.\n" |
| 453 | + "The black line is the 14-day rolling mean of the state of charge." |
448 | 454 | ) |
449 | 455 | def graph_state_of_charge(tools): |
| 456 | + # Each panel is a period and scenario |
| 457 | + panel_group = ["period", "scenario_name"] |
| 458 | + rolling_mean_window_size = '14D' |
| 459 | + |
450 | 460 | # Get the total state of charge per timepoint and scenario |
451 | | - df = tools.get_dataframe("storage_dispatch") |
452 | | - df = df.groupby(["timepoint", "scenario_name"], as_index=False)["StateOfCharge"].sum() |
| 461 | + soc = tools.get_dataframe("storage_dispatch.csv").rename({"StateOfCharge": "value"}, axis=1) |
| 462 | + soc = soc.groupby(["timepoint", "scenario_name"], as_index=False).value.sum() |
| 463 | + # Convert values to TWh |
| 464 | + soc.value /= 1e6 |
453 | 465 | # Add datetime information |
454 | | - df = tools.transform.timestamp(df, key_col="timepoint") |
| 466 | + soc = tools.transform.timestamp(soc, key_col="timepoint")[panel_group + ["datetime", "value"]] |
455 | 467 | # Count num rows |
456 | | - num_periods = len(df["period"].unique()) |
| 468 | + num_periods = len(soc["period"].unique()) |
457 | 469 |
|
458 | | - # Get the total capacity per period and scenario |
459 | | - capacity = tools.get_dataframe("storage_capacity.csv") |
460 | | - capacity = capacity.groupby(["period", "scenario_name"], as_index=False)["OnlineEnergyCapacityMWh"].sum() |
| 470 | + # Used later |
| 471 | + grouped_soc = soc.set_index("datetime").groupby(panel_group, as_index=False) |
461 | 472 |
|
462 | | - # Add the capacity to our dataframe |
463 | | - df = df.merge( |
464 | | - capacity, |
465 | | - on=["period", "scenario_name"], |
466 | | - validate="many_to_one", |
467 | | - how="left" |
468 | | - ) |
| 473 | + # Calculate the weekly SOC |
| 474 | + weekly_soc = grouped_soc.rolling(rolling_mean_window_size, center=True) \ |
| 475 | + .value \ |
| 476 | + .mean() \ |
| 477 | + .reset_index() |
469 | 478 |
|
470 | | - # Convert values to TWh |
471 | | - df["StateOfCharge"] /= 1e6 |
472 | | - df["OnlineEnergyCapacityMWh"] /= 1e6 |
| 479 | + # Get the total capacity per period and scenario |
| 480 | + capacity = tools.get_dataframe("storage_capacity.csv") |
| 481 | + capacity = capacity.groupby(panel_group, as_index=False)["OnlineEnergyCapacityMWh"]\ |
| 482 | + .sum()\ |
| 483 | + .rename({"OnlineEnergyCapacityMWh": "value"}, axis=1) |
| 484 | + capacity.value /= 1e6 |
| 485 | + capacity["type"] = "Total Energy Capacity" |
473 | 486 |
|
474 | 487 | # Add information regarding the diurnal cycle to the dataframe |
475 | 488 | # Find the difference between the min and max for every day of the year |
476 | | - group = df.groupby(["period", "scenario_name", tools.pd.Grouper(freq="D", key="datetime")])["StateOfCharge"] |
477 | | - daily_size = (group.max() - group.min()).reset_index() |
| 489 | + group = grouped_soc.rolling('D', center=True).value |
| 490 | + daily_size = (group.max() - group.min()).reset_index().groupby(panel_group, as_index=False) |
478 | 491 | # Find the mean between the difference of the min and max |
479 | | - daily_size = daily_size.groupby(["period", "scenario_name"], as_index=False).mean().reset_index() |
480 | | - # Add the mean to the dataframe under the name DiurnalCycle |
481 | | - df = df.merge( |
482 | | - daily_size.rename({"StateOfCharge": "DiurnalCycle"}, axis=1), |
483 | | - on=["period", "scenario_name"], |
484 | | - how="left" |
485 | | - ) |
| 492 | + avg_daily_size = daily_size.mean()[panel_group + ["value"]] |
| 493 | + avg_daily_size["type"] = "Mean Daily Charge/Discharge" |
| 494 | + max_daily_size = daily_size.max()[panel_group + ["value"]] |
| 495 | + max_daily_size["type"] = "Maximum Daily Charge/Discharge" |
486 | 496 |
|
487 | 497 | # Determine information for the labels |
488 | | - y_axis_max = df["OnlineEnergyCapacityMWh"].max() |
489 | | - label_offset = y_axis_max * 0.05 |
490 | | - label_x_pos = df["datetime"].median() |
| 498 | + y_axis_max = capacity.value.max() |
| 499 | + label_x_pos = soc["datetime"].median() |
| 500 | + |
| 501 | + hlines = pd.concat([capacity, avg_daily_size, max_daily_size]) |
| 502 | + |
491 | 503 | # For the max label |
492 | | - df["label_position"] = df["OnlineEnergyCapacityMWh"] + label_offset |
493 | | - df["label"] = df["OnlineEnergyCapacityMWh"].round(decimals=2) |
494 | | - # For the diurnal cycle label |
495 | | - df["diurnal_label"] = df["DiurnalCycle"].round(decimals=2) |
496 | | - df["diurnal_label_pos"] = df["DiurnalCycle"] + label_offset |
| 504 | + hlines["label_pos"] = hlines.value + y_axis_max * 0.05 |
| 505 | + hlines["label"] = hlines.value.round(decimals=2) |
497 | 506 |
|
498 | 507 | # Plot with plotnine |
499 | 508 | pn = tools.pn |
500 | | - plot = pn.ggplot(df, pn.aes(x="datetime", y="StateOfCharge")) \ |
501 | | - + pn.geom_line() \ |
| 509 | + plot = pn.ggplot(soc, pn.aes(x="datetime", y="value")) \ |
| 510 | + + pn.geom_line(color='gray') \ |
| 511 | + + pn.geom_line(data=weekly_soc, color='black') \ |
502 | 512 | + pn.labs(y="State of Charge (TWh)", x="Time of Year") \ |
503 | | - + pn.geom_hline(pn.aes(yintercept="OnlineEnergyCapacityMWh"), linetype="dashed", color='blue') \ |
504 | | - + pn.geom_text(pn.aes(label="label", x=label_x_pos, y="label_position"), fontweight="light", size="10") \ |
505 | | - + pn.geom_hline(pn.aes(yintercept="DiurnalCycle"), linetype="dashed", color='red') \ |
506 | | - + pn.geom_text(pn.aes(label="diurnal_label", x=label_x_pos, y="diurnal_label_pos"), fontweight="light", size="10", color="red") |
507 | | - |
508 | | - |
| 513 | + + pn.geom_hline(pn.aes(yintercept="value", label="label", color="type"), data=hlines, linetype="dashed") \ |
| 514 | + + pn.geom_text(pn.aes(label="label", x=label_x_pos, y="label_pos"), data=hlines, fontweight="light", size="10") |
509 | 515 | tools.save_figure(by_scenario_and_period(tools, plot, num_periods).draw()) |
510 | 516 |
|
511 | 517 |
|
|
0 commit comments