Simulating Scenarios of future U.S. Electrical Grids

Benjamin Rouillé-d'Orfeuil
Singularity
Published in
13 min readMay 14, 2024

--

Back when I was working at Breakthrough Energy, my colleague and I developed a Python software ecosystem to simulate scenarios of future grids, featuring grid modifications intended to evaluate various policy choices. The purpose of this story is to outline the main features of this project and to provide a quick start guide to use this simulation platform.

Let’s start with a brief overview of the U.S. power system model at the core of the project.

Network Model Overview

A detailed description of the source data that were used to develop
the model of the U.S. power system along with the corresponding synchronized weather and demand profiles can be found here. In short, the network has 82,000 nodes and over 104,000 branches and hourly resolution profiles for both electric demand and generation potential from wind, solar, and hydro resources. The transmission lines connect 14,000 generators to 38,000 demand buses.

This grid model is representative of real patterns of geography and network topology and was updated to reflect the generation capacities and power flow patterns of the year 2016.

Simulation Engine

The network model and hourly profiles are fed into a production cost
model and run as a series of multi-period optimal power flow
problems, using the Direct Current power flow approximation. The initial conditions of each problem are constrained by the final conditions of the preceding problem, enabling temporal constraints such as generator ramp rate limits to bridge the individual optimization problems. The formulation of the optimization problem is given here.

Scenario Framework

Main Features

Below are a few things that the software ecosystem can do:

  • Provide a flexible modeling tool to create complex scenarios
  • Perform investment cost studies
  • Run power flow study using interface to external simulation engine
  • Manage data throughout the lifecycle of a simulation
  • Analyze and plot outputs of scenarios

Installation

The easiest way to use our tool is through plug, a workflow for running a standalone installation of our software on a single computer. To use it, you need to have docker installed.

Clone the repository from GitHub to your local computer using:

git clone https://github.com/Breakthrough-Energy/plug.git

Then, run cd standalone in your shell from the root of the repository, followed by docker-compose up. You can pass -d to run in the background. The docker images will be downloaded automatically from the container registry.

The client container contains both the powersimdata and postreisePython packages, for scenario management and analysis, respectively. The default compose file starts the client running bash, which serves as an entry point to either an ipython shell:

docker-compose exec client ipython

or a jupyter notebook:

docker-compose exec client jupyter lab --port=10000 --no-browser --ip=0.0.0.0 --allow-root

Creating a Scenario

Several resources are available to help you use the software. Check the PowerSimData tutorial for an overview of the framework, or try the commands in demo_*.py located in the standalone directory for a simple example.

Let’s create one.

>>> from powersimdata import Scenario
>>>
>>> scenario = Scenario()
>>> print(scenario.state.name)
create
>>>
>>> scenario.set_grid(interconnect="Western")
Transferring ScenarioList.csv from local_fs
ScenarioList.csv.2 not found on local machine
--> Begin: Existing Study
Nothing yet
<-- End: Existing Study
--> Begin: Available profiles
demand: vJan2021
hydro: vJan2021
solar: vJan2021
wind: vJan2021
<-- End: Available profiles
Reading bus.csv
Reading plant.csv
Reading gencost.csv
Reading branch.csv
Reading dcline.csv
Reading sub.csv
Reading bus2sub.csv
Reading zone.csv

As a first time user, you don’t have a ScenarioList.csv file yet. It will be generated once you create your first scenario. For the same reason, no study is displayed. A list of available profiles is then printed out. Profiles are generated when updates to the base grid are made. Last update happens in January 2021.

We create a scenario in the Western interconnection. All the necessary files are loaded to build a Grid object. You can access, this object and its attributes as follows:

>>> base_grid = scenario.get_grid()
>>> base_grid.plant
bus_id Pg Qg Qmax Qmin Vg ... GenIOC GenIOD zone_id zone_name lat lon
plant_id ...
10390 2010683 0.00 0.00 61.16 -7.8 1.0464 ... 0.0 0 201 Washington 46.6451 -119.908
10391 2010684 85.29 56.74 61.16 -7.8 1.0201 ... 0.0 0 201 Washington 46.6451 -119.908
10392 2010685 138.65 56.74 61.16 -7.8 1.0201 ... 0.0 0 201 Washington 46.6451 -119.908
10393 2010686 0.00 0.00 61.16 -7.8 1.0464 ... 0.0 0 201 Washington 46.6451 -119.908
10394 2010687 120.63 56.74 61.16 -7.8 1.0188 ... 0.0 0 201 Washington 46.6451 -119.908
... ... ... ... ... ... ... ... ... ... ... ... ... ...
14015 2025835 0.00 0.00 0.00 0.0 1.0000 ... 0.0 0 206 Southwest California 35.0609 -118.292
14016 2077044 0.00 0.00 0.00 0.0 1.0000 ... 0.0 0 215 Montana Western 45.8849 -109.888
14017 2060759 0.00 0.00 0.00 0.0 1.0000 ... 0.0 0 212 Colorado 37.7033 -102.623
14018 2060594 0.00 0.00 0.00 0.0 1.0000 ... 0.0 0 212 Colorado 37.8648 -104.109
14019 2053000 0.00 0.00 0.00 0.0 1.0000 ... 0.0 0 211 New Mexico Western 35.5753 -104.898

[2616 rows x 35 columns]
>>> base_grid.sub
name interconnect_sub_id lat lon interconnect
sub_id
35000 NEAH BAY 10000 48.241400 -124.578000 Western
35001 FORKS 10001 47.695600 -124.184000 Western
35002 OCEAN SHORES 10002 47.040000 -124.057000 Western
35003 WESTPORT 10003 46.927500 -124.172000 Western
35004 LONG BEACH 1 10004 46.316500 -124.061000 Western
... ... ... ... ... ...
41078 OREGON PORTLAND 81019 45.435890 -124.324466 Western
41079 WASHINGTON RAYMOND 81020 46.576647 -124.349185 Western
41080 WASHINGTON OCEAN SHORES 81021 46.979165 -124.478275 Western
41081 WASHINGTON QUEETS 81022 47.534671 -124.655429 Western
41082 WASHINGTON OZETTE 81023 48.168686 -125.050937 Western

[4786 rows x 5 columns]
>>> base_grid.dcline
from_bus_id to_bus_id status Pf Pt Qf Qt Vf Vt ... loss1 muPmin muPmax muQminF muQmaxF muQminT muQmaxT from_interconnect to_interconnect
dcline_id ...
9 2050383 2028847 1 2400.0 2300.0 0 0 1 1 ... 0 0 0 0 0 0 0 Western Western
10 2013620 2025332 1 3100.0 3000.0 0 0 1 1 ... 0 0 0 0 0 0 0 Western Western
15 2021181 2021641 1 400.0 375.0 0 0 1 1 ... 0 0 0 0 0 0 0 Western Western

[3 rows x 25 columns]
>>> base_grid.id2zone
{201: 'Washington', 202: 'Oregon', 203: 'Northern California', 204: 'Bay Area', 205: 'Central California', 206: 'Southwest California', 207: 'Southeast California', 208: 'Nevada', 209: 'Arizona', 210: 'Utah', 211: 'New Mexico Western', 212: 'Colorado', 213: 'Wyoming', 214: 'Idaho', 215: 'Montana Western', 216: 'El Paso'}

The various attributes of the Grid object can be found in the PowerSimData tutorial. We can now set the scenario parameters (name, start and end dates along with interval, profiles version) and change the base grid.

>>> # set plan and scenario names
>>> scenario.set_name("test", "medium_blog_post")
>>> # set start date, end date and interval
>>> scenario.set_time("2016-08-01 00:00:00", "2016-08-02 23:00:00", "24H")

>>> # set demand profile version
>>> scenario.set_base_profile("demand", "vJan2021")
>>> # set hydro profile version
>>> scenario.set_base_profile("hydro", "vJan2021")
>>> # set solar profile version
>>> scenario.set_base_profile("solar", "vJan2021")
>>> # set wind profile version
>>> scenario.set_base_profile("wind", "vJan2021")
>>>
>>> # scale capacity of solar plants in WA and AZ by 5 and 2.5, respectively
>>> scenario.change_table.scale_plant_capacity(
... "solar", zone_name={"Washington": 5, "Arizona": 2.5}
... )
>>> # scale capacity of wind farms in OR and MT by 1.5 and 2, respectively
>>> scenario.change_table.scale_plant_capacity(
... "wind", zone_name={"Oregon": 1.5, "Montana Western": 2}
... )
>>> # scale capacity of branches in NV and WY by 2
>>> scenario.change_table.scale_branch_capacity(zone_name={"Nevada": 2, "Wyoming": 2})
>>>
>>> # add AC lines in NM and CO
>>> scenario.change_table.add_branch(
... [
... {"capacity": 200, "from_bus_id": 2053002, "to_bus_id": 2053303},
... {"capacity": 150, "from_bus_id": 2060002, "to_bus_id": 2060046},
... ]
... )
>>>
>>> # add DC line between CO and CA (Bay Area)
>>> scenario.change_table.add_dcline(
... [{"capacity": 2000, "from_bus_id": 2060771, "to_bus_id": 2021598}]
... )
>>>
>>> # add a solar plant in NV, a coal plant in ID and a natural gas plant in OR
>>> scenario.change_table.add_plant(
... [
... {"type": "solar", "bus_id": 2030454, "Pmax": 75},
... {
... "type": "coal",
... "bus_id": 2074334,
... "Pmin": 25,
... "Pmax": 750,
... "c0": 1800,
... "c1": 30,
... "c2": 0.0025,
... },
... {
... "type": "ng",
... "bus_id": 2090018,
... "Pmax": 75,
... "c0": 900,
... "c1": 30,
... "c2": 0.0015,
... },
... ]
... )
>>>
>>> # add storage at newly created solar plant
>>> scenario.change_table.add_storage_capacity([{"bus_id": 2030454, "capacity": 25}])
>>>
>>> # add a new bus, and a new one-way DC line connected to this bus
>>> scenario.change_table.add_bus([{"lat": 48, "lon": -125, "zone_id": 201, "baseKV": 138}])
>>> scenario.change_table.add_dcline(
... [{"from_bus_id": 2090023, "to_bus_id": 2090024, "Pmin": 0, "Pmax": 200}]
... )

The set of changes with respect to the base grid are enclosed in the change table.

>>> # get change table used to alter the base grid.
>>> ct = scenario.get_ct()
>>> {'solar': {'zone_id': {201: 5, 209: 2.5}}, 'wind': {'zone_id': {202: 1.5, 215: 2}}, 'branch': {'zone_id': {208: 2, 213: 2}}, 'new_branch': [{'from_bus_id': 2053002, 'to_bus_id': 2053303, 'Pmax': 200, 'Pmin': -200}, {'from_bus_id': 2060002, 'to_bus_id': 2060046, 'Pmax': 150, 'Pmin': -150}], 'new_dcline': [{'from_bus_id': 2060771, 'to_bus_id': 2021598, 'Pmax': 2000, 'Pmin': -2000}, {'from_bus_id': 2090023, 'to_bus_id': 2090024, 'Pmin': 0, 'Pmax': 200}], 'new_plant': [{'type': 'solar', 'bus_id': 2030454, 'Pmax': 75, 'Pmin': 0, 'plant_id_neighbor': 11977}, {'type': 'coal', 'bus_id': 2074334, 'Pmin': 25, 'Pmax': 750, 'c0': 1800, 'c1': 30, 'c2': 0.0025}, {'type': 'ng', 'bus_id': 2090018, 'Pmax': 75, 'c0': 900, 'c1': 30, 'c2': 0.0015, 'Pmin': 0}], 'storage': [{'bus_id': 2030454, 'capacity': 25, 'terminal_min': 0, 'terminal_max': 1, 'energy_value': 20, 'InEff': 0.9, 'duration': 4, 'LossFactor': 0, 'max_stor': 0.95, 'OutEff': 0.9, 'min_stor': 0.05}], 'new_bus': [{'lat': 48, 'lon': -125, 'zone_id': 201, 'baseKV': 138, 'Pd': 0}]}

Each time you make changes, these are reflected in the grid. One can see that the two added DC lines are listed in the dc_line data frame below:

>>> # get grid used in scenario
>>> scenario_grid = scenario.get_grid()
>>> scenario_grid.dcline
from_bus_id to_bus_id status Pf Pt Qf Qt Vf ... muPmin muPmax muQminF muQmaxF muQminT muQmaxT from_interconnect to_interconnect
dcline_id ...
9 2050383 2028847 1 2400.0 2300.0 0 0 1 ... 0 0 0 0 0 0 Western Western
10 2013620 2025332 1 3100.0 3000.0 0 0 1 ... 0 0 0 0 0 0 Western Western
15 2021181 2021641 1 400.0 375.0 0 0 1 ... 0 0 0 0 0 0 Western Western
16 2060771 2021598 1 2000.0 1960.0 0 0 0 ... 0 0 0 0 0 0 Western Western
17 2090023 2090024 1 200.0 196.0 0 0 0 ... 0 0 0 0 0 0 Western Western

[5 rows x 25 columns]

Running the Scenario

It is time now to create the scenario and launch the simulation:

>>> scenario.create_scenario()
CREATING SCENARIO: test | medium_blog_post

Transferring ScenarioList.csv from local_fs
ScenarioList.csv.2 not found on local machine
--> Adding entry in ScenarioList.csv
Writing data/input/1_ct.pkl
Transferring ExecuteList.csv from local_fs
ExecuteList.csv.2 not found on local machine
--> Setting status=created in execute list
SCENARIO SUCCESSFULLY CREATED WITH ID #1
State switching: create --> execute
SCENARIO: test | medium_blog_post

--> State
execute
--> Status
created
Reading bus.csv
Reading plant.csv
Reading gencost.csv
Reading branch.csv
Reading dcline.csv
Reading sub.csv
Reading bus2sub.csv
Reading zone.csv
--> Loading ct
>>>
>>> scenario.prepare_simulation_input()
Transferring ExecuteList.csv from local_fs
ExecuteList.csv.2 not found on local machine
---------------------------
PREPARING SIMULATION INPUTS
---------------------------
--> Loading demand
Writing tmp/scenario_1/demand.csv
--> Loading hydro
Writing tmp/scenario_1/hydro.csv
--> Loading solar
Writing tmp/scenario_1/solar.csv
--> Loading wind
Writing tmp/scenario_1/wind.csv
--> Preparing grid data
Writing tmp/scenario_1/grid.pkl
Transferring ExecuteList.csv from local_fs
ExecuteList.csv.2 not found on local machine
--> Setting status=prepared in execute list
>>>
>>> process_run = scenario.launch_simulation(threads=8, solver="glpk")
Transferring ExecuteList.csv from local_fs
ExecuteList.csv.2 not found on local machine
--> Launching simulation on container

The progress of the simulation can be checked with:

>>> scenario.check_progress()
{'errors': [], 'output': ['Validation complete!', 'Launching scenario with parameters:', "{'interval': 24, 'n_interval': 2, 'start_index': 5113, 'input_dir': '/mnt/bes/pcm/tmp/scenario_1', 'threads': 8, 'julia_env': None, 'solver_kwargs': None}", 'INFO: threads not supported by GLPK, ignoring', 'Reading from folder: /mnt/bes/pcm/tmp/scenario_1', '...loading demand.csv', '...loading hydro.csv', '...loading wind.csv', '...loading solar.csv', '...loading storage', 'Demand flexibility parameters not found in /mnt/bes/pcm/tmp/scenario_1', 'Demand flexibility parameters will default to allowing demand flexibility to occur.', 'Demand flexibility up profile not found in /mnt/bes/pcm/tmp/scenario_1', 'Demand flexibility up-shift cost profiles not found in /mnt/bes/pcm/tmp/scenario_1. Will default to no cost for up-shifting demand.', 'Demand flexibility dn profile not found in /mnt/bes/pcm/tmp/scenario_1', 'Demand flexibility dn-shift cost profiles not found in /mnt/bes/pcm/tmp/scenario_1. Will default to no cost for dn-shifting demand.', 'All scenario files loaded!', 'All preparation complete!', 'Redirecting outputs, see stdout.log & stderr.err in outputfolder'], 'scenario_id': 1, 'status': 'running'}

Several days later … You will need a commercial solver such as GUROBI or CPLEX to run scenario for a larger time range and/or network (Eastern or USA).

You can load the scenario as follows:

root@client:/plug# python
Python 3.8.3 (default, Jun 9 2020, 17:39:39)
[GCC 8.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> from powersimdata import Scenario
>>> scenario = Scenario(1)
Transferring ScenarioList.csv from local_fs
ScenarioList.csv.2 not found on local machine
Transferring ExecuteList.csv from local_fs
ExecuteList.csv.2 not found on local machine
SCENARIO: test | medium_blog_post

--> State
analyze
--> Loading grid
--> Loading ct
>>> scenario.print_scenario_info()
--------------------
SCENARIO INFORMATION
--------------------
id: 1
plan: test
name: medium_blog_post
state: analyze
grid_model: usa_tamu
grid_model_version:
interconnect: Western
base_demand: vJan2021
base_hydro: vJan2021
base_solar: vJan2021
base_wind: vJan2021
change_table: Yes
start_date: 2016-08-01 00:00:00
end_date: 2016-08-02 23:00:00
interval: 24H
engine: REISE.jl
runtime: 63:46
infeasibilities:

There are no infeasibilities and the runtime was 63:46. You notice that the scenario is in the analyze state. You can access results with a set of methods listed in the tutorial. For example:

>>> # get generation profile for generators
>>> scenario.get_pg().head()
--> Loading PG
10390 10391 10392 10393 10394 10395 ... 14017 14018 14019 14020 14021 14022
UTC ...
2016-08-01 00:00:00 71.120422 71.118927 71.118927 71.118927 71.118927 71.118927 ... 0.015464 0.641995 0.164916 17.579849 25.000000 75.0
2016-08-01 01:00:00 68.457428 68.455986 68.455986 68.455986 68.455986 68.455986 ... 0.035395 0.619206 0.140073 15.926925 25.000000 75.0
2016-08-01 02:00:00 69.752136 69.750664 69.750664 69.750664 69.750664 69.750664 ... 0.038280 0.559067 0.148730 2.560950 219.031616 75.0
2016-08-01 03:00:00 70.287285 70.285805 70.285805 70.285805 70.285805 70.285805 ... 0.050753 0.519339 0.106266 0.000000 194.505341 75.0
2016-08-01 04:00:00 67.749001 67.747566 67.747566 67.747566 67.747566 67.747566 ... 0.058644 0.578008 0.064916 0.000000 152.754929 75.0

[5 rows x 2619 columns]
>>> # get generation profile for storage units (if present in scenario)
>>> scenario.get_storage_pg().head()
--> Loading STORAGE_PG
0
UTC
2016-08-01 00:00:00 0.000000e+00
2016-08-01 01:00:00 1.550000e+01
2016-08-01 02:00:00 2.500000e+01
2016-08-01 03:00:00 -1.071356e-10
2016-08-01 04:00:00 0.000000e+00
>>> # get power flow profile for AC lines
>>> scenario.get_pf().head()
--> Loading PF
88209 88210 88211 88212 88213 88214 ... 104188 104189 104190 104191 104192 104193
UTC ...
2016-08-01 00:00:00 16.481243 -13.547144 -0.380948 31.017967 33.388161 13.229003 ... 3.755764 54.893749 28.920828 4.340323 -9.724829 -12.265690
2016-08-01 01:00:00 19.774017 -16.418140 -10.660921 38.944805 34.858242 6.952124 ... 2.564785 71.853699 42.693325 4.039045 -6.870174 -12.380457
2016-08-01 02:00:00 25.074022 -19.905392 -26.063171 50.971554 38.436024 -1.480694 ... 1.149935 96.855324 63.413517 2.237109 -9.242074 -12.234606
2016-08-01 03:00:00 26.936014 -20.969118 -32.954079 51.557713 34.689419 -5.940944 ... 1.009915 100.000000 72.024841 1.353441 -6.753407 -12.147563
2016-08-01 04:00:00 23.974009 -17.252871 -21.832535 41.062172 29.078369 -1.290696 ... 2.910713 79.372520 57.681168 0.442279 -3.236432 -11.940262

[5 rows x 12732 columns]
>>> # get power flow profile for DC lines
>>> scenario.get_dcline_pf().head()
--> Loading PF_DCLINE
9 10 15 16 17
UTC
2016-08-01 00:00:00 938.386475 -279.926544 400.0 1483.420044 0.0
2016-08-01 01:00:00 1817.444336 -121.997574 400.0 1483.420044 0.0
2016-08-01 02:00:00 1613.904785 -710.521240 400.0 1483.420044 0.0
2016-08-01 03:00:00 1895.520020 221.439529 400.0 1483.420044 0.0
2016-08-01 04:00:00 1836.993530 -301.759857 400.0 1483.420044 0.0
>>> # get locational marginal price profile for each bus
>>> scenario.get_lmp().head()
--> Loading LMP
2010001 2010002 2010003 2010004 2010005 2010006 ... 2090019 2090020 2090021 2090022 2090023 2090024
UTC ...
2016-08-01 00:00:00 30.933472 30.933531 30.933567 30.933569 30.933569 30.939894 ... 30.940090 30.939863 30.933569 30.933531 30.933472 30.933472
2016-08-01 01:00:00 31.434069 31.434183 31.434254 31.434256 31.434256 31.447039 ... 31.447481 31.446968 31.434256 31.434183 31.434069 31.434069
2016-08-01 02:00:00 31.817999 31.818161 31.818256 31.818260 31.818260 31.836245 ... 31.836880 31.836142 31.818260 31.818161 31.817999 31.817999
2016-08-01 03:00:00 31.792437 31.792599 31.792696 31.792700 31.792700 31.810747 ... 31.811382 31.810644 -0.000000 31.792599 31.792437 31.792437
2016-08-01 04:00:00 31.717680 31.717890 31.718018 31.718021 31.718021 31.741350 ... 31.742157 31.741219 31.718021 31.717890 31.717680 31.717680

[5 rows x 10025 columns]
>>> # get congestion (upper power flow limit) profile for AC lines
>>> scenario.get_congu().head()
--> Loading CONGU
88209 88210 88211 88212 88213 88214 88215 88216 ... 104186 104187 104188 104189 104190 104191 104192 104193
UTC ...
2016-08-01 00:00:00 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0000 0.0 0.0 0.0 0.0
2016-08-01 01:00:00 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0000 0.0 0.0 0.0 0.0
2016-08-01 02:00:00 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0000 0.0 0.0 0.0 0.0
2016-08-01 03:00:00 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 31.7927 0.0 0.0 0.0 0.0
2016-08-01 04:00:00 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0000 0.0 0.0 0.0 0.0

[5 rows x 12732 columns]

Looking at Scenario Results

You can use the postreise package to analyze and plot the output data. An up to date list of the library of functions we developed can be found here.

To illustrate you can get the total generation for each generator type and load zone combination with:

>>> from powersimdata import Scenario
>>>
>>> from postreise.analyze.generation.summarize import sum_generation_by_type_zone
>>>
>>>
>>> scenario = Scenario(1)
Transferring ScenarioList.csv from local_fs
ScenarioList.csv.2 not found on local machine
Transferring ExecuteList.csv from local_fs
ExecuteList.csv.2 not found on local machine
SCENARIO: test | run_readme_to_test_grid_model_50e313e2-2b35-4b71-9a43-c96d582ab183

--> State
analyze
--> Loading grid
--> Loading ct
>>> sum_generation_by_type_zone(scenario)
--> Loading PG
zone_id 201 202 203 204 205 ... 212 213 214 215 216
type ...
coal 14985.119141 9034.080078 0.000000 8709.709961 4571.680176 ... 223221.890625 282401.625000 3110.656250 89222.601562 0.000000
geothermal 0.000000 0.000000 34922.878906 0.000000 7919.520508 ... 0.000000 0.000000 0.000000 0.000000 0.000000
hydro 371762.812500 111482.281250 82048.406250 0.000000 67099.234375 ... 7533.466309 6778.829102 55593.199219 36054.941406 0.000000
ng 55361.726562 95633.578125 46814.796875 145878.640625 132084.500000 ... 85911.125000 332.500000 18132.492188 4172.809570 16153.599609
nuclear 57600.000000 0.000000 0.000000 0.000000 0.000000 ... 0.000000 0.000000 0.000000 0.000000 0.000000
solar 27880.367188 8162.219727 41832.890625 830.324219 26187.933594 ... 2850.047852 1410.679688 2723.915527 0.000000 158.418304
wind 43776.734375 72465.898438 2889.514893 16683.130859 0.000000 ... 30108.701172 20316.064453 12618.542969 6879.406250 0.000000
wind_offshore 9214.302734 6825.282715 11562.808594 4752.900391 3036.091797 ... 0.000000 0.000000 0.000000 0.000000 0.000000

[8 rows x 16 columns]
>>> grid = scenario.get_grid()
>>> grid.id2zone
{201: 'Washington', 202: 'Oregon', 203: 'Northern California', 204: 'Bay Area', 205: 'Central California', 206: 'Southwest California', 207: 'Southeast California', 208: 'Nevada', 209: 'Arizona', 210: 'Utah', 211: 'New Mexico Western', 212: 'Colorado', 213: 'Wyoming', 214: 'Idaho', 215: 'Montana Western', 216: 'El Paso'}

You can plot stacked generation time series in an area:

from powersimdata import Scenario

from postreise.plot.plot_generation_ts_stack import plot_generation_time_series_stack


scenario = Scenario(1)
resources = [
"nuclear",
"coal",
"hydro",
"geothermal",
"other",
"dfo",
"ng",
"solar",
"wind",
"storage",
"solar_curtailment",
"wind_curtailment",
"wind_offshore_curtailment",
]
plot_generation_time_series_stack(
scenario,
"Western",
resources,
time_freq="H",
show_demand=False,
show_net_demand=False,
save=True
)
Generation Time Series

Or plot the renewable generators curtailment time series in the Western interconnection:

>>> from powersimdata import Scenario

>>> from postreise.plot.plot_curtailment_ts import plot_curtailment_time_series

>>> scenario = Scenario(1)
>>> t2c = {"wind_curtailment": "blue", "solar_curtailment": "blue"}
>>> plot_curtailment_time_series(
... scenario,
... "Western",
... ["solar", "wind"],
... time_freq="H",
... t2c=t2c,
... label_fontsize=30,
... title_fontsize=35,
... tick_fontsize=25,
... legend_fontsize=25,
... save=True
... )
Wind Curtailment
Solar Curtailment

Summary

We presented a set of tools that can be used to run scenarios in future U.S. electric grids. A toy model was used to help you get started. Don’t stop here and take a look at the report Breakthrough Energy Sciences wrote: A 2030 United States Macro Grid to get an idea of the kind of studies that can be achieved with this simulation framework.

--

--