diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 1071d8847f..1dc3a1cf58 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,6 +3,11 @@ Change Log [upcoming release] - 2026-..-.. ------------------------------- +- [ADDED] :code:`BinarySearchControl.for_stations`: one station controller instance managing many stations, each with its own control modus, set point, measurement, outputs and optional droop characteristic (droop is folded into the station residual instead of a chained :code:`DroopControl`); reduces run_control overhead by >10x for hundreds of stations +- [ADDED] opt-in :code:`update_method="jacobian"` for multi-station controllers: coupled Newton steps for V_ctrl stations from the dVm/dQ sensitivities of the powerflow Jacobian (fewer powerflows, converges electrically coupled stations that oscillate under independent iterations); new utility :code:`pandapower.control.util.sensitivity.calc_dvm_dq` +- [CHANGED] stabilized the BinarySearchControl update: residual-scaled first step for Q/PF/tan(phi) modi, bracketing (Illinois regula falsi) safeguards, bounded steps on flat measurement response (no more output blow-up), stagnation diagnostics; the power factor set point is no longer permanently overwritten by the near-zero clipping +- [CHANGED] vectorized the BinarySearchControl hot path (positional lookups precomputed in initialize_control, no per-iteration scan of all controllers); fixed crashes with partially out-of-service output elements +- [ADDED] station controller characterization and benchmark suites (pandapower/test/control/test_stactrl_characterization.py, .../benchmarks/) [3.5.4] - 2026-07-08 ------------------------------- diff --git a/doc/control.rst b/doc/control.rst index bcb150aa41..31219ea3ca 100644 --- a/doc/control.rst +++ b/doc/control.rst @@ -9,10 +9,11 @@ The control module allows you to simulate these control strategies by either usi controller in an object oriented framework. The controller module is closely integrated with the timeseries module, which allows you to run quasi-static timeseries simulations with controlled elements. -.. toctree:: +.. toctree:: :maxdepth: 2 - + control/control_loop control/run control/controller + control/station_control control/tutorials \ No newline at end of file diff --git a/doc/control/controller.rst b/doc/control/controller.rst index a6714ca0fd..ee00416e60 100644 --- a/doc/control/controller.rst +++ b/doc/control/controller.rst @@ -115,6 +115,11 @@ Station Controller The following controllers are used for the representation of station controllers as used in PowerFactory. The Vdroop is a new controller class used for the local droop voltage control. +A ``BinarySearchControl`` created through its constructor controls one station; one controller instance can +also manage many stations at once via :meth:`BinarySearchControl.for_stations`, including per-station droop +characteristics and an opt-in Jacobian-based update (``update_method="jacobian"``). Usage, convergence +behaviour and benchmark results are documented in :doc:`station_control`. + ********************** Binary Search Control ********************** diff --git a/doc/control/station_control.rst b/doc/control/station_control.rst new file mode 100644 index 0000000000..b29f7f93d6 --- /dev/null +++ b/doc/control/station_control.rst @@ -0,0 +1,371 @@ +################## +Station Controller +################## + +The station controller (:class:`~pandapower.control.controller.station_control.BinarySearchControl`, +short *BSC*) adjusts the reactive power of a group of generation units ("a station") until a +measured quantity reaches a set point. It supports reactive power control (``Q_ctrl``), voltage +control (``V_ctrl``), power factor control (``PF_ctrl_ind`` / ``PF_ctrl_cap``), ``tan_phi_ctrl`` +and droop variants of the Q and V control modi, mirroring the station controllers known from +PowerFactory. + +This page describes the reworked implementation: how to use the controller correctly (single +station and the new multi-station mode), the opt-in Jacobian sensitivity update, and the +convergence and runtime improvements over the previous implementation. + +.. contents:: + :local: + :depth: 2 + + +Controlling a single station +============================ + +The constructor API is unchanged and fully backward compatible: one +``BinarySearchControl`` instance controls one station. Saved nets (JSON) created with earlier +pandapower versions and nets imported from PowerFactory keep working without modification, +including chained :class:`~pandapower.control.controller.station_control.DroopControl` / +:class:`~pandapower.control.controller.station_control.VDroopControl_local` controllers. + +:: + + from pandapower.control.controller.station_control import BinarySearchControl + from pandapower.run import runpp + + BinarySearchControl( + net, name="station_1", ctrl_in_service=True, + output_element="sgen", output_variable="q_mvar", + output_element_index=[0, 1], output_element_in_service=[True, True], + output_values_distribution=[0.6, 0.4], # Q share of each output element + input_element="res_line", input_variable=["q_to_mvar"], input_element_index=0, + set_point=1.0, control_modus="Q_ctrl", tol=1e-6) + + runpp(net, run_control=True) + +Notes on correct usage: + +- ``output_values_distribution`` is normalized internally; it defines how the total station + output is shared among the output elements. +- The measurement (``input_element`` / ``input_variable`` / ``input_element_index``) must + actually respond to the controlled elements. If it does not (for example a line on a + different feeder), the controller now keeps its outputs bounded and logs a stagnation + warning instead of blowing the values up, but it can of course not converge. +- ``input_inverted=True`` flips the sign of the measurement (needed when the measured branch + orientation is opposite to the control direction, e.g. for PowerFactory imports). +- Reactive power limits (``min_q_mvar`` / ``max_q_mvar`` columns, or Q capability + characteristics) are respected when the powerflow is run with ``enforce_q_lims=True``. +- ``damping_factor`` (e.g. 0.9) softens the first probing step and the fallback steps; it is + deliberately *not* applied to regular secant steps, so well-behaved controllers converge at + full speed. + + +Controlling many stations: ``for_stations`` +=========================================== + +For grids with tens to hundreds of stations, creating one controller instance per station +wastes most of the run time in per-controller bookkeeping. The recommended API is one +controller instance that manages all stations of one output table: + +:: + + from pandapower.control.controller.station_control import BinarySearchControl + + stations = [ + # a voltage-controlled station with two sgens + dict(control_modus="V_ctrl", set_point=1.02, + input_element="res_bus", input_variable="vm_pu", input_element_index=2, + output_element_index=[0, 1], output_values_distribution=[0.6, 0.4]), + # a Q-controlled station measuring a line flow + dict(control_modus="Q_ctrl", set_point=0.5, + input_element="res_line", input_variable="q_to_mvar", input_element_index=4, + output_element_index=[2, 3], output_values_distribution=[0.5, 0.5]), + # a voltage-controlled station with Q droop: in the converged state + # vm(bus 7) == set_point + q_hv_mvar(trafo 1) / q_droop_mvar + dict(control_modus="V_ctrl_Q_droop", set_point=1.02, + input_element="res_trafo", input_variable="q_hv_mvar", input_element_index=1, + droop=dict(q_droop_mvar=40, bus_idx=7), + output_element_index=[4, 5], output_values_distribution=[1, 1]), + ] + BinarySearchControl.for_stations(net, stations, output_element="sgen", + output_variable="q_mvar", tol=1e-6) + +Each station dict carries its own configuration: + +.. list-table:: + :widths: 25 75 + :header-rows: 1 + + * - key + - meaning + * - ``control_modus`` + - ``"Q_ctrl"``, ``"V_ctrl"``, ``"PF_ctrl_ind"``, ``"PF_ctrl_cap"``, ``"tan_phi_ctrl"``, + ``"Q_ctrl_V_droop"``, ``"V_ctrl_Q_droop"`` or ``"V_ctrl_Q_droop_local"`` + * - ``set_point`` + - target value; for droop modi the *base* set point of the droop characteristic + * - ``input_element`` / ``input_variable`` / ``input_element_index`` + - the measurement, e.g. ``"res_line"`` / ``"q_to_mvar"`` / ``4``; plain ``V_ctrl`` + measures ``"res_bus"`` / ``"vm_pu"`` at the controlled bus. Lists are allowed for + multiple measurement elements per station + * - ``input_inverted`` + - bool or list of bool, flips the measurement sign + * - ``output_element_index`` / ``output_values_distribution`` + - controlled elements in the shared output table and their share of the station total + * - ``tol`` + - optional per-station tolerance (defaults to the instance tolerance) + * - ``droop`` + - required for droop modi, see below + * - ``name`` + - optional, used in log messages + +Droop is part of the station, not a second controller +------------------------------------------------------ + +In the legacy API, droop behaviour required chaining a separate ``DroopControl`` that +rewrites the BSC set point between iterations -- two coupled fixed-point loops that iterate +against each other. In multi-station mode, the droop characteristic is evaluated inside the +station residual, so there is only one loop. The ``droop`` dict supports: + +.. list-table:: + :widths: 30 70 + :header-rows: 1 + + * - key + - meaning + * - ``q_droop_mvar`` + - droop constant in Mvar/pu (all droop modi) + * - ``bus_idx`` + - the voltage-measured bus (all droop modi) + * - ``vm_set_lb`` / ``vm_set_ub`` + - deadband borders for ``Q_ctrl_V_droop``: inside the band the station holds + ``set_point``, outside it the Q set point follows the droop line + * - ``vm_set_pu`` + - voltage reference for ``Q_ctrl_V_droop`` *without* deadband: + ``q_set = set_point + (vm_set_pu - vm) * q_droop_mvar`` + * - ``q_set_mvar`` + - local Q reference for ``V_ctrl_Q_droop_local``: + ``vm = set_point - (q_meas - q_set_mvar) / q_droop_mvar`` + +Converged droop states: + +- ``V_ctrl_Q_droop``: ``vm(bus_idx) = set_point + q_meas / q_droop_mvar`` +- ``Q_ctrl_V_droop`` (deadband): ``q_meas = set_point`` inside the band, + ``q_meas = set_point ± (band border - vm) * q_droop_mvar`` outside +- ``V_ctrl_Q_droop_local``: ``vm(bus_idx) = set_point - (q_meas - q_set_mvar) / q_droop_mvar`` + +Constraints and behaviour +------------------------- + +- One instance writes one output table and column (``output_element`` / + ``output_variable``). Stations writing to a different table (e.g. shunt steps) need a + second instance. Output elements should not be shared between stations. +- Stations whose input or output elements are all out of service are skipped with a warning; + the remaining stations keep working. +- Stations that reach their reactive power limits (with ``enforce_q_lims=True``) freeze the + limited outputs at the limit and redistribute the remainder; if all outputs are limited, + the station is reported converged-at-limit. +- Already-converged stations are frozen while the others keep iterating. +- ``for_stations`` controllers serialize with :func:`pandapower.to_json` and restore with + :func:`pandapower.from_json` like any other controller. + + +Jacobian sensitivities: ``update_method="jacobian"`` +==================================================== + +By default (``update_method="secant"``) every station iterates independently on its own +residual. For voltage control this has a fundamental limitation: two ``V_ctrl`` stations that +are electrically close (for example behind the same transformer) see each other's actions as +measurement noise, and the interleaved iteration may oscillate without converging -- the +previous implementation could not solve such configurations either. + +With ``update_method="jacobian"``, plain ``V_ctrl`` stations take **coupled Newton steps** +instead: after each powerflow, the sensitivities ``dVm/dQ`` of all measured buses to all +controlled injections are extracted from the final Newton-Raphson Jacobian (which pandapower +keeps in ``net._ppc["internal"]["J"]`` -- no extra powerflow is needed), the cross-station +sensitivity matrix is built and one linear solve updates all stations simultaneously: + +:: + + BinarySearchControl.for_stations(net, stations, tol=1e-6, + update_method="jacobian") + +Effects: + +- fewer powerflows for voltage control (3 instead of 5 in the benchmark below, i.e. one + Newton step typically suffices), which matters most when each powerflow is expensive; +- convergence of electrically coupled V_ctrl stations that independent iterations cannot + solve (see ``test_jacobian_coupled_same_feeder``). + +Scope and safety: + +- The Newton step is applied to plain ``V_ctrl`` stations. Q/PF/tan(phi) modi gain nothing + from it (their residual already follows the station output nearly 1:1) and droop modi + currently keep the secant update as well. +- The controller falls back to the safeguarded secant automatically whenever no Jacobian is + available (non-NR solver, stripped internals), a controlled or measured bus is not a PQ bus + in the powerflow, the linear system is unusable, or a Newton step increased the residual. + A run can therefore never get *worse* by enabling the flag. +- The underlying sensitivity function is available as a standalone utility:: + + from pandapower.control.util.sensitivity import calc_dvm_dq + sensitivity = calc_dvm_dq(net, q_bus_idx=[4, 5], vm_bus_idx=[2, 3]) # pu per Mvar + + Its sign, scaling and index mapping are pinned against finite differences in + ``test_dvm_dq_matches_finite_difference``. + + +Convergence improvements +======================== + +The update rule was rebuilt around a safeguarded secant method (all of this applies to the +legacy single-station API as well): + +- **Residual-scaled first step** for Q/PF/tan(phi) modi: the first perturbation is sized by + the residual (nearly a Newton step) instead of a fixed 1 kvar probe. +- **Bracketing**: once two iterates with opposite residual signs are known, an + Illinois-damped regula falsi keeps every further iterate inside the bracket -- no more + overshooting oscillations. Brackets that were collected while *other* controllers still + moved the operating point are detected via stagnation and discarded. +- **No more blow-ups on flat responses**: if the measurement does not react to the output, + the legacy code divided by a 1e-6 dummy slope and multiplied the outputs into the millions + of Mvar. The new update takes bounded fallback steps and logs a stagnation warning. +- **Bug fixes**: the power factor set point is no longer permanently overwritten by the + near-zero clipping; internal output state is re-synchronized with the net before every + step (robust against other controllers writing the same elements); stations with partially + out-of-service output elements no longer crash. + +The characterization suite (``pandapower/test/control/test_stactrl_characterization.py``) +pins the converged results and powerflow counts of 15 scenarios against the pre-refactor +implementation: all scenarios converge to the same results with the same or fewer powerflows +(e.g. ``tan_phi_ctrl`` 4 → 3, ``Q_ctrl_V_droop`` with deadband 5 → 4). + +A known limitation is documented as an expected failure: several *independently iterating* +V_ctrl stations behind the same transformer do not converge with the secant method (the +pre-refactor implementation fails there too). Use one ``for_stations`` instance with +``update_method="jacobian"`` for such configurations. + + +Timing results +============== + +Measured with the committed benchmark harness on the synthetic benchmark grid (one HV slack, +n MV feeders, one station with two sgens per feeder; recorded in +``pandapower/test/control/benchmarks/baseline.json``). "Overhead" is the controller +bookkeeping time of a full ``run_control``, i.e. wall time minus the time spent inside the +powerflows -- the metric that scales with the number of stations. + +Controller overhead, n = 500 stations: + +.. list-table:: + :widths: 30 20 20 20 12 + :header-rows: 1 + + * - mode + - before (s) + - per-station instances, after (s) + - one ``for_stations`` instance (s) + - speed-up + * - ``Q_ctrl`` + - 0.69 + - 0.43 + - 0.06 + - 11x + * - ``V_ctrl`` + - 1.03 + - 0.67 + - 0.08 + - 13x + * - ``V_ctrl_Q_droop`` + - 1.75 + - 0.89 + - 0.08 + - 22x + +Controller overhead, n = 200 stations: + +.. list-table:: + :widths: 30 20 20 20 12 + :header-rows: 1 + + * - mode + - before (s) + - per-station instances, after (s) + - one ``for_stations`` instance (s) + - speed-up + * - ``Q_ctrl`` + - 0.21 + - 0.16 + - 0.02 + - 9x + * - ``V_ctrl`` + - 0.52 + - 0.27 + - 0.04 + - 13x + * - ``V_ctrl_Q_droop`` + - 0.58 + - 0.31 + - 0.04 + - 15x + +The gain has three sources: an O(n²) scan of all controllers in every iteration was removed, +per-iteration pandas element access was replaced by positional numpy lookups precomputed in +``initialize_control``, and the multi-station mode reduces n controller objects (plus n +chained droop controllers) to a handful of table reads and one table write per iteration. + +Number of powerflows (n = 200, multi-station): + +.. list-table:: + :widths: 34 22 22 22 + :header-rows: 1 + + * - mode + - before + - secant (after) + - ``jacobian`` + * - ``Q_ctrl`` + - 3 + - 2 + - 2 (secant path) + * - ``V_ctrl`` + - 5 + - 5 + - 3 + * - ``V_ctrl_Q_droop`` + - 5 + - 4 + - 4 (secant path) + +On the small benchmark feeders a powerflow is cheap, so the Jacobian mode mainly saves +iterations; on large real grids, where each ``runpp`` dominates the run time, the reduction +from 5 to 3 powerflows translates directly into wall time. + +Reproducing the numbers:: + + python -m pandapower.test.control.benchmarks.bench_station_control \ + --n 50 200 500 --mode Q_ctrl V_ctrl V_ctrl_Q_droop --label my_run + python -m pandapower.test.control.benchmarks.bench_station_control \ + --n 200 --mode V_ctrl --multistation --update-method jacobian --label my_run_jac + + +Backward compatibility +====================== + +- The ``BinarySearchControl`` constructor signature, the ``DroopControl`` / + ``VDroopControl_local`` chaining mechanism and all ``ControlModusEnum`` values are + unchanged. Legacy nets keep their behaviour; the single-station code path reproduces the + pre-refactor results bit-exactly (verified by the characterization suite). +- Nets saved with earlier pandapower versions load and solve unchanged; this is guarded by a + frozen pre-refactor fixture + (``pandapower/test/control/testfiles/stactrl_prerefactor_v1.json``). +- Mixed operation is supported: legacy controllers and ``for_stations`` instances can run in + the same net (``test_multistation_prerefactor_interop``). + +API reference +============= + +See the class documentation of +:class:`~pandapower.control.controller.station_control.BinarySearchControl` (including +:meth:`~pandapower.control.controller.station_control.BinarySearchControl.for_stations`), +:class:`~pandapower.control.controller.station_control.DroopControl` and +:class:`~pandapower.control.controller.station_control.VDroopControl_local` in +:doc:`controller`. diff --git a/pandapower/control/controller/station_control.py b/pandapower/control/controller/station_control.py index 42a5350ba5..0c5a8a628b 100644 --- a/pandapower/control/controller/station_control.py +++ b/pandapower/control/controller/station_control.py @@ -266,7 +266,19 @@ def __getattr__(self, name): raise AttributeError(f"{self.__class__.__name__!r} has no attribute {name!r}") + # derived per-run state (_vstate, cached droop links) must never end up in saved nets + json_excludes = Controller.json_excludes + ["_vstate", "_linked_droop_objs"] + + # maps the result table an input measurement is taken from to the element table that + # carries the in_service information + _RES_TO_ELEMENT = {"res_line": "line", "res_trafo": "trafo", "res_trafo3w": "trafo3w", + "res_switch": "switch", "res_impedance": "impedance", "res_bus": "bus", + "res_gen": "gen"} + def initialize_control(self, net): + if getattr(self, 'stations', None): + self._initialize_stations(net) + return output_element_index = np.atleast_1d(self.output_element_index)[0] if self.write_flag == 'single_index' else \ self.output_element_index #ruggedize for single index self.output_values = read_from_net(net, self.output_element, output_element_index, self.output_variable, @@ -276,6 +288,154 @@ def initialize_control(self, net): for distribution, service in zip(np.atleast_1d(self.output_values_distribution), np.atleast_1d(self.output_element_in_service))], dtype=bool) + self._build_vstate(net) + + def _build_vstate(self, net): + """Precompute positional indices and cached lookups for the per-iteration hot path. + + Rebuilt at the beginning of every run_control (initialize_control) and lazily for + controllers restored from JSON (from_dict does not call __init__). Never serialized + (see json_excludes). Falls back to the legacy label-based access paths whenever the + preconditions for positional access are not met. + """ + vs = {} + # inputs: positional indices into the element table (for in_service masks) + element_table = self._RES_TO_ELEMENT.get(self.input_element) + input_idx = ([] if self.input_element_index is None + else list(np.atleast_1d(self.input_element_index))) + read_flags = list(np.atleast_1d(getattr(self, 'read_flag', []))) + fast_input = (element_table is not None and element_table in net + and len(read_flags) == len(input_idx) + and all(flag == 'single_index' for flag in read_flags)) + if fast_input: + pos = net[element_table].index.get_indexer(input_idx) + fast_input = not np.any(pos == -1) + vs['input_pos'] = pos + vs['fast_input'] = fast_input + vs['input_idx'] = input_idx + vs['input_element_table'] = element_table + vs['input_in_service_col'] = 'closed' if element_table == 'switch' else 'in_service' + # positional indices into the result table (for value reads) are resolved lazily on + # the first read because result tables are only guaranteed to exist after a powerflow + vs['res_pos'] = None + # outputs: positional indices into the output element table + output_idx = list(np.atleast_1d(self.output_element_index)) + fast_output = (self.output_element in ('gen', 'sgen', 'shunt') + and self.output_element in net) + if fast_output: + pos = net[self.output_element].index.get_indexer(output_idx) + fast_output = not np.any(pos == -1) + vs['output_pos'] = pos + vs['fast_output'] = fast_output + self._vstate = vs + # cache controllers linked to this one (droop controllers reference their binary + # search controller via controller_idx); avoids an O(n_controllers) scan of + # net.controller in every is_converged call. controller_idx is looked up via + # __dict__ because getattr would fall through to the (slow) __getattr__ shim on + # every controller that has no controller_idx + self._linked_droop_objs = [ + obj for obj in net.controller['object'].values + if getattr(obj, '__dict__', {}).get('controller_idx') == self.index + and obj is not self] + return vs + + def _refresh_in_service(self, net, vs): + """Update input_element_in_service / output_element_in_service from the net.""" + if vs['fast_input']: + column = vs['input_in_service_col'] + self.input_element_in_service = list( + net[vs['input_element_table']][column].values[vs['input_pos']]) + else: + self.input_element_in_service = [] + for input_index in np.atleast_1d(self.input_element_index): + if self.input_element == "res_line": + self.input_element_in_service.append(net.line.in_service[input_index]) + elif self.input_element == "res_trafo": + self.input_element_in_service.append(net.trafo.in_service[input_index]) + elif self.input_element == "res_trafo3w": + self.input_element_in_service.append(net.trafo3w.in_service[input_index]) + elif self.input_element == "res_switch": + self.input_element_in_service.append(net.switch.closed[input_index]) + elif self.input_element == "res_impedance": + self.input_element_in_service.append(net.impedance.in_service[input_index]) + elif self.input_element == "res_bus": + self.input_element_in_service.append(net.bus.in_service[input_index]) + elif self.input_element == "res_gen": + self.input_element_in_service.append(net.gen.in_service[input_index]) + if vs['fast_output']: + self.output_element_in_service = list( + net[self.output_element]['in_service'].values[vs['output_pos']]) + else: + self.output_element_in_service = [] + for output_index in np.atleast_1d(self.output_element_index): + if self.output_element == "gen": + self.output_element_in_service.append(net.gen.in_service[output_index]) + elif self.output_element == "sgen": + self.output_element_in_service.append(net.sgen.in_service[output_index]) + elif self.output_element == "shunt": + self.output_element_in_service.append(net.shunt.in_service[output_index]) + + def _read_input_values(self, net, vs, need_p): + """Read the measurement values of all in-service input elements. + + Returns plain lists in the same order as the legacy per-element read loop, so all + downstream arithmetic (sign multiplication, summation) is unchanged. + """ + input_values, p_input_values = [], [] + fast_read = vs['fast_input'] + if fast_read: + res_pos = vs['res_pos'] + if res_pos is None: + res_pos = net[self.input_element].index.get_indexer(vs['input_idx']) + if np.any(res_pos == -1): + fast_read = False + vs['fast_input'] = False + else: + vs['res_pos'] = res_pos + if fast_read: + res_table = net[self.input_element] + columns = {} + for counter, pos in enumerate(vs['res_pos']): + if not self.input_element_in_service[counter]: + continue + column = self.input_variable[counter] + values = columns.get(column) + if values is None: + values = columns[column] = res_table[column].values + input_values.append(values[pos]) + if need_p: + p_column = self.input_variable_p[counter] + p_values = columns.get(p_column) + if p_values is None: + p_values = columns[p_column] = res_table[p_column].values + p_input_values.append(p_values[pos]) + else: + counter = 0 + for input_index in self.input_element_index: + if self.input_element_in_service[counter]: + input_values.append(read_from_net(net, self.input_element, input_index, + self.input_variable[counter], self.read_flag[counter])) + if need_p: + p_input_values.append(read_from_net(net, self.input_element, input_index, + self.input_variable_p[counter], self.read_flag[counter])) + counter += 1 + return input_values, p_input_values + + def _limits_reached_else_refresh(self, log_prefix): + """Handle the shared "are any outputs still adjustable" block of all control modi. + + Returns True (and sets converged) if every output element has reached its reactive + power limit; otherwise drops out-of-service outputs from output_adjustable and + renormalizes the distribution. + """ + if not any(self.output_adjustable): + logging.info(log_prefix + 'All stations controlled by %s reached reactive power limits.' % self.name) + self.converged = True + return True + self.output_adjustable = np.array([in_service and adjustable for in_service, adjustable in zip( + self.output_element_in_service, self.output_adjustable)], dtype=bool) + self._normalize_distribution_in_service() + return False def is_converged(self, net): """ @@ -286,31 +446,15 @@ def is_converged(self, net): if not self.in_service: self.converged = True return self.converged + if getattr(self, 'stations', None): + return self._is_converged_stations(net) + # derived state is built by initialize_control; build lazily for controllers + # restored from JSON or used outside run_control + vs = getattr(self, '_vstate', None) + if vs is None: + vs = self._build_vstate(net) ###updating input & output elements in service lists - self.input_element_in_service = [] - self.output_element_in_service = [] - for input_index in np.atleast_1d(self.input_element_index): - if self.input_element == "res_line": - self.input_element_in_service.append(net.line.in_service[input_index]) - elif self.input_element == "res_trafo": - self.input_element_in_service.append(net.trafo.in_service[input_index]) - elif self.input_element == "res_trafo3w": - self.input_element_in_service.append(net.trafo3w.in_service[input_index]) - elif self.input_element == "res_switch": - self.input_element_in_service.append(net.switch.closed[input_index]) - elif self.input_element == "res_impedance": - self.input_element_in_service.append(net.impedance.in_service[input_index]) - elif self.input_element == "res_bus": - self.input_element_in_service.append(net.bus.in_service[input_index]) - elif self.input_element == "res_gen": - self.input_element_in_service.append(net.gen.in_service[input_index]) - for output_index in np.atleast_1d(self.output_element_index): - if self.output_element == "gen": - self.output_element_in_service.append(net.gen.in_service[output_index]) - elif self.output_element == "sgen": - self.output_element_in_service.append(net.sgen.in_service[output_index]) - elif self.output_element == "shunt": - self.output_element_in_service.append(net.shunt.in_service[output_index]) + self._refresh_in_service(net, vs) # check if at least one input and one output element is in_service if not (any(self.input_element_in_service) and any(self.output_element_in_service)): @@ -329,27 +473,22 @@ def is_converged(self, net): f' at index {np.array(self.output_element_index)}' f' will provide 100% of the reactive power in Controller {self.index}.\n') else: + in_service_mask = np.asarray(self.output_element_in_service, dtype=bool) logger.warning( f'Reactive Power Distribution for one output element cannot be modified. The active ' - f'{self.output_element[np.array(self.output_element_in_service)]} at index ' - f'{self.output_element_index[np.array(self.output_element_in_service)]} will provide 100% of the' + f'{self.output_element} at index ' + f'{np.asarray(self.output_element_index)[in_service_mask]} will provide 100% of the' f' reactive power in Controller {self.index}.\n') # read input values input_values = [] #reactive power q p_input_values = [] #active power p for power factor controllers - counter = 0 if self.input_element != 'res_bus': - for input_index in self.input_element_index: - if self.input_element_in_service[counter]: # input element not in service - input_values.append(read_from_net(net, self.input_element, input_index, - self.input_variable[counter], self.read_flag[counter])) - if self.control_modus in ControlModusEnum.pf_modes() or self.control_modus == ControlModusEnum.tan_phi_ctrl: - p_input_values.append(read_from_net(net,self.input_element, input_index, - self.input_variable_p[counter], self.read_flag[counter])) - counter += 1 + need_p = (self.control_modus in ControlModusEnum.pf_modes() + or self.control_modus == ControlModusEnum.tan_phi_ctrl) + input_values, p_input_values = self._read_input_values(net, vs, need_p) input_values = (self.input_sign * np.asarray(input_values)).tolist() - if self.control_modus in ControlModusEnum.pf_modes() or self.control_modus == ControlModusEnum.tan_phi_ctrl: + if need_p: p_input_values = (self.input_sign * np.asarray(p_input_values)).tolist() # compare old and new set values if self.control_modus in ControlModusEnum.q_modes() or (self.control_modus in ControlModusEnum.v_modes() @@ -358,18 +497,8 @@ def is_converged(self, net): logger.warning('Missing attribute self.input_element_index, defaulting to Q_ctrl\n') self.control_modus = ControlModusEnum.q_ctrl self.diff_old = self.diff - if not any(self.output_adjustable): - logging.info('All stations controlled by %s reached reactive power limits.' %self.name) - self.converged = True + if self._limits_reached_else_refresh(''): return self.converged - else: - # adapt output adjustable depending on in_service - self.output_adjustable = np.array([in_service and adjustable for in_service, adjustable in zip( - self.output_element_in_service, self.output_adjustable - )], dtype=bool) - - # normalize the values distribution - self._normalize_distribution_in_service() self.diff = self.set_point - sum(input_values) self.converged = np.all(np.abs(self.diff) < self.tol) @@ -382,41 +511,23 @@ def is_converged(self, net): self.reactance = -1 self.diff_old = self.diff - if not any(self.output_adjustable): - logging.info('PF_ctrl: All stations controlled by %s reached reactive power limits.' %self.name) - self.converged = True + if self._limits_reached_else_refresh('PF_ctrl: '): return self.converged - else: - # adapt output adjustable depending on in_service - self.output_adjustable = np.array([in_service and adjustable for in_service, adjustable - in zip(self.output_element_in_service, self.output_adjustable)], dtype=bool) - # normalize the values distribution - self._normalize_distribution_in_service() - if -0.012 < self.set_point < 0.012: #clip set_point to handle pf=0 - min_q = -0.012 - max_q = -min_q - self.set_point = float(np.where((self.set_point >= 0) & (self.set_point <= max_q), max_q, self.set_point)) - self.set_point = float(np.where((self.set_point >= min_q) & (self.set_point < 0), float(min_q), self.set_point)) - logger.warning(f"Power factor calculation with set_point 0 not possible with BSC {self.index}.\n" - f"Maximizing Q output by clipping set_point to {self.set_point}\n") - q_set = self.reactance * sum(p_input_values)/len(p_input_values) * (np.tan(np.arccos(self.set_point))) + set_point = self.set_point + if -0.012 < set_point < 0.012: #clip set_point to handle pf=0, without mutating self.set_point + set_point = 0.012 if set_point >= 0 else -0.012 + if not vs.get('pf_clip_warned', False): + vs['pf_clip_warned'] = True + logger.warning(f"Power factor calculation with set_point 0 not possible with BSC {self.index}.\n" + f"Maximizing Q output by clipping set_point to {set_point}\n") + q_set = self.reactance * sum(p_input_values)/len(p_input_values) * (np.tan(np.arccos(set_point))) self.diff = q_set - sum(input_values)/len(input_values) self.converged = np.all(np.abs(self.diff) following step will correct this - self.output_values_old, self.output_values = ( - np.atleast_1d(self.output_values)[self.output_element_in_service], - np.atleast_1d(self.output_values)[self.output_element_in_service] + 1e-3) + # output_values keeps one entry per output element (also out-of-service ones); + # out-of-service entries are excluded when writing to the net + values = np.atleast_1d(self.output_values).astype(np.float64) + probe_total = self._initial_probe_total(values, damping) + if probe_total is None: + # V modi: the voltage response in MVAr/pu is grid specific, keep the small + # legacy probe (update_method="jacobian" will compute the true sensitivity) + self.output_values_old, self.output_values = (values, values + 1e-3) + else: + distribution = np.atleast_1d(self.output_values_distribution).astype(np.float64) + self.output_values_old, self.output_values = (values, values + probe_total * distribution) positions_not_adjustable = [i for i, val in enumerate(self.output_adjustable) if not val] for i in positions_not_adjustable: if self.output_values_distribution[i]==0 or not self.output_element_in_service[i] : @@ -539,17 +627,13 @@ def _binary_search_control_step(self, net): else: continue else: #second step - step_diff = self.diff - self.diff_old - x = self.output_values - self.diff * (self.output_values - self.output_values_old) / np.where( - step_diff == 0, 1e-6, step_diff) # converging - - rel_cap = 2 - cap = rel_cap * (np.abs(self.output_values) + 1e-6) + 50 # add epsilon to avoid zero; absolute cap +50 MVAr - - delta = x - self.output_values - delta = np.clip(delta, -cap, +cap) - - x = self.output_values + delta + # another controller or enforce_q_lims may have modified the written values in + # the net since the last step -- the powerflow saw the net values, so they are + # the true evaluation point of the secant + self._resync_output_values(net, vs) + x_total = self._safeguarded_secant_total(vs, damping) + distribution = np.atleast_1d(self.output_values_distribution).astype(np.float64) + x = x_total * distribution if not all(self.output_adjustable) and net._options.get('enforce_q_lims', False): positions_adjustable = [i for i, val in enumerate(self.output_adjustable) if val] # gives which is/are adjustable @@ -644,13 +728,623 @@ def _binary_search_control_step(self, net): self.output_values = x else: self.output_values_old, self.output_values = self.output_values, x - ### write new set of Q values to output elements### - output_element_index = (list(np.atleast_1d(self.output_element_index)[self.output_element_in_service])[0] if self.write_flag - == 'single_index' else list(np.array(self.output_element_index)[self.output_element_in_service])) #ruggedizing code - output_values = (list(self.output_values)[0] if self.write_flag - == 'single_index' else list(self.output_values)) # ruggedizing code + ### write new set of Q values to output elements (out-of-service outputs excluded)### + in_service_mask = np.asarray(self.output_element_in_service, dtype=bool) + values = np.atleast_1d(self.output_values) + if len(values) == len(in_service_mask): + values = values[in_service_mask] + if self.write_flag == 'single_index': + output_element_index = list(np.atleast_1d(self.output_element_index)[in_service_mask])[0] + output_values = list(values)[0] + else: + output_element_index = list(np.array(self.output_element_index)[in_service_mask]) + output_values = list(values) write_to_net(net, self.output_element, output_element_index, self.output_variable, output_values, self.write_flag) + def _resync_output_values(self, net, vs): + """Align the internal output state with the values currently in the net tables.""" + if not vs.get('fast_output', False) or not isinstance(self.output_variable, str): + return + current = net[self.output_element][self.output_variable].values[vs['output_pos']] + values = np.atleast_1d(self.output_values).astype(np.float64) + mask = np.asarray(self.output_element_in_service, dtype=bool) + if len(current) != len(values) or len(mask) != len(values): + return + values[mask] = current[mask] + self.output_values = values + + def _initial_probe_total(self, values, damping): + """Total first-step perturbation for Q-type control modi, or None for the legacy probe. + + For Q/PF/tan(phi) control the measured quantity follows the summed station output + nearly 1:1, so a residual-sized first step is already close to the Newton step; the + secant update afterwards corrects the remaining slope error. For V modi the voltage + response in MVAr/pu is grid specific, so None is returned and the caller keeps the + small legacy probe. + """ + if self.control_modus in ControlModusEnum.v_modes(): + return None + if self.diff is None or np.ndim(self.diff) != 0 or not np.isfinite(self.diff): + return None + cap = 2.0 * float(np.abs(values).sum()) + 50.0 + probe_total = float(np.clip(damping * float(self.diff), -cap, cap)) + if abs(probe_total) < 1e-3: + probe_total = 1e-3 # keep the perturbation measurable for the secant slope + return probe_total + + def _safeguarded_secant_total(self, vs, damping): + """Next total station output from a bracketing-safeguarded secant update. + + The update works on the summed station output. As soon as two iterates with opposite + residual sign are known, the solution is bracketed and an Illinois-damped regula + falsi keeps all further iterates inside the bracket. Without a bracket, a (damped) + secant step with a step-size cap is taken; a flat measurement response takes a + bounded unit-slope step instead of dividing by a near-zero slope. + """ + solver = vs.setdefault('solver', self._new_solver_state(self.set_point)) + values = np.atleast_1d(self.output_values).astype(np.float64) + values_old = np.atleast_1d(self.output_values_old).astype(np.float64) + total = float(values.sum()) + total_old = float(values_old.sum()) + try: + f = float(self.diff) + f_old = f if self.diff_old is None else float(self.diff_old) + except (TypeError, ValueError): + # non-scalar residual: legacy per-element secant as fallback + step_diff = self.diff - self.diff_old + x = values - self.diff * (values - values_old) / np.where(step_diff == 0, 1e-6, step_diff) + cap = 2 * (np.abs(values) + 1e-6) + 50 + return float(np.sum(values + np.clip(x - values, -cap, cap))) + cap = 2.0 * float(np.abs(values).sum()) + 50.0 + return self._secant_core(solver, f, f_old, total, total_old, damping, + self.set_point, cap, "%s (index %s)" % (self.name, self.index)) + + @staticmethod + def _new_solver_state(set_point): + return {'lo': None, 'hi': None, 'side': 0, 'best': None, 'stall': 0, + 'stall_warned': False, 'slope': None, 'set_point': set_point} + + @staticmethod + def _secant_core(solver, f, f_old, total, total_old, damping, set_point, cap, label): + """Bracketing-safeguarded secant update on a scalar residual, see + _safeguarded_secant_total. ``solver`` carries the state between calls.""" + # a changed set point (e.g. written by a chained droop controller) changes the + # residual function, previously collected bracket points are no longer valid + if solver['set_point'] != set_point: + solver['lo'] = solver['hi'] = None + solver['side'] = 0 + solver['set_point'] = set_point + + # remember the most recent meaningful secant slope (df/dQ_total); used when the last + # two iterates collapse onto each other and no local slope can be computed + if abs(total - total_old) > 1e-12 and abs(f - f_old) > 1e-12 * max(1.0, abs(f)): + solver['slope'] = (f - f_old) / (total - total_old) + + # stagnation diagnostics; a frozen residual with an active bracket means the bracket + # was collected while other controllers still moved the operating point (stale) -- + # discard it and continue with plain secant steps on fresh information + if solver['best'] is None or abs(f) < 0.9 * solver['best']: + solver['best'] = abs(f) if solver['best'] is None else min(abs(f), solver['best']) + solver['stall'] = 0 + else: + solver['stall'] += 1 + if solver['stall'] >= 3 and solver['lo'] is not None: + logger.debug("BinarySearchControl %s: discarding stale bracket" % label) + solver['lo'] = solver['hi'] = None + solver['side'] = 0 + solver['best'] = abs(f) + solver['stall'] = 0 + elif solver['stall'] >= 8 and not solver['stall_warned']: + solver['stall_warned'] = True + logger.warning( + "BinarySearchControl %s: residual %.3g is not decreasing " + "after %d control steps" % (label, abs(f), solver['stall'])) + + # maintain the bracket around the zero crossing + if solver['lo'] is None: + if f_old * f < 0: + first, second = (total_old, f_old), (total, f) + solver['lo'], solver['hi'] = ((first, second) if first[0] <= second[0] + else (second, first)) + solver['side'] = 0 + else: + lo_x, lo_f = solver['lo'] + hi_x, hi_f = solver['hi'] + if f == 0.0: + return total + if f * lo_f > 0: + solver['lo'] = (total, f) + if solver['side'] == -1: + solver['hi'] = (hi_x, hi_f * 0.5) # Illinois damping + solver['side'] = -1 + elif f * hi_f > 0: + solver['hi'] = (total, f) + if solver['side'] == 1: + solver['lo'] = (lo_x, lo_f * 0.5) # Illinois damping + solver['side'] = 1 + + if solver['lo'] is not None: + lo_x, lo_f = solver['lo'] + hi_x, hi_f = solver['hi'] + x_new = (lo_x * hi_f - hi_x * lo_f) / (hi_f - lo_f) + if not (min(lo_x, hi_x) < x_new < max(lo_x, hi_x)): + x_new = 0.5 * (lo_x + hi_x) # numerical safety: bisect + return x_new + + # no bracket yet: plain secant with a step-size cap. damping_factor is deliberately + # not applied to regular secant steps (it would slow every well-behaved controller); + # it only softens the fallback steps below and the first probe + step_diff = f - f_old + if abs(step_diff) <= 1e-12 * max(1.0, abs(f)) or abs(total - total_old) <= 1e-12: + if solver['stall'] >= 5: + # the measurement does not respond to the output at all: hold the position + # instead of pushing ever more reactive power into the grid (cumulative + # fallback steps would eventually make the powerflow itself collapse); + # run_control reports ControllerNotConverged via max_iter + return total + if solver['slope']: + # local slope unavailable (iterates collapsed): Newton with remembered slope + x_new = total - damping * f / solver['slope'] + else: + x_new = total + damping * f # flat response: bounded unit-slope step + else: + x_new = total - f * (total - total_old) / step_diff + return total + float(np.clip(x_new - total, -cap, cap)) + + # ------------------------------------------------------------------------------------ + # multi-station mode: one controller instance manages many stations, each with its own + # control modus, set point, measurement, outputs and (optionally) droop characteristic. + # Droop is part of the station residual (single fixed-point loop), not a chained + # controller. Created via BinarySearchControl.for_stations; single-station controllers + # created through __init__ keep the legacy code path above. + # ------------------------------------------------------------------------------------ + + @classmethod + def for_stations(cls, net, stations, output_element="sgen", output_variable="q_mvar", + tol=1e-3, in_service=True, order=0, level=0, name="", + update_method="secant", drop_same_existing_ctrl=False, + matching_params=None, **kwargs): + """Create one BinarySearchControl instance controlling multiple stations. + + Parameters + ---------- + net : pandapowerNet + stations : list of dict + One dict per station with the keys: + + - ``control_modus`` (str): ``"Q_ctrl"``, ``"V_ctrl"``, ``"PF_ctrl_ind"``, + ``"PF_ctrl_cap"``, ``"tan_phi_ctrl"``, ``"Q_ctrl_V_droop"``, + ``"V_ctrl_Q_droop"`` or ``"V_ctrl_Q_droop_local"`` + - ``set_point`` (float): reactive power / voltage / power factor / tan(phi) + set point (base set point for droop modi) + - ``input_element`` (str): result table of the measurement, e.g. ``"res_line"``, + ``"res_trafo"``; ``"res_bus"`` for plain V_ctrl + - ``input_variable`` (str or list of str): measured column(s), e.g. + ``"q_to_mvar"``; ``"vm_pu"`` for plain V_ctrl + - ``input_element_index`` (int or list of int) + - ``input_inverted`` (bool or list of bool, optional) + - ``output_element_index`` (list of int): controlled elements in the (shared) + output table + - ``output_values_distribution`` (list of float): Q distribution among outputs + - ``tol`` (float, optional): per-station tolerance override + - ``name`` (str, optional) + - ``droop`` (dict, required for droop modi): + ``q_droop_mvar`` (Mvar/pu), ``bus_idx`` (measured bus), + ``vm_set_lb``/``vm_set_ub`` (deadband, Q_ctrl_V_droop), + ``vm_set_pu`` (no-deadband voltage reference, Q_ctrl_V_droop), + ``q_set_mvar`` (local Q reference, V_ctrl_Q_droop_local) + output_element : str + Output table shared by all stations of this instance (``"sgen"``, ``"gen"`` or + ``"shunt"``). Stations with different output tables need separate instances. + output_variable : str + Written column, e.g. ``"q_mvar"`` or ``"step"``. + update_method : str + ``"secant"`` (default) or ``"jacobian"``. With ``"jacobian"``, plain ``V_ctrl`` + stations take coupled Newton steps based on the dVm/dQ sensitivities from the + powerflow Jacobian (captures the interaction of electrically close stations and + reduces the number of powerflows); all other modi and any failure case + automatically fall back to the safeguarded secant update. + """ + self = cls.__new__(cls) + Controller.__init__(self, net, in_service=in_service, order=order, level=level, + drop_same_existing_ctrl=drop_same_existing_ctrl, + matching_params=matching_params) + for key, value in kwargs.items(): + setattr(self, key, value) + self.name = name + self.output_element = output_element + self.output_variable = output_variable + self.write_flag = 'loc' + self.tol = tol + self.in_service = in_service + self.update_method = update_method + self.converged = False + # harmless flat attributes for __str__ / external inspection + self.input_element = 'stations' + self.input_variable = [] + self.output_element_index = [] + self.set_point = None + self.diff = None + self.diff_old = None + self.stations = [cls._normalize_station(s, k, tol) for k, s in enumerate(stations)] + return self + + @staticmethod + def _normalize_station(station, position, default_tol): + """Validate a station dict and normalize it to canonical (JSON-safe) form.""" + s = dict(station) + try: + modus = ControlModusEnum(s.get('control_modus')) + except ValueError: + raise UserWarning(f"station {position}: unknown control_modus " + f"{s.get('control_modus')!r}") + s['control_modus'] = modus.value + if 'set_point' not in s: + raise UserWarning(f"station {position}: set_point is required") + s['set_point'] = float(s['set_point']) + if modus in ControlModusEnum.pf_modes() and abs(s['set_point']) > 1: + raise UserWarning(f"station {position}: power factor set point out of range [-1, 1]") + s['tol'] = float(s.get('tol', default_tol)) + s['input_element_index'] = [int(i) for i in np.atleast_1d(s['input_element_index'])] + n_inputs = len(s['input_element_index']) + variables = s['input_variable'] + s['input_variable'] = ([variables] * n_inputs if isinstance(variables, str) + else list(variables)) + if len(s['input_variable']) != n_inputs: + raise UserWarning(f"station {position}: input_variable and input_element_index " + f"lengths differ") + if (modus in ControlModusEnum.pf_modes() or modus == ControlModusEnum.tan_phi_ctrl) \ + and s['input_element'] == 'res_bus': + raise UserWarning(f"station {position}: {modus.value} needs a branch measurement, " + f"not res_bus") + inverted = np.atleast_1d(s.get('input_inverted', False)) + if len(inverted) == 1: + inverted = np.repeat(inverted, n_inputs) + if len(inverted) != n_inputs: + raise UserWarning(f"station {position}: input_inverted and input_element_index " + f"lengths differ") + s['input_sign'] = [-1.0 if inv else 1.0 for inv in inverted] + s.pop('input_inverted', None) + s['output_element_index'] = [int(i) for i in np.atleast_1d(s['output_element_index'])] + distribution = np.asarray( + np.atleast_1d(s.get('output_values_distribution', + [1.0] * len(s['output_element_index']))), dtype=np.float64) + if len(distribution) != len(s['output_element_index']): + raise UserWarning(f"station {position}: output_values_distribution and " + f"output_element_index lengths differ") + s['output_values_distribution'] = [float(v) for v in distribution / distribution.sum()] + droop = s.get('droop') + if modus in ControlModusEnum.droop_modes(): + if not droop or 'q_droop_mvar' not in droop or 'bus_idx' not in droop: + raise UserWarning(f"station {position}: droop modus {modus.value} requires a " + f"droop dict with q_droop_mvar and bus_idx") + if (modus == ControlModusEnum.q_ctrl_v_droop + and ('vm_set_lb' in droop) != ('vm_set_ub' in droop)): + raise UserWarning(f"station {position}: Q_ctrl_V_droop needs both or none of " + f"vm_set_lb/vm_set_ub") + if (modus == ControlModusEnum.q_ctrl_v_droop and 'vm_set_lb' not in droop + and 'vm_set_pu' not in droop): + raise UserWarning(f"station {position}: Q_ctrl_V_droop without deadband needs " + f"a vm_set_pu voltage reference") + elif modus in ControlModusEnum.v_modes(): + if s['input_element'] != 'res_bus' and (not droop or 'bus_idx' not in droop): + raise UserWarning(f"station {position}: V_ctrl needs input_element 'res_bus' " + f"or a droop dict with bus_idx") + return s + + def _initialize_stations(self, net): + """Build the runtime state (positional indices, solver state) for all stations.""" + vs = {'stations': [], 'res_resolved': False} + output_table = net[self.output_element] + output_values = output_table[self.output_variable].values + for k, s in enumerate(self.stations): + rt = {'cfg': s, 'label': s.get('name') or f"{self.name}[{k}]"} + rt['modus'] = ControlModusEnum(s['control_modus']) + rt['set_point'] = s['set_point'] + rt['tol'] = s['tol'] + rt['droop'] = s.get('droop') + rt['out_idx'] = np.asarray(s['output_element_index']) + rt['out_pos'] = output_table.index.get_indexer(rt['out_idx']) + if np.any(rt['out_pos'] == -1): + raise UserWarning(f"station {rt['label']}: output element(s) " + f"{s['output_element_index']} not found in " + f"{self.output_element}") + rt['dist_base'] = np.asarray(s['output_values_distribution'], dtype=np.float64) + rt['at_limit'] = np.zeros(len(rt['out_idx']), dtype=bool) + rt['values'] = output_values[rt['out_pos']].astype(np.float64) + rt['values_old'] = None + rt['solver'] = self._new_solver_state(rt['set_point']) + rt['f'] = None + rt['f_old'] = None + rt['converged'] = False + rt['disabled'] = False + rt['warned'] = False + element_table = self._RES_TO_ELEMENT.get(s['input_element']) + if element_table is None or element_table not in net: + raise UserWarning(f"station {rt['label']}: unsupported input_element " + f"{s['input_element']!r}") + rt['in_res'] = s['input_element'] + rt['in_element_table'] = element_table + rt['in_service_col'] = 'closed' if element_table == 'switch' else 'in_service' + rt['in_idx'] = list(s['input_element_index']) + rt['in_pos'] = net[element_table].index.get_indexer(rt['in_idx']) + if np.any(rt['in_pos'] == -1): + raise UserWarning(f"station {rt['label']}: input element(s) {rt['in_idx']} " + f"not found in {element_table}") + rt['in_cols'] = list(s['input_variable']) + rt['in_sign'] = np.asarray(s['input_sign'], dtype=np.float64) + rt['res_pos'] = None # resolved lazily against the result table + if rt['modus'] in ControlModusEnum.pf_modes() or rt['modus'] == ControlModusEnum.tan_phi_ctrl: + rt['p_cols'] = [c.replace('q', 'p').replace('var', 'w') for c in rt['in_cols']] + rt['reactance'] = -1.0 if rt['modus'] == ControlModusEnum.PF_ctrl_cap else 1.0 + # controlled bus (V modi and droop modi) + bus = None + if rt['droop'] and 'bus_idx' in rt['droop']: + bus = rt['droop']['bus_idx'] + elif rt['modus'] in ControlModusEnum.v_modes(): + bus = rt['in_idx'][0] + rt['bus'] = bus + rt['bus_pos'] = None if bus is None else net.bus.index.get_loc(bus) + vs['stations'].append(rt) + self._vstate = vs + self._linked_droop_objs = [] + + @staticmethod + def _station_effective_residual(rt, q_meas, vm): + """Residual of one station; droop characteristics are folded into the residual.""" + modus = rt['modus'] + set_point = rt['set_point'] + droop = rt['droop'] + if modus == ControlModusEnum.q_ctrl: + return set_point - q_meas + if modus == ControlModusEnum.q_ctrl_v_droop: + k = droop['q_droop_mvar'] + if droop.get('vm_set_lb') is not None and droop.get('vm_set_ub') is not None: + if vm > droop['vm_set_ub']: + q_set = set_point - (droop['vm_set_ub'] - vm) * k + elif vm < droop['vm_set_lb']: + q_set = set_point + (droop['vm_set_lb'] - vm) * k + else: + q_set = set_point + else: + q_set = set_point + (droop['vm_set_pu'] - vm) * k + return q_set - q_meas + if modus == ControlModusEnum.v_ctrl: + return set_point - vm + if modus == ControlModusEnum.v_ctrl_q_droop: + return set_point + q_meas / droop['q_droop_mvar'] - vm + if modus == ControlModusEnum.v_ctrl_q_droop_local: + q_set = droop.get('q_set_mvar', 0.0) or 0.0 + return set_point - (q_meas - q_set) / droop['q_droop_mvar'] - vm + raise UserWarning(f"unsupported control modus {modus} in multi-station mode") + + def _is_converged_stations(self, net): + vs = getattr(self, '_vstate', None) + if vs is None or 'stations' not in vs: + self._initialize_stations(net) + vs = self._vstate + cache = {} + + def col(table, column): + key = (table, column) + if key not in cache: + cache[key] = net[table][column].values + return cache[key] + + all_converged = True + for rt in vs['stations']: + if rt['disabled']: + continue + out_in_service = col(self.output_element, 'in_service')[rt['out_pos']].astype(bool) + in_mask = col(rt['in_element_table'], + rt['in_service_col'])[rt['in_pos']].astype(bool) + if not out_in_service.any() or not in_mask.any(): + if not rt['warned']: + logger.warning("station %s: all input or output elements out of service, " + "skipping station" % rt['label']) + rt['warned'] = True + rt['disabled'] = True + continue + rt['out_in_service'] = out_in_service + rt['in_mask'] = in_mask + # distribution over outputs that are in service and not at a Q limit + dist = rt['dist_base'] * out_in_service * ~rt['at_limit'] + total_dist = dist.sum() + adjustable = dist != 0 + if total_dist > 0: + dist = dist / total_dist + rt['dist'] = dist + rt['adjustable'] = adjustable + if not adjustable.any(): + if not rt['warned']: + logging.info('All outputs of station %s reached their reactive power ' + 'limits.' % rt['label']) + rt['warned'] = True + rt['converged'] = True + continue + + if rt['in_res'] == 'res_bus': + q_meas = 0.0 + else: + res_pos = rt['res_pos'] + if res_pos is None: + res_pos = net[rt['in_res']].index.get_indexer(rt['in_idx']) + rt['res_pos'] = res_pos + q_meas = 0.0 + p_meas = 0.0 + n_active = 0 + for i, pos in enumerate(res_pos): + if not in_mask[i]: + continue + q_meas += rt['in_sign'][i] * col(rt['in_res'], rt['in_cols'][i])[pos] + if 'p_cols' in rt: + p_meas += rt['in_sign'][i] * col(rt['in_res'], rt['p_cols'][i])[pos] + n_active += 1 + vm = None if rt['bus_pos'] is None else col('res_bus', 'vm_pu')[rt['bus_pos']] + modus = rt['modus'] + if modus in ControlModusEnum.pf_modes(): + set_point = rt['set_point'] + if -0.012 < set_point < 0.012: + set_point = 0.012 if set_point >= 0 else -0.012 + q_set = rt['reactance'] * p_meas / n_active * np.tan(np.arccos(set_point)) + f = q_set - q_meas / n_active + elif modus == ControlModusEnum.tan_phi_ctrl: + f = p_meas / n_active * rt['set_point'] - q_meas / n_active + else: + f = self._station_effective_residual(rt, q_meas, vm) + rt['f_old'], rt['f'] = rt['f'], float(f) + rt['converged'] = abs(rt['f']) < rt['tol'] + if not rt['converged']: + all_converged = False + self.converged = all_converged + return self.converged + + def _jacobian_deltas(self, net, vs, damping): + """Coupled Newton steps {station position: dQ_total} for plain V_ctrl stations. + + Builds the cross-station sensitivity matrix M[s, t] = dVm(bus_s)/dQ_total(t) from the + Newton-Raphson Jacobian of the last powerflow and solves M * dQ = r for all eligible + stations simultaneously -- this captures the interaction between electrically close + stations that makes independent per-station updates oscillate. Any failure returns {} + and the caller falls back to the safeguarded secant for this iteration. + """ + from pandapower.control.util.sensitivity import calc_dvm_dq + stations = vs['stations'] + candidates = [k for k, rt in enumerate(stations) + if not rt['disabled'] and not rt['converged'] and rt['f'] is not None + and rt['modus'] == ControlModusEnum.v_ctrl and rt['bus'] is not None + and rt['adjustable'].any()] + if not candidates: + return {} + output_buses = net[self.output_element]['bus'].values + vm_buses = [stations[k]['bus'] for k in candidates] + q_buses, q_slices = [], [] + for k in candidates: + buses = output_buses[stations[k]['out_pos']] + q_slices.append((len(q_buses), len(q_buses) + len(buses))) + q_buses.extend(buses) + sensitivity = calc_dvm_dq(net, q_buses, vm_buses) + if sensitivity is None: + logger.debug("%s: no Jacobian available, secant fallback" % self.name) + return {} + # station-total sensitivities: outputs weighted with the current distribution. + # deliberately no dense BLAS calls (@ / dot) anywhere in this method: powerflow + # backends shipping their own BLAS (e.g. lightsim2grid with MKL numpy on Windows) + # crash inside dense LAPACK/BLAS kernels + matrix = np.empty((len(candidates), len(candidates))) + for j, k in enumerate(candidates): + start, stop = q_slices[j] + matrix[:, j] = np.sum(sensitivity[:, start:stop] * stations[k]['dist'], axis=1) + # drop stations touching non-PQ buses (NaN sensitivities) + valid = ~(np.isnan(matrix).any(axis=1) | np.isnan(matrix).any(axis=0)) + if not valid.all(): + logger.debug("%s: stations at non-PQ buses use the secant fallback" % self.name) + candidates = [k for k, ok in zip(candidates, valid) if ok] + if not candidates: + return {} + matrix = matrix[np.ix_(valid, valid)] + residual = np.array([stations[k]['f'] for k in candidates]) + # step rejection: if the previous jacobian step increased the residual norm, take a + # safeguarded secant step on fresh information instead + jac_state = vs.setdefault('jac', {'prev_rnorm': None}) + rnorm = float(np.max(np.abs(residual))) + if jac_state['prev_rnorm'] is not None and rnorm > jac_state['prev_rnorm']: + jac_state['prev_rnorm'] = None + logger.debug("%s: jacobian step increased the residual, secant fallback" % self.name) + return {} + diagonal = np.diag(matrix) + if np.any(diagonal == 0): + return {} + # the solve goes through SuperLU (like the sensitivity computation) instead of dense + # LAPACK: environments where a powerflow backend ships its own BLAS (e.g. + # lightsim2grid + MKL numpy on Windows) crash inside dense LAPACK calls + from scipy.sparse import csc_matrix + from scipy.sparse.linalg import spsolve + sparse_matrix = csc_matrix(matrix) + try: + dq = np.atleast_1d(spsolve(sparse_matrix, residual)) + # validate instead of a cond() estimate: fall back to the decoupled diagonal + # update when the solution is unusable + if (not np.all(np.isfinite(dq)) + or np.max(np.abs(sparse_matrix.dot(dq) - residual)) > 1e-8 * max(1.0, rnorm)): + dq = residual / diagonal + except RuntimeError: + dq = residual / diagonal + if not np.all(np.isfinite(dq)): + return {} + jac_state['prev_rnorm'] = rnorm + return {k: damping * delta for k, delta in zip(candidates, dq)} + + def _control_step_stations(self, net): + vs = self._vstate + damping = float(getattr(self, 'damping_factor', 1.0) or 1.0) + enforce_q_lims = net._options.get('enforce_q_lims', False) + output_table = net[self.output_element] + current_values = output_table[self.output_variable].values + min_q = max_q = None + if enforce_q_lims and 'min_q_mvar' in output_table.columns: + min_q = np.nan_to_num(output_table['min_q_mvar'].values.astype(np.float64), + nan=-np.inf) + if enforce_q_lims and 'max_q_mvar' in output_table.columns: + max_q = np.nan_to_num(output_table['max_q_mvar'].values.astype(np.float64), + nan=np.inf) + jacobian_deltas = {} + if getattr(self, 'update_method', 'secant') == 'jacobian': + jacobian_deltas = self._jacobian_deltas(net, vs, damping) + write_index, write_values = [], [] + for position, rt in enumerate(vs['stations']): + if rt['disabled'] or rt['converged'] or rt['f'] is None: + continue + out_in_service = rt['out_in_service'] + # the powerflow saw the values currently in the net -> true evaluation point + values = current_values[rt['out_pos']].astype(np.float64) * out_in_service + f = rt['f'] + cap = 2.0 * float(np.abs(values).sum()) + 50.0 + frozen = rt['at_limit'] & out_in_service + if position in jacobian_deltas: + total = float(values.sum()) + x_total = total + float(np.clip(jacobian_deltas[position], -cap, cap)) + x = (x_total - float(values[frozen].sum())) * rt['dist'] + x[frozen] = values[frozen] + elif rt['values_old'] is None: # first step: probe + if rt['modus'] in ControlModusEnum.v_modes(): + x = values + 1e-3 * (rt['dist'] > 0) + else: + probe_total = float(np.clip(damping * f, -cap, cap)) + if abs(probe_total) < 1e-3: + probe_total = 1e-3 + x = values + probe_total * rt['dist'] + else: + total = float(values.sum()) + total_old = float((rt['values_old'] * out_in_service).sum()) + f_old = f if rt['f_old'] is None else rt['f_old'] + x_total = self._secant_core(rt['solver'], f, f_old, total, total_old, + damping, rt['set_point'], cap, rt['label']) + # outputs at a limit keep their clamped value, the rest shares the remainder + x = (x_total - float(values[frozen].sum())) * rt['dist'] + x[frozen] = values[frozen] + if enforce_q_lims and (min_q is not None or max_q is not None): + station_min = (min_q[rt['out_pos']] if min_q is not None + else np.full(len(x), -np.inf)) + station_max = (max_q[rt['out_pos']] if max_q is not None + else np.full(len(x), np.inf)) + over = (x > station_max) & rt['adjustable'] + under = (x < station_min) & rt['adjustable'] + if over.any() or under.any(): + reached = over | under + x = np.where(over, station_max, x) + x = np.where(under, station_min, x) + rt['at_limit'] = rt['at_limit'] | reached + logging.info('Station %s: output element(s) %s reached a reactive power ' + 'limit.' % (rt['label'], + list(rt['out_idx'][reached]))) + rt['values_old'], rt['values'] = values, x + write_index.extend(rt['out_idx'][out_in_service]) + write_values.extend(x[out_in_service]) + if write_index: + write_to_net(net, self.output_element, write_index, self.output_variable, + write_values, 'loc') + def _normalize_distribution_in_service(self, initial_pf_distribution=None): # normalize distribution depending on in service of stations if initial_pf_distribution is None: @@ -833,27 +1527,27 @@ def check_control_modus_and_values(self, net): self.control_modus = net.controller.at[self.controller_idx, 'object'].control_modus def is_converged(self, net): - if (not net.controller.at[self.controller_idx, "object"].in_service or - net.controller.at[self.controller_idx, "object"].converged): + bsc = net.controller.at[self.controller_idx, "object"] + if not bsc.in_service or bsc.converged: self.converged = True return self.converged ###check control_modus### self.check_control_modus_and_values(net) if self.control_modus in ControlModusEnum.v_modes(): - self.diff = (net.controller.at[self.controller_idx, "object"].set_point - + self.diff = (bsc.set_point - read_from_net(net, "res_bus", int(self.bus_idx), "vm_pu", self.read_flag)) else: counter = 0 input_values = [] - for input_index in net.controller.at[self.controller_idx, "object"].input_element_index: + for input_index in bsc.input_element_index: input_values.append( - read_from_net(net, net.controller.at[self.controller_idx, "object"].input_element, input_index, - net.controller.at[self.controller_idx, "object"].input_variable[counter], - net.controller.at[self.controller_idx, "object"].read_flag[counter])) + read_from_net(net, bsc.input_element, input_index, + bsc.input_variable[counter], + bsc.read_flag[counter])) counter += 1 - input_sign = np.asarray(net.controller.at[self.controller_idx, "object"].input_sign) + input_sign = np.asarray(bsc.input_sign) input_values = (input_sign * np.asarray(input_values)).tolist() - self.diff = (net.controller.at[self.controller_idx, "object"].set_point - sum(input_values)) + self.diff = (bsc.set_point - sum(input_values)) self.converged = np.all(np.abs(self.diff) < self.tol) return self.converged @@ -861,11 +1555,12 @@ def control_step(self, net): self._droop_control_step(net) def _droop_control_step(self, net): + bsc = net.controller.at[self.controller_idx, "object"] self.vm_pu_old = self.vm_pu self.vm_pu = read_from_net(net, "res_bus", self.bus_idx, "vm_pu", flag=self.read_flag) if self.control_modus not in ControlModusEnum.v_modes(): if self.q_set_mvar_bsc is None: - self.q_set_mvar_bsc = net.controller.at[self.controller_idx, "object"].set_point + self.q_set_mvar_bsc = bsc.set_point if self.lb_voltage is not None and self.ub_voltage is not None: if self.vm_pu > self.ub_voltage: self.q_set_old_mvar, self.q_set_mvar = ( @@ -883,24 +1578,23 @@ def _droop_control_step(self, net): if self.q_set_old_mvar is not None: self.diff = self.q_set_mvar - self.q_set_old_mvar if self.q_set_mvar is not None: - net.controller.at[self.controller_idx, "object"].set_point = self.q_set_mvar + bsc.set_point = self.q_set_mvar else: - input_element = net.controller.at[self.controller_idx, "object"].input_element - input_element_index = net.controller.at[self.controller_idx, "object"].input_element_index - input_variable = net.controller.at[self.controller_idx, "object"].input_variable - read_flag = net.controller.at[self.controller_idx, "object"].read_flag + input_element = bsc.input_element + input_element_index = bsc.input_element_index + input_variable = bsc.input_variable + read_flag = bsc.read_flag input_values = [] counter = 0 for input_index in input_element_index: input_values.append(read_from_net(net, input_element, input_index, input_variable[counter], read_flag[counter])) - input_values = ( - net.controller.at[self.controller_idx, "object"].input_sign * np.asarray(input_values)).tolist() - self.vm_set_pu = getattr(self, 'vm_set_pu', net.controller.object[self.controller_idx].set_point) + input_values = (bsc.input_sign * np.asarray(input_values)).tolist() + self.vm_set_pu = getattr(self, 'vm_set_pu', bsc.set_point) self.vm_set_pu_new = self.vm_set_pu + sum( input_values) / self.q_droop_mvar - net.controller.at[self.controller_idx, "object"].set_point = self.vm_set_pu_new + bsc.set_point = self.vm_set_pu_new class VDroopControl_local(Controller): diff --git a/pandapower/control/run_control.py b/pandapower/control/run_control.py index 463d29558e..f39878a516 100644 --- a/pandapower/control/run_control.py +++ b/pandapower/control/run_control.py @@ -49,7 +49,9 @@ def get_controller_order(nets, controller): controller_order.append([*zip(rel_controller[order.argsort()], nets[to_add][order.argsort()])]) # controller_order.append(net.controller[to_add].sort_values(["order"]).object.values) - if logger.level <= pplog.DEBUG: + # getEffectiveLevel resolves the NOTSET (0) default to the parent logger level; comparing + # logger.level directly made every run build the huge debug string below + if logger.getEffectiveLevel() <= pplog.DEBUG: logger.debug("levellist: " + str(level_list)) logger.debug("order: " + str(controller_order)) # Note: creates a long string if many controllers are present diff --git a/pandapower/control/util/sensitivity.py b/pandapower/control/util/sensitivity.py new file mode 100644 index 0000000000..82dfcbcdcc --- /dev/null +++ b/pandapower/control/util/sensitivity.py @@ -0,0 +1,103 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2026 by University of Kassel and Fraunhofer Institute for Energy Economics +# and Energy System Technology (IEE), Kassel. All rights reserved. + +""" +Voltage sensitivities from the Newton-Raphson Jacobian of the last powerflow. + +After every AC powerflow, pandapower keeps the final Newton-Raphson Jacobian in +``net._ppc["internal"]["J"]`` (see pandapower/pf/run_newton_raphson_pf.py). Its state +ordering is ``x = [Va(pv+pq); Vm(pq)]`` with the mismatch rows ``[dP(pv+pq); dQ(pq)]``, +all in ppci-internal bus numbering (``net._pd2ppc_lookups["bus"]`` maps pandapower bus +indices into that numbering after the powerflow). + +At the solution, F(x, Q_spec) = 0 with F_Q = Q_calc(x) - Q_spec, so a unit increase of the +reactive power injection at PQ bus b gives J * dx/dQ_b = e_{row_Q(b)} and therefore +dVm_m/dQ_b = (J^-1)[row_Vm(m), row_Q(b)]. The adjoint formulation solves +J^T y = e_{row_Vm(m)} once per *measured* bus, which is usually the smaller dimension. +""" + +import logging + +import numpy as np +from scipy.sparse.linalg import splu + +logger = logging.getLogger(__name__) + +# one-slot cache of the LU factorization of the current Jacobian. Deliberately NOT stored in +# net (_ppc or controllers): SuperLU objects cannot be deepcopied or serialized and would +# break net copying. Identity of the J matrix object decides validity; run_control triggers +# one powerflow per iteration, so all controllers of an iteration share one factorization. +_LU_CACHE = {"J": None, "lu": None} + + +def _factorized_jacobian(J): + if _LU_CACHE["J"] is not J: + _LU_CACHE["J"] = J + _LU_CACHE["lu"] = splu(J.tocsc()) + return _LU_CACHE["lu"] + + +def calc_dvm_dq(net, q_bus_idx, vm_bus_idx): + """Sensitivity of bus voltage magnitudes to reactive power injections. + + Parameters + ---------- + net : pandapowerNet + Net after a converged Newton-Raphson powerflow (runpp). + q_bus_idx : array-like of int + Pandapower bus indices where reactive power is injected (positive injection = + generation, e.g. positive sgen q_mvar). + vm_bus_idx : array-like of int + Pandapower bus indices whose voltage magnitude response is wanted. + + Returns + ------- + numpy.ndarray of shape (len(vm_bus_idx), len(q_bus_idx)) + dVm/dQ in pu per Mvar. Entries are NaN when either bus was not a PQ bus in the last + powerflow (slack/PV voltages are fixed; their Q is balanced by the generator), or + None if no Jacobian is available (e.g. no NR powerflow ran). + """ + ppc = net.get("_ppc") if hasattr(net, "get") else None + internal = ppc.get("internal") if ppc else None + J = internal.get("J") if internal else None + if J is None: + return None + pv, pq = internal["pv"], internal["pq"] + base_mva = internal["baseMVA"] + lookup = net["_pd2ppc_lookups"]["bus"] + npvpq = len(pv) + len(pq) + if J.shape[0] != npvpq + len(pq): + # FACTS/extended formulations append state variables; not supported here + logger.debug("calc_dvm_dq: Jacobian has extended state variables, skipping") + return None + pq_position = {int(b): i for i, b in enumerate(pq)} + + def block_row(pd_bus): + internal_bus = int(lookup[int(pd_bus)]) + position = pq_position.get(internal_bus) + return None if position is None else npvpq + position + + q_rows = [block_row(b) for b in q_bus_idx] + vm_rows = [block_row(b) for b in vm_bus_idx] + result = np.full((len(vm_rows), len(q_rows)), np.nan) + try: + lu = _factorized_jacobian(J) + except RuntimeError: # singular + logger.debug("calc_dvm_dq: Jacobian factorization failed") + return None + n = J.shape[0] + active_vm = [(i, row) for i, row in enumerate(vm_rows) if row is not None] + if not active_vm: + return result + # one batched adjoint solve for all measured buses + rhs = np.zeros((n, len(active_vm))) + for column, (_, vm_row) in enumerate(active_vm): + rhs[vm_row, column] = 1.0 + y = lu.solve(rhs, trans='T') + active_q = [(j, row) for j, row in enumerate(q_rows) if row is not None] + for column, (i, _) in enumerate(active_vm): + for j, q_row in active_q: + result[i, j] = y[q_row, column] / base_mva + return result diff --git a/pandapower/test/control/benchmarks/__init__.py b/pandapower/test/control/benchmarks/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/pandapower/test/control/benchmarks/baseline.json b/pandapower/test/control/benchmarks/baseline.json new file mode 100644 index 0000000000..223077bdc0 --- /dev/null +++ b/pandapower/test/control/benchmarks/baseline.json @@ -0,0 +1,316 @@ +{ + "baseline": { + "git_rev": "0c6a1f0a6", + "results": [ + { + "n_stations": 50, + "mode": "Q_ctrl", + "converged": true, + "wall_s": 1.7751, + "runpp_calls": 3, + "runpp_time_s": 1.7245, + "overhead_s": 0.0506 + }, + { + "n_stations": 200, + "mode": "Q_ctrl", + "converged": true, + "wall_s": 0.2922, + "runpp_calls": 3, + "runpp_time_s": 0.0791, + "overhead_s": 0.2131 + }, + { + "n_stations": 500, + "mode": "Q_ctrl", + "converged": true, + "wall_s": 0.7754, + "runpp_calls": 3, + "runpp_time_s": 0.0881, + "overhead_s": 0.6873 + }, + { + "n_stations": 50, + "mode": "V_ctrl", + "converged": true, + "wall_s": 0.2342, + "runpp_calls": 5, + "runpp_time_s": 0.145, + "overhead_s": 0.0892 + }, + { + "n_stations": 200, + "mode": "V_ctrl", + "converged": true, + "wall_s": 0.7248, + "runpp_calls": 5, + "runpp_time_s": 0.2018, + "overhead_s": 0.5231 + }, + { + "n_stations": 500, + "mode": "V_ctrl", + "converged": true, + "wall_s": 1.1831, + "runpp_calls": 5, + "runpp_time_s": 0.1485, + "overhead_s": 1.0345 + }, + { + "n_stations": 50, + "mode": "V_ctrl_Q_droop", + "converged": true, + "wall_s": 0.2525, + "runpp_calls": 5, + "runpp_time_s": 0.1333, + "overhead_s": 0.1192 + }, + { + "n_stations": 200, + "mode": "V_ctrl_Q_droop", + "converged": true, + "wall_s": 0.7304, + "runpp_calls": 5, + "runpp_time_s": 0.1476, + "overhead_s": 0.5828 + }, + { + "n_stations": 500, + "mode": "V_ctrl_Q_droop", + "converged": true, + "wall_s": 1.8922, + "runpp_calls": 5, + "runpp_time_s": 0.1405, + "overhead_s": 1.7517 + } + ] + }, + "post_step5_multistation": { + "git_rev": "0c6a1f0a6", + "results": [ + { + "n_stations": 50, + "mode": "Q_ctrl", + "multistation": true, + "converged": true, + "wall_s": 1.4507, + "runpp_calls": 2, + "runpp_time_s": 1.4441, + "overhead_s": 0.0066 + }, + { + "n_stations": 200, + "mode": "Q_ctrl", + "multistation": true, + "converged": true, + "wall_s": 0.0697, + "runpp_calls": 2, + "runpp_time_s": 0.0448, + "overhead_s": 0.0249 + }, + { + "n_stations": 500, + "mode": "Q_ctrl", + "multistation": true, + "converged": true, + "wall_s": 0.1128, + "runpp_calls": 2, + "runpp_time_s": 0.0528, + "overhead_s": 0.06 + }, + { + "n_stations": 50, + "mode": "V_ctrl", + "multistation": true, + "converged": true, + "wall_s": 0.1165, + "runpp_calls": 5, + "runpp_time_s": 0.1062, + "overhead_s": 0.0103 + }, + { + "n_stations": 200, + "mode": "V_ctrl", + "multistation": true, + "converged": true, + "wall_s": 0.1671, + "runpp_calls": 5, + "runpp_time_s": 0.1258, + "overhead_s": 0.0413 + }, + { + "n_stations": 500, + "mode": "V_ctrl", + "multistation": true, + "converged": true, + "wall_s": 0.1916, + "runpp_calls": 5, + "runpp_time_s": 0.1138, + "overhead_s": 0.0778 + }, + { + "n_stations": 50, + "mode": "V_ctrl_Q_droop", + "multistation": true, + "converged": true, + "wall_s": 0.1001, + "runpp_calls": 4, + "runpp_time_s": 0.0891, + "overhead_s": 0.0109 + }, + { + "n_stations": 200, + "mode": "V_ctrl_Q_droop", + "multistation": true, + "converged": true, + "wall_s": 0.1294, + "runpp_calls": 4, + "runpp_time_s": 0.0917, + "overhead_s": 0.0376 + }, + { + "n_stations": 500, + "mode": "V_ctrl_Q_droop", + "multistation": true, + "converged": true, + "wall_s": 0.1721, + "runpp_calls": 4, + "runpp_time_s": 0.0908, + "overhead_s": 0.0812 + } + ] + }, + "post_step5_per_station": { + "git_rev": "0c6a1f0a6", + "results": [ + { + "n_stations": 50, + "mode": "Q_ctrl", + "multistation": false, + "converged": true, + "wall_s": 1.5222, + "runpp_calls": 3, + "runpp_time_s": 1.4809, + "overhead_s": 0.0413 + }, + { + "n_stations": 200, + "mode": "Q_ctrl", + "multistation": false, + "converged": true, + "wall_s": 0.2422, + "runpp_calls": 3, + "runpp_time_s": 0.0809, + "overhead_s": 0.1613 + }, + { + "n_stations": 500, + "mode": "Q_ctrl", + "multistation": false, + "converged": true, + "wall_s": 0.5023, + "runpp_calls": 3, + "runpp_time_s": 0.0696, + "overhead_s": 0.4327 + }, + { + "n_stations": 50, + "mode": "V_ctrl", + "multistation": false, + "converged": true, + "wall_s": 0.1903, + "runpp_calls": 5, + "runpp_time_s": 0.114, + "overhead_s": 0.0763 + }, + { + "n_stations": 200, + "mode": "V_ctrl", + "multistation": false, + "converged": true, + "wall_s": 0.3714, + "runpp_calls": 5, + "runpp_time_s": 0.1051, + "overhead_s": 0.2663 + }, + { + "n_stations": 500, + "mode": "V_ctrl", + "multistation": false, + "converged": true, + "wall_s": 0.7898, + "runpp_calls": 5, + "runpp_time_s": 0.1149, + "overhead_s": 0.6749 + }, + { + "n_stations": 50, + "mode": "V_ctrl_Q_droop", + "multistation": false, + "converged": true, + "wall_s": 0.1979, + "runpp_calls": 5, + "runpp_time_s": 0.1128, + "overhead_s": 0.0851 + }, + { + "n_stations": 200, + "mode": "V_ctrl_Q_droop", + "multistation": false, + "converged": true, + "wall_s": 0.4226, + "runpp_calls": 5, + "runpp_time_s": 0.1083, + "overhead_s": 0.3143 + }, + { + "n_stations": 500, + "mode": "V_ctrl_Q_droop", + "multistation": false, + "converged": true, + "wall_s": 1.0303, + "runpp_calls": 5, + "runpp_time_s": 0.1382, + "overhead_s": 0.8922 + } + ] + }, + "post_step7_jacobian": { + "git_rev": "0c6a1f0a6", + "results": [ + { + "n_stations": 50, + "mode": "V_ctrl", + "multistation": true, + "update_method": "jacobian", + "converged": true, + "wall_s": 1.4268, + "runpp_calls": 3, + "runpp_time_s": 1.414, + "overhead_s": 0.0129 + }, + { + "n_stations": 200, + "mode": "V_ctrl", + "multistation": true, + "update_method": "jacobian", + "converged": true, + "wall_s": 0.1481, + "runpp_calls": 3, + "runpp_time_s": 0.0706, + "overhead_s": 0.0775 + }, + { + "n_stations": 500, + "mode": "V_ctrl", + "multistation": true, + "update_method": "jacobian", + "converged": true, + "wall_s": 0.4305, + "runpp_calls": 3, + "runpp_time_s": 0.0771, + "overhead_s": 0.3533 + } + ] + } +} \ No newline at end of file diff --git a/pandapower/test/control/benchmarks/bench_station_control.py b/pandapower/test/control/benchmarks/bench_station_control.py new file mode 100644 index 0000000000..f4dbc1d17c --- /dev/null +++ b/pandapower/test/control/benchmarks/bench_station_control.py @@ -0,0 +1,203 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2026 by University of Kassel and Fraunhofer Institute for Energy Economics +# and Energy System Technology (IEE), Kassel. All rights reserved. + +""" +Benchmark harness for the station controller (BinarySearchControl / DroopControl). + +Builds a synthetic HV/MV net with n independent stations (one MV feeder each, two sgens per +station) and measures, for a full run_control: + +- wall time +- number of powerflow (runpp) calls +- controller overhead: wall time minus the time spent inside runpp + +Usage (record a baseline, from the repo root): + + python -m pandapower.test.control.benchmarks.bench_station_control \ + --n 50 200 500 --mode Q_ctrl V_ctrl V_ctrl_Q_droop --label baseline --out baseline.json + +Results are appended to the JSON file under the given label, together with the git revision. +The committed ``baseline.json`` pins the pre-refactor performance; refactor steps are gated +against it (see the project plan: overhead reduction, runpp calls <= baseline). +""" + +import argparse +import json +import os +import subprocess +import time + +from pandapower.auxiliary import ControllerNotConverged +from pandapower.control.controller.station_control import BinarySearchControl, DroopControl +from pandapower.control.run_control import run_control +from pandapower.create import ( + create_empty_network, create_bus, create_ext_grid, create_transformer, create_load, + create_line, create_sgen) +from pandapower.run import runpp + +MODES = ("Q_ctrl", "V_ctrl", "V_ctrl_Q_droop") +DEFAULT_OUT = os.path.join(os.path.dirname(__file__), "baseline.json") + + +class TimingRunpp: + """runpp wrapper for run_control(net, run=...) counting calls and accumulated runpp time.""" + + def __init__(self): + self.count = 0 + self.time_s = 0.0 + + def __call__(self, net, **kwargs): + kwargs.pop("run", None) + self.count += 1 + t0 = time.perf_counter() + runpp(net, **kwargs) + self.time_s += time.perf_counter() - t0 + + +def build_bench_net(n_stations, mode): + """HV slack with ``n_stations`` identical MV feeders; one station controller per feeder. + + Each feeder: 110/20 kV trafo, MV load, MV->LV line, two sgens on the LV bus controlled + with distribution [0.6, 0.4]. ``mode`` selects the controller configuration per station. + """ + if mode not in MODES: + raise ValueError(f"mode must be one of {MODES}, got {mode!r}") + net = create_empty_network() + hv = create_bus(net, 110) + create_ext_grid(net, hv) + for k in range(n_stations): + mv = create_bus(net, 20) + lv = create_bus(net, 20) + trafo = create_transformer(net, hv, mv, "63 MVA 110/20 kV") + create_load(net, mv, 3, 0.1) + sgen_a = create_sgen(net, lv, p_mw=2., sn_mva=10) + sgen_b = create_sgen(net, lv, p_mw=1., sn_mva=10) + line = create_line(net, mv, lv, length_km=0.1, std_type="NAYY 4x50 SE") + outputs = dict( + output_element="sgen", output_variable="q_mvar", + output_element_index=[sgen_a, sgen_b], output_element_in_service=[True, True], + output_values_distribution=[0.6, 0.4]) + if mode == "Q_ctrl": + BinarySearchControl( + net, name=f"q_ctrl_{k}", ctrl_in_service=True, input_element="res_line", + damping_factor=0.9, input_variable=["q_to_mvar"], input_element_index=line, + set_point=0.5, control_modus="Q_ctrl", tol=1e-6, **outputs) + elif mode == "V_ctrl": + BinarySearchControl( + net, name=f"v_ctrl_{k}", ctrl_in_service=True, input_element="res_bus", + input_variable="vm_pu", input_element_index=[mv], set_point=1.02, + control_modus="V_ctrl", tol=1e-6, **outputs) + else: # V_ctrl_Q_droop + bsc = BinarySearchControl( + net, name=f"v_droop_bsc_{k}", ctrl_in_service=True, input_element="res_trafo", + input_variable="q_hv_mvar", input_element_index=[trafo], set_point=1.02, + control_modus="V_ctrl_Q_droop", bus_idx=mv, tol=1e-6, **outputs) + DroopControl(net, name=f"v_droop_{k}", q_droop_mvar=40, bus_idx=mv, vm_set_pu=1.02, + controller_idx=bsc.index, control_modus="V_ctrl_Q_droop", tol=1e-6) + return net + + +def station_dicts(mode, n_stations): + """Station dicts for BinarySearchControl.for_stations matching build_bench_net(mode).""" + stations = [] + for k in range(n_stations): + outputs = dict(output_element_index=[2 * k, 2 * k + 1], + output_values_distribution=[0.6, 0.4]) + if mode == "Q_ctrl": + stations.append(dict(control_modus="Q_ctrl", set_point=0.5, + input_element="res_line", input_variable="q_to_mvar", + input_element_index=k, **outputs)) + elif mode == "V_ctrl": + stations.append(dict(control_modus="V_ctrl", set_point=1.02, + input_element="res_bus", input_variable="vm_pu", + input_element_index=1 + 2 * k, **outputs)) + else: # V_ctrl_Q_droop + stations.append(dict(control_modus="V_ctrl_Q_droop", set_point=1.02, + input_element="res_trafo", input_variable="q_hv_mvar", + input_element_index=k, + droop=dict(q_droop_mvar=40, bus_idx=1 + 2 * k), **outputs)) + return stations + + +def build_bench_net_multistation(n_stations, mode, update_method="secant"): + """Same net as build_bench_net, but all stations in one for_stations instance.""" + net = build_bench_net(n_stations, mode) + net.controller.drop(net.controller.index, inplace=True) + BinarySearchControl.for_stations(net, station_dicts(mode, n_stations), + name="bench_multi", tol=1e-6, + update_method=update_method) + return net + + +def bench(n_stations, mode, max_iter=30, multistation=False, update_method="secant"): + """Run one benchmark case, return a result dict.""" + if multistation: + net = build_bench_net_multistation(n_stations, mode, update_method=update_method) + else: + net = build_bench_net(n_stations, mode) + timer = TimingRunpp() + t0 = time.perf_counter() + converged = True + try: + run_control(net, run=timer, max_iter=max_iter) + except ControllerNotConverged: + converged = False + wall_s = time.perf_counter() - t0 + return { + "n_stations": n_stations, + "mode": mode, + "multistation": multistation, + "update_method": update_method if multistation else None, + "converged": converged, + "wall_s": round(wall_s, 4), + "runpp_calls": timer.count, + "runpp_time_s": round(timer.time_s, 4), + "overhead_s": round(wall_s - timer.time_s, 4), + } + + +def _git_rev(): + try: + return subprocess.check_output( + ["git", "rev-parse", "--short", "HEAD"], + cwd=os.path.dirname(__file__)).decode().strip() + except Exception: # noqa: BLE001 - benchmark metadata only + return "unknown" + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--n", type=int, nargs="+", default=[50, 200, 500]) + parser.add_argument("--mode", nargs="+", default=list(MODES), choices=MODES) + parser.add_argument("--label", default="baseline", + help="section name in the output JSON, e.g. baseline/post_vectorization") + parser.add_argument("--out", default=DEFAULT_OUT) + parser.add_argument("--max-iter", type=int, default=30) + parser.add_argument("--multistation", action="store_true", + help="all stations in one for_stations controller instance") + parser.add_argument("--update-method", default="secant", choices=("secant", "jacobian"), + help="update method for --multistation runs") + args = parser.parse_args(argv) + + results = [] + for mode in args.mode: + for n in args.n: + result = bench(n, mode, max_iter=args.max_iter, multistation=args.multistation, + update_method=args.update_method) + results.append(result) + print(json.dumps(result)) + + data = {} + if os.path.isfile(args.out): + with open(args.out) as f: + data = json.load(f) + data[args.label] = {"git_rev": _git_rev(), "results": results} + with open(args.out, "w") as f: + json.dump(data, f, indent=2) + print(f"written section {args.label!r} to {args.out}") + + +if __name__ == "__main__": + main() diff --git a/pandapower/test/control/benchmarks/test_bench_smoke.py b/pandapower/test/control/benchmarks/test_bench_smoke.py new file mode 100644 index 0000000000..09647e5906 --- /dev/null +++ b/pandapower/test/control/benchmarks/test_bench_smoke.py @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2026 by University of Kassel and Fraunhofer Institute for Energy Economics +# and Energy System Technology (IEE), Kassel. All rights reserved. + +"""Smoke test keeping the station-control benchmark harness functional.""" + +import pytest + +from pandapower.test.control.benchmarks.bench_station_control import MODES, bench + + +@pytest.mark.parametrize("mode", MODES) +def test_bench_smoke(mode): + result = bench(10, mode) + assert result["converged"] + assert result["runpp_calls"] >= 1 + assert result["wall_s"] > 0 + + +if __name__ == '__main__': + pytest.main(['-s', __file__]) diff --git a/pandapower/test/control/test_stactrl_characterization.py b/pandapower/test/control/test_stactrl_characterization.py new file mode 100644 index 0000000000..c257c1bf13 --- /dev/null +++ b/pandapower/test/control/test_stactrl_characterization.py @@ -0,0 +1,604 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2026 by University of Kassel and Fraunhofer Institute for Energy Economics +# and Energy System Technology (IEE), Kassel. All rights reserved. + +""" +Characterization tests for the station controller (BinarySearchControl, DroopControl, +VDroopControl_local). + +The RECORDS dict below pins the converged results and the number of powerflow calls of every +scenario, recorded on the pre-refactor implementation (develop, 2026-07). Refactor steps of the +station controller must reproduce these values within VALUE_ATOL and must not need more powerflow +calls than the recorded baseline (fewer is allowed and expected). + +To (re-)record after an intentional behavior change, run: + python pandapower/test/control/test_stactrl_characterization.py --record +and paste the printed dict into RECORDS. +""" + +import os +import logging + +import numpy as np +import pytest + +from pandapower import pp_dir +from pandapower.control.controller.station_control import ( + BinarySearchControl, DroopControl, VDroopControl_local) +from pandapower.control.run_control import run_control +from pandapower.create import ( + create_empty_network, create_bus, create_buses, create_ext_grid, create_transformer, + create_load, create_line, create_line_from_parameters, create_sgen, create_gen, + create_impedance, create_shunt) +from pandapower.file_io import from_json +from pandapower.run import runpp + +logger = logging.getLogger(__name__) + +VALUE_ATOL = 5e-6 +# station outputs that merely realize a target (sgen/gen setpoints, shunt steps) depend on +# which iterate first satisfies |diff| < tol; they get a looser tolerance than the +# controlled quantities themselves +OUTPUT_ATOL = 5e-3 +PREREFACTOR_JSON = os.path.join(pp_dir, 'test', 'control', 'testfiles', + 'stactrl_prerefactor_v1.json') + + +def _atol_for(label): + if label.startswith(("q_sgen", "q_gen", "shunt_step")): + return OUTPUT_ATOL + return VALUE_ATOL + + +class CountingRunpp: + """runpp wrapper passed to run_control(net, run=...) that counts powerflow calls.""" + + def __init__(self): + self.count = 0 + + def __call__(self, net, **kwargs): + kwargs.pop("run", None) + self.count += 1 + runpp(net, **kwargs) + + +def _simple_test_net(): + # same net as test_stactrl.simple_test_net + net = create_empty_network() + create_bus(net, 110) + create_buses(net, 2, 20) + create_ext_grid(net, 0) + create_transformer(net, 0, 1, "63 MVA 110/20 kV") + create_load(net, 1, 3, 0.1) + create_sgen(net, 2, p_mw=2., sn_mva=10, name="sgen1") + create_line(net, 1, 2, length_km=0.1, std_type="NAYY 4x50 SE") + return net + + +def _multi_feeder_net(n_feeders): + """110 kV slack bus with n identical 20 kV feeders (trafo, load, line, sgen each).""" + net = create_empty_network() + hv = create_bus(net, 110) + create_ext_grid(net, hv) + for _ in range(n_feeders): + mv = create_bus(net, 20) + lv = create_bus(net, 20) + create_transformer(net, hv, mv, "63 MVA 110/20 kV") + create_load(net, mv, 3, 0.1) + create_sgen(net, lv, p_mw=2., sn_mva=10) + create_line(net, mv, lv, length_km=0.1, std_type="NAYY 4x50 SE") + return net + + +# ---------------------------------------------------------------------------------------------- +# scenario builders: each returns (net, run_kwargs, extract) where extract(net) -> {label: value} +# ---------------------------------------------------------------------------------------------- + +def build_v_ctrl(): + net = _simple_test_net() + BinarySearchControl( + net, name="BSC1", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[0], output_element_in_service=[True], output_values_distribution=[1], + input_element="res_bus", input_variable="vm_pu", input_element_index=[1], + set_point=1.02, control_modus="V_ctrl", tol=1e-6) + return net, {}, lambda net: { + "vm_bus1": net.res_bus.vm_pu.at[1], + "q_sgen0": net.res_sgen.q_mvar.at[0]} + + +def build_v_ctrl_two_sgens(): + net = _simple_test_net() + create_sgen(net, 2, p_mw=1., sn_mva=10, name="sgen2") + BinarySearchControl( + net, name="BSC1", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[0, 1], output_element_in_service=[True, True], + output_values_distribution=[0.6, 0.4], input_element="res_bus", input_variable="vm_pu", + input_element_index=[1], set_point=1.02, control_modus="V_ctrl", tol=1e-6) + return net, {}, lambda net: { + "vm_bus1": net.res_bus.vm_pu.at[1], + "q_sgen0": net.res_sgen.q_mvar.at[0], + "q_sgen1": net.res_sgen.q_mvar.at[1]} + + +def build_q_ctrl_line(): + net = _simple_test_net() + BinarySearchControl( + net, name="BSC1", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[0], output_element_in_service=[True], output_values_distribution=[1], + input_element="res_line", damping_factor=0.9, input_variable=["q_to_mvar"], + input_element_index=0, set_point=1, control_modus="Q_ctrl", tol=1e-6) + return net, {}, lambda net: { + "q_to_line0": net.res_line.q_to_mvar.at[0], + "q_sgen0": net.res_sgen.q_mvar.at[0]} + + +def build_q_ctrl_impedance(): + net = _simple_test_net() + create_impedance(net, 1, 2, sn_mva=1, rft_pu=0.01, xft_pu=0.01, rtf_pu=0.01, xtf_pu=0.01) + BinarySearchControl( + net, name="BSC1", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[0], output_element_in_service=[True], output_values_distribution=[1], + input_element="res_impedance", damping_factor=0.9, input_variable="q_to_mvar", + input_element_index=0, set_point=1, control_modus="Q_ctrl", tol=1e-6) + return net, {}, lambda net: { + "q_to_imp0": net.res_impedance.q_to_mvar.at[0], + "q_sgen0": net.res_sgen.q_mvar.at[0]} + + +def build_q_ctrl_inverted(): + net = _simple_test_net() + BinarySearchControl( + net, name="BSC1", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[0], output_element_in_service=[True], output_values_distribution=[1], + input_element="res_line", damping_factor=0.9, input_variable=["q_from_mvar"], + input_inverted=True, input_element_index=0, set_point=1, control_modus="Q_ctrl", tol=1e-6) + return net, {}, lambda net: { + "q_from_line0": net.res_line.q_from_mvar.at[0], + "q_sgen0": net.res_sgen.q_mvar.at[0]} + + +def build_pf_ctrl_cap(): + net = _simple_test_net() + BinarySearchControl( + net, name="BSC1", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=0, output_element_in_service=True, output_values_distribution=1, + input_element="res_line", damping_factor=0.9, input_variable="q_to_mvar", + input_element_index=0, set_point=0.7, control_modus="PF_ctrl_cap", tol=1e-6) + return net, {}, lambda net: { + "phi_to_line0": np.arctan(net.res_line.q_to_mvar.at[0] / net.res_line.p_to_mw.at[0]), + "q_sgen0": net.res_sgen.q_mvar.at[0]} + + +def build_pf_ctrl_ind(): + net = _simple_test_net() + BinarySearchControl( + net, name="BSC1", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=0, output_element_in_service=True, output_values_distribution=1, + input_element="res_line", damping_factor=0.9, input_variable="q_to_mvar", + input_element_index=0, set_point=0.7, control_modus="PF_ctrl_ind", tol=1e-6) + return net, {}, lambda net: { + "phi_to_line0": np.arctan(net.res_line.q_to_mvar.at[0] / net.res_line.p_to_mw.at[0]), + "q_sgen0": net.res_sgen.q_mvar.at[0]} + + +def build_tan_phi_ctrl(): + net = _simple_test_net() + BinarySearchControl( + net, name="BSC1", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=0, output_element_in_service=True, output_values_distribution=1, + input_element="res_trafo", input_variable="q_lv_mvar", input_element_index=0, + set_point=2, control_modus="tan_phi_ctrl", tol=1e-6) + return net, {}, lambda net: { + "tan_phi_trafo0": net.res_trafo.q_lv_mvar.at[0] / net.res_trafo.p_lv_mw.at[0], + "q_sgen0": net.res_sgen.q_mvar.at[0]} + + +def build_v_ctrl_q_droop(): + net = _simple_test_net() + bsc = BinarySearchControl( + net, name="BSC1", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[0], output_element_in_service=[True], output_values_distribution=[1], + input_element="res_trafo", input_variable="q_hv_mvar", input_element_index=[0], + set_point=1.02, control_modus="V_ctrl_Q_droop", bus_idx=1, tol=1e-6) + DroopControl(net, name="DC1", q_droop_mvar=40, bus_idx=1, vm_set_pu=1.02, + controller_idx=bsc.index, control_modus="V_ctrl_Q_droop", tol=1e-6) + return net, {}, lambda net: { + "vm_bus1": net.res_bus.vm_pu.at[1], + "q_hv_trafo0": net.res_trafo.q_hv_mvar.at[0], + "q_sgen0": net.res_sgen.q_mvar.at[0]} + + +def build_q_ctrl_v_droop_deadband(): + net = _simple_test_net() + net.load.loc[0, "p_mw"] = 60 # create voltage drop at bus 1 + bsc = BinarySearchControl( + net, name="BSC1", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[0], output_element_in_service=[True], output_values_distribution=[1], + input_element="res_line", damping_factor=0.9, input_variable=["q_from_mvar"], + input_inverted=True, input_element_index=0, set_point=1, control_modus="Q_ctrl_V_droop", + tol=1e-6) + DroopControl(net, name="DC1", q_droop_mvar=40, bus_idx=1, vm_set_pu=1, vm_set_ub=1.005, + vm_set_lb=0.995, controller_idx=bsc.index, control_modus="Q_ctrl_V_droop") + return net, {}, lambda net: { + "vm_bus1": net.res_bus.vm_pu.at[1], + "q_from_line0": net.res_line.q_from_mvar.at[0], + "q_sgen0": net.res_sgen.q_mvar.at[0]} + + +def build_v_droop_local(): + # gen voltage setpoint controlled so that the gen's Q output follows a local V droop, + # mirroring the PowerFactory converter pattern (pp_import_functions.py ~2216) + net = _simple_test_net() + # start away from the droop fixed point so the controllers actually iterate + create_gen(net, 2, p_mw=2., vm_pu=1.02, sn_mva=10) + bsc = BinarySearchControl( + net, name="BSC1", ctrl_in_service=True, output_element="gen", output_variable="vm_pu", + output_element_index=[0], output_element_in_service=[True], output_values_distribution=[1], + input_element="res_gen", input_variable="q_mvar", input_element_index=[0], + input_inverted=[False], set_point=1.0, control_modus="V_ctrl_Q_droop_local", bus_idx=2, + tol=1e-5) + VDroopControl_local(net, q_droop_mvar=20, controller_idx=bsc.index, bus_idx=2, + control_modus="V_ctrl_Q_droop_local", q_set_mvar=0.5, vm_set_pu_bsc=1.0, + tol=1e-5) + return net, {}, lambda net: { + "vm_bus2": net.res_bus.vm_pu.at[2], + "q_gen0": net.res_gen.q_mvar.at[0]} + + +def build_qlims_q_ctrl(): + net = _simple_test_net() + net.sgen['min_q_mvar'] = -0.5 + net.sgen['max_q_mvar'] = 0.5 + BinarySearchControl( + net, name="BSC1", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[0], output_element_in_service=[True], output_values_distribution=[1], + input_element="res_line", damping_factor=0.9, input_variable=["q_to_mvar"], + input_element_index=0, set_point=1, control_modus="Q_ctrl", tol=1e-6) + return net, {"enforce_q_lims": True}, lambda net: { + "q_sgen0": net.res_sgen.q_mvar.at[0]} + + +def build_qlims_v_ctrl(): + net = _simple_test_net() + net.sgen['min_q_mvar'] = -0.7 + net.sgen['max_q_mvar'] = 0.7 + BinarySearchControl( + net, name="BSC1", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[0], output_element_in_service=[True], output_values_distribution=[1], + input_element="res_bus", input_variable="vm_pu", input_element_index=[1], + set_point=1.02, control_modus="V_ctrl", tol=1e-6) + return net, {"enforce_q_lims": True}, lambda net: { + "vm_bus1": net.res_bus.vm_pu.at[1], + "q_sgen0": net.res_sgen.q_mvar.at[0]} + + +def build_shunt_step(): + net = create_empty_network() + b = create_buses(net, 2, 110) + create_ext_grid(net, b[0]) + create_line_from_parameters(net, from_bus=b[0], to_bus=b[1], length_km=50, + r_ohm_per_km=0.1021, x_ohm_per_km=0.1570796, max_i_ka=0.461, + c_nf_per_km=130) + create_shunt(net, bus=b[1], q_mvar=-50, p_mw=0, step=1, max_step=5) + BinarySearchControl( + net, ctrl_in_service=True, output_element='shunt', output_variable='step', + output_element_index=[0], output_element_in_service=[True], output_values_distribution=[1], + input_element='res_bus', input_variable='vm_pu', input_element_index=[1], + set_point=1.08, control_modus="V_ctrl", tol=1e-6) + return net, {}, lambda net: { + "vm_bus1": net.res_bus.vm_pu.at[1], + "shunt_step": net.shunt.step.at[0]} + + +def build_three_controllers(): + # three stations on one HV bus: interacting controllers of different modi + net = _multi_feeder_net(3) + # feeder k: buses (1+2k, 2+2k), trafo k, line k, sgen k + BinarySearchControl( + net, name="V", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[0], output_element_in_service=[True], output_values_distribution=[1], + input_element="res_bus", input_variable="vm_pu", input_element_index=[1], + set_point=1.02, control_modus="V_ctrl", tol=1e-6) + BinarySearchControl( + net, name="Q", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[1], output_element_in_service=[True], output_values_distribution=[1], + input_element="res_line", damping_factor=0.9, input_variable=["q_to_mvar"], + input_element_index=1, set_point=0.5, control_modus="Q_ctrl", tol=1e-6) + BinarySearchControl( + net, name="T", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[2], output_element_in_service=[True], output_values_distribution=[1], + input_element="res_trafo", input_variable="q_lv_mvar", input_element_index=2, + set_point=0.5, control_modus="tan_phi_ctrl", tol=1e-6) + return net, {}, lambda net: { + "vm_bus1": net.res_bus.vm_pu.at[1], + "q_to_line1": net.res_line.q_to_mvar.at[1], + "tan_phi_trafo2": net.res_trafo.q_lv_mvar.at[2] / net.res_trafo.p_lv_mw.at[2], + "q_sgen0": net.res_sgen.q_mvar.at[0], + "q_sgen1": net.res_sgen.q_mvar.at[1], + "q_sgen2": net.res_sgen.q_mvar.at[2]} + + +SCENARIOS = { + "v_ctrl": build_v_ctrl, + "v_ctrl_two_sgens": build_v_ctrl_two_sgens, + "q_ctrl_line": build_q_ctrl_line, + "q_ctrl_impedance": build_q_ctrl_impedance, + "q_ctrl_inverted": build_q_ctrl_inverted, + "pf_ctrl_cap": build_pf_ctrl_cap, + "pf_ctrl_ind": build_pf_ctrl_ind, + "tan_phi_ctrl": build_tan_phi_ctrl, + "v_ctrl_q_droop": build_v_ctrl_q_droop, + "q_ctrl_v_droop_deadband": build_q_ctrl_v_droop_deadband, + "v_droop_local": build_v_droop_local, + "qlims_q_ctrl": build_qlims_q_ctrl, + "qlims_v_ctrl": build_qlims_v_ctrl, + "shunt_step": build_shunt_step, + "three_controllers": build_three_controllers, +} + +# recorded on pre-refactor develop (0c6a1f0a6, 2026-07) -- see module docstring +RECORDS = { + 'v_ctrl': { + 'runpp_count': 5, + 'values': { + 'vm_bus1': 1.019999996915152, + 'q_sgen0': 7.265806884389418, + }, + }, + 'v_ctrl_two_sgens': { + 'runpp_count': 5, + 'values': { + 'vm_bus1': 1.0199999969589186, + 'q_sgen0': 4.347800920511612, + 'q_sgen1': 2.8985339470077416, + }, + }, + 'q_ctrl_line': { + 'runpp_count': 3, + 'values': { + 'q_to_line0': 0.9999999983680944, + 'q_sgen0': 0.9999999983679086, + }, + }, + 'q_ctrl_impedance': { + 'runpp_count': 4, + 'values': { + 'q_to_imp0': 0.9999996694339104, + 'q_sgen0': 109.23659617195352, + }, + }, + 'q_ctrl_inverted': { + 'runpp_count': 4, + 'values': { + 'q_from_line0': -0.999999999577785, + 'q_sgen0': 0.997450108031786, + }, + }, + 'pf_ctrl_cap': { + 'runpp_count': 3, + 'values': { + 'phi_to_line0': -0.7953988279821941, + 'q_sgen0': -2.040408113453427, + }, + }, + 'pf_ctrl_ind': { + 'runpp_count': 3, + 'values': { + 'phi_to_line0': 0.7953988307386679, + 'q_sgen0': 2.040408124707689, + }, + }, + 'tan_phi_ctrl': { + 'runpp_count': 4, + 'values': { + 'tan_phi_trafo0': 1.9999993561180684, + 'q_sgen0': -1.9049229990555525, + }, + }, + 'v_ctrl_q_droop': { + 'runpp_count': 5, + 'values': { + 'vm_bus1': 1.0020245144045044, + 'q_hv_trafo0': -0.7190194237899732, + 'q_sgen0': 0.8332080621680016, + }, + }, + 'q_ctrl_v_droop_deadband': { + 'runpp_count': 5, + 'values': { + 'vm_bus1': 0.9863775299392599, + 'q_from_line0': -1.34489880242843, + 'q_sgen0': 1.342454080724194, + }, + }, + 'v_droop_local': { + 'runpp_count': 6, + 'values': { + 'vm_bus2': 1.0017284837979823, + 'q_gen0': 0.4654303789138794, + }, + }, + 'qlims_q_ctrl': { + 'runpp_count': 3, + 'values': { + 'q_sgen0': 0.5, + }, + }, + 'qlims_v_ctrl': { + 'runpp_count': 3, + 'values': { + 'vm_bus1': 1.0016454077928925, + 'q_sgen0': 0.7, + }, + }, + 'shunt_step': { + 'runpp_count': 5, + 'values': { + 'vm_bus1': 1.0799999784869583, + 'shunt_step': 2.0752751205395854, + }, + }, + 'three_controllers': { + 'runpp_count': 5, + 'values': { + 'vm_bus1': 1.019999996915152, + 'q_to_line1': 0.49999999918549787, + 'tan_phi_trafo2': 0.4999999997653505, + 'q_sgen0': 7.265806884389418, + 'q_sgen1': 0.4999999991854217, + 'q_sgen2': -0.4028800134651635, + }, + }, + 'prerefactor_json': { + 'runpp_count': 6, + 'keys': [ + ('res_bus', 1, 'vm_pu'), + ('res_line', 1, 'q_to_mvar'), + ('res_line', 2, 'q_to_mvar'), + ('res_line', 2, 'p_to_mw'), + ('res_trafo', 3, 'q_lv_mvar'), + ('res_trafo', 3, 'p_lv_mw'), + ('res_bus', 9, 'vm_pu'), + ('res_trafo', 4, 'q_hv_mvar'), + ('res_bus', 12, 'vm_pu'), + ('res_gen', 0, 'q_mvar'), + ('res_sgen', 0, 'q_mvar'), + ('res_sgen', 1, 'q_mvar'), + ('res_sgen', 2, 'q_mvar'), + ('res_sgen', 3, 'q_mvar'), + ('res_sgen', 4, 'q_mvar'), + ], + 'values': [ + 1.019999996915152, + 0.5000000005905079, + 0.9686442123565855, + 2.000000000000376, + -0.5003347375516176, + -1.000669475572584, + 1.0020245144045037, + -0.719019423789689, + 1.0017284837979825, + 0.4654303789138794, + 7.26580688439267, + 0.5000000005886843, + 0.9686442123544164, + -0.4028800134649547, + 0.8332080621679098, + ], + }, +} + + +def run_scenario(name): + net, run_kwargs, extract = SCENARIOS[name]() + counter = CountingRunpp() + run_control(net, run=counter, **run_kwargs) + assert all(net.controller.object[i].converged for i in net.controller.index) + return counter.count, extract(net) + + +@pytest.mark.parametrize("name", sorted(SCENARIOS)) +def test_characterization(name): + if name not in RECORDS: + pytest.skip(f"no record for scenario {name} -- record with --record first") + runpp_count, values = run_scenario(name) + expected = RECORDS[name] + assert runpp_count <= expected["runpp_count"], ( + f"{name}: {runpp_count} powerflow calls, baseline is {expected['runpp_count']}") + for label, expected_value in expected["values"].items(): + assert values[label] == pytest.approx(expected_value, abs=_atol_for(label)), ( + f"{name}: {label} = {values[label]}, recorded {expected_value}") + + +def test_load_prerefactor_json_and_solve(): + """A net saved with the pre-refactor implementation must load and solve unchanged. + + Guards attribute-level backward compatibility: from_json restores controller __dict__ + without calling __init__. + """ + if not os.path.isfile(PREREFACTOR_JSON): + pytest.skip("frozen pre-refactor fixture not generated yet") + net = from_json(PREREFACTOR_JSON) + counter = CountingRunpp() + run_control(net, run=counter) + assert all(net.controller.object[i].converged for i in net.controller.index) + if "prerefactor_json" in RECORDS: + expected = RECORDS["prerefactor_json"] + assert counter.count <= expected["runpp_count"] + for (element, index, column), expected_value in zip( + expected["keys"], expected["values"]): + if element in ("res_sgen", "res_gen"): + atol = OUTPUT_ATOL + elif element == "res_bus": + atol = VALUE_ATOL + else: + atol = 1e-4 # branch measurements shift slightly with the realized outputs + assert net[element].at[index, column] == pytest.approx( + expected_value, abs=atol), (element, index, column) + + +def test_json_roundtrip_after_refactor(tmp_path): + """Controllers keep working after to_json/from_json and derived state is not serialized.""" + from pandapower.file_io import to_json + + net, run_kwargs, extract = SCENARIOS["v_ctrl_two_sgens"]() + run_control(net, **run_kwargs) # populates derived state (_vstate etc.) on the controller + ctrl = net.controller.object.at[0] + serialized = ctrl.to_dict() + assert "_vstate" not in serialized + assert "_linked_droop_objs" not in serialized + + json_file = os.path.join(tmp_path, "roundtrip.json") + to_json(net, json_file) + net2 = from_json(json_file) + counter = CountingRunpp() + run_control(net2, run=counter, **run_kwargs) + assert all(net2.controller.object[i].converged for i in net2.controller.index) + reference, restored = extract(net), extract(net2) + for label in reference: + assert restored[label] == pytest.approx(reference[label], abs=VALUE_ATOL), label + + +def test_out_of_service_output_element_midrun(): + """in_service changes between two run_control calls must be picked up (derived positional + state is rebuilt in initialize_control).""" + net, run_kwargs, extract = SCENARIOS["v_ctrl_two_sgens"]() + run_control(net, **run_kwargs) + assert net.res_sgen.q_mvar.at[1] != 0.0 + net.sgen.at[1, "in_service"] = False + run_control(net, **run_kwargs) + assert all(net.controller.object[i].converged for i in net.controller.index) + # remaining sgen carries the whole station, target voltage still reached; the + # out-of-service sgen contributes nothing (its stale setpoint is not written/applied) + assert net.res_bus.vm_pu.at[1] == pytest.approx(1.02, abs=1e-5) + assert net.res_sgen.q_mvar.at[1] == 0.0 + + +def _record(): + records = {} + for name in SCENARIOS: + try: + runpp_count, values = run_scenario(name) + except Exception as err: # noqa: BLE001 - report scenario failures during recording + print(f"# scenario {name} FAILED: {err!r}") + continue + records[name] = {"runpp_count": runpp_count, "values": values} + print("RECORDS = {") + for name, rec in records.items(): + print(f" {name!r}: {{") + print(f" 'runpp_count': {rec['runpp_count']},") + print(" 'values': {") + for label, value in rec["values"].items(): + print(f" {label!r}: {float(value)!r},") + print(" },") + print(" },") + print("}") + + +if __name__ == '__main__': + import sys + if "--record" in sys.argv: + _record() + else: + pytest.main(['-s', __file__]) diff --git a/pandapower/test/control/test_stactrl_convergence.py b/pandapower/test/control/test_stactrl_convergence.py new file mode 100644 index 0000000000..76bc3f28bf --- /dev/null +++ b/pandapower/test/control/test_stactrl_convergence.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2026 by University of Kassel and Fraunhofer Institute for Energy Economics +# and Energy System Technology (IEE), Kassel. All rights reserved. + +"""Tests for the safeguarded secant update of BinarySearchControl (convergence stabilization).""" + +import numpy as np +import pytest + +from pandapower.auxiliary import ControllerNotConverged +from pandapower.control.controller.station_control import BinarySearchControl +from pandapower.control.run_control import run_control +from pandapower.create import ( + create_empty_network, create_bus, create_buses, create_ext_grid, create_transformer, + create_load, create_line, create_sgen) # noqa: F401 - create_buses used in tests +from pandapower.run import runpp + + +def simple_test_net(): + net = create_empty_network() + create_bus(net, 110) + create_buses(net, 2, 20) + create_ext_grid(net, 0) + create_transformer(net, 0, 1, "63 MVA 110/20 kV") + create_load(net, 1, 3, 0.1) + create_sgen(net, 2, p_mw=2., sn_mva=10, name="sgen1") + create_line(net, 1, 2, length_km=0.1, std_type="NAYY 4x50 SE") + return net + + +def test_near_zero_slope_no_blowup(): + """A measurement that does not respond to the output must not blow up the outputs. + + The legacy secant divided by a 1e-6 dummy slope, multiplying the residual into the + output values. The controller cannot converge (expected), but outputs stay bounded. + """ + net = simple_test_net() + # second, electrically separate feeder: its line flow is unaffected by sgen 0 + create_buses(net, 2, 20) + create_transformer(net, 0, 3, "63 MVA 110/20 kV") + create_load(net, 4, 3, 0.5) + create_line(net, 3, 4, length_km=0.1, std_type="NAYY 4x50 SE") + BinarySearchControl( + net, name="flat", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[0], output_element_in_service=[True], output_values_distribution=[1], + input_element="res_line", input_variable=["q_to_mvar"], input_element_index=1, + set_point=1, control_modus="Q_ctrl", tol=1e-6) + # plain NR solver (like the CI environments): the weaker solver must never be driven + # into voltage collapse by the controller fallback steps + with pytest.raises(ControllerNotConverged): + run_control(net, lightsim2grid=False) + assert abs(net.sgen.q_mvar.at[0]) < 20, "flat-response outputs must stay bounded" + + +def test_pf_setpoint_not_mutated(): + """The near-zero power factor clipping must not overwrite the user-given set point.""" + net = simple_test_net() + ctrl = BinarySearchControl( + net, name="pf", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=0, output_element_in_service=True, output_values_distribution=1, + input_element="res_line", input_variable="q_to_mvar", input_element_index=0, + set_point=0.005, control_modus="PF_ctrl_ind", tol=1e-6) + run_control(net) + assert ctrl.set_point == 0.005 + assert ctrl.converged + + +def test_damping_factor_scales_first_probe(): + """damping_factor scales the residual-sized first probe of Q-type controllers.""" + results = {} + for damping in (1.0, 0.5): + net = simple_test_net() + ctrl = BinarySearchControl( + net, name="q", ctrl_in_service=True, output_element="sgen", + output_variable="q_mvar", output_element_index=[0], output_element_in_service=[True], + output_values_distribution=[1], input_element="res_line", + input_variable=["q_to_mvar"], input_element_index=0, set_point=1, + control_modus="Q_ctrl", tol=1e-6, damping_factor=damping) + runpp(net) + ctrl.initialize_control(net) + assert not ctrl.is_converged(net) + ctrl.control_step(net) + results[damping] = net.sgen.q_mvar.at[0] + # residual is ~1 Mvar; the probe is damping * residual + assert results[1.0] == pytest.approx(2 * results[0.5], rel=1e-6) + + +def test_bracketing_monotone(): + """Once a bracket is established, all further iterates stay inside it and converge.""" + net = simple_test_net() + ctrl = BinarySearchControl( + net, name="unit", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[0], output_element_in_service=[True], + output_values_distribution=[1], input_element="res_line", input_variable=["q_to_mvar"], + input_element_index=0, set_point=0, control_modus="Q_ctrl", tol=1e-9) + + def residual(x): # nonlinear, root at x = ln(2) / 0.3 + return 2.0 - np.exp(0.3 * x) + + root = np.log(2.0) / 0.3 + vs = {} + x_old, x = 0.0, 10.0 # residual(0) > 0, residual(10) < 0 -> bracket on first update + ctrl.set_point = 0 + ctrl.diff_old, ctrl.diff = residual(x_old), residual(x) + bracket_seen = False + for _ in range(60): + ctrl.output_values_old = np.array([x_old]) + ctrl.output_values = np.array([x]) + x_new = ctrl._safeguarded_secant_total(vs, damping=1.0) + if vs.get('solver', {}).get('lo') is not None: + bracket_seen = True + lo_x = vs['solver']['lo'][0] + hi_x = vs['solver']['hi'][0] + assert min(lo_x, hi_x) <= x_new <= max(lo_x, hi_x) + x_old, x = x, x_new + ctrl.diff_old, ctrl.diff = ctrl.diff, residual(x_new) + if abs(ctrl.diff) < 1e-12: + break + assert bracket_seen + assert x == pytest.approx(root, abs=1e-6) + + +def test_output_resync(): + """External modifications of the written output values are picked up before the update.""" + net = simple_test_net() + ctrl = BinarySearchControl( + net, name="q", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[0], output_element_in_service=[True], + output_values_distribution=[1], input_element="res_line", input_variable=["q_to_mvar"], + input_element_index=0, set_point=1, control_modus="Q_ctrl", tol=1e-6) + run_control(net) + assert ctrl.converged + net.sgen.q_mvar.at[0] = -3.0 # external actor overwrites the setpoint + ctrl._resync_output_values(net, ctrl._vstate) + assert np.atleast_1d(ctrl.output_values)[0] == pytest.approx(-3.0) + # and a full re-run still converges to the target + run_control(net) + assert ctrl.converged + assert net.res_line.q_to_mvar.at[0] == pytest.approx(1.0, abs=1e-6) + + +def _two_station_chain_net(): + """Two V_ctrl stations along the same feeder (strong coupling through the transformer).""" + net = simple_test_net() + b3 = create_bus(net, 20) + create_line(net, 2, b3, length_km=5.0, std_type="NAYY 4x50 SE") + create_sgen(net, b3, p_mw=1., sn_mva=10, name="sgen2") + BinarySearchControl( + net, name="v1", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[0], output_element_in_service=[True], + output_values_distribution=[1], input_element="res_bus", input_variable="vm_pu", + input_element_index=[1], set_point=1.02, control_modus="V_ctrl", tol=1e-6) + BinarySearchControl( + net, name="v2", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[1], output_element_in_service=[True], + output_values_distribution=[1], input_element="res_bus", input_variable="vm_pu", + input_element_index=[b3], set_point=1.03, control_modus="V_ctrl", tol=1e-6) + return net, b3 + + +def test_coupled_two_stations_iteration_budget(): + """Two V_ctrl stations in different substations converge within a bounded number of + powerflows (coupling through the shared HV bus).""" + from pandapower.test.control.test_stactrl_characterization import CountingRunpp + + net = simple_test_net() + # second station on its own feeder + b3, b4 = create_buses(net, 2, 20) + create_transformer(net, 0, b3, "63 MVA 110/20 kV") + create_load(net, b3, 3, 0.1) + create_line(net, b3, b4, length_km=0.1, std_type="NAYY 4x50 SE") + create_sgen(net, b4, p_mw=1., sn_mva=10, name="sgen2") + BinarySearchControl( + net, name="v1", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[0], output_element_in_service=[True], + output_values_distribution=[1], input_element="res_bus", input_variable="vm_pu", + input_element_index=[1], set_point=1.02, control_modus="V_ctrl", tol=1e-6) + BinarySearchControl( + net, name="v2", ctrl_in_service=True, output_element="sgen", output_variable="q_mvar", + output_element_index=[1], output_element_in_service=[True], + output_values_distribution=[1], input_element="res_bus", input_variable="vm_pu", + input_element_index=[b3], set_point=1.03, control_modus="V_ctrl", tol=1e-6) + counter = CountingRunpp() + run_control(net, run=counter) + assert all(net.controller.object[i].converged for i in net.controller.index) + assert net.res_bus.vm_pu.at[1] == pytest.approx(1.02, abs=1e-6) + assert net.res_bus.vm_pu.at[b3] == pytest.approx(1.03, abs=1e-6) + assert counter.count <= 12, f"coupled stations needed {counter.count} powerflows" + + +@pytest.mark.xfail(reason="two independently iterating V_ctrl stations behind the same " + "transformer have a Jacobi contraction factor near 1; the " + "pre-refactor implementation does not converge this case either " + "(verified with max_iter=100). Solved by the coupled Newton of " + "update_method='jacobian'.", strict=True) +def test_coupled_two_stations_same_feeder(): + net, b3 = _two_station_chain_net() + run_control(net) + assert net.res_bus.vm_pu.at[1] == pytest.approx(1.02, abs=1e-6) + assert net.res_bus.vm_pu.at[b3] == pytest.approx(1.03, abs=1e-6) + + +if __name__ == '__main__': + pytest.main(['-s', __file__]) diff --git a/pandapower/test/control/test_stactrl_jacobian.py b/pandapower/test/control/test_stactrl_jacobian.py new file mode 100644 index 0000000000..d4d556321e --- /dev/null +++ b/pandapower/test/control/test_stactrl_jacobian.py @@ -0,0 +1,156 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2026 by University of Kassel and Fraunhofer Institute for Energy Economics +# and Energy System Technology (IEE), Kassel. All rights reserved. + +"""Tests for the opt-in Jacobian sensitivity update (update_method="jacobian").""" + +import copy + +import numpy as np +import pytest + +from pandapower.control.controller.station_control import BinarySearchControl +from pandapower.control.run_control import run_control +from pandapower.control.util.sensitivity import calc_dvm_dq +from pandapower.run import runpp +from pandapower.test.control.benchmarks.bench_station_control import ( + build_bench_net, station_dicts) +from pandapower.test.control.test_stactrl_characterization import CountingRunpp + + +def _bare_bench_net(n_stations): + net = build_bench_net(n_stations, "Q_ctrl") + net.controller.drop(net.controller.index, inplace=True) + return net + + +def test_dvm_dq_matches_finite_difference(): + """Pins sign, scaling (pu/Mvar) and index mapping of calc_dvm_dq.""" + net = _bare_bench_net(2) + runpp(net) + q_buses = [2, 4] # lv buses carrying the sgens + vm_buses = [1, 3] # mv buses + sensitivity = calc_dvm_dq(net, q_buses, vm_buses) + assert sensitivity.shape == (2, 2) + assert np.all(np.isfinite(sensitivity)) + # injection raises the own feeder voltage; the feeders decouple through the slack bus + assert np.all(np.diag(sensitivity) > 0) + delta = 0.5 # Mvar, central difference + for j, sgen in enumerate([0, 2]): # sgen 0 at bus 2, sgen 2 at bus 4 + vm = {} + for sign in (+1, -1): + pert = copy.deepcopy(net) + pert.sgen.at[sgen, "q_mvar"] += sign * delta + runpp(pert) + vm[sign] = pert.res_bus.vm_pu.values[[1, 3]] + finite_difference = (vm[+1] - vm[-1]) / (2 * delta) + assert np.allclose(sensitivity[:, j], finite_difference, rtol=1e-3, atol=1e-9), ( + f"column {j}: {sensitivity[:, j]} vs FD {finite_difference}") + # non-PQ buses give NaN: the slack bus (ref) + assert np.isnan(calc_dvm_dq(net, [0], [1])[0, 0]) + assert np.isnan(calc_dvm_dq(net, [2], [0])[0, 0]) + + +@pytest.mark.parametrize("n", [3, 20]) +def test_jacobian_vctrl_same_result_fewer_runpp(n): + """Jacobian mode reaches the same fixed point with strictly fewer powerflows.""" + counts, nets = {}, {} + for method in ("secant", "jacobian"): + net = _bare_bench_net(n) + BinarySearchControl.for_stations(net, station_dicts("V_ctrl", n), name=method, + tol=1e-6, update_method=method) + counter = CountingRunpp() + run_control(net, run=counter) + assert net.controller.object.at[0].converged + counts[method] = counter.count + nets[method] = net + mv_buses = [1 + 2 * k for k in range(n)] + assert np.allclose(nets["jacobian"].res_bus.vm_pu.values[mv_buses], 1.02, atol=2e-6) + assert np.allclose(nets["jacobian"].res_sgen.q_mvar.values, + nets["secant"].res_sgen.q_mvar.values, atol=1e-3) + assert counts["jacobian"] < counts["secant"], counts + + +def test_jacobian_coupled_same_feeder(): + """Two V_ctrl stations behind the same transformer: unsolvable for independent secant + iterations (see test_stactrl_convergence xfail), solved by the coupled Newton.""" + from pandapower.create import create_bus, create_line, create_sgen + from pandapower.test.control.test_stactrl_convergence import simple_test_net + + net = simple_test_net() + b3 = create_bus(net, 20) + create_line(net, 2, b3, length_km=5.0, std_type="NAYY 4x50 SE") + create_sgen(net, b3, p_mw=1., sn_mva=10, name="sgen2") + stations = [ + dict(control_modus="V_ctrl", set_point=1.02, input_element="res_bus", + input_variable="vm_pu", input_element_index=1, + output_element_index=[0], output_values_distribution=[1]), + dict(control_modus="V_ctrl", set_point=1.03, input_element="res_bus", + input_variable="vm_pu", input_element_index=b3, + output_element_index=[1], output_values_distribution=[1]), + ] + BinarySearchControl.for_stations(net, stations, name="coupled", tol=1e-6, + update_method="jacobian") + counter = CountingRunpp() + run_control(net, run=counter) + assert net.controller.object.at[0].converged + assert net.res_bus.vm_pu.at[1] == pytest.approx(1.02, abs=1e-6) + assert net.res_bus.vm_pu.at[b3] == pytest.approx(1.03, abs=1e-6) + assert counter.count <= 8, f"coupled Newton needed {counter.count} powerflows" + + +def test_jacobian_fallback_no_internal(): + """Without a stored Jacobian the controller falls back to secant and still converges.""" + n = 3 + net = _bare_bench_net(n) + BinarySearchControl.for_stations(net, station_dicts("V_ctrl", n), name="fb", + tol=1e-6, update_method="jacobian") + + class JacobianStrippingRunpp(CountingRunpp): + def __call__(self, net, **kwargs): + super().__call__(net, **kwargs) + net._ppc["internal"].pop("J", None) + + counter = JacobianStrippingRunpp() + run_control(net, run=counter) + assert net.controller.object.at[0].converged + assert np.allclose(net.res_bus.vm_pu.values[[1, 3, 5]], 1.02, atol=2e-6) + + +def test_jacobian_with_qlims(): + """Stations at their Q limits leave the Newton system; limits are respected.""" + n = 2 + net = _bare_bench_net(n) + net.sgen["min_q_mvar"] = [-0.1, -0.1, -50.0, -50.0] + net.sgen["max_q_mvar"] = [0.1, 0.1, 50.0, 50.0] + BinarySearchControl.for_stations(net, station_dicts("V_ctrl", n), name="lims", + tol=1e-6, update_method="jacobian") + run_control(net, enforce_q_lims=True) + assert net.controller.object.at[0].converged + # station 0 saturates + assert net.sgen.q_mvar.at[0] == pytest.approx(0.1, abs=1e-6) + assert net.sgen.q_mvar.at[1] == pytest.approx(0.1, abs=1e-6) + assert net.res_bus.vm_pu.at[1] < 1.02 + # station 1 reaches its set point + assert net.res_bus.vm_pu.at[3] == pytest.approx(1.02, abs=1e-6) + + +def test_qctrl_ignores_jacobian_flag(): + """Q_ctrl stations keep the exact secant iterates under update_method='jacobian'.""" + n = 3 + counts, results = {}, {} + for method in ("secant", "jacobian"): + net = _bare_bench_net(n) + BinarySearchControl.for_stations(net, station_dicts("Q_ctrl", n), name=method, + tol=1e-6, update_method=method) + counter = CountingRunpp() + run_control(net, run=counter) + counts[method] = counter.count + results[method] = net.sgen.q_mvar.values.copy() + assert counts["jacobian"] == counts["secant"] + assert np.allclose(results["jacobian"], results["secant"], atol=1e-12) + + +if __name__ == '__main__': + pytest.main(['-s', __file__]) diff --git a/pandapower/test/control/test_stactrl_multistation.py b/pandapower/test/control/test_stactrl_multistation.py new file mode 100644 index 0000000000..0651fe498f --- /dev/null +++ b/pandapower/test/control/test_stactrl_multistation.py @@ -0,0 +1,233 @@ +# -*- coding: utf-8 -*- + +# Copyright (c) 2016-2026 by University of Kassel and Fraunhofer Institute for Energy Economics +# and Energy System Technology (IEE), Kassel. All rights reserved. + +"""Tests for the multi-station mode of BinarySearchControl (for_stations).""" + +import os + +import numpy as np +import pytest + +from pandapower.control.controller.station_control import BinarySearchControl, DroopControl +from pandapower.control.run_control import run_control +from pandapower.file_io import from_json, to_json +from pandapower.run import runpp +from pandapower.test.control.benchmarks.bench_station_control import build_bench_net +from pandapower.test.control.test_stactrl_characterization import CountingRunpp + + +def _station_dicts(net, mode, n_stations): + """Station dicts matching the controllers that build_bench_net(mode) would create.""" + stations = [] + for k in range(n_stations): + sgen_a, sgen_b = 2 * k, 2 * k + 1 + mv = 1 + 2 * k + outputs = dict(output_element_index=[sgen_a, sgen_b], + output_values_distribution=[0.6, 0.4]) + if mode == "Q_ctrl": + stations.append(dict(control_modus="Q_ctrl", set_point=0.5, + input_element="res_line", input_variable="q_to_mvar", + input_element_index=k, **outputs)) + elif mode == "V_ctrl": + stations.append(dict(control_modus="V_ctrl", set_point=1.02, + input_element="res_bus", input_variable="vm_pu", + input_element_index=mv, **outputs)) + else: # V_ctrl_Q_droop + stations.append(dict(control_modus="V_ctrl_Q_droop", set_point=1.02, + input_element="res_trafo", input_variable="q_hv_mvar", + input_element_index=k, + droop=dict(q_droop_mvar=40, bus_idx=mv), **outputs)) + return stations + + +def _bare_bench_net(n_stations): + """The bench net without any controllers.""" + net = build_bench_net(n_stations, "Q_ctrl") + net.controller.drop(net.controller.index, inplace=True) + return net + + +@pytest.mark.parametrize("mode", ["Q_ctrl", "V_ctrl", "V_ctrl_Q_droop"]) +def test_multistation_equals_n_single_controllers(mode): + """One for_stations instance produces the same result as n single controllers.""" + n = 5 + # reference: n single controllers (legacy path, incl. chained DroopControl) + net_ref = build_bench_net(n, mode) + counter_ref = CountingRunpp() + run_control(net_ref, run=counter_ref) + # one multi-station instance + net = _bare_bench_net(n) + BinarySearchControl.for_stations(net, _station_dicts(net, mode, n), name="multi", tol=1e-6) + counter = CountingRunpp() + run_control(net, run=counter) + assert all(net.controller.object[i].converged for i in net.controller.index) + assert np.allclose(net.res_sgen.q_mvar.values, net_ref.res_sgen.q_mvar.values, atol=1e-4) + assert np.allclose(net.res_bus.vm_pu.values, net_ref.res_bus.vm_pu.values, atol=1e-6) + assert counter.count <= counter_ref.count + 1, ( + f"multi-station needed {counter.count} powerflows, reference {counter_ref.count}") + + +def test_multistation_mixed_modes(): + """Stations with different control modi in one instance.""" + net = _bare_bench_net(3) + stations = [ + dict(control_modus="V_ctrl", set_point=1.02, input_element="res_bus", + input_variable="vm_pu", input_element_index=1, + output_element_index=[0, 1], output_values_distribution=[0.6, 0.4]), + dict(control_modus="Q_ctrl", set_point=0.5, input_element="res_line", + input_variable="q_to_mvar", input_element_index=1, + output_element_index=[2, 3], output_values_distribution=[0.5, 0.5]), + dict(control_modus="tan_phi_ctrl", set_point=0.5, input_element="res_trafo", + input_variable="q_lv_mvar", input_element_index=2, + output_element_index=[4, 5], output_values_distribution=[1, 1]), + ] + ctrl = BinarySearchControl.for_stations(net, stations, name="mixed", tol=1e-6) + run_control(net) + assert ctrl.converged + assert net.res_bus.vm_pu.at[1] == pytest.approx(1.02, abs=1e-6) + assert net.res_line.q_to_mvar.at[1] == pytest.approx(0.5, abs=1e-6) + tan_phi = net.res_trafo.q_lv_mvar.at[2] / net.res_trafo.p_lv_mw.at[2] + assert tan_phi == pytest.approx(0.5, abs=1e-5) + + +def test_multistation_per_station_droop(): + """One station with V droop, one without, in the same instance.""" + net = _bare_bench_net(2) + stations = [ + dict(control_modus="V_ctrl_Q_droop", set_point=1.02, input_element="res_trafo", + input_variable="q_hv_mvar", input_element_index=0, + droop=dict(q_droop_mvar=40, bus_idx=1), + output_element_index=[0, 1], output_values_distribution=[0.6, 0.4]), + dict(control_modus="V_ctrl", set_point=1.02, input_element="res_bus", + input_variable="vm_pu", input_element_index=3, + output_element_index=[2, 3], output_values_distribution=[0.6, 0.4]), + ] + ctrl = BinarySearchControl.for_stations(net, stations, name="droopmix", tol=1e-6) + run_control(net) + assert ctrl.converged + # droop station: vm = set point + q_meas / droop + assert net.res_bus.vm_pu.at[1] == pytest.approx( + 1.02 + net.res_trafo.q_hv_mvar.at[0] / 40, abs=1e-6) + # plain station: vm = set point + assert net.res_bus.vm_pu.at[3] == pytest.approx(1.02, abs=1e-6) + + +def test_multistation_droop_equals_chained_droop(): + """The folded-in droop reproduces the chained BSC+DroopControl result.""" + net_ref = build_bench_net(1, "V_ctrl_Q_droop") # legacy chained pair + run_control(net_ref) + net = _bare_bench_net(1) + BinarySearchControl.for_stations( + net, _station_dicts(net, "V_ctrl_Q_droop", 1), name="folded", tol=1e-6) + run_control(net) + assert np.allclose(net.res_bus.vm_pu.values, net_ref.res_bus.vm_pu.values, atol=1e-6) + assert np.allclose(net.res_sgen.q_mvar.values, net_ref.res_sgen.q_mvar.values, atol=1e-4) + + +def test_multistation_qlims_isolated(): + """Station A hitting its Q limits must not disturb station B.""" + net = _bare_bench_net(2) + net.sgen["min_q_mvar"] = [-0.1, -0.1, -50.0, -50.0] + net.sgen["max_q_mvar"] = [0.1, 0.1, 50.0, 50.0] + stations = [ + dict(control_modus="V_ctrl", set_point=1.02, input_element="res_bus", + input_variable="vm_pu", input_element_index=1, + output_element_index=[0, 1], output_values_distribution=[0.6, 0.4]), + dict(control_modus="Q_ctrl", set_point=0.5, input_element="res_line", + input_variable="q_to_mvar", input_element_index=1, + output_element_index=[2, 3], output_values_distribution=[0.5, 0.5]), + ] + ctrl = BinarySearchControl.for_stations(net, stations, name="lims", tol=1e-6) + run_control(net, enforce_q_lims=True) + assert ctrl.converged + # station A saturates at its limits (target 1.02 unreachable with 0.2 Mvar) + assert net.sgen.q_mvar.at[0] == pytest.approx(0.1, abs=1e-6) + assert net.sgen.q_mvar.at[1] == pytest.approx(0.1, abs=1e-6) + assert net.res_bus.vm_pu.at[1] < 1.02 + # station B unaffected + assert net.res_line.q_to_mvar.at[1] == pytest.approx(0.5, abs=1e-6) + assert net.sgen.q_mvar.at[2] == pytest.approx(net.sgen.q_mvar.at[3], abs=1e-6) + + +def test_multistation_json_roundtrip(tmp_path): + """for_stations controllers survive to_json/from_json and keep solving.""" + net = _bare_bench_net(2) + BinarySearchControl.for_stations(net, _station_dicts(net, "V_ctrl", 2), name="rt", tol=1e-6) + json_file = os.path.join(tmp_path, "multistation.json") + to_json(net, json_file) + net2 = from_json(json_file) + ctrl2 = net2.controller.object.at[0] + assert "_vstate" not in ctrl2.to_dict() + assert len(ctrl2.stations) == 2 + run_control(net2) + assert ctrl2.converged + assert net2.res_bus.vm_pu.at[1] == pytest.approx(1.02, abs=1e-6) + assert net2.res_bus.vm_pu.at[3] == pytest.approx(1.02, abs=1e-6) + + +def test_multistation_partial_convergence(): + """Outputs of already-converged stations stay frozen while others iterate.""" + net = _bare_bench_net(2) + # station 0 starts at its solution (set point = initial measurement), station 1 does not + runpp(net) + q_initial = net.res_line.q_to_mvar.at[0] + stations = [ + dict(control_modus="Q_ctrl", set_point=float(q_initial), input_element="res_line", + input_variable="q_to_mvar", input_element_index=0, + output_element_index=[0, 1], output_values_distribution=[0.5, 0.5]), + dict(control_modus="Q_ctrl", set_point=2.0, input_element="res_line", + input_variable="q_to_mvar", input_element_index=1, + output_element_index=[2, 3], output_values_distribution=[0.5, 0.5]), + ] + ctrl = BinarySearchControl.for_stations(net, stations, name="partial", tol=1e-6) + run_control(net) + assert ctrl.converged + # station 0 was converged from the start and must not have been touched + assert net.sgen.q_mvar.at[0] == 0.0 + assert net.sgen.q_mvar.at[1] == 0.0 + assert net.res_line.q_to_mvar.at[1] == pytest.approx(2.0, abs=1e-6) + + +def test_multistation_out_of_service_station(): + """A station whose outputs are out of service is skipped, the rest keeps working.""" + net = _bare_bench_net(2) + net.sgen.loc[[0, 1], "in_service"] = False + stations = _station_dicts(net, "Q_ctrl", 2) + ctrl = BinarySearchControl.for_stations(net, stations, name="oos", tol=1e-6) + run_control(net) + assert ctrl.converged + assert net.res_line.q_to_mvar.at[1] == pytest.approx(0.5, abs=1e-6) + assert net.sgen.q_mvar.at[0] == 0.0 # untouched + + +def test_multistation_prerefactor_interop(): + """A frozen pre-refactor net (legacy controllers incl. chained droop) extended with a + new for_stations controller converges as a whole.""" + from pandapower.create import create_bus, create_line, create_load, create_sgen, \ + create_transformer + from pandapower.test.control.test_stactrl_characterization import PREREFACTOR_JSON + + net = from_json(PREREFACTOR_JSON) + # add a new feeder controlled by a multi-station controller + mv = create_bus(net, 20) + lv = create_bus(net, 20) + create_transformer(net, 0, mv, "63 MVA 110/20 kV") + create_load(net, mv, 3, 0.1) + new_sgen = create_sgen(net, lv, p_mw=2., sn_mva=10) + create_line(net, mv, lv, length_km=0.1, std_type="NAYY 4x50 SE") + BinarySearchControl.for_stations(net, [ + dict(control_modus="V_ctrl", set_point=1.015, input_element="res_bus", + input_variable="vm_pu", input_element_index=mv, + output_element_index=[new_sgen], output_values_distribution=[1])], + name="new_station", tol=1e-6) + run_control(net) + assert all(net.controller.object[i].converged for i in net.controller.index) + assert net.res_bus.vm_pu.at[mv] == pytest.approx(1.015, abs=1e-6) + # legacy V_ctrl station in the frozen net still reaches its set point + assert net.res_bus.vm_pu.at[1] == pytest.approx(1.02, abs=1e-6) + + +if __name__ == '__main__': + pytest.main(['-s', __file__]) diff --git a/pandapower/test/control/testfiles/stactrl_prerefactor_v1.json b/pandapower/test/control/testfiles/stactrl_prerefactor_v1.json new file mode 100644 index 0000000000..81409045bb --- /dev/null +++ b/pandapower/test/control/testfiles/stactrl_prerefactor_v1.json @@ -0,0 +1,3520 @@ +{ + "_module": "pandapower.auxiliary", + "_class": "pandapowerNet", + "_object": { + "bus": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"vn_kv\",\"type\",\"zone\",\"in_service\",\"geo\"],\"index\":[0,1,2,3,4,5,6,7,8,9,10,11,12],\"data\":[[null,110.0,\"b\",null,true,null],[null,20.0,\"b\",null,true,null],[null,20.0,\"b\",null,true,null],[null,20.0,\"b\",null,true,null],[null,20.0,\"b\",null,true,null],[null,20.0,\"b\",null,true,null],[null,20.0,\"b\",null,true,null],[null,20.0,\"b\",null,true,null],[null,20.0,\"b\",null,true,null],[null,20.0,\"b\",null,true,null],[null,20.0,\"b\",null,true,null],[null,20.0,\"b\",null,true,null],[null,20.0,\"b\",null,true,null]]}", + "orient": "split", + "dtype": { + "name": "object", + "vn_kv": "float64", + "type": "object", + "zone": "object", + "in_service": "bool", + "geo": "object" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "bus_dc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"vn_kv\",\"type\",\"zone\",\"in_service\",\"geo\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "vn_kv": "float64", + "type": "object", + "zone": "object", + "in_service": "bool", + "geo": "object" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "load": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"q_mvar\",\"const_z_p_percent\",\"const_i_p_percent\",\"const_z_q_percent\",\"const_i_q_percent\",\"sn_mva\",\"scaling\",\"in_service\",\"type\"],\"index\":[0,1,2,3,4,5],\"data\":[[null,1,3.0,0.1,0.0,0.0,0.0,0.0,null,1.0,true,\"wye\"],[null,3,3.0,0.1,0.0,0.0,0.0,0.0,null,1.0,true,\"wye\"],[null,5,3.0,0.1,0.0,0.0,0.0,0.0,null,1.0,true,\"wye\"],[null,7,3.0,0.1,0.0,0.0,0.0,0.0,null,1.0,true,\"wye\"],[null,9,3.0,0.1,0.0,0.0,0.0,0.0,null,1.0,true,\"wye\"],[null,11,3.0,0.1,0.0,0.0,0.0,0.0,null,1.0,true,\"wye\"]]}", + "orient": "split", + "dtype": { + "name": "object", + "bus": "uint32", + "p_mw": "float64", + "q_mvar": "float64", + "const_z_p_percent": "float64", + "const_i_p_percent": "float64", + "const_z_q_percent": "float64", + "const_i_q_percent": "float64", + "sn_mva": "float64", + "scaling": "float64", + "in_service": "bool", + "type": "object" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "sgen": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"q_mvar\",\"min_q_mvar\",\"max_q_mvar\",\"sn_mva\",\"scaling\",\"controllable\",\"id_q_capability_characteristic\",\"reactive_capability_curve\",\"curve_style\",\"in_service\",\"type\",\"current_source\"],\"index\":[0,1,2,3,4,5],\"data\":[[null,2,2.0,0.0,null,null,10.0,1.0,false,null,false,null,true,null,true],[null,4,2.0,0.0,null,null,10.0,1.0,false,null,false,null,true,null,true],[null,6,2.0,0.0,null,null,10.0,1.0,false,null,false,null,true,null,true],[null,8,2.0,0.0,null,null,10.0,1.0,false,null,false,null,true,null,true],[null,10,2.0,0.0,null,null,10.0,1.0,false,null,false,null,true,null,true],[null,12,2.0,0.0,null,null,10.0,1.0,false,null,false,null,true,null,true]]}", + "orient": "split", + "dtype": { + "name": "object", + "bus": "int64", + "p_mw": "float64", + "q_mvar": "float64", + "min_q_mvar": "float64", + "max_q_mvar": "float64", + "sn_mva": "float64", + "scaling": "float64", + "controllable": "bool", + "id_q_capability_characteristic": "Int64", + "reactive_capability_curve": "bool", + "curve_style": "object", + "in_service": "bool", + "type": "object", + "current_source": "bool" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "motor": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"pn_mech_mw\",\"loading_percent\",\"cos_phi\",\"cos_phi_n\",\"efficiency_percent\",\"efficiency_n_percent\",\"lrc_pu\",\"vn_kv\",\"scaling\",\"in_service\",\"rx\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "bus": "int64", + "pn_mech_mw": "float64", + "loading_percent": "float64", + "cos_phi": "float64", + "cos_phi_n": "float64", + "efficiency_percent": "float64", + "efficiency_n_percent": "float64", + "lrc_pu": "float64", + "vn_kv": "float64", + "scaling": "float64", + "in_service": "bool", + "rx": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "asymmetric_load": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"p_a_mw\",\"q_a_mvar\",\"p_b_mw\",\"q_b_mvar\",\"p_c_mw\",\"q_c_mvar\",\"sn_a_mva\",\"sn_b_mva\",\"sn_c_mva\",\"sn_mva\",\"scaling\",\"in_service\",\"type\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "bus": "uint32", + "p_a_mw": "float64", + "q_a_mvar": "float64", + "p_b_mw": "float64", + "q_b_mvar": "float64", + "p_c_mw": "float64", + "q_c_mvar": "float64", + "sn_a_mva": "float64", + "sn_b_mva": "float64", + "sn_c_mva": "float64", + "sn_mva": "float64", + "scaling": "float64", + "in_service": "bool", + "type": "object" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "asymmetric_sgen": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"p_a_mw\",\"q_a_mvar\",\"p_b_mw\",\"q_b_mvar\",\"p_c_mw\",\"q_c_mvar\",\"sn_a_mva\",\"sn_b_mva\",\"sn_c_mva\",\"sn_mva\",\"scaling\",\"in_service\",\"type\",\"current_source\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "bus": "int64", + "p_a_mw": "float64", + "q_a_mvar": "float64", + "p_b_mw": "float64", + "q_b_mvar": "float64", + "p_c_mw": "float64", + "q_c_mvar": "float64", + "sn_a_mva": "float64", + "sn_b_mva": "float64", + "sn_c_mva": "float64", + "sn_mva": "float64", + "scaling": "float64", + "in_service": "bool", + "type": "object", + "current_source": "bool" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "storage": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"q_mvar\",\"sn_mva\",\"soc_percent\",\"min_e_mwh\",\"max_e_mwh\",\"scaling\",\"in_service\",\"type\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "bus": "int64", + "p_mw": "float64", + "q_mvar": "float64", + "sn_mva": "float64", + "soc_percent": "float64", + "min_e_mwh": "float64", + "max_e_mwh": "float64", + "scaling": "float64", + "in_service": "bool", + "type": "object" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "gen": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"p_mw\",\"vm_pu\",\"sn_mva\",\"min_q_mvar\",\"max_q_mvar\",\"scaling\",\"slack\",\"controllable\",\"id_q_capability_characteristic\",\"reactive_capability_curve\",\"curve_style\",\"in_service\",\"slack_weight\",\"type\"],\"index\":[0],\"data\":[[null,12,2.0,1.02,10.0,null,null,1.0,false,true,null,false,null,true,0.0,null]]}", + "orient": "split", + "dtype": { + "name": "object", + "bus": "uint32", + "p_mw": "float64", + "vm_pu": "float64", + "sn_mva": "float64", + "min_q_mvar": "float64", + "max_q_mvar": "float64", + "scaling": "float64", + "slack": "bool", + "controllable": "bool", + "id_q_capability_characteristic": "Int64", + "reactive_capability_curve": "bool", + "curve_style": "object", + "in_service": "bool", + "slack_weight": "float64", + "type": "object" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "switch": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"bus\",\"element\",\"et\",\"type\",\"closed\",\"name\",\"z_ohm\",\"in_ka\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "bus": "int64", + "element": "int64", + "et": "object", + "type": "object", + "closed": "bool", + "name": "object", + "z_ohm": "float64", + "in_ka": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "shunt": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"bus\",\"name\",\"q_mvar\",\"p_mw\",\"vn_kv\",\"step\",\"max_step\",\"id_characteristic_table\",\"step_dependency_table\",\"in_service\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "bus": "uint32", + "name": "object", + "q_mvar": "float64", + "p_mw": "float64", + "vn_kv": "float64", + "step": "float64", + "max_step": "uint32", + "id_characteristic_table": "Int64", + "step_dependency_table": "bool", + "in_service": "bool" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "svc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"x_l_ohm\",\"x_cvar_ohm\",\"set_vm_pu\",\"thyristor_firing_angle_degree\",\"controllable\",\"in_service\",\"min_angle_degree\",\"max_angle_degree\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "bus": "uint32", + "x_l_ohm": "float64", + "x_cvar_ohm": "float64", + "set_vm_pu": "float64", + "thyristor_firing_angle_degree": "float64", + "controllable": "bool", + "in_service": "bool", + "min_angle_degree": "float64", + "max_angle_degree": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "ssc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"r_ohm\",\"x_ohm\",\"vm_internal_pu\",\"va_internal_degree\",\"set_vm_pu\",\"controllable\",\"in_service\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "bus": "uint32", + "r_ohm": "float64", + "x_ohm": "float64", + "vm_internal_pu": "float64", + "va_internal_degree": "float64", + "set_vm_pu": "float64", + "controllable": "bool", + "in_service": "bool" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "vsc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"bus_dc\",\"r_ohm\",\"x_ohm\",\"r_dc_ohm\",\"pl_dc_mw\",\"control_mode_ac\",\"control_value_ac\",\"control_mode_dc\",\"control_value_dc\",\"controllable\",\"in_service\",\"ref_bus\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "bus": "uint32", + "bus_dc": "uint32", + "r_ohm": "float64", + "x_ohm": "float64", + "r_dc_ohm": "float64", + "pl_dc_mw": "float64", + "control_mode_ac": "object", + "control_value_ac": "float64", + "control_mode_dc": "object", + "control_value_dc": "float64", + "controllable": "bool", + "in_service": "bool", + "ref_bus": "uint32" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "ext_grid": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"vm_pu\",\"va_degree\",\"slack_weight\",\"in_service\",\"controllable\"],\"index\":[0],\"data\":[[null,0,1.0,0.0,1.0,true,false]]}", + "orient": "split", + "dtype": { + "name": "object", + "bus": "uint32", + "vm_pu": "float64", + "va_degree": "float64", + "slack_weight": "float64", + "in_service": "bool", + "controllable": "bool" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "line": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"std_type\",\"from_bus\",\"to_bus\",\"length_km\",\"r_ohm_per_km\",\"x_ohm_per_km\",\"c_nf_per_km\",\"g_us_per_km\",\"max_i_ka\",\"df\",\"parallel\",\"type\",\"in_service\",\"geo\"],\"index\":[0,1,2,3,4,5],\"data\":[[null,\"NAYY 4x50 SE\",1,2,0.1,0.642,0.083,210.0,0.0,0.142,1.0,1,\"cs\",true,null],[null,\"NAYY 4x50 SE\",3,4,0.1,0.642,0.083,210.0,0.0,0.142,1.0,1,\"cs\",true,null],[null,\"NAYY 4x50 SE\",5,6,0.1,0.642,0.083,210.0,0.0,0.142,1.0,1,\"cs\",true,null],[null,\"NAYY 4x50 SE\",7,8,0.1,0.642,0.083,210.0,0.0,0.142,1.0,1,\"cs\",true,null],[null,\"NAYY 4x50 SE\",9,10,0.1,0.642,0.083,210.0,0.0,0.142,1.0,1,\"cs\",true,null],[null,\"NAYY 4x50 SE\",11,12,0.1,0.642,0.083,210.0,0.0,0.142,1.0,1,\"cs\",true,null]]}", + "orient": "split", + "dtype": { + "name": "object", + "std_type": "object", + "from_bus": "uint32", + "to_bus": "uint32", + "length_km": "float64", + "r_ohm_per_km": "float64", + "x_ohm_per_km": "float64", + "c_nf_per_km": "float64", + "g_us_per_km": "float64", + "max_i_ka": "float64", + "df": "float64", + "parallel": "uint32", + "type": "object", + "in_service": "bool", + "geo": "object" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "line_dc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"std_type\",\"from_bus_dc\",\"to_bus_dc\",\"length_km\",\"r_ohm_per_km\",\"g_us_per_km\",\"max_i_ka\",\"df\",\"parallel\",\"type\",\"in_service\",\"geo\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "std_type": "object", + "from_bus_dc": "uint32", + "to_bus_dc": "uint32", + "length_km": "float64", + "r_ohm_per_km": "float64", + "g_us_per_km": "float64", + "max_i_ka": "float64", + "df": "float64", + "parallel": "uint32", + "type": "object", + "in_service": "bool", + "geo": "object" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "trafo": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"std_type\",\"hv_bus\",\"lv_bus\",\"sn_mva\",\"vn_hv_kv\",\"vn_lv_kv\",\"vk_percent\",\"vkr_percent\",\"pfe_kw\",\"i0_percent\",\"shift_degree\",\"tap_side\",\"tap_neutral\",\"tap_min\",\"tap_max\",\"tap_step_percent\",\"tap_step_degree\",\"tap_pos\",\"tap_changer_type\",\"id_characteristic_table\",\"tap_dependency_table\",\"parallel\",\"df\",\"in_service\",\"vector_group\",\"oltc\"],\"index\":[0,1,2,3,4,5],\"data\":[[null,\"63 MVA 110\\/20 kV\",0,1,63.0,110.0,20.0,18.0,0.32,22.0,0.04,150.0,\"hv\",0.0,-9.0,9.0,1.5,0.0,0.0,\"Ratio\",null,false,1,1.0,true,\"YNd5\",false],[null,\"63 MVA 110\\/20 kV\",0,3,63.0,110.0,20.0,18.0,0.32,22.0,0.04,150.0,\"hv\",0.0,-9.0,9.0,1.5,0.0,0.0,\"Ratio\",null,false,1,1.0,true,\"YNd5\",false],[null,\"63 MVA 110\\/20 kV\",0,5,63.0,110.0,20.0,18.0,0.32,22.0,0.04,150.0,\"hv\",0.0,-9.0,9.0,1.5,0.0,0.0,\"Ratio\",null,false,1,1.0,true,\"YNd5\",false],[null,\"63 MVA 110\\/20 kV\",0,7,63.0,110.0,20.0,18.0,0.32,22.0,0.04,150.0,\"hv\",0.0,-9.0,9.0,1.5,0.0,0.0,\"Ratio\",null,false,1,1.0,true,\"YNd5\",false],[null,\"63 MVA 110\\/20 kV\",0,9,63.0,110.0,20.0,18.0,0.32,22.0,0.04,150.0,\"hv\",0.0,-9.0,9.0,1.5,0.0,0.0,\"Ratio\",null,false,1,1.0,true,\"YNd5\",false],[null,\"63 MVA 110\\/20 kV\",0,11,63.0,110.0,20.0,18.0,0.32,22.0,0.04,150.0,\"hv\",0.0,-9.0,9.0,1.5,0.0,0.0,\"Ratio\",null,false,1,1.0,true,\"YNd5\",false]]}", + "orient": "split", + "dtype": { + "name": "object", + "std_type": "object", + "hv_bus": "uint32", + "lv_bus": "uint32", + "sn_mva": "float64", + "vn_hv_kv": "float64", + "vn_lv_kv": "float64", + "vk_percent": "float64", + "vkr_percent": "float64", + "pfe_kw": "float64", + "i0_percent": "float64", + "shift_degree": "float64", + "tap_side": "object", + "tap_neutral": "float64", + "tap_min": "float64", + "tap_max": "float64", + "tap_step_percent": "float64", + "tap_step_degree": "float64", + "tap_pos": "float64", + "tap_changer_type": "object", + "id_characteristic_table": "Int64", + "tap_dependency_table": "bool", + "parallel": "uint32", + "df": "float64", + "in_service": "bool", + "vector_group": "object", + "oltc": "bool" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "trafo3w": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"std_type\",\"hv_bus\",\"mv_bus\",\"lv_bus\",\"sn_hv_mva\",\"sn_mv_mva\",\"sn_lv_mva\",\"vn_hv_kv\",\"vn_mv_kv\",\"vn_lv_kv\",\"vk_hv_percent\",\"vk_mv_percent\",\"vk_lv_percent\",\"vkr_hv_percent\",\"vkr_mv_percent\",\"vkr_lv_percent\",\"pfe_kw\",\"i0_percent\",\"shift_mv_degree\",\"shift_lv_degree\",\"tap_side\",\"tap_neutral\",\"tap_min\",\"tap_max\",\"tap_step_percent\",\"tap_step_degree\",\"tap_pos\",\"tap_at_star_point\",\"tap_changer_type\",\"id_characteristic_table\",\"tap_dependency_table\",\"in_service\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "std_type": "object", + "hv_bus": "uint32", + "mv_bus": "uint32", + "lv_bus": "uint32", + "sn_hv_mva": "float64", + "sn_mv_mva": "float64", + "sn_lv_mva": "float64", + "vn_hv_kv": "float64", + "vn_mv_kv": "float64", + "vn_lv_kv": "float64", + "vk_hv_percent": "float64", + "vk_mv_percent": "float64", + "vk_lv_percent": "float64", + "vkr_hv_percent": "float64", + "vkr_mv_percent": "float64", + "vkr_lv_percent": "float64", + "pfe_kw": "float64", + "i0_percent": "float64", + "shift_mv_degree": "float64", + "shift_lv_degree": "float64", + "tap_side": "object", + "tap_neutral": "float64", + "tap_min": "float64", + "tap_max": "float64", + "tap_step_percent": "float64", + "tap_step_degree": "float64", + "tap_pos": "float64", + "tap_at_star_point": "bool", + "tap_changer_type": "object", + "id_characteristic_table": "Int64", + "tap_dependency_table": "bool", + "in_service": "bool" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "impedance": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"from_bus\",\"to_bus\",\"rft_pu\",\"xft_pu\",\"rtf_pu\",\"xtf_pu\",\"gf_pu\",\"bf_pu\",\"gt_pu\",\"bt_pu\",\"sn_mva\",\"in_service\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "from_bus": "uint32", + "to_bus": "uint32", + "rft_pu": "float64", + "xft_pu": "float64", + "rtf_pu": "float64", + "xtf_pu": "float64", + "gf_pu": "float64", + "bf_pu": "float64", + "gt_pu": "float64", + "bt_pu": "float64", + "sn_mva": "float64", + "in_service": "bool" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "tcsc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"from_bus\",\"to_bus\",\"x_l_ohm\",\"x_cvar_ohm\",\"set_p_to_mw\",\"thyristor_firing_angle_degree\",\"controllable\",\"in_service\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "from_bus": "uint32", + "to_bus": "uint32", + "x_l_ohm": "float64", + "x_cvar_ohm": "float64", + "set_p_to_mw": "float64", + "thyristor_firing_angle_degree": "float64", + "controllable": "bool", + "in_service": "bool" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "dcline": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"from_bus\",\"to_bus\",\"p_mw\",\"loss_percent\",\"loss_mw\",\"vm_from_pu\",\"vm_to_pu\",\"max_p_mw\",\"min_q_from_mvar\",\"min_q_to_mvar\",\"max_q_from_mvar\",\"max_q_to_mvar\",\"in_service\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "from_bus": "uint32", + "to_bus": "uint32", + "p_mw": "float64", + "loss_percent": "float64", + "loss_mw": "float64", + "vm_from_pu": "float64", + "vm_to_pu": "float64", + "max_p_mw": "float64", + "min_q_from_mvar": "float64", + "min_q_to_mvar": "float64", + "max_q_from_mvar": "float64", + "max_q_to_mvar": "float64", + "in_service": "bool" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "ward": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"ps_mw\",\"qs_mvar\",\"qz_mvar\",\"pz_mw\",\"in_service\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "bus": "uint32", + "ps_mw": "float64", + "qs_mvar": "float64", + "qz_mvar": "float64", + "pz_mw": "float64", + "in_service": "bool" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "xward": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"ps_mw\",\"qs_mvar\",\"qz_mvar\",\"pz_mw\",\"r_ohm\",\"x_ohm\",\"vm_pu\",\"slack_weight\",\"in_service\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "bus": "uint32", + "ps_mw": "float64", + "qs_mvar": "float64", + "qz_mvar": "float64", + "pz_mw": "float64", + "r_ohm": "float64", + "x_ohm": "float64", + "vm_pu": "float64", + "slack_weight": "float64", + "in_service": "bool" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "measurement": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"measurement_type\",\"element_type\",\"element\",\"value\",\"std_dev\",\"side\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "measurement_type": "object", + "element_type": "object", + "element": "uint32", + "value": "float64", + "std_dev": "float64", + "side": "object" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "pwl_cost": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"power_type\",\"element\",\"et\",\"points\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "power_type": "object", + "element": "uint32", + "et": "object", + "points": "object" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "poly_cost": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"element\",\"et\",\"cp0_eur\",\"cp1_eur_per_mw\",\"cp2_eur_per_mw2\",\"cq0_eur\",\"cq1_eur_per_mvar\",\"cq2_eur_per_mvar2\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "element": "uint32", + "et": "object", + "cp0_eur": "float64", + "cp1_eur_per_mw": "float64", + "cp2_eur_per_mw2": "float64", + "cq0_eur": "float64", + "cq1_eur_per_mvar": "float64", + "cq2_eur_per_mvar2": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "controller": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"object\",\"in_service\",\"order\",\"level\",\"initial_run\",\"recycle\",\"name\"],\"index\":[0,1,2,3,4,5,6,7],\"data\":[[{\"_module\":\"pandapower.control.controller.station_control\",\"_class\":\"BinarySearchControl\",\"_object\":\"{\\\"index\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"int64\\\", \\\"_object\\\": 0}, \\\"matching_params\\\": {}, \\\"name\\\": \\\"v_ctrl\\\", \\\"set_point\\\": 1.02, \\\"tol\\\": 1e-06, \\\"input_sign\\\": [1], \\\"input_variable\\\": [\\\"vm_pu\\\"], \\\"input_variable_p\\\": [], \\\"input_element_in_service\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"bool\\\", \\\"_object\\\": \\\"true\\\"}], \\\"input_element_index\\\": [1], \\\"in_service\\\": true, \\\"input_element\\\": \\\"res_bus\\\", \\\"output_values\\\": null, \\\"output_values_old\\\": null, \\\"output_element\\\": \\\"sgen\\\", \\\"output_values_distribution\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"array\\\", \\\"_object\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"float64\\\", \\\"_object\\\": 1.0}], \\\"dtype\\\": \\\"float64\\\"}, \\\"diff\\\": null, \\\"diff_old\\\": null, \\\"converged\\\": false, \\\"redistribute_values\\\": null, \\\"counter_warning\\\": false, \\\"read_flag\\\": [\\\"single_index\\\"], \\\"write_flag\\\": \\\"loc\\\", \\\"output_variable\\\": \\\"q_mvar\\\", \\\"output_element_index\\\": [0], \\\"output_element_in_service\\\": [true], \\\"output_adjustable\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"array\\\", \\\"_object\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"bool\\\", \\\"_object\\\": \\\"true\\\"}], \\\"dtype\\\": \\\"bool\\\"}, \\\"control_modus\\\": {\\\"_module\\\": \\\"pandapower.control.controller.station_control\\\", \\\"_class\\\": \\\"ControlModusEnum\\\", \\\"_object\\\": \\\"V_ctrl\\\"}, \\\"reactance\\\": 1}\"},true,0.0,0,true,false,null],[{\"_module\":\"pandapower.control.controller.station_control\",\"_class\":\"BinarySearchControl\",\"_object\":\"{\\\"index\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"int64\\\", \\\"_object\\\": 1}, \\\"matching_params\\\": {}, \\\"damping_factor\\\": 0.9, \\\"name\\\": \\\"q_ctrl\\\", \\\"set_point\\\": 0.5, \\\"tol\\\": 1e-06, \\\"input_sign\\\": [1], \\\"input_variable\\\": [\\\"q_to_mvar\\\"], \\\"input_variable_p\\\": [], \\\"input_element_in_service\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"bool\\\", \\\"_object\\\": \\\"true\\\"}], \\\"input_element_index\\\": [1], \\\"in_service\\\": true, \\\"input_element\\\": \\\"res_line\\\", \\\"output_values\\\": null, \\\"output_values_old\\\": null, \\\"output_element\\\": \\\"sgen\\\", \\\"output_values_distribution\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"array\\\", \\\"_object\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"float64\\\", \\\"_object\\\": 1.0}], \\\"dtype\\\": \\\"float64\\\"}, \\\"diff\\\": null, \\\"diff_old\\\": null, \\\"converged\\\": false, \\\"redistribute_values\\\": null, \\\"counter_warning\\\": false, \\\"read_flag\\\": [\\\"single_index\\\"], \\\"write_flag\\\": \\\"loc\\\", \\\"output_variable\\\": \\\"q_mvar\\\", \\\"output_element_index\\\": [1], \\\"output_element_in_service\\\": [true], \\\"output_adjustable\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"array\\\", \\\"_object\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"bool\\\", \\\"_object\\\": \\\"true\\\"}], \\\"dtype\\\": \\\"bool\\\"}, \\\"control_modus\\\": {\\\"_module\\\": \\\"pandapower.control.controller.station_control\\\", \\\"_class\\\": \\\"ControlModusEnum\\\", \\\"_object\\\": \\\"Q_ctrl\\\"}, \\\"reactance\\\": 1}\"},true,0.0,0,true,false,null],[{\"_module\":\"pandapower.control.controller.station_control\",\"_class\":\"BinarySearchControl\",\"_object\":\"{\\\"index\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"int64\\\", \\\"_object\\\": 2}, \\\"matching_params\\\": {}, \\\"damping_factor\\\": 0.9, \\\"name\\\": \\\"pf_ctrl\\\", \\\"set_point\\\": 0.9, \\\"tol\\\": 1e-06, \\\"input_sign\\\": [1], \\\"input_variable\\\": [\\\"q_to_mvar\\\"], \\\"input_variable_p\\\": [\\\"p_to_mw\\\"], \\\"input_element_in_service\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"bool\\\", \\\"_object\\\": \\\"true\\\"}], \\\"input_element_index\\\": [2], \\\"in_service\\\": true, \\\"input_element\\\": \\\"res_line\\\", \\\"output_values\\\": null, \\\"output_values_old\\\": null, \\\"output_element\\\": \\\"sgen\\\", \\\"output_values_distribution\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"array\\\", \\\"_object\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"float64\\\", \\\"_object\\\": 1.0}], \\\"dtype\\\": \\\"float64\\\"}, \\\"diff\\\": null, \\\"diff_old\\\": null, \\\"converged\\\": false, \\\"redistribute_values\\\": null, \\\"counter_warning\\\": false, \\\"read_flag\\\": [\\\"single_index\\\"], \\\"write_flag\\\": \\\"single_index\\\", \\\"output_variable\\\": \\\"q_mvar\\\", \\\"output_element_index\\\": 2, \\\"output_element_in_service\\\": true, \\\"output_adjustable\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"array\\\", \\\"_object\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"bool\\\", \\\"_object\\\": \\\"true\\\"}], \\\"dtype\\\": \\\"bool\\\"}, \\\"control_modus\\\": {\\\"_module\\\": \\\"pandapower.control.controller.station_control\\\", \\\"_class\\\": \\\"ControlModusEnum\\\", \\\"_object\\\": \\\"PF_ctrl_ind\\\"}, \\\"reactance\\\": 1}\"},true,0.0,0,true,false,null],[{\"_module\":\"pandapower.control.controller.station_control\",\"_class\":\"BinarySearchControl\",\"_object\":\"{\\\"index\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"int64\\\", \\\"_object\\\": 3}, \\\"matching_params\\\": {}, \\\"name\\\": \\\"tan_phi\\\", \\\"set_point\\\": 0.5, \\\"tol\\\": 1e-06, \\\"input_sign\\\": [1], \\\"input_variable\\\": [\\\"q_lv_mvar\\\"], \\\"input_variable_p\\\": [\\\"p_lv_mw\\\"], \\\"input_element_in_service\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"bool\\\", \\\"_object\\\": \\\"true\\\"}], \\\"input_element_index\\\": [3], \\\"in_service\\\": true, \\\"input_element\\\": \\\"res_trafo\\\", \\\"output_values\\\": null, \\\"output_values_old\\\": null, \\\"output_element\\\": \\\"sgen\\\", \\\"output_values_distribution\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"array\\\", \\\"_object\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"float64\\\", \\\"_object\\\": 1.0}], \\\"dtype\\\": \\\"float64\\\"}, \\\"diff\\\": null, \\\"diff_old\\\": null, \\\"converged\\\": false, \\\"redistribute_values\\\": null, \\\"counter_warning\\\": false, \\\"read_flag\\\": [\\\"single_index\\\"], \\\"write_flag\\\": \\\"single_index\\\", \\\"output_variable\\\": \\\"q_mvar\\\", \\\"output_element_index\\\": 3, \\\"output_element_in_service\\\": true, \\\"output_adjustable\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"array\\\", \\\"_object\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"bool\\\", \\\"_object\\\": \\\"true\\\"}], \\\"dtype\\\": \\\"bool\\\"}, \\\"control_modus\\\": {\\\"_module\\\": \\\"pandapower.control.controller.station_control\\\", \\\"_class\\\": \\\"ControlModusEnum\\\", \\\"_object\\\": \\\"tan_phi_ctrl\\\"}, \\\"reactance\\\": 1}\"},true,0.0,0,true,false,null],[{\"_module\":\"pandapower.control.controller.station_control\",\"_class\":\"BinarySearchControl\",\"_object\":\"{\\\"index\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"int64\\\", \\\"_object\\\": 4}, \\\"matching_params\\\": {}, \\\"bus_idx\\\": 9, \\\"name\\\": \\\"v_droop_bsc\\\", \\\"set_point\\\": 1.02, \\\"tol\\\": 1e-06, \\\"input_sign\\\": [1], \\\"input_variable\\\": [\\\"q_hv_mvar\\\"], \\\"input_variable_p\\\": [], \\\"input_element_in_service\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"bool\\\", \\\"_object\\\": \\\"true\\\"}], \\\"input_element_index\\\": [4], \\\"in_service\\\": true, \\\"input_element\\\": \\\"res_trafo\\\", \\\"output_values\\\": null, \\\"output_values_old\\\": null, \\\"output_element\\\": \\\"sgen\\\", \\\"output_values_distribution\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"array\\\", \\\"_object\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"float64\\\", \\\"_object\\\": 1.0}], \\\"dtype\\\": \\\"float64\\\"}, \\\"diff\\\": null, \\\"diff_old\\\": null, \\\"converged\\\": false, \\\"redistribute_values\\\": null, \\\"counter_warning\\\": false, \\\"read_flag\\\": [\\\"single_index\\\"], \\\"write_flag\\\": \\\"loc\\\", \\\"output_variable\\\": \\\"q_mvar\\\", \\\"output_element_index\\\": [4], \\\"output_element_in_service\\\": [true], \\\"output_adjustable\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"array\\\", \\\"_object\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"bool\\\", \\\"_object\\\": \\\"true\\\"}], \\\"dtype\\\": \\\"bool\\\"}, \\\"control_modus\\\": {\\\"_module\\\": \\\"pandapower.control.controller.station_control\\\", \\\"_class\\\": \\\"ControlModusEnum\\\", \\\"_object\\\": \\\"V_ctrl_Q_droop\\\"}, \\\"reactance\\\": 1}\"},true,0.0,0,true,false,null],[{\"_module\":\"pandapower.control.controller.station_control\",\"_class\":\"DroopControl\",\"_object\":\"{\\\"index\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"int64\\\", \\\"_object\\\": 5}, \\\"matching_params\\\": {}, \\\"name\\\": \\\"v_droop\\\", \\\"vm_set_pu\\\": 1.02, \\\"q_droop_mvar\\\": 40, \\\"bus_idx\\\": 9, \\\"vm_pu\\\": null, \\\"vm_pu_old\\\": null, \\\"vm_set_pu_bsc\\\": 1.02, \\\"vm_set_pu_new\\\": null, \\\"lb_voltage\\\": null, \\\"ub_voltage\\\": null, \\\"controller_idx\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"int64\\\", \\\"_object\\\": 4}, \\\"control_modus\\\": {\\\"_module\\\": \\\"pandapower.control.controller.station_control\\\", \\\"_class\\\": \\\"ControlModusEnum\\\", \\\"_object\\\": \\\"V_ctrl_Q_droop\\\"}, \\\"tol\\\": 1e-06, \\\"applied\\\": false, \\\"read_flag\\\": \\\"single_index\\\", \\\"input_variable\\\": \\\"vm_pu\\\", \\\"q_set_mvar_bsc\\\": null, \\\"q_set_mvar\\\": null, \\\"q_set_old_mvar\\\": null, \\\"diff\\\": null, \\\"converged\\\": false, \\\"_deprecation_warned\\\": false}\"},true,-1.0,0,true,false,null],[{\"_module\":\"pandapower.control.controller.station_control\",\"_class\":\"BinarySearchControl\",\"_object\":\"{\\\"index\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"int64\\\", \\\"_object\\\": 6}, \\\"matching_params\\\": {}, \\\"bus_idx\\\": 12, \\\"name\\\": \\\"v_droop_local_bsc\\\", \\\"set_point\\\": 1.0, \\\"tol\\\": 1e-05, \\\"input_sign\\\": [1], \\\"input_variable\\\": [\\\"q_mvar\\\"], \\\"input_variable_p\\\": [], \\\"input_element_in_service\\\": [true], \\\"input_element_index\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"int64\\\", \\\"_object\\\": 0}], \\\"in_service\\\": true, \\\"input_element\\\": \\\"res_gen\\\", \\\"output_values\\\": null, \\\"output_values_old\\\": null, \\\"output_element\\\": \\\"gen\\\", \\\"output_values_distribution\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"array\\\", \\\"_object\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"float64\\\", \\\"_object\\\": 1.0}], \\\"dtype\\\": \\\"float64\\\"}, \\\"diff\\\": null, \\\"diff_old\\\": null, \\\"converged\\\": false, \\\"redistribute_values\\\": null, \\\"counter_warning\\\": false, \\\"read_flag\\\": [\\\"single_index\\\"], \\\"write_flag\\\": \\\"loc\\\", \\\"output_variable\\\": \\\"vm_pu\\\", \\\"output_element_index\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"int64\\\", \\\"_object\\\": 0}], \\\"output_element_in_service\\\": [true], \\\"output_adjustable\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"array\\\", \\\"_object\\\": [{\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"bool\\\", \\\"_object\\\": \\\"true\\\"}], \\\"dtype\\\": \\\"bool\\\"}, \\\"control_modus\\\": {\\\"_module\\\": \\\"pandapower.control.controller.station_control\\\", \\\"_class\\\": \\\"ControlModusEnum\\\", \\\"_object\\\": \\\"V_ctrl_Q_droop_local\\\"}, \\\"reactance\\\": 1}\"},true,0.0,0,true,false,null],[{\"_module\":\"pandapower.control.controller.station_control\",\"_class\":\"VDroopControl_local\",\"_object\":\"{\\\"index\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"int64\\\", \\\"_object\\\": 7}, \\\"matching_params\\\": {}, \\\"name\\\": \\\"\\\", \\\"q_droop_mvar\\\": 20, \\\"vm_pu\\\": null, \\\"vm_pu_old\\\": null, \\\"vm_set_pu_bsc\\\": 1.0, \\\"vm_set_pu_new\\\": null, \\\"q_set_mvar\\\": 0.5, \\\"lb_voltage\\\": null, \\\"ub_voltage\\\": null, \\\"controller_idx\\\": {\\\"_module\\\": \\\"numpy\\\", \\\"_class\\\": \\\"int64\\\", \\\"_object\\\": 6}, \\\"bus_idx\\\": 12, \\\"control_modus\\\": {\\\"_module\\\": \\\"pandapower.control.controller.station_control\\\", \\\"_class\\\": \\\"ControlModusEnum\\\", \\\"_object\\\": \\\"V_ctrl_Q_droop_local\\\"}, \\\"tol\\\": 1e-05, \\\"applied\\\": false, \\\"read_flag\\\": \\\"single_index\\\", \\\"input_variable\\\": \\\"vm_pu\\\", \\\"diff\\\": null, \\\"converged\\\": false}\"},true,-1.0,0,true,false,null]]}", + "orient": "split", + "dtype": { + "object": "object", + "in_service": "bool", + "order": "float64", + "level": "object", + "initial_run": "bool", + "recycle": "object", + "name": "object" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "group": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"element_type\",\"element_index\",\"reference_column\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "element_type": "object", + "element_index": "object", + "reference_column": "object" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "source_dc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus_dc\",\"vm_pu\",\"in_service\",\"type\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "bus_dc": "uint32", + "vm_pu": "float64", + "in_service": "bool", + "type": "object" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "load_dc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus_dc\",\"p_dc_mw\",\"scaling\",\"in_service\",\"controllable\",\"type\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "bus_dc": "uint32", + "p_dc_mw": "float64", + "scaling": "float64", + "in_service": "bool", + "controllable": "bool", + "type": "object" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "vsc_stacked": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"bus_dc_plus\",\"bus_dc_minus\",\"r_ohm\",\"x_ohm\",\"r_dc_ohm\",\"pl_dc_mw\",\"control_mode_ac\",\"control_value_ac\",\"control_mode_dc\",\"control_value_dc\",\"controllable\",\"in_service\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "bus": "uint32", + "bus_dc_plus": "uint32", + "bus_dc_minus": "uint32", + "r_ohm": "float64", + "x_ohm": "float64", + "r_dc_ohm": "float64", + "pl_dc_mw": "float64", + "control_mode_ac": "object", + "control_value_ac": "float64", + "control_mode_dc": "object", + "control_value_dc": "float64", + "controllable": "bool", + "in_service": "bool" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "vsc_bipolar": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"name\",\"bus\",\"bus_dc_plus\",\"bus_dc_minus\",\"r_ohm\",\"x_ohm\",\"r_dc_ohm\",\"pl_dc_mw\",\"control_mode\",\"control_value_1\",\"control_value_2\",\"controllable\",\"in_service\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "name": "object", + "bus": "uint32", + "bus_dc_plus": "uint32", + "bus_dc_minus": "uint32", + "r_ohm": "float64", + "x_ohm": "float64", + "r_dc_ohm": "float64", + "pl_dc_mw": "float64", + "control_mode": "object", + "control_value_1": "float64", + "control_value_2": "float64", + "controllable": "bool", + "in_service": "bool" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "version": "3.4.0", + "format_version": "3.1.0", + "converged": false, + "OPF_converged": false, + "name": "", + "f_hz": 50.0, + "sn_mva": 1, + "std_types": { + "line": { + "NAYY 4x50 SE": { + "c_nf_per_km": 210, + "r_ohm_per_km": 0.642, + "x_ohm_per_km": 0.083, + "max_i_ka": 0.142, + "type": "cs", + "q_mm2": 50, + "alpha": 0.00403, + "voltage_rating": "LV" + }, + "NAYY 4x120 SE": { + "c_nf_per_km": 264, + "r_ohm_per_km": 0.225, + "x_ohm_per_km": 0.08, + "max_i_ka": 0.242, + "type": "cs", + "q_mm2": 120, + "alpha": 0.00403, + "voltage_rating": "LV" + }, + "NAYY 4x150 SE": { + "c_nf_per_km": 261, + "r_ohm_per_km": 0.208, + "x_ohm_per_km": 0.08, + "max_i_ka": 0.27, + "type": "cs", + "q_mm2": 150, + "alpha": 0.00403, + "voltage_rating": "LV" + }, + "NA2XS2Y 1x95 RM/25 12/20 kV": { + "c_nf_per_km": 216, + "r_ohm_per_km": 0.313, + "x_ohm_per_km": 0.132, + "max_i_ka": 0.252, + "type": "cs", + "q_mm2": 95, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "NA2XS2Y 1x185 RM/25 12/20 kV": { + "c_nf_per_km": 273, + "r_ohm_per_km": 0.161, + "x_ohm_per_km": 0.117, + "max_i_ka": 0.362, + "type": "cs", + "q_mm2": 185, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "NA2XS2Y 1x240 RM/25 12/20 kV": { + "c_nf_per_km": 304, + "r_ohm_per_km": 0.122, + "x_ohm_per_km": 0.112, + "max_i_ka": 0.421, + "type": "cs", + "q_mm2": 240, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "NA2XS2Y 1x95 RM/25 6/10 kV": { + "c_nf_per_km": 315, + "r_ohm_per_km": 0.313, + "x_ohm_per_km": 0.123, + "max_i_ka": 0.249, + "type": "cs", + "q_mm2": 95, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "NA2XS2Y 1x185 RM/25 6/10 kV": { + "c_nf_per_km": 406, + "r_ohm_per_km": 0.161, + "x_ohm_per_km": 0.11, + "max_i_ka": 0.358, + "type": "cs", + "q_mm2": 185, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "NA2XS2Y 1x240 RM/25 6/10 kV": { + "c_nf_per_km": 456, + "r_ohm_per_km": 0.122, + "x_ohm_per_km": 0.105, + "max_i_ka": 0.416, + "type": "cs", + "q_mm2": 240, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "NA2XS2Y 1x150 RM/25 12/20 kV": { + "c_nf_per_km": 250, + "r_ohm_per_km": 0.206, + "x_ohm_per_km": 0.116, + "max_i_ka": 0.319, + "type": "cs", + "q_mm2": 150, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "NA2XS2Y 1x120 RM/25 12/20 kV": { + "c_nf_per_km": 230, + "r_ohm_per_km": 0.253, + "x_ohm_per_km": 0.119, + "max_i_ka": 0.283, + "type": "cs", + "q_mm2": 120, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "NA2XS2Y 1x70 RM/25 12/20 kV": { + "c_nf_per_km": 190, + "r_ohm_per_km": 0.443, + "x_ohm_per_km": 0.132, + "max_i_ka": 0.22, + "type": "cs", + "q_mm2": 70, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "NA2XS2Y 1x150 RM/25 6/10 kV": { + "c_nf_per_km": 360, + "r_ohm_per_km": 0.206, + "x_ohm_per_km": 0.11, + "max_i_ka": 0.315, + "type": "cs", + "q_mm2": 150, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "NA2XS2Y 1x120 RM/25 6/10 kV": { + "c_nf_per_km": 340, + "r_ohm_per_km": 0.253, + "x_ohm_per_km": 0.113, + "max_i_ka": 0.28, + "type": "cs", + "q_mm2": 120, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "NA2XS2Y 1x70 RM/25 6/10 kV": { + "c_nf_per_km": 280, + "r_ohm_per_km": 0.443, + "x_ohm_per_km": 0.123, + "max_i_ka": 0.217, + "type": "cs", + "q_mm2": 70, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "N2XS(FL)2Y 1x120 RM/35 64/110 kV": { + "c_nf_per_km": 112, + "r_ohm_per_km": 0.153, + "x_ohm_per_km": 0.166, + "max_i_ka": 0.366, + "type": "cs", + "q_mm2": 120, + "alpha": 0.00393, + "voltage_rating": "HV" + }, + "N2XS(FL)2Y 1x185 RM/35 64/110 kV": { + "c_nf_per_km": 125, + "r_ohm_per_km": 0.099, + "x_ohm_per_km": 0.156, + "max_i_ka": 0.457, + "type": "cs", + "q_mm2": 185, + "alpha": 0.00393, + "voltage_rating": "HV" + }, + "N2XS(FL)2Y 1x240 RM/35 64/110 kV": { + "c_nf_per_km": 135, + "r_ohm_per_km": 0.075, + "x_ohm_per_km": 0.149, + "max_i_ka": 0.526, + "type": "cs", + "q_mm2": 240, + "alpha": 0.00393, + "voltage_rating": "HV" + }, + "N2XS(FL)2Y 1x300 RM/35 64/110 kV": { + "c_nf_per_km": 144, + "r_ohm_per_km": 0.06, + "x_ohm_per_km": 0.144, + "max_i_ka": 0.588, + "type": "cs", + "q_mm2": 300, + "alpha": 0.00393, + "voltage_rating": "HV" + }, + "15-AL1/3-ST1A 0.4": { + "c_nf_per_km": 11, + "r_ohm_per_km": 1.8769, + "x_ohm_per_km": 0.35, + "max_i_ka": 0.105, + "type": "ol", + "q_mm2": 16, + "alpha": 0.00403, + "voltage_rating": "LV" + }, + "24-AL1/4-ST1A 0.4": { + "c_nf_per_km": 11.25, + "r_ohm_per_km": 1.2012, + "x_ohm_per_km": 0.335, + "max_i_ka": 0.14, + "type": "ol", + "q_mm2": 24, + "alpha": 0.00403, + "voltage_rating": "LV" + }, + "48-AL1/8-ST1A 0.4": { + "c_nf_per_km": 12.2, + "r_ohm_per_km": 0.5939, + "x_ohm_per_km": 0.3, + "max_i_ka": 0.21, + "type": "ol", + "q_mm2": 48, + "alpha": 0.00403, + "voltage_rating": "LV" + }, + "94-AL1/15-ST1A 0.4": { + "c_nf_per_km": 13.2, + "r_ohm_per_km": 0.306, + "x_ohm_per_km": 0.29, + "max_i_ka": 0.35, + "type": "ol", + "q_mm2": 94, + "alpha": 0.00403, + "voltage_rating": "LV" + }, + "34-AL1/6-ST1A 10.0": { + "c_nf_per_km": 9.7, + "r_ohm_per_km": 0.8342, + "x_ohm_per_km": 0.36, + "max_i_ka": 0.17, + "type": "ol", + "q_mm2": 34, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "48-AL1/8-ST1A 10.0": { + "c_nf_per_km": 10.1, + "r_ohm_per_km": 0.5939, + "x_ohm_per_km": 0.35, + "max_i_ka": 0.21, + "type": "ol", + "q_mm2": 48, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "70-AL1/11-ST1A 10.0": { + "c_nf_per_km": 10.4, + "r_ohm_per_km": 0.4132, + "x_ohm_per_km": 0.339, + "max_i_ka": 0.29, + "type": "ol", + "q_mm2": 70, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "94-AL1/15-ST1A 10.0": { + "c_nf_per_km": 10.75, + "r_ohm_per_km": 0.306, + "x_ohm_per_km": 0.33, + "max_i_ka": 0.35, + "type": "ol", + "q_mm2": 94, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "122-AL1/20-ST1A 10.0": { + "c_nf_per_km": 11.1, + "r_ohm_per_km": 0.2376, + "x_ohm_per_km": 0.323, + "max_i_ka": 0.41, + "type": "ol", + "q_mm2": 122, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "149-AL1/24-ST1A 10.0": { + "c_nf_per_km": 11.25, + "r_ohm_per_km": 0.194, + "x_ohm_per_km": 0.315, + "max_i_ka": 0.47, + "type": "ol", + "q_mm2": 149, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "34-AL1/6-ST1A 20.0": { + "c_nf_per_km": 9.15, + "r_ohm_per_km": 0.8342, + "x_ohm_per_km": 0.382, + "max_i_ka": 0.17, + "type": "ol", + "q_mm2": 34, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "48-AL1/8-ST1A 20.0": { + "c_nf_per_km": 9.5, + "r_ohm_per_km": 0.5939, + "x_ohm_per_km": 0.372, + "max_i_ka": 0.21, + "type": "ol", + "q_mm2": 48, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "70-AL1/11-ST1A 20.0": { + "c_nf_per_km": 9.7, + "r_ohm_per_km": 0.4132, + "x_ohm_per_km": 0.36, + "max_i_ka": 0.29, + "type": "ol", + "q_mm2": 70, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "94-AL1/15-ST1A 20.0": { + "c_nf_per_km": 10, + "r_ohm_per_km": 0.306, + "x_ohm_per_km": 0.35, + "max_i_ka": 0.35, + "type": "ol", + "q_mm2": 94, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "122-AL1/20-ST1A 20.0": { + "c_nf_per_km": 10.3, + "r_ohm_per_km": 0.2376, + "x_ohm_per_km": 0.344, + "max_i_ka": 0.41, + "type": "ol", + "q_mm2": 122, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "149-AL1/24-ST1A 20.0": { + "c_nf_per_km": 10.5, + "r_ohm_per_km": 0.194, + "x_ohm_per_km": 0.337, + "max_i_ka": 0.47, + "type": "ol", + "q_mm2": 149, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "184-AL1/30-ST1A 20.0": { + "c_nf_per_km": 10.75, + "r_ohm_per_km": 0.1571, + "x_ohm_per_km": 0.33, + "max_i_ka": 0.535, + "type": "ol", + "q_mm2": 184, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "243-AL1/39-ST1A 20.0": { + "c_nf_per_km": 11, + "r_ohm_per_km": 0.1188, + "x_ohm_per_km": 0.32, + "max_i_ka": 0.645, + "type": "ol", + "q_mm2": 243, + "alpha": 0.00403, + "voltage_rating": "MV" + }, + "48-AL1/8-ST1A 110.0": { + "c_nf_per_km": 8, + "r_ohm_per_km": 0.5939, + "x_ohm_per_km": 0.46, + "max_i_ka": 0.21, + "type": "ol", + "q_mm2": 48, + "alpha": 0.00403, + "voltage_rating": "HV" + }, + "70-AL1/11-ST1A 110.0": { + "c_nf_per_km": 8.4, + "r_ohm_per_km": 0.4132, + "x_ohm_per_km": 0.45, + "max_i_ka": 0.29, + "type": "ol", + "q_mm2": 70, + "alpha": 0.00403, + "voltage_rating": "HV" + }, + "94-AL1/15-ST1A 110.0": { + "c_nf_per_km": 8.65, + "r_ohm_per_km": 0.306, + "x_ohm_per_km": 0.44, + "max_i_ka": 0.35, + "type": "ol", + "q_mm2": 94, + "alpha": 0.00403, + "voltage_rating": "HV" + }, + "122-AL1/20-ST1A 110.0": { + "c_nf_per_km": 8.5, + "r_ohm_per_km": 0.2376, + "x_ohm_per_km": 0.43, + "max_i_ka": 0.41, + "type": "ol", + "q_mm2": 122, + "alpha": 0.00403, + "voltage_rating": "HV" + }, + "149-AL1/24-ST1A 110.0": { + "c_nf_per_km": 8.75, + "r_ohm_per_km": 0.194, + "x_ohm_per_km": 0.41, + "max_i_ka": 0.47, + "type": "ol", + "q_mm2": 149, + "alpha": 0.00403, + "voltage_rating": "HV" + }, + "184-AL1/30-ST1A 110.0": { + "c_nf_per_km": 8.8, + "r_ohm_per_km": 0.1571, + "x_ohm_per_km": 0.4, + "max_i_ka": 0.535, + "type": "ol", + "q_mm2": 184, + "alpha": 0.00403, + "voltage_rating": "HV" + }, + "243-AL1/39-ST1A 110.0": { + "c_nf_per_km": 9, + "r_ohm_per_km": 0.1188, + "x_ohm_per_km": 0.39, + "max_i_ka": 0.645, + "type": "ol", + "q_mm2": 243, + "alpha": 0.00403, + "voltage_rating": "HV" + }, + "305-AL1/39-ST1A 110.0": { + "c_nf_per_km": 9.2, + "r_ohm_per_km": 0.0949, + "x_ohm_per_km": 0.38, + "max_i_ka": 0.74, + "type": "ol", + "q_mm2": 305, + "alpha": 0.00403, + "voltage_rating": "HV" + }, + "490-AL1/64-ST1A 110.0": { + "c_nf_per_km": 9.75, + "r_ohm_per_km": 0.059, + "x_ohm_per_km": 0.37, + "max_i_ka": 0.96, + "type": "ol", + "q_mm2": 490, + "alpha": 0.00403, + "voltage_rating": "HV" + }, + "679-AL1/86-ST1A 110.0": { + "c_nf_per_km": 9.95, + "r_ohm_per_km": 0.042, + "x_ohm_per_km": 0.36, + "max_i_ka": 1.15, + "type": "ol", + "q_mm2": 679, + "alpha": 0.00403, + "voltage_rating": "HV" + }, + "490-AL1/64-ST1A 220.0": { + "c_nf_per_km": 10, + "r_ohm_per_km": 0.059, + "x_ohm_per_km": 0.285, + "max_i_ka": 0.96, + "type": "ol", + "q_mm2": 490, + "alpha": 0.00403, + "voltage_rating": "HV" + }, + "679-AL1/86-ST1A 220.0": { + "c_nf_per_km": 11.7, + "r_ohm_per_km": 0.042, + "x_ohm_per_km": 0.275, + "max_i_ka": 1.15, + "type": "ol", + "q_mm2": 679, + "alpha": 0.00403, + "voltage_rating": "HV" + }, + "490-AL1/64-ST1A 380.0": { + "c_nf_per_km": 11, + "r_ohm_per_km": 0.059, + "x_ohm_per_km": 0.253, + "max_i_ka": 0.96, + "type": "ol", + "q_mm2": 490, + "alpha": 0.00403, + "voltage_rating": "HV" + }, + "679-AL1/86-ST1A 380.0": { + "c_nf_per_km": 14.6, + "r_ohm_per_km": 0.042, + "x_ohm_per_km": 0.25, + "max_i_ka": 1.15, + "type": "ol", + "q_mm2": 679, + "alpha": 0.00403, + "voltage_rating": "HV" + } + }, + "line_dc": { + "95-CU": { + "r_ohm_per_km": 0.193, + "max_i_ka": 0.404, + "type": "cs", + "q_mm2": 95, + "alpha": 0.00393 + }, + "400-CU": { + "r_ohm_per_km": 0.047, + "max_i_ka": 0.922, + "type": "cs", + "q_mm2": 400, + "alpha": 0.00393 + }, + "1200-CU": { + "r_ohm_per_km": 0.0151, + "max_i_ka": 1.791, + "type": "cs", + "q_mm2": 1200, + "alpha": 0.00393 + }, + "2400-CU": { + "r_ohm_per_km": 0.0073, + "max_i_ka": 2.678, + "type": "cs", + "q_mm2": 2400, + "alpha": 0.00393 + }, + "95-AL": { + "r_ohm_per_km": 0.32, + "max_i_ka": 0.31, + "type": "cs", + "q_mm2": 95, + "alpha": 0.00403 + }, + "400-AL": { + "r_ohm_per_km": 0.0778, + "max_i_ka": 0.705, + "type": "cs", + "q_mm2": 400, + "alpha": 0.00403 + }, + "1200-AL": { + "r_ohm_per_km": 0.0247, + "max_i_ka": 1.371, + "type": "cs", + "q_mm2": 1200, + "alpha": 0.00403 + }, + "2400-AL": { + "r_ohm_per_km": 0.0121, + "max_i_ka": 2.066, + "type": "cs", + "q_mm2": 2400, + "alpha": 0.00403 + } + }, + "trafo": { + "160 MVA 380/110 kV": { + "i0_percent": 0.06, + "pfe_kw": 60, + "vkr_percent": 0.25, + "sn_mva": 160, + "vn_lv_kv": 110.0, + "vn_hv_kv": 380.0, + "vk_percent": 12.2, + "shift_degree": 0, + "vector_group": "Yy0", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_changer_type": "Ratio", + "trafo_characteristic_table": false + }, + "100 MVA 220/110 kV": { + "i0_percent": 0.06, + "pfe_kw": 55, + "vkr_percent": 0.26, + "sn_mva": 100, + "vn_lv_kv": 110.0, + "vn_hv_kv": 220.0, + "vk_percent": 12.0, + "shift_degree": 0, + "vector_group": "Yy0", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_changer_type": "Ratio", + "trafo_characteristic_table": false + }, + "63 MVA 110/20 kV": { + "i0_percent": 0.04, + "pfe_kw": 22, + "vkr_percent": 0.32, + "sn_mva": 63, + "vn_lv_kv": 20.0, + "vn_hv_kv": 110.0, + "vk_percent": 18, + "shift_degree": 150, + "vector_group": "YNd5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_changer_type": "Ratio", + "trafo_characteristic_table": false + }, + "40 MVA 110/20 kV": { + "i0_percent": 0.05, + "pfe_kw": 18, + "vkr_percent": 0.34, + "sn_mva": 40, + "vn_lv_kv": 20.0, + "vn_hv_kv": 110.0, + "vk_percent": 16.2, + "shift_degree": 150, + "vector_group": "YNd5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_changer_type": "Ratio", + "trafo_characteristic_table": false + }, + "25 MVA 110/20 kV": { + "i0_percent": 0.07, + "pfe_kw": 14, + "vkr_percent": 0.41, + "sn_mva": 25, + "vn_lv_kv": 20.0, + "vn_hv_kv": 110.0, + "vk_percent": 12, + "shift_degree": 150, + "vector_group": "YNd5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_changer_type": "Ratio", + "trafo_characteristic_table": false + }, + "63 MVA 110/10 kV": { + "sn_mva": 63, + "vn_hv_kv": 110, + "vn_lv_kv": 10, + "vk_percent": 18, + "vkr_percent": 0.32, + "pfe_kw": 22, + "i0_percent": 0.04, + "shift_degree": 150, + "vector_group": "YNd5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_changer_type": "Ratio", + "trafo_characteristic_table": false + }, + "40 MVA 110/10 kV": { + "sn_mva": 40, + "vn_hv_kv": 110, + "vn_lv_kv": 10, + "vk_percent": 16.2, + "vkr_percent": 0.34, + "pfe_kw": 18, + "i0_percent": 0.05, + "shift_degree": 150, + "vector_group": "YNd5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_changer_type": "Ratio", + "trafo_characteristic_table": false + }, + "25 MVA 110/10 kV": { + "sn_mva": 25, + "vn_hv_kv": 110, + "vn_lv_kv": 10, + "vk_percent": 12, + "vkr_percent": 0.41, + "pfe_kw": 14, + "i0_percent": 0.07, + "shift_degree": 150, + "vector_group": "YNd5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -9, + "tap_max": 9, + "tap_step_degree": 0, + "tap_step_percent": 1.5, + "tap_changer_type": "Ratio", + "trafo_characteristic_table": false + }, + "0.25 MVA 20/0.4 kV": { + "sn_mva": 0.25, + "vn_hv_kv": 20, + "vn_lv_kv": 0.4, + "vk_percent": 6, + "vkr_percent": 1.44, + "pfe_kw": 0.8, + "i0_percent": 0.32, + "shift_degree": 150, + "vector_group": "Yzn5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -2, + "tap_max": 2, + "tap_step_degree": 0, + "tap_step_percent": 2.5, + "tap_changer_type": "Ratio", + "trafo_characteristic_table": false + }, + "0.4 MVA 20/0.4 kV": { + "sn_mva": 0.4, + "vn_hv_kv": 20, + "vn_lv_kv": 0.4, + "vk_percent": 6, + "vkr_percent": 1.425, + "pfe_kw": 1.35, + "i0_percent": 0.3375, + "shift_degree": 150, + "vector_group": "Dyn5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -2, + "tap_max": 2, + "tap_step_degree": 0, + "tap_step_percent": 2.5, + "tap_changer_type": "Ratio", + "trafo_characteristic_table": false + }, + "0.63 MVA 20/0.4 kV": { + "sn_mva": 0.63, + "vn_hv_kv": 20, + "vn_lv_kv": 0.4, + "vk_percent": 6, + "vkr_percent": 1.206, + "pfe_kw": 1.65, + "i0_percent": 0.2619, + "shift_degree": 150, + "vector_group": "Dyn5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -2, + "tap_max": 2, + "tap_step_degree": 0, + "tap_step_percent": 2.5, + "tap_changer_type": "Ratio", + "trafo_characteristic_table": false + }, + "0.25 MVA 10/0.4 kV": { + "sn_mva": 0.25, + "vn_hv_kv": 10, + "vn_lv_kv": 0.4, + "vk_percent": 4, + "vkr_percent": 1.2, + "pfe_kw": 0.6, + "i0_percent": 0.24, + "shift_degree": 150, + "vector_group": "Dyn5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -2, + "tap_max": 2, + "tap_step_degree": 0, + "tap_step_percent": 2.5, + "tap_changer_type": "Ratio", + "trafo_characteristic_table": false + }, + "0.4 MVA 10/0.4 kV": { + "sn_mva": 0.4, + "vn_hv_kv": 10, + "vn_lv_kv": 0.4, + "vk_percent": 4, + "vkr_percent": 1.325, + "pfe_kw": 0.95, + "i0_percent": 0.2375, + "shift_degree": 150, + "vector_group": "Dyn5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -2, + "tap_max": 2, + "tap_step_degree": 0, + "tap_step_percent": 2.5, + "tap_changer_type": "Ratio", + "trafo_characteristic_table": false + }, + "0.63 MVA 10/0.4 kV": { + "sn_mva": 0.63, + "vn_hv_kv": 10, + "vn_lv_kv": 0.4, + "vk_percent": 4, + "vkr_percent": 1.0794, + "pfe_kw": 1.18, + "i0_percent": 0.1873, + "shift_degree": 150, + "vector_group": "Dyn5", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -2, + "tap_max": 2, + "tap_step_degree": 0, + "tap_step_percent": 2.5, + "tap_changer_type": "Ratio", + "trafo_characteristic_table": false + } + }, + "trafo3w": { + "63/25/38 MVA 110/20/10 kV": { + "sn_hv_mva": 63, + "sn_mv_mva": 25, + "sn_lv_mva": 38, + "vn_hv_kv": 110, + "vn_mv_kv": 20, + "vn_lv_kv": 10, + "vk_hv_percent": 10.4, + "vk_mv_percent": 10.4, + "vk_lv_percent": 10.4, + "vkr_hv_percent": 0.28, + "vkr_mv_percent": 0.32, + "vkr_lv_percent": 0.35, + "pfe_kw": 35, + "i0_percent": 0.89, + "shift_mv_degree": 0, + "shift_lv_degree": 0, + "vector_group": "YN0yn0yn0", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -10, + "tap_max": 10, + "tap_step_percent": 1.2, + "tap_changer_type": "Ratio", + "trafo_characteristic_table": false + }, + "63/25/38 MVA 110/10/10 kV": { + "sn_hv_mva": 63, + "sn_mv_mva": 25, + "sn_lv_mva": 38, + "vn_hv_kv": 110, + "vn_mv_kv": 10, + "vn_lv_kv": 10, + "vk_hv_percent": 10.4, + "vk_mv_percent": 10.4, + "vk_lv_percent": 10.4, + "vkr_hv_percent": 0.28, + "vkr_mv_percent": 0.32, + "vkr_lv_percent": 0.35, + "pfe_kw": 35, + "i0_percent": 0.89, + "shift_mv_degree": 0, + "shift_lv_degree": 0, + "vector_group": "YN0yn0yn0", + "tap_side": "hv", + "tap_neutral": 0, + "tap_min": -10, + "tap_max": 10, + "tap_step_percent": 1.2, + "tap_changer_type": "Ratio", + "trafo_characteristic_table": false + } + }, + "fuse": { + "HV 100A": { + "fuse_type": "HV 100A", + "i_rated_a": 100.0, + "t_avg": 0, + "t_min": [ + 10.0, + 3.64, + 0.854, + 0.281, + 0.1, + 0.0531, + 0.022, + 0.01 + ], + "t_total": [ + 10.0, + 4.267, + 1.21, + 0.403, + 0.1, + 0.058, + 0.022, + 0.01 + ], + "x_avg": 0, + "x_min": [ + 300.0, + 350.0, + 450.0, + 550.0, + 700.0, + 850.0, + 1200.0, + 1752.0 + ], + "x_total": [ + 600.0, + 700.0, + 900.0, + 1150.0, + 1665.0, + 2000.0, + 3000.0, + 4313.0 + ] + }, + "HV 10A": { + "fuse_type": "HV 10A", + "i_rated_a": 10.0, + "t_avg": 0, + "t_min": [ + 10.0, + 1675.0, + 0.344, + 0.156, + 0.1, + 0.0417, + 0.0171, + 0.01 + ], + "t_total": [ + 10.0, + 1.3, + 0.3, + 0.155, + 0.1, + 0.0555, + 0.023, + 0.01 + ], + "x_avg": 0, + "x_min": [ + 30.0, + 32.0, + 35.0, + 37.0, + 39.0, + 50.0, + 70.0, + 88.0 + ], + "x_total": [ + 60.0, + 70.0, + 80.0, + 87.0, + 94.0, + 110.0, + 150.0, + 216.0 + ] + }, + "HV 125A": { + "fuse_type": "HV 125A", + "i_rated_a": 125.0, + "t_avg": 0, + "t_min": [ + 10.0, + 1.82, + 0.344, + 0.1, + 0.0467, + 0.0269, + 0.01 + ], + "t_total": [ + 10.0, + 2.478, + 0.426, + 0.1, + 0.0427, + 0.0211, + 0.01 + ], + "x_avg": 0, + "x_min": [ + 375.0, + 500.0, + 700.0, + 925.0, + 1200.0, + 1500.0, + 2341.0 + ], + "x_total": [ + 750.0, + 1000.0, + 1500.0, + 2200.0, + 3000.0, + 4000.0, + 5765.0 + ] + }, + "HV 160A": { + "fuse_type": "HV 160A", + "i_rated_a": 160.0, + "t_avg": 0, + "t_min": [ + 10.0, + 4.15, + 1.03, + 0.198, + 0.1, + 0.051, + 0.0172, + 0.01 + ], + "t_total": [ + 10.0, + 2.3, + 0.734, + 0.274, + 0.1, + 0.046, + 0.0177, + 0.01 + ], + "x_avg": 0, + "x_min": [ + 480.0, + 550.0, + 700.0, + 1000.0, + 1260.0, + 1600.0, + 2500.0, + 3227.0 + ], + "x_total": [ + 960.0, + 1300.0, + 1700.0, + 2200.0, + 2996.0, + 4000.0, + 6000.0, + 7946.0 + ] + }, + "HV 16A": { + "fuse_type": "HV 16A", + "i_rated_a": 16.0, + "t_avg": 0, + "t_min": [ + 10.0, + 0.352, + 0.164, + 0.1, + 0.0649, + 0.0342, + 0.01 + ], + "t_total": [ + 10.0, + 2.34, + 0.722, + 0.181, + 0.1, + 0.055, + 0.0296, + 0.01 + ], + "x_avg": 0, + "x_min": [ + 48.0, + 60.0, + 65.0, + 71.0, + 80.0, + 100.0, + 162.0 + ], + "x_total": [ + 96.0, + 110.0, + 125.0, + 150.0, + 168.0, + 200.0, + 250.0, + 398.0 + ] + }, + "HV 200A": { + "fuse_type": "HV 200A", + "i_rated_a": 200.0, + "t_avg": 0, + "t_min": [ + 10.0, + 4.267, + 1.21, + 0.403, + 0.1, + 0.058, + 0.022, + 0.01 + ], + "t_total": [ + 10.0, + 3.73, + 1.654, + 0.328, + 0.1, + 0.0531, + 0.019, + 0.01 + ], + "x_avg": 0, + "x_min": [ + 600.0, + 700.0, + 900.0, + 1150.0, + 1665.0, + 2000.0, + 3000.0, + 4313.0 + ], + "x_total": [ + 1200.0, + 1500.0, + 1800.0, + 2700.0, + 3960.0, + 5000.0, + 7500.0, + 10620.0 + ] + }, + "HV 20A": { + "fuse_type": "HV 20A", + "i_rated_a": 20.0, + "t_avg": 0, + "t_min": [ + 10.0, + 1.3, + 0.3, + 0.155, + 0.1, + 0.0555, + 0.023, + 0.01 + ], + "t_total": [ + 10.0, + 1.3, + 0.161, + 0.1, + 0.0611, + 0.0399, + 0.0141, + 0.01 + ], + "x_avg": 0, + "x_min": [ + 60.0, + 70.0, + 80.0, + 87.0, + 94.0, + 110.0, + 150.0, + 216.0 + ], + "x_total": [ + 120.0, + 150.0, + 200.0, + 223.0, + 260.0, + 300.0, + 450.0, + 532.0 + ] + }, + "HV 25A": { + "fuse_type": "HV 25A", + "i_rated_a": 25.0, + "t_avg": 0, + "t_min": [ + 10.0, + 2.512, + 0.833, + 0.299, + 0.1, + 0.0372, + 0.0223, + 0.01 + ], + "t_total": [ + 10.0, + 3.125, + 0.597, + 0.198, + 0.1, + 0.0378, + 0.022, + 0.01 + ], + "x_avg": 0, + "x_min": [ + 75.0, + 82.0, + 90.0, + 100.0, + 124.0, + 170.0, + 200.0, + 289.0 + ], + "x_total": [ + 150.0, + 170.0, + 210.0, + 250.0, + 294.0, + 400.0, + 500.0, + 711.0 + ] + }, + "HV 31.5A": { + "fuse_type": "HV 31.5A", + "i_rated_a": 31.5, + "t_avg": 0, + "t_min": [ + 10.0, + 2.34, + 0.722, + 0.181, + 0.1, + 0.055, + 0.0296, + 0.01 + ], + "t_total": [ + 10.0, + 2.84, + 0.368, + 0.164, + 0.1, + 0.0621, + 0.0378, + 0.0195, + 0.01 + ], + "x_avg": 0, + "x_min": [ + 95.0, + 110.0, + 125.0, + 150.0, + 165.0, + 200.0, + 250.0, + 390.0 + ], + "x_total": [ + 189.0, + 220.0, + 300.0, + 350.0, + 393.0, + 450.0, + 530.0, + 700.0, + 960.0 + ] + }, + "HV 40A": { + "fuse_type": "HV 40A", + "i_rated_a": 40.0, + "t_avg": 0, + "t_min": [ + 10.0, + 1.3, + 0.161, + 0.1, + 0.0611, + 0.0399, + 0.0141, + 0.01 + ], + "t_total": [ + 10.0, + 2.05, + 0.369, + 0.198, + 0.1, + 0.051, + 0.0298, + 0.01 + ], + "x_avg": 0, + "x_min": [ + 120.0, + 150.0, + 200.0, + 223.0, + 260.0, + 300.0, + 450.0, + 532.0 + ], + "x_total": [ + 240.0, + 300.0, + 400.0, + 450.0, + 530.0, + 650.0, + 800.0, + 1311.0 + ] + }, + "HV 50A": { + "fuse_type": "HV 50A", + "i_rated_a": 50.0, + "t_avg": 0, + "t_min": [ + 10.0, + 3.215, + 0.597, + 0.198, + 0.1, + 0.0378, + 0.022, + 0.01 + ], + "t_total": [ + 10.0, + 3.64, + 0.854, + 0.281, + 0.1, + 0.0531, + 0.022, + 0.01 + ], + "x_avg": 0, + "x_min": [ + 150.0, + 170.0, + 210.0, + 250.0, + 294.0, + 400.0, + 500.0, + 711.0 + ], + "x_total": [ + 300.0, + 350.0, + 450.0, + 550.0, + 700.0, + 850.0, + 1200.0, + 1752.0 + ] + }, + "HV 6.3A": { + "fuse_type": "HV 6.3A", + "i_rated_a": 6.3, + "t_avg": 0, + "t_min": [ + 10.0, + 1.39, + 0.344, + 0.168, + 0.1, + 0.056, + 0.0263, + 0.01 + ], + "t_total": [ + 10.0, + 1.711, + 0.516, + 0.198, + 0.1, + 0.0634, + 0.0303, + 0.01 + ], + "x_avg": 0, + "x_min": [ + 19.0, + 19.5, + 20.4, + 20.8, + 22.0, + 25.0, + 32.0, + 48.0 + ], + "x_total": [ + 38.0, + 40.0, + 43.0, + 48.0, + 53.0, + 60.0, + 75.0, + 118.0 + ] + }, + "HV 63A": { + "fuse_type": "HV 63A", + "i_rated_a": 63.0, + "t_avg": 0, + "t_min": [ + 10.0, + 2.84, + 0.368, + 0.164, + 0.1, + 0.0621, + 0.0378, + 0.0195, + 0.01 + ], + "t_total": [ + 10.0, + 1.82, + 0.344, + 0.1, + 0.0467, + 0.0269, + 0.01 + ], + "x_avg": 0, + "x_min": [ + 189.0, + 220.0, + 300.0, + 350.0, + 393.0, + 450.0, + 530.0, + 700.0, + 961.0 + ], + "x_total": [ + 378.0, + 500.0, + 700.0, + 934.0, + 1200.0, + 1500.0, + 2366.0 + ] + }, + "HV 80A": { + "fuse_type": "HV 80A", + "i_rated_a": 80.0, + "t_avg": 0, + "t_min": [ + 10.0, + 2.05, + 0.369, + 0.198, + 0.1, + 0.051, + 0.0298, + 0.01 + ], + "t_total": [ + 10.0, + 4.15, + 1.03, + 0.198, + 0.1, + 0.051, + 0.0172, + 0.01 + ], + "x_avg": 0, + "x_min": [ + 240.0, + 300.0, + 400.0, + 450.0, + 530.0, + 650.0, + 800.0, + 1311.0 + ], + "x_total": [ + 480.0, + 550.0, + 700.0, + 1000.0, + 1260.0, + 1600.0, + 2500.0, + 3227.0 + ] + }, + "Siemens NH-1-100": { + "fuse_type": "Siemens NH-1-100", + "i_rated_a": 100.0, + "t_avg": [ + 5400.0, + 2000.0, + 400.0, + 20.0, + 1.0, + 0.2, + 0.012, + 0.004 + ], + "t_min": 0, + "t_total": 0, + "x_avg": [ + 150.0, + 190.0, + 250.0, + 430.0, + 900.0, + 1250.0, + 2700.0, + 3600.0 + ], + "x_min": 0, + "x_total": 0 + }, + "Siemens NH-1-125": { + "fuse_type": "Siemens NH-1-125", + "i_rated_a": 125.0, + "t_avg": [ + 4800.0, + 120.0, + 7.0, + 0.1, + 0.004 + ], + "t_min": 0, + "t_total": 0, + "x_avg": [ + 180.0, + 400.0, + 740.0, + 2000.0, + 4250.0 + ], + "x_min": 0, + "x_total": 0 + }, + "Siemens NH-1-16": { + "fuse_type": "Siemens NH-1-16", + "i_rated_a": 16.0, + "t_avg": [ + 4000.0, + 400.0, + 2.0, + 0.1, + 0.04, + 0.01 + ], + "t_min": 0, + "t_total": 0, + "x_avg": [ + 26.0, + 35.0, + 75.0, + 150.0, + 200.0, + 300.0 + ], + "x_min": 0, + "x_total": 0 + }, + "Siemens NH-1-160": { + "fuse_type": "Siemens NH-1-160", + "i_rated_a": 160.0, + "t_avg": [ + 4800.0, + 120.0, + 7.0, + 0.1, + 0.004 + ], + "t_min": 0, + "t_total": 0, + "x_avg": [ + 210.0, + 500.0, + 900.0, + 2300.0, + 5000.0 + ], + "x_min": 0, + "x_total": 0 + }, + "Siemens NH-1-25": { + "fuse_type": "Siemens NH-1-25", + "i_rated_a": 25.0, + "t_avg": [ + 4000.0, + 1000.0, + 10.0, + 0.2, + 0.02, + 0.01 + ], + "t_min": 0, + "t_total": 0, + "x_avg": [ + 40.0, + 50.0, + 100.0, + 210.0, + 400.0, + 500.0 + ], + "x_min": 0, + "x_total": 0 + }, + "Siemens NH-1-50": { + "fuse_type": "Siemens NH-1-50", + "i_rated_a": 50.0, + "t_avg": [ + 4000.0, + 40.0, + 4.0, + 1.0, + 0.02, + 0.01 + ], + "t_min": 0, + "t_total": 0, + "x_avg": [ + 86.0, + 200.0, + 300.0, + 400.0, + 1000.0, + 1280.0 + ], + "x_min": 0, + "x_total": 0 + }, + "Siemens NH-1-63": { + "fuse_type": "Siemens NH-1-63", + "i_rated_a": 63.0, + "t_avg": [ + 4000.0, + 100.0, + 10.0, + 2.0, + 0.04, + 0.01 + ], + "t_min": 0, + "t_total": 0, + "x_avg": [ + 100.0, + 200.0, + 300.0, + 400.0, + 1000.0, + 1500.0 + ], + "x_min": 0, + "x_total": 0 + }, + "Siemens NH-1-80": { + "fuse_type": "Siemens NH-1-80", + "i_rated_a": 80.0, + "t_avg": [ + 4800.0, + 120.0, + 7.0, + 0.1, + 0.01 + ], + "t_min": 0, + "t_total": 0, + "x_avg": [ + 150.58, + 250.0, + 450.0, + 1150.0, + 2470.0 + ], + "x_min": 0, + "x_total": 0 + }, + "Siemens NH-2-1000": { + "fuse_type": "Siemens NH-2-1000", + "i_rated_a": 1000.0, + "t_avg": [ + 4800.0, + 120.0, + 7.0, + 0.1, + 0.004 + ], + "t_min": 0, + "t_total": 0, + "x_avg": [ + 1900.0, + 3500.0, + 8400.0, + 24000.0, + 52000.0 + ], + "x_min": 0, + "x_total": 0 + }, + "Siemens NH-2-200": { + "fuse_type": "Siemens NH-2-200", + "i_rated_a": 200.0, + "t_avg": [ + 4800.0, + 120.0, + 7.0, + 0.1, + 0.004 + ], + "t_min": 0, + "t_total": 0, + "x_avg": [ + 280.0, + 650.0, + 1200.0, + 3000.0, + 7000.0 + ], + "x_min": 0, + "x_total": 0 + }, + "Siemens NH-2-224": { + "fuse_type": "Siemens NH-2-224", + "i_rated_a": 224.0, + "t_avg": [ + 4800.0, + 120.0, + 7.0, + 0.2, + 0.04, + 0.004 + ], + "t_min": 0, + "t_total": 0, + "x_avg": [ + 400.0, + 750.0, + 1453.0, + 3025.0, + 4315.0, + 7600.0 + ], + "x_min": 0, + "x_total": 0 + }, + "Siemens NH-2-250": { + "fuse_type": "Siemens NH-2-250", + "i_rated_a": 250.0, + "t_avg": [ + 4800.0, + 120.0, + 7.0, + 0.1, + 0.004 + ], + "t_min": 0, + "t_total": 0, + "x_avg": [ + 450.0, + 800.0, + 1650.0, + 4000.0, + 8500.0 + ], + "x_min": 0, + "x_total": 0 + }, + "Siemens NH-2-315": { + "fuse_type": "Siemens NH-2-315", + "i_rated_a": 315.0, + "t_avg": [ + 4800.0, + 120.0, + 7.0, + 0.1, + 0.004 + ], + "t_min": 0, + "t_total": 0, + "x_avg": [ + 550.0, + 920.0, + 1900.0, + 5000.0, + 11000.0 + ], + "x_min": 0, + "x_total": 0 + }, + "Siemens NH-2-355": { + "fuse_type": "Siemens NH-2-355", + "i_rated_a": 355.0, + "t_avg": [ + 4800.0, + 120.0, + 6.0, + 0.1, + 0.004 + ], + "t_min": 0, + "t_total": 0, + "x_avg": [ + 650.0, + 1116.27, + 2350.0, + 5840.0, + 12790.0 + ], + "x_min": 0, + "x_total": 0 + }, + "Siemens NH-2-400": { + "fuse_type": "Siemens NH-2-400", + "i_rated_a": 400.0, + "t_avg": [ + 4800.0, + 120.0, + 7.0, + 0.1, + 0.004 + ], + "t_min": 0, + "t_total": 0, + "x_avg": [ + 720.0, + 1350.0, + 2800.0, + 6500.0, + 15000.0 + ], + "x_min": 0, + "x_total": 0 + }, + "Siemens NH-2-425": { + "fuse_type": "Siemens NH-2-425", + "i_rated_a": 425.0, + "t_avg": [ + 4800.0, + 120.0, + 7.0, + 0.1, + 0.004 + ], + "t_min": 0, + "t_total": 0, + "x_avg": [ + 850.0, + 1500.0, + 3050.0, + 7500.0, + 16500.0 + ], + "x_min": 0, + "x_total": 0 + }, + "Siemens NH-2-630": { + "fuse_type": "Siemens NH-2-630", + "i_rated_a": 630.0, + "t_avg": [ + 4800.0, + 120.0, + 7.0, + 0.1, + 0.004 + ], + "t_min": 0, + "t_total": 0, + "x_avg": [ + 1200.0, + 2000.0, + 4800.0, + 12000.0, + 26000.0 + ], + "x_min": 0, + "x_total": 0 + } + } + }, + "res_bus": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"vm_pu\",\"va_degree\",\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "vm_pu": "float64", + "va_degree": "float64", + "p_mw": "float64", + "q_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_bus_dc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"vm_pu\",\"p_mw\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "vm_pu": "float64", + "p_mw": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_line": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\",\"pl_mw\",\"ql_mvar\",\"i_from_ka\",\"i_to_ka\",\"i_ka\",\"vm_from_pu\",\"va_from_degree\",\"vm_to_pu\",\"va_to_degree\",\"loading_percent\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_from_mw": "float64", + "q_from_mvar": "float64", + "p_to_mw": "float64", + "q_to_mvar": "float64", + "pl_mw": "float64", + "ql_mvar": "float64", + "i_from_ka": "float64", + "i_to_ka": "float64", + "i_ka": "float64", + "vm_from_pu": "float64", + "va_from_degree": "float64", + "vm_to_pu": "float64", + "va_to_degree": "float64", + "loading_percent": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_line_dc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_from_mw\",\"p_to_mw\",\"pl_mw\",\"i_from_ka\",\"i_to_ka\",\"i_ka\",\"vm_from_pu\",\"vm_to_pu\",\"loading_percent\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_from_mw": "float64", + "p_to_mw": "float64", + "pl_mw": "float64", + "i_from_ka": "float64", + "i_to_ka": "float64", + "i_ka": "float64", + "vm_from_pu": "float64", + "vm_to_pu": "float64", + "loading_percent": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_trafo": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_hv_mw\",\"q_hv_mvar\",\"p_lv_mw\",\"q_lv_mvar\",\"pl_mw\",\"ql_mvar\",\"i_hv_ka\",\"i_lv_ka\",\"vm_hv_pu\",\"va_hv_degree\",\"vm_lv_pu\",\"va_lv_degree\",\"loading_percent\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_hv_mw": "float64", + "q_hv_mvar": "float64", + "p_lv_mw": "float64", + "q_lv_mvar": "float64", + "pl_mw": "float64", + "ql_mvar": "float64", + "i_hv_ka": "float64", + "i_lv_ka": "float64", + "vm_hv_pu": "float64", + "va_hv_degree": "float64", + "vm_lv_pu": "float64", + "va_lv_degree": "float64", + "loading_percent": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_trafo3w": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_hv_mw\",\"q_hv_mvar\",\"p_mv_mw\",\"q_mv_mvar\",\"p_lv_mw\",\"q_lv_mvar\",\"pl_mw\",\"ql_mvar\",\"i_hv_ka\",\"i_mv_ka\",\"i_lv_ka\",\"vm_hv_pu\",\"va_hv_degree\",\"vm_mv_pu\",\"va_mv_degree\",\"vm_lv_pu\",\"va_lv_degree\",\"va_internal_degree\",\"vm_internal_pu\",\"loading_percent\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_hv_mw": "float64", + "q_hv_mvar": "float64", + "p_mv_mw": "float64", + "q_mv_mvar": "float64", + "p_lv_mw": "float64", + "q_lv_mvar": "float64", + "pl_mw": "float64", + "ql_mvar": "float64", + "i_hv_ka": "float64", + "i_mv_ka": "float64", + "i_lv_ka": "float64", + "vm_hv_pu": "float64", + "va_hv_degree": "float64", + "vm_mv_pu": "float64", + "va_mv_degree": "float64", + "vm_lv_pu": "float64", + "va_lv_degree": "float64", + "va_internal_degree": "float64", + "vm_internal_pu": "float64", + "loading_percent": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_impedance": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\",\"pl_mw\",\"ql_mvar\",\"i_from_ka\",\"i_to_ka\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_from_mw": "float64", + "q_from_mvar": "float64", + "p_to_mw": "float64", + "q_to_mvar": "float64", + "pl_mw": "float64", + "ql_mvar": "float64", + "i_from_ka": "float64", + "i_to_ka": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_ext_grid": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_load": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_load_dc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_dc_mw\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_dc_mw": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_motor": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_sgen": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_storage": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_shunt": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"vm_pu\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64", + "vm_pu": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_gen": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"va_degree\",\"vm_pu\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64", + "va_degree": "float64", + "vm_pu": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_ward": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"vm_pu\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64", + "vm_pu": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_xward": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"vm_pu\",\"va_internal_degree\",\"vm_internal_pu\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64", + "vm_pu": "float64", + "va_internal_degree": "float64", + "vm_internal_pu": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_dcline": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\",\"pl_mw\",\"vm_from_pu\",\"va_from_degree\",\"vm_to_pu\",\"va_to_degree\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_from_mw": "float64", + "q_from_mvar": "float64", + "p_to_mw": "float64", + "q_to_mvar": "float64", + "pl_mw": "float64", + "vm_from_pu": "float64", + "va_from_degree": "float64", + "vm_to_pu": "float64", + "va_to_degree": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_asymmetric_load": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_asymmetric_sgen": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_source_dc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_dc_mw\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_dc_mw": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_switch": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"i_ka\",\"loading_percent\",\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "i_ka": "float64", + "loading_percent": "float64", + "p_from_mw": "float64", + "q_from_mvar": "float64", + "p_to_mw": "float64", + "q_to_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_tcsc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"thyristor_firing_angle_degree\",\"x_ohm\",\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\",\"pl_mw\",\"ql_mvar\",\"i_ka\",\"vm_from_pu\",\"va_from_degree\",\"vm_to_pu\",\"va_to_degree\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "thyristor_firing_angle_degree": "float64", + "x_ohm": "float64", + "p_from_mw": "float64", + "q_from_mvar": "float64", + "p_to_mw": "float64", + "q_to_mvar": "float64", + "pl_mw": "float64", + "ql_mvar": "float64", + "i_ka": "float64", + "vm_from_pu": "float64", + "va_from_degree": "float64", + "vm_to_pu": "float64", + "va_to_degree": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_svc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"thyristor_firing_angle_degree\",\"x_ohm\",\"q_mvar\",\"vm_pu\",\"va_degree\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "thyristor_firing_angle_degree": "float64", + "x_ohm": "float64", + "q_mvar": "float64", + "vm_pu": "float64", + "va_degree": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_ssc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"q_mvar\",\"vm_internal_pu\",\"va_internal_degree\",\"vm_pu\",\"va_degree\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "q_mvar": "float64", + "vm_internal_pu": "float64", + "va_internal_degree": "float64", + "vm_pu": "float64", + "va_degree": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_vsc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"p_dc_mw\",\"vm_internal_pu\",\"va_internal_degree\",\"vm_pu\",\"va_degree\",\"vm_internal_dc_pu\",\"vm_dc_pu\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64", + "p_dc_mw": "float64", + "vm_internal_pu": "float64", + "va_internal_degree": "float64", + "vm_pu": "float64", + "va_degree": "float64", + "vm_internal_dc_pu": "float64", + "vm_dc_pu": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_vsc_stacked": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"p_dc_mw_p\",\"p_dc_mw_m\",\"vm_internal_pu\",\"vm_internal_degree\",\"vm_pu\",\"va_degree\",\"vm_internal_dc_pu_p\",\"vm_internal_dc_pu_m\",\"vm_dc_pu_p\",\"vm_dc_pu_m\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64", + "p_dc_mw_p": "float64", + "p_dc_mw_m": "float64", + "vm_internal_pu": "float64", + "vm_internal_degree": "float64", + "vm_pu": "float64", + "va_degree": "float64", + "vm_internal_dc_pu_p": "float64", + "vm_internal_dc_pu_m": "float64", + "vm_dc_pu_p": "float64", + "vm_dc_pu_m": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_vsc_bipolar": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"p_dc_mw_p\",\"p_dc_mw_m\",\"vm_internal_pu\",\"vm_internal_degree\",\"vm_pu\",\"va_degree\",\"vm_internal_dc_pu_p\",\"vm_internal_dc_pu_m\",\"vm_dc_pu_p\",\"vm_dc_pu_m\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64", + "p_dc_mw_p": "float64", + "p_dc_mw_m": "float64", + "vm_internal_pu": "float64", + "vm_internal_degree": "float64", + "vm_pu": "float64", + "va_degree": "float64", + "vm_internal_dc_pu_p": "float64", + "vm_internal_dc_pu_m": "float64", + "vm_dc_pu_p": "float64", + "vm_dc_pu_m": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_bus_est": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"vm_pu\",\"va_degree\",\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "vm_pu": "float64", + "va_degree": "float64", + "p_mw": "float64", + "q_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_line_est": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\",\"pl_mw\",\"ql_mvar\",\"i_from_ka\",\"i_to_ka\",\"i_ka\",\"vm_from_pu\",\"va_from_degree\",\"vm_to_pu\",\"va_to_degree\",\"loading_percent\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_from_mw": "float64", + "q_from_mvar": "float64", + "p_to_mw": "float64", + "q_to_mvar": "float64", + "pl_mw": "float64", + "ql_mvar": "float64", + "i_from_ka": "float64", + "i_to_ka": "float64", + "i_ka": "float64", + "vm_from_pu": "float64", + "va_from_degree": "float64", + "vm_to_pu": "float64", + "va_to_degree": "float64", + "loading_percent": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_trafo_est": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_hv_mw\",\"q_hv_mvar\",\"p_lv_mw\",\"q_lv_mvar\",\"pl_mw\",\"ql_mvar\",\"i_hv_ka\",\"i_lv_ka\",\"vm_hv_pu\",\"va_hv_degree\",\"vm_lv_pu\",\"va_lv_degree\",\"loading_percent\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_hv_mw": "float64", + "q_hv_mvar": "float64", + "p_lv_mw": "float64", + "q_lv_mvar": "float64", + "pl_mw": "float64", + "ql_mvar": "float64", + "i_hv_ka": "float64", + "i_lv_ka": "float64", + "vm_hv_pu": "float64", + "va_hv_degree": "float64", + "vm_lv_pu": "float64", + "va_lv_degree": "float64", + "loading_percent": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_trafo3w_est": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_hv_mw\",\"q_hv_mvar\",\"p_mv_mw\",\"q_mv_mvar\",\"p_lv_mw\",\"q_lv_mvar\",\"pl_mw\",\"ql_mvar\",\"i_hv_ka\",\"i_mv_ka\",\"i_lv_ka\",\"vm_hv_pu\",\"va_hv_degree\",\"vm_mv_pu\",\"va_mv_degree\",\"vm_lv_pu\",\"va_lv_degree\",\"va_internal_degree\",\"vm_internal_pu\",\"loading_percent\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_hv_mw": "float64", + "q_hv_mvar": "float64", + "p_mv_mw": "float64", + "q_mv_mvar": "float64", + "p_lv_mw": "float64", + "q_lv_mvar": "float64", + "pl_mw": "float64", + "ql_mvar": "float64", + "i_hv_ka": "float64", + "i_mv_ka": "float64", + "i_lv_ka": "float64", + "vm_hv_pu": "float64", + "va_hv_degree": "float64", + "vm_mv_pu": "float64", + "va_mv_degree": "float64", + "vm_lv_pu": "float64", + "va_lv_degree": "float64", + "va_internal_degree": "float64", + "vm_internal_pu": "float64", + "loading_percent": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_impedance_est": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\",\"pl_mw\",\"ql_mvar\",\"i_from_ka\",\"i_to_ka\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_from_mw": "float64", + "q_from_mvar": "float64", + "p_to_mw": "float64", + "q_to_mvar": "float64", + "pl_mw": "float64", + "ql_mvar": "float64", + "i_from_ka": "float64", + "i_to_ka": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_switch_est": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"i_ka\",\"loading_percent\",\"p_from_mw\",\"q_from_mvar\",\"p_to_mw\",\"q_to_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "i_ka": "float64", + "loading_percent": "float64", + "p_from_mw": "float64", + "q_from_mvar": "float64", + "p_to_mw": "float64", + "q_to_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_shunt_est": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\",\"vm_pu\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64", + "vm_pu": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_bus_sc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[],\"index\":[],\"data\":[]}", + "orient": "split", + "is_multiindex": false, + "is_multicolumn": false + }, + "res_line_sc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[],\"index\":[],\"data\":[]}", + "orient": "split", + "is_multiindex": false, + "is_multicolumn": false + }, + "res_trafo_sc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[],\"index\":[],\"data\":[]}", + "orient": "split", + "is_multiindex": false, + "is_multicolumn": false + }, + "res_trafo3w_sc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[],\"index\":[],\"data\":[]}", + "orient": "split", + "is_multiindex": false, + "is_multicolumn": false + }, + "res_ext_grid_sc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[],\"index\":[],\"data\":[]}", + "orient": "split", + "is_multiindex": false, + "is_multicolumn": false + }, + "res_gen_sc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[],\"index\":[],\"data\":[]}", + "orient": "split", + "is_multiindex": false, + "is_multicolumn": false + }, + "res_sgen_sc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[],\"index\":[],\"data\":[]}", + "orient": "split", + "is_multiindex": false, + "is_multicolumn": false + }, + "res_switch_sc": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[],\"index\":[],\"data\":[]}", + "orient": "split", + "is_multiindex": false, + "is_multicolumn": false + }, + "res_bus_3ph": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"vm_a_pu\",\"va_a_degree\",\"vm_b_pu\",\"va_b_degree\",\"vm_c_pu\",\"va_c_degree\",\"p_a_mw\",\"q_a_mvar\",\"p_b_mw\",\"q_b_mvar\",\"p_c_mw\",\"q_c_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "vm_a_pu": "float64", + "va_a_degree": "float64", + "vm_b_pu": "float64", + "va_b_degree": "float64", + "vm_c_pu": "float64", + "va_c_degree": "float64", + "p_a_mw": "float64", + "q_a_mvar": "float64", + "p_b_mw": "float64", + "q_b_mvar": "float64", + "p_c_mw": "float64", + "q_c_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_line_3ph": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_a_from_mw\",\"q_a_from_mvar\",\"p_b_from_mw\",\"q_b_from_mvar\",\"p_c_from_mw\",\"q_c_from_mvar\",\"p_a_to_mw\",\"q_a_to_mvar\",\"p_b_to_mw\",\"q_b_to_mvar\",\"p_c_to_mw\",\"q_c_to_mvar\",\"pl_a_mw\",\"ql_a_mvar\",\"pl_b_mw\",\"ql_b_mvar\",\"pl_c_mw\",\"ql_c_mvar\",\"i_a_from_ka\",\"i_a_to_ka\",\"i_b_from_ka\",\"i_b_to_ka\",\"i_c_from_ka\",\"i_c_to_ka\",\"i_a_ka\",\"i_b_ka\",\"i_c_ka\",\"i_n_from_ka\",\"i_n_to_ka\",\"i_n_ka\",\"loading_a_percent\",\"loading_b_percent\",\"loading_c_percent\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_a_from_mw": "float64", + "q_a_from_mvar": "float64", + "p_b_from_mw": "float64", + "q_b_from_mvar": "float64", + "p_c_from_mw": "float64", + "q_c_from_mvar": "float64", + "p_a_to_mw": "float64", + "q_a_to_mvar": "float64", + "p_b_to_mw": "float64", + "q_b_to_mvar": "float64", + "p_c_to_mw": "float64", + "q_c_to_mvar": "float64", + "pl_a_mw": "float64", + "ql_a_mvar": "float64", + "pl_b_mw": "float64", + "ql_b_mvar": "float64", + "pl_c_mw": "float64", + "ql_c_mvar": "float64", + "i_a_from_ka": "float64", + "i_a_to_ka": "float64", + "i_b_from_ka": "float64", + "i_b_to_ka": "float64", + "i_c_from_ka": "float64", + "i_c_to_ka": "float64", + "i_a_ka": "float64", + "i_b_ka": "float64", + "i_c_ka": "float64", + "i_n_from_ka": "float64", + "i_n_to_ka": "float64", + "i_n_ka": "float64", + "loading_a_percent": "float64", + "loading_b_percent": "float64", + "loading_c_percent": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_trafo_3ph": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_a_hv_mw\",\"q_a_hv_mvar\",\"p_b_hv_mw\",\"q_b_hv_mvar\",\"p_c_hv_mw\",\"q_c_hv_mvar\",\"p_a_lv_mw\",\"q_a_lv_mvar\",\"p_b_lv_mw\",\"q_b_lv_mvar\",\"p_c_lv_mw\",\"q_c_lv_mvar\",\"pl_a_mw\",\"ql_a_mvar\",\"pl_b_mw\",\"ql_b_mvar\",\"pl_c_mw\",\"ql_c_mvar\",\"i_a_hv_ka\",\"i_a_lv_ka\",\"i_b_hv_ka\",\"i_b_lv_ka\",\"i_c_hv_ka\",\"i_c_lv_ka\",\"loading_a_percent\",\"loading_b_percent\",\"loading_c_percent\",\"loading_percent\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_a_hv_mw": "float64", + "q_a_hv_mvar": "float64", + "p_b_hv_mw": "float64", + "q_b_hv_mvar": "float64", + "p_c_hv_mw": "float64", + "q_c_hv_mvar": "float64", + "p_a_lv_mw": "float64", + "q_a_lv_mvar": "float64", + "p_b_lv_mw": "float64", + "q_b_lv_mvar": "float64", + "p_c_lv_mw": "float64", + "q_c_lv_mvar": "float64", + "pl_a_mw": "float64", + "ql_a_mvar": "float64", + "pl_b_mw": "float64", + "ql_b_mvar": "float64", + "pl_c_mw": "float64", + "ql_c_mvar": "float64", + "i_a_hv_ka": "float64", + "i_a_lv_ka": "float64", + "i_b_hv_ka": "float64", + "i_b_lv_ka": "float64", + "i_c_hv_ka": "float64", + "i_c_lv_ka": "float64", + "loading_a_percent": "float64", + "loading_b_percent": "float64", + "loading_c_percent": "float64", + "loading_percent": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_ext_grid_3ph": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_a_mw\",\"q_a_mvar\",\"p_b_mw\",\"q_b_mvar\",\"p_c_mw\",\"q_c_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_a_mw": "float64", + "q_a_mvar": "float64", + "p_b_mw": "float64", + "q_b_mvar": "float64", + "p_c_mw": "float64", + "q_c_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_shunt_3ph": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[],\"index\":[],\"data\":[]}", + "orient": "split", + "is_multiindex": false, + "is_multicolumn": false + }, + "res_gen_3ph": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"vm_a_pu\",\"va_a_degree\",\"vm_b_pu\",\"va_b_degree\",\"vm_c_pu\",\"va_c_degree\",\"p_a_mw\",\"q_a_mvar\",\"p_b_mw\",\"q_b_mvar\",\"p_c_mw\",\"q_c_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "vm_a_pu": "float64", + "va_a_degree": "float64", + "vm_b_pu": "float64", + "va_b_degree": "float64", + "vm_c_pu": "float64", + "va_c_degree": "float64", + "p_a_mw": "float64", + "q_a_mvar": "float64", + "p_b_mw": "float64", + "q_b_mvar": "float64", + "p_c_mw": "float64", + "q_c_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_load_3ph": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_sgen_3ph": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_storage_3ph": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_mw\",\"q_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_mw": "float64", + "q_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_asymmetric_load_3ph": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_a_mw\",\"q_a_mvar\",\"p_b_mw\",\"q_b_mvar\",\"p_c_mw\",\"q_c_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_a_mw": "float64", + "q_a_mvar": "float64", + "p_b_mw": "float64", + "q_b_mvar": "float64", + "p_c_mw": "float64", + "q_c_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "res_asymmetric_sgen_3ph": { + "_module": "pandas.core.frame", + "_class": "DataFrame", + "_object": "{\"columns\":[\"p_a_mw\",\"q_a_mvar\",\"p_b_mw\",\"q_b_mvar\",\"p_c_mw\",\"q_c_mvar\"],\"index\":[],\"data\":[]}", + "orient": "split", + "dtype": { + "p_a_mw": "float64", + "q_a_mvar": "float64", + "p_b_mw": "float64", + "q_b_mvar": "float64", + "p_c_mw": "float64", + "q_c_mvar": "float64" + }, + "is_multiindex": false, + "is_multicolumn": false + }, + "user_pf_options": {} + } +} \ No newline at end of file