3. Power Flow Analysis#

Power flow analysis, also known as load flow, is the foundation of power system studies. It calculates the steady-state operating point of a power system by solving a set of nonlinear algebraic equations that represent power balance at each bus. The results include voltage magnitudes and angles at all buses, as well as active and reactive power flows on all branches.

This tutorial covers running power flow in ANDES, accessing and interpreting results, configuring solver options, and troubleshooting convergence issues.

3.1. Setup#

We begin by importing ANDES and configuring the logger. The logger controls what information is displayed during execution, with level 20 (INFO) being appropriate for normal use.

%matplotlib inline

import andes
andes.config_logger(stream_level=20)

3.2. Running Power Flow#

To run power flow, you first load a case file to create a System object, then call the run() method on the PFlow routine. ANDES implements the Newton-Raphson method, which iteratively solves the power balance equations until the mismatch (residual) falls below the convergence tolerance.

For this tutorial, we use the IEEE 14-bus test system, a standard benchmark that contains 14 buses, 5 generators, and 20 branches. This system is small enough to examine results easily while being realistic enough to demonstrate typical power flow behavior.

ss = andes.load(andes.get_case('ieee14/ieee14.json'))
ss.PFlow.run()
Working directory: "/home/docs/checkouts/readthedocs.org/user_builds/andes/checkouts/stable/docs/source/tutorials"
> Loaded generated Python code in "/home/docs/.andes/pycode".
Parsing input file "/home/docs/checkouts/readthedocs.org/user_builds/andes/envs/stable/lib/python3.11/site-packages/andes/cases/ieee14/ieee14.json"...
Input file parsed in 0.0021 seconds.
Connectivity check completed in 0.0001 seconds.
-> System connectivity check results:
  No islanded bus detected.
  System is interconnected.
  Each island has a slack bus correctly defined and enabled.
System internal structure set up in 0.0200 seconds.

-> Power flow calculation
           Numba: Off
   Sparse solver: KLU
 Solution method: NR method
Power flow initialized in 0.0034 seconds.
0: |F(x)| = 0.5605182134
1: |F(x)| = 0.006202200332
2: |F(x)| = 5.819382827e-06
3: |F(x)| = 6.957087684e-12
Converged in 4 iterations in 0.0022 seconds.
Report saved to "ieee14_out.txt" in 0.0011 seconds.
True

After the solver completes, you can check whether the solution converged and how many iterations were required. A well-conditioned power flow typically converges in 3-6 iterations. If the iteration count is high or convergence fails, see the Troubleshooting section below.

print(f"Converged: {ss.PFlow.converged}")
print(f"Iterations: {ss.PFlow.niter}")
Converged: True
Iterations: 3

3.2.1. Command Line Alternative#

Power flow can also be executed from the command line without writing Python code. This is useful for quick analyses or batch processing. The following command runs power flow on a case file:

andes run ieee14.json

The CLI automatically generates output files containing the results.

3.3. Accessing Results#

After power flow converges, results are stored in the model objects that make up the System. Each model (such as Bus, Line, PV) contains variables whose solved values can be accessed through the .v attribute. Understanding how to navigate this structure is essential for extracting the information you need.

3.3.1. Bus Results#

Bus voltages are the primary output of power flow analysis. The voltage magnitude is stored in ss.Bus.v and the voltage angle in ss.Bus.a. To access the numerical values as NumPy arrays, append .v to get the value array.

# Voltage magnitudes (per unit)
ss.Bus.v.v
array([1.03      , 1.03      , 1.01      , 1.01140345, 1.0172555 ,
       1.03      , 1.0224715 , 1.03      , 1.02176879, 1.01554206,
       1.01911514, 1.01740698, 1.01445023, 1.0163402 ])
# Voltage angles (radians)
ss.Bus.a.v
array([ 7.95343065e-14, -3.07888354e-02, -6.17345595e-02, -7.69651166e-02,
       -6.70734614e-02, -1.12621496e-01, -8.52626931e-02, -2.68773277e-02,
       -1.26464055e-01, -1.29424839e-01, -1.23564069e-01, -1.30428971e-01,
       -1.34752637e-01, -1.65476685e-01])

For a more comprehensive view, you can retrieve all bus parameters and variables as a pandas DataFrame using as_df(). This is particularly useful for inspecting multiple quantities at once or exporting results to other tools.

ss.Bus.as_df()
idx u name Vn vmax vmin v0 a0 xcoord ycoord area zone owner
uid
0 1 1.0 BUS1 69.0 1.1 0.9 1.03000 0.000000 0 0 1 1 1
1 2 1.0 BUS2 69.0 1.1 0.9 1.01970 -0.027981 0 0 1 1 1
2 3 1.0 BUS3 69.0 1.1 0.9 1.00042 -0.060097 0 0 1 1 1
3 4 1.0 BUS4 69.0 1.1 0.9 0.99858 -0.074721 0 0 1 1 1
4 5 1.0 BUS5 69.0 1.1 0.9 1.00443 -0.064315 0 0 1 1 1
5 6 1.0 BUS6 138.0 1.1 0.9 0.99871 -0.109998 0 0 2 2 2
6 7 1.0 BUS7 138.0 1.1 0.9 1.00682 -0.084285 0 0 2 2 2
7 8 1.0 BUS8 69.0 1.1 0.9 1.01895 -0.024339 0 0 2 2 2
8 9 1.0 BUS9 138.0 1.1 0.9 1.00193 -0.127502 0 0 2 2 2
9 10 1.0 BUS10 138.0 1.1 0.9 0.99351 -0.130202 0 0 2 2 2
10 11 1.0 BUS11 138.0 1.1 0.9 0.99245 -0.122948 0 0 2 2 2
11 12 1.0 BUS12 138.0 1.1 0.9 0.98639 -0.128934 0 0 2 2 2
12 13 1.0 BUS13 138.0 1.1 0.9 0.98403 -0.133786 0 0 2 2 2
13 14 1.0 BUS14 138.0 1.1 0.9 0.99063 -0.166916 0 0 2 2 2

3.3.2. Generator Results#

Generator outputs are distributed across different model types depending on their control mode. In power flow, PV buses represent generators with fixed voltage magnitude and active power output, while Slack (or swing) buses balance the system by absorbing any mismatch. The solved reactive power output q is determined by the power flow solution.

print("PV generators - Active power (pu):", ss.PV.p.v)
print("Slack generator - Active power (pu):", ss.Slack.p.v)
PV generators - Active power (pu): [0.4  0.4  0.3  0.35]
Slack generator - Active power (pu): [0.81427214]
print("PV generators - Reactive power (pu):", ss.PV.q.v)
print("Slack generator - Reactive power (pu):", ss.Slack.q.v)
PV generators - Reactive power (pu): [0.30436147 0.12597133 0.20986596 0.07396392]
Slack generator - Reactive power (pu): [-0.21617103]

3.3.3. Line Flows#

Branch power flows indicate how power is transmitted through the network. For each line, a1 represents the active power flow at the from-bus end, and a2 represents the active power flow at the to-bus end. The difference between these values accounts for transmission losses.

# Show flows for first 5 lines
print("From-bus active power (pu):", ss.Line.a1.v[:5])
print("To-bus active power (pu):", ss.Line.a2.v[:5])
From-bus active power (pu): [ 7.95343065e-14  7.95343065e-14 -3.07888354e-02 -3.07888354e-02
 -3.07888354e-02]
To-bus active power (pu): [-0.03078884 -0.06707346 -0.06173456 -0.07696512 -0.06707346]

3.3.4. Exploring Bus Connections#

When working with an unfamiliar system, you often need to know what devices are connected to a particular bus. The find_connected() method scans all models and returns the devices referencing a given bus (or any other device). This eliminates the need to manually search through model data.

# What's connected to Bus 1 (slack bus)?
ss.find_connected('Bus', 1)
OrderedDict([('Slack', [1]),
             ('Line', ['Line_1', 'Line_2']),
             ('GENROU', ['GENROU_1']),
             ('BusFreq', ['BusFreq_2'])])

3.4. Configuration#

The power flow solver behavior can be customized through configuration options accessible via ss.PFlow.config. These settings control convergence criteria, iteration limits, and solver behavior. Viewing the current configuration shows all available options and their values.

ss.PFlow.config
OrderedDict([('linsolve', 0),
             ('tol', 1e-06),
             ('max_iter', 25),
             ('method', 'NR'),
             ('n_factorize', 4),
             ('report', 1),
             ('degree', 0),
             ('init_tds', 0),
             ('linesearch', 1)])

The most commonly adjusted options are summarized below:

Option

Default

Description

max_iter

25

Maximum number of Newton-Raphson iterations

tol

1e-6

Convergence tolerance for power mismatch

method

'NR'

Solution method (NR = Newton-Raphson)

init_tds

0

Whether to initialize TDS models after power flow

To modify these settings, assign new values directly to the config attributes. Changes take effect on the next run() call.

ss.PFlow.config.max_iter = 50
ss.PFlow.config.tol = 1e-8

For persistent configuration that applies to all ANDES sessions, you can edit the configuration file located at ~/.andes/andes.conf. Settings in this file are loaded automatically when ANDES starts.

[PFlow]
max_iter = 50
tol = 1e-8

3.5. Troubleshooting#

3.5.1. Non-Convergence#

Power flow convergence failures are typically caused by one of three issues:

  1. Data errors: Zero impedance lines create singular Jacobian matrices, and isolated buses have no valid solution. Check your network topology for disconnected components.

  2. Infeasible operating point: If total load exceeds generation capacity, or if reactive power requirements cannot be met by available sources, no solution exists. Verify that generation and load are balanced.

  3. Poor initial guess: The Newton-Raphson method requires a reasonable starting point. For very stressed systems, the flat start (1.0 pu voltage, 0 angle) may be too far from the solution.

When convergence fails, try the following debugging steps:

# Allow more iterations for difficult cases
ss.PFlow.config.max_iter = 100

# Enable PV-to-PQ conversion when generators hit reactive limits
ss.PV.config.pv2pq = 1

3.5.2. Viewing Iteration Progress#

To diagnose convergence issues, you can enable debug-level logging which displays the residual magnitude at each iteration. A healthy convergence shows the residual decreasing rapidly (quadratic convergence is characteristic of Newton-Raphson). If the residual oscillates or decreases slowly, there may be numerical issues with the case data.

andes.config_logger(stream_level=10)  # DEBUG level

ss_debug = andes.load(andes.get_case('ieee14/ieee14.json'))
ss_debug.PFlow.run()
Working directory: "/home/docs/checkouts/readthedocs.org/user_builds/andes/checkouts/stable/docs/source/tutorials"
Found files: ['/home/docs/checkouts/readthedocs.org/user_builds/andes/envs/stable/lib/python3.11/site-packages/andes/cases/ieee14/ieee14.json']
Registered deprecated group alias ACTopology -> ACNode
> Reloaded generated Python code of module "pycode".
Input format guessed as json.
Parsing input file "/home/docs/checkouts/readthedocs.org/user_builds/andes/envs/stable/lib/python3.11/site-packages/andes/cases/ieee14/ieee14.json"...
Input file parsed in 0.0022 seconds.
Setting internal address for Bus
Setting internal address for PQ
Setting internal address for PV
Setting internal address for Slack
Setting internal address for Shunt
Setting internal address for Line
Setting internal address for Area
Entering connectivity check.
Connectivity check completed in 0.0002 seconds.
-> System connectivity check results:
  No islanded bus detected.
  System is interconnected.
  Bus indices in interconnected system (0-based): [[0, 4, 5, 12, 13, 8, 9, 6, 7, 11, 10, 3, 2, 1]]
  Each island has a slack bus correctly defined and enabled.
System internal structure set up in 0.0207 seconds.

-> Power flow calculation
           Numba: Off
   Sparse solver: KLU
 Solution method: NR method
========== Bus has <pflow_init> = True ==========
Initialization sequence:
a -> v
Bus: Pass 1 — unconstrained
Bus: Pass 2 — with discrete evaluation
========== PQ has <pflow_init> = True ==========
Initialization sequence:
a -> v
PQ: Pass 1 — unconstrained
PQ: Pass 2 — with discrete evaluation
========== PV has <pflow_init> = True ==========
Initialization sequence:
q -> a -> v
PV: Pass 1 — unconstrained
PV: Pass 2 — with discrete evaluation
========== Slack has <pflow_init> = True ==========
Initialization sequence:
q -> p -> a -> v
Slack: Pass 1 — unconstrained
Slack: Pass 2 — with discrete evaluation
========== Shunt has <pflow_init> = True ==========
Initialization sequence:
a -> v
Shunt: Pass 1 — unconstrained
Shunt: Pass 2 — with discrete evaluation
========== Line has <pflow_init> = True ==========
Initialization sequence:
a1 -> a2 -> v1 -> v2
Line: Pass 1 — unconstrained
Line: Pass 2 — with discrete evaluation
========== Area has <pflow_init> = True ==========
Initialization sequence:

Area: Pass 1 — unconstrained
Area: Pass 2 — with discrete evaluation
Power flow initialized in 0.0041 seconds.
Max. algeb mismatch 0.5605182134 on v Bus 6
0: |F(x)| = 0.5605182134
Jacobian updated at t=-1.000000.
Max. algeb mismatch 0.006202200332 on v Bus 13
1: |F(x)| = 0.006202200332
Jacobian updated at t=-1.000000.
Max. algeb mismatch 5.819382827e-06 on v Bus 13
2: |F(x)| = 5.819382827e-06
Jacobian updated at t=-1.000000.
Max. algeb mismatch 6.957087684e-12 on v Bus 5
3: |F(x)| = 6.957087684e-12
Converged in 4 iterations in 0.0023 seconds.
Report saved to "ieee14_out.txt" in 0.0016 seconds.
True

The output shows the residual norm at each iteration:

0: |F(x)| = 14.9282832
1: |F(x)| = 3.608627841
2: |F(x)| = 0.1701107882
...

Quadratic convergence means that each iteration roughly squares the number of correct digits, so you should see the residual drop by several orders of magnitude in just a few iterations.

3.6. Power Flow Report#

After a successful power flow, ANDES generates a text report file containing formatted results. This report includes system statistics, bus data (voltage, angle, injections), and branch data (flows, losses). The report path can be accessed through the files attribute.

ss.files.txt
'ieee14_out.txt'

The report is organized into sections:

  1. System statistics: Total generation, load, and losses

  2. Bus data: Voltage magnitude and angle, P/Q injection at each bus

  3. Line data: Active and reactive power flows, losses on each branch

  4. Other algebraic variables: Additional solved quantities from device models

3.7. Example: Formatted Bus Voltage Display#

The following example demonstrates how to iterate through bus results and display them in a readable format. This pattern is useful for creating custom reports or validating results against other tools.

import math

print("Bus Voltage Results")
print("-" * 40)
for name, v, a in zip(ss.Bus.name.v, ss.Bus.v.v, ss.Bus.a.v):
    print(f"Bus {name:>4}: V = {v:.4f} pu, θ = {math.degrees(a):>7.2f}°")
Bus Voltage Results
----------------------------------------
Bus BUS1: V = 1.0300 pu, θ =    0.00°
Bus BUS2: V = 1.0300 pu, θ =   -1.76°
Bus BUS3: V = 1.0100 pu, θ =   -3.54°
Bus BUS4: V = 1.0114 pu, θ =   -4.41°
Bus BUS5: V = 1.0173 pu, θ =   -3.84°
Bus BUS6: V = 1.0300 pu, θ =   -6.45°
Bus BUS7: V = 1.0225 pu, θ =   -4.89°
Bus BUS8: V = 1.0300 pu, θ =   -1.54°
Bus BUS9: V = 1.0218 pu, θ =   -7.25°
Bus BUS10: V = 1.0155 pu, θ =   -7.42°
Bus BUS11: V = 1.0191 pu, θ =   -7.08°
Bus BUS12: V = 1.0174 pu, θ =   -7.47°
Bus BUS13: V = 1.0145 pu, θ =   -7.72°
Bus BUS14: V = 1.0163 pu, θ =   -9.48°

3.8. Cleanup#

Remove output files generated during this tutorial.

!andes misc -C
"/home/docs/checkouts/readthedocs.org/user_builds/andes/checkouts/stable/docs/source/tutorials/ieee14_out.txt" removed.

3.9. Next Steps#

With power flow complete, you can proceed to dynamic analysis: