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);
/home/runner/work/PyPSA/PyPSA/pypsa/network/io.py:2082: FutureWarning: pandas infers the `str` dtype for string data since its version 3.0. PyPSA still converts it back to numpy object dtype on import, but will keep it from PyPSA 2.0 on. Set `pypsa.options.api.legacy_string_dtype` explicitly to suppress this warning. new_static = _coerce_string_dtypes(new_static)
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)
INFO:pypsa.network.power_flow:Performing linear load-flow on AC sub-network <pypsa.SubNetwork object at 0x7f2bb8902270> for snapshot(s) Index(['now'], dtype='str', name='snapshot')
INFO:pypsa.network.power_flow:Performing non-linear load-flow on AC sub-network <pypsa.SubNetwork object at 0x7f2b5252a350> for snapshots Index(['now'], dtype='str', name='snapshot')
| Voltage Angles | Voltage Magnitude | |
|---|---|---|
| name | ||
| MV bus | 0.000000 | 1.020000 |
| LV1 bus | -150.760126 | 1.008843 |
| LV2 bus | -149.884141 | 0.964431 |
n.transformers.tap_position = 2
run_power_flow(n)
INFO:pypsa.network.power_flow:Performing linear load-flow on AC sub-network <pypsa.SubNetwork object at 0x7f2b5252a350> for snapshot(s) Index(['now'], dtype='str', name='snapshot')
INFO:pypsa.network.power_flow:Performing non-linear load-flow on AC sub-network <pypsa.SubNetwork object at 0x7f2b525702b0> for snapshots Index(['now'], dtype='str', name='snapshot')
| Voltage Angles | Voltage Magnitude | |
|---|---|---|
| name | ||
| MV bus | 0.000000 | 1.020000 |
| LV1 bus | -150.843911 | 0.959655 |
| LV2 bus | -149.870837 | 0.912713 |
n.transformers.tap_position = -2
run_power_flow(n)
INFO:pypsa.network.power_flow:Performing linear load-flow on AC sub-network <pypsa.SubNetwork object at 0x7f2b525702b0> for snapshot(s) Index(['now'], dtype='str', name='snapshot')
INFO:pypsa.network.power_flow:Performing non-linear load-flow on AC sub-network <pypsa.SubNetwork object at 0x7f2b51d715b0> for snapshots Index(['now'], dtype='str', name='snapshot')
| Voltage Angles | Voltage Magnitude | |
|---|---|---|
| name | ||
| MV bus | 0.000000 | 1.020000 |
| LV1 bus | -150.681666 | 1.063133 |
| LV2 bus | -149.896634 | 1.021202 |
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
| New trafo | |
|---|---|
| f_nom | 50.0 |
| s_nom | 0.4 |
| v_nom_0 | 20.0 |
| v_nom_1 | 0.4 |
| vsc | 6.0 |
| vscr | 1.425 |
| pfe | 1.35 |
| i0 | 0.3375 |
| phase_shift | 150.0 |
| tap_side | 1 |
| tap_neutral | 0 |
| tap_min | -2 |
| tap_max | 2 |
| tap_step | 2.5 |
| references | pandapower;Oswald - Transformatoren - Vorlesun... |
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)
INFO:pypsa.network.power_flow:Performing linear load-flow on AC sub-network <pypsa.SubNetwork object at 0x7f2b52525ae0> for snapshot(s) Index(['now'], dtype='str', name='snapshot')
INFO:pypsa.network.power_flow:Performing non-linear load-flow on AC sub-network <pypsa.SubNetwork object at 0x7f2b52525ae0> for snapshots Index(['now'], dtype='str', name='snapshot')
| Voltage Angles | Voltage Magnitude | |
|---|---|---|
| name | ||
| MV bus | 0.000000 | 1.020000 |
| LV1 bus | -150.755820 | 1.059317 |
| LV2 bus | -149.964875 | 1.017221 |
n.transformers.T
| name | MV-LV trafo |
|---|---|
| bus0 | MV bus |
| bus1 | LV1 bus |
| type | New trafo |
| model | t |
| x | 0.058283 |
| r | 0.01425 |
| g | 0.003375 |
| b | -0.0 |
| s_nom | 0.4 |
| s_nom_mod | 0.0 |
| s_nom_extendable | False |
| s_nom_min | 0.0 |
| s_nom_max | inf |
| s_nom_set | NaN |
| s_max_pu | 1.0 |
| capital_cost | 0.0 |
| overnight_cost | NaN |
| discount_rate | NaN |
| fom_cost | 0.0 |
| num_parallel | 1.0 |
| tap_ratio | 1.05 |
| tap_side | 1 |
| tap_position | 2 |
| phase_shift | 150.0 |
| phase_shift_min | 0.0 |
| phase_shift_max | 0.0 |
| active | True |
| build_year | 0 |
| lifetime | inf |
| v_ang_min | -inf |
| v_ang_max | inf |
| sub_network | 0 |
| x_pu | 0.145712 |
| r_pu | 0.035618 |
| g_pu | 0.00135 |
| b_pu | -0.0 |
| x_pu_eff | 0.152994 |
| r_pu_eff | 0.037406 |
| s_nom_opt | 0.0 |
n.transformers.tap_position = -2
run_power_flow(n)
INFO:pypsa.network.power_flow:Performing linear load-flow on AC sub-network <pypsa.SubNetwork object at 0x7f2b535f2150> for snapshot(s) Index(['now'], dtype='str', name='snapshot')
INFO:pypsa.network.power_flow:Performing non-linear load-flow on AC sub-network <pypsa.SubNetwork object at 0x7f2b6d8e1050> for snapshots Index(['now'], dtype='str', name='snapshot')
| Voltage Angles | Voltage Magnitude | |
|---|---|---|
| name | ||
| MV bus | 0.000000 | 1.020000 |
| LV1 bus | -150.765232 | 0.958366 |
| LV2 bus | -149.789394 | 0.911353 |
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"]]
)
}
)
/tmp/ipykernel_8200/981123384.py:3: FutureWarning: The default value of `include_objective_constant` will change from True to False in version 2.0. Set `include_objective_constant` explicitly to suppress this warning. Using False improves LP numerical conditioning by not including the objective constant as a variable. n.optimize() WARNING:pypsa.consistency:The following buses have carriers which are not defined. Run n.sanitize() to add them. Components with undefined carriers: Index(['MV bus', 'LV1 bus', 'LV2 bus'], dtype='object', name='name')
WARNING:pypsa.consistency:Encountered nan's in varying data 'p' for columns ['External Grid'] of component 'Generator'.
WARNING:pypsa.consistency:The following lines have carriers which are not defined. Run n.sanitize() to add them. Components with undefined carriers: Index(['LV cable'], dtype='object', name='name')
WARNING:pypsa.consistency:The following sub_networks have carriers which are not defined. Run n.sanitize() to add them. Components with undefined carriers: Index(['0'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Gurobi solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.04s
Set parameter WLSAccessID
Set parameter WLSSecret
Set parameter LicenseID to value 2537914
Academic license 2537914 - for non-commercial use only - registered to l.___@tu-berlin.de
Read LP format model from file /tmp/linopy-problem-g9pnd7w6.lp
Reading time = 0.00 seconds
obj: 9 rows, 3 columns, 11 nonzeros
Set parameter LogToConsole to value 0
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 3 primals, 9 duals Objective: 1.00e+00 Solver: gurobi Runtime: 0.00s Dual bound: 1.00e+00 Solver model: available Solver message: 2
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-fix-p-lower, Generator-fix-p-upper, Line-fix-s-lower, Line-fix-s-upper, Transformer-fix-s-lower, Transformer-fix-s-upper were not assigned to the network.
| Active Power p0 (MW) | |
|---|---|
| name | |
| LV cable | 0.1 |
| MV-LV trafo | 0.1 |
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)
/tmp/ipykernel_8200/3936709006.py:23: FutureWarning: The default value of `include_objective_constant` will change from True to False in version 2.0. Set `include_objective_constant` explicitly to suppress this warning. Using False improves LP numerical conditioning by not including the objective constant as a variable. opt.optimize() WARNING:pypsa.consistency:The following buses have carriers which are not defined. Run n.sanitize() to add them. Components with undefined carriers: Index(['A', 'B'], dtype='object', name='name')
WARNING:pypsa.consistency:The following lines have carriers which are not defined. Run n.sanitize() to add them. Components with undefined carriers: Index(['line'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Gurobi solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.04s
Set parameter WLSAccessID
Set parameter WLSSecret
Set parameter LicenseID to value 2537914
Academic license 2537914 - for non-commercial use only - registered to l.___@tu-berlin.de
Read LP format model from file /tmp/linopy-problem-zi7njawy.lp
Reading time = 0.00 seconds
obj: 9 rows, 3 columns, 13 nonzeros
Set parameter LogToConsole to value 0
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 3 primals, 9 duals Objective: 5.00e+02 Solver: gurobi Runtime: 0.00s Dual bound: 5.00e+02 Solver model: available Solver message: 2
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-fix-p-lower, Generator-fix-p-upper, Line-fix-s-lower, Line-fix-s-upper, Transformer-fix-s-lower, Transformer-fix-s-upper, Kirchhoff-Voltage-Law were not assigned to the network.
INFO:pypsa.network.power_flow:Performing linear load-flow on AC sub-network <pypsa.SubNetwork object at 0x7f2b50fcfa10> for snapshot(s) Index(['now'], dtype='str', name='snapshot')
/tmp/ipykernel_8200/3936709006.py:23: FutureWarning: The default value of `include_objective_constant` will change from True to False in version 2.0. Set `include_objective_constant` explicitly to suppress this warning. Using False improves LP numerical conditioning by not including the objective constant as a variable. opt.optimize() WARNING:pypsa.consistency:The following buses have carriers which are not defined. Run n.sanitize() to add them. Components with undefined carriers: Index(['A', 'B'], dtype='object', name='name')
WARNING:pypsa.consistency:The following lines have carriers which are not defined. Run n.sanitize() to add them. Components with undefined carriers: Index(['line'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Gurobi solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.03s
Set parameter WLSAccessID
Set parameter WLSSecret
Set parameter LicenseID to value 2537914
Academic license 2537914 - for non-commercial use only - registered to l.___@tu-berlin.de
Read LP format model from file /tmp/linopy-problem-ihirksxl.lp
Reading time = 0.00 seconds
obj: 9 rows, 3 columns, 13 nonzeros
Set parameter LogToConsole to value 0
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 3 primals, 9 duals Objective: 5.00e+02 Solver: gurobi Runtime: 0.00s Dual bound: 5.00e+02 Solver model: available Solver message: 2
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-fix-p-lower, Generator-fix-p-upper, Line-fix-s-lower, Line-fix-s-upper, Transformer-fix-s-lower, Transformer-fix-s-upper, Kirchhoff-Voltage-Law were not assigned to the network.
INFO:pypsa.network.power_flow:Performing linear load-flow on AC sub-network <pypsa.SubNetwork object at 0x7f2b7189d550> for snapshot(s) Index(['now'], dtype='str', name='snapshot')
/tmp/ipykernel_8200/3936709006.py:23: FutureWarning: The default value of `include_objective_constant` will change from True to False in version 2.0. Set `include_objective_constant` explicitly to suppress this warning. Using False improves LP numerical conditioning by not including the objective constant as a variable. opt.optimize() WARNING:pypsa.consistency:The following buses have carriers which are not defined. Run n.sanitize() to add them. Components with undefined carriers: Index(['A', 'B'], dtype='object', name='name')
WARNING:pypsa.consistency:The following lines have carriers which are not defined. Run n.sanitize() to add them. Components with undefined carriers: Index(['line'], dtype='object', name='name')
INFO:linopy.model: Solve problem using Gurobi solver
INFO:linopy.model:Solver options: - log_to_console: False
INFO:linopy.io: Writing time: 0.03s
Set parameter WLSAccessID
Set parameter WLSSecret
Set parameter LicenseID to value 2537914
Academic license 2537914 - for non-commercial use only - registered to l.___@tu-berlin.de
Read LP format model from file /tmp/linopy-problem-jo6i1d9m.lp
Reading time = 0.00 seconds
obj: 9 rows, 3 columns, 13 nonzeros
Set parameter LogToConsole to value 0
INFO:linopy.constants: Optimization successful: Status: ok Termination condition: optimal Solution: 3 primals, 9 duals Objective: 5.00e+02 Solver: gurobi Runtime: 0.00s Dual bound: 5.00e+02 Solver model: available Solver message: 2
INFO:pypsa.optimization.optimize:The shadow-prices of the constraints Generator-fix-p-lower, Generator-fix-p-upper, Line-fix-s-lower, Line-fix-s-upper, Transformer-fix-s-lower, Transformer-fix-s-upper, Kirchhoff-Voltage-Law were not assigned to the network.
INFO:pypsa.network.power_flow:Performing linear load-flow on AC sub-network <pypsa.SubNetwork object at 0x7f2b7189edd0> for snapshot(s) Index(['now'], dtype='str', name='snapshot')
| phase_shift (deg) | line p0 — optimize | PST p0 — optimize | line p0 — lpf | PST p0 — lpf | |
|---|---|---|---|---|---|
| 0 | 0.0 | 25.000 | 25.000 | 25.000 | 25.000 |
| 1 | 5.0 | 29.363 | 20.637 | 29.363 | 20.637 |
| 2 | 10.0 | 33.727 | 16.273 | 33.727 | 16.273 |
A positive phase shift moves flow off the transformer onto the parallel line, and the optimisation and linear power flow agree to numerical precision.