Coding challenge: Build an electrical dispatch model
In this coding challenge, you will build a simple electrical dispatch model. Dispatch models are used by electrical grid operators to determine which power plants should run when.
This coding challenge was developed for the PowerUp 2026 conference in Boulder, Colorado and uses data from the California Test System.1
Use agentic AI judiciously
Agentic AI tools like Claude Code can certainly "solve" this coding challenge for you, but you will best learn the concepts taught here if you limit your use of such tools. Why bother learning what an AI agent can do? I find that modelers with a solid grasp of the concepts behind modeling frameworks like Pyoframe are more effective at guiding AI agents and building complex, cutting-edge models.
A. Set up the project
-
Install Python if you haven't already (version 3.10 or above).
-
Download and unzip the starter code and data for this challenge.
-
Optional: In the downloaded folder, create and active a virtual environment to prevent dependency conflicts.
-
In the downloaded folder, run the following command to install Pyoframe, HiGHS (a free solver), Altair (a plotting library), and pandas. If you prefer using another solver like Gurobi, refer to our installation instructions.
-
Run
main.py. The file should produce no errors and the log should end with,If you encounter errors, let us know.
Pandas or Polars
The provided starter code uses pandas since that is what most people are familiar with. However, if you're feeling adventurous, we recommend you try using polars instead. Polars is a much faster alternative to Pandas with a more consistent, readable, and powerful syntax. Pyoframe uses Polars internally but works just fine with Pandas.
B. Discover the data
Like most real optimization problems, we will be integrating external data into our model! Take a look at the input_data folder and inspect the following two files:
-
generators.parquet: a list of power generator and their characteristics. For now, you will only need the following columns:-
gen_id, a unique ID for each generator -
Pmax, the maximum power the generator can output (in MW) -
cost_per_MWh_linear, the cost of per MWh of producing energy
-
-
loads.parquet: the electrical demand at every hour and every electrical bus
Parquet files
Parquet files are a modern, compact, and machine-friendly alternative to CSV files. You can inspect .parquet files using the Data Wrangler extension in VSCode, or you can read them with Pandas (as is done in main.py) and inspect them using regular Pandas commands (e.g., DataFrame.info()).
C. Build a single-timestep, copper-plate model
To being, you will build the simplest possible dispatch model: a single-timestep, copper-plate model. Copper-plate models are simplistic models that assume unlimited, lossless transmission between regions, as if all power generators and loads were located in the same region (or on the same copper plate). As such, this model has neither a spatial or temporal component.
Build the model in main.py using the two previously mentioned data files. You will need to add the following 6 lines of code beneath the YOUR CODE GOES HERE comment.
-
A line to create the Pyoframe
Modelobject that forms the basis of the model. -
A line to add a
Dispatchvariable to the model. The variable should by indexed ("dimensioned") over thegen_idcolumn and should have both a lower and upper bound. The upper bound should come from thePmaxcolumn. -
A line to create a constraint that ensures the total
Dispatchis greater than theMIDDAY_LOAD. This is your power balance constraint. -
A line to set the objective function. The objective should be to minimize the total cost which is the sum of the product of
Dispatchwithcost_per_MWh_linear. -
A line with
.optimize()to solve the model. -
A line to retrieve the solution for
Dispatchand return it. Note that themainfunction must return the solution for the plotting to work.
Key Pyoframe concepts
Building this model will require understanding two important Pyoframe concepts
-
Pyoframe objects including variables, expressions, and constraints can be either dimensionless (i.e., a single constraint) or dimensioned (i.e., a family of constraints indexed over the same dimensions). For example, the
Dispatchwill need to be a dimensioned variable and will need to have thegen_iddimension since we want one variable for every generator. -
Pyoframe will automatically convert DataFrames into Pyoframe expressions according to a convenient rule: the last column is assumed to be the expression's value while all previous columns become the expressions' dimensions. So, if you'd like to create a dimensioned Expression for the cost of a generator, you'll need to select two columns
df_generators[["gen_id", "cost_per_MWh_linear"]].
If successful, running your code should result in the following plot!

D. Add time to your model
Our code only models power generation at noon. Let's extend it to all 24 hours of our data!
Update your power balance constraint to use the full load timeseries (df_load) instead of just the load at mid-day (MIDDAY_LOAD). Note that you'll need to change your Dispatch variable since we now want one variable for every generator and every hour.
Conflicting dimensions and .sum_by
-
Try simply swapping
MIDDAY_LOADfordf_load. What error do you observe? What does it mean? -
Hint:
.sum_bymight be useful when updating your power balance constraint.
After your modifications, your code should produce the following plot.

E. Integrate Variable Capacity Factors
Notice anything weird in the previous plot? Solar power is being produced at night! This is because the only constraint on solar generation is the total capacity of the solar farm, unrelated to the sunlight at that time of day. Let's fix this.
Add one additional constraint to limit the Dispatch of variable generators (like Solar and Wind) using the variable capacity factors in the variable_capacity_factors.parquet file. Variable capacity factors are ratios (e.g., 0.6) that indicate the fraction of the generators' Pmax that can be produced at a given time based on historical conditions.
Hints
-
You might find it helpful to complete this task in two steps. First, build a DataFrame containing the maximum generation limit at every hour (i.e., the product of the capacity factor with
Pmax), then use that DataFrame to create a new constraint. Pandas'.mergemethod might be helpful when creating the DataFrame. -
Since not all generators are renewable generators with capacity factors, you will face a
PyoframeError. Learn to use.keep_extras()or.drop_extras()to tell Pyoframe whether these "extra" generators should or shouldn't be kept in the constraint.
Your results should now look like this. This looks much more realistic!

F. Bonus: Add bulk transmission
If you'd like an extra challenge, use the data in lines.parquet to add transmission to the model. You'll need to define a new variable for the transmitted amount on each line at every timepoint, and then use this power transfer variable in your power balance constraint. This is a rather large model so you might not be able to run it on your personal computer.
-
Taylor, S. et al. California Test System (CATS): A Geographically Accurate Test System Based on the California Grid. Policy and Regulation IEEE Transactions on Energy Markets 2, 107–118 (2024). ↩
Comments