IEEE39 Bus Tutorial - Part I: Model Creation

This tutorial can be downloaded as a normal Julia script here.

This is the first part of a four-part tutorial series for the IEEE 39-bus test system:

  • Part I: Model Creation (this tutorial) - Build the network structure with buses, lines, and components
  • Part II: Initialization - Perform power flow calculations and dynamic initialization
  • Part III: Dynamic Simulation - Run time-domain simulations and analyze system behavior
  • Part IV: Advanced Modeling & Parameter Optimization - Create custom components and optimize system parameters

In this first part, we'll construct the complete IEEE 39-bus network model using PowerDynamics.jl, including generators, loads, transmission lines, and control systems.

System Structure

The system consists of 39 buses (with 10 generators and 19 loads) and 46 branches (12 of which are transformers).

The buses fall into the following categories:

  • Junction: pure transient buses without dynamic components
  • Load: buses with loads only
  • Controlled Machine: buses with controlled machines (generators with AVR and GOV)
  • Controlled Machine + Load: buses with controlled machines and loads
  • Uncontrolled Machine + Load: buses with uncontrolled machines and loads

For the power flow solution, we have a slack bus, PV buses and PQ buses.

For the dynamic simulation, we will use the following models:

  • ZIP Load for loads,
  • 6th Order Sauer-Pai Machine and
  • AVR Type I and TGOV1 for controlled machines.

Setup and Data Loading

No Standardized Data Import

As of now, PowerDynamics.jl does not support any advanced import mechanisms for power grids. Therefore, this tutorial loads the data from some custom CSV files.

First, we'll load the required packages and read the system data from CSV files. The IEEE 39-bus system data is organized into separate files for different components.

using PowerDynamics
using PowerDynamics.Library
using ModelingToolkitBase
using NetworkDynamics
using DataFrames
using CSV

DATA_DIR = joinpath(pkgdir(PowerDynamics), "docs", "examples", "ieee39data")

The system data is stored in CSV files containing:

bus.csv - Bus Configuration Data
ParameterDescription
busBus number (unique identifier)
bus_typePower flow bus type: "PQ" (load), "PV" (generator), "Slack" (reference)
categoryComponent category: "junction", "load", "ctrld_machine", "ctrld_machine_load", "unctrld_machine_load"
PActive power injection [pu] (positive = generation, negative = load)
QReactive power injection [pu] (positive = generation, negative = load)
VVoltage magnitude [pu] (for PV and Slack buses)
base_kvBase voltage level [kV]
has_loadBoolean flag indicating presence of load component
has_genBoolean flag indicating presence of generator component
has_avrBoolean flag indicating presence of automatic voltage regulator
has_govBoolean flag indicating presence of turbine governor
branch.csv - Transmission Line and Transformer Data
ParameterDescription
src_busSource bus number
dst_busDestination bus number
transformerTransformer flag (0 = line, 1 = transformer)
r_srcSource end transformation ratio [pu]
RSeries resistance [pu]
XSeries reactance [pu]
G_srcSource end shunt conductance [pu]
G_dstDestination end shunt conductance [pu]
B_srcSource end shunt susceptance [pu]
B_dstDestination end shunt susceptance [pu]
load.csv - ZIP Load Model Parameters
ParameterDescription
busBus number where load is connected
PsetActive power at operation point [pu]
QsetReactive power at operation point [pu]
KpZActive power constant impedance fraction
KqZReactive power constant impedance fraction
KpIActive power constant current fraction
KqIReactive power constant current fraction
KpCActive power constant power fraction (1-KpZ-KpI)
KqCReactive power constant power fraction (1-KqZ-KqI)

Note: ZIP loads combine constant impedance (Z), constant current (I), and constant power (P) components.

machine.csv - Generator (Sauer-Pai Machine) Parameters
ParameterDescription
busBus number where generator is connected
SnMachine power rating [MVA]
V_bBus voltage base [kV] (unused: duplicates base_kv in bus.csv)
VnMachine voltage rating [kV]
R_sStator resistance [pu]
X_lsStator leakage reactance [pu]
X_dd-axis synchronous reactance [pu]
X_qq-axis synchronous reactance [pu]
X′_dd-axis transient reactance [pu]
X′_qq-axis transient reactance [pu]
X″_dd-axis subtransient reactance [pu]
X″_qq-axis subtransient reactance [pu]
T′_d0d-axis transient time constant [s]
T′_q0q-axis transient time constant [s]
T″_d0d-axis subtransient time constant [s]
T″_q0q-axis subtransient time constant [s]
HInertia constant [s]
DDirect shaft damping coefficient
avr.csv - Automatic Voltage Regulator (AVR Type I) Parameters
ParameterDescription
busBus number where AVR-controlled generator is located
KaAmplifier gain
KeField circuit integral deviation
KfStabilizer gain
TaAmplifier time constant [s]
TfStabilizer time constant [s]
TeField circuit time constant [s]
TrMeasurement time constant [s]
vr_minMinimum regulator voltage [pu]
vr_maxMaximum regulator voltage [pu]
E1First ceiling voltage [pu]
Se1First ceiling saturation factor
E2Second ceiling voltage [pu]
Se2Second ceiling saturation factor
gov.csv - Turbine Governor (TGOV1) Parameters
ParameterDescription
busBus number where governor-controlled generator is located
V_minMinimum valve position [pu]
V_maxMaximum valve position [pu]
RGovernor droop [Machine PU]
T1First transient time constant [s]
T2Second transient time constant [s]
T3Third transient time constant [s]
DTTurbine damping coefficient
ω_refFrequency setpoint [pu]
branch_df = CSV.read(joinpath(DATA_DIR, "branch.csv"), DataFrame)
bus_df = CSV.read(joinpath(DATA_DIR, "bus.csv"), DataFrame)
load_df = CSV.read(joinpath(DATA_DIR, "load.csv"), DataFrame)
machine_df = CSV.read(joinpath(DATA_DIR, "machine.csv"), DataFrame)
avr_df = CSV.read(joinpath(DATA_DIR, "avr.csv"), DataFrame)
gov_df = CSV.read(joinpath(DATA_DIR, "gov.csv"), DataFrame)

System base values follow the IEEE 39-bus standard. Sbase and the frequency base are global in PowerDynamics: they are set once, before any model is constructed, and every component built afterwards bakes them in. Since they are read at construction time, changing them later has no effect on models that already exist — and by the same token, a script that changes them should restore the defaults when it is done, which we do at the very end of this page.

BASE_MVA = 100.0
BASE_FREQ = 60.0
set_Sbase!(BASE_MVA)
set_fbase!(BASE_FREQ)

Subcomponent Definition

As stated above, our buses fall into 5 different categories. We will define a "template" for each of those categories and then create the individual buses from those templates. By doing so, we can reach substantial performance improvements, as we do not have to repeatedly compile the same models (the symbolic simplification is quite costly). Instead, we copy the templates and adjust parameters.

However, before we can define the bus templates, we need to define the individual subcomponents. Those subcomponents are MTK models and not yet compiled node models. See Modeling Concepts and the custom bus tutorial.

Load Model

We use the ZIP load model to represent loads. This model satisfies the Injector Interface.

(t) ┌──────────┐
 o──┤ ZIP Load │
    └──────────┘

Every parameter that we are going to read from the CSV files is declared free by passing nothing instead of a value. A library model normally comes with sensible defaults; passing nothing removes the default for that parameter, so the compiled model carries the parameter but no value for it. That way the CSV data is the single source of truth: if a column is missing, initialization fails loudly instead of silently falling back to a library default.

load = ZIPLoad(;
    name=:ZIPLoad,
    Pset=nothing, Qset=nothing,
    KpZ=nothing, KqZ=nothing,
    KpI=nothing, KqI=nothing,
    KpC=nothing, KqC=nothing,
)

Generator Models

For generators, we use the Sauer-Pai machine model, which is a 6th-order synchronous machine model. We create two variants:

Uncontrolled Machine: No external control inputs for mechanical torque or field voltage. This model satisfies the Injector Interface directly.

(t) ┌─────────┐
 o──┤ Machine │
    └─────────┘
# all machine parameters come from machine.csv, so they are declared free
unset_machine_p = (;
    Sn=nothing, Vn=nothing, R_s=nothing, X_ls=nothing,
    X_d=nothing, X_q=nothing, X′_d=nothing, X′_q=nothing, X″_d=nothing, X″_q=nothing,
    T′_d0=nothing, T′_q0=nothing, T″_d0=nothing, T″_q0=nothing,
    H=nothing, D=nothing,
)

uncontrolled_machine = SauerPaiMachine(;
    τ_m_input=false,  ## No external mechanical torque input
    vf_input=false,   ## No external field voltage input
    name=:machine,
    unset_machine_p...,
)

Controlled Machine: Includes automatic voltage regulator (AVR) and turbine governor controls.

The controlled machine is modeled as a composite injector. It consists of 3 subcomponents: the machine, the AVR and the governor. The AVR receives the voltage magnitude measurement from the terminal of the machine and sets the field voltage. The governor receives the frequency measurement and sets the mechanical torque. Together, they satisfy the Injector Interface.

      ┌───────────────────────────────┐
      │ CtrldMachine  u_mag_meas      │
      │              ╭─────→────╮     │
      │    ┌─────────┴─┐      ┌─┴───┐ │
  (t) │    │           ├───←──┤ AVR │ │
   o──┼────┤ Sauer-Pai │ vf   └─────┘ │
      │    │ Machine   │ τ_m  ┌─────┐ │
      │    │           ├───←──┤ Gov │ │
      │    └─────────┬─┘      └─┬───┘ │
      │              ╰─────→────╯     │
      │                 ω_meas        │
      └───────────────────────────────┘
_machine = SauerPaiMachine(;
    name=:machine,
    unset_machine_p...,
)
_avr = AVRTypeI(;
    name=:avr,
    ceiling_function=:quadratic,
    # from avr.csv
    Ka=nothing, Ke=nothing, Kf=nothing,
    Ta=nothing, Tf=nothing, Te=nothing, Tr=nothing,
    vr_min=nothing, vr_max=nothing,
    E1=nothing, Se1=nothing, E2=nothing, Se2=nothing,
)
_gov = TGOV1(;
    name=:gov,
    # from gov.csv
    V_min=nothing, V_max=nothing, R=nothing,
    T1=nothing, T2=nothing, T3=nothing, DT=nothing, ω_ref=nothing,
)

controlled_machine = CompositeInjector(
    [_machine, _avr, _gov],
    name=:ctrld_gen
)

Bus Template Creation

Now we have all the components (i.e., the MTK models) so we can combine them into full bus models and compile the methods.

Junction Bus

Pure transmission buses with no generation or load

           ╔══════════════════════╗
           ║ Junction (compiled)  ║
 Network   ║  ┌─────────────────┐ ║
interface  ║  │MTKBus           │ ║
 current ────→│┌──────┐         │ ║
           ║  ││BusBar│(nothing)│ ║
 voltage ←────│└──────┘         │ ║
           ║  └─────────────────┘ ║
           ╚══════════════════════╝

Note that the per-unit bases need no attention here. Sbase and ωbase were set globally at the top of this page and are picked up by every compile_bus call below; they end up on the bus's systembase, and each device's own Sbase/ωbase are structurally bound to it rather than being per-device parameters. The one genuinely per-bus base, busbar₊Vbase, is set from bus.csv when we instantiate the individual buses further down.

@named junction_bus_template = compile_bus(MTKBus())
VertexModel :junction_bus_template PureStateMap()
 ├─ 2 inputs:  [busbar₊i_r, busbar₊i_i]
 ├─ 2 states:  [busbar₊u_r=1, busbar₊u_i=0]
 |    with diagonal mass matrix [0, 0]
 ├─ 2 outputs: [busbar₊u_r=1, busbar₊u_i=0]
 └─ 4 params:  [busbar₊Vbase=1, systembase₊Sbase=100, systembase₊ωbase=376.99, systembase₊ωframe=1]

Load Bus

Buses with only load components

           ╔═════════════════════╗
           ║ Load (compiled)     ║
 Network   ║  ┌────────────────┐ ║
interface  ║  │MTKBus          │ ║
 current ────→│┌──────┐ ┌────┐ │ ║
           ║  ││BusBar├o┤Load│ │ ║
 voltage ←────│└──────┘ └────┘ │ ║
           ║  └────────────────┘ ║
           ╚═════════════════════╝
@named load_bus_template = compile_bus(MTKBus(load))
VertexModel :load_bus_template PureStateMap()
 ├─  2 inputs:  [busbar₊i_r, busbar₊i_i]
 ├─  2 states:  [busbar₊u_r=1, busbar₊u_i=0]
 |     with diagonal mass matrix [0, 0]
 ├─  2 outputs: [busbar₊u_r=1, busbar₊u_i=0]
 └─ 13 params:  [busbar₊Vbase=1, systembase₊Sbase=100, systembase₊ωbase=376.99, systembase₊ωframe=1, ZIPLoad₊Pset≈-1, ZIPLoad₊Qset≈0, ZIPLoad₊Vset≈1, ZIPLoad₊KpZ, ZIPLoad₊KqZ, ZIPLoad₊KpI, ZIPLoad₊KqI, ZIPLoad₊KpC, ZIPLoad₊KqC]

Generator Bus (Controlled)

Buses with controlled generators (machine + AVR + governor)

            ╔════════════════════════════════════════════════╗
            ║ Ctrld Machine Bus (compiled)                   ║
            ║  ┌───────────────────────────────────────────┐ ║
            ║  │MTKBus                                     │ ║
            ║  │         ┌───────────────────────────────┐ │ ║
  Network   ║  │         │CtrldMachine  ╭─────→────╮     │ │ ║
 interface  ║  │         │    ┌─────────┴─┐      ┌─┴───┐ │ │ ║
  current ────→│┌──────┐ │    │           ├───←──┤ AVR │ │ │ ║
            ║  ││BusBar├o┼────┤ Sauer-Pai │      └─────┘ │ │ ║
  voltage ←────│└──────┘ │    │ Machine   │      ┌─────┐ │ │ ║
            ║  │         │    │           ├───←──┤ Gov │ │ │ ║
            ║  │         │    └─────────┬─┘      └─┬───┘ │ │ ║
            ║  │         │              ╰─────→────╯     │ │ ║
            ║  │         └───────────────────────────────┘ │ ║
            ║  └───────────────────────────────────────────┘ ║
            ╚════════════════════════════════════════════════╝
@named ctrld_machine_bus_template = compile_bus(
    MTKBus(controlled_machine);
)
VertexModel :ctrld_machine_bus_template PureStateMap()
 ├─  2 inputs:  [busbar₊i_r, busbar₊i_i]
 ├─ 14 states:  [ctrld_gen₊gov₊xg2≈0, ctrld_gen₊gov₊xg1≈1, ctrld_gen₊avr₊v_fb≈0, ctrld_gen₊avr₊vfout≈1, ctrld_gen₊avr₊vr≈0, ctrld_gen₊avr₊vm≈1, ctrld_gen₊machine₊ψ″_q≈0, ctrld_gen₊machine₊ψ″_d≈1, ctrld_gen₊machine₊E′_d≈0, ctrld_gen₊machine₊E′_q≈1, ctrld_gen₊machine₊ω≈1, ctrld_gen₊machine₊δ≈0, busbar₊u_r=1, busbar₊u_i=0]
 |     with diagonal mass matrix [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0]
 ├─  2 outputs: [busbar₊u_r=1, busbar₊u_i=0]
 ├─ 43 params:  [busbar₊Vbase=1, systembase₊Sbase=100, systembase₊ωbase=376.99, systembase₊ωframe=1, ctrld_gen₊machine₊R_s, ctrld_gen₊machine₊X_d, ctrld_gen₊machine₊X_q, ctrld_gen₊machine₊X′_d, ctrld_gen₊machine₊X′_q, ctrld_gen₊machine₊X″_d, ctrld_gen₊machine₊X″_q, ctrld_gen₊machine₊X_ls, ctrld_gen₊machine₊T′_d0, ctrld_gen₊machine₊T″_d0, ctrld_gen₊machine₊T′_q0, ctrld_gen₊machine₊T″_q0, ctrld_gen₊machine₊H, ctrld_gen₊machine₊D, ctrld_gen₊machine₊Sn, ctrld_gen₊machine₊Vn, ctrld_gen₊avr₊Tr, ctrld_gen₊avr₊vref≈1, ctrld_gen₊avr₊Ka, ctrld_gen₊avr₊Ke, ctrld_gen₊avr₊Kf, ctrld_gen₊avr₊Ta, ctrld_gen₊avr₊Tf, ctrld_gen₊avr₊Te, ctrld_gen₊avr₊vr_min, ctrld_gen₊avr₊vr_max, ctrld_gen₊avr₊E1, ctrld_gen₊avr₊E2, ctrld_gen₊avr₊Se1, ctrld_gen₊avr₊Se2, ctrld_gen₊gov₊ω_ref, ctrld_gen₊gov₊p_ref≈1, ctrld_gen₊gov₊V_min, ctrld_gen₊gov₊V_max, ctrld_gen₊gov₊R, ctrld_gen₊gov₊T1, ctrld_gen₊gov₊T2, ctrld_gen₊gov₊T3, ctrld_gen₊gov₊DT]
 └─  2 InitFormula clusters:
       explicitly initializes 1/51 variables from seeds [1 already set param]
       explicitly initializes 1/51 variables from seeds [1 already set param]

Generator + Load Bus (Controlled)

Buses with both controlled generators and loads

            ╔═════════════════════════════════════════════════╗
            ║ Ctrld Machine Load Bus (compiled)               ║
            ║  ┌────────────────────────────────────────────┐ ║
            ║  │MTKBus    ┌───────────────────────────────┐ │ ║
            ║  │          │CtrldMachine  ╭─────→────╮     │ │ ║
            ║  │          │    ┌─────────┴─┐      ┌─┴───┐ │ │ ║
            ║  │          │    │           ├───←──┤ AVR │ │ │ ║
  Network   ║  │        ┌─┼────┤ Sauer-Pai │      └─────┘ │ │ ║
 interface  ║  │        │ │    │ Machine   │      ┌─────┐ │ │ ║
  current ────→│┌──────┐│ │    │           ├───←──┤ Gov │ │ │ ║
            ║  ││BusBar├o │    └─────────┬─┘      └─┬───┘ │ │ ║
  voltage ←────│└──────┘│ │              ╰─────→────╯     │ │ ║
            ║  │        │ └───────────────────────────────┘ │ ║
            ║  │        │ ┌──────┐                          │ ║
            ║  │        └─┤ Load │                          │ ║
            ║  │          └──────┘                          │ ║
            ║  └────────────────────────────────────────────┘ ║
            ╚═════════════════════════════════════════════════╝
@named ctrld_machine_load_bus_template = compile_bus(
    MTKBus(controlled_machine, load);
)
VertexModel :ctrld_machine_load_bus_template PureStateMap()
 ├─  2 inputs:  [busbar₊i_r, busbar₊i_i]
 ├─ 14 states:  [ctrld_gen₊gov₊xg2≈0, ctrld_gen₊gov₊xg1≈1, ctrld_gen₊avr₊v_fb≈0, ctrld_gen₊avr₊vfout≈1, ctrld_gen₊avr₊vr≈0, ctrld_gen₊avr₊vm≈1, ctrld_gen₊machine₊ψ″_q≈0, ctrld_gen₊machine₊ψ″_d≈1, ctrld_gen₊machine₊E′_d≈0, ctrld_gen₊machine₊E′_q≈1, ctrld_gen₊machine₊ω≈1, ctrld_gen₊machine₊δ≈0, busbar₊u_r=1, busbar₊u_i=0]
 |     with diagonal mass matrix [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0]
 ├─  2 outputs: [busbar₊u_r=1, busbar₊u_i=0]
 ├─ 52 params:  [busbar₊Vbase=1, systembase₊Sbase=100, systembase₊ωbase=376.99, systembase₊ωframe=1, ctrld_gen₊machine₊R_s, ctrld_gen₊machine₊X_d, ctrld_gen₊machine₊X_q, ctrld_gen₊machine₊X′_d, ctrld_gen₊machine₊X′_q, ctrld_gen₊machine₊X″_d, ctrld_gen₊machine₊X″_q, ctrld_gen₊machine₊X_ls, ctrld_gen₊machine₊T′_d0, ctrld_gen₊machine₊T″_d0, ctrld_gen₊machine₊T′_q0, ctrld_gen₊machine₊T″_q0, ctrld_gen₊machine₊H, ctrld_gen₊machine₊D, ctrld_gen₊machine₊Sn, ctrld_gen₊machine₊Vn, ctrld_gen₊avr₊Tr, ctrld_gen₊avr₊vref≈1, ctrld_gen₊avr₊Ka, ctrld_gen₊avr₊Ke, ctrld_gen₊avr₊Kf, ctrld_gen₊avr₊Ta, ctrld_gen₊avr₊Tf, ctrld_gen₊avr₊Te, ctrld_gen₊avr₊vr_min, ctrld_gen₊avr₊vr_max, ctrld_gen₊avr₊E1, ctrld_gen₊avr₊E2, ctrld_gen₊avr₊Se1, ctrld_gen₊avr₊Se2, ctrld_gen₊gov₊ω_ref, ctrld_gen₊gov₊p_ref≈1, ctrld_gen₊gov₊V_min, ctrld_gen₊gov₊V_max, ctrld_gen₊gov₊R, ctrld_gen₊gov₊T1, ctrld_gen₊gov₊T2, ctrld_gen₊gov₊T3, ctrld_gen₊gov₊DT, ZIPLoad₊Pset≈-1, ZIPLoad₊Qset≈0, ZIPLoad₊Vset≈1, ZIPLoad₊KpZ, ZIPLoad₊KqZ, ZIPLoad₊KpI, ZIPLoad₊KqI, ZIPLoad₊KpC, ZIPLoad₊KqC]
 └─  2 InitFormula clusters:
       explicitly initializes 1/60 variables from seeds [1 already set param]
       explicitly initializes 1/60 variables from seeds [1 already set param]

Generator + Load Bus (Uncontrolled)

Buses with uncontrolled generators and loads

            ╔════════════════════════════════╗
            ║ Unctr. Ma. Load Bus (compiled) ║
            ║  ┌────────────────────────┐    ║
  Network   ║  │MTKBus      ┌─────────┐ │    ║
 interface  ║  │          ┌─┤ Machine │ │    ║
  current ────→│ ┌──────┐ │ └─────────┘ │    ║
            ║  │ │BusBar├─o             │    ║
  voltage ←────│ └──────┘ │ ┌──────┐    │    ║
            ║  │          └─┤ Load │    │    ║
            ║  │            └──────┘    │    ║
            ║  └────────────────────────┘    ║
            ╚════════════════════════════════╝
@named unctrld_machine_load_bus_template = compile_bus(
    MTKBus(uncontrolled_machine, load);
)
VertexModel :unctrld_machine_load_bus_template PureStateMap()
 ├─  2 inputs:  [busbar₊i_r, busbar₊i_i]
 ├─  8 states:  [machine₊ψ″_q≈0, machine₊ψ″_d≈1, machine₊E′_d≈0, machine₊E′_q≈1, machine₊ω≈1, machine₊δ≈0, busbar₊u_r=1, busbar₊u_i=0]
 |     with diagonal mass matrix [1, 1, 1, 1, 1, 1, 0, 0]
 ├─  2 outputs: [busbar₊u_r=1, busbar₊u_i=0]
 ├─ 31 params:  [busbar₊Vbase=1, systembase₊Sbase=100, systembase₊ωbase=376.99, systembase₊ωframe=1, machine₊vf_set≈1, machine₊τ_m_set≈1, machine₊R_s, machine₊X_d, machine₊X_q, machine₊X′_d, machine₊X′_q, machine₊X″_d, machine₊X″_q, machine₊X_ls, machine₊T′_d0, machine₊T″_d0, machine₊T′_q0, machine₊T″_q0, machine₊H, machine₊D, machine₊Sn, machine₊Vn, ZIPLoad₊Pset≈-1, ZIPLoad₊Qset≈0, ZIPLoad₊Vset≈1, ZIPLoad₊KpZ, ZIPLoad₊KqZ, ZIPLoad₊KpI, ZIPLoad₊KqI, ZIPLoad₊KpC, ZIPLoad₊KqC]
 └─  2 InitFormula clusters:
       explicitly initializes 1/33 variables from seeds [1 already set param]
       explicitly initializes 1/33 variables from seeds [1 already set param]

Bus Instantiation and Parameter Setting

Now we create the actual bus instances by copying templates and applying specific parameters from the CSV data files.

# Helper function to apply CSV parameters to bus components
function apply_csv_params!(bus, table, bus_index)
    row_idx = findfirst(table.bus .== bus_index)

    # Apply all parameters except "bus" column
    row = table[row_idx, :]
    for col_name in names(table)
        col_name == "bus" && continue
        # `V_b` in machine.csv duplicates `base_kv` in bus.csv. The voltage base is a
        # property of the bus, not of the machine, so we take it from bus.csv below.
        col_name == "V_b" && continue
        set_default!(bus, Regex(col_name*"\$"), row[col_name])
    end
end

For each bus in the system, we:

  1. Select the appropriate template based on its category
  2. Create a bus instance with the correct vertex index and name
  3. Apply component-specific parameters from CSV files
  4. Set the power flow model (PQ, PV, or Slack)
busses = []
for row in eachrow(bus_df)
    i = row.bus

    # Select template based on bus category
    bus = if row.category == "junction"
        compile_bus(junction_bus_template; vidx=i, name=Symbol("bus$i"))
    elseif row.category == "load"
        compile_bus(load_bus_template; vidx=i, name=Symbol("bus$i"))
    elseif row.category == "ctrld_machine"
        compile_bus(ctrld_machine_bus_template; vidx=i, name=Symbol("bus$i"))
    elseif row.category == "ctrld_machine_load"
        compile_bus(ctrld_machine_load_bus_template; vidx=i, name=Symbol("bus$i"))
    elseif row.category == "unctrld_machine_load"
        compile_bus(unctrld_machine_load_bus_template; vidx=i, name=Symbol("bus$i"))
    end

    # The voltage base is the one per-unit base that is genuinely per bus (this system mixes
    # 16.5, 138, 230 and 345 kV). It only affects the SI observables (`u_kV`, `P_MW`, …) and
    # is handed down to the incident lines during initialization.
    set_default!(bus, :busbar₊Vbase, row.base_kv)

    # Apply component parameters from CSV files
    row.has_load && apply_csv_params!(bus, load_df, i)
    row.has_gen && apply_csv_params!(bus, machine_df, i)
    row.has_avr && apply_csv_params!(bus, avr_df, i)
    row.has_gov && apply_csv_params!(bus, gov_df, i)

    # Set power flow model based on bus type
    pf_model = if row.bus_type == "PQ"
        pfPQ(P=row.P, Q=row.Q)  ## Load bus: fixed P and Q
    elseif row.bus_type == "PV"
        pfPV(P=row.P, V=row.V)  ## Generator bus: fixed P and V
    elseif row.bus_type == "Slack"
        pfSlack(V=row.V, δ=0)   ## Slack bus: fixed V and angle
    end
    set_pfmodel!(bus, pf_model)

    push!(busses, bus)
end

Transmission Line Creation

The IEEE 39-bus system includes both transmission lines and transformers, all modeled using the π-line equivalent circuit model.

The model consists of several layers:

  1. The PiModel, which satisfies the Branch Interface as it has two terminals
  2. The MTKLine constructor, which creates a MTK model fulfilling the MTKLine Interface
  3. The compiled EdgeModel created by calling the compile_line constructor
       ╔══════════════════════════════════╗
       ║ EdgeModel (compiled)             ║
   src ║ ┌──────────────────────────────┐ ║ dst
vertex ║ │MTKLine                       │ ║ vertex
   u ───→│┌───────┐ ┌────────┐ ┌───────┐│←─── u
       ║ ││LineEnd├o┤ PiLine ├o┤LineEnd││ ║
   i ←───│└───────┘ └────────┘ └───────┘│───→ i
       ║ └──────────────────────────────┘ ║
       ╚══════════════════════════════════╝

(We used the PiLine_fault model since we plan on simulating short circuits later.)

A line has two voltage bases, src₊Vbase and dst₊Vbase, one per LineEnd — a transformer between the 16.5 kV generator buses and the 345 kV grid has genuinely different bases on its two ends. We do not set them here: each LineEnd declares that it inherits its Vbase from the bus it is attached to, and that inheritance is resolved during initialization, once the line knows its neighbours. Part II shows the result.

@named piline_template = compile_line(MTKLine(PiLine_fault(;name=:piline)))
EdgeModel :piline_template PureFeedForward()
 ├─ 2/2 inputs:              src=[src₊u_r, src₊u_i] dst=[dst₊u_r, dst₊u_i]
 ├─   0 states:              []  
 ├─ 2/2 outputs:             src=[src₊i_r, src₊i_i] dst=[dst₊i_r, dst₊i_i]
 ├─  21 params:              [src₊Vbase, dst₊Vbase, systembase₊Sbase=100, systembase₊ωbase=376.99, systembase₊ωframe=1, piline₊R, piline₊X, piline₊G_src, piline₊B_src, piline₊G_dst, piline₊B_dst, piline₊r_src=1, piline₊r_dst=1, piline₊R_fault=0, piline₊X_fault=0, piline₊G_fault=0, piline₊B_fault=0, piline₊pos=0.5, piline₊active=1, piline₊shortcircuit=0, piline₊faultimp=0]
 └─   2 params default_from: :src₊Vbase ← src busbar₊Vbase, :dst₊Vbase ← dst busbar₊Vbase

Each transmission element is created by:

  1. Instantiating a line from the template with source and destination buses
  2. Setting electrical parameters (resistance, reactance, susceptance) from CSV data
branches = []
for row in eachrow(branch_df)
    # Create line instance with topology
    line = compile_line(piline_template; src=row.src_bus, dst=row.dst_bus)

    # Apply electrical parameters from CSV data
    for col_name in names(branch_df)
        if col_name ∉ ["src_bus", "dst_bus", "transformer"]
            set_default!(line, Regex(col_name*"\$"), row[col_name])
        end
    end

    push!(branches, line)
end

Network Assembly

Finally, we combine all buses and transmission lines into a complete network model. This creates the IEEE 39-bus test system ready for initialization and simulation.

nw = Network(busses, branches)
Network with 192 states and 1662 parameters
 ├─ 39 vertices (5 unique types)
 └─ 46 edges (1 unique type)
Edge-Aggregation using SequentialAggregator(+)

All models are built, so we restore the global bases to their defaults. Calling the setters without an argument does that, and it keeps this page's 60 Hz from leaking into whatever is constructed next in the same session.

set_Sbase!()
set_fbase!()
314.1592653589793

The network nw now contains the complete IEEE 39-bus model structure. In Part 2 of this tutorial series, we'll initialize this network by solving the power flow and setting up the dynamic initial conditions.


This page was generated using Literate.jl.