Transformers¶
This example illustrates the use of transformers with non-trivial phase shift and tap ratio. The example is a copy of the pandapower minimal example.
import numpy as np
import pandas as pd
import pypsa
n = pypsa.Network()
n.add("Bus", "MV bus", v_nom=20, v_mag_pu_set=1.02)
n.add("Bus", "LV1 bus", v_nom=0.4)
n.add("Bus", "LV2 bus", v_nom=0.4)
n.add(
"Transformer",
"MV-LV trafo",
type="0.4 MVA 20/0.4 kV",
bus0="MV bus",
bus1="LV1 bus",
)
n.add(
"Line", "LV cable", type="NAYY 4x50 SE", bus0="LV1 bus", bus1="LV2 bus", length=0.1
)
n.add("Generator", "External Grid", bus="MV bus", control="Slack", marginal_cost=10)
n.add("Load", "LV load", bus="LV2 bus", p_set=0.1, q_set=0.05);
def run_power_flow(n: pypsa.Network) -> pd.DataFrame:
n.lpf()
n.pf(use_seed=True)
return pd.DataFrame(
{
"Voltage Angles": n.buses_t.v_ang.loc["now"] * 180.0 / np.pi,
"Voltage Magnitude": n.buses_t.v_mag_pu.loc["now"],
}
)
run_power_flow(n)
n.transformers.tap_position = 2
run_power_flow(n)
n.transformers.tap_position = -2
run_power_flow(n)
Now play with tap changer on LV side
new_trafo_lv_tap = n.transformer_types.loc[["0.4 MVA 20/0.4 kV"]]
new_trafo_lv_tap.index = ["New trafo"]
new_trafo_lv_tap.tap_side = 1
new_trafo_lv_tap.T
n.add("TransformerType", "New trafo", **new_trafo_lv_tap.iloc[0].to_dict())
n.transformers.type = "New trafo"
n.transformers.tap_position = 2
run_power_flow(n)
n.transformers.T
n.transformers.tap_position = -2
run_power_flow(n)
Finally, consider the phase shift in the linear optimal power flow ([n.optimize()][pypsa.Network.optimize.call]). It solves a cycle-based linearised power flow, enforcing Kirchhoff's voltage law around the network's cycles. The radial network above has no cycle, so the phase shift cannot redistribute power and does not affect the optimised flows — it only offsets the absolute voltage angles seen in n.pf()/n.lpf(). The branch flows below are therefore unchanged by the phase shift:
n.generators.p_nom = 1
n.lines.s_nom = 1
n.optimize()
pd.DataFrame(
{
"Active Power p0 (MW)": pd.concat(
[n.lines_t.p0.loc["now"], n.transformers_t.p0.loc["now"]]
)
}
)
Phase shift in a meshed network¶
To see the phase shift act through the optimisation we need a cycle. Below, two buses are joined by two parallel paths — an ordinary line and a phase-shifting transformer — with equal per-unit reactance. Without a phase shift the 50 MW load splits evenly between the two paths; a phase shift on the transformer drives a circulating flow around the cycle that redistributes the power. The optimisation now reproduces exactly what n.lpf() computes.
Note the input attribute is phase_shift; the realised/optimised shift is written back to the output n.transformers_t.phase_shift_opt.
def meshed_network(phase_shift: float) -> pypsa.Network:
m = pypsa.Network()
m.add("Bus", ["A", "B"], v_nom=1.0)
m.add("Generator", "gen", bus="A", control="Slack", p_nom=100, marginal_cost=10)
m.add("Load", "load", bus="B", p_set=50)
m.add("Line", "line", bus0="A", bus1="B", x=0.01, r=1e-6, s_nom=100)
m.add(
"Transformer",
"PST",
bus0="A",
bus1="B",
x=1.0,
r=1e-6,
s_nom=100,
phase_shift=phase_shift,
)
return m
rows = []
for angle in [0.0, 5.0, 10.0]:
opt = meshed_network(angle)
opt.optimize()
ref = meshed_network(angle)
ref.lpf()
rows.append(
{
"phase_shift (deg)": angle,
"line p0 — optimize": opt.lines_t.p0.loc["now", "line"],
"PST p0 — optimize": opt.transformers_t.p0.loc["now", "PST"],
"line p0 — lpf": ref.lines_t.p0.loc["now", "line"],
"PST p0 — lpf": ref.transformers_t.p0.loc["now", "PST"],
}
)
pd.DataFrame(rows).round(3)
A positive phase shift moves flow off the transformer onto the parallel line, and the optimisation and linear power flow agree to numerical precision.