|
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 * |
@@ -515,90 +517,91 @@ def post_solve(instance, outdir): |
515 | 517 | "state_of_charge", |
516 | 518 | title="State of Charge Throughout the Year", |
517 | 519 | supports_multi_scenario=True, |
| 520 | + note="The daily charge/discharge amount is calculated as" |
| 521 | + " the difference between the maximum and minimum" |
| 522 | + " state of charge in a 1-day rolling window.\n" |
| 523 | + "The black line is the 14-day rolling mean of the state of charge.", |
518 | 524 | ) |
519 | 525 | def graph_state_of_charge(tools): |
| 526 | + # Each panel is a period and scenario |
| 527 | + panel_group = ["period", "scenario_name"] |
| 528 | + rolling_mean_window_size = "14D" |
| 529 | + |
520 | 530 | # Get the total state of charge per timepoint and scenario |
521 | | - df = tools.get_dataframe("storage_dispatch") |
522 | | - df = df.groupby(["timepoint", "scenario_name"], as_index=False)[ |
523 | | - "StateOfCharge" |
524 | | - ].sum() |
| 531 | + soc = tools.get_dataframe("storage_dispatch.csv").rename( |
| 532 | + {"StateOfCharge": "value"}, axis=1 |
| 533 | + ) |
| 534 | + soc = soc.groupby(["timepoint", "scenario_name"], as_index=False).value.sum() |
| 535 | + # Convert values to TWh |
| 536 | + soc.value /= 1e6 |
525 | 537 | # Add datetime information |
526 | | - df = tools.transform.timestamp(df, key_col="timepoint") |
| 538 | + soc = tools.transform.timestamp(soc, key_col="timepoint")[ |
| 539 | + panel_group + ["datetime", "value"] |
| 540 | + ] |
527 | 541 | # Count num rows |
528 | | - num_periods = len(df["period"].unique()) |
| 542 | + num_periods = len(soc["period"].unique()) |
529 | 543 |
|
530 | | - # Get the total capacity per period and scenario |
531 | | - capacity = tools.get_dataframe("storage_capacity.csv") |
532 | | - capacity = capacity.groupby(["period", "scenario_name"], as_index=False)[ |
533 | | - "OnlineEnergyCapacityMWh" |
534 | | - ].sum() |
| 544 | + # Used later |
| 545 | + grouped_soc = soc.set_index("datetime").groupby(panel_group, as_index=False) |
535 | 546 |
|
536 | | - # Add the capacity to our dataframe |
537 | | - df = df.merge( |
538 | | - capacity, on=["period", "scenario_name"], validate="many_to_one", how="left" |
| 547 | + # Calculate the weekly SOC |
| 548 | + weekly_soc = ( |
| 549 | + grouped_soc.rolling(rolling_mean_window_size, center=True) |
| 550 | + .value.mean() |
| 551 | + .reset_index() |
539 | 552 | ) |
540 | 553 |
|
541 | | - # Convert values to TWh |
542 | | - df["StateOfCharge"] /= 1e6 |
543 | | - df["OnlineEnergyCapacityMWh"] /= 1e6 |
| 554 | + # Get the total capacity per period and scenario |
| 555 | + capacity = tools.get_dataframe("storage_capacity.csv") |
| 556 | + capacity = ( |
| 557 | + capacity.groupby(panel_group, as_index=False)["OnlineEnergyCapacityMWh"] |
| 558 | + .sum() |
| 559 | + .rename({"OnlineEnergyCapacityMWh": "value"}, axis=1) |
| 560 | + ) |
| 561 | + capacity.value /= 1e6 |
| 562 | + capacity["type"] = "Total Energy Capacity" |
544 | 563 |
|
545 | 564 | # Add information regarding the diurnal cycle to the dataframe |
546 | 565 | # Find the difference between the min and max for every day of the year |
547 | | - group = df.groupby( |
548 | | - ["period", "scenario_name", tools.pd.Grouper(freq="D", key="datetime")] |
549 | | - )["StateOfCharge"] |
550 | | - daily_size = (group.max() - group.min()).reset_index() |
551 | | - # Find the mean between the difference of the min and max |
| 566 | + group = grouped_soc.rolling("D", center=True).value |
552 | 567 | daily_size = ( |
553 | | - daily_size.groupby(["period", "scenario_name"], as_index=False) |
554 | | - .mean() |
555 | | - .reset_index() |
556 | | - ) |
557 | | - # Add the mean to the dataframe under the name DiurnalCycle |
558 | | - df = df.merge( |
559 | | - daily_size.rename({"StateOfCharge": "DiurnalCycle"}, axis=1), |
560 | | - on=["period", "scenario_name"], |
561 | | - how="left", |
| 568 | + (group.max() - group.min()).reset_index().groupby(panel_group, as_index=False) |
562 | 569 | ) |
| 570 | + # Find the mean between the difference of the min and max |
| 571 | + avg_daily_size = daily_size.mean()[panel_group + ["value"]] |
| 572 | + avg_daily_size["type"] = "Mean Daily Charge/Discharge" |
| 573 | + max_daily_size = daily_size.max()[panel_group + ["value"]] |
| 574 | + max_daily_size["type"] = "Maximum Daily Charge/Discharge" |
563 | 575 |
|
564 | 576 | # Determine information for the labels |
565 | | - y_axis_max = df["OnlineEnergyCapacityMWh"].max() |
566 | | - label_offset = y_axis_max * 0.05 |
567 | | - label_x_pos = df["datetime"].median() |
| 577 | + y_axis_max = capacity.value.max() |
| 578 | + label_x_pos = soc["datetime"].median() |
| 579 | + |
| 580 | + hlines = pd.concat([capacity, avg_daily_size, max_daily_size]) |
| 581 | + |
568 | 582 | # For the max label |
569 | | - df["label_position"] = df["OnlineEnergyCapacityMWh"] + label_offset |
570 | | - df["label"] = df["OnlineEnergyCapacityMWh"].round(decimals=2) |
571 | | - # For the diurnal cycle label |
572 | | - df["diurnal_label"] = df["DiurnalCycle"].round(decimals=2) |
573 | | - df["diurnal_label_pos"] = df["DiurnalCycle"] + label_offset |
| 583 | + hlines["label_pos"] = hlines.value + y_axis_max * 0.05 |
| 584 | + hlines["label"] = hlines.value.round(decimals=2) |
574 | 585 |
|
575 | 586 | # Plot with plotnine |
576 | 587 | pn = tools.pn |
577 | 588 | plot = ( |
578 | | - pn.ggplot(df, pn.aes(x="datetime", y="StateOfCharge")) |
579 | | - + pn.geom_line() |
| 589 | + pn.ggplot(soc, pn.aes(x="datetime", y="value")) |
| 590 | + + pn.geom_line(color="gray") |
| 591 | + + pn.geom_line(data=weekly_soc, color="black") |
580 | 592 | + pn.labs(y="State of Charge (TWh)", x="Time of Year") |
581 | 593 | + pn.geom_hline( |
582 | | - pn.aes(yintercept="OnlineEnergyCapacityMWh"), |
| 594 | + pn.aes(yintercept="value", label="label", color="type"), |
| 595 | + data=hlines, |
583 | 596 | linetype="dashed", |
584 | | - color="blue", |
585 | 597 | ) |
586 | 598 | + pn.geom_text( |
587 | | - pn.aes(label="label", x=label_x_pos, y="label_position"), |
| 599 | + pn.aes(label="label", x=label_x_pos, y="label_pos"), |
| 600 | + data=hlines, |
588 | 601 | fontweight="light", |
589 | 602 | size="10", |
590 | 603 | ) |
591 | | - + pn.geom_hline( |
592 | | - pn.aes(yintercept="DiurnalCycle"), linetype="dashed", color="red" |
593 | | - ) |
594 | | - + pn.geom_text( |
595 | | - pn.aes(label="diurnal_label", x=label_x_pos, y="diurnal_label_pos"), |
596 | | - fontweight="light", |
597 | | - size="10", |
598 | | - color="red", |
599 | | - ) |
600 | 604 | ) |
601 | | - |
602 | 605 | tools.save_figure(by_scenario_and_period(tools, plot, num_periods).draw()) |
603 | 606 |
|
604 | 607 |
|
|
0 commit comments