Running Power Flow In The Loop with Unit Commitment
To follow along, you can download this tutorial as a Julia script (.jl) or Jupyter notebook (.ipynb).
In this tutorial, you'll configure a unit commitment (UC) simulation that automatically calls an AC power flow solver from PowerFlows.jl at every dispatch interval.
You'll validate the committed dispatch against the full AC network model at each hour, check for branch overloads, and export the results to PSS/e format if needed for further analysis.
This tutorial builds on the tutorial for Running a Multi-Stage Production Cost Simulation.
Setup
Load the needed packages and define basic inputs:
using PowerSystemCaseBuilder
using PowerSimulations
using HydroPowerSimulations
using PowerFlows
using PowerSystems
using DataFrames
using HiGHS
using Dates
using Logging
const SCENARIO_NAME = "uc_with_pf"
const INLOOP_SYSTEM = "modified_RTS_GMLC_DA_sys"
const INLOOP_SIM_STEPS = 1
run_dir = joinpath(".", "uc_power_flow_in_the_loop_results")
mkpath(run_dir)
export_dir = joinpath(run_dir, "psse_exports")
mkpath(export_dir)"./uc_power_flow_in_the_loop_results/psse_exports"Build a test PowerSystems.System via PowerSystemCaseBuilder.build_system. We use modified_RTS_GMLC_DA_sys (24+ DA forecast steps):
sys = build_system(
PSISystems,
INLOOP_SYSTEM;
skip_serialization = true,
runchecks = false,
)| System | |
| Property | Value |
|---|---|
| Name | |
| Description | |
| System Units Base | SYSTEM_BASE |
| Base Power | 100.0 |
| Base Frequency | 60.0 |
| Num Components | 504 |
| Static Components | |
| Type | Count |
|---|---|
| ACBus | 73 |
| Arc | 109 |
| Area | 3 |
| FixedAdmittance | 3 |
| HydroDispatch | 1 |
| Line | 105 |
| LoadZone | 21 |
| PowerLoad | 51 |
| RenewableDispatch | 29 |
| RenewableNonDispatch | 31 |
| SynchronousCondenser | 3 |
| TapTransformer | 15 |
| ThermalStandard | 54 |
| TwoTerminalGenericHVDCLine | 1 |
| VariableReserve{ReserveDown} | 1 |
| VariableReserve{ReserveUp} | 4 |
| StaticTimeSeries Summary | |||||||
| owner_type | owner_category | name | time_series_type | initial_timestamp | resolution | count | time_step_count |
|---|---|---|---|---|---|---|---|
| String | String | String | String | String | Dates.CompoundPeriod | Int64 | Int64 |
| Area | Component | max_active_power | SingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 3 | 8784 |
| FixedAdmittance | Component | max_active_power | SingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 3 | 8784 |
| HydroDispatch | Component | max_active_power | SingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 1 | 8784 |
| PowerLoad | Component | max_active_power | SingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 51 | 8784 |
| RenewableDispatch | Component | max_active_power | SingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 29 | 8784 |
| RenewableNonDispatch | Component | max_active_power | SingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 31 | 8784 |
| VariableReserve | Component | requirement | SingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 5 | 8784 |
| Forecast Summary | |||||||||
| owner_type | owner_category | name | time_series_type | initial_timestamp | resolution | count | horizon | interval | window_count |
|---|---|---|---|---|---|---|---|---|---|
| String | String | String | String | String | Dates.CompoundPeriod | Int64 | Dates.CompoundPeriod | Dates.CompoundPeriod | Int64 |
| Area | Component | max_active_power | DeterministicSingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 3 | 2 days | 1 day | 365 |
| FixedAdmittance | Component | max_active_power | DeterministicSingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 3 | 2 days | 1 day | 365 |
| HydroDispatch | Component | max_active_power | DeterministicSingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 1 | 2 days | 1 day | 365 |
| PowerLoad | Component | max_active_power | DeterministicSingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 51 | 2 days | 1 day | 365 |
| RenewableDispatch | Component | max_active_power | DeterministicSingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 29 | 2 days | 1 day | 365 |
| RenewableNonDispatch | Component | max_active_power | DeterministicSingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 31 | 2 days | 1 day | 365 |
| VariableReserve | Component | requirement | DeterministicSingleTimeSeries | 2020-01-01T00:00:00 | 1 hour | 5 | 2 days | 1 day | 365 |
Configuring the Power Flow Solver
Create an PowerFlows.ACPowerFlow solver. We attach a PowerFlows.PSSEExportPowerFlow exporter so that we can automatically write a PSS/e .raw file for each solved interval.
psse_export = PSSEExportPowerFlow(;
psse_version = :v33,
export_dir = export_dir,
overwrite = true,
)
power_flow_model = ACPowerFlow(; exporter = psse_export)PowerFlows.ACPolarPowerFlow{PowerFlows.NewtonRaphsonACPowerFlow}(false, PowerFlows.PSSEExportPowerFlow(:v33, "./uc_power_flow_in_the_loop_results/psse_exports", "export", false, true), false, false, nothing, true, false, false, false, PowerNetworkMatrices.NetworkReduction[], 1, String[], false, Dict{Symbol, Any}())Building the UC Problem Template
Create a ProblemTemplate with a NetworkModel that uses PTDFPowerModel and power_flow_evaluation set to the solver we just configured. Assign device formulations with set_device_model!. This is the key step that enables power flow in the loop:
template_uc = ProblemTemplate(
NetworkModel(
PTDFPowerModel;
use_slacks = true,
power_flow_evaluation = power_flow_model,
),
)
set_device_model!(
template_uc,
ThermalStandard,
ThermalStandardUnitCommitment,
)
set_device_model!(template_uc, RenewableDispatch, RenewableFullDispatch)
set_device_model!(template_uc, RenewableNonDispatch, FixedOutput)
set_device_model!(template_uc, PowerLoad, StaticPowerLoad)
set_device_model!(template_uc, HydroDispatch, HydroDispatchRunOfRiver)
set_device_model!(template_uc, Line, StaticBranchUnbounded)
set_device_model!(template_uc, Transformer2W, StaticBranchUnbounded)
set_device_model!(template_uc, MonitoredLine, StaticBranch)use_slacks = true allows the simulation to remain feasible when there is a small mismatch between the PTDF-based UC network model and the full AC power flow.
Building and Executing the Simulation
Follow the same pattern as in the tutorial for Running a Multi-Stage Production Cost Simulation: package the UC DecisionModel in SimulationModels, define a SimulationSequence with InterProblemChronology, construct a Simulation, then build! and execute! it for one simulation step. The in-step horizon on modified_RTS_GMLC_DA_sys provides 24 hourly realized rows.
solver = optimizer_with_attributes(
HiGHS.Optimizer,
"log_to_console" => false,
"mip_rel_gap" => 0.05,
"time_limit" => 900.0,
)
models = SimulationModels(;
decision_models = [
DecisionModel(
template_uc,
sys;
name = "UC",
optimizer = solver,
store_variable_names = true,
),
],
)
sequence = SimulationSequence(;
models = models,
ini_cond_chronology = InterProblemChronology(),
)
sim = Simulation(;
name = SCENARIO_NAME,
steps = INLOOP_SIM_STEPS,
models = models,
sequence = sequence,
simulation_folder = run_dir,
)
build!(sim; console_level = Logging.Error, file_level = Logging.Error)
execute!(sim; enable_progress_bar = true)InfrastructureSystems.Simulation.RunStatusModule.RunStatus.SUCCESSFULLY_FINALIZED = 0Loading Simulation Results
Load the simulation results with SimulationResults and extract the UC problem results with get_decision_problem_results:
sim_results = SimulationResults(sim)
uc_results = get_decision_problem_results(sim_results, "UC")Start: 2020-01-01T00:00:00
End: 2020-01-01T00:00:00
Resolution: 60 minutes
| UC Problem Auxiliary variables Results |
| PowerFlowBranchActivePowerFromTo__TapTransformer |
| TimeDurationOn__ThermalStandard |
| PowerFlowBranchReactivePowerToFrom__Line |
| PowerFlowVoltageMagnitude__ACBus |
| PowerFlowBranchActivePowerLoss__TwoTerminalGenericHVDCLine |
| PowerFlowBranchActivePowerLoss__Line |
| PowerFlowBranchReactivePowerFromTo__Line |
| PowerFlowBranchActivePowerToFrom__TwoTerminalGenericHVDCLine |
| PowerFlowBranchActivePowerToFrom__Line |
| PowerFlowBranchActivePowerToFrom__TapTransformer |
| PowerFlowBranchReactivePowerFromTo__TwoTerminalGenericHVDCLine |
| PowerFlowBranchReactivePowerFromTo__TapTransformer |
| PowerFlowVoltageAngle__ACBus |
| PowerFlowBranchReactivePowerToFrom__TapTransformer |
| PowerFlowBranchActivePowerFromTo__Line |
| PowerFlowBranchReactivePowerToFrom__TwoTerminalGenericHVDCLine |
| TimeDurationOff__ThermalStandard |
| PowerFlowBranchActivePowerFromTo__TwoTerminalGenericHVDCLine |
| HydroEnergyOutput__HydroDispatch |
| PowerFlowBranchActivePowerLoss__TapTransformer |
| UC Problem Expressions Results |
| ShutDownCostExpression__ThermalStandard |
| ActivePowerBalance__ACBus |
| VOMCostExpression__RenewableDispatch |
| FixedCostExpression__ThermalStandard |
| ProductionCostExpression__RenewableDispatch |
| StartUpCostExpression__ThermalStandard |
| PTDFBranchFlow__Line |
| ProductionCostExpression__ThermalStandard |
| FixedCostExpression__RenewableDispatch |
| ProductionCostExpression__HydroDispatch |
| FuelConsumptionExpression__ThermalStandard |
| ActivePowerBalance__System |
| VOMCostExpression__ThermalStandard |
| FuelCostExpression__ThermalStandard |
| CurtailmentCostExpression__RenewableDispatch |
| UC Problem Parameters Results |
| ReactivePowerTimeSeriesParameter__HydroDispatch |
| ReactivePowerTimeSeriesParameter__PowerLoad |
| ActivePowerTimeSeriesParameter__HydroDispatch |
| ReactivePowerTimeSeriesParameter__RenewableNonDispatch |
| ActivePowerTimeSeriesParameter__RenewableDispatch |
| ActivePowerTimeSeriesParameter__RenewableNonDispatch |
| ReactivePowerTimeSeriesParameter__RenewableDispatch |
| ActivePowerTimeSeriesParameter__PowerLoad |
| UC Problem Variables Results |
| ActivePowerVariable__HydroDispatch |
| StartVariable__ThermalStandard |
| ActivePowerVariable__ThermalStandard |
| SystemBalanceSlackUp__System |
| OnVariable__ThermalStandard |
| ActivePowerVariable__RenewableDispatch |
| SystemBalanceSlackDown__System |
| StopVariable__ThermalStandard |
Notice the "UC Problem Auxiliary variables Results" table, which lists the active and reactive power flow and bus voltage magnitude and angle results from the AC power flow (e.g., PowerFlowBranchReactivePowerFromTo__Line, PowerFlowVoltageMagnitude__ACBus). These are not output when a UC problem is run alone.
PTDF UC Flows vs. AC Power Flow In the Loop
Now, we'll compare the PTDF UC flows to the AC power flow results.
The UC stage optimizes flows using the PTDF network model, and the line flows are not variables; they are recorded in the PTDFBranchFlow__* expressions.
First, load in the PTDF branch flows for one line with read_realized_expression:
ptdf_flows = read_realized_expression(uc_results, "PTDFBranchFlow__Line")
example_line = first(unique(ptdf_flows.name))
ptdf_line = filter(row -> row.name == example_line, ptdf_flows)| DateTime | name | value |
|---|---|---|
| Dates.DateTime | String | Float64 |
| 2020-01-01T00:00:00 | A1 | 20.411365763521367 |
| 2020-01-01T01:00:00 | A1 | 18.974231910761745 |
| 2020-01-01T02:00:00 | A1 | 20.094662756073827 |
| 2020-01-01T03:00:00 | A1 | 20.67962260485144 |
| 2020-01-01T04:00:00 | A1 | 16.511279486614868 |
| 2020-01-01T05:00:00 | A1 | 18.030431449676943 |
| 2020-01-01T06:00:00 | A1 | 15.291795036026606 |
| 2020-01-01T07:00:00 | A1 | 29.770571270724094 |
| 2020-01-01T08:00:00 | A1 | 41.53998185350879 |
| 2020-01-01T09:00:00 | A1 | 33.60584365155333 |
| ⋮ | ⋮ | ⋮ |
After each in-step hour, the AC power flow writes branch flows into auxiliary variables such as PowerFlowBranchActivePowerFromTo__Line. Load those flows with read_realized_aux_variable and compare PTDF expression flows to AC flows for our selected line:
pf_flows_ft = read_realized_aux_variable(
uc_results,
"PowerFlowBranchActivePowerFromTo__Line",
)
pf_line = filter(row -> row.name == example_line, pf_flows_ft)
innerjoin(
rename(select(ptdf_line, :DateTime, :value), :value => :PTDF_MW),
rename(select(pf_line, :DateTime, :value), :value => :AC_PF_MW);
on = :DateTime,
)| DateTime | PTDF_MW | AC_PF_MW |
|---|---|---|
| Dates.DateTime | Float64 | Float64 |
| 2020-01-01T00:00:00 | 20.411365763521367 | 17.905261228634547 |
| 2020-01-01T01:00:00 | 18.974231910761745 | 16.680300222292487 |
| 2020-01-01T02:00:00 | 20.094662756073827 | 17.726350963393163 |
| 2020-01-01T03:00:00 | 20.67962260485144 | 18.228077098026112 |
| 2020-01-01T04:00:00 | 16.511279486614868 | 14.451997369396658 |
| 2020-01-01T05:00:00 | 18.030431449676943 | 15.839176212981258 |
| 2020-01-01T06:00:00 | 15.291795036026606 | 13.453506655854605 |
| 2020-01-01T07:00:00 | 29.770571270724094 | 29.143666851474308 |
| 2020-01-01T08:00:00 | 41.53998185350879 | 41.148136578740576 |
| 2020-01-01T09:00:00 | 33.60584365155333 | 33.27782134838367 |
| ⋮ | ⋮ | ⋮ |
With use_slacks = true in the template, small differences between PTDF and AC flows are expected — the UC model can absorb minor mismatches. The AC auxiliary flows should track the PTDF values closely when the network is well conditioned.
Checking for Branch Overloads
Now, we will run a post-hoc check on our UC results to scan every interval for branches whose AC flow exceeds the thermal rating.
First, build a helper to extract branch flow limits from the PowerSystems.System — name in the auxiliary results matches the branch name on each PowerSystems.ACBranch. Limits are scaled with PowerSystems.get_base_power. AC lines and transformers use PowerSystems.get_rating; HVDC branches use active-power limits instead.
function _branch_flow_limit_mw(branch, sys)
base_power = get_base_power(sys)
try
if branch isa TwoTerminalVSCLine
return get_rating(branch) * base_power
elseif branch isa TwoTerminalHVDC
return max(
get_active_power_limits_from(branch).max,
get_active_power_limits_to(branch).max,
)
elseif branch isa GenericArcImpedance
return get_max_flow(branch) * base_power
else
return get_rating(branch) * base_power
end
catch e
e isa MethodError || rethrow(e)
@warn "Could not get rating for $(typeof(branch)): $e — treating as unlimited"
return Inf
end
end_branch_flow_limit_mw (generic function with 1 method)Next, build a lookup for each PowerSystems.ACBranch keyed by branch name using PowerSystems.get_components and get_name:
ratings = Dict{String, Float64}()
for b in get_components(ACBranch, sys)
ratings[get_name(b)] = _branch_flow_limit_mw(b, sys)
endThen, scan every interval for branches whose AC flow exceeds the thermal rating:
overloads = DataFrame(;
DateTime = Dates.DateTime[],
name = String[],
P_from_to = Float64[],
rating = Float64[],
)
for row in eachrow(pf_flows_ft)
rating = get(ratings, row.name, Inf)
if abs(row.value) > rating
push!(overloads, (row.DateTime, row.name, row.value, rating))
end
end
overloads| DateTime | name | P_from_to | rating |
|---|---|---|---|
| Dates.DateTime | String | Float64 | Float64 |
| 2020-01-01T00:00:00 | C6 | 189.5145348857181 | 175.0 |
| 2020-01-01T01:00:00 | C6 | 250.4639107394733 | 175.0 |
| 2020-01-01T02:00:00 | C6 | 189.66427935296832 | 175.0 |
| 2020-01-01T05:00:00 | C6 | 175.51180847545996 | 175.0 |
| 2020-01-01T18:00:00 | AB1 | 181.30592402464646 | 175.0 |
Notice we have multiple overloads in this example. Each row identifies a congestion event: the interval (DateTime), branch name (name), actual AC flow in MW (P_from_to), and thermal rating (rating). In-loop AC power flow records realized flows but does not add thermal-limit constraints to the UC optimization. The UC model's network representation could be too loose if overloads appear — add transmission constraints, re-run UC, and repeat the AC power flow check until the table is empty.
PSS/e Export Files
The PowerFlows.PSSEExportPowerFlow exporter writes under export_dir (here, results/psse_exports). After running, we have one folder per solved interval in the 24-hour in-step horizon, plus the 24-hour lookahead:
psse_export_folders = readdir(export_dir)
length(psse_export_folders)48Each folder contains the PSS/e .raw file for that interval, available for further analysis:
first_subdir = first(sort(psse_export_folders))
readdir(joinpath(export_dir, first_subdir))2-element Vector{String}:
"export_1_1.raw"
"export_1_1_export_metadata.json"Next Steps
- Tighten the UC network model with transmission constraints if needed until overloads are removed — see the formulation library Network and
PowerSystems.BranchFormulations pages. - See the Solving a Power Flow tutorial in PowerFlows.jl for standalone power flow examples.