From fd167b8553fd2fa24fe619b19b6335d23864571a Mon Sep 17 00:00:00 2001 From: pawellytaev Date: Wed, 4 Sep 2024 13:58:48 +0200 Subject: [PATCH 1/6] add test for out of service trafo with controller --- pandapower/test/control/test_continuous_tap_control.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/pandapower/test/control/test_continuous_tap_control.py b/pandapower/test/control/test_continuous_tap_control.py index b1e24057c..dd1f25388 100644 --- a/pandapower/test/control/test_continuous_tap_control.py +++ b/pandapower/test/control/test_continuous_tap_control.py @@ -288,6 +288,11 @@ def test_continuous_tap_control_side_hv_reversed_3w(): assert np.allclose(net.res_trafo3w.vm_hv_pu.values, 1.02, atol=tol) assert not np.allclose(net.trafo3w.tap_pos.values, 0) +def test_continuous_trafo_control_with_oos_trafo(): + net = pp.networks.mv_oberrhein() + # switch transformer out of service + net.trafo.loc[114, 'in_service'] = False + ContinuousTapControl(net=net, element_index=114, vm_set_pu=1.0, tol=0.001) if __name__ == '__main__': pytest.main([__file__, "-xs"]) From f5baf7a828fe44cac040421cebb40d3b62eb8422 Mon Sep 17 00:00:00 2001 From: pawellytaev Date: Wed, 4 Sep 2024 18:28:50 +0200 Subject: [PATCH 2/6] enhance nothing_to_do to check for element in service status instead of in TrafoControl.__init__ --- .../control/controller/trafo_control.py | 59 +++++++++++-------- 1 file changed, 34 insertions(+), 25 deletions(-) diff --git a/pandapower/control/controller/trafo_control.py b/pandapower/control/controller/trafo_control.py index 267238786..d3cb3ae44 100644 --- a/pandapower/control/controller/trafo_control.py +++ b/pandapower/control/controller/trafo_control.py @@ -46,7 +46,7 @@ def __init__(self, net, element_index, side, tol, in_service, element, level=0, self._set_side(side) self._set_read_write_flag(net) - self._set_valid_controlled_index_and_bus(net) + # self._set_valid_controlled_index_and_bus(net) self._set_tap_parameters(net) self._set_tap_side_coeff(net) @@ -54,6 +54,8 @@ def __init__(self, net, element_index, side, tol, in_service, element, level=0, self.set_recycle(net) + self.trafobus = read_from_net(net, self.element, self.element_index, self.side + '_bus', self._read_write_flag) + def _set_read_write_flag(self, net): # if someone changes indices of the controller from single index to array and vice versa self._read_write_flag, _ = _detect_read_write_flag(net, self.element, self.element_index, "tap_pos") @@ -67,25 +69,36 @@ def initialize_control(self, net): # update trafo tap parameters # we assume side does not change after the controller is created self._set_read_write_flag(net) - self._set_valid_controlled_index_and_bus(net) + # self._set_valid_controlled_index_and_bus(net) if self.nothing_to_do(net): return self._set_tap_parameters(net) self._set_tap_side_coeff(net) def nothing_to_do(self, net): - # if the controller shouldn't do anything, return True - if self.controlled_element_index is None or ( - self._read_write_flag != 'single_index' and len(self.controlled_element_index) == 0): - return True - return False + element_in_service = read_from_net(net, self.element, self.element_index, 'in_service', self._read_write_flag) + ext_grid_bus = np.isin(self.trafobus, net.ext_grid.loc[net.ext_grid.in_service, 'bus'].values) + element_index_in_net = np.isin(self.element_index, net[self.element].index.values) + self.controlled = np.logical_and(np.logical_and(element_in_service, element_index_in_net), np.logical_not(ext_grid_bus)) + if isinstance(self.element_index, np.int64) or isinstance(self.element_index, int): + # if the controller shouldn't do anything, return True + if not element_in_service or ext_grid_bus or not element_index_in_net or ( + self._read_write_flag != 'single_index' and len(self.element_index) == 0): + return True + return False + else: + # if the controller shouldn't do anything, return True + if np.all(~element_in_service[self.controlled]) or np.all(ext_grid_bus[self.controlled]) or np.all(~element_index_in_net[self.controlled]) or ( + self._read_write_flag != 'single_index' and len(self.element_index) == 0): + return True + return False def _set_tap_side_coeff(self, net): - tap_side = read_from_net(net, self.element, self.controlled_element_index, 'tap_side', self._read_write_flag) + tap_side = read_from_net(net, self.element, self.element_index, 'tap_side', self._read_write_flag) if (len(np.setdiff1d(tap_side, ['hv', 'lv'])) > 0 and self.element == "trafo") or \ (len(np.setdiff1d(tap_side, ['hv', 'lv', 'mv'])) > 0 and self.element == "trafo3w"): raise ValueError("Trafo tap side (in net.%s) has to be either hv or lv, " - "but received: %s for trafo %s" % (self.element, tap_side, self.controlled_element_index)) + "but received: %s for trafo %s" % (self.element, tap_side, self.element_index)) if self._read_write_flag == "single_index": self.tap_side_coeff = 1 if tap_side == 'hv' else -1 @@ -113,29 +126,25 @@ def _set_side(self, side): self.side = side def _set_valid_controlled_index_and_bus(self, net): - self.trafobus = read_from_net(net, self.element, self.element_index, self.side + '_bus', self._read_write_flag) element_in_service = read_from_net(net, self.element, self.element_index, 'in_service', self._read_write_flag) ext_grid_bus = np.isin(self.trafobus, net.ext_grid.loc[net.ext_grid.in_service, 'bus'].values) element_index_in_net = np.isin(self.element_index, net[self.element].index.values) - controlled = np.logical_and(np.logical_and(element_in_service, element_index_in_net), np.logical_not(ext_grid_bus)) - if self._read_write_flag == 'single_index': - self.controlled_element_index = self.element_index if controlled else None - self.controlled_bus = self.trafobus if controlled else None - else: - self.controlled_element_index = self.element_index[controlled] - self.controlled_bus = self.trafobus[controlled] + self.controlled = np.logical_and(np.logical_and(element_in_service, element_index_in_net), np.logical_not(ext_grid_bus)) + if self._read_write_flag != 'single_index': + self.element_index = self.element_index[self.controlled] + self.trafobus = self.trafobus[self.controlled] - if np.all(~controlled): + if np.all(~self.controlled): logger.warning("All controlled buses are not valid: controller has no effect") def _set_tap_parameters(self, net): - self.tap_min = read_from_net(net, self.element, self.controlled_element_index, "tap_min", self._read_write_flag) - self.tap_max = read_from_net(net, self.element, self.controlled_element_index, "tap_max", self._read_write_flag) - self.tap_neutral = read_from_net(net, self.element, self.controlled_element_index, "tap_neutral", self._read_write_flag) - self.tap_step_percent = read_from_net(net, self.element, self.controlled_element_index, "tap_step_percent", self._read_write_flag) - self.tap_step_degree = read_from_net(net, self.element, self.controlled_element_index, "tap_step_degree", self._read_write_flag) + self.tap_min = read_from_net(net, self.element, self.element_index, "tap_min", self._read_write_flag) + self.tap_max = read_from_net(net, self.element, self.element_index, "tap_max", self._read_write_flag) + self.tap_neutral = read_from_net(net, self.element, self.element_index, "tap_neutral", self._read_write_flag) + self.tap_step_percent = read_from_net(net, self.element, self.element_index, "tap_step_percent", self._read_write_flag) + self.tap_step_degree = read_from_net(net, self.element, self.element_index, "tap_step_degree", self._read_write_flag) - self.tap_pos = read_from_net(net, self.element, self.controlled_element_index, "tap_pos", self._read_write_flag) + self.tap_pos = read_from_net(net, self.element, self.element_index, "tap_pos", self._read_write_flag) if self._read_write_flag == "single_index": self.tap_sign = 1 if np.isnan(self.tap_step_degree) else np.sign(np.cos(np.deg2rad(self.tap_step_degree))) if (self.tap_sign == 0) | (np.isnan(self.tap_sign)): @@ -163,7 +172,7 @@ def set_recycle(self, net): net.controller.at[self.index, 'recycle'] = recycle # def timestep(self, net): - # self.tap_pos = net[self.element].at[self.controlled_element_index, "tap_pos"] + # self.tap_pos = net[self.element].at[self.element_index, "tap_pos"] def __repr__(self): s = '%s of %s %s' % (self.__class__.__name__, self.element, self.element_index) From c339ff5ffb1f9e87b82317c77a914388e1d14cf8 Mon Sep 17 00:00:00 2001 From: pawellytaev Date: Wed, 4 Sep 2024 18:30:02 +0200 Subject: [PATCH 3/6] change convert_format and tap controllers for new parameter names, controlled_bus -> trafobus, controlled_element_index -> element_index --- .../controller/trafo/ContinuousTapControl.py | 20 +++++++++---------- .../controller/trafo/DiscreteTapControl.py | 10 +++++----- pandapower/convert_format.py | 2 ++ 3 files changed, 17 insertions(+), 15 deletions(-) diff --git a/pandapower/control/controller/trafo/ContinuousTapControl.py b/pandapower/control/controller/trafo/ContinuousTapControl.py index 612b94b14..20e07d68b 100644 --- a/pandapower/control/controller/trafo/ContinuousTapControl.py +++ b/pandapower/control/controller/trafo/ContinuousTapControl.py @@ -50,18 +50,18 @@ def __init__(self, net, element_index, vm_set_pu, tol=1e-3, side="lv", element=" self.vm_set_pu = vm_set_pu def _set_t_nom(self, net): - vn_hv_kv = read_from_net(net, self.element, self.controlled_element_index, 'vn_hv_kv', self._read_write_flag) - hv_bus = read_from_net(net, self.element, self.controlled_element_index, 'hv_bus', self._read_write_flag) + vn_hv_kv = read_from_net(net, self.element, self.element_index, 'vn_hv_kv', self._read_write_flag) + hv_bus = read_from_net(net, self.element, self.element_index, 'hv_bus', self._read_write_flag) vn_hv_bus_kv = read_from_net(net, "bus", hv_bus, 'vn_kv', self._read_write_flag) if self.element == "trafo3w" and self.side == "mv": - vn_mv_kv = read_from_net(net, self.element, self.controlled_element_index, 'vn_mv_kv', self._read_write_flag) - mv_bus = read_from_net(net, self.element, self.controlled_element_index, 'mv_bus', self._read_write_flag) + vn_mv_kv = read_from_net(net, self.element, self.element_index, 'vn_mv_kv', self._read_write_flag) + mv_bus = read_from_net(net, self.element, self.element_index, 'mv_bus', self._read_write_flag) vn_mv_bus_kv = read_from_net(net, "bus", mv_bus, 'vn_kv', self._read_write_flag) self.t_nom = vn_mv_kv / vn_hv_kv * vn_hv_bus_kv / vn_mv_bus_kv else: - vn_lv_kv = read_from_net(net, self.element, self.controlled_element_index, 'vn_lv_kv', self._read_write_flag) - lv_bus = read_from_net(net, self.element, self.controlled_element_index, 'lv_bus', self._read_write_flag) + vn_lv_kv = read_from_net(net, self.element, self.element_index, 'vn_lv_kv', self._read_write_flag) + lv_bus = read_from_net(net, self.element, self.element_index, 'lv_bus', self._read_write_flag) vn_lv_bus_kv = read_from_net(net, "bus", lv_bus, 'vn_kv', self._read_write_flag) self.t_nom = vn_lv_kv / vn_hv_kv * vn_hv_bus_kv / vn_lv_bus_kv @@ -77,7 +77,7 @@ def control_step(self, net): if self.nothing_to_do(net): return - delta_vm_pu = read_from_net(net, "res_bus", self.controlled_bus, 'vm_pu', self._read_write_flag) - self.vm_set_pu + delta_vm_pu = read_from_net(net, "res_bus", self.trafobus, 'vm_pu', self._read_write_flag) - self.vm_set_pu tc = delta_vm_pu / self.tap_step_percent * 100 / self.t_nom self.tap_pos = self.tap_pos + tc * self.tap_side_coeff * self.tap_sign if self.check_tap_bounds: @@ -87,7 +87,7 @@ def control_step(self, net): # necessary in case the dtype of the column is int if net[self.element].tap_pos.dtype != "float": net[self.element].tap_pos = net[self.element].tap_pos.astype(float) - write_to_net(net, self.element, self.controlled_element_index, "tap_pos", self.tap_pos, self._read_write_flag) + write_to_net(net, self.element, self.element_index, "tap_pos", self.tap_pos, self._read_write_flag) def is_converged(self, net): """ @@ -97,10 +97,10 @@ def is_converged(self, net): if self.nothing_to_do(net): return True - vm_pu = read_from_net(net, "res_bus", self.controlled_bus, "vm_pu", self._read_write_flag) + vm_pu = read_from_net(net, "res_bus", self.trafobus, "vm_pu", self._read_write_flag) # this is possible in case the trafo is set out of service by the connectivity check is_nan = np.isnan(vm_pu) - self.tap_pos = read_from_net(net, self.element, self.controlled_element_index, "tap_pos", self._read_write_flag) + self.tap_pos = read_from_net(net, self.element, self.element_index, "tap_pos", self._read_write_flag) difference = 1 - self.vm_set_pu / vm_pu if self.check_tap_bounds: diff --git a/pandapower/control/controller/trafo/DiscreteTapControl.py b/pandapower/control/controller/trafo/DiscreteTapControl.py index d2647d778..aaa1d80db 100644 --- a/pandapower/control/controller/trafo/DiscreteTapControl.py +++ b/pandapower/control/controller/trafo/DiscreteTapControl.py @@ -108,9 +108,9 @@ def control_step(self, net): if self.nothing_to_do(net): return - vm_pu = read_from_net(net, "res_bus", self.controlled_bus, "vm_pu", self._read_write_flag) + vm_pu = read_from_net(net, "res_bus", self.trafobus, "vm_pu", self._read_write_flag) self.tap_pos = read_from_net( - net, self.element, self.controlled_element_index, "tap_pos", self._read_write_flag) + net, self.element, self.element_index, "tap_pos", self._read_write_flag) increment = np.where( self.tap_side_coeff * self.tap_sign == 1, @@ -126,7 +126,7 @@ def control_step(self, net): self._hunting_taps = self._hunting_taps[1:, :] # WRITE TO NET - write_to_net(net, self.element, self.controlled_element_index, 'tap_pos', + write_to_net(net, self.element, self.element_index, 'tap_pos', self.tap_pos, self._read_write_flag) def is_converged(self, net): @@ -136,11 +136,11 @@ def is_converged(self, net): if self.nothing_to_do(net): return True - vm_pu = read_from_net(net, "res_bus", self.controlled_bus, "vm_pu", self._read_write_flag) + vm_pu = read_from_net(net, "res_bus", self.trafobus, "vm_pu", self._read_write_flag) # this is possible in case the trafo is set out of service by the connectivity check is_nan = np.isnan(vm_pu) self.tap_pos = read_from_net( - net, self.element, self.controlled_element_index, "tap_pos", self._read_write_flag) + net, self.element, self.element_index, "tap_pos", self._read_write_flag) reached_limit = np.where(self.tap_side_coeff * self.tap_sign == 1, (vm_pu < self.vm_lower_pu) & (self.tap_pos == self.tap_min) | diff --git a/pandapower/convert_format.py b/pandapower/convert_format.py index e30dfef70..94e2f0962 100644 --- a/pandapower/convert_format.py +++ b/pandapower/convert_format.py @@ -154,6 +154,8 @@ def _convert_trafo_controller_parameter_names(net): elif "trafotype" in controller.__dict__.keys(): controller.__dict__["element"] = controller.__dict__.pop("trafotype") + if "controlled_bus" in controller.__dict__.keys(): + controller.__dict__["trafobus"] = controller.__dict__.pop("controlled_bus") def _convert_bus_pq_meas_to_load_reference(net, elements_to_deserialize): if _check_elements_to_deserialize('measurement', elements_to_deserialize): From 83d07e6542bbe8967f48f178f3d324eb2add4eec Mon Sep 17 00:00:00 2001 From: pawellytaev Date: Wed, 4 Sep 2024 18:30:32 +0200 Subject: [PATCH 4/6] adjust tests for new trafo control parameter names --- .../test/control/test_discrete_tap_control.py | 5 +++++ .../test/control/test_vm_set_tap_control.py | 20 +++++++++---------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/pandapower/test/control/test_discrete_tap_control.py b/pandapower/test/control/test_discrete_tap_control.py index 5a12eaab2..3d8f34769 100644 --- a/pandapower/test/control/test_discrete_tap_control.py +++ b/pandapower/test/control/test_discrete_tap_control.py @@ -390,6 +390,11 @@ def test_continuous_tap_control_side_mv(): assert all(_vm_in_desired_area(net, 1.01, 1.03, "mv", trafo_table="trafo3w")) assert not np.allclose(net.trafo3w.tap_pos.values, 0) +def test_discrete_trafo_control_with_oos_trafo(): + net = pp.networks.mv_oberrhein() + # switch transformer out of service + net.trafo.loc[114, 'in_service'] = False + DiscreteTapControl(net=net, element_index=114, vm_lower_pu=1.01, vm_upper_pu=1.03) if __name__ == '__main__': pytest.main([__file__, "-xs"]) diff --git a/pandapower/test/control/test_vm_set_tap_control.py b/pandapower/test/control/test_vm_set_tap_control.py index 61e87d42c..033a3dfd9 100644 --- a/pandapower/test/control/test_vm_set_tap_control.py +++ b/pandapower/test/control/test_vm_set_tap_control.py @@ -28,21 +28,21 @@ def test_continuous_p(): pp.runpp(net, run_control=True) # we expect the tap to converge at upper voltage limit - assert abs(net.res_bus.vm_pu.at[c.controlled_bus] - 1.05) < eps + assert abs(net.res_bus.vm_pu.at[c.trafobus] - 1.05) < eps # power sums up to 15kW net.sgen.at[gid, "p_mw"] = 5 pp.runpp(net, run_control=True) # we expect the tap to converge at 1.0 pu - assert abs(net.res_bus.vm_pu.at[c.controlled_bus] - 1.) < eps + assert abs(net.res_bus.vm_pu.at[c.trafobus] - 1.) < eps # generation now cancels load net.sgen.at[gid, "p_mw"] = 10 pp.runpp(net, run_control=True) # we expect the tap to converge at lower voltage limit - assert abs(net.res_bus.vm_pu.at[c.controlled_bus] - 0.95) < eps + assert abs(net.res_bus.vm_pu.at[c.trafobus] - 0.95) < eps # testing limits # power flowing back @@ -50,7 +50,7 @@ def test_continuous_p(): pp.runpp(net, run_control=True) # we expect the tap to converge at lower voltage limit and not drop even lower - assert abs(net.res_bus.vm_pu.at[c.controlled_bus] - 0.95) < eps + assert abs(net.res_bus.vm_pu.at[c.trafobus] - 0.95) < eps # excessive load net.sgen.at[gid, "p_mw"] = 0 @@ -58,7 +58,7 @@ def test_continuous_p(): pp.runpp(net, run_control=True) # we expect the tap to converge at upper voltage limit and not to go beyond - assert abs(net.res_bus.vm_pu.at[c.controlled_bus] - 1.05) < eps + assert abs(net.res_bus.vm_pu.at[c.trafobus] - 1.05) < eps def test_continuous_i(): @@ -81,21 +81,21 @@ def test_continuous_i(): pp.runpp(net, run_control=True) # we expect the tap to converge at upper voltage limit - assert abs(net.res_bus.vm_pu.at[c.controlled_bus] - 1.05) < eps + assert abs(net.res_bus.vm_pu.at[c.trafobus] - 1.05) < eps # power sums up to 15kW net.sgen.at[gid, "p_mw"] = 5 pp.runpp(net, run_control=True) # we expect the tap to converge at 1.0 pu - assert abs(net.res_bus.vm_pu.at[c.controlled_bus] - 1.) < eps + assert abs(net.res_bus.vm_pu.at[c.trafobus] - 1.) < eps # generation now cancels load net.sgen.at[gid, "p_mw"] = 10 pp.runpp(net, run_control=True) # we expect the tap to converge at lower voltage limit - assert abs(net.res_bus.vm_pu.at[c.controlled_bus] - 0.95) < eps + assert abs(net.res_bus.vm_pu.at[c.trafobus] - 0.95) < eps # testing limits # power flowing back @@ -103,7 +103,7 @@ def test_continuous_i(): pp.runpp(net, run_control=True) # we expect the tap to converge at lower voltage limit and not drop even lower - assert abs(net.res_bus.vm_pu.at[c.controlled_bus] - 0.95) < eps + assert abs(net.res_bus.vm_pu.at[c.trafobus] - 0.95) < eps # excessive load net.sgen.at[gid, "p_mw"] = 0 @@ -111,7 +111,7 @@ def test_continuous_i(): pp.runpp(net, run_control=True) # we expect the tap to converge at upper voltage limit and not to go beyond - assert abs(net.res_bus.vm_pu.at[c.controlled_bus] - 1.05) < eps + assert abs(net.res_bus.vm_pu.at[c.trafobus] - 1.05) < eps if __name__ == '__main__': pytest.main([__file__, "-xs"]) \ No newline at end of file From 5e67f17c52e05febd94ea971e4bfb25292a7548c Mon Sep 17 00:00:00 2001 From: pawellytaev Date: Wed, 4 Sep 2024 18:59:37 +0200 Subject: [PATCH 5/6] changelog for trafo ctrl changes --- CHANGELOG.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index c06c4aba3..4417777df 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -79,7 +79,8 @@ Change Log - [ADDED] Add VSC element, dc buses, dc lines, and hybrid AC/DC power flow calculation - [CHANGED] accelerate _integrate_power_elements_connected_with_switch_buses() in get_equivalent() - [CHANGED] accelerate distributed slack power flow calculation by using sparse-aware operations in _subnetworks() -- [ADDED] Discrete shunt controller for local voltage regulation with shunt steps +- [ADDED] Discrete shunt controller for local voltage regulation with shunt +- [CHANGED] Trafo Controllers can now be added to elements that are out of service, changed self.nothing_to_do() [2.14.7] - 2024-06-14 ------------------------------- From f7b36532749209987b9749f3a716858aa794bec5 Mon Sep 17 00:00:00 2001 From: pawellytaev Date: Wed, 4 Sep 2024 21:30:10 +0200 Subject: [PATCH 6/6] update results in minimal_example.ipynb --- tutorials/minimal_example.ipynb | 560 +++++++++++++++++++------------- 1 file changed, 342 insertions(+), 218 deletions(-) diff --git a/tutorials/minimal_example.ipynb b/tutorials/minimal_example.ipynb index 2a33f4eb8..9d9e3f1c1 100644 --- a/tutorials/minimal_example.ipynb +++ b/tutorials/minimal_example.ipynb @@ -18,9 +18,12 @@ }, { "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [], + "metadata": { + "ExecuteTime": { + "end_time": "2024-09-04T19:28:40.415709Z", + "start_time": "2024-09-04T19:28:31.792136Z" + } + }, "source": [ "import pandapower as pp\n", "\n", @@ -39,7 +42,9 @@ "#create branch elements\n", "trafo = pp.create_transformer(net, hv_bus=bus1, lv_bus=bus2, std_type=\"0.4 MVA 20/0.4 kV\", name=\"Trafo\")\n", "line = pp.create_line(net, from_bus=bus2, to_bus=bus3, length_km=0.1, std_type=\"NAYY 4x50 SE\", name=\"Line\")" - ] + ], + "outputs": [], + "execution_count": 1 }, { "cell_type": "markdown", @@ -52,11 +57,24 @@ }, { "cell_type": "code", - "execution_count": 2, - "metadata": {}, + "metadata": { + "ExecuteTime": { + "end_time": "2024-09-04T19:28:40.446233Z", + "start_time": "2024-09-04T19:28:40.419268Z" + } + }, + "source": [ + "net.bus" + ], "outputs": [ { "data": { + "text/plain": [ + " name vn_kv type zone in_service geo\n", + "0 Bus 1 20.0 b None True None\n", + "1 Bus 2 0.4 b None True None\n", + "2 Bus 3 0.4 b None True None" + ], "text/html": [ "
\n", "