Tutorial Small Signal Analysis with Continuation Power Flow

Originally Contributed by: Rodrigo Henriquez-Auba

Introduction

This tutorial will introduce you to the functionality of PowerSimulationsDynamics and PowerFlows for running small signal analysis in a continuation power flow.

This tutorial presents a simulation of a two-bus system with a generator (represented with a GENROU + SEXS + TGOV1 model) at bus 1, and a load on bus 2. We will increase the load demand to observe the P-V curve and run a small-signal analysis to check if the system satisfies small-signal stability at different operating points.

Dependencies

julia> using PowerSimulationsDynamics
julia> PSID = PowerSimulationsDynamicsPowerSimulationsDynamics
julia> using PowerSystemCaseBuilder
julia> using PowerSystems
julia> using PowerFlows
julia> const PSY = PowerSystemsPowerSystems
julia> using Plots
julia> gr() # Disable Logging to avoid excessive informationPlots.GRBackend()
julia> using Logging
julia> Logging.disable_logging(Logging.Info);
julia> Logging.disable_logging(Logging.Warn);
Note

PowerSystemCaseBuilder.jl is a helper library that makes it easier to reproduce examples in the documentation and tutorials. Normally you would pass your local files to create the system data instead of calling the function build_system. For more details visit PowerSystemCaseBuilder Documentation

PowerSystems (abbreviated with PSY) is used to properly define the data structure and establish an equilibrium point initial condition with a power flow routine using PowerFlows.

Load the system

We load the system using PowerSystemCaseBuilder.jl. This system only have a generator without dynamic data on which we can use PowerFlows to generate a P-V (or nose) curve.

julia> sys_static = build_system(PSIDSystems, "2 Bus Load Tutorial")System
┌───────────────────┬─────────────┐
│ Property          │ Value       │
├───────────────────┼─────────────┤
│ Name              │             │
│ Description       │             │
│ System Units Base │ SYSTEM_BASE │
│ Base Power        │ 100.0       │
│ Base Frequency    │ 60.0        │
│ Num Components    │ 8           │
└───────────────────┴─────────────┘

Static Components
┌─────────────────┬───────┬────────────────────────┬───────────────┐
│ Type            │ Count │ Has Static Time Series │ Has Forecasts │
├─────────────────┼───────┼────────────────────────┼───────────────┤
│ ACBus           │ 2     │ false                  │ false         │
│ Arc             │ 1     │ false                  │ false         │
│ Area            │ 1     │ false                  │ false         │
│ ExponentialLoad │ 1     │ false                  │ false         │
│ Line            │ 1     │ false                  │ false         │
│ LoadZone        │ 1     │ false                  │ false         │
│ ThermalStandard │ 1     │ false                  │ false         │
└─────────────────┴───────┴────────────────────────┴───────────────┘

Note that this system contains an Exponential Load, but the parameters are set up to zero, so it behaves a Constant Power Load:

julia> first(get_components(PSY.ExponentialLoad, sys_static))load1021 (PowerSystems.ExponentialLoad):
   name: load1021
   available: true
   bus: BUS 2 (ACBus)
   active_power: 0.1
   reactive_power: 0.032799999999999996
   active_power_coefficient: 0.0
   reactive_power_coefficient: 0.0
   base_power: 100.0
   max_active_power: 0.1
   max_reactive_power: 0.032799999999999996
   services: 0-element Vector{Service}
   dynamic_injector: nothing
   ext: Dict{String, Any}()
   time_series_container:
   InfrastructureSystems.SystemUnitsSettings:
      base_value: 100.0
      unit_system: UnitSystem.SYSTEM_BASE = 0

Create a P-V curve

The next step is to run multiple power flows and store the voltage at the load and the active power. For this example we will set up the power factor to be unitary (i.e. no reactive power at the load).

julia> # Create a Power Range to change the power load active power
       P_range = 0.01:0.01:4.6;
       # Choose the power factor
julia> load_pf = 1.0;

Then create vectors to store the results

julia> # PV Curve Results
       P_load_p = Vector{Float64}();
julia> V_load_p = Vector{Float64}();

Then, we run multiple power flows in a for loop by changing the active power of the load:

julia> for p in P_range
           # Change the active power and reactive power of the load
           power = p * 1.0
           load = get_component(PSY.ExponentialLoad, sys_static, "load1021")
           set_active_power!(load, power)
           q_power = power * tan(acos(load_pf))
           set_reactive_power!(load, q_power)
           # Run Power Flow
           status = solve_ac_powerflow!(sys_static)
           if !status
               # Finish the loop if the power flow fails
               print("Power Flow failed at p = $(power)")
               break
           end
           # Obtain the bus voltage information
           bus = get_component(Bus, sys_static, "BUS 2")
           Vm = get_magnitude(bus)
           # Store values in the vectors
           push!(V_load_p, Vm)
           push!(P_load_p, power)
       end┌ Error: The powerflow solver returned convergence = false
└ @ PowerFlows ~/.julia/packages/PowerFlows/6nw6s/src/nlsolve_ac_powerflow.jl:153
┌ Error: The powerflow solver returned convergence = false
└ @ PowerFlows ~/.julia/packages/PowerFlows/6nw6s/src/nlsolve_ac_powerflow.jl:52
Power Flow failed at p = 4.53

The plot can be visualized with:

julia> plot(P_load_p,
            V_load_p,
            label = "PV Curve",
            xlabel = "Load Power [pu]",
            ylabel = "Load Bus Voltage [pu]",
            color = :black
       )Plot{Plots.GRBackend() n=1}

plot

Run Small-Signal Analysis besides the Continuation Power Flow

To run a small-signal analysis we require a dynamic model of the machine. We can use PowerSystemCaseBuilder to the load the same system, but with a dynamic model for the generator, including a GENROU + SEXS exciter + TGOV1 governor.

julia> sys = build_system(PSIDSystems, "2 Bus Load Tutorial GENROU")System
┌───────────────────┬─────────────┐
│ Property          │ Value       │
├───────────────────┼─────────────┤
│ Name              │             │
│ Description       │             │
│ System Units Base │ SYSTEM_BASE │
│ Base Power        │ 100.0       │
│ Base Frequency    │ 60.0        │
│ Num Components    │ 9           │
└───────────────────┴─────────────┘

Static Components
┌─────────────────┬───────┬────────────────────────┬───────────────┐
│ Type            │ Count │ Has Static Time Series │ Has Forecasts │
├─────────────────┼───────┼────────────────────────┼───────────────┤
│ ACBus           │ 2     │ false                  │ false         │
│ Arc             │ 1     │ false                  │ false         │
│ Area            │ 1     │ false                  │ false         │
│ ExponentialLoad │ 1     │ false                  │ false         │
│ Line            │ 1     │ false                  │ false         │
│ LoadZone        │ 1     │ false                  │ false         │
│ ThermalStandard │ 1     │ false                  │ false         │
└─────────────────┴───────┴────────────────────────┴───────────────┘

Dynamic Components
┌──────────────────┬───────┐
│ Type             │ Count │
├──────────────────┼───────┤
│ DynamicGenerator │ 1     │
└──────────────────┴───────┘

Here are the components of the generator:

julia> first(get_components(DynamicGenerator, sys))generator-101-1 (DynamicGenerator{RoundRotorQuadratic, SingleMass, SEXS, SteamTurbineGov1, PSSFixed}):
   name: generator-101-1
   ω_ref: 1.0
   machine: RoundRotorQuadratic
   shaft: SingleMass
   avr: SEXS
   prime_mover: SteamTurbineGov1
   pss: PSSFixed
   base_power: 100.0
   n_states: 10
   states: [:eq_p, :ed_p, :ψ_kd, :ψ_kq, :δ, :ω, :Vf, :Vr, :x_g1, :x_g2]
   ext: Dict{String, Any}()
   InfrastructureSystems.SystemUnitsSettings:
      base_value: 100.0
      unit_system: UnitSystem.SYSTEM_BASE = 0

Besides the results of the P-V curve, we need to store if the system is small-signal stable or not by looking if there is a positive real part eigenvalue.

julia> # Vectors to store stability using a boolean (true for stable).
       stable_vec = Vector{Bool}();
julia> status_vec = Vector{Bool}(); # PV Curve Results
julia> P_load_p = Vector{Float64}();
julia> V_load_p = Vector{Float64}();

We then run the main for loop by updating the load active power, but in addition we create a PowerSimulationsDynamics simulation on which we can run a small-signal analysis to check stability.

julia> for p in P_range
           # Change the active power and reactive power of the load
           power = p * 1.0
           load = get_component(PSY.ExponentialLoad, sys_static, "load1021")
           set_active_power!(load, power)
           q_power = power * tan(acos(load_pf))
           set_reactive_power!(load, q_power)
           # Run Power Flow
           status = solve_ac_powerflow!(sys_static)
           if !status
               # Finish the loop if the power flow fails
               print("Power Flow failed at p = $(power)")
               break
           end
           # Obtain the bus voltage information
           bus = get_component(Bus, sys_static, "BUS 2")
           Vm = get_magnitude(bus)
           # Store values in the vectors
           push!(V_load_p, Vm)
           push!(P_load_p, power)
       
           # Update Load Power in the GENROU system
           load = get_component(PSY.ExponentialLoad, sys, "load1021")
           set_active_power!(load, power)
           q_power = power * tan(acos(load_pf))
           set_reactive_power!(load, q_power)
           # Construct Simulation
           sim = Simulation(ResidualModel, sys, mktempdir(), (0.0, 1.0))
           if sim.status == PSID.BUILT
               # Check small-signal stability
               sm = small_signal_analysis(sim).stable
               # Push results of small-signal stability
               push!(stable_vec, sm)
               # Push results if the simulation was able to be constructed
               push!(status_vec, true)
           else
               # Push results if the simulation was not able to be constructed
               push!(status_vec, false)
           end
       end┌ Error: The powerflow solver returned convergence = false
└ @ PowerFlows ~/.julia/packages/PowerFlows/6nw6s/src/nlsolve_ac_powerflow.jl:153
┌ Error: The powerflow solver returned convergence = false
└ @ PowerFlows ~/.julia/packages/PowerFlows/6nw6s/src/nlsolve_ac_powerflow.jl:52
Power Flow failed at p = 4.53

The following plot showcases the P-V curve, while also showcasing (in red) the regions on which the system is small-signal stable.

julia> # Find where is stable and unstable
       dict_true_ixs_p = Vector();
julia> dict_false_ixs_p = Vector();
julia> dict_true_ixs_p = findall(x->x, stable_vec);
julia> dict_false_ixs_p = findall(x->!x, stable_vec); # Create plot
julia> true_ixs = dict_true_ixs_p;
julia> plot(P_load_p, V_load_p, color = :blue, label = "PV Curve", xlabel = "Load Power [pu]", ylabel = "Load Bus Voltage [pu]")Plot{Plots.GRBackend() n=1}
julia> plot!(Plots.scatter!(P_load_p[true_ixs] , V_load_p[true_ixs], markerstrokewidth= 0, label = "GENROU SSA"))Plot{Plots.GRBackend() n=2}

plot

This results is consistent with most of the literature for dynamic generator models supplying constant power loads, on which by increasing the active power of the load, produce critical eigenvalues which cross the $j\omega$ axis at some point. This is called a Hopf Bifurcation, in this case a subcritical one since the limit cycles are unstable.