Industry DFAX values
In this tutorial we map the industry's "DFAX" vocabulary (Distribution Factors, as used in PSS/E's DFAX activity and downstream by the NERC IDC for TLR and Congestion Management Procedures) onto the matrices provided by PowerNetworkMatrices. Every flavor of DFAX — GSF, LSF, LODF, OTDF, transfer DFAX, flowgate DFAX, and multi-element (N-k) DFAX — is a special case of one unified formula. The tutorial walks through each case with executable examples on the RTS-GMLC system.
We assume you have already worked through the PTDF matrix and LODF matrix tutorials. For the Woodbury-identity derivation behind post-contingency PTDF rows, see the Flowgate Methodology explanation.
What is DFAX?
"DFAX" is shorthand for distribution factor. The term originated with PSS/E's DFAX activity, which writes a .dfx file consumed by the NERC Interchange Distribution Calculator (IDC). The IDC uses these distribution factors during Transmission Loading Relief (TLR) procedures and Congestion Management Procedures (CMP) — for example, applying a 5% threshold to decide whether a given source-to-sink transfer is a "significant" contributor to a monitored flowgate.
In practice DFAX is an umbrella term that covers several specific quantities. The table below maps each industry term to the PowerNetworkMatrices primitive that computes it:
| Industry term | What it answers | PNM primitive |
|---|---|---|
| GSF / ISF | Δflow on m per 1 MW injection at bus b | PTDF[m, b] |
| LSF | Same as GSF for loads (opposite sign) | -PTDF[m, b] |
| LODF | Flow redistribution after one branch trips | LODF[m, c] |
| OTDF | GSF with a contingency already in place | PTDF[m,b] + LODF[m,c]·PTDF[c,b], or one entry of a VirtualMODF row |
| Transfer DFAX (pre-cont.) | Fraction of a source→sink transfer reaching m | PTDF[m,:]·(s_v − k_v) |
| Flowgate DFAX | Transfer DFAX on a (monitored, contingency) pair | VirtualMODF[m, ctg]·(s_v − k_v) |
| Multi-element (N-k) DFAX | Same with multiple simultaneous outages | VirtualMODF (multi-arc NetworkModification) |
The Phase Shifter Factor (PSF) is part of the broader DFAX vocabulary but is not a first-class primitive in PowerNetworkMatrices. Users who need it can build it manually through NetworkModification and Ybus; the present tutorial covers only flow-based distribution factors.
The unified DFAX formula
In the DC power-flow model every flavor of DFAX is a special case of the same quantity. For a monitored arc $m$, a source participation vector $s_v$, a sink participation vector $k_v$, and a (possibly empty) set of network modifications $C$,
\[\mathrm{DFAX}(m,\ s \to k,\ C) \;=\; \mathrm{PTDF}_C[m,\,:] \cdot (s_v - k_v),\]
where $\mathrm{PTDF}_C$ is the post-modification PTDF (equal to the base $\mathrm{PTDF}$ when $C = \emptyset$). The formula has two degrees of freedom — who is shifting (the source/sink vectors) and what state the network is in (the contingency $C$). Each section below fixes one or both.
The reference (slack) bus is implicit in PTDF: a row of PTDF already encodes "inject at bus $b$, absorb at the slack". So setting $k_v = 0$ in the formula means "let the slack absorb the sink", and the GSF section below reduces to a single PTDF entry.
Setup
All subsequent sections build on this setup block. It loads the RTS-GMLC system and constructs the three matrices the rest of the tutorial uses.
julia> using PowerSystemsjulia> using PowerNetworkMatricesjulia> using PowerSystemCaseBuilderjulia> using DataFramesjulia> const PSY = PowerSystemsPowerSystemsjulia> const PNM = PowerNetworkMatricesPowerNetworkMatricesjulia> const PSB = PowerSystemCaseBuilderPowerSystemCaseBuilderjulia> sys = PSB.build_system(PSB.PSISystems, "RTS_GMLC_DA_sys");┌ Info: Building new system RTS_GMLC_DA_sys from raw data └ sys_descriptor.raw_data = "/home/runner/.julia/artifacts/c257b2c37b981f261fdc328b0fb9b96436a96537/RTS-GMLC-0.2.3" [ Info: Parsing csv data in branch.csv ... [ Info: Successfully parsed branch.csv [ Info: Parsing csv data in bus.csv ... [ Info: Successfully parsed bus.csv [ Info: Parsing csv data in dc_branch.csv ... [ Info: Successfully parsed dc_branch.csv [ Info: Parsing csv data in gen.csv ... [ Info: Successfully parsed gen.csv [ Info: Parsing csv data in reserves.csv ... [ Info: Successfully parsed reserves.csv [ Info: Parsing csv data in simulation_objects.csv ... [ Info: Successfully parsed simulation_objects.csv [ Info: Parsing csv data in storage.csv ... [ Info: Successfully parsed storage.csv [ Info: Parsing csv data in timeseries_pointers.csv ... [ Info: Successfully parsed timeseries_pointers.csv [ Info: Unit System changed to InfrastructureSystems.UnitSystemModule.UnitSystem.DEVICE_BASE = 1 ┌ Warning: Missing PowerSystems.InputCategoryModule.InputCategory.LOAD = 5 data. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:300 ┌ Warning: User-defined column name Startup Ramp Rate MW/min is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Shutdown Ramp Rate MW/min is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Status at Start is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Time at Status is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Cold is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Warm is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Hot is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Must Run is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: Rating calculation returned 0.0. Changing to 1.0 in the p.u. of the device. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/common.jl:120 ┌ Warning: Generator SynchronousCondenser: 114_SYNC_COND_1 has base power of zero: changing device base power to match system base power, 100.0 └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:938 ┌ Warning: Rating calculation returned 0.0. Changing to 1.0 in the p.u. of the device. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/common.jl:120 ┌ Warning: Generator SynchronousCondenser: 214_SYNC_COND_1 has base power of zero: changing device base power to match system base power, 100.0 └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:938 ┌ Warning: Rating calculation returned 0.0. Changing to 1.0 in the p.u. of the device. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/common.jl:120 ┌ Warning: Generator SynchronousCondenser: 314_SYNC_COND_1 has base power of zero: changing device base power to match system base power, 100.0 └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:938 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 ┌ Warning: Heat rate parsing not valid for RenewableGen replacing with zero cost └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1039 [ Info: Adding contributing generators for Spin_Up_R1 by category ┌ Warning: User-defined column name Startup Ramp Rate MW/min is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Shutdown Ramp Rate MW/min is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Status at Start is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Time at Status is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Cold is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Warm is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Hot is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Must Run is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 [ Info: Adding contributing generators for Spin_Up_R2 by category ┌ Warning: User-defined column name Startup Ramp Rate MW/min is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Shutdown Ramp Rate MW/min is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Status at Start is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Time at Status is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Cold is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Warm is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Hot is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Must Run is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 [ Info: Adding contributing generators for Spin_Up_R3 by category ┌ Warning: User-defined column name Startup Ramp Rate MW/min is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Shutdown Ramp Rate MW/min is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Status at Start is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Time at Status is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Cold is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Warm is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Hot is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Must Run is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 [ Info: Adding contributing generators for Flex_Up by category ┌ Warning: User-defined column name Startup Ramp Rate MW/min is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Shutdown Ramp Rate MW/min is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Status at Start is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Time at Status is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Cold is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Warm is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Hot is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Must Run is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 [ Info: Adding contributing generators for Flex_Down by category ┌ Warning: User-defined column name Startup Ramp Rate MW/min is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Shutdown Ramp Rate MW/min is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Status at Start is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Time at Status is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Cold is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Warm is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Hot is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Must Run is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 [ Info: Adding contributing generators for Reg_Up by category ┌ Warning: User-defined column name Startup Ramp Rate MW/min is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Shutdown Ramp Rate MW/min is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Status at Start is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Time at Status is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Cold is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Warm is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Hot is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Must Run is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 [ Info: Adding contributing generators for Reg_Down by category ┌ Warning: User-defined column name Startup Ramp Rate MW/min is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Shutdown Ramp Rate MW/min is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Status at Start is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Time at Status is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Cold is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Warm is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Start Cost Hot is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: User-defined column name Must Run is not in dataframe. └ @ PowerSystems ~/.julia/packages/PowerSystems/uwQkn/src/parsers/power_system_table_data.jl:1778 ┌ Warning: no component category=PowerSystems.Component name=201_HYDRO_4_RESERVOIR_head └ @ InfrastructureSystems ~/.julia/packages/InfrastructureSystems/euBLZ/src/system_data.jl:870 [ Info: Serialized time series data to /home/runner/.julia/packages/PowerSystemCaseBuilder/zW01F/data/serialized_system/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/RTS_GMLC_DA_sys_time_series_storage.h5. [ Info: Serialized System to /home/runner/.julia/packages/PowerSystemCaseBuilder/zW01F/data/serialized_system/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/RTS_GMLC_DA_sys.json [ Info: Serialized System metadata to /home/runner/.julia/packages/PowerSystemCaseBuilder/zW01F/data/serialized_system/e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855/RTS_GMLC_DA_sys_metadata.json [ Info: Unit System changed to InfrastructureSystems.UnitSystemModule.UnitSystem.SYSTEM_BASE = 0julia> ptdf = PTDF(sys);[ Info: Finding subnetworks via iterative union findjulia> lodf = LODF(sys);[ Info: Finding subnetworks via iterative union findjulia> vmodf = VirtualMODF(sys);[ Info: Finding subnetworks via iterative union find ┌ Warning: No outage supplemental attributes found in system. VirtualMODF contingency cache is empty. └ @ PowerNetworkMatrices ~/work/PowerNetworkMatrices.jl/PowerNetworkMatrices.jl/src/virtual_modf_calculations.jl:426
VirtualMODF is the most general object — it can compute post-modification PTDF rows under any contingency. We also build PTDF and LODF up front because the pre-contingency and single-element-outage sections below use them directly (faster than going through Woodbury when those special cases apply).
GSF and LSF (no contingency, point source)
The simplest special case sets $k_v = 0$ (the slack absorbs the sink) and $s_v = e_b$ (a unit vector at one bus). The unified formula collapses to a single PTDF entry — this is the Generation Shift Factor:
julia> m = (107, 203); # monitored arc AB1 (Area 1 → Area 2)julia> b = 101; # injection bus in Area 1julia> gsf = ptdf[m, b]0.06472612328595928
The Load Shift Factor is the same quantity with the opposite sign (loads withdraw power instead of inject):
julia> lsf = -gsf-0.06472612328595928
Subsystem-aggregated GSF
In practice analysts care about a subsystem of generators (for example, all generators in an area) rather than a single bus. Build a participation vector by weighting each generator's bus by its Pmax share within the subsystem, then dot the vector with the PTDF row:
julia> area1_gens = filter( g -> PSY.get_name(PSY.get_area(PSY.get_bus(g))) == "1", collect(PSY.get_available_components(PSY.Generator, sys)), );julia> total_pmax = sum(PSY.get_max_active_power, area1_gens);julia> src_weights = Dict{Int, Float64}();julia> for g in area1_gens bn = PSY.get_number(PSY.get_bus(g)) src_weights[bn] = get(src_weights, bn, 0.0) + PSY.get_max_active_power(g) / total_pmax endjulia> gsf_area1 = sum(w * ptdf[m, bn] for (bn, w) in src_weights)0.03929482595488813
gsf_area1 is the fraction of an aggregate 1 MW dispatch increase across all Area 1 generators (split by Pmax) that lands on AB1. The slack still absorbs the corresponding withdrawal — this is a one-sided shift.
If the swing should be distributed across many buses instead of falling on the single reference bus, pass a dist_slack dictionary to the PTDF constructor (see the PTDF matrix tutorial). That is a different concept from subsystem aggregation: dist_slack redefines the reference, whereas the participation vector above defines the source of the transfer.
Transfer DFAX (no contingency, multi-bus source and sink)
When both source and sink are subsystems, the unified formula is the difference of two weighted PTDF row dot-products:
\[\mathrm{TDF}(m,\ s \to k) \;=\; \mathrm{PTDF}[m,\,:] \cdot s_v \;-\; \mathrm{PTDF}[m,\,:] \cdot k_v.\]
Build the sink vector from Area 2 loads, max-active-power weighted. We filter to PSY.PowerLoad because the abstract ElectricLoad type also covers shunt admittance components (FixedAdmittance) that don't carry a real-power weight:
julia> area2_loads = filter( l -> PSY.get_name(PSY.get_area(PSY.get_bus(l))) == "2", collect(PSY.get_available_components(PSY.PowerLoad, sys)), );julia> total_load = sum(PSY.get_max_active_power, area2_loads);julia> snk_weights = Dict{Int, Float64}();julia> for l in area2_loads bn = PSY.get_number(PSY.get_bus(l)) snk_weights[bn] = get(snk_weights, bn, 0.0) + PSY.get_max_active_power(l) / total_load end
The pre-contingency transfer DFAX for Area 1 → Area 2 on AB1 is then:
julia> tdf_pre = sum(w * ptdf[m, bn] for (bn, w) in src_weights) - sum(w * ptdf[m, bn] for (bn, w) in snk_weights)0.16827314506700808
tdf_pre answers: if Area 1 ramps up by 1 MW (split by generator Pmax) and Area 2's load grows by 1 MW (split by load size), what fraction of that transfer shows up on AB1? In market and TLR settings, this is the pre-contingency component of the flowgate impact.
OTDF (single contingency, point source)
The Outage Transfer Distribution Factor is the GSF you would observe if a specific outage were already in effect. For a single-element contingency on arc $c$, OTDF has a closed-form expression in terms of PTDF and LODF:
\[\mathrm{OTDF}(m, b, c) \;=\; \mathrm{PTDF}[m, b] + \mathrm{LODF}[m, c] \cdot \mathrm{PTDF}[c, b].\]
This is the unified formula with $C = \{c\}$ and the slack absorbing the sink. VirtualMODF computes the same quantity through the Woodbury identity, which generalizes naturally to multi-element contingencies (see the N-k section below). For a single outage the two routes agree:
julia> c = (113, 215); # contingency: AB2 outagejulia> otdf_closed = ptdf[m, b] + lodf[m, c] * ptdf[c, b]0.04170904684929677julia> ctg = NetworkModification(vmodf, c);julia> row_c = vmodf[m, ctg];julia> bus_lookup = PNM.get_bus_lookup(vmodf);julia> otdf_vmodf = row_c[bus_lookup[b]]0.04170904684929675julia> isapprox(otdf_closed, otdf_vmodf; rtol = 1e-10)true
The isapprox check is the tutorial's internal validation: VirtualMODF and the closed-form LODF expansion are the same calculation expressed two different ways. Whenever both apply, they agree to floating-point tolerance.
Flowgate DFAX (single contingency, source–sink transfer)
A flowgate in NERC parlance is the pair (monitored facility, contingency). The flowgate DFAX is the unified formula with both nontrivial source/sink vectors and a nonempty $C$: the source–sink subtraction from the transfer-DFAX section applied to the post-contingency row from the OTDF section. We reuse row_c, src_weights, and snk_weights already in scope:
julia> flowgate_dfax = sum(w * row_c[bus_lookup[bn]] for (bn, w) in src_weights) - sum(w * row_c[bus_lookup[bn]] for (bn, w) in snk_weights)0.22721744953725392
The NERC 5% rule treats a transfer as a "significant" contributor to a flowgate when the absolute DFAX exceeds 0.05. The check is one line:
julia> significant = abs(flowgate_dfax) >= 0.05true
When significant == true, the transfer is subject to curtailment or mitigation under the relevant TLR / CMP procedure.
N-k DFAX (multi-element contingency)
When the contingency C contains more than one element, the closed-form LODF expansion of the OTDF section no longer applies — there is no scalar LODF[m, c] when c is itself a set. The unified formula still applies, and VirtualMODF is built to handle it directly. Pass a vector of arc tuples to NetworkModification:
julia> ctg_n2 = NetworkModification(vmodf, [(113, 215), (123, 217)]); # AB2 and AB3 outagedERROR: MethodError: no method matching NetworkModification(::VirtualMODF{Tuple{Vector{Tuple{Int64, Int64}}, Vector{Int64}}, Tuple{Dict{Tuple{Int64, Int64}, Int64}, Dict{Int64, Int64}}, PowerNetworkMatrices.KLUWrapper.KLULinSolveCache{Float64, Int64}}, ::Vector{Tuple{Int64, Int64}}) The type `NetworkModification` exists, but no method is defined for this combination of argument types when trying to construct it. Closest candidates are: NetworkModification(::PowerNetworkMatrices.PowerNetworkMatrix, ::Tuple{Int64, Int64}) @ PowerNetworkMatrices ~/work/PowerNetworkMatrices.jl/PowerNetworkMatrices.jl/src/network_modification.jl:97 NetworkModification(::PowerNetworkMatrices.PowerNetworkMatrix, ::PowerSystems.ACTransmission) @ PowerNetworkMatrices ~/work/PowerNetworkMatrices.jl/PowerNetworkMatrices.jl/src/network_modification.jl:115 NetworkModification(::PowerNetworkMatrices.PowerNetworkMatrix, ::PowerSystems.System, ::PowerSystems.Outage) @ PowerNetworkMatrices ~/work/PowerNetworkMatrices.jl/PowerNetworkMatrices.jl/src/network_modification.jl:143 ...julia> row_n2 = vmodf[m, ctg_n2];ERROR: UndefVarError: `ctg_n2` not defined in `Main` Suggestion: add an appropriate import or assignment. This global was declared but not assigned.julia> flowgate_dfax_n2 = sum(w * row_n2[bus_lookup[bn]] for (bn, w) in src_weights) - sum(w * row_n2[bus_lookup[bn]] for (bn, w) in snk_weights)ERROR: UndefVarError: `row_n2` not defined in `Main` Suggestion: add an appropriate import or assignment. This global was declared but not assigned.
Removing two of the three parallel Area 1 → Area 2 paths forces a much larger fraction of any inter-area transfer onto AB1, so flowgate_dfax_n2 is substantially larger than the N-1 value computed in the previous section. The same indexing call (vmodf[m, ctg]) handles N-1, N-2, and higher orders — that is the operational advantage of going through VirtualMODF.
Capstone: assembling a DFAX report
In production use, an analyst typically wants a table of distribution factors covering several transfers, several monitored facilities, and several contingencies — the kind of report that a PSS/E .dfx file plus IDC post-processing produces. Building that report is one nested loop around the unified formula.
For this example we use one transfer (Area 1 → Area 2 from the transfer DFAX section), three monitored arcs (the three parallel Area 1 → Area 2 paths), and three contingencies (each of the other two paths individually, plus the N-2 double-outage from the previous section). Each contingency carries the set of arc tuples it outages so that we can skip the ill-defined case of monitoring an outaged element:
julia> monitored = [(107, 203), (113, 215), (123, 217)]; # AB1, AB2, AB3julia> contingencies = [ ("AB2 out", Set([(113, 215)]), NetworkModification(vmodf, (113, 215))), ("AB3 out", Set([(123, 217)]), NetworkModification(vmodf, (123, 217))), ( "AB2 & AB3 out", Set([(113, 215), (123, 217)]), NetworkModification(vmodf, [(113, 215), (123, 217)]), ), ];ERROR: MethodError: no method matching NetworkModification(::VirtualMODF{Tuple{Vector{Tuple{Int64, Int64}}, Vector{Int64}}, Tuple{Dict{Tuple{Int64, Int64}, Int64}, Dict{Int64, Int64}}, PowerNetworkMatrices.KLUWrapper.KLULinSolveCache{Float64, Int64}}, ::Vector{Tuple{Int64, Int64}}) The type `NetworkModification` exists, but no method is defined for this combination of argument types when trying to construct it. Closest candidates are: NetworkModification(::PowerNetworkMatrices.PowerNetworkMatrix, ::Tuple{Int64, Int64}) @ PowerNetworkMatrices ~/work/PowerNetworkMatrices.jl/PowerNetworkMatrices.jl/src/network_modification.jl:97 NetworkModification(::PowerNetworkMatrices.PowerNetworkMatrix, ::PowerSystems.ACTransmission) @ PowerNetworkMatrices ~/work/PowerNetworkMatrices.jl/PowerNetworkMatrices.jl/src/network_modification.jl:115 NetworkModification(::PowerNetworkMatrices.PowerNetworkMatrix, ::PowerSystems.System, ::PowerSystems.Outage) @ PowerNetworkMatrices ~/work/PowerNetworkMatrices.jl/PowerNetworkMatrices.jl/src/network_modification.jl:143 ...julia> rows = NamedTuple[]NamedTuple[]julia> for mon in monitored for (label, outaged, ctg_k) in contingencies mon in outaged && continue row = vmodf[mon, ctg_k] df = sum(w * row[bus_lookup[bn]] for (bn, w) in src_weights) - sum(w * row[bus_lookup[bn]] for (bn, w) in snk_weights) push!( rows, ( monitored = mon, contingency = label, dfax = df, significant = abs(df) >= 0.05, ), ) end endERROR: UndefVarError: `contingencies` not defined in `Main` Suggestion: add an appropriate import or assignment. This global was declared but not assigned.julia> report = sort(DataFrame(rows), :dfax; by = abs, rev = true)ERROR: ArgumentError: column name "dfax" not found in the data frame since it has no columns
The sort by abs(dfax) puts the largest flowgate impacts at the top — the ones a TLR coordinator would investigate first. Filtering to report[report.significant, :] would keep only NERC-significant rows.
This table is the kind of output that drives downstream congestion and seams-coordination workflows; assembling it requires nothing beyond the matrices in this tutorial.
When to use which primitive
| Need | Reach for |
|---|---|
| Many transfers, no contingencies | PTDF |
| One contingency, all monitored branches | LODF + PTDF |
Specific flowgates (monitored, contingency) pairs | VirtualMODF |
| Multi-element / N-k contingencies | VirtualMODF |
| Memory-constrained or sparse usage | VirtualPTDF / VirtualLODF / VirtualMODF |
VirtualMODF is strictly more general than LODF-based OTDF arithmetic, but the closed-form route in the OTDF section is faster when you only have a single outage and many bus injections to evaluate. The decision is about which direction of the matrix you traverse most often, not about which one is "correct".