diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 51f9d85f3..abe611195 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,10 @@ Change Log ============= +[upcoming release] - 2024-..-.. +------------------------------- +- [FIXED] Increasing geojson precision as the default precision might cause problems with pandahub + [2.14.11] - 2024-07-08 ------------------------------- - [FIXED] Lightsim2grid version @@ -86,6 +90,7 @@ 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() +- [CHANGED] Trafo Controllers can now be added to elements that are out of service, changed self.nothing_to_do() - [ADDED] Discrete shunt controller for local voltage regulation with shunt steps - [ADDED] cim2pp converter: Using lxml to parse XML files (better performance) diff --git a/pandapower/__init__.py b/pandapower/__init__.py index acf95de67..120ddb725 100644 --- a/pandapower/__init__.py +++ b/pandapower/__init__.py @@ -17,6 +17,9 @@ from pandapower.runpm import * from pandapower.pf.runpp_3ph import runpp_3ph +import geojson +geojson.geometry.DEFAULT_PRECISION = 8 + import pandas as pd pd.options.mode.chained_assignment = None # default='warn' 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/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) diff --git a/pandapower/convert_format.py b/pandapower/convert_format.py index a9870cef8..e060df1f3 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): 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"]) 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 diff --git a/pyproject.toml b/pyproject.toml index e55eee3ba..77234e152 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ Changelog = "https://github.com/e2nIEE/pandapower/blob/develop/CHANGELOG.rst" docs = ["numpydoc", "matplotlib", "sphinx", "sphinx_rtd_theme", "sphinx-pyproject"] plotting = ["plotly>=3.1.1", "matplotlib", "igraph", "geopandas>=1.0"] test = ["pytest~=8.1", "pytest-xdist", "nbmake"] -performance = ["ortools", "numba>=0.25", "lightsim2grid>=0.9.0"] +performance = ["ortools", "numba>=0.25", "lightsim2grid==0.9.0"] fileio = ["xlsxwriter", "openpyxl", "cryptography", "geopandas", "psycopg2"] converter = ["matpowercaseframes"] pgm = ["power-grid-model-io"] @@ -71,7 +71,7 @@ all = [ "numpydoc", "sphinx", "sphinx_rtd_theme", "sphinx-pyproject", "plotly>=3.1.1", "matplotlib", "igraph", "geopandas>=1.0", "pytest~=8.1", "pytest-xdist", "nbmake", - "ortools", "numba>=0.25", "lightsim2grid>=0.9.0", + "ortools", "numba>=0.25", "lightsim2grid==0.9.0", "xlsxwriter", "openpyxl", "cryptography", "psycopg2", "matpowercaseframes", "power-grid-model-io" 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", "