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