@@ -619,7 +619,6 @@ def graph_hourly_curtailment(tools):
619619@graph (
620620 "total_dispatch" ,
621621 title = "Total dispatched electricity" ,
622- is_long = True ,
623622)
624623def graph_total_dispatch (tools ):
625624 # ---------------------------------- #
@@ -659,6 +658,134 @@ def graph_total_dispatch(tools):
659658 ylabel = "Total dispatched electricity (TWh)"
660659 )
661660
661+ tools .bar_label ()
662+
663+ @graph (
664+ "energy_balance" ,
665+ title = "Energy Balance For Every Month" ,
666+ supports_multi_scenario = True ,
667+ is_long = True
668+ )
669+ def energy_balance (tools ):
670+ # Get dispatch dataframe
671+ cols = ["timestamp" , "gen_tech" , "gen_energy_source" , "DispatchGen_MW" , "scenario_name" , "scenario_index" ,
672+ "Curtailment_MW" ]
673+ df = tools .get_dataframe ("dispatch.csv" , drop_scenario_info = False )[cols ]
674+ df = tools .transform .gen_type (df )
675+
676+ # Rename and add needed columns
677+ df ["Dispatch Limit" ] = df ["DispatchGen_MW" ] + df ["Curtailment_MW" ]
678+ df = df .drop ("Curtailment_MW" , axis = 1 )
679+ df = df .rename ({"DispatchGen_MW" : "Dispatch" }, axis = 1 )
680+ # Sum dispatch across all the projects of the same type and timepoint
681+ key_columns = ["timestamp" , "gen_type" , "scenario_name" , "scenario_index" ]
682+ df = df .groupby (key_columns , as_index = False ).sum ()
683+ df = df .melt (id_vars = key_columns , value_vars = ["Dispatch" , "Dispatch Limit" ], var_name = "Type" )
684+ df = df .rename ({"gen_type" : "Source" }, axis = 1 )
685+
686+ discharge = df [(df ["Source" ] == "Storage" ) & (df ["Type" ] == "Dispatch" )].drop (["Source" , "Type" ], axis = 1 ).rename (
687+ {"value" : "discharge" }, axis = 1 )
688+
689+ # Get load dataframe
690+ load = tools .get_dataframe ("load_balance.csv" , drop_scenario_info = False )
691+ load = load .drop ("normalized_energy_balance_duals_dollar_per_mwh" , axis = 1 )
692+
693+ # Sum load across all the load zones
694+ key_columns = ["timestamp" , "scenario_name" , "scenario_index" ]
695+ load = load .groupby (key_columns , as_index = False ).sum ()
696+
697+ # Subtract storage dispatch from generation and add it to the storage charge to get net flow
698+ load = load .merge (
699+ discharge ,
700+ how = "left" ,
701+ on = key_columns ,
702+ validate = "one_to_one"
703+ )
704+ load ["ZoneTotalCentralDispatch" ] -= load ["discharge" ]
705+ load ["StorageNetCharge" ] += load ["discharge" ]
706+ load = load .drop ("discharge" , axis = 1 )
707+
708+ # Rename and convert from wide to long format
709+ load = load .rename ({
710+ "ZoneTotalCentralDispatch" : "Total Generation (excl. storage discharge)" ,
711+ "TXPowerNet" : "Transmission Losses" ,
712+ "StorageNetCharge" : "Storage Net Flow" ,
713+ "zone_demand_mw" : "Demand" ,
714+ }, axis = 1 ).sort_index (axis = 1 )
715+ load = load .melt (id_vars = key_columns , var_name = "Source" )
716+ load ["Type" ] = "Dispatch"
717+
718+ # Merge dispatch contributions with load contributions
719+ df = pd .concat ([load , df ])
720+
721+ # Add the timestamp information and make period string to ensure it doesn't mess up the graphing
722+ df = tools .transform .timestamp (df ).astype ({"period" : str })
723+
724+ # Convert to TWh (incl. multiply by timepoint duration)
725+ df ["value" ] *= df ["tp_duration" ] / 1e6
726+
727+ FREQUENCY = "1W"
728+
729+ def groupby_time (df ):
730+ return df .groupby ([
731+ "scenario_name" ,
732+ "period" ,
733+ "Source" ,
734+ "Type" ,
735+ tools .pd .Grouper (key = "datetime" , freq = FREQUENCY , origin = "start" )
736+ ])["value" ]
737+
738+ df = groupby_time (df ).sum ().reset_index ()
739+
740+ # Get the state of charge data
741+ soc = tools .get_dataframe ("StateOfCharge.csv" , dtype = {"STORAGE_GEN_TPS_1" : str }, drop_scenario_info = False )
742+ soc = soc .rename ({"STORAGE_GEN_TPS_2" : "timepoint" , "StateOfCharge" : "value" }, axis = 1 )
743+ # Sum over all the projects that are in the same scenario with the same timepoint
744+ soc = soc .groupby (["timepoint" , "scenario_name" ], as_index = False ).sum ()
745+ soc ["Source" ] = "State Of Charge"
746+ soc ["value" ] /= 1e6 # Convert to TWh
747+
748+ # Group by time
749+ soc = tools .transform .timestamp (soc , use_timepoint = True , key_col = "timepoint" ).astype ({"period" : str })
750+ soc ["Type" ] = "Dispatch"
751+ soc = groupby_time (soc ).mean ().reset_index ()
752+
753+ # Add state of charge to dataframe
754+ df = pd .concat ([df , soc ])
755+ # Add column for day since that's what we really care about
756+ df ["day" ] = df ["datetime" ].dt .dayofyear
757+
758+ # Plot
759+ # Get the colors for the lines
760+ colors = tools .get_colors ()
761+ colors .update ({
762+ "Transmission Losses" : "brown" ,
763+ "Storage Net Flow" : "cadetblue" ,
764+ "Demand" : "black" ,
765+ "Total Generation (excl. storage discharge)" : "black" ,
766+ "State Of Charge" : "green"
767+ })
768+
769+ # plot
770+ num_periods = df ["period" ].nunique ()
771+ pn = tools .pn
772+ plot = pn .ggplot (df ) + \
773+ pn .geom_line (pn .aes (x = "day" , y = "value" , color = "Source" , linetype = "Type" )) + \
774+ pn .facet_grid ("period ~ scenario_name" ) + \
775+ pn .labs (y = "Contribution to Energy Balance (TWh)" ) + \
776+ pn .scales .scale_color_manual (values = colors , aesthetics = "color" , na_value = colors ["Other" ]) + \
777+ pn .scales .scale_x_continuous (
778+ name = "Month" ,
779+ labels = ["J" , "F" , "M" , "A" , "M" , "J" , "J" , "A" , "S" , "O" , "N" , "D" ],
780+ breaks = (15 , 46 , 76 , 106 , 137 , 167 , 198 , 228 , 259 , 289 , 319 , 350 ),
781+ limits = (0 , 366 )) + \
782+ pn .scales .scale_linetype_manual (
783+ values = {"Dispatch Limit" : "dotted" , "Dispatch" : "solid" }
784+ ) + \
785+ pn .theme (
786+ figure_size = (pn .options .figure_size [0 ] * tools .num_scenarios , pn .options .figure_size [1 ] * num_periods ))
787+
788+ tools .save_figure (plot .draw ())
662789
663790@graph (
664791 "curtailment_per_period" ,
0 commit comments