Getting started with Roseau Load Flow¶
Make sure you have followed the installation instructions.
In this tutorial you will learn how to:
Creating a network¶
An electrical network can be built by assembling basic elements described in the Models section. The following is a summary of the available elements:
Buses:
Bus
: A basic multi-phase electrical bus.
Branches:
Line
: A line connects two buses. The physical parameters of the line, like its impedance, are defined by aLineParameters
object.Switch
: A basic switch element.Transformer
: A generic transformer. The physical parameters of the transformer like its impedance and winding configuration are defined by aTransformerParameters
object.
Loads:
The ZIP load model is available via the following classes:
ImpedanceLoad
: A constant impedance (Z) load: \(S = |V|^2 \times \overline{Z}\), \(|S|\) is proportional to \(|V|^2\).CurrentLoad
A constant current (I) load: \(S = V \times \overline{I}\), \(|S|\) is proportional to \(|V|^1\).PowerLoad
: A constant power (P) load: \(S = \mathrm{constant}\), \(|S|\) is proportional to \(|V|^0\).
A power load can be made flexible (controllable) by using the following class:
FlexibleParameter
: This object defines the parameters of the flexible load’s control (Maximum power, projection, type, etc.)
Note that flexible loads are an advanced feature that most users don’t need. They are explained in details here.
Sources:
VoltageSource
: Represents an infinite power source with a constant voltage.
Others:
Ground
: A ground acts as a perfect conductor. If two elements are connected to the ground, the potentials at the connection points are always equal.PotentialRef
: A potential reference sets the reference of potentials in the network. It can be connected to buses or grounds.
Let’s use some of these elements to build the following network with a voltage source, a simple line and a constant power load. This network is a low voltage network (three-phase + neutral wire).
>>> import numpy as np
... import roseau.load_flow as rlf
>>> # Nominal phase-to-neutral voltage (typical european voltage)
... un = 400 / np.sqrt(3) # In Volts
... # Optional network limits (for analyzing network violations)
... u_min = 0.9 * un # V
... u_max = 1.1 * un # V
... i_max = 500.0 # A
>>> # Create two buses (notice the phases of LV buses include the neutral, as in typical LV networks)
... source_bus = rlf.Bus(id="sb", phases="abcn", min_voltage=u_min, max_voltage=u_max)
... load_bus = rlf.Bus(id="lb", phases="abcn", min_voltage=u_min, max_voltage=u_max)
>>> # Define the reference of potentials to be the earth's potential
... # First define the "Ground" object representing the earth
... ground = rlf.Ground(id="gnd")
... # second, fix the potential of the ground at 0 V
... pref = rlf.PotentialRef(id="pref", element=ground)
... # third, ground the neutral wire of the source bus by connecting it to the Ground element
... ground.connect(source_bus, phase="n")
>>> # Create a three-phase voltage source at the first bus
... # (voltages are phase-to-neutral because the source's phases include a neutral)
... vs = rlf.VoltageSource(id="vs", bus=source_bus, voltages=un)
>>> # Add a 30kW load at the second bus (balanced load, 10 kW per phase)
... load = rlf.PowerLoad(id="load", bus=load_bus, powers=[10e3 + 0j, 10e3, 10e3]) # In VA
>>> # Add a 2 km line between the source bus and the load bus with R = 0.1 Ohm/km and X = 0
... lp = rlf.LineParameters("lp", z_line=(0.1 + 0.0j) * np.eye(4), max_current=i_max)
... line = rlf.Line(id="line", bus1=source_bus, bus2=load_bus, parameters=lp, length=2.0)
Notice how the phases of the load, line and source are not explicitly given. They are inferred to be
"abcn"
from the buses they are connected to. You can also explicitly declare the phases of these
elements. For example, to create a delta-connected source instead, you can explicitly set its phases
to "abc"
:
>>> vs_delta = rlf.VoltageSource(
... id="vs_delta",
... bus=source_bus,
... voltages=un * np.sqrt(3),
... phases="abc", # <- delta source
... )
Note the use of un * np.sqrt(3)
as the source voltages as they now represent the phase-to-phase
voltages. This is because everywhere in roseau-load-flow
, the voltages
of an element depend on
the element’s phases
. Voltages of elements connected in a Star (wye) configuration (elements
that have a neutral connection indicated by the presence of the 'n'
character in their phases
attribute) are the phase-to-neutral voltages. Voltages of elements connected in a Delta
configuration (elements that do not have a neutral connection indicated by the absence of the
'n'
char from their phases
attribute) are the phase-to-phase voltages. To see between which
phases the voltage is defined, you can use the voltage_phases
property of the element.
>>> vs.voltage_phases
['an', 'bn', 'cn']
>>> vs_delta.voltage_phases
['ab', 'bc', 'ca']
When creating balanced loads, you can, as a shortcut, pass a single scalar value to the powers
argument. This value will be used equally for all phases. For example, the load definition above
can be simplified as follows:
>>> load = rlf.PowerLoad(
... id="load", bus=load_bus, powers=10e3 + 0j
... ) # 10kW per phase, 30kW total
At this point, all the basic elements of the network have been defined and connected. Now,
everything can be encapsulated in an ElectricalNetwork
object, but first, some important
notes on the Ground
and PotentialRef
elements:
Important
The Ground
element does not have a fixed potential as one would expect from a real ground
connection. The ground element here merely represents a “perfect conductor”, equivalent to a
single-conductor line with zero impedance. The potential reference, 0 Volts, is defined by the
PotentialRef
element that itself can be connected to any bus or ground in the network. This
gives more flexibility for the user to define the potential reference of their network.
A PotentialRef
defines the potential reference for the network. It is a mandatory reference
for the load flow resolution to be well-defined. A network MUST have one and only one potential
reference per a galvanically isolated section.
When in doubt, define the ground and potential reference similar to the example above.
An ElectricalNetwork
object can now be created using the from_element
constructor. The source
bus source_bus
is given to this constructor. All the elements connected to this bus are
automatically included into the network.
>>> en = rlf.ElectricalNetwork.from_element(source_bus)
Solving a load flow¶
A license is required. You can use the free but limited licence key or get a personal and unlimited key by contacting us at contact@roseautechnologies.com. Once you have your license key, you can activate it by following the instructions in the License activation page.
Afterwards, the load flow can be solved by calling the solve_load_flow
method of the ElectricalNetwork
>>> en.solve_load_flow()
(2, 1.8595619621919468e-07)
It returns the number of iterations performed by the Newton-Raphson solver, and the residual error after convergence. Here, the load flow converged in 2 iterations with a residual error of \(1.86 \times 10^{-7}\).
Getting the results¶
The results are now available for all elements of the network. Results can be accessed through
special properties prefixed with res_
on each element. For instance, the potentials
of the load_bus
can be accessed using the property load_bus.res_potentials
. It contains 4
values representing the potentials of its phases a
, b
, c
and n
(neutral). The potentials
are returned as complex numbers. Calling abs(load_bus.res_potentials)
returns their magnitude
(in Volts) and np.angle(load_bus.res_potentials)
returns their angle (phase shift) in radians.
Roseau Load Flow uses Pint Quantity
objects to present
unit-aware data to the user. Most input data (load powers, source voltages, etc.) are expected
to be either given in SI units or using the pint Quantity interface for non-SI units (example below).
Look at the documentation of a method to see its default units.
In the following example, we create a load with powers expressed in kVA:
load = rlf.PowerLoad(
id="load", bus=load_bus, phases="abcn", powers=rlf.Q_([10, 10, 10], "kVA")
)
The results returned by the res_
properties are also Quantity
objects.
Available results¶
The available results depend on the type of element. The following table lists the available results for each element type:
Element type |
Available results |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
¹ res_flexible_powers
is only available for flexible loads (PowerLoad
s with flexible_params
). You’ll see
an example on the usage of flexible loads in the “Flexible Loads” page.
Getting results per object¶
In order to get the potentials or voltages of a bus, use the res_potentials
or res_voltages
properties of buses as follows:
>>> load_bus.res_potentials
array([ 2.21928183e+02-2.60536682e-18j, -1.10964092e+02-1.92195445e+02j,
-1.10964092e+02+1.92195445e+02j, 2.68637675e-15-6.67652444e-17j]) <Unit('volt')>
As the results are pint quantities, they can be converted to different units easily.
>>> abs(load_bus.res_voltages).to("kV") # Get a Quantity in kV
array([0.22192818, 0.22192818, 0.22192818]) <Unit('kilovolt')>
>>> abs(load_bus.res_voltages).m_as("kV") # Get the magnitude in kV
array([0.22192818, 0.22192818, 0.22192818])
>>> abs(load_bus.res_voltages).m # Get the default magnitude (Volts)
array([221.928183, 221.928183, 221.928183])
The currents of the line are available using the res_currents
property of the line
object.
It contains two arrays:
the first is the current flowing from the first bus of the line to the second bus of the line. It contains 4 values: one per phase and the neutral current.
the second is the current flowing from the second bus of the line to the first bus of the line.
Here, the sum of these currents is 0 as we have chosen a simple line model, i.e, a line with only series impedance elements without shunt components. If shunt components were modelled, the sum would have been non-zero.
>>> line.res_currents
(array([ 4.50596216e+01+1.30268341e-17j, -2.25298108e+01-3.90227770e+01j,
-2.25298108e+01+3.90227770e+01j, -1.34318838e-14+3.33826222e-16j]) <Unit('ampere')>,
array([-4.50596216e+01-1.30268341e-17j, 2.25298108e+01+3.90227770e+01j,
2.25298108e+01-3.90227770e+01j, 1.34318838e-14-3.33826222e-16j]) <Unit('ampere')>)
Dataframe network results¶
The results can also be retrieved for the entire network using res_
properties of the
ElectricalNetwork
instance as pandas DataFrames.
Available results for the network are:
res_buses
: Buses potentials indexed by (bus id, phase)res_buses_voltages
: Buses voltages and voltage limits indexed by (bus id, voltage phase²)res_transformers
: Transformers currents, powers, potentials, and power limits indexed by (transformer id, phase)res_lines
: Lines currents, powers, potentials, series losses, series currents, and current limits indexed by (line id, phase)res_switches
: Switches currents, powers, and potentials indexed by (switch id, phase)res_loads
: Loads currents, powers, and potentials indexed by (load id, phase)res_loads_voltages
: Loads voltages indexed by (load id, voltage phase)res_loads_flexible_powers
: Loads flexible powers (only for flexible loads) indexed by (load id, voltage phase)res_sources
: Sources currents, powers, and potentials indexed by (source id, phase)res_grounds
: Grounds potentials indexed by ground idres_potential_refs
: Potential references currents indexed by potential ref id (always zero for a successful load flow)
² a “voltage phase” is a composite phase like an
or ab
All the results are complex numbers. You can always access the magnitude of the results using
the abs
function and the angle in radians using the np.angle
function. For instance,
abs(network.res_loads)
gives you the magnitude of the loads’ results in SI units.
Below are the results of the load flow for en
, rounded to 2 decimal places:
>>> en.res_buses
bus_id |
phase |
potential |
---|---|---|
sb |
a |
230.94+0j |
sb |
b |
-115.47-200j |
sb |
c |
-115.47+200j |
sb |
n |
0j |
lb |
a |
221.93-0j |
lb |
b |
-110.96-192.2j |
lb |
c |
-110.96+192.2j |
lb |
n |
-0j |
>>> en.res_buses_voltages
bus_id |
phase |
voltage |
min_voltage |
max_voltage |
violated |
---|---|---|---|---|---|
sb |
an |
230.94+0j |
207.846 |
254.034 |
False |
sb |
bn |
-115.47-200j |
207.846 |
254.034 |
False |
sb |
cn |
-115.47+200j |
207.846 |
254.034 |
False |
lb |
an |
221.93-0j |
207.846 |
254.034 |
False |
lb |
bn |
-110.96-192.2j |
207.846 |
254.034 |
False |
lb |
cn |
-110.96+192.2j |
207.846 |
254.034 |
False |
>>> en.res_lines
line_id |
phase |
current1 |
current2 |
power1 |
power2 |
potential1 |
potential2 |
series_losses |
series_current |
max_current |
violated |
---|---|---|---|---|---|---|---|---|---|---|---|
line |
a |
45.06-0j |
-45.06+0j |
10406.07+0j |
-10000-0j |
230.94+0j |
221.93+0j |
406.07-0j |
45.06-0j |
500 |
False |
line |
b |
-22.53-39.02j |
22.53+39.02j |
10406.07+0j |
-10000-0j |
-115.47-200j |
-110.96-192.2j |
406.07-0j |
-22.53-39.02j |
500 |
False |
line |
c |
-22.53+39.02j |
22.53-39.02j |
10406.07-0j |
-10000+0j |
-115.47+200j |
-110.96+192.2j |
406.07+0j |
-22.53+39.02j |
500 |
False |
line |
n |
-0-0j |
0j |
-0+0j |
-0j |
0j |
0j |
-0j |
-0-0j |
500 |
False |
>>> en.res_transformers # empty as the network does not contain transformers
transformer_id |
phase |
current1 |
current2 |
power1 |
power2 |
potential1 |
potential2 |
max_power |
violated |
---|
>>> en.res_switches # empty as the network does not contain switches
switch_id |
phase |
current1 |
current2 |
power1 |
power2 |
potential1 |
potential2 |
---|
>>> en.res_loads
load_id |
phase |
type |
current |
power |
potential |
---|---|---|---|---|---|
load |
a |
power |
45.06+0j |
10000-0j |
221.93-0j |
load |
b |
power |
-22.53-39.02j |
10000-0j |
-110.96-192.2j |
load |
c |
power |
-22.53+39.02j |
10000+0j |
-110.96+192.2j |
load |
n |
power |
0j |
0j |
-0j |
>>> en.res_loads_voltages
load_id |
phase |
type |
voltage |
---|---|---|---|
load |
an |
power |
221.93+0j |
load |
bn |
power |
-110.96-192.2j |
load |
cn |
power |
-110.96+192.2j |
>>> en.res_sources
source_id |
phase |
current |
power |
potential |
---|---|---|---|---|
vs |
a |
-45.06-0j |
-10406.07+0j |
230.94+0j |
vs |
b |
22.53+39.02j |
-10406.07-0j |
-115.47-200j |
vs |
c |
22.53-39.02j |
-10406.07+0j |
-115.47+200j |
vs |
n |
0j |
0j |
0j |
>>> en.res_grounds
ground_id |
potential |
---|---|
gnd |
0j |
>>> en.res_potential_refs
potential_ref_id |
current |
---|---|
pref |
0j |
Using the transform
method of data frames, the results can easily be converted from complex values
to magnitude and angle values (radians).
>>> en.res_buses_voltages["voltage"].transform([np.abs, np.angle])
bus_id |
phase |
absolute |
angle |
---|---|---|---|
sb |
an |
230.94 |
0 |
sb |
bn |
230.94 |
-2.0944 |
sb |
cn |
230.94 |
2.0944 |
lb |
an |
221.928 |
2.89102e-19 |
lb |
bn |
221.928 |
-2.0944 |
lb |
cn |
221.928 |
2.0944 |
Or, if you prefer degrees:
>>> import functools as ft
... en.res_buses_voltages["voltage"].transform([np.abs, ft.partial(np.angle, deg=True)])
bus_id |
phase |
absolute |
angle |
---|---|---|---|
sb |
an |
230.94 |
0 |
sb |
bn |
230.94 |
-120 |
sb |
cn |
230.94 |
120 |
lb |
an |
221.928 |
1.65643e-17 |
lb |
bn |
221.928 |
-120 |
lb |
cn |
221.928 |
120 |
Analyzing the results and detecting violations¶
In the example network above, min_voltage
and max_voltage
arguments were passed to the Bus
constructor and max_current
was passed to the LineParameters
constructor. These arguments
define the limits of the network that can be used to check if the network is in a valid state
or not. Note that these limits have no effect on the load flow calculation.
If you set min_voltage
or max_voltage
on a bus, the res_violated
property will tell you if
the voltage limits are violated or not at this bus. Here, the voltage limits are not violated.
>>> load_bus.res_violated
False
Similarly, if you set max_current
on a line, the res_violated
property will tell you if the
current loading of the line in any phase exceeds the limit. Here, the current limit is not violated.
>>> line.res_violated
False
The power limit of the transformer can be defined using the max_power
argument of the
TransformerParameters
. Transformers also have a res_violated
property that indicates if the
power loading of the transformer exceeds the limit.
The data frame results on the electrical network also include a violated
column that indicates if
the limits are violated or not for the corresponding element.
Tip
You can use the Bus.propagate_limits()
method to
propagate the limits from a bus to buses connected to it galvanically (i.e. via lines or switches).
Updating elements of the network¶
Network elements can be updated. Here, the load’s power values are changed to create an unbalanced situation.
>>> load.powers = rlf.Q_(
... [15, 0, 0], "kVA"
... ) # <- 15 kW on phase "a", 0 W on phases "b" and "c"
>>> en.solve_load_flow()
(3, 1.686343545e-07)
>>> load_bus.res_potentials
array([ 216.02252269 +0.j, -115.47005384-200.j, -115.47005384+200.j, 14.91758499 +0.j]) <Unit('volt')>
Notice how the unbalance manifested in the neutral’s potential of the bus no longer being close to 0 V.
More information on modifying the network elements can be found in the Modifying a network page.
Saving/loading the network¶
An electrical network can be written to a JSON file, for later analysis or for sharing with others,
using the to_json()
method.
>>> en.to_json("my_network.json")
For more information see this section on network JSON serialization.