Skip to content

Commit 8a20401

Browse files
committed
fix the handling of phase shifters when init from pandapower
Signed-off-by: DONNOT Benjamin <benjamin.donnot@rte-france.com>
1 parent 2b8564e commit 8a20401

File tree

10 files changed

+94
-30
lines changed

10 files changed

+94
-30
lines changed

CHANGELOG.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ TODO: in `main.cpp` check the returned policy of pybind11 and also the `py::call
2929
TODO: a cpp class that is able to compute (DC powerflow) ContingencyAnalysis and TimeSeries using PTDF and LODF
3030
TODO: integration test with pandapower (see `pandapower/contingency/contingency.py` and import `lightsim2grid_installed` and check it's True)
3131

32+
[0.12.1] 2026-xx-yy
33+
---------------------
34+
- [FIXED] phase shift transformers are now properly modeled
35+
for both pandapower (new in this version) and pypowsybl (already
36+
the case in previous version)
37+
3238
[0.12.0] 2026-01-06
3339
--------------------
3440
- [BREAKING] for better consistency, and following pypowsybl convention, trafo and lines "side"

lightsim2grid/gridmodel/from_pypowsybl/_from_pypowsybl.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ def init(net : pypo.network.Network,
361361
ratio_tap_changer = net_pu.get_ratio_tap_changers()
362362

363363
if 'alpha' in df_trafo_pu:
364-
shift_ = np.rad2deg(df_trafo_pu['alpha'].values) # given in radian by pypowsybl
364+
shift_ = -np.rad2deg(df_trafo_pu['alpha'].values) # given in radian by pypowsybl
365365
else:
366366
if net.get_phase_tap_changers().shape[0] > 0:
367367
raise RuntimeError("Phase tap changer are not handled by the pypowsybl converter "

lightsim2grid/tests/test_ACPF.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def create_transformers(n_pdp):
6060
return r, x, g, b
6161

6262

63-
class MyTestCase(unittest.TestCase):
63+
class TestACPF(unittest.TestCase):
6464
def setUp(self) -> None:
6565
self.tol = 3e-5 # results are equal if they match up to tol
6666
self.raise_error = True
@@ -69,6 +69,31 @@ def test_case14(self):
6969
case = pn.case14()
7070
self._aux_test(case)
7171

72+
def test_case14_with_phaseshift(self):
73+
case = pn.case14()
74+
hv_bus=0
75+
lv_bus=2
76+
pp.create_transformer_from_parameters(case,
77+
hv_bus=hv_bus,
78+
lv_bus=lv_bus,
79+
# sn_mva=1184.0, # case RTE
80+
sn_mva=9900.0,
81+
vn_hv_kv=case.bus.iloc[hv_bus]["vn_kv"],
82+
vn_lv_kv=case.bus.iloc[lv_bus]["vn_kv"],
83+
# i0_percent=-0.05152, # case RTE
84+
i0_percent=0.0,
85+
# vk_percent=0.404445, # case RTE
86+
vk_percent=2070.288000,
87+
# vkr_percent=0.049728, # case RTE
88+
vkr_percent=0.0,
89+
shift_degree=-10.0, # case RTE
90+
# shift_degree=0.,
91+
pfe_kw=0.
92+
)
93+
# self.tol = 2e-3
94+
case.name = "case14_2"
95+
self._aux_test(case)
96+
7297
def test_case39(self):
7398
case = pn.case39()
7499
self.tol = 3.1e-5 # results are equal if they match up to tol

lightsim2grid/tests/test_DCPF.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -43,11 +43,10 @@ def setUp(self) -> None:
4343
def test_case14(self):
4444
case = pn.case14()
4545
case.name = "case14"
46-
# self.tol = 2e-3
4746
self._aux_test(case)
4847

4948
def test_case14_with_phaseshift(self):
50-
self.skipTest("pypowsybl and pandapower does not align on DC computation and phase shift... Lightsim2grid choses pypowsybl")
49+
# self.skipTest("pandapower might do something weird in DC with phase shift")
5150
case = pn.case14()
5251
hv_bus=0
5352
lv_bus=2
@@ -84,43 +83,43 @@ def test_case118(self):
8483
self.tol = 4e-5
8584
self._aux_test(case)
8685

87-
# def test_case1888rte(self):
88-
# # does not work probably with None in converters
89-
# case = pn.case1888rte()
90-
# case.name = "case1888rte"
91-
# self.tol_kcl = 2e-3
92-
# self._aux_test(case)
86+
def test_case1888rte(self):
87+
# self.skipTest("pandapower might do something weird in DC with phase shift")
88+
case = pn.case1888rte()
89+
case.name = "case1888rte"
90+
self.tol_kcl = 2e-3
91+
self._aux_test(case)
9392

9493
def test_case300(self):
9594
case = pn.case300()
9695
self._aux_test(case)
9796

9897
# def test_case2848rte(self):
99-
# # does not work probably with None in converters
98+
# # too large for CI
10099
# case = pn.case2848rte()
101100
# case.name = "case2848rte"
102101
# self.tol = 4e-5
103102
# self.tol_kcl = 7e-4
104103
# self._aux_test(case)
105104

106105
# def test_case6470rte(self):
107-
# # does not work probably with None in converters
106+
# # # too large for CI
108107
# case = pn.case6470rte()
109108
# case.name = "case6470rte"
110109
# self.tol = 2e-4
111110
# self.tol_kcl = 7e-4
112111
# self._aux_test(case)
113112

114113
# def test_case6495rte(self):
115-
# # does not work probably with None in converters
114+
# # too large for CI
116115
# case = pn.case6495rte()
117116
# case.name = "case6495rte"
118117
# self.tol = 2e-4
119118
# self.tol_kcl = 5e-3
120119
# self._aux_test(case)
121120

122121
# def test_case6515rte(self):
123-
# # does not work probably with None in converters
122+
# # too large for CI
124123
# case = pn.case6515rte()
125124
# case.name = "case6515rte"
126125
# self.tol = 2e-4

lightsim2grid/tests/test_GridModel_pypowsybl.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,8 @@
88

99
import unittest
1010
import numpy as np
11-
import pandas as pd
12-
from packaging import version as version_packaging
1311

1412
import pypowsybl.network as pp_network
15-
import pypowsybl as pp
1613
import pypowsybl.loadflow as pp_lf
1714

1815
from lightsim2grid.gridmodel import init_from_pypowsybl
@@ -535,6 +532,8 @@ def test_change_trafo_shift(self):
535532
# check it has an impact on the load flow
536533
Vfinal = self._run_both_pf(self.net_ref)
537534
with self.assertRaises(AssertionError):
535+
# this checks the results are not the same
536+
# when pypowsybl grid is not updated
538537
self.check_res(Vfinal, self.net_ref)
539538

540539
self.skipTest("Hard to change the alpha of phase tap changer in pypowsbl")

lightsim2grid/tests/test_init_from_pypowsybl.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def test_basic(self):
112112
assert len(self.gridmodel.get_shunts()) == self.network_ref.get_shunt_compensators().shape[0]
113113

114114
def test_compare_pp(self):
115-
"""compare from the reference case14"""
115+
"""test I get the same results than pypowsybl, in AC"""
116116
if not self.can_pp:
117117
self.skipTest("no equivalent pandapower grid")
118118
if not self.compare_pp():
@@ -158,7 +158,7 @@ def test_compare_pp(self):
158158
assert max_ <= self.tol_eq, f"error for dc Sbus : {max_:.2e}"
159159

160160
def test_dc_pf(self):
161-
"""test I get the same results as pandapower in dc"""
161+
"""test I get the same results as pypowsybl in dc"""
162162
# if CURRENT_PYPOW_VERSION >= MIN_PYPO_DC_NOT_WORKING:
163163
# self.skipTest("Test not correct: pypowsybl change DC approx, see https://github.com/powsybl/pypowsybl/issues/1127")
164164
v_ls = self.gridmodel.dc_pf(self.V_init_dc, 2, self.tol)
@@ -175,7 +175,27 @@ def test_dc_pf(self):
175175
# v_mag not really relevant in dc so i study only va
176176
v_ang_pypo = self.network_ref.get_buses()["v_angle"].values
177177
v_ang_ls = np.rad2deg(np.angle(v_ls))
178-
# np.where(np.abs(v_ang_ls[reorder] - v_ang_pypo) >= 9.)
178+
# for case300
179+
# self.gridmodel.get_2_windings_transformers()[85]
180+
# shift of 0.19896753472735357 radian
181+
# 161 (bus1_id) and 424 (bus2_id)
182+
# DC
183+
# -9.859724243884386
184+
# 9.859724243884614
185+
# AC
186+
# 83.57031489368106
187+
# -83.56243958793598
188+
189+
# self.network_ref.get_2_windings_transformers(all_attributes=True).iloc[85]
190+
# alpha = 11.4 deg
191+
# bus1_id VL196_0, bus2_id VL196_1
192+
# self.network_ref.get_buses().loc[["VL196_0", "VL196_1"]]
193+
# DC
194+
# p1 88.74429
195+
# p2 -88.74429
196+
# AC
197+
# p1 83.570175
198+
# p2 -83.5623
179199
if self.compare_pp():
180200
v_ang_pp = self.pp_samecase.res_bus["va_degree"].values
181201
assert np.abs(v_ang_ls[reorder] - v_ang_pp).max() <= self.tol_eq, f"error for va results for dc: {np.abs(v_ang_ls[reorder] - v_ang_pp).max():.2e}"

src/GridModel.hpp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,14 @@ class GridModel final : public GenericContainer
608608
void change_ratio_trafo(int trafo_id, real_type new_ratio){
609609
trafos_.change_ratio(trafo_id, new_ratio, solver_control_);
610610
}
611+
612+
/**
613+
* The shift is in radian (not degree !)
614+
*
615+
* It is the shift on the "side 1" (regardless of the value of "is_tap_hv_side").
616+
* If the tap is on the other side, the user has the reponsibility to
617+
* take the opposite (ie -0.1 instead of +0.1)
618+
*/
611619
void change_shift_trafo(int trafo_id, real_type new_shift){
612620
trafos_.change_shift(trafo_id, new_shift, solver_control_);
613621
}

src/element_container/TrafoContainer.cpp

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ void TrafoContainer::_update_model_coeffs_one_el(int el_id)
136136
real_type theta_shift = shift_(el_id);
137137
if(!is_tap_hv_side_[el_id]){
138138
tau = my_one_ / tau;
139-
theta_shift = -theta_shift;
139+
// theta_shift = -theta_shift;
140140
}
141141
cplx_type eitheta_shift = {my_one_, my_zero_}; // exp(j * alpha)
142142
cplx_type emitheta_shift = {my_one_, my_zero_}; // exp(-j * alpha)
@@ -162,9 +162,9 @@ void TrafoContainer::_update_model_coeffs_one_el(int el_id)
162162
ydc_21_(el_id) = -tmp;
163163
ydc_12_(el_id) = -tmp;
164164

165-
dc_x_tau_shift_(el_id) = std::real(tmp) * theta_shift;
166-
// if(!is_tap_hv_side_[el_id]) dc_x_tau_shift_(el_id) = std::real(tmp) * theta_shift;
167-
// else dc_x_tau_shift_(el_id) = -std::real(tmp) * theta_shift;
165+
// dc_x_tau_shift_(el_id) = std::real(tmp) * theta_shift;
166+
if(!is_tap_hv_side_[el_id]) dc_x_tau_shift_(el_id) = -std::real(tmp) * theta_shift;
167+
else dc_x_tau_shift_(el_id) = +std::real(tmp) * theta_shift;
168168
}
169169

170170
void TrafoContainer::hack_Sbus_for_dc_phase_shifter(
@@ -222,8 +222,8 @@ void TrafoContainer::hack_Sbus_for_dc_phase_shifter(
222222
exc_ << " is connected (side 1) to a disconnected bus while being connected";
223223
throw std::runtime_error(exc_.str());
224224
}
225-
Sbus.coeffRef(bus_id_solver_hv.cast_int()) += dc_x_tau_shift_[trafo_id];
226-
Sbus.coeffRef(bus_id_solver_lv.cast_int()) -= dc_x_tau_shift_[trafo_id];
225+
Sbus.coeffRef(bus_id_solver_hv.cast_int()) -= dc_x_tau_shift_[trafo_id];
226+
Sbus.coeffRef(bus_id_solver_lv.cast_int()) += dc_x_tau_shift_[trafo_id];
227227
}
228228
}
229229

src/element_container/TrafoContainer.hpp

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -150,12 +150,19 @@ class TrafoContainer : public TwoSidesContainer_rxh_A<OneSideContainer_ForBranch
150150
}
151151
}
152152

153+
/**
154+
* The shift is in radian (not degree !)
155+
*
156+
* It is the shift on the "side 1" (regardless of the value of "is_tap_hv_side").
157+
* If the tap is on the other side, the user has the reponsibility to
158+
* take the opposite (ie -0.1 instead of +0.1)
159+
*/
153160
void change_shift(
154161
int el_id,
155-
real_type new_shift,
162+
real_type new_shift_rad,
156163
SolverControl & solver_control){
157-
if(std::abs(shift_(el_id) - new_shift) >_tol_equal_float){
158-
shift_(el_id) = new_shift;
164+
if(std::abs(shift_(el_id) - new_shift_rad) >_tol_equal_float){
165+
shift_(el_id) = new_shift_rad;
159166
// TODO speed: only some part needs to be recomputed
160167
_update_model_coeffs_one_el(el_id);
161168
solver_control.tell_recompute_ybus();

src/main.cpp

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -919,7 +919,7 @@ between 0 and `n_sub_ * max_nb_bus_per_sub_`
919919
.def("get_bus1_trafo", &GridModel::get_bus1_trafo, DocGridModel::_internal_do_not_use.c_str(), py::return_value_policy::reference)
920920
.def("get_bus2_trafo", &GridModel::get_bus2_trafo, DocGridModel::_internal_do_not_use.c_str(), py::return_value_policy::reference)
921921
.def("change_ratio_trafo", &GridModel::change_ratio_trafo, "TODO")
922-
.def("change_shift_trafo", &GridModel::change_shift_trafo, "TODO")
922+
.def("change_shift_trafo", &GridModel::change_shift_trafo, "TODO Change the phase shift ratio for a given transformer. It should be expressed in rad (not in deg) and on side1 (and not side2, as opposed to pypowsybl. If alpha is 10 in pypowsybl, you should convert it to rad (*eg* np.deg2rad(10.)), and invert it, so -np.deg2rad(10.). )")
923923

924924
.def("deactivate_load", &GridModel::deactivate_load, DocGridModel::_internal_do_not_use.c_str())
925925
.def("reactivate_load", &GridModel::reactivate_load, DocGridModel::_internal_do_not_use.c_str())

0 commit comments

Comments
 (0)