diff --git a/.github/ISSUE_TEMPLATE/01-bug-report.yml b/.github/ISSUE_TEMPLATE/01-bug-report.yml index 1812c7cdcd..0d65acb22f 100644 --- a/.github/ISSUE_TEMPLATE/01-bug-report.yml +++ b/.github/ISSUE_TEMPLATE/01-bug-report.yml @@ -7,7 +7,7 @@ body: - type: checkboxes id: checks attributes: - label: Bug report checklis + label: Bug report checklist options: - label: > Searched the [issues page](https://github.com/e2nIEE/pandapower/issues) for similar reports @@ -48,9 +48,9 @@ body: Please follow [this guide](https://matthewrocklin.com/blog/work/2018/02/28/minimal-bug-reports) on how to provide a minimal, copy-pastable example. placeholder: > - from pandapower.create import create_empty_network + from pandapower.network import pandapowerNet - net = create_empty_network() + net = pandapowerNet(name="Bug Report") ... render: python diff --git a/.github/workflows/github_test_action.yml b/.github/workflows/github_test_action.yml index d8b8dc59a8..456e5bedaf 100644 --- a/.github/workflows/github_test_action.yml +++ b/.github/workflows/github_test_action.yml @@ -30,7 +30,7 @@ jobs: timeout-minutes: 20 strategy: matrix: - python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: [ '3.11', '3.12', '3.13', '3.14' ] # Reminder: when removing the support of an old python version here, then don't forget to # remove it also in pyproject.toml 'requires-python' and the release pipelines group: [ 1, 2 ] @@ -41,13 +41,13 @@ jobs: with: python-version: ${{ matrix.python-version }} - name: Install dependencies - if: ${{ (matrix.python-version != '3.10') && (matrix.python-version != '3.11') }} + if: ${{matrix.python-version != '3.11'}} run: | uv sync --extra all --extra test --extra pandamodels --extra pgm uv pip install pytest-split uv pip uninstall juliacall - - name: Install dependencies - if: ${{ (matrix.python-version == '3.10') || (matrix.python-version == '3.11') }} + - name: Install dependencies # Test without pgm + if: ${{matrix.python-version == '3.11'}} run: | uv sync --extra all --extra test --extra pandamodels uv pip install pytest-split @@ -78,7 +78,7 @@ jobs: timeout-minutes: 30 strategy: matrix: - #python-version: ['3.10', '3.14'] + #python-version: ['3.11', '3.14'] # Reminder: when removing the support of an old python version here, then don't forget to # remove it also in pyproject.toml 'requires-python' and the release pipelines group: [ 1, 2, 3, 4 ] @@ -241,7 +241,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: [ '3.11', '3.12', '3.13', '3.14' ] steps: - uses: actions/checkout@v6 - name: Install uv @@ -251,11 +251,10 @@ jobs: - name: Install dependencies run: | uv sync --extra test - uv pip install setuptools - if [ -f requirements.txt ]; then uv pip install -r requirements.txt; fi + uv pip install setuptools uv pip install matplotlib uv pip install lxml - if ${{ matrix.python-version != '3.10' }}; then uv pip install numba; fi + if ${{ matrix.python-version != '3.11' }}; then uv pip install numba; fi - name: Install pandapipes and simbench run: | uv pip install git+https://github.com/e2nIEE/pandapipes@develop#egg=pandapipes @@ -278,7 +277,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ['3.10'] + python-version: [ '3.11' ] steps: - uses: actions/checkout@v6 - name: Install uv @@ -354,20 +353,25 @@ jobs: - name: List of installed packages run: | uv pip list - - name: Create test_schema in sandbox database + - name: Test postgresql available + # Test that will fail the pipeline if the tests would be skipped. Prevents pipeline success if psycopg import fails env: - PGPASSWORD: secret + DSN: 'postgresql://test_user:secret@localhost:55432/sandbox' + SCHEMA: 'test_schema' run: | - psql -h localhost -U test_user -d sandbox -c "CREATE SCHEMA test_schema AUTHORIZATION test_user" - psql -h localhost -U test_user --list sandbox + uv run python -c "import sys; from pandapower.test.api.test_sql_io import POSTGRESQL_AVAILABLE; sys.exit(not POSTGRESQL_AVAILABLE);" - name: Test pandapower File I/O + env: + DSN: 'postgresql://test_user:secret@localhost/sandbox' + SCHEMA: 'test_schema' run: | - uv run python -c "import os; import json; from pandapower import pp_dir; conn_data={'host': 'localhost', 'user': 'test_user', 'database': 'sandbox', 'password': 'secret', 'schema': 'test_schema'}; fp = open(os.path.join(pp_dir, 'test', 'test_files', 'postgresql_connect_data.json'), 'w'); json.dump(conn_data, fp); fp.close()" - uv run python -c "from pandapower import pp_dir; import pytest; import sys; import os; ec = pytest.main([os.path.join(pp_dir,'test','api','test_sql_io.py')]); sys.exit(ec)" + uv run pytest pandapower/test/api/test_sql_io.py - name: Test pandapower File I/O against alternative port + env: + DSN: 'postgresql://test_user:secret@localhost:55432/sandbox' + SCHEMA: 'test_schema' run: | - uv run python -c "import os; import json; from pandapower import pp_dir; conn_data={'host': 'localhost', 'user': 'test_user', 'database': 'sandbox', 'password': 'secret', 'schema': 'test_schema', 'port': 55432}; fp = open(os.path.join(pp_dir, 'test', 'test_files', 'postgresql_connect_data.json'), 'w'); json.dump(conn_data, fp); fp.close()" - uv run python -c "from pandapower import pp_dir; import pytest; import sys; import os; ec = pytest.main([os.path.join(pp_dir,'test','api','test_sql_io.py')]); sys.exit(ec)" + uv run pytest pandapower/test/api/test_sql_io.py tutorial_tests: needs: build @@ -417,7 +421,7 @@ jobs: - name: Check sphinx build uses: ammaraskar/sphinx-action@54e52bfb642e9b60ea5b6bcb05fe3f74b40d290a #8.2.3 with: - pre-build-command: "python -m pip install uv && uv pip install .[dev] --system --link-mode=copy" + pre-build-command: "python -m pip install uv && uv pip install .[docs] --system --link-mode=copy" build-command: "sphinx-build -b html . _build -W" docs-folder: "doc/" diff --git a/.github/workflows/test_release.yml b/.github/workflows/test_release.yml index 1470157a9f..7318e265bb 100644 --- a/.github/workflows/test_release.yml +++ b/.github/workflows/test_release.yml @@ -20,7 +20,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: [ '3.11', '3.12', '3.13', '3.14' ] os: [ ubuntu-latest, windows-latest ] group: [ 1, 2, 3, 4 ] @@ -41,16 +41,16 @@ jobs: - name: Install specific dependencies (Windows) if: matrix.os == 'windows-latest' run: | - if ( '${{ matrix.python-version }}' -eq '3.10' ) { uv pip install pypower } - if ( '${{ matrix.python-version }}' -ne '3.10' ) { uv pip install numba } - if ( '${{ matrix.python-version }}' -eq '3.11' ) { uv pip install lightsim2grid } + if ( '${{ matrix.python-version }}' -eq '3.11' ) { uv pip install pypower } + if ( '${{ matrix.python-version }}' -ne '3.11' ) { uv pip install numba } + if ( '${{ matrix.python-version }}' -eq '3.12' ) { uv pip install lightsim2grid } - name: Install specific dependencies (Ubuntu) if: matrix.os == 'ubuntu-latest' run: | - if ${{ matrix.python-version == '3.10' }}; then uv pip install pypower; fi - if ${{ matrix.python-version != '3.10' }}; then uv pip install numba; fi - if ${{ matrix.python-version == '3.11' }}; then uv pip install lightsim2grid; fi + if ${{ matrix.python-version == '3.11' }}; then uv pip install pypower; fi + if ${{ matrix.python-version != '3.11' }}; then uv pip install numba; fi + if ${{ matrix.python-version == '3.12' }}; then uv pip install lightsim2grid; fi - name: List all installed packages run: | diff --git a/.github/workflows/upload_release.yml b/.github/workflows/upload_release.yml index 8e58f711c6..4ba785ff4f 100644 --- a/.github/workflows/upload_release.yml +++ b/.github/workflows/upload_release.yml @@ -30,6 +30,10 @@ jobs: permissions: id-token: write + environment: + name: pypi + url: https://pypi.org/p/pandapower/ + # Steps represent a sequence of tasks that will be executed as part of the job steps: # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it @@ -38,7 +42,7 @@ jobs: # Sets up python3 - uses: actions/setup-python@v6 with: - python-version: '3.10' + python-version: '3.11' # Installs and upgrades pip, installs other dependencies and installs the package - name: Install dependencies @@ -70,8 +74,8 @@ jobs: - name: Build and Upload to PyPI if: ${{ inputs.upload_server == 'pypi' }} uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e #release/v1 - with: - repository-url: https://upload.pypi.org/legacy/ + #with: + # repository-url: https://upload.pypi.org/legacy/ #run: | # python3 -m build # python3 -m twine check dist/* --strict diff --git a/.gitignore b/.gitignore index 256218d85c..e381f1d6b6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *~ build/ .pytest_cache/ +.mypy_cache/ dist/ .cache/ .idea @@ -12,6 +13,7 @@ doc/api/ doc/.build/* doc/_build/* doc/elements/table_structures/ +doc/std_types/tables/ *.ipynb_checkpoints .cache *__pycache__* @@ -31,7 +33,7 @@ pandapower/timeseries/logs/* /.vscode/ pandapower/test/opf/buffer_file.json pandapower/test/buffer_file.json -/pandapower/test/test_files/postgresql_connect_data.json sonar-project.properties .scannerwork/ uv.lock +*.code-workspace diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 5394c58811..00b0b0d08c 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -4,9 +4,55 @@ Change Log [upcoming 4.0.0] - 2026-..-.. ------------------------------- - [ADDED] pandera network validation +- [CHANGED] removed deprecated trafo support characteristics table (replaced by trafo_characteristc_table) +- [ADDED] create function for trafo characteristic entries +- [ADDED] converter for trafo characteristics +- [FIXED] create_gen and create_gens now use connected bus voltage limits as generator voltage-limit defaults. [upcoming release] - 2026-..-.. ------------------------------- +- [FIXED] speedup dump_to_geojson_node_branch +- [FIXED] added the LV Schutterwald network to the network documentation. +- [FIXED] restored ``OpenDSSDirect.py`` to the ``all``/``dev`` extras so the OpenDSS converter is exercised (and its coverage reported) in CI again; it was dropped in #3062 because installing it alongside ``pytest~=9.1`` crashed the pytest process on Windows. Root cause (see `dss-extensions/OpenDSSDirect.py#148 `_): pytest enables Python's ``faulthandler`` by default, which intercepts a first-chance Windows structured exception that OpenDSSDirect.py's native backend raises -- and normally handles itself -- during import, and misreports it as fatal. Bracketing the import with ``faulthandler.disable()``/``.enable()`` avoids the false crash while leaving ``faulthandler`` protecting the rest of the test run; the converter now runs on Windows instead of merely skipping there. +- [ADDED] OpenDSS converter: series (bus-to-bus) ``Reactor`` elements are now imported as a fixed-impedance ``line``, the pattern some feeder libraries (e.g. EPRI's Ckt5/Ckt7) use to model the substation's Thevenin-equivalent source impedance instead of a ``Transformer``. +- [FIXED] impedance element docs: the ``z_tf`` equation used ``rft_pu`` instead of ``rtf_pu``, and the ``gt_pu``/``bt_pu``/``gt0_pu``/``bt0_pu`` shunt parameters were described as being at the ``from_bus`` instead of the ``to_bus``. +- [FIXED] fixed tutorial for plotting with pyqt + +[3.5.4] - 2026-07-08 +------------------------------- +- [UPDATED] versions of dependent libraries +- [FIXED] improved from_json even further + +[3.5.3] - 2026-07-07 +------------------------------- +- [FIXED] moved a misleading logger.warning message + +[3.5.2] - 2026-07-06 +------------------------------- +- [REMOVED] direct dependency to helmpy, if you want to use it, you have to install it manually or uncomment the line in pyproject.toml. Otherwise pypi.org rejects the package. + +[3.5.1] - 2026-07-06 +------------------------------- +- [FIXED] upload pipeline + +[3.5.0] - 2026-07-06 +------------------------------- +- [ADDED] toolbox: :code:`compute_switch_flows` computes power flow through zero-impedance bus-bus switches via nodal balance +- [ADDED] Redispatch optimizer based on a custom formulation in pandamodels.jl, supports cost based and power based redispatch. +- [ADDED] OpenDSS converter: ``from_opendss`` imports an OpenDSS feeder into a balanced positive-sequence pandapower net (revisits #1442). +- [FIXED] OPF: fixed generalized cost Hessian evaluation in ``opf_costfcn``. +- [FIXED] kwargs handling in pandamodels functions +- [ADDED] added optional init_pq parameter to the PandaModels opf runpm wrappers for active/reactive power starts +- [ADDED] zigzag earthing transformer vector groups (`ZNyn`, `ZNd`, `ZNy`, `ZN`) in the zero-sequence short-circuit model. Per the YNzn zero-sequence model (IEC / transformer references), the zigzag winding completely decouples the primary and secondary zero sequences, presenting a low-impedance shunt-to-ground at its terminal: the HV-winding leakage portion (`si0_hv_partial`) in series with the neutral earthing impedance `3·(rn_ohm + j·xn_ohm)` at the star point. An earthed-wye secondary (`ZNyn`) gets its own decoupled zero-sequence path to ground (LV leakage portion + zero-sequence magnetising); a delta / unearthed secondary (`ZNd`/`ZNy`) has none. Enables single-phase earth-fault studies on delta-fed systems earthed via a zigzag transformer. +- [ADDED] `rn_ohm` (resistive neutral earthing impedance) for two-winding transformers; the neutral earthing impedance `(rn_ohm + j*xn_ohm)` is now applied as `3*Z_N` to the zero sequence of all earthed-star vector groups in the short-circuit calculation (previously `xn_ohm` was only considered for power station unit transformers, and only as a reactance). Enables modelling neutral earthing resistors (NER/NGR) for single-phase earth-fault studies. +- [FIXED] gen for 3ph load flow, so load flow is run without errors. +- [ADDED] hardening of from_json function with a white list +- [FIXED] (Log)SplineCharacteristic now have their own de-/serializer +- [ADDED] HELMpy as an additional solver +- [CHANGED] sql_io now requires single dsn string for database connection instead of separate host, user, etc. keywords. +- [FIXED] sql_io not working with version of psycopg installed +- [FIXED] cim2pp add GeographicalRegion from eqbd profile +- [FIXED] behavior of PowerFactory2pp-converter if coordinates are saved at cpSubstat. - [CHANGED] differing behavior for :code:`in_service=False` depending on `voltage_depend_loads` has been fixed. Out of Service elements for res_bus now always contain `nan` as result. - [FIXED] `DiscreteTapControl` to work with `negative tap_step_percent`. - [CHANGED] updated the contributing file and documentation @@ -17,17 +63,21 @@ Change Log - [ADDED] check to check if vkr_percent values are reasonable (see issue #786). - [FIXED] cim2pp shift_lv_degree was translated from wrong entry - [FIXED] UnboundLocalError in _from_ppc_branch when creating impedance elements +- [FIXED] incompatible network versions (i.e. networks with newer format versions than the currently installed pandapower version) are identified in convert_format and raise an error - [ADDED] LTDS support - [FIXED] ucte2pp: voltage setpoints from gens connected to the same busbar are now averaged - [FIXED] ucte2pp: small X values are clipped to 0.05 Ohm (according to UCTE-DEF) to increase convergence - [FIXED] ucte2pp: symmetrical tap changers are now handled as symmetrical tap changers in pandapower (not ideal phase shifters) - [FIXED] ucte2pp: prevent nan values for impedances and transformers for B/G/P_fe/i0 -- [FIXED] cim2pp: CimConverter backwards-compatible (default value for cin_version) +- [FIXED] cim2pp: CimConverter backwards-compatible (default value for cim_version) - [FIXED] jao converter: calculation of trafo parameters is based on primary side (hv) now - [ADDED] toolbox: :code:`get_all_elements` returns all elements of a pp.pandapowerNet as a DataFrame - [ADDED] plotting: highlighting feature and hovering functionality to :code:`simple_plot()` - [FIXED] pf2pp: :code:`app.GetActiveScenario().loc_name` can be :code:`None` +- [FIXED] pf2pp: static generator without a bus does not cause import errors (it is ignored) - [ADDED] plotting: added a toggleable colormap feature to :code:`simple_plot` +- [ADDED] introducing pypowsybl to pandapower converter +- [ADDED] cim2pp: mapping SV data to the assets with flag use_sv_data_for_assets=True [3.4.0] - 2026-02-09 ------------------------------- diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index aedaa6165b..b71e841c72 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -192,7 +192,7 @@ tests for the toolbox functions. You then add a new test function to this file: ```python def test_double_rated_power(): init_sn_mva = 100 - net = create_empty_network(sn_mva=init_sn_mva) + net = pandapowerNet(name="test_double_rated_power", sn_mva=init_sn_mva) assert net.sn_mva == init_sn_mva double_rated_power(net) assert net.sn_mva == init_sn_mva*2 diff --git a/README.rst b/README.rst index 268c2f2feb..91371c5fdb 100644 --- a/README.rst +++ b/README.rst @@ -87,17 +87,17 @@ pandapower's sibling project *pandapipes*. `pandapipes.org `__. +`contribution guidelines `__. diff --git a/doc/_exts/gen_csv.py b/doc/_exts/save_element_tables.py similarity index 100% rename from doc/_exts/gen_csv.py rename to doc/_exts/save_element_tables.py diff --git a/doc/_exts/save_std_types.py b/doc/_exts/save_std_types.py new file mode 100644 index 0000000000..5923e7db0c --- /dev/null +++ b/doc/_exts/save_std_types.py @@ -0,0 +1,42 @@ +""" +Created on Fri Jul 1 13:40:07 2016 + +@author: thurner +""" +import os +import shutil + +from pandapower.std_types import available_std_types +from pandapower.network import pandapowerNet +from pandapower.network_structure import get_structure_dict + +file_path = os.path.dirname(os.path.realpath(__file__)) + + +def save_std_types(_): + path = os.path.join(file_path, '..', 'std_types', 'tables') + if os.path.isdir(path): + shutil.rmtree(path) + os.makedirs(path) + print("Generating std_types csv files") + + structure_dict = get_structure_dict(required_only=False) + net = pandapowerNet(name="save_pp_std_types") + + for type_ in net.std_types.keys(): + types = available_std_types(net, type_) + if type_ in structure_dict: + columns = [c for c in types.columns if structure_dict[type_]] + else: + columns = types.columns # fuse is not contained in structure dict + types = types.reindex(columns, axis=1) + types.to_csv(os.path.join(path, f"{type_}_std_types.csv"), sep=";") + + +def setup(app): + app.connect('builder-inited', save_std_types) + return {'version': '0.1'} + + +if __name__ == '__main__': + save_std_types(None) diff --git a/doc/conf.py b/doc/conf.py index ab3ee2c017..169cd63f0a 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # jsonpy documentation build configuration file, created by # sphinx-quickstart on Fri May 03 17:30:42 2013. # @@ -37,7 +35,9 @@ 'sphinx.ext.napoleon', 'sphinxcontrib.imagesvg', 'numpydoc', - 'gen_csv', + # custom extensions: + 'save_element_tables', + 'save_std_types' ] # sphinx.ext.mathjax and sphinx.ext.imgmath don't comply with each other any more. diff --git a/doc/converter.rst b/doc/converter.rst index 1bd8d15a2d..c52f6bbb30 100644 --- a/doc/converter.rst +++ b/doc/converter.rst @@ -18,4 +18,5 @@ These tools are: converter/cgmes converter/ucte converter/jao + converter/opendss diff --git a/doc/converter/opendss.rst b/doc/converter/opendss.rst new file mode 100644 index 0000000000..0eefc52b0b --- /dev/null +++ b/doc/converter/opendss.rst @@ -0,0 +1,22 @@ +================== +OpenDSS +================== + +.. _converter_opendss: + +The following function imports an `OpenDSS `_ +feeder into a **balanced (positive-sequence)** pandapower net. It reads the circuit +through ``OpenDSSDirect.py`` (an optional dependency, ``pip install pandapower[opendss]``), +mapping buses, lines (with their LineCode carried through as ``std_type``), +series (bus-to-bus) reactors (the pattern some feeder libraries use to model the +substation's source impedance), two-winding transformers (at their solved tap), +center-tapped split-phase service transformers (collapsed to a two-winding +equivalent), loads, shunt capacitors, switches and the source. + +Positive-sequence is exact for symmetric (e.g. European 3-phase 4-wire) feeders and +a documented approximation for unsymmetrical North-American topology (single-phase +laterals, split-phase), which pandapower's ``runpp_3ph`` cannot represent (see issue +#873). The returned net carries an ``opendss_import`` report with element counts, +per-bus phase counts, the OpenDSS-solved voltages and the approximations made. + +.. autofunction:: pandapower.converter.opendss.from_opendss diff --git a/doc/elements/empty_network.rst b/doc/elements/empty_network.rst index cffd79a6f4..184439d12c 100644 --- a/doc/elements/empty_network.rst +++ b/doc/elements/empty_network.rst @@ -5,4 +5,4 @@ Empty Network Create Function ================== -.. autofunction:: pandapower.create.create_empty_network +.. autoclass:: pandapower.network.pandapowerNet diff --git a/doc/elements/impedance.rst b/doc/elements/impedance.rst index 24b80f7390..cd9902014e 100644 --- a/doc/elements/impedance.rst +++ b/doc/elements/impedance.rst @@ -42,7 +42,7 @@ The per unit values are therefore transformed into the network per unit system: \begin{align*} \underline{z}_{ft} &= (rft\_pu + j \cdot xft\_pu) \cdot \frac{S_{N}}{sn\_mva} \\ - \underline{z}_{tf} &= (rft\_pu + j \cdot xtf\_pu) \cdot \frac{S_{N}}{sn\_mva} \\ + \underline{z}_{tf} &= (rtf\_pu + j \cdot xtf\_pu) \cdot \frac{S_{N}}{sn\_mva} \\ \end{align*} where :math:`S_{N}` is the reference power of the per unit system (see :ref:`Unit Systems and Conventions`). diff --git a/doc/elements/impedance_par.csv b/doc/elements/impedance_par.csv new file mode 100644 index 0000000000..e69de29bb2 diff --git a/doc/elements/motor_par.csv b/doc/elements/motor_par.csv new file mode 100644 index 0000000000..e69de29bb2 diff --git a/doc/elements/switch.rst b/doc/elements/switch.rst index 5fee56be39..383c626363 100644 --- a/doc/elements/switch.rst +++ b/doc/elements/switch.rst @@ -1,4 +1,4 @@ -.. _switch_model: +.. _switch_model: ============= Switch @@ -38,6 +38,13 @@ This has the following advantages compared to modelling the switch as a small im - there is no voltage drop over the switch (ideal switch) - no convergence problems due to small impedances / large admittances - less buses in the admittance matrix + +.. note:: + + Because fused buses share one internal node, ``res_switch`` contains NaN for + bus-bus switches with ``z_ohm=0`` after ``runpp()``. To compute the power + flow through these switches via nodal balance, call + :func:`pandapower.toolbox.compute_switch_flows` after the load flow. *Bus-Element-Switches:* diff --git a/doc/networks.rst b/doc/networks.rst index b1898c2b3b..81514f0ee6 100644 --- a/doc/networks.rst +++ b/doc/networks.rst @@ -8,10 +8,11 @@ Besides creating your own grids using pandapower functions, pandapower provides benchmark grids through the networks module. The pandapower networks module contains example grids, simple test grids, randomly generated grids, CIGRE test grids, -IEEE case files (including 3-phase grids) and synthetic low voltage grids from Georg Kerber, Lindner et. al. and Dickert -et. al. If you want to evaluate your algorithms on benchmark grids with corresponding full-year load, generation, and -storage profiles or want to publish your results in a reproducible manner, we recommend the SimBench repository -(`SimBench Homepage `__, `SimBench GitHub Repository `__). +IEEE case files (including 3-phase grids), the LV Schutterwald network and synthetic low voltage grids from Georg +Kerber, Lindner et. al. and Dickert et. al. If you want to evaluate your algorithms on benchmark grids with +corresponding full-year load, generation, and storage profiles or want to publish your results in a reproducible +manner, we recommend the SimBench repository (`SimBench Homepage `__, `SimBench GitHub +Repository `__). You can find documentation for the individual network modules of pandapower here: @@ -22,6 +23,7 @@ You can find documentation for the individual network modules of pandapower here networks/test networks/cigre networks/mv_oberrhein + networks/lv_schutterwald networks/power_system_test_cases networks/kerber networks/synthetic_voltage_control_lv_networks diff --git a/doc/networks/lv_schutterwald.rst b/doc/networks/lv_schutterwald.rst new file mode 100644 index 0000000000..01e9924bc9 --- /dev/null +++ b/doc/networks/lv_schutterwald.rst @@ -0,0 +1,10 @@ +======================= +LV Schutterwald Network +======================= + +The LV Schutterwald network is a generic 0.4 kV network supplied by 14 MV/LV transformer stations of the +Oberrhein network. It represents 1,506 customers and can optionally include 1,251 heat pumps from the +`underlying study `_. The network also contains geographical information for +its buses and lines and can be returned as 14 separate subnetworks. + +.. autofunction:: pandapower.networks.lv_schutterwald diff --git a/doc/networks/mv_oberrhein.rst b/doc/networks/mv_oberrhein.rst index ff6508d80a..ce18f8d0f5 100644 --- a/doc/networks/mv_oberrhein.rst +++ b/doc/networks/mv_oberrhein.rst @@ -14,7 +14,7 @@ The geographical representation of the network looks like this: :width: 30em :align: center -The different colors of the MV/LV stations indicate the feeders which are galvanically seperated by open switches. +The different colors of the MV/LV stations indicate the feeders which are galvanically separated by open switches. If you are interested in how to make plots such as these, check out the pandapower tutorial on plotting. The power flow results of the network in the different worst case scenarios look like this: diff --git a/doc/opf/pandamodels.rst b/doc/opf/pandamodels.rst index a1f9b0bd1b..dc283d32d8 100644 --- a/doc/opf/pandamodels.rst +++ b/doc/opf/pandamodels.rst @@ -115,6 +115,41 @@ The usage is explained in the `PandaModels tutorial `_. Additional packages including "juniper" .. autofunction:: pandapower.runpm_tnep diff --git a/doc/powerflow/ac.rst b/doc/powerflow/ac.rst index b7ea6fa7c0..d4441cb73a 100644 --- a/doc/powerflow/ac.rst +++ b/doc/powerflow/ac.rst @@ -14,6 +14,23 @@ pandapower uses PYPOWER to solve the power flow problem: :code:`net["_ppc"]`. However all necessary information is written into the pandapower format net, so the pandapower user should not usually have to deal with pypower. +Possible Solver Algorithm +----------------------------- + +Pandapower brings a variety of powerflow algorithm, some through the pypower core, other via external libraries: + +- "nr" Newton-Raphson (pypower implementation with numba accelerations) +- "iwamoto_nr" Newton-Raphson with Iwamoto multiplier (maybe slower than NR but more robust) +- "bfsw" backward/forward sweep (specially suited for radial and weakly-meshed networks) +- "gs" gauss-seidel (pypower implementation) +- "fdbx" fast-decoupled (pypower implementation) +- "fdxb" fast-decoupled (pypower implementation) +- "helm" holomorphic embedded loadflow method (Copyright (C) 2019 Tulio Molina tuliojose8@gmail.com and Juan José Ortega juanjoseop10@gmail.com) + +An algorithm might be better suited for a peculiar use case. The user can select an algorithm via :code:`runpp(algorithm="nr", ...)` +If the algorithm option is not given, "nr" is selected as a default. Please note, not all features / power system elements +are equally supported in all solvers. The most feature complete at the moment is "nr". + Accelerating Packages ------------------------- diff --git a/doc/std_types/basic.rst b/doc/std_types/basic.rst index ccf2a5d402..49b85e8932 100644 --- a/doc/std_types/basic.rst +++ b/doc/std_types/basic.rst @@ -8,13 +8,12 @@ Every pandapower network comes with a default set of standard types. The pandapower standard types are compatible with 50 Hz systems, please be aware that the standard type values might not be realistic for 60 Hz (or other) power systems. Lines --------- +----- -.. tabularcolumns:: |l|l|l|l|l|l|l|l| -.. csv-table:: - :file: linetypes.csv - :delim: ; - :widths: 60, 15, 15, 15, 15, 15, 15, 15 +.. csv-table:: + :file: tables/line_std_types.csv + :header-rows: 1 + :delim: ; .. note :: To add the optional column "alpha" to net.line that is used if power flow is calculated for a different line temperature than 20 °C, use the function pp.add_temperature_coefficient() @@ -22,30 +21,34 @@ Lines DC Lines -------- -.. tabularcolumns:: |l|l|l|l|l|l| .. csv-table:: - :file: linedctypes.csv - :delim: ; - :widths: 60, 15, 15, 15, 15, 15 + :file: tables/line_dc_std_types.csv + :header-rows: 1 + :delim: ; .. note :: To add the optional column "alpha" to net.line_dc that is used if power flow is calculated for a different line temperature than 20 °C, use the function pp.add_temperature_coefficient() Transformers ------------------ +------------ -.. tabularcolumns:: |l|l|l|l|l|l|l|l|l|l|l|l|l|l|l|l| .. csv-table:: - :file: trafotypes.csv - :delim: ; - :widths: 60, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15 + :file: tables/trafo_std_types.csv + :header-rows: 1 + :delim: ; Three Winding Transformers --------------------------------- +-------------------------- -.. tabularcolumns:: |l|l|l|l|l|l|l|l|l|l|l||l|l|l|l|l|l|l|l|l|l|l|l| .. csv-table:: - :file: trafo3wtypes.csv - :delim: ; - :widths: 60, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15 + :file: tables/trafo3w_std_types.csv + :header-rows: 1 + :delim: ; +Fuse +---- + +.. csv-table:: + :file: tables/fuse_std_types.csv + :header-rows: 1 + :delim: ; diff --git a/doc/std_types/linedctypes.csv b/doc/std_types/linedctypes.csv deleted file mode 100644 index 5d38cf4e9f..0000000000 --- a/doc/std_types/linedctypes.csv +++ /dev/null @@ -1,9 +0,0 @@ -;r_ohm_per_km;max_i_ka;type;q_mm2;alpha -95-CU;0.193; 0.404;cs;95;0.00393 -400-CU;0.0470;0.922;cs;400;0.00393 -1200-CU;0.0151;1.791;cs;1200;0.00393 -2400-CU;0.0073;2.678;cs;2400;0.00393 -95-AL;0.32;0.310;cs;95;0.00403 -400-AL;0.0778;0.705;cs;400;0.00403 -1200-AL;0.0247;1.371;cs;1200;0.00403 -2400-AL;0.0121;2.066;cs;2400;0.00403 diff --git a/doc/std_types/linetypes.csv b/doc/std_types/linetypes.csv deleted file mode 100644 index 7a69aa37e5..0000000000 --- a/doc/std_types/linetypes.csv +++ /dev/null @@ -1,52 +0,0 @@ -;r_ohm_per_km;x_ohm_per_km;c_nf_per_km;max_i_ka;type;q_mm2;alpha -NAYY 4x50 SE;0.642;0.083;210.0;0.142;cs;50;0.00403 -NAYY 4x120 SE;0.225;0.08;264.0;0.242;cs;120;0.00403 -NAYY 4x150 SE;0.208;0.08;261.0;0.27;cs;150;0.00403 -NA2XS2Y 1x95 RM/25 12/20 kV;0.313;0.132;216.0;0.252;cs;95;0.00403 -NA2XS2Y 1x185 RM/25 12/20 kV;0.161;0.117;273.0;0.362;cs;185;0.00403 -NA2XS2Y 1x240 RM/25 12/20 kV;0.122;0.112;304.0;0.421;cs;240;0.00403 -NA2XS2Y 1x95 RM/25 6/10 kV;0.313;0.123;315.0;0.249;cs;95;0.00403 -NA2XS2Y 1x185 RM/25 6/10 kV;0.161;0.11;406.0;0.358;cs;185;0.00403 -NA2XS2Y 1x240 RM/25 6/10 kV;0.122;0.105;456.0;0.416;cs;240;0.00403 -NA2XS2Y 1x150 RM/25 12/20 kV;0.206;0.116;250.0;0.319;cs;150;0.00403 -NA2XS2Y 1x120 RM/25 12/20 kV;0.253;0.119;230.0;0.283;cs;120;0.00403 -NA2XS2Y 1x70 RM/25 12/20 kV;0.443;0.132;190.0;0.22;cs;70;0.00403 -NA2XS2Y 1x150 RM/25 6/10 kV;0.206;0.11;360.0;0.315;cs;150;0.00403 -NA2XS2Y 1x120 RM/25 6/10 kV;0.253;0.113;340.0;0.28;cs;120;0.00403 -NA2XS2Y 1x70 RM/25 6/10 kV;0.443;0.123;280.0;0.217;cs;70;0.00403 -N2XS(FL)2Y 1x120 RM/35 64/110 kV;0.153;0.166;112.0;0.366;cs;120;0.00393 -N2XS(FL)2Y 1x185 RM/35 64/110 kV;0.099;0.156;125.0;0.457;cs;185;0.00393 -N2XS(FL)2Y 1x240 RM/35 64/110 kV;0.075;0.149;135.0;0.526;cs;240;0.00393 -N2XS(FL)2Y 1x300 RM/35 64/110 kV;0.06;0.144;144.0;0.588;cs;300;0.00393 -15-AL1/3-ST1A 0.4;1.8769;0.35;11.0;0.105;ol;16;0.00403 -24-AL1/4-ST1A 0.4;1.2012;0.335;11.25;0.14;ol;24;0.00403 -48-AL1/8-ST1A 0.4;0.5939;0.3;12.2;0.21;ol;48;0.00403 -94-AL1/15-ST1A 0.4;0.306;0.29;13.2;0.35;ol;94;0.00403 -34-AL1/6-ST1A 10.0;0.8342;0.36;9.7;0.17;ol;34;0.00403 -48-AL1/8-ST1A 10.0;0.5939;0.35;10.1;0.21;ol;48;0.00403 -70-AL1/11-ST1A 10.0;0.4132;0.339;10.4;0.29;ol;70;0.00403 -94-AL1/15-ST1A 10.0;0.306;0.33;10.75;0.35;ol;94;0.00403 -122-AL1/20-ST1A 10.0;0.2376;0.323;11.1;0.41;ol;122;0.00403 -149-AL1/24-ST1A 10.0;0.194;0.315;11.25;0.47;ol;149;0.00403 -34-AL1/6-ST1A 20.0;0.8342;0.382;9.15;0.17;ol;34;0.00403 -48-AL1/8-ST1A 20.0;0.5939;0.372;9.5;0.21;ol;48;0.00403 -70-AL1/11-ST1A 20.0;0.4132;0.36;9.7;0.29;ol;70;0.00403 -94-AL1/15-ST1A 20.0;0.306;0.35;10.0;0.35;ol;94;0.00403 -122-AL1/20-ST1A 20.0;0.2376;0.344;10.3;0.41;ol;122;0.00403 -149-AL1/24-ST1A 20.0;0.194;0.337;10.5;0.47;ol;149;0.00403 -184-AL1/30-ST1A 20.0;0.1571;0.33;10.75;0.535;ol;184;0.00403 -243-AL1/39-ST1A 20.0;0.1188;0.32;11.0;0.645;ol;243;0.00403 -48-AL1/8-ST1A 110.0;0.5939;0.46;8.0;0.21;ol;48;0.00403 -70-AL1/11-ST1A 110.0;0.4132;0.45;8.4;0.29;ol;70;0.00403 -94-AL1/15-ST1A 110.0;0.306;0.44;8.65;0.35;ol;94;0.00403 -122-AL1/20-ST1A 110.0;0.2376;0.43;8.5;0.41;ol;122;0.00403 -149-AL1/24-ST1A 110.0;0.194;0.41;8.75;0.47;ol;149;0.00403 -184-AL1/30-ST1A 110.0;0.1571;0.4;8.8;0.535;ol;184;0.00403 -243-AL1/39-ST1A 110.0;0.1188;0.39;9.0;0.645;ol;243;0.00403 -305-AL1/39-ST1A 110.0;0.0949;0.38;9.2;0.74;ol;305;0.00403 -490-AL1/64-ST1A 110.0;0.059;0.37;9.75;0.96;ol;490;0.00403 -679-AL1/86-ST1A 110.0;0.042;0.36;9.95;1.15;ol;679;0.00403 -490-AL1/64-ST1A 220.0;0.059;0.285;10.0;0.96;ol;490;0.00403 -679-AL1/86-ST1A 220.0;0.042;0.275;11.7;1.15;ol;679;0.00403 -490-AL1/64-ST1A 380.0;0.059;0.253;11.0;0.96;ol;490;0.00403 -679-AL1/86-ST1A 380.0;0.042;0.25;14.6;1.15;ol;679;0.00403 diff --git a/doc/std_types/save_pp_std_types.py b/doc/std_types/save_pp_std_types.py deleted file mode 100644 index 50502fd607..0000000000 --- a/doc/std_types/save_pp_std_types.py +++ /dev/null @@ -1,23 +0,0 @@ -# -*- coding: utf-8 -*- -""" -Created on Fri Jul 1 13:40:07 2016 - -@author: thurner -""" -from pandapower.std_types import available_std_types -from pandapower.create import create_empty_network - -net = create_empty_network() - -linetypes = available_std_types(net, "line") -columns = [c for c in net.line.columns if c in linetypes.columns] + ["q_mm2", "alpha"] -linetypes = linetypes.reindex(columns, axis=1) -linetypes.to_csv("linetypes.csv", sep=";") - -trafotypes = available_std_types(net, "trafo") -trafotypes = trafotypes.reindex([c for c in net.trafo.columns if c in trafotypes.columns], axis=1) -trafotypes.to_csv("trafotypes.csv", sep=";") - -trafo3wtypes = available_std_types(net, "trafo3w") -trafo3wtypes = trafo3wtypes.reindex([c for c in net.trafo3w.columns if c in trafo3wtypes.columns], axis=1) -trafo3wtypes.to_csv("trafo3wtypes.csv", sep=";") diff --git a/doc/std_types/trafo3wtypes.csv b/doc/std_types/trafo3wtypes.csv deleted file mode 100644 index 9dafc0d8f3..0000000000 --- a/doc/std_types/trafo3wtypes.csv +++ /dev/null @@ -1,3 +0,0 @@ -;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_changer_type -63/25/38 MVA 110/20/10 kV;63;25;38;110;20;10;10.4;10.4;10.4;0.28;0.32;0.35;35;0.89;0;0;hv;0;-10;10;1.2;Ratio -63/25/38 MVA 110/10/10 kV;63;25;38;110;10;10;10.4;10.4;10.4;0.28;0.32;0.35;35;0.89;0;0;hv;0;-10;10;1.2;Ratio diff --git a/doc/std_types/trafotypes.csv b/doc/std_types/trafotypes.csv deleted file mode 100644 index daa3876589..0000000000 --- a/doc/std_types/trafotypes.csv +++ /dev/null @@ -1,15 +0,0 @@ -;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_changer_type -160 MVA 380/110 kV;160.0;380.0;110.0;12.2;0.25;60.0;0.06;0;hv;0;-9;9;1.5;0;Ratio -100 MVA 220/110 kV;100.0;220.0;110.0;12.0;0.26;55.0;0.06;0;hv;0;-9;9;1.5;0;Ratio -63 MVA 110/20 kV;63.0;110.0;20.0;18.0;0.32;22.0;0.04;150;hv;0;-9;9;1.5;0;Ratio -40 MVA 110/20 kV;40.0;110.0;20.0;16.2;0.34;18.0;0.05;150;hv;0;-9;9;1.5;0;Ratio -25 MVA 110/20 kV;25.0;110.0;20.0;12.0;0.41;14.0;0.07;150;hv;0;-9;9;1.5;0;Ratio -63 MVA 110/10 kV;63.0;110.0;10.0;18.0;0.32;22.0;0.04;150;hv;0;-9;9;1.5;0;Ratio -40 MVA 110/10 kV;40.0;110.0;10.0;16.2;0.34;18.0;0.05;150;hv;0;-9;9;1.5;0;Ratio -25 MVA 110/10 kV;25.0;110.0;10.0;12.0;0.41;14.0;0.07;150;hv;0;-9;9;1.5;0;Ratio -0.25 MVA 20/0.4 kV;0.25;20.0;0.4;6.0;1.44;0.8;0.32;150;hv;0;-2;2;2.5;0;Ratio -0.4 MVA 20/0.4 kV;0.4;20.0;0.4;6.0;1.425;1.35;0.3375;150;hv;0;-2;2;2.5;0;Ratio -0.63 MVA 20/0.4 kV;0.63;20.0;0.4;6.0;1.206;1.65;0.2619;150;hv;0;-2;2;2.5;0;Ratio -0.25 MVA 10/0.4 kV;0.25;10.0;0.4;4.0;1.2;0.6;0.24;150;hv;0;-2;2;2.5;0;Ratio -0.4 MVA 10/0.4 kV;0.4;10.0;0.4;4.0;1.325;0.95;0.2375;150;hv;0;-2;2;2.5;0;Ratio -0.63 MVA 10/0.4 kV;0.63;10.0;0.4;4.0;1.0794;1.18;0.1873;150;hv;0;-2;2;2.5;0;Ratio diff --git a/doc/toolbox.rst b/doc/toolbox.rst index c69421e7ed..626a7602bc 100644 --- a/doc/toolbox.rst +++ b/doc/toolbox.rst @@ -47,6 +47,8 @@ Result Information .. autofunction:: pandapower.toolbox.clear_result_tables +.. autofunction:: pandapower.toolbox.compute_switch_flows + .. autofunction:: pandapower.toolbox.res_power_columns ==================================== diff --git a/doc/topology/examples.rst b/doc/topology/examples.rst index 848f6d892a..8c14ab235e 100644 --- a/doc/topology/examples.rst +++ b/doc/topology/examples.rst @@ -11,49 +11,49 @@ Here are a few examples of what you can do: .. code:: python - from pandapower.create import create_empty_network, create_bus, create_ext_grid, create_line, \ - create_transformer_from_parameters, create_load, create_switch - - net = create_empty_network() - - create_bus(net, name = "110 kV bar", vn_kv = 110, type = 'b') - create_bus(net, name = "20 kV bar", vn_kv = 20, type = 'b') - create_bus(net, name = "bus 2", vn_kv = 20, type = 'b') - create_bus(net, name = "bus 3", vn_kv = 20, type = 'b') - create_bus(net, name = "bus 4", vn_kv = 20, type = 'b') - create_bus(net, name = "bus 5", vn_kv = 20, type = 'b') - create_bus(net, name = "bus 6", vn_kv = 20, type = 'b') - - create_ext_grid(net, 0, vm_pu = 1) - - create_line(net, name = "line 0", from_bus = 1, to_bus = 2, length_km = 1, std_type = "NAYY 4x150 SE") - create_line(net, name = "line 1", from_bus = 2, to_bus = 3, length_km = 1, std_type = "NAYY 4x150 SE") - create_line(net, name = "line 2", from_bus = 3, to_bus = 4, length_km = 1, std_type = "NAYY 4x150 SE") - create_line(net, name = "line 3", from_bus = 4, to_bus = 5, length_km = 1, std_type = "NAYY 4x150 SE") - create_line(net, name = "line 4", from_bus = 5, to_bus = 6, length_km = 1, std_type = "NAYY 4x150 SE") - create_line(net, name = "line 5", from_bus = 6, to_bus = 1, length_km = 1, std_type = "NAYY 4x150 SE") - - create_transformer_from_parameters(net, hv_bus=0, lv_bus=1, i0_percent=0.038, pfe_kw=11.6, - vkr_percent=0.322, sn_mva=40, vn_lv_kv=22.0, vn_hv_kv=110.0, vk_percent=17.8) - - create_load(net, 2, p_mw = 1, q_mvar = 0.2, name = "load 0") - create_load(net, 3, p_mw = 1, q_mvar = 0.2, name = "load 1") - create_load(net, 4, p_mw = 1, q_mvar = 0.2, name = "load 2") - create_load(net, 5, p_mw = 1, q_mvar = 0.2, name = "load 3") - create_load(net, 6, p_mw = 1, q_mvar = 0.2, name = "load 4") - - create_switch(net, bus = 1, element = 0, et = 'l') - create_switch(net, bus = 2, element = 0, et = 'l') - create_switch(net, bus = 2, element = 1, et = 'l') - create_switch(net, bus = 3, element = 1, et = 'l') - create_switch(net, bus = 3, element = 2, et = 'l') - create_switch(net, bus = 4, element = 2, et = 'l') - create_switch(net, bus = 4, element = 3, et = 'l', closed = 0) - create_switch(net, bus = 5, element = 3, et = 'l') - create_switch(net, bus = 5, element = 4, et = 'l') - create_switch(net, bus = 6, element = 4, et = 'l') - create_switch(net, bus = 6, element = 5, et = 'l') - create_switch(net, bus = 1, element = 5, et = 'l') + from pandapower.create import create_bus, create_ext_grid, create_line, create_transformer_from_parameters, create_load, create_switch + from pandapower.network import pandapowerNet + + net = pandapowerNet(name="basic example network") + + create_bus(net, name = "110 kV bar", vn_kv = 110, type = 'b') + create_bus(net, name = "20 kV bar", vn_kv = 20, type = 'b') + create_bus(net, name = "bus 2", vn_kv = 20, type = 'b') + create_bus(net, name = "bus 3", vn_kv = 20, type = 'b') + create_bus(net, name = "bus 4", vn_kv = 20, type = 'b') + create_bus(net, name = "bus 5", vn_kv = 20, type = 'b') + create_bus(net, name = "bus 6", vn_kv = 20, type = 'b') + + create_ext_grid(net, 0, vm_pu = 1) + + create_line(net, name = "line 0", from_bus = 1, to_bus = 2, length_km = 1, std_type = "NAYY 4x150 SE") + create_line(net, name = "line 1", from_bus = 2, to_bus = 3, length_km = 1, std_type = "NAYY 4x150 SE") + create_line(net, name = "line 2", from_bus = 3, to_bus = 4, length_km = 1, std_type = "NAYY 4x150 SE") + create_line(net, name = "line 3", from_bus = 4, to_bus = 5, length_km = 1, std_type = "NAYY 4x150 SE") + create_line(net, name = "line 4", from_bus = 5, to_bus = 6, length_km = 1, std_type = "NAYY 4x150 SE") + create_line(net, name = "line 5", from_bus = 6, to_bus = 1, length_km = 1, std_type = "NAYY 4x150 SE") + + create_transformer_from_parameters(net, hv_bus=0, lv_bus=1, i0_percent=0.038, pfe_kw=11.6, + vkr_percent=0.322, sn_mva=40, vn_lv_kv=22.0, vn_hv_kv=110.0, vk_percent=17.8) + + create_load(net, 2, p_mw = 1, q_mvar = 0.2, name = "load 0") + create_load(net, 3, p_mw = 1, q_mvar = 0.2, name = "load 1") + create_load(net, 4, p_mw = 1, q_mvar = 0.2, name = "load 2") + create_load(net, 5, p_mw = 1, q_mvar = 0.2, name = "load 3") + create_load(net, 6, p_mw = 1, q_mvar = 0.2, name = "load 4") + + create_switch(net, bus = 1, element = 0, et = 'l') + create_switch(net, bus = 2, element = 0, et = 'l') + create_switch(net, bus = 2, element = 1, et = 'l') + create_switch(net, bus = 3, element = 1, et = 'l') + create_switch(net, bus = 3, element = 2, et = 'l') + create_switch(net, bus = 4, element = 2, et = 'l') + create_switch(net, bus = 4, element = 3, et = 'l', closed = 0) + create_switch(net, bus = 5, element = 3, et = 'l') + create_switch(net, bus = 5, element = 4, et = 'l') + create_switch(net, bus = 6, element = 4, et = 'l') + create_switch(net, bus = 6, element = 5, et = 'l') + create_switch(net, bus = 1, element = 5, et = 'l') Using NetworkX algorithms: shortest path @@ -64,20 +64,20 @@ Finding the shortest path between two buses is a good example for that. .. code:: python - from pandapower.topology import create_nxgraph - import networkx as nx + from pandapower.topology import create_nxgraph + import networkx as nx - mg = create_nxgraph(net) - nx.shortest_path(mg, 0, 5) + mg = create_nxgraph(net) + nx.shortest_path(mg, 0, 5) .. code:: python - Out: [0, 1, 6, 5] + Out: [0, 1, 6, 5] .. image:: /pics/topology/nx_shortest_path.png - :width: 42em - :alt: alternate Text - :align: center + :width: 42em + :alt: alternate Text + :align: center Find disconnected buses ------------------------ @@ -86,19 +86,19 @@ With *unsupplied_buses* you can easily find buses that are not connected to an e .. code:: python - from pandapower.topology import unsupplied_buses + from pandapower.topology import unsupplied_buses - net.switch.closed.at[11] = 0 - unsupplied_buses(net) + net.switch.closed.at[11] = 0 + unsupplied_buses(net) .. code:: python - Out: {5, 6} + Out: {5, 6} .. image:: /pics/topology/top_disconnected_buses.png - :width: 42em - :alt: alternate Text - :align: center + :width: 42em + :alt: alternate Text + :align: center Calculate distances between buses @@ -110,52 +110,52 @@ This is possible since line lengths are being transferred into the MultiGraph as .. code:: python - from pandapower.topology import calc_distance_to_bus + from pandapower.topology import calc_distance_to_bus - net.switch.closed.at[6] = 1 - net.switch.closed.at[8] = 0 - calc_distance_to_bus(net, 1) + net.switch.closed.at[6] = 1 + net.switch.closed.at[8] = 0 + calc_distance_to_bus(net, 1) .. code:: python - Out: - 0 0 - 1 0 - 2 1 - 3 2 - 4 3 - 5 4 - 6 1 + Out: + 0 0 + 1 0 + 2 1 + 3 2 + 4 3 + 5 4 + 6 1 **Interpretation:** The distance between bus 1 and itself is 0 km. Bus 1 is also 0 km away from bus 0, since they are connected with a transformer. The shortest path between bus 1 and bus 5 is 4 km long. .. image:: /pics/topology/top_calc_distance_to_bus.png - :width: 42em - :alt: alternate Text - :align: center + :width: 42em + :alt: alternate Text + :align: center Find connected buses with the same voltage level -------------------------------------------------- .. code:: python - from pandapower.topology import create_nxgraph, connected_components + from pandapower.topology import create_nxgraph, connected_components - mg_no_trafos = create_nxgraph(net, include_trafos = False) - cc = connected_components(mg_no_trafos) + mg_no_trafos = create_nxgraph(net, include_trafos = False) + cc = connected_components(mg_no_trafos) .. code:: python - In : next(cc) - Out : {0} - In : next(cc) - Out : {1, 2, 3, 4, 5, 6} + In : next(cc) + Out : {0} + In : next(cc) + Out : {1, 2, 3, 4, 5, 6} .. image:: /pics/topology/multigraph_example_include_trafos.png - :width: 42em - :alt: alternate Text - :align: center + :width: 42em + :alt: alternate Text + :align: center Find rings and ring sections @@ -167,43 +167,43 @@ be set as a nogobuses. With *respect_switches = True* you get the ring sections, .. code:: python - from pandapower.topology import create_nxgraph, connected_components + from pandapower.topology import create_nxgraph, connected_components - mg_ring_sections = create_nxgraph(net, nogobuses = [0, 1]) - cc_ring_sections = connected_components(mg_ring_sections) + mg_ring_sections = create_nxgraph(net, nogobuses = [0, 1]) + cc_ring_sections = connected_components(mg_ring_sections) .. code:: python - In : next(cc_ring_sections) - Out : {2, 3, 4} + In : next(cc_ring_sections) + Out : {2, 3, 4} - In : next(cc_ring_sections) - Out : {5, 6} + In : next(cc_ring_sections) + Out : {5, 6} .. image:: /pics/topology/top_find_ring_sections.png - :width: 42em - :alt: alternate Text - :align: center + :width: 42em + :alt: alternate Text + :align: center .. code:: python - from pandapower.topology import create_nxgraph, connected_components + from pandapower.topology import create_nxgraph, connected_components - mg_ring = create_nxgraph(net, respect_switches = False, nogobuses = [0,1]) - cc_ring = connected_components(mg_ring) + mg_ring = create_nxgraph(net, respect_switches = False, nogobuses = [0,1]) + cc_ring = connected_components(mg_ring) .. code:: python - In : next(cc_ring) - Out : {2, 3, 4, 5, 6} + In : next(cc_ring) + Out : {2, 3, 4, 5, 6} .. image:: /pics/topology/top_find_rings.png - :width: 42em - :alt: alternate Text - :align: center + :width: 42em + :alt: alternate Text + :align: center Find stubs --------------------- @@ -215,53 +215,53 @@ This is a small extension for the example network: .. code:: python - pp.create_bus(net, name = "bus 7", vn_kv = 20, type = 'b') - pp.create_bus(net, name = "bus 8", vn_kv = 20, type = 'b') + pp.create_bus(net, name = "bus 7", vn_kv = 20, type = 'b') + pp.create_bus(net, name = "bus 8", vn_kv = 20, type = 'b') - pp.create_line(net, name = "line 6", from_bus = 6, to_bus = 7, length_km = 1, std_type = "NAYY 4x150 SE") - pp.create_line(net, name = "line 7", from_bus = 7, to_bus = 8, length_km = 1, std_type = "NAYY 4x150 SE") + pp.create_line(net, name = "line 6", from_bus = 6, to_bus = 7, length_km = 1, std_type = "NAYY 4x150 SE") + pp.create_line(net, name = "line 7", from_bus = 7, to_bus = 8, length_km = 1, std_type = "NAYY 4x150 SE") - pp.create_load(net, 7, p_mw = 1, q_mvar = 0.2, name = "load 5") - pp.create_load(net, 8, p_mw = 1, q_mvar = 0.2, name = "load 6") + pp.create_load(net, 7, p_mw = 1, q_mvar = 0.2, name = "load 5") + pp.create_load(net, 8, p_mw = 1, q_mvar = 0.2, name = "load 6") .. code:: python - from pandapower.topology import determine_stubs - determine_stubs(net, roots = [0,1]) + from pandapower.topology import determine_stubs + determine_stubs(net, roots = [0,1]) .. code:: python - In: net.bus - - Out: - name vn_kv type zone in_service on_stub - 0 110 kV bar 110.0 b None True False - 1 20 kV bar 20.0 b None True False - 2 bus 2 20.0 b None True False - 3 bus 3 20.0 b None True False - 4 bus 4 20.0 b None True False - 5 bus 5 20.0 b None True False - 6 bus 6 20.0 b None True False - 7 bus 7 20.0 b None True True - 8 bus 8 20.0 b None True True - - In: net.line - - Out: - 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 is_stub - 0 line 0 NAYY 4x150 SE 1 2 1.0 0.208 0.08 261.0 0.0 0.27 1.0 1 cs True False - 1 line 1 NAYY 4x150 SE 2 3 1.0 0.208 0.08 261.0 0.0 0.27 1.0 1 cs True False - 2 line 2 NAYY 4x150 SE 3 4 1.0 0.208 0.08 261.0 0.0 0.27 1.0 1 cs True False - 3 line 3 NAYY 4x150 SE 4 5 1.0 0.208 0.08 261.0 0.0 0.27 1.0 1 cs True False - 4 line 4 NAYY 4x150 SE 5 6 1.0 0.208 0.08 261.0 0.0 0.27 1.0 1 cs True False - 5 line 5 NAYY 4x150 SE 6 1 1.0 0.208 0.08 261.0 0.0 0.27 1.0 1 cs True False - 6 line 6 NAYY 4x150 SE 6 7 1.0 0.208 0.08 261.0 0.0 0.27 1.0 1 cs True False - 7 line 7 NAYY 4x150 SE 7 8 1.0 0.208 0.08 261.0 0.0 0.27 1.0 1 cs True False + In: net.bus + + Out: + name vn_kv type zone in_service on_stub + 0 110 kV bar 110.0 b None True False + 1 20 kV bar 20.0 b None True False + 2 bus 2 20.0 b None True False + 3 bus 3 20.0 b None True False + 4 bus 4 20.0 b None True False + 5 bus 5 20.0 b None True False + 6 bus 6 20.0 b None True False + 7 bus 7 20.0 b None True True + 8 bus 8 20.0 b None True True + + In: net.line + + Out: + 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 is_stub + 0 line 0 NAYY 4x150 SE 1 2 1.0 0.208 0.08 261.0 0.0 0.27 1.0 1 cs True False + 1 line 1 NAYY 4x150 SE 2 3 1.0 0.208 0.08 261.0 0.0 0.27 1.0 1 cs True False + 2 line 2 NAYY 4x150 SE 3 4 1.0 0.208 0.08 261.0 0.0 0.27 1.0 1 cs True False + 3 line 3 NAYY 4x150 SE 4 5 1.0 0.208 0.08 261.0 0.0 0.27 1.0 1 cs True False + 4 line 4 NAYY 4x150 SE 5 6 1.0 0.208 0.08 261.0 0.0 0.27 1.0 1 cs True False + 5 line 5 NAYY 4x150 SE 6 1 1.0 0.208 0.08 261.0 0.0 0.27 1.0 1 cs True False + 6 line 6 NAYY 4x150 SE 6 7 1.0 0.208 0.08 261.0 0.0 0.27 1.0 1 cs True False + 7 line 7 NAYY 4x150 SE 7 8 1.0 0.208 0.08 261.0 0.0 0.27 1.0 1 cs True False .. image:: /pics/topology/top_determine_stubs.png - :width: 42em - :alt: alternate Text - :align: center + :width: 42em + :alt: alternate Text + :align: center diff --git a/juliapkg.json b/juliapkg.json index bbbee5b1d1..58142299a3 100644 --- a/juliapkg.json +++ b/juliapkg.json @@ -3,7 +3,7 @@ "packages": { "PandaModels": { "uuid": "2dbab86a-7cbf-476f-9afe-75ffd3079e7c", - "version": "0.8" + "version": "0.9.1" } } } \ No newline at end of file diff --git a/mypy.ini b/mypy.ini index eefef2c1b9..99190f0fbb 100644 --- a/mypy.ini +++ b/mypy.ini @@ -1,26 +1,25 @@ [mypy] packages = pandapower -exclude = (?x)( - control/.*$ - | converter/.*$ - | estimation/.*$ - | plotting/.*$ - | protection/.*$ - | diagnostic/.*$ - | create/.*$ - ) +; uncomment to ignore certain folders +; exclude = (?x)( +; control/.*$ +; | create/.*$ +; ) ; test with the oldest supported version -python_version = 3.10 +python_version = 3.11 follow_imports = skip allow_redefinition = true [mypy-geojson.*] ignore_missing_imports = true +[mypy-geopy.*] +ignore_missing_imports = true + [mypy-graph_tool.*] ignore_missing_imports = true -[mypy-geopandas.*] +[mypy-igraph.*] ignore_missing_imports = true [mypy-julia.*] @@ -32,6 +31,9 @@ ignore_missing_imports = true [mypy-lightsim2grid_cpp.*] ignore_missing_imports = true +[mypy-matpowercaseframes.*] +ignore_missing_imports = true + [mypy-mplcursors.*] ignore_missing_imports = true @@ -41,20 +43,25 @@ ignore_missing_imports = true [mypy-numba.*] ignore_missing_imports = true +[mypy-ortools.*] +ignore_missing_imports = true + [mypy-pandaplan.*] ignore_missing_imports = true [mypy-plotly.*] ignore_missing_imports = true +[mypy-powerfactory.*] +ignore_missing_imports = true + [mypy-power_grid_model.*] ignore_missing_imports = true [mypy-power_grid_model_io.*] ignore_missing_imports = true -[mypy-scipy.*] -# scipy-stubs requires Python >= 3.10 +[mypy-pypower.*] ignore_missing_imports = true [mypy-xlsxwriter.*] diff --git a/pandapower/__init__.py b/pandapower/__init__.py index fa8ea64aeb..3ab17498ee 100644 --- a/pandapower/__init__.py +++ b/pandapower/__init__.py @@ -1,5 +1,5 @@ import os -pp_dir = os.path.dirname(os.path.realpath(__file__)) +pp_dir: str = os.path.dirname(os.path.realpath(__file__)) from pandapower._version import __version__, __format_version__ diff --git a/pandapower/auxiliary.py b/pandapower/auxiliary.py index f0c802ef23..40ca99c747 100644 --- a/pandapower/auxiliary.py +++ b/pandapower/auxiliary.py @@ -1,5 +1,3 @@ -# -*- 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. @@ -15,6 +13,7 @@ TypeVar, overload, Final, + TYPE_CHECKING, ) import numpy as np @@ -42,7 +41,9 @@ lightsim2grid_available = True except ImportError: lightsim2grid_available = False + import logging + try: from geopandas import GeoSeries from shapely import from_geojson @@ -50,8 +51,12 @@ geopandas_available = True except ImportError: geopandas_available = False - # for typing only - GeoSeries = object + + +if TYPE_CHECKING: + if not geopandas_available: + raise ImportError("GeoPandas required for type checking!") + from geopandas import GeoSeries PyPowerNetwork = dict[str, Any] @@ -85,15 +90,9 @@ def log_to_level( def version_check( package_name: str, - level: Literal["error", "warning", "info", "debug", "UserWarning"] = "UserWarning", - ignore_not_installed: bool = False ) -> None: # FIXME: version should NEVER be defined in code! - minimum_version = {'plotly': "3.1.1", - 'numba': "0.25", - } - if ignore_not_installed and package_name not in minimum_version.keys(): - return + minimum_version = {'numba': "0.25"} try: version = version_str(package_name) @@ -101,13 +100,9 @@ def version_check( log_to_level(( f"{package_name} version {version} is no longer supported by pandapower.\r\n" f"Please upgrade your installation. Possibly it can be done via " - f"'pip install --upgrade {package_name}'."), logger, level) + f"'pip install --upgrade {package_name}'."), logger, "UserWarning") except PackageNotFoundError: - if ignore_not_installed: - raise PackageNotFoundError( - f"Python package '{package_name}', is needed.\r\nPlease install it. " - f"Possibly it can be installed via 'pip install {package_name}'.") - + pass try: from numba import jit @@ -161,7 +156,7 @@ class GeoAccessor: def __init__(self, pandas_obj: "pdt.Series[str]") -> None: self._validate(pandas_obj) - self._obj = pandas_obj + self._obj: "pdt.Series[str]" = pandas_obj @staticmethod def _validate(obj: "pdt.Series[str]") -> None: @@ -177,7 +172,7 @@ def _validate(obj: "pdt.Series[str]") -> None: def _extract_coords(x: GeoJSON) -> NDArray[np.float64] | list[NDArray[np.float64]]: if x["type"] == "Point": return np.array(x["coordinates"]) - return [np.array(y) for y in x["coordinates"]] + return [np.array(y, dtype=np.float64) for y in x["coordinates"]] @property def _coords(self): @@ -1579,8 +1574,8 @@ def _add_dcline_gens(net: pandapowerNet) -> None: from pandapower.create import create_gen for dctab in net.dcline.itertuples(): - p_mw = np.abs(dctab.p_mw) - p_loss = p_mw * (1 - dctab.loss_percent / 100) - dctab.loss_mw # type: ignore[operator] + p_mw: float = np.abs(dctab.p_mw) # type: ignore[operator] + p_loss: float = p_mw * (1 - dctab.loss_percent / 100) - dctab.loss_mw # type: ignore[assignment,operator] max_p_mw: float if hasattr(dctab, 'max_p_mw') and pd.notna(dctab.max_p_mw): @@ -1589,6 +1584,9 @@ def _add_dcline_gens(net: pandapowerNet) -> None: max_p_mw = float('nan') p_min: float p_max: float + + p_to: float + p_from: float if np.sign(dctab.p_mw) > 0: p_to = p_loss p_from = -p_mw @@ -1660,9 +1658,21 @@ def _add_vsc_stacked(net: pandapowerNet): ref_bus = bus_dc_minus control_mode_dc = 'vm_pu_diff_p' - create_vsc(net, ac_bus, bus_dc_plus, r_ohm/2., x_ohm/2., r_dc_ohm/2., pl_dc_mw=pl_dc_mw, - control_mode_ac=control_mode_ac, control_value_ac=control_value_ac, name=str(name)+"+", - control_mode_dc=control_mode_dc, control_value_dc=control_value_dc, ref_bus=ref_bus) + create_vsc( + net, + ac_bus, + bus_dc_plus, + r_ohm / 2.0, + x_ohm / 2.0, + r_dc_ohm / 2.0, + pl_dc_mw=pl_dc_mw, + control_mode_ac=control_mode_ac, + control_value_ac=control_value_ac, + name=str(name) + "+", + control_mode_dc=control_mode_dc, # type: ignore[arg-type] + control_value_dc=control_value_dc, + ref_bus=ref_bus, + ) ref_bus = None if control_mode_dc == 'vm_pu_diff_p': @@ -1670,9 +1680,21 @@ def _add_vsc_stacked(net: pandapowerNet): control_mode_dc = 'vm_pu_diff_m' control_value_dc = -control_value_dc - create_vsc(net, ac_bus, bus_dc_minus, r_ohm/2., x_ohm/2., r_dc_ohm/2., pl_dc_mw=pl_dc_mw, - control_mode_ac=control_mode_ac, control_value_ac=control_value_ac, name=str(name)+"-", - control_mode_dc=control_mode_dc, control_value_dc=control_value_dc, ref_bus=ref_bus) + create_vsc( + net, + ac_bus, + bus_dc_minus, + r_ohm / 2.0, + x_ohm / 2.0, + r_dc_ohm / 2.0, + pl_dc_mw=pl_dc_mw, + control_mode_ac=control_mode_ac, + control_value_ac=control_value_ac, + name=str(name) + "-", + control_mode_dc=control_mode_dc, # type: ignore[arg-type] + control_value_dc=control_value_dc, + ref_bus=ref_bus, + ) def _add_auxiliary_elements(net: pandapowerNet): @@ -1706,7 +1728,7 @@ def _replace_nans_with_default_limits(net: pandapowerNet, ppc: PyPowerNetwork) - def _init_runpp_options( net: pandapowerNet, - algorithm: Literal["nr", "iwamoto_nr", "bfsw", "gs", "fdxb", "fdbx"], + algorithm: Literal["nr", "iwamoto_nr", "bfsw", "gs", "fdxb", "fdbx", "helm"], calculate_voltage_angles: Literal["auto"] | bool, init: Literal["auto", "dc", "flat", "results"] | float, max_iteration: Literal["auto"] | int, @@ -1787,7 +1809,7 @@ def _init_runpp_options( calculate_voltage_angles = True default_max_iteration = {"nr": 10, "iwamoto_nr": 10, "bfsw": 100, "gs": 10000, "fdxb": 30, - "fdbx": 30} + "fdbx": 30, "helm": 40} with_facts = net.svc.in_service.any() or net.tcsc.in_service.any() or \ net.ssc.in_service.any() or net.vsc.in_service.any() or \ net.vsc_stacked.in_service.any() or net.vsc_bipolar.in_service.any() @@ -1841,9 +1863,9 @@ def _init_runpp_options( logger.warning("Currently distributed_slack is implemented for 'ext_grid', 'gen' " "and 'xward' only, not for '" + "', '".join( false_slack_weight_elms) + "'.") - if algorithm != 'nr': + if algorithm != 'nr' and algorithm != 'helm': raise NotImplementedError( - 'Distributed slack is only implemented for Newton Raphson algorithm.') + 'Distributed slack is only implemented for Newton Raphson algorithm and HELM.') if tdpf: if algorithm != 'nr': diff --git a/pandapower/build_branch.py b/pandapower/build_branch.py index b9c4641507..0d4f97d190 100644 --- a/pandapower/build_branch.py +++ b/pandapower/build_branch.py @@ -33,6 +33,7 @@ from pandapower.pypower.idx_bus_sc import C_MIN, C_MAX from pandapower.pypower.idx_tcsc import TCSC_F_BUS, TCSC_T_BUS, TCSC_X_L, TCSC_X_CVAR, TCSC_SET_P, \ TCSC_THYRISTOR_FIRING_ANGLE, TCSC_STATUS, TCSC_CONTROLLABLE, tcsc_cols, TCSC_MIN_FIRING_ANGLE, TCSC_MAX_FIRING_ANGLE +from pandapower.pf.create_jacobian_tdpf import ALPHA_TDPF from pandapower.create._utils import add_column_to_df @@ -515,22 +516,12 @@ def _calc_branch_values_from_trafo_df(net, ppc, trafo_df=None, sequence=1): def _calc_r_x_y_from_dataframe(net, trafo_df, vn_trafo_lv, vn_lv, ppc, sequence=1): mode = net["_options"]["mode"] trafo_model = net["_options"]["trafo_model"] - if 'tap_dependency_table' in trafo_df: - if 'trafo_characteristic_table' in net: - r, x = _calc_r_x_from_dataframe( - mode, trafo_df, vn_lv, vn_trafo_lv, net.sn_mva, sequence=sequence, - trafo_characteristic_table=net.trafo_characteristic_table) - else: - r, x = _calc_r_x_from_dataframe(mode, trafo_df, vn_lv, vn_trafo_lv, net.sn_mva, sequence=sequence) - else: - warnings.warn(DeprecationWarning( - "tap_dependency_table is missing in net, which is most probably due to unsupported net data. " - "tap_dependency_table was introduced with pandapower 3.0 and replaced spline characteristics. " - "Spline characteristics will still work, but they are deprecated and will be removed in future releases." - )) + if 'tap_dependency_table' in trafo_df and 'trafo_characteristic_table' in net: r, x = _calc_r_x_from_dataframe( - mode, trafo_df, vn_lv, vn_trafo_lv, net.sn_mva, sequence=sequence, characteristic=net.get("characteristic") - ) + mode, trafo_df, vn_lv, vn_trafo_lv, net.sn_mva, sequence=sequence, + trafo_characteristic_table=net.trafo_characteristic_table) + else: + r, x = _calc_r_x_from_dataframe(mode, trafo_df, vn_lv, vn_trafo_lv, net.sn_mva, sequence=sequence) if mode == "sc": if net._options.get("use_pre_fault_voltage", False): @@ -538,13 +529,11 @@ def _calc_r_x_y_from_dataframe(net, trafo_df, vn_trafo_lv, vn_lv, ppc, sequence= else: g, b = 0, 0 # why for sc are we assigning y directly as 0? if isinstance(trafo_df, pd.DataFrame): # 2w trafo is dataframe, 3w trafo is dict - bus_lookup = net._pd2ppc_lookups["bus"] - cmax = ppc["bus"][bus_lookup[net.trafo.lv_bus.values], C_MAX] - # todo: kt is only used for case = max and only for network transformers! (IEC 60909-0:2016 section 6.3.3) - # kt is only calculated for network transformers (IEC 60909-0:2016 section 6.3.3) if not net._options.get("use_pre_fault_voltage", False): - kt = _transformer_correction_factor( - trafo_df, trafo_df.vk_percent, trafo_df.vkr_percent, trafo_df.sn_mva, cmax) + bus_lookup = net._pd2ppc_lookups["bus"] + cmax = ppc["bus"][bus_lookup[net.trafo.lv_bus.values], C_MAX] + case = net._options["case"] + kt = _transformer_correction_factor(trafo_df, trafo_df.vk_percent, trafo_df.vkr_percent, trafo_df.sn_mva, cmax, case) r *= kt x *= kt else: @@ -859,14 +848,14 @@ def _arctan(x: ArrayLike) -> ArrayLike: percent_is_set = np.nan_to_num(tap_step_percent, nan=0) != 0 else: tap_step_percent = float('nan') - percent_is_set = False + percent_is_set = np.False_ if tap_step_degree is not None: tap_step_degree = tap_step_degree[mask] degree_is_set = np.nan_to_num(tap_step_degree, nan=0) != 0 else: tap_step_degree = float('nan') - degree_is_set = False + degree_is_set = np.False_ # ideal tap changer if ideal: @@ -961,7 +950,8 @@ def _get_vk_values_from_table( return vals -def _get_vk_values(trafo_df: pd.DataFrame, characteristic, trafotype: Literal["2W", "3W"] = "2W") -> tuple[str, ...]: +def _get_vk_values(trafo_df: pd.DataFrame, trafotype: Literal["2W", "3W"] = "2W") -> tuple[ + str, ...]: """ get vk values from trafo table @@ -969,7 +959,6 @@ def _get_vk_values(trafo_df: pd.DataFrame, characteristic, trafotype: Literal["2 Parameters: trafo_df: the trafo table - characteristic: the trafo characteristic table trafotype: the trafo type (2W or 3W) Returns: @@ -1002,10 +991,6 @@ def _get_vk_values(trafo_df: pd.DataFrame, characteristic, trafotype: Literal["2 use_tap_dependent_impedance = np.any(tap_dependent_impedance) if use_tap_dependent_impedance: - # is net.characteristic table in net? - if characteristic is None: - raise UserWarning("tap_dependent_impedance of transformers requires net.characteristic") - # if any but 1 characteristic is missing per trafo, we assume it's by design; # but if all are missing, we raise an error # first, we read all characteristic indices @@ -1034,13 +1019,7 @@ def _get_vk_values(trafo_df: pd.DataFrame, characteristic, trafotype: Literal["2 for _, vk_var in enumerate(vk_variables): vk_value = get_trafo_values(trafo_df, vk_var) - # TODO: vk char columns deprecated: remove once support is dropped - if use_tap_dependent_impedance and vk_var in char_columns: - vals += (_calc_tap_dependent_value( - tap_pos, vk_value, tap_dependent_impedance, - characteristic, all_characteristic_idx[:, index_column[vk_var]]),) - else: - vals += (vk_value,) + vals += (vk_value,) return vals @@ -1067,8 +1046,7 @@ def _get_characteristic_for_tap_pos(characteristic, tap_pos, is_relevant): # FIXME: behavior differs depending on trafo_df type dict or pandas.DataFrame. This should be changed! # use test: loadflow/test_runpp.py::test_tap_table_order and change output of _trafo_df_from_trafo3w to DataFrame to # trigger issue -def _calc_r_x_from_dataframe(mode, trafo_df, vn_lv, vn_trafo_lv, sn_mva, sequence=1, characteristic=None, - trafo_characteristic_table=None): +def _calc_r_x_from_dataframe(mode, trafo_df, vn_lv, vn_trafo_lv, sn_mva, sequence=1, trafo_characteristic_table=None): """ Calculates (Vectorized) the resistance and reactance according to the transformer values @@ -1076,7 +1054,9 @@ def _calc_r_x_from_dataframe(mode, trafo_df, vn_lv, vn_trafo_lv, sn_mva, sequenc parallel = get_trafo_values(trafo_df, "parallel") if sequence == 1: tap_dependency = get_trafo_values(trafo_df, "tap_dependency_table", na_replacement=float("nan")) - if tap_dependency is not None: + if tap_dependency is None: + vk_percent, vkr_percent = _get_vk_values(trafo_df) + else: tap_dependency = np.array( [False if isinstance(x, float) and np.isnan(x) else x for x in tap_dependency]) if any(tap_dependency) and not isinstance(trafo_df, dict): @@ -1087,16 +1067,6 @@ def _calc_r_x_from_dataframe(mode, trafo_df, vn_lv, vn_trafo_lv, sn_mva, sequenc else: vk_percent = get_trafo_values(trafo_df, "vk_percent") vkr_percent = get_trafo_values(trafo_df, "vkr_percent") - else: - warnings.warn(DeprecationWarning( - "tap_dependency_table is missing in net, which is most probably due to unsupported net data." - " tap_dependency_table was introduced with pandapower 3.0 and replaced spline characteristics. " - "Spline characteristics will still work, but they are deprecated and will be removed " - "in future releases." - )) - - vk_percent, vkr_percent = _get_vk_values(trafo_df, characteristic) - elif sequence == 0: vk_percent = get_trafo_values(trafo_df, "vk0_percent") vkr_percent = get_trafo_values(trafo_df, "vkr0_percent") @@ -1558,16 +1528,17 @@ def _end_temperature_correction_factor(net, short_circuit=False, dc=False): delta_t_degree_celsius = net[element].temperature_degree_celsius.values.astype(np.float64) - 20 if 'alpha' in net[element].columns: - alpha = net[element].alpha.values.astype(np.float64) + alpha = np.nan_to_num(net[element].alpha.values.astype(np.float64), nan=0, copy=True) else: - alpha = 4e-3 + alpha = ALPHA_TDPF + warnings.warn(f"'alpha' is assumed to {alpha} and required for the calculation of the temperature based resistance.") r_correction_for_temperature = 1 + alpha * delta_t_degree_celsius return r_correction_for_temperature -def _transformer_correction_factor(trafo_df, vk, vkr, sn, cmax): +def _transformer_correction_factor(trafo_df, vk, vkr, sn, cmax, case): """ 2W-Transformer impedance correction factor in short circuit calculations, based on the IEC 60909-0:2016 standard. @@ -1578,14 +1549,20 @@ def _transformer_correction_factor(trafo_df, vk, vkr, sn, cmax): vkr: real-part of transformer short-circuit voltage, percent sn: transformer rating, kVA cmax: voltage factor to account for maximum worst-case currents, based on the lv side + case: short-circuit calculation case (str, "min"/"max") Returns: kt: transformer impedance correction factor for short-circuit calculations """ + # The transformer correction factor shall only be applied in the max case according to + # norm IEC 60909-0:2016 section 6.3.3 + if case != "max": + return np.ones(len(trafo_df)) + if "power_station_unit" in trafo_df.columns: - power_station_unit = trafo_df.power_station_unit.fillna(False).values.astype(bool) + power_station_unit = trafo_df.power_station_unit.astype("boolean").fillna(False).to_numpy() else: power_station_unit = np.zeros(len(trafo_df)).astype(bool) @@ -1618,22 +1595,18 @@ def _trafo_df_from_trafo3w(net: pandapowerNet, sequence: int = 1) -> dict: # todo check magnetizing impedance implementation: # loss_side = net._options["trafo3w_losses"].lower() nr_trafos = len(net.trafo3w) - loss_side = net.trafo3w.loss_side.values if "loss_side" in net.trafo3w.columns else np.full(nr_trafos, - net._options["trafo3w_losses"].lower()) - + loss_side = net.trafo3w.loss_side.values if "loss_side" in net.trafo3w.columns else np.full( + nr_trafos, net._options["trafo3w_losses"].lower() + ) if sequence == 1: - if 'tap_dependency_table' in net.trafo3w: - mode_tmp = "type_c" if mode == "sc" and net._options.get("use_pre_fault_voltage", False) else mode - _calculate_sc_voltages_of_equivalent_transformers(net.trafo3w, trafo2, mode_tmp, net=net) - else: - mode_tmp = "type_c" if mode == "sc" and net._options.get("use_pre_fault_voltage", False) else mode - _calculate_sc_voltages_of_equivalent_transformers(net.trafo3w, trafo2, mode_tmp, characteristic=net.get( - 'characteristic')) + mode_tmp = "type_c" if mode == "sc" and net._options.get("use_pre_fault_voltage", False) else mode + _calculate_sc_voltages_of_equivalent_transformers(net.trafo3w, trafo2, mode_tmp, net) elif sequence == 0: if mode != "sc": raise NotImplementedError( "0 seq impedance calculation only implemented for short-circuit calculation!") - _calculate_sc_voltages_of_equivalent_transformers_zero_sequence(net.trafo3w, trafo2,) + case = net._options.get("case", 'max') + _calculate_sc_voltages_of_equivalent_transformers_zero_sequence(net.trafo3w, trafo2, case=case) else: raise UserWarning("Unsupported sequence for trafo3w convertion") _calculate_3w_tap_changers(net, trafo2, sides) @@ -1667,9 +1640,7 @@ def _trafo_df_from_trafo3w(net: pandapowerNet, sequence: int = 1) -> dict: return {var: np.concatenate([trafo2[var][side] for side in sides]) for var in trafo2.keys()} -def _calculate_sc_voltages_of_equivalent_transformers( - t3, t2, mode, characteristic=None, net=None -): +def _calculate_sc_voltages_of_equivalent_transformers(t3, t2, mode, net): tap_dependency_table = get_trafo_values(t3, "tap_dependency_table") if tap_dependency_table is not None: tap_dependency_table = np.array( @@ -1682,12 +1653,7 @@ def _calculate_sc_voltages_of_equivalent_transformers( t3['vk_hv_percent'], t3['vkr_hv_percent'], t3['vk_mv_percent'], t3['vkr_mv_percent'], t3['vk_lv_percent'], t3['vkr_lv_percent']) else: - warnings.warn(DeprecationWarning("tap_dependency_table is missing in net, which is most probably due to " - "old net data. tap_dependency_table was introduced with " - "pandapower 3.0 and replaced spline characteristics. Spline " - "characteristics will still work, but they are deprecated and will be " - "removed in future releases.")) - vk_hv, vkr_hv, vk_mv, vkr_mv, vk_lv, vkr_lv = _get_vk_values(t3, characteristic, "3W") + vk_hv, vkr_hv, vk_mv, vkr_mv, vk_lv, vkr_lv = _get_vk_values(t3, "3W") vk_3w = np.stack([vk_hv, vk_mv, vk_lv]) vkr_3w = np.stack([vkr_hv, vkr_mv, vkr_lv]) @@ -1696,7 +1662,8 @@ def _calculate_sc_voltages_of_equivalent_transformers( vk_2w_delta = z_br_to_bus_vector(vk_3w, sn) vkr_2w_delta = z_br_to_bus_vector(vkr_3w, sn) if mode == "sc": - kt = _transformer_correction_factor(t3, vk_3w, vkr_3w, sn, 1.1) + case = net._options.get("case", 'max') + kt = _transformer_correction_factor(t3, vk_3w, vkr_3w, sn, 1.1, case) vk_2w_delta *= kt vkr_2w_delta *= kt vki_2w_delta = np.sqrt(vk_2w_delta ** 2 - vkr_2w_delta ** 2) @@ -1710,7 +1677,7 @@ def _calculate_sc_voltages_of_equivalent_transformers( t2["sn_mva"] = {"hv": sn[0, :], "mv": sn[1, :], "lv": sn[2, :]} -def _calculate_sc_voltages_of_equivalent_transformers_zero_sequence(t3, t2): +def _calculate_sc_voltages_of_equivalent_transformers_zero_sequence(t3, t2, case): vk_3w = np.stack([t3.vk_hv_percent.values, t3.vk_mv_percent.values, t3.vk_lv_percent.values]) vkr_3w = np.stack([t3.vkr_hv_percent.values, t3.vkr_mv_percent.values, t3.vkr_lv_percent.values]) vk0_3w = np.stack([t3.vk0_hv_percent.values, t3.vk0_mv_percent.values, t3.vk0_lv_percent.values]) @@ -1721,7 +1688,7 @@ def _calculate_sc_voltages_of_equivalent_transformers_zero_sequence(t3, t2): vkr0_2w_delta = z_br_to_bus_vector(vkr0_3w, sn) # Only for "sc", calculated with positive sequence value - kt = _transformer_correction_factor(t3, vk_3w, vkr_3w, sn, 1.1) + kt = _transformer_correction_factor(t3, vk_3w, vkr_3w, sn, 1.1, case) vk0_2w_delta *= kt vkr0_2w_delta *= kt @@ -1777,8 +1744,7 @@ def _calculate_3w_tap_changers(net, t2, sides): # net.trafo3w with tap changer at star points if any_at_star_point & np.any(mask_star_point := (tap_mask & at_star_point)): - t = (tap_arrays["tap_step_percent"][side][mask_star_point] * - np.exp(1j * np.deg2rad(tap_arrays["tap_step_degree"][side][mask_star_point]))) + t = tap_arrays["tap_step_percent"][side][mask_star_point] * np.exp(1j * np.deg2rad(tap_arrays["tap_step_degree"][side][mask_star_point])) tap_pos = tap_arrays["tap_pos"][side][mask_star_point] tap_neutral = tap_arrays["tap_neutral"][side][mask_star_point] t_corrected = 100 * t / (100 + (t * (tap_pos-tap_neutral))) diff --git a/pandapower/build_bus.py b/pandapower/build_bus.py index f8c308b4f9..5bbdc3f432 100644 --- a/pandapower/build_bus.py +++ b/pandapower/build_bus.py @@ -1,9 +1,7 @@ -# -*- coding: utf-8 -*- -import warnings # Copyright (c) 2016-2026 by University of Kassel and Fraunhofer Institute for Energy Economics # and Energy System Technology (IEE), Kassel. All rights reserved. - +import warnings from collections import defaultdict from itertools import chain @@ -1009,13 +1007,13 @@ def _add_ext_grid_sc_impedance(net, ppc): c = ppc["bus"][eg_buses_ppc, C_MAX] if case == "max" else ppc["bus"][eg_buses_ppc, C_MIN] else: c = 1.1 - if "s_sc_%s_mva" % case not in eg: - raise ValueError(("short circuit apparent power s_sc_%s_mva needs to be specified for " - "external grid \n Try: net.ext_grid['s_sc_max_mva'] = 1000") % case) + if f"s_sc_{case}_mva" not in eg: + raise ValueError(f"short circuit apparent power s_sc_{case}_mva needs to be specified for external grid \n" + f" Try: net.ext_grid['s_sc_{case}_mva'] = 1000") s_sc = eg["s_sc_%s_mva" % case].values/ppc['baseMVA'] - if "rx_%s" % case not in eg: - raise ValueError(("short circuit R/X rate rx_%s needs to be specified for external grid \n" - " Try: net.ext_grid['rx_max'] = 0.1") % case) + if f"rx_{case}" not in eg: + raise ValueError(f"short circuit R/X rate rx_{case} needs to be specified for external grid \n" + f" Try: net.ext_grid['rx_{case}'] = 0.1") rx = eg["rx_%s" % case].values z_grid = c / s_sc diff --git a/pandapower/build_gen.py b/pandapower/build_gen.py index 3083c4bc29..193c898549 100644 --- a/pandapower/build_gen.py +++ b/pandapower/build_gen.py @@ -1,5 +1,3 @@ -# -*- 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. @@ -25,7 +23,7 @@ logger = logging.getLogger(__name__) -def _build_gen_ppc(net: pandapowerNet, ppc: dict): +def _build_gen_ppc(net: pandapowerNet, ppc: dict, sequence=None): """ Takes the empty ppc network and fills it with the gen values. The gen datatype will be floated afterwards. @@ -54,7 +52,7 @@ def _build_gen_ppc(net: pandapowerNet, ppc: dict): _init_ppc_gen(net, ppc, f) for element, (f, t) in gen_order.items(): - add_element_to_gen(net, ppc, element, f, t) + add_element_to_gen(net, ppc, element, f, t, sequence=sequence) net._gen_order = gen_order if distributed_slack: @@ -85,11 +83,11 @@ def _init_ppc_gen(net, ppc, nr_gens): ppc["gen"][:, QMIN] = -q_lim_default -def add_element_to_gen(net, ppc, element, f, t): +def add_element_to_gen(net, ppc, element, f, t, sequence): if element == "ext_grid": _build_pp_ext_grid(net, ppc, f, t) elif element == "gen": - _build_pp_gen(net, ppc, f, t) + _build_pp_gen(net, ppc, f, t, sequence) elif element == "sgen_controllable": _build_pp_pq_element(net, ppc, "sgen", f, t) elif element == "load_controllable": @@ -201,7 +199,7 @@ def _enforce_controllable_vm_pu_p_mw(net, ppc, gen_is, f, t): return ppc -def _build_pp_gen(net, ppc, f, t): +def _build_pp_gen(net, ppc, f, t, sequence): delta = net["_options"]["delta"] gen_is = net._is_elements["gen"] bus_lookup = net["_pd2ppc_lookups"]["bus"] @@ -233,6 +231,11 @@ def _build_pp_gen(net, ppc, f, t): else: p_mw = net["gen"]["p_mw"].values[gen_is] + if sequence == 1: + p_mw /= 3 + elif sequence == 0 or sequence == 2: + p_mw *= 0 + ppc["gen"][f:t, PG] = (p_mw * net["gen"]["scaling"].values[gen_is]) ppc["gen"][f:t, MBASE] = net["gen"]["sn_mva"].values[gen_is] if "sn_mva" in net["gen"].columns else np.full( diff --git a/pandapower/control/basic_controller.py b/pandapower/control/basic_controller.py index ac574a72ec..5737f4b4cd 100644 --- a/pandapower/control/basic_controller.py +++ b/pandapower/control/basic_controller.py @@ -1,5 +1,3 @@ -# -*- 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. diff --git a/pandapower/control/controller/DERController/DERBasics.py b/pandapower/control/controller/DERController/DERBasics.py index 9146d0931f..12493e14e5 100644 --- a/pandapower/control/controller/DERController/DERBasics.py +++ b/pandapower/control/controller/DERController/DERBasics.py @@ -1,5 +1,3 @@ -# -*- 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. diff --git a/pandapower/control/controller/DERController/PQVAreas.py b/pandapower/control/controller/DERController/PQVAreas.py index 51bb880c53..4bb0a62378 100644 --- a/pandapower/control/controller/DERController/PQVAreas.py +++ b/pandapower/control/controller/DERController/PQVAreas.py @@ -1,5 +1,3 @@ -# -*- 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. @@ -146,7 +144,8 @@ def in_area(self, p_pu, q_pu, vm_pu): return np.array([self.polygon.contains(Point(vmi, qi)) for vmi, qi in zip(vm_pu, q_pu)]) def q_flexibility(self, p_pu, vm_pu): - assert all(vm_pu >= 0) and all(vm_pu <= 2) + if not (all(vm_pu >= 0) and all(vm_pu <= 2)): + raise AssertionError("vm_pu not always between 0 and 2") def _q_flex(vm_pu): line = LineString([(vm_pu, -1), (vm_pu, 1)]) if line.intersects(self.polygon): diff --git a/pandapower/control/controller/DERController/QModels.py b/pandapower/control/controller/DERController/QModels.py index e38c7d3ce7..be091e88e5 100644 --- a/pandapower/control/controller/DERController/QModels.py +++ b/pandapower/control/controller/DERController/QModels.py @@ -1,5 +1,3 @@ -# -*- 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. @@ -63,8 +61,10 @@ def __init__(self, cosphi_v_curve): self.cosphi_v_curve = cosphi_v_curve def step(self, vm_pu, p_pu=None): - assert p_pu is not None - assert all(p_pu >= 0) + if p_pu is None: + raise AssertionError("p_pu is None") + if not all(p_pu >= 0): + raise AssertionError("not every p_pu is >= 0") return self.cosphi_v_curve.step(vm_pu, p_pu) @@ -76,12 +76,14 @@ class QModelCosphiP(QModel): """ def __init__(self, cosphi: float): - assert -1 <= cosphi <= 1 + if not (-1 <= cosphi <= 1): + raise AssertionError("cosphi not between -1 and 1") self.cosphi = cosphi self.q_setpoint_pu = np.sign(self.cosphi) * np.sqrt(1-self.cosphi**2) def step(self, vm_pu=None, p_pu=None,): - assert p_pu is not None + if p_pu is None: + raise AssertionError("p_pu is None") if any(p_pu < 0): logger.warning("p < 0 is assumed as p=0 in QModelCosphiP.step()") p_pu[p_pu< 0] = 0 @@ -116,12 +118,14 @@ class QModelCosphiPQ(QModel): """ def __init__(self, cosphi: float): - assert -1 <= cosphi <= 1 + if not (-1 <= cosphi <= 1): + raise AssertionError("cosphi not between -1 and 1") self.cosphi = cosphi self.q_setpoint_pu = np.sign(self.cosphi) * np.sqrt(1-self.cosphi**2) def step(self, vm_pu=None, p_pu=None,): - assert p_pu is not None + if p_pu is None: + raise AssertionError("p_pu is None") if any(p_pu < 0): logger.warning("p_pu < 0 is assumed as p_pu=0 QModelCosphiPQ.step()") p_pu[p_pu < 0] = 0 @@ -143,8 +147,10 @@ def __init__(self, cosphi_p_curve): self.cosphi_p_curve = cosphi_p_curve def step(self, vm_pu=None, p_pu=None): - assert p_pu is not None - assert all(p_pu >= 0) + if p_pu is None: + raise AssertionError("p_pu is None") + if not all(p_pu >= 0): + raise AssertionError("not every p_pu is >= 0") return self.cosphi_p_curve.step(p_pu) diff --git a/pandapower/control/controller/DERController/der_control.py b/pandapower/control/controller/DERController/der_control.py index ec1fd933ab..2f5cb7d6ff 100644 --- a/pandapower/control/controller/DERController/der_control.py +++ b/pandapower/control/controller/DERController/der_control.py @@ -1,5 +1,3 @@ -# -*- 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. diff --git a/pandapower/control/controller/DERController/der_control_plot.py b/pandapower/control/controller/DERController/der_control_plot.py index 216d65510a..8f1d1fe4a1 100644 --- a/pandapower/control/controller/DERController/der_control_plot.py +++ b/pandapower/control/controller/DERController/der_control_plot.py @@ -1,5 +1,3 @@ -# -*- 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. @@ -123,25 +121,3 @@ def generate_circle_segment(center_x, center_y, radius, start, stop, step): x = center_x + radius * np.cos(sample_angles) y = center_y + radius * np.sin(sample_angles) return x, y - - -if __name__ == "__main__": - from pandapower.control.controller.DERController import PQVArea4120V1, PQVArea4120V2, PQVArea4120V3 - - pq_area = PQVArea4120V1() - plot_pq_area(pq_area, "PQVArea4120V1") - plt.show() - plot_qv_area(pq_area, "PQVArea4120V1") - plt.show() - - pq_area = PQVArea4120V2() - plot_pq_area(pq_area, "PQVArea4120V2") - plt.show() - plot_qv_area(pq_area, "PQVArea4120V2") - plt.show() - - pq_area = PQVArea4120V3() - plot_pq_area(pq_area, "PQVArea4120V3") - plt.show() - plot_qv_area(pq_area, "PQVArea4120V3") - plt.show() diff --git a/pandapower/control/controller/characteristic_control.py b/pandapower/control/controller/characteristic_control.py index f15c17dd72..cc31241a9d 100644 --- a/pandapower/control/controller/characteristic_control.py +++ b/pandapower/control/controller/characteristic_control.py @@ -1,5 +1,3 @@ -# -*- 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. diff --git a/pandapower/control/controller/const_control.py b/pandapower/control/controller/const_control.py index 473bbc081d..5677d1a52a 100644 --- a/pandapower/control/controller/const_control.py +++ b/pandapower/control/controller/const_control.py @@ -1,5 +1,3 @@ -# -*- 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. diff --git a/pandapower/control/controller/dmr_control.py b/pandapower/control/controller/dmr_control.py index ec7fe45da6..89430dfe13 100644 --- a/pandapower/control/controller/dmr_control.py +++ b/pandapower/control/controller/dmr_control.py @@ -1,5 +1,3 @@ -# -*- 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. diff --git a/pandapower/control/controller/station_control.py b/pandapower/control/controller/station_control.py index 3b48e650cd..c77c276fbb 100644 --- a/pandapower/control/controller/station_control.py +++ b/pandapower/control/controller/station_control.py @@ -90,11 +90,31 @@ class BinarySearchControl(Controller): Additional keyword arguments. """ - def __init__(self, net, ctrl_in_service, output_element, output_variable, output_element_index, - output_element_in_service, output_values_distribution, input_element, input_variable, - input_element_index, set_point, control_modus:str=None, name="", input_inverted=None, - tol=0.001, in_service=True, order=0, level=0, drop_same_existing_ctrl=False, - matching_params=None, **kwargs): + + def __init__( + self, + net, + ctrl_in_service, + output_element, + output_variable, + output_element_index, + output_element_in_service, + output_values_distribution, + input_element, + input_variable, + input_element_index, + set_point, + control_modus: str | None = None, + name="", + input_inverted=None, + tol=0.001, + in_service=True, + order=0, + level=0, + drop_same_existing_ctrl=False, + matching_params=None, + **kwargs, + ): super().__init__(net, in_service=in_service, order=order, level=level, drop_same_existing_ctrl=drop_same_existing_ctrl, matching_params=matching_params) @@ -165,10 +185,16 @@ def __init__(self, net, ctrl_in_service, output_element, output_variable, output # normalize the values distribution: self._normalize_distribution_in_service(initial_pf_distribution=output_values_distribution) - self.output_adjustable = np.array([False if not distribution else service - for distribution, service in zip(np.atleast_1d(self.output_values_distribution), - np.atleast_1d(self.output_element_in_service))], dtype=bool) - ###finding correct control_modus, catching deprecated voltage_ctrl argument### + self.output_adjustable = np.array( + [ + False if not distribution else service + for distribution, service in zip( + np.atleast_1d(self.output_values_distribution), np.atleast_1d(self.output_element_in_service) + ) + ], + dtype=bool, + ) + # finding correct control_modus, catching deprecated voltage_ctrl argument if control_modus is None: #catching old attribute voltage_ctrl if hasattr(self, 'voltage_ctrl'): control_modus = self.voltage_ctrl @@ -187,12 +213,14 @@ def __init__(self, net, ctrl_in_service, output_element, output_variable, output f" types 'Q_ctrl', 'V_ctrl', 'PF_ctrl' or 'tan(phi)_ctrl'\n") else: try: - self.control_modus = ControlModusEnum(control_modus) + self.control_modus = ControlModusEnum(control_modus) # type: ignore[has-type] except ValueError: - logger.warning(f"Control_modus {control_modus} not recognized, using 'Q_ctrl' from available" - f" types 'Q_ctrl', 'V_ctrl', 'PF_ctrl' or 'tan_phi_ctrl'\n") - self.control_modus = ControlModusEnum.q_ctrl - if self.control_modus == ControlModusEnum.PF_ctrl_cap: #-1 for capacitive, 1 for inductive systems + logger.warning( + f"Control_modus {control_modus} not recognized, using 'Q_ctrl' from available" + f" types 'Q_ctrl', 'V_ctrl', 'PF_ctrl' or 'tan_phi_ctrl'\n" + ) + self.control_modus = ControlModusEnum.q_ctrl # type: ignore[has-type] + if self.control_modus == ControlModusEnum.PF_ctrl_cap: # type: ignore[has-type] #-1 for capacitive, 1 for inductive systems self.reactance= -1 else: if control_modus == ControlModusEnum.PF_ctrl: @@ -201,7 +229,7 @@ def __init__(self, net, ctrl_in_service, output_element, output_variable, output self.control_modus = ControlModusEnum.PF_ctrl_ind self.reactance = 1 - if self.control_modus in ControlModusEnum.pf_modes(): #checking cos(phi) limits + if self.control_modus in ControlModusEnum.pf_modes(): # type: ignore[has-type] # checking cos(phi) limits if abs(self.set_point) > 1: raise UserWarning(f'Power Factor Controller {self.index}: Set point out of range ([-1,1]') ###adding input elements### @@ -229,7 +257,7 @@ def __init__(self, net, ctrl_in_service, output_element, output_variable, output input_index, input_variable) ###get p variables for input elements for Phi controller - if self.control_modus in ControlModusEnum.pf_modes() or self.control_modus== ControlModusEnum.tan_phi_ctrl: + if self.control_modus in ControlModusEnum.pf_modes() or self.control_modus == ControlModusEnum.tan_phi_ctrl: # type: ignore[has-type] if isinstance(input_variable, list): input_variable_p = input_variable[counter].replace('q', 'p').replace('var','w') _, input_variable_temp_p = _detect_read_write_flag(net, self.input_element,input_index, diff --git a/pandapower/control/controller/trafo/ContinuousTapControl.py b/pandapower/control/controller/trafo/ContinuousTapControl.py index b42e563363..60e01e2879 100644 --- a/pandapower/control/controller/trafo/ContinuousTapControl.py +++ b/pandapower/control/controller/trafo/ContinuousTapControl.py @@ -1,5 +1,3 @@ -# -*- 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. diff --git a/pandapower/control/controller/trafo/DiscreteTapControl.py b/pandapower/control/controller/trafo/DiscreteTapControl.py index c7576f8b4a..4d1b0e0ced 100644 --- a/pandapower/control/controller/trafo/DiscreteTapControl.py +++ b/pandapower/control/controller/trafo/DiscreteTapControl.py @@ -1,5 +1,3 @@ -# -*- 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. diff --git a/pandapower/control/controller/trafo/TapDependentImpedance.py b/pandapower/control/controller/trafo/TapDependentImpedance.py index b967a5b3be..c3ad46fed1 100644 --- a/pandapower/control/controller/trafo/TapDependentImpedance.py +++ b/pandapower/control/controller/trafo/TapDependentImpedance.py @@ -1,5 +1,3 @@ -# -*- 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. diff --git a/pandapower/control/controller/trafo/VmSetTapControl.py b/pandapower/control/controller/trafo/VmSetTapControl.py index 1560f27220..83c57ac7a2 100644 --- a/pandapower/control/controller/trafo/VmSetTapControl.py +++ b/pandapower/control/controller/trafo/VmSetTapControl.py @@ -1,5 +1,3 @@ -# -*- 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. diff --git a/pandapower/control/controller/trafo_control.py b/pandapower/control/controller/trafo_control.py index f16ecda5c1..b21437d14b 100644 --- a/pandapower/control/controller/trafo_control.py +++ b/pandapower/control/controller/trafo_control.py @@ -1,5 +1,3 @@ -# -*- 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. import pandas as pd diff --git a/pandapower/control/run_control.py b/pandapower/control/run_control.py index 463d29558e..bbbd954ab8 100644 --- a/pandapower/control/run_control.py +++ b/pandapower/control/run_control.py @@ -1,5 +1,3 @@ -# -*- 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. diff --git a/pandapower/control/util/auxiliary.py b/pandapower/control/util/auxiliary.py index d97e95cf4d..c55fffda40 100644 --- a/pandapower/control/util/auxiliary.py +++ b/pandapower/control/util/auxiliary.py @@ -1,5 +1,3 @@ -# -*- 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. import sys diff --git a/pandapower/control/util/characteristic.py b/pandapower/control/util/characteristic.py index f789c87b5f..7b1efae89d 100644 --- a/pandapower/control/util/characteristic.py +++ b/pandapower/control/util/characteristic.py @@ -1,5 +1,3 @@ -# -*- 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. @@ -17,6 +15,15 @@ logger = pplog.getLogger(__name__) +def _make_hashable(obj): + if isinstance(obj, dict): + return tuple(sorted((k, _make_hashable(v)) for k, v in obj.items())) + if isinstance(obj, (list, tuple)): + return tuple(_make_hashable(v) for v in obj) + if isinstance(obj, np.ndarray): + return tuple(_make_hashable(v) for v in obj.tolist()) + return obj + class Characteristic(JSONSerializableClass): """ This class represents a characteristics curve. The curve is described as a piecewise linear function. @@ -154,6 +161,8 @@ def __init__(self, net, x_values, y_values, interpolator_kind="interp1d", table= super().__init__(net, x_values=x_values, y_values=y_values, table=table) self.kwargs = kwargs self.interpolator_kind = interpolator_kind + self._interpolator = None + @property def interpolator(self): @@ -165,11 +174,7 @@ def interpolator(self): demand. As soon as the characteristic is called, if the interpolator is there, we can use it. If not, we recreate it. """ - return self._interpolator - - @interpolator.getter - def interpolator(self): - if not hasattr(self, '_interpolator'): + if self._interpolator is None: if self.interpolator_kind == "interp1d": self._interpolator = default_interp1d(self.x_vals, self.y_vals, **self.kwargs) elif self.interpolator_kind == "Pchip": @@ -191,6 +196,34 @@ def __call__(self, x): """ return self.interpolator(x) + def __eq__(self, other): + if self.__class__ is not other.__class__: + return False + return ( + self.interpolator_kind == other.interpolator_kind + and self.kwargs == other.kwargs + and np.array_equal(self.x_vals, other.x_vals) + and np.array_equal(self.y_vals, other.y_vals) + ) + + + def __repr__(self): + return self.__class__.__name__ + f"({self.interpolator_kind}, {self.kwargs})" + + def __str__(self): + return self.__class__.__name__ + f"({self.interpolator_kind}, {self.kwargs})" + + def __hash__(self): + return hash( + ( + self.__class__, + self.interpolator_kind, + _make_hashable(self.kwargs), + _make_hashable(self.x_vals), + _make_hashable(self.y_vals), + ) + ) + class LogSplineCharacteristic(SplineCharacteristic): @@ -201,19 +234,19 @@ def __init__(self, net, x_values, y_values, **kwargs): def x_vals(self): return self._x_vals - @property - def y_vals(self): - return self._y_vals - @x_vals.setter def x_vals(self, x_values): - if np.any(x_values == 0): + if np.any([True if x <= 0 else False for x in x_values]): logger.warning("zero-values not supported in x_values") self._x_vals = np.log10(x_values) + @property + def y_vals(self): + return self._y_vals + @y_vals.setter def y_vals(self, y_values): - if np.any(y_values == 0): + if np.any([True if y <= 0 else False for y in y_values]): logger.warning("zero-values not supported in y_values") self._y_vals = np.log10(y_values) diff --git a/pandapower/control/util/diagnostic.py b/pandapower/control/util/diagnostic.py index bae8e5fddc..59fbc98103 100644 --- a/pandapower/control/util/diagnostic.py +++ b/pandapower/control/util/diagnostic.py @@ -1,5 +1,3 @@ -# -*- 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. from copy import deepcopy diff --git a/pandapower/convert_format.py b/pandapower/convert_format.py index 0959f17271..4981d305cf 100644 --- a/pandapower/convert_format.py +++ b/pandapower/convert_format.py @@ -13,7 +13,8 @@ from pandapower import pandapowerNet from pandapower.control import TrafoController, BinarySearchControl, DroopControl from pandapower.create._utils import add_column_to_df -from pandapower.create import create_empty_network, create_poly_cost +from pandapower.create import create_poly_cost +from pandapower.create.trafo_characteristic_create import create_trafo_characteristic from pandapower.network_structure import get_structure_dict from pandapower.plotting.geo import convert_geodata_to_geojson, _is_valid_number from pandapower.results import reset_results @@ -21,18 +22,31 @@ logger = logging.getLogger(__name__) -def convert_format(net, elements_to_deserialize=None, drop_invalid_geodata=False): +def convert_format(net, elements_to_deserialize=None, drop_invalid_geodata=False, + donot_open_newer=True): """ Converts old nets to new format to ensure consistency. The converted net is returned. """ from pandapower.toolbox.data_modification import set_data_type_of_columns_to_default if not isinstance(net.version, str) or not hasattr(net, 'format_version'): - net.format_version = net.version - if Version(str(net.format_version)) > Version(str(net.version).split('.dev')[0]): - # TODO: create error/warning when pandapower version is older then network + # if net.format_version and net.version are integers, this network is very old and will + # just pass the whole convert_format process. net.format_version = net.version net_format_version = Version(str(net.format_version)) - if isinstance(net.format_version, str) and net_format_version >= Version(__format_version__): + if net_format_version > Version(str(net.version).split('.dev')[0]): + net.format_version = net.version + if net_format_version > Version(__format_version__): + msg1 = (f"The network format version {net.format_version} is newer than the current " + f"pandapower version {__format_version__}. ") + if donot_open_newer: + msg = msg1 + ("Please update pandapower to the latest version (e.g. by using `pip " + "install --upgrade pandapower`).") + raise ValueError(msg) + else: + logger.warning(msg1 + "Some features may not work as expected. You should consider " + "updating pandapower to the latest version (e.g. by using `pip " + "install --upgrade pandapower`).") + if isinstance(net.format_version, str) and Version(net.format_version) >= Version(__format_version__): return net _add_nominal_power(net) _add_missing_tables(net) @@ -310,13 +324,13 @@ def _create_seperate_cost_tables(net, elements_to_deserialize): "cost_per_kw" in net.sgen: for index, cost in net.sgen.cost_per_kw.items(): if not np.isnan(cost): - create_poly_cost(net, index, "sgen", cp1_eur_per_mw=cost*1000) + create_poly_cost(net, index, "sgen", cp1_eur_per_mw=cost * 1e3) if _check_elements_to_deserialize('ext_grid', elements_to_deserialize) and \ "cost_per_kw" in net.ext_grid: for index, cost in net.ext_grid.cost_per_kw.items(): if not np.isnan(cost): - create_poly_cost(net, index, "ext_grid", cp1_eur_per_mw=cost*1000) + create_poly_cost(net, index, "ext_grid", cp1_eur_per_mw=cost * 1e3) if _check_elements_to_deserialize('gen', elements_to_deserialize) and \ "cost_per_kvar" in net.gen: @@ -719,6 +733,56 @@ def _update_characteristics(net, elements_to_deserialize): c.interpolator_kind = "interp1d" c.kwargs = {"kind": c.__dict__.pop("kind"), "bounds_error": False, "fill_value": c.__dict__.pop("fill_value")} + # convert characteristics to tap_dependency_table + characteristic_cols = [("trafo", ["vk_percent_characteristic", "vkr_percent_characteristic"]), + ("trafo3w", ["vk_hv_percent_characteristic", "vkr_hv_percent_characteristic", + "vk_mv_percent_characteristic", "vkr_mv_percent_characteristic", + "vk_lv_percent_characteristic", "vkr_lv_percent_characteristic"])] + + for table, columns in characteristic_cols: + if "tap_dependency_table" not in net[table].columns: + net[table]["tap_dependency_table"] = False + if "id_characteristic_table" not in net[table].columns: + net[table]["id_characteristic_table"] = pd.Series(pd.NA, dtype=pd.Int64Dtype()) + for ind, row in net[table].iterrows(): + characteristics = {} + fallbacks = {} + for name in columns: + # new column name does not end with _characteristic so it is sliced here. + new_name = name[:-len("_characteristic")] + if name in net[table] and pd.notna(row[name]): # check if col exists and is not nan + characteristics[new_name] = net.characteristic.loc[row[name], 'object'] + elif new_name in net[table] and pd.notna(row[new_name]): + fallbacks[new_name] = row[new_name] + if len(characteristics) == 0: + continue + tap_min = row["tap_min"] + if pd.isna(tap_min): + raise UserWarning(f'tap_min is missing for {table} with id {row.name}') + tap_max = row["tap_max"] + if pd.isna(tap_max): + raise UserWarning(f'tap_max is missing for {table} with id {row.name}') + # cast to int, for compatibility with range + tap_max = int(tap_max) + tap_min = int(tap_min) + tap_step_percent = row["tap_step_percent"] + tap_step_degree = row["tap_step_degree"] + if tap_max < tap_min: + tap_min, tap_max = tap_max, tap_min + vals = { + 'step': list(range(tap_min, tap_max + 1)) + } + vals['voltage_ratio'] = [(100 + step * tap_step_percent) / 100 for step in vals['step']] + vals['angle_deg'] = [step * tap_step_degree for step in vals['step']] + for key, value in fallbacks.items(): + vals[key] = [value] * len(vals['step']) + for key, value in characteristics.items(): + vals[key] = value.interpolator(range(tap_min, tap_max + 1)) + net[table].at[ind, 'id_characteristic_table'] = create_trafo_characteristic(net, vals) + net[table].at[ind, 'tap_dependency_table'] = True + + net[table] = net[table].drop(columns=columns, errors="ignore") # ignore errors if columns are not present + def convert_trafo_pst_logic(net): """ diff --git a/pandapower/converter/cim/__init__.py b/pandapower/converter/cim/__init__.py index ceef2ca23e..0285e2692f 100644 --- a/pandapower/converter/cim/__init__.py +++ b/pandapower/converter/cim/__init__.py @@ -1,5 +1,3 @@ -# -*- 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. diff --git a/pandapower/converter/cim/cim2pp/__init__.py b/pandapower/converter/cim/cim2pp/__init__.py index ff732ee93f..fc1328cdef 100644 --- a/pandapower/converter/cim/cim2pp/__init__.py +++ b/pandapower/converter/cim/cim2pp/__init__.py @@ -1,5 +1,3 @@ -# -*- 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. from . import converter_classes diff --git a/pandapower/converter/cim/cim2pp/build_pp_net.py b/pandapower/converter/cim/cim2pp/build_pp_net.py index cdfc3d8593..618d3ea5f9 100644 --- a/pandapower/converter/cim/cim2pp/build_pp_net.py +++ b/pandapower/converter/cim/cim2pp/build_pp_net.py @@ -1,5 +1,3 @@ -# -*- 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. import logging @@ -171,6 +169,10 @@ def convert_to_pp( self.report_container.add_log(Report(level=LogLevel.EXCEPTION, code=ReportCode.EXCEPTION_CONVERTING, message=traceback.format_exc())) + # check if SV data should be considered + if kwargs.get('use_sv_data_for_assets', False): + CreateMeasurements(self.net, self.cim).map_sv_data_from_assets() + # set the datatypes after the conversion, especially for integer and boolean columns self.net = pp_tools.set_pp_col_types(net=self.net) # create transformer tap controller diff --git a/pandapower/converter/cim/cim2pp/convert_measurements.py b/pandapower/converter/cim/cim2pp/convert_measurements.py index edaefdf438..e60d44041d 100644 --- a/pandapower/converter/cim/cim2pp/convert_measurements.py +++ b/pandapower/converter/cim/cim2pp/convert_measurements.py @@ -1,5 +1,3 @@ -# -*- 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. import logging @@ -352,3 +350,44 @@ def create_measurements_from_sv(self): self._set_measurement_element_datatype() self.logger.info("Needed time for creating the measurements: %ss" % (time.time() - time_start)) + + def map_sv_data_from_assets(self): + self.logger.info("--------------------------- Updating assets from SV ---------------------------") + time_start = time.time() + sc = cim_tools.get_pp_net_special_columns_dict() + # get the measurements from the sv profile and set the Terminal as index + sv_powerflow_g = self.cim['sv']['SvPowerFlow'][['Terminal', 'p', 'q']] + sv_powerflow_g['p'] *= -1 + sv_powerflow_g['q'] *= -1 + sv_powerflow_g = sv_powerflow_g.set_index('Terminal').to_dict() + sv_powerflow_l = self.cim['sv']['SvPowerFlow'][['Terminal', 'p', 'q']].set_index('Terminal').to_dict() + + # update sgen + self.net.sgen.p_mw = self.net.sgen[sc['t']].map(sv_powerflow_g['p']).fillna(self.net.sgen.p_mw) + self.net.sgen.q_mvar = self.net.sgen[sc['t']].map(sv_powerflow_g['q']).fillna(self.net.sgen.q_mvar) + # update gen + self.net.gen.p_mw = self.net.gen[sc['t']].map(sv_powerflow_g['p']).fillna(self.net.gen.p_mw) + + # update load + self.net.load.p_mw = self.net.load[sc['t']].map(sv_powerflow_l['p']).fillna(self.net.load.p_mw) + self.net.load.q_mvar = self.net.load[sc['t']].map(sv_powerflow_l['q']).fillna(self.net.load.q_mvar) + + # update ward + self.net.ward.ps_mw = self.net.ward[sc['t']].map(sv_powerflow_l['p']).fillna(self.net.ward.ps_mw) + self.net.ward.qs_mvar = self.net.ward[sc['t']].map(sv_powerflow_l['q']).fillna(self.net.ward.qs_mvar) + # update xward + self.net.xward.ps_mw = self.net.xward[sc['t']].map(sv_powerflow_l['p']).fillna(self.net.xward.ps_mw) + self.net.xward.qs_mvar = self.net.xward[sc['t']].map(sv_powerflow_l['q']).fillna(self.net.xward.qs_mvar) + + # update shunts + sv_scs = self.cim['sv']['SvShuntCompensatorSections'][['ShuntCompensator', 'sections']] + sv_scs = sv_scs.set_index('ShuntCompensator') + self.net.shunt.step = self.net.shunt[sc['o_id']].map(sv_scs['sections']).fillna(self.net.shunt.step) + + # update trafo tap changer position + sv_ts = self.cim['sv']['SvTapStep'][['TapChanger', 'position']].set_index('TapChanger') + self.net.trafo.tap_pos = self.net.trafo[sc['tc_id']].map(sv_ts['position']).fillna(self.net.trafo.tap_pos) + self.net.trafo.tap2_pos = self.net.trafo[sc['tc2_id']].map(sv_ts['position']).fillna(self.net.trafo.tap2_pos) + self.net.trafo3w.tap_pos = self.net.trafo3w[sc['tc_id']].map(sv_ts['position']).fillna(self.net.trafo3w.tap_pos) + + self.logger.info(f"Needed time for updating the assets: {time.time() - time_start}s") diff --git a/pandapower/converter/cim/cim2pp/converter_classes/connectivitynodes/connectivityNodesCim16.py b/pandapower/converter/cim/cim2pp/converter_classes/connectivitynodes/connectivityNodesCim16.py index 61305d2aa3..666af968ac 100644 --- a/pandapower/converter/cim/cim2pp/converter_classes/connectivitynodes/connectivityNodesCim16.py +++ b/pandapower/converter/cim/cim2pp/converter_classes/connectivitynodes/connectivityNodesCim16.py @@ -149,16 +149,17 @@ def _prepare_connectivity_nodes_cim16(self) -> Tuple[pd.DataFrame, pd.DataFrame] connectivity_nodes = connectivity_nodes.drop(columns=['BaseVoltage_2']) del mapping # check if there is a mix between BB and NB models - terminals_temp = \ - self.cimConverter.cim['eq']['Terminal'].loc[ - self.cimConverter.cim['eq']['Terminal']['ConnectivityNode'].isna(), 'rdfId'] + terminals_temp = self.cimConverter.cim['eq']['Terminal'].loc[ + self.cimConverter.cim['eq']['Terminal']['ConnectivityNode'].isna(), + 'rdfId' + ] if terminals_temp.index.size > 0: - terminals_temp = pd.merge(terminals_temp, + terminals_temp_df = pd.merge(terminals_temp, self.cimConverter.cim['tp']['Terminal'][['rdfId', 'TopologicalNode']], how='left', on='rdfId') - terminals_temp = terminals_temp.drop(columns=['rdfId']) - terminals_temp = terminals_temp.rename(columns={'TopologicalNode': 'rdfId'}) - terminals_temp = terminals_temp.drop_duplicates(subset=['rdfId']) + terminals_temp_df = terminals_temp_df.drop(columns=['rdfId']) + terminals_temp_df = terminals_temp_df.rename(columns={'TopologicalNode': 'rdfId'}) + terminals_temp_df = terminals_temp_df.drop_duplicates(subset=['rdfId']) tp_temp = self.cimConverter.cim['tp']['TopologicalNode'][ ['rdfId', 'name', 'description', 'BaseVoltage']] tp_temp[sc['o_prf']] = 'tp' @@ -168,7 +169,7 @@ def _prepare_connectivity_nodes_cim16(self) -> Tuple[pd.DataFrame, pd.DataFrame] sort=False) tp_temp[sc['o_prf']] = tp_temp[sc['o_prf']].fillna('tp_bd') tp_temp[sc['o_cl']] = 'TopologicalNode' - tp_temp = pd.merge(terminals_temp, tp_temp, how='inner', on='rdfId') + tp_temp = pd.merge(terminals_temp_df, tp_temp, how='inner', on='rdfId') connectivity_nodes = pd.concat([connectivity_nodes, tp_temp], ignore_index=True, sort=False) else: # Bus-Branch model @@ -252,8 +253,8 @@ def _prepare_connectivity_nodes_cim16(self) -> Tuple[pd.DataFrame, pd.DataFrame] self.cimConverter.report_container.add_log(Report( level=LogLevel.WARNING, code=ReportCode.WARNING_CONVERTING, message="There is a problem at the busses!")) - dups = connectivity_nodes.pivot_table(index=['rdfId'], aggfunc='size') - dups = dups.loc[dups != 1] + dups = connectivity_nodes.pivot_table(index=['rdfId'], aggfunc=lambda x: x.size) + dups = dups.loc[dups != 1] # type: ignore[call-overload] for rdfId, count in dups.items(): self.logger.warning("The ConnectivityNode with RDF ID %s has %s TopologicalNodes!" % (rdfId, count)) self.logger.warning("The ConnectivityNode data: \n%s" % @@ -280,7 +281,10 @@ def _prepare_connectivity_nodes_cim16(self) -> Tuple[pd.DataFrame, pd.DataFrame] sgr = self.cimConverter.cim['eq']['SubGeographicalRegion'][['rdfId', 'name', 'Region']] regions = pd.merge(self.cimConverter.cim['eq']['Substation'], sgr, left_on="Region", right_on="rdfId", suffixes=["_substation", "_SubGeographicalRegion"]) - regions = pd.merge(self.cimConverter.cim['eq']['GeographicalRegion'], regions, left_on="rdfId", right_on="Region_SubGeographicalRegion") + geo_regions = pd.concat( + [self.cimConverter.cim['eq']['GeographicalRegion'], self.cimConverter.cim['eq_bd']['GeographicalRegion']], + sort=False, ignore_index=True) + regions = pd.merge(geo_regions, regions, left_on="rdfId", right_on="Region_SubGeographicalRegion") regions = regions.rename(columns={'name': 'GeographicalRegion_name', 'name_SubGeographicalRegion': 'SubGeographicalRegion_name', 'rdfId': 'GeographicalRegion_id', 'rdfId_SubGeographicalRegion': 'SubGeographicalRegion_id'}) regions = regions.drop(columns=['name_substation', 'Region_substation', 'Region_SubGeographicalRegion']) diff --git a/pandapower/converter/cim/cim2pp/converter_classes/coordinates/coordinatesFromDLCim16.py b/pandapower/converter/cim/cim2pp/converter_classes/coordinates/coordinatesFromDLCim16.py index 3c659b39d6..4c76bcd516 100644 --- a/pandapower/converter/cim/cim2pp/converter_classes/coordinates/coordinatesFromDLCim16.py +++ b/pandapower/converter/cim/cim2pp/converter_classes/coordinates/coordinatesFromDLCim16.py @@ -18,12 +18,12 @@ def __init__(self, cimConverter: build_pp_net.CimConverter): self.logger = logging.getLogger(self.__class__.__name__) self.cimConverter = cimConverter - def add_coordinates_from_dl_cim16(self, diagram_name: str = None): + def add_coordinates_from_dl_cim16(self, diagram_name: str | None = None): self.logger.info("Creating the coordinates from CGMES DiagramLayout.") time_start = time.time() # choose a diagram if it is not given (the first one ascending) if diagram_name is None: - diagram_name = self.cimConverter.cim['dl']['Diagram'].sort_values(by='name')['name'].values[0] + diagram_name = self.cimConverter.cim['dl']['Diagram'].sort_values(by='name')['name'].values[0] # type: ignore[assignment] self.logger.debug("Choosing the geo coordinates from diagram %s" % diagram_name) if diagram_name != 'all': # reduce the source data to the chosen diagram only @@ -58,8 +58,9 @@ def add_coordinates_from_dl_cim16(self, diagram_name: str = None): one_ele_df['coords'] = one_ele_df[['xPosition', 'yPosition']].values.tolist() one_ele_df['coords'] = one_ele_df[['coords']].values.tolist() for _, df_group in one_ele_df.groupby(by=sc['o_id']): - one_ele_df.at[df_group.index.values[0], 'coords'] = df_group[ - ['xPosition', 'yPosition']].values.tolist() + one_ele_df.at[df_group.index.values[0], 'coords'] = df_group[ # type: ignore[assignment, index] + ['xPosition', 'yPosition'] + ].values.tolist() one_ele_df = one_ele_df.drop_duplicates([sc['o_id']], keep='first') one_ele_df['coords'] = one_ele_df['coords'].astype(str) one_ele_df['diagram'] = '{"coordinates": '+one_ele_df['coords'].astype(str)+', "type": "LineString"}' diff --git a/pandapower/converter/cim/cim2pp/converter_classes/generators/synchronousMachinesCim16.py b/pandapower/converter/cim/cim2pp/converter_classes/generators/synchronousMachinesCim16.py index 3dfbe17d22..1d895acc29 100644 --- a/pandapower/converter/cim/cim2pp/converter_classes/generators/synchronousMachinesCim16.py +++ b/pandapower/converter/cim/cim2pp/converter_classes/generators/synchronousMachinesCim16.py @@ -79,7 +79,7 @@ def _prepare_synchronous_machines_cim16(self) -> pd.DataFrame: # if voltage is not on connectivity node, topological node will be used eqtp_terminals = pd.merge(self.cimConverter.cim['eq']['Terminal'], self.cimConverter.cim['tp']['Terminal'], how='left', on='rdfId') - eqtp_terminals.ConnectivityNode = eqtp_terminals.ConnectivityNode.fillna(eqtp_terminals.TopologicalNode) + eqtp_terminals.ConnectivityNode = eqtp_terminals.ConnectivityNode.fillna(eqtp_terminals.TopologicalNode) # type: ignore[attr-defined] eqtp_terminals = eqtp_terminals[['rdfId', 'ConnectivityNode']].rename( columns={'rdfId': 'Terminal', 'ConnectivityNode': 'reg_control_cnode'}) eqssh_reg_control = pd.merge(eqssh_reg_control, eqtp_terminals, how='left', on='Terminal') diff --git a/pandapower/converter/cim/cim2pp/converter_classes/impedance/equivalentBranchesCim16.py b/pandapower/converter/cim/cim2pp/converter_classes/impedance/equivalentBranchesCim16.py index fcfabf78c3..6bdc214227 100644 --- a/pandapower/converter/cim/cim2pp/converter_classes/impedance/equivalentBranchesCim16.py +++ b/pandapower/converter/cim/cim2pp/converter_classes/impedance/equivalentBranchesCim16.py @@ -71,8 +71,8 @@ def _prepare_equivalent_branches_cim16(self) -> pd.DataFrame: message="There is a problem at the EquivalentBranches source data: Not each EquivalentBranch " "has two Terminals, %s Terminals should be given but there are %s Terminals available" % (eqb_length_before_merge * 2, eqb.index.size))) - dups = eqb.pivot_table(index=['rdfId'], aggfunc='size') - dups = dups.loc[dups != 2] + dups = eqb.pivot_table(index=['rdfId'], aggfunc='size') # type: ignore[arg-type] + dups = dups.loc[dups != 2] # type: ignore[call-overload] for rdfId, count in dups.items(): self.logger.warning("The EquivalentBranch with RDF ID %s has %s Terminals!" % (rdfId, count)) self.logger.warning("The EquivalentBranch data: \n%s" % eqb[eqb['rdfId'] == rdfId]) @@ -89,9 +89,9 @@ def _prepare_equivalent_branches_cim16(self) -> pd.DataFrame: eqb = eqb.reset_index() # here is where the magic happens: just remove the first value from the copied columns, reset the index # and replace the old column with the cut one. At least just remove the duplicates on column rdfId - eqb.rdfId_Terminal2 = eqb.rdfId_Terminal2.iloc[1:].reset_index().rdfId_Terminal2 - eqb.connected2 = eqb.connected2.iloc[1:].reset_index().connected2 - eqb.index_bus2 = eqb.index_bus2.iloc[1:].reset_index().index_bus2 + eqb.rdfId_Terminal2 = eqb.rdfId_Terminal2.iloc[1:].reset_index().rdfId_Terminal2 # type: ignore[attr-defined] + eqb.connected2 = eqb.connected2.iloc[1:].reset_index().connected2 # type: ignore[attr-defined] + eqb.index_bus2 = eqb.index_bus2.iloc[1:].reset_index().index_bus2 # type: ignore[attr-defined] eqb = eqb.drop_duplicates(['rdfId'], keep='first') if hasattr(self.cimConverter.net, 'sn_mva'): eqb['sn_mva'] = self.cimConverter.net['sn_mva'] diff --git a/pandapower/converter/cim/cim2pp/converter_classes/impedance/seriesCompensatorsCim16.py b/pandapower/converter/cim/cim2pp/converter_classes/impedance/seriesCompensatorsCim16.py index a6d2e439d8..7931cf9ebb 100644 --- a/pandapower/converter/cim/cim2pp/converter_classes/impedance/seriesCompensatorsCim16.py +++ b/pandapower/converter/cim/cim2pp/converter_classes/impedance/seriesCompensatorsCim16.py @@ -81,8 +81,8 @@ def _prepare_series_compensators_cim16(self) -> pd.DataFrame: message="There is a problem at the SeriesCompensators source data: Not each SeriesCompensator " "has two Terminals, %s Terminals should be given but there are %s Terminals available" % (eqs_length_before_merge * 2, ser_comp.index.size))) - dups = ser_comp.pivot_table(index=['rdfId'], aggfunc='size') - dups = dups.loc[dups != 2] + dups = ser_comp.pivot_table(index=['rdfId'], aggfunc='size') # type: ignore[arg-type] + dups = dups.loc[dups != 2] # type: ignore[call-overload] for rdfId, count in dups.items(): self.logger.warning("The SeriesCompensator with RDF ID %s has %s Terminals!" % (rdfId, count)) self.logger.warning("The SeriesCompensator data: \n%s" % ser_comp[ser_comp['rdfId'] == rdfId]) @@ -99,9 +99,9 @@ def _prepare_series_compensators_cim16(self) -> pd.DataFrame: ser_comp = ser_comp.reset_index() # here is where the magic happens: just remove the first value from the copied columns, reset the index # and replace the old column with the cut one. At least just remove the duplicates on column rdfId - ser_comp.rdfId_Terminal2 = ser_comp.rdfId_Terminal2.iloc[1:].reset_index().rdfId_Terminal2 - ser_comp.connected2 = ser_comp.connected2.iloc[1:].reset_index().connected2 - ser_comp.index_bus2 = ser_comp.index_bus2.iloc[1:].reset_index().index_bus2 + ser_comp.rdfId_Terminal2 = ser_comp.rdfId_Terminal2.iloc[1:].reset_index().rdfId_Terminal2 # type: ignore[attr-defined] + ser_comp.connected2 = ser_comp.connected2.iloc[1:].reset_index().connected2 # type: ignore[attr-defined] + ser_comp.index_bus2 = ser_comp.index_bus2.iloc[1:].reset_index().index_bus2 # type: ignore[attr-defined] ser_comp = ser_comp.drop_duplicates(['rdfId'], keep='first') if hasattr(self.cimConverter.net, 'sn_mva'): ser_comp['sn_mva'] = self.cimConverter.net['sn_mva'] diff --git a/pandapower/converter/cim/cim2pp/converter_classes/lines/acLineSegmentsCim16.py b/pandapower/converter/cim/cim2pp/converter_classes/lines/acLineSegmentsCim16.py index 742d39ee5c..3950ab1f67 100644 --- a/pandapower/converter/cim/cim2pp/converter_classes/lines/acLineSegmentsCim16.py +++ b/pandapower/converter/cim/cim2pp/converter_classes/lines/acLineSegmentsCim16.py @@ -80,8 +80,8 @@ def _prepare_ac_line_segments_cim16(self, convert_line_to_switch, line_r_limit, self.cimConverter.report_container.add_log(Report( level=LogLevel.ERROR, code=ReportCode.ERROR_CONVERTING, message="Error processing the ACLineSegments, there is a problem with Terminals in the source data!")) - dups = ac_line_segments.pivot_table(index=['rdfId'], aggfunc='size') - dups = dups.loc[dups != 2] + dups = ac_line_segments.pivot_table(index=['rdfId'], aggfunc='size') # type: ignore[arg-type] + dups = dups.loc[dups != 2] # type: ignore[call-overload] for rdfId, count in dups.items(): self.logger.warning("The ACLineSegment with RDF ID %s has %s Terminals!" % (rdfId, count)) self.logger.warning("The ACLineSegment data: \n%s" % @@ -113,7 +113,7 @@ def _prepare_ac_line_segments_cim16(self, convert_line_to_switch, line_r_limit, 'OperationalLimitSet': 'rdfId_OperationalLimitSet'}) ac_line_segments = pd.merge(ac_line_segments, current_limits, how='left', on='rdfId_OperationalLimitSet') - ac_line_segments.value = ac_line_segments.value.astype(float) + ac_line_segments.value = ac_line_segments.value.astype(float) # type: ignore[attr-defined] # sort by rdfId, sequenceNumber and value. value is max_i_ka, choose the lowest one if more than one is # given (A line may have more than one max_i_ka in CIM, different modes e.g. normal) ac_line_segments = ac_line_segments.sort_values(by=['rdfId', 'sequenceNumber', 'value']) @@ -127,11 +127,10 @@ def _prepare_ac_line_segments_cim16(self, convert_line_to_switch, line_r_limit, ac_line_segments = ac_line_segments.reset_index() # here is where the magic happens: just remove the first value from the copied columns, reset the index # and replace the old column with the cut one. At least just remove the duplicates on column rdfId - ac_line_segments.rdfId_Terminal2 = ac_line_segments.rdfId_Terminal2.iloc[ - 1:].reset_index().rdfId_Terminal2 - ac_line_segments.connected2 = ac_line_segments.connected2.iloc[1:].reset_index().connected2 - ac_line_segments.index_bus2 = ac_line_segments.index_bus2.iloc[1:].reset_index().index_bus2 - ac_line_segments.value2 = ac_line_segments.value2.iloc[1:].reset_index().value2 + ac_line_segments.rdfId_Terminal2 = ac_line_segments.rdfId_Terminal2.iloc[1:].reset_index().rdfId_Terminal2 # type: ignore[attr-defined] + ac_line_segments.connected2 = ac_line_segments.connected2.iloc[1:].reset_index().connected2 # type: ignore[attr-defined] + ac_line_segments.index_bus2 = ac_line_segments.index_bus2.iloc[1:].reset_index().index_bus2 # type: ignore[attr-defined] + ac_line_segments.value2 = ac_line_segments.value2.iloc[1:].reset_index().value2 # type: ignore[attr-defined] ac_line_segments = ac_line_segments.drop_duplicates(['rdfId'], keep='first') # get the max_i_ka ac_line_segments['max_i_ka'] = ac_line_segments['value'].fillna(ac_line_segments['value2']) * 1e-3 diff --git a/pandapower/converter/cim/cim2pp/converter_classes/lines/dcLineSegmentsCim16.py b/pandapower/converter/cim/cim2pp/converter_classes/lines/dcLineSegmentsCim16.py index 3fabfd6eb3..4f1d6182c4 100644 --- a/pandapower/converter/cim/cim2pp/converter_classes/lines/dcLineSegmentsCim16.py +++ b/pandapower/converter/cim/cim2pp/converter_classes/lines/dcLineSegmentsCim16.py @@ -83,7 +83,7 @@ def _prepare_dc_line_segments_cim16(self) -> pd.DataFrame: t = self.cimConverter.cim['eq']['DCTerminal'][['DCNode', 'DCConductingEquipment', 'sequenceNumber']] t = t.rename(columns={'DCNode': 'ConnectivityNode', 'DCConductingEquipment': 'ConductingEquipment'}) - def search_converter(cn_ids: Dict[str, str], visited_cns: List[str]) -> str: + def search_converter(cn_ids: Dict[str, str], visited_cns: List[str]) -> str | None: new_cn_dict = {} for one_cn, from_dev in cn_ids.items(): # get the Terminals @@ -103,11 +103,13 @@ def search_converter(cn_ids: Dict[str, str], visited_cns: List[str]) -> str: new_cn_dict[id_temp] = one_t['ConductingEquipment'] if len(new_cn_dict) > 0: visited_cns.extend(list(cn_ids)) - return search_converter(cn_ids=new_cn_dict, visited_cns=visited_cns) + return search_converter(cn_ids=new_cn_dict, visited_cns=visited_cns) # type: ignore[arg-type] + + return None for row_index, row in dc_line_segments[dc_line_segments['converters'].isna()].iterrows(): - conv = search_converter(cn_ids={row['ConnectivityNode']: row['rdfId']}, - visited_cns=[row['ConnectivityNode']]) + conv = search_converter(cn_ids={row['ConnectivityNode']: row['rdfId']}, # type: ignore[dict-item] + visited_cns=[row['ConnectivityNode']]) # type: ignore[list-item] dc_line_segments.loc[row_index, 'converters'] = conv if conv is None: self.logger.warning("Problem with converting tht DC line %s: No ACDC converter found, maybe the DC " @@ -132,9 +134,12 @@ def search_converter(cn_ids: Dict[str, str], visited_cns: List[str]) -> str: dc_line_segments[one_item + '2'] = dc_line_segments[one_item + '2'].iloc[1:].reset_index()[one_item + '2'] del copy_list, one_item dc_line_segments = dc_line_segments.drop_duplicates(['rdfId'], keep='first') - dc_line_segments = pd.merge(dc_line_segments, - pd.DataFrame(dc_line_segments.pivot_table(index=['converters'], aggfunc='size'), - columns=['converter_dups']), how='left', on='converters') + dc_line_segments = pd.merge( + dc_line_segments, + pd.DataFrame( + dc_line_segments.pivot_table(index=['converters'], aggfunc='size'), columns=['converter_dups']), # type: ignore[arg-type] + how='left', on='converters' + ) dc_line_segments['loss_mw'] = \ abs(abs(dc_line_segments['p']) - abs(dc_line_segments['p2'])) / dc_line_segments['converter_dups'] dc_line_segments['p_mw'] = dc_line_segments['p'] / dc_line_segments['converter_dups'] diff --git a/pandapower/converter/cim/cim2pp/converter_classes/switches/switchesCim16.py b/pandapower/converter/cim/cim2pp/converter_classes/switches/switchesCim16.py index e9a3ff4bec..9970f7a728 100644 --- a/pandapower/converter/cim/cim2pp/converter_classes/switches/switchesCim16.py +++ b/pandapower/converter/cim/cim2pp/converter_classes/switches/switchesCim16.py @@ -56,9 +56,9 @@ def _prepare_switches_cim16(self) -> pd.DataFrame: if eqssh_switches.index.size == switch_length_before_merge * 2: # here is where the magic happens: just remove the first value from the copied columns, reset the index # and replace the old column with the cut one. At least just remove the duplicates on column rdfId - eqssh_switches.rdfId_Terminal2 = eqssh_switches.rdfId_Terminal2.iloc[1:].reset_index().rdfId_Terminal2 - eqssh_switches.connected2 = eqssh_switches.connected2.iloc[1:].reset_index().connected2 - eqssh_switches.index_bus2 = eqssh_switches.index_bus2.iloc[1:].reset_index().index_bus2 + eqssh_switches.rdfId_Terminal2 = eqssh_switches.rdfId_Terminal2.iloc[1:].reset_index().rdfId_Terminal2 # type: ignore[attr-defined] + eqssh_switches.connected2 = eqssh_switches.connected2.iloc[1:].reset_index().connected2 # type: ignore[attr-defined] + eqssh_switches.index_bus2 = eqssh_switches.index_bus2.iloc[1:].reset_index().index_bus2 # type: ignore[attr-defined] eqssh_switches = eqssh_switches.drop_duplicates(subset=['rdfId'], keep='first') else: self.logger.error("Something went wrong at switches, seems like that terminals for connection with " @@ -67,8 +67,8 @@ def _prepare_switches_cim16(self) -> pd.DataFrame: level=LogLevel.ERROR, code=ReportCode.ERROR_CONVERTING, message="Something went wrong at switches, seems like that terminals for connection with " "connectivity nodes are missing!")) - dups = eqssh_switches.pivot_table(index=['rdfId'], aggfunc='size') - dups = dups.loc[dups != 2] + dups = eqssh_switches.pivot_table(index=['rdfId'], aggfunc='size') # type: ignore[arg-type] + dups = dups.loc[dups != 2] # type: ignore[call-overload] for rdfId, count in dups.items(): self.logger.warning("The switch with RDF ID %s has %s Terminals!" % (rdfId, count)) self.logger.warning("The switch data: \n%s" % eqssh_switches[eqssh_switches['rdfId'] == rdfId]) diff --git a/pandapower/converter/cim/cim2pp/converter_classes/transformers/powerTransformersCim16.py b/pandapower/converter/cim/cim2pp/converter_classes/transformers/powerTransformersCim16.py index 6a6d8b966e..5b07be8ee5 100644 --- a/pandapower/converter/cim/cim2pp/converter_classes/transformers/powerTransformersCim16.py +++ b/pandapower/converter/cim/cim2pp/converter_classes/transformers/powerTransformersCim16.py @@ -238,39 +238,35 @@ def _create_trafo_characteristic_table(self, trafo_type, trafo_df_origin): 'vkr_hv_percent': [], 'vkr_mv_percent': [], 'vkr_lv_percent': [], 'vk_hv_percent': [], 'vk_mv_percent': [], 'vk_lv_percent': []} - def append_row(res_dict, id_c, row, cols): - res_dict['id_characteristic'].append(id_c) - res_dict['step'].append(row.tabular_step) - for variable in ['voltage_ratio', 'angle_deg', 'vkr_percent', 'vk_percent', 'vk_hv_percent', - 'vkr_hv_percent', 'vk_mv_percent', 'vkr_mv_percent', 'vk_lv_percent', 'vkr_lv_percent']: - if variable in cols: - res_dict[variable].append(getattr(row, variable)) - - id_characteristic = self.cimConverter.net['trafo_characteristic_table']['id_characteristic'].max() + 1 - if math.isnan(id_characteristic): - id_characteristic = 0 - for one_id, one_df in trafo_df.groupby(sc['pte_id']): - # get next id_characteristic - if len(append_dict['id_characteristic']) > 0: - id_characteristic = max(append_dict['id_characteristic']) + 1 - # set the ID at the corresponding transformer - trafo_df_origin.loc[trafo_df_origin['PowerTransformer'] == trafo_df_origin.loc[ - trafo_df_origin[sc['pte_id']] == one_id, 'PowerTransformer'].values[ - 0], 'id_characteristic_table'] = id_characteristic - # iterate over the rows and get the desired data - for one_row in one_df.itertuples(): - # to add only selected characteristic data instead of all available data, disable the next line and - # uncomment the rest - append_row(append_dict, id_characteristic, one_row, one_df.columns) - # if one_row.tabular_step == one_row.highStep: - # append_row(append_dict, id_characteristic, one_row, one_df.columns) - # elif one_row.tabular_step == one_row.lowStep: - # append_row(append_dict, id_characteristic, one_row, one_df.columns) - # elif one_row.tabular_step == one_row.neutralStep: - # append_row(append_dict, id_characteristic, one_row, one_df.columns) - # elif one_row.tabular_step == one_row.step and one_row.step != one_row.highStep \ - # and one_row.step != one_row.lowStep and one_row.step != one_row.neutralStep: - # append_row(append_dict, id_characteristic, one_row, one_df.columns) + value_cols = [col for col in append_dict if col not in ('id_characteristic', 'step')] + + id_characteristic_base = self.cimConverter.net['trafo_characteristic_table']['id_characteristic'].max() + 1 + if math.isnan(id_characteristic_base): + id_characteristic_base = 0 + id_characteristic_base = int(id_characteristic_base) + + # One id_characteristic per tap-changer end; stable sort + factorize preserves legacy groupby order. + trafo_df = trafo_df[trafo_df[sc['pte_id']].notna()].sort_values(sc['pte_id'], kind='stable') + if trafo_df.index.size > 0: + group_codes = pd.factorize(trafo_df[sc['pte_id']], sort=True)[0] + trafo_df['id_characteristic'] = group_codes + id_characteristic_base + + char_df = pd.DataFrame({ + 'id_characteristic': trafo_df['id_characteristic'].to_numpy(), + 'step': trafo_df['tabular_step'].to_numpy(), + }) + for variable in value_cols: + char_df[variable] = trafo_df[variable].to_numpy() + + char_per_pte = trafo_df.drop_duplicates(subset=sc['pte_id']).set_index(sc['pte_id'])['id_characteristic'] + pte_to_pt = (trafo_df_origin.loc[trafo_df_origin[sc['pte_id']].isin(char_per_pte.index), + [sc['pte_id'], 'PowerTransformer']] + .drop_duplicates(subset=sc['pte_id']).set_index(sc['pte_id'])['PowerTransformer']) + pt_to_char = pd.Series(char_per_pte.reindex(pte_to_pt.index).to_numpy(), index=pte_to_pt.to_numpy()) + mapped_char = trafo_df_origin['PowerTransformer'].map(pt_to_char) + trafo_df_origin.loc[mapped_char.notna(), 'id_characteristic_table'] = mapped_char[mapped_char.notna()] + else: + char_df = pd.DataFrame(append_dict) # create tap_dependency_table flag if 'tap_dependency_table' not in trafo_df_origin.columns: @@ -279,7 +275,7 @@ def append_row(res_dict, id_c, row, cols): trafo_df_origin.loc[trafo_df_origin['id_characteristic_table'].notna(), 'tap_dependency_table'] = True self.cimConverter.net['trafo_characteristic_table'] = pd.concat( - [self.cimConverter.net['trafo_characteristic_table'], pd.DataFrame(append_dict)], + [self.cimConverter.net['trafo_characteristic_table'], char_df], ignore_index=True, sort=False) self.cimConverter.net['trafo_characteristic_table']['step'] = \ self.cimConverter.net['trafo_characteristic_table']['step'].astype(int) @@ -392,12 +388,12 @@ def _prepare_power_transformers_cim16(self) -> pd.DataFrame: keep_index = keep_index[0] else: self.logger.warning("Ignoring PhaseTapChangerTabular with ID: %s. The current tap position is missing " - "in the PhaseTapChangerTablePoints!" % one_id) - ptct = ptct.drop(drop_index) + "in the PhaseTapChangerTablePoints!" % one_id) # type: ignore[str-bytes-safe] + ptct = ptct.drop(drop_index) # type: ignore[call-overload] continue one_df = one_df.set_index('step') neutral_step = one_df['neutralStep'].iloc[0] - ptct = ptct.drop(drop_index) + ptct = ptct.drop(drop_index) # type: ignore[call-overload] # keep the angle and ratio based on neutral tap position (to populate tap_step_percent and tap_step_degree) ptct.loc[keep_index, 'angle'] = one_df.loc[neutral_step, 'angle'] ptct.loc[keep_index, 'ratio'] = (one_df.loc[neutral_step, 'ratio'] - 1) * 100 diff --git a/pandapower/converter/cim/cim2pp/converter_classes/wards/equivalentInjectionsCim16.py b/pandapower/converter/cim/cim2pp/converter_classes/wards/equivalentInjectionsCim16.py index ee35f006a0..1d190a3640 100644 --- a/pandapower/converter/cim/cim2pp/converter_classes/wards/equivalentInjectionsCim16.py +++ b/pandapower/converter/cim/cim2pp/converter_classes/wards/equivalentInjectionsCim16.py @@ -1,5 +1,6 @@ import logging import time +from typing import runtime_checkable, Protocol import pandas as pd @@ -34,6 +35,11 @@ def convert_equivalent_injections_cim16(self): (eqssh_ei_wards.index.size, eqssh_ei_xwards.index.size, time.time() - time_start))) def _prepare_equivalent_injections_cim16(self) -> pd.DataFrame: + + @runtime_checkable + class EquivalentInjectionDF(Protocol): + nominalVoltage: pd.Series + if 'sc' in self.cimConverter.cim: equivalent_injection = self.cimConverter.merge_eq_other_profiles(['ssh', 'sc'], 'EquivalentInjection', add_cim_type_column=True) @@ -50,7 +56,7 @@ def _prepare_equivalent_injections_cim16(self) -> pd.DataFrame: # maybe the BaseVoltage is not given, also get the nominalVoltage from the buses equivalent_injection = pd.merge(equivalent_injection, self.cimConverter.net.bus[['vn_kv']], how='left', left_on='index_bus', right_index=True) - equivalent_injection.nominalVoltage = equivalent_injection.nominalVoltage.fillna(equivalent_injection.vn_kv) + equivalent_injection.nominalVoltage = equivalent_injection.nominalVoltage.fillna(equivalent_injection.vn_kv) # type: ignore[attr-defined] equivalent_injection['regulationStatus'] = equivalent_injection['regulationStatus'].fillna(False) equivalent_injection['vm_pu'] = equivalent_injection.regulationTarget / equivalent_injection.nominalVoltage if self.cimConverter.cim_version == '3.0': diff --git a/pandapower/converter/cim/cim2pp/from_cim.py b/pandapower/converter/cim/cim2pp/from_cim.py index 93758212f7..aa9bbc5d21 100644 --- a/pandapower/converter/cim/cim2pp/from_cim.py +++ b/pandapower/converter/cim/cim2pp/from_cim.py @@ -1,5 +1,3 @@ -# -*- 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. @@ -18,12 +16,12 @@ def from_cim_dict(cim_parser: cim_classes.CimParser, log_debug=False, convert_line_to_switch: bool = False, line_r_limit: float = 0.1, line_x_limit: float = 0.1, - repair_cim: Union[str, interfaces.CIMRepair] = None, - repair_cim_class: Type[interfaces.CIMRepair] = None, - repair_pp: Union[str, interfaces.PandapowerRepair] = None, - repair_pp_class: Type[interfaces.PandapowerRepair] = None, - custom_converter_classes: Dict = None, - cim_version: str = None, + repair_cim: Union[str, interfaces.CIMRepair] | None = None, + repair_cim_class: Type[interfaces.CIMRepair] | None = None, + repair_pp: Union[str, interfaces.PandapowerRepair] | None = None, + repair_pp_class: Type[interfaces.PandapowerRepair] | None = None, + custom_converter_classes: Dict | None = None, + cim_version: str | None = None, **kwargs) -> pandapowerNet: """ Creates a pandapower net from a CIM data structure. @@ -102,13 +100,13 @@ def get_converter_classes() -> dict[str, object]: return converter_classes -def from_cim(file_list: Union[str, List[str]] = None, encoding: str = None, convert_line_to_switch: bool = False, +def from_cim(file_list: Union[str, List[str]] | None = None, encoding: str | None = None, convert_line_to_switch: bool = False, line_r_limit: float = 0.1, line_x_limit: float = 0.1, - repair_cim: Union[str, interfaces.CIMRepair] = None, - repair_cim_class: Type[interfaces.CIMRepair] = None, - repair_pp: Union[str, interfaces.PandapowerRepair] = None, - repair_pp_class: Type[interfaces.PandapowerRepair] = None, - custom_converter_classes: Dict = None, + repair_cim: Union[str, interfaces.CIMRepair] | None = None, + repair_cim_class: Type[interfaces.CIMRepair] | None = None, + repair_pp: Union[str, interfaces.PandapowerRepair] | None = None, + repair_pp_class: Type[interfaces.PandapowerRepair] | None = None, + custom_converter_classes: Dict | None = None, cgmes_version: str = '2.4.15', **kwargs) -> pandapowerNet: """ Converts a CIM net to a pandapower net from XML files. @@ -128,6 +126,8 @@ def from_cim(file_list: Union[str, List[str]] = None, encoding: str = None, conv - ignore_errors (bool): Option to disable raising of internal errors. Useful if you need to get a network not matter if there are errors in the conversion. Default: True. - set_switch_impedance (bool): Set the line impedance for lines that will be converted to switches + - use_sv_data_for_assets (bool): Use the SV PowerFlow, SvTapStep and SvShuntCompensatorSections and map them to the + assets in the grid. Default: False. :param file_list: The path to the CGMES files as a string or list. :param encoding: The encoding from the files. Optional, default: None diff --git a/pandapower/converter/cim/cim_classes.py b/pandapower/converter/cim/cim_classes.py index 042353ba11..1a522897e4 100644 --- a/pandapower/converter/cim/cim_classes.py +++ b/pandapower/converter/cim/cim_classes.py @@ -1,5 +1,3 @@ -# -*- 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. from __future__ import annotations @@ -95,8 +93,8 @@ def set_cim_data_types(self) -> CimParser: self.logger.debug("Skipping CIM element type %s from profile %s." % (cim_element_type, profile)) continue if col in cim_schema[profile][cim_element_type]['fields'] and \ - 'data_type_prim' in cim_schema[profile][cim_element_type]['fields'][col]: - data_type_col_str = cim_schema[profile][cim_element_type]['fields'][col]['data_type_prim'] + 'data_type_prim' in cim_schema[profile][cim_element_type]['fields'][col]: # type: ignore[index] + data_type_col_str = cim_schema[profile][cim_element_type]['fields'][col]['data_type_prim'] # type: ignore[index] if data_type_col_str in data_types_map: data_type_col = data_types_map[data_type_col_str] else: @@ -116,7 +114,7 @@ def set_cim_data_types(self) -> CimParser: self.cim[profile][cim_element_type][col] = \ self.cim[profile][cim_element_type][col].astype(float_type) self.cim[profile][cim_element_type][col] = \ - self.cim[profile][cim_element_type][col].astype(data_type_col) + self.cim[profile][cim_element_type][col].astype(data_type_col) # type: ignore[call-overload] except Exception as e: self.logger.warning(f"Couldn't set the datatype to {data_type_col_str} for field {col} " f"at CIM type {cim_element_type} in profile {profile}!") @@ -311,7 +309,8 @@ def _initialize_cim16_data_structure(self) -> MappingProxyType[str, MappingProxy 'Terminal': pd.DataFrame( columns=['rdfId', 'ConnectivityNode', 'ConductingEquipment', 'sequenceNumber']), 'EnergySource': pd.DataFrame(columns=['rdfId', 'name', 'nominalVoltage', 'EnergySchedulingType']), - 'EnergySchedulingType': pd.DataFrame(columns=['rdfId', 'name']) + 'EnergySchedulingType': pd.DataFrame(columns=['rdfId', 'name']), + 'GeographicalRegion': pd.DataFrame(columns=['rdfId', 'name']) }), 'ssh': MappingProxyType({ 'ControlArea': pd.DataFrame(columns=['rdfId', 'netInterchange']), @@ -384,16 +383,18 @@ def _initialize_cim16_data_structure(self) -> MappingProxyType[str, MappingProxy def _parse_element(self, element, parsed=None): if parsed is None: parsed = {} - for key in element.keys(): - combined_key = element.tag + '-' + key + tag = element.tag + for key, value in element.attrib.items(): + combined_key = tag + '-' + key if combined_key not in parsed: - parsed[combined_key] = element.attrib.get(key) + parsed[combined_key] = value else: - if not isinstance(parsed[combined_key], list): - parsed[combined_key] = [parsed[combined_key]] - parsed[combined_key].append(element.attrib.get(key)) - if element.tag not in parsed and element.text is not None and element.text.strip(' \t\n\r'): - parsed[element.tag] = element.text + existing = parsed[combined_key] + if not isinstance(existing, list): + existing = parsed[combined_key] = [existing] + existing.append(value) + if tag not in parsed and element.text is not None and element.text.strip(' \t\n\r'): + parsed[tag] = element.text for child in element: self._parse_element(child, parsed) return parsed @@ -416,11 +417,11 @@ def _get_cgmes_profile_from_xml(self, root: etree._Element, default_profile: str element_types = element_types.drop_duplicates() full_model = element_types.str.find('FullModel') if full_model.max() >= 0: - full_model = element_types[full_model >= 0].values[0] + full_model = element_types[full_model >= 0].values[0] # type: ignore[assignment] else: - full_model = 'FullModel' - full_model_profile = full_model[:-9] + 'Model.profile' - full_model_df = self._get_df(root.findall('.//' + full_model)) + full_model = 'FullModel' # type: ignore[assignment] + full_model_profile = full_model[:-9] + "Model.profile" # type: ignore[operator] + full_model_df = self._get_df(root.findall(".//" + full_model)) # type: ignore[arg-type,operator] if full_model_df.index.size == 0 and self.ignore_errors: self.logger.warning("The FullModel is not given in the XML tree, returning %s" % default_profile) return default_profile @@ -502,23 +503,26 @@ def _parse_source_file(self, file: str, output: dict, encoding: str | None): xml_tree = etree.parse(file, parser) prf = self._get_cgmes_profile_from_xml(xml_tree.getroot()) self.file_names[prf] = file - self._parse_xml_tree(xml_tree.getroot(), prf, output) + self._parse_xml_tree(xml_tree.getroot(), prf, output) # type: ignore[arg-type] - def _parse_xml_tree(self, xml_tree, profile_name: str, output: dict | None = None): + def _parse_xml_tree(self, xml_tree: etree._Element, profile_name: str, output: dict | None = None): output = self.cim if output is None else output - # get all CIM elements to parse - element_types = pd.Series([ele.tag for ele in xml_tree]) - element_types = element_types.drop_duplicates() + # group the direct children by tag in a single pass; dict insertion order matches the previous + # drop_duplicates ordering while avoiding a separate findall scan of the tree per element type + elements_by_type: dict[str, list] = {} + for ele in xml_tree: + tag = ele.tag + if not isinstance(tag, str): + continue + elements_by_type.setdefault(tag, []).append(ele) prf_content: dict[str, pd.DataFrame] = {} - ns_dict = {} + ns_dict: dict = {} prf = profile_name if prf not in ns_dict: ns_dict[prf] = {} - for _, element_type in element_types.items(): - if not isinstance(element_type, str): - continue + for element_type, elements in elements_by_type.items(): element_type_c = re.sub('{.*}', '', element_type) - prf_content[element_type_c] = self._get_df(xml_tree.findall(element_type)) + prf_content[element_type_c] = self._get_df(elements) # rename the columns (remove the namespaces) if element_type_c not in ns_dict[prf]: ns_dict[prf][element_type_c] = {} @@ -536,10 +540,10 @@ def _parse_xml_tree(self, xml_tree, profile_name: str, output: dict | None = Non # get the namespace from the literal, Note: get the largest string because some values could # be nan name_space = \ - prf_content[element_type_c][col].values[prf_content[element_type_c][col].str.len().idxmax()] + prf_content[element_type_c][col].values[prf_content[element_type_c][col].str.len().idxmax()] # type: ignore[index] # remove the namespace from the literal prf_content[element_type_c][col] = \ - prf_content[element_type_c][col].str[name_space.rfind('.') + 1:] + prf_content[element_type_c][col].str[name_space.rfind('.') + 1:] # type: ignore[union-attr] elif col_new.endswith('-about'): col_new = 'rdfId' prf_content[element_type_c][col] = prf_content[element_type_c][col].str[1:] @@ -550,12 +554,12 @@ def _parse_xml_tree(self, xml_tree, profile_name: str, output: dict | None = Non if prf not in output: output[prf] = prf_content else: - for ele, df in prf_content.items(): + for ele, df in prf_content.items(): # type: ignore[assignment] if ele not in output[prf]: - concat_list = [prf_content[ele]] + concat_list = [prf_content[ele]] # type: ignore[index] else: - concat_list = [output[prf][ele], prf_content[ele]] - output[prf][ele] = pd.concat(concat_list, ignore_index=True, sort=False) + concat_list = [output[prf][ele], prf_content[ele]] # type: ignore[index] + output[prf][ele] = pd.concat(concat_list, ignore_index=True, sort=False) # type: ignore[index] def _check_file(self, file: str) -> bool: if not os.path.isfile(file): @@ -725,7 +729,8 @@ def _initialize_cim100_data_structure(self) -> MappingProxyType[str, MappingProx 'Terminal': pd.DataFrame( columns=['rdfId', 'ConnectivityNode', 'ConductingEquipment', 'sequenceNumber']), 'EnergySource': pd.DataFrame(columns=['rdfId', 'nominalVoltage']), - 'EnergySchedulingType': pd.DataFrame(columns=['rdfId', 'name']) + 'EnergySchedulingType': pd.DataFrame(columns=['rdfId', 'name']), + 'GeographicalRegion': pd.DataFrame(columns=['rdfId', 'name']) }), 'op': MappingProxyType({ # TODO: value attributes from AnalogValue and DiscreteValue need clarification 'Analog': pd.DataFrame(columns=[ diff --git a/pandapower/converter/cim/cim_tools.py b/pandapower/converter/cim/cim_tools.py index 8b5bbc9d0a..485afd2619 100644 --- a/pandapower/converter/cim/cim_tools.py +++ b/pandapower/converter/cim/cim_tools.py @@ -57,7 +57,7 @@ def extend_pp_net_cim(net: pandapowerNet, override: bool = True) -> pandapowerNe return net -def get_cim_schema(cgmes_version: str = '2.4.15') -> Dict[str, Dict[str, Dict[str, str or Dict[str, Dict[str, str]]]]]: +def get_cim_schema(cgmes_version: str = '2.4.15') -> Dict[str, Dict[str, Dict[str, str | Dict[str, Dict[str, str]]]]]: """ Parses the CIM schema from the serialized CIM schema json files which have been created from the RDF schema files. The schema is parsed for the serializer from the CIM data structure used by the cim2pp and pp2cim converters. @@ -80,3 +80,5 @@ def get_cim_schema(cgmes_version: str = '2.4.15') -> Dict[str, Dict[str, Dict[st with open(path_to_schema, encoding='UTF-8', mode='r') as f: cim_schema = json.load(f) return cim_schema + + raise RuntimeError("This line should not be reached") diff --git a/pandapower/converter/cim/interfaces.py b/pandapower/converter/cim/interfaces.py index 5d76ac733d..611bf60fb7 100644 --- a/pandapower/converter/cim/interfaces.py +++ b/pandapower/converter/cim/interfaces.py @@ -1,10 +1,9 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- """ @author: mrichter """ from __future__ import annotations + from typing import List, Dict, Union import pandas as pd import json @@ -27,40 +26,38 @@ class AssetType(CustomLogger): class RepairMain(CustomLogger): - def __init__(self, assets: List[str] = None): + def __init__(self, assets: List[AssetType] | None = None): super().__init__() self.assets = assets def get_assets(self): pass - def repair(self, data: Dict[str, pd.DataFrame], report_container: ReportContainer = None): + def repair(self, data: Dict[str, pd.DataFrame], report_container: ReportContainer | None = None): pass - def deserialize(self, path_or_json_str: str, report_container: ReportContainer) -> RepairMain: + def deserialize(self, path_or_json_str: str, report_container: ReportContainer) -> RepairMain: # type: ignore[empty-body] pass class PandapowerRepair(RepairMain): - def __init__(self, assets: List[AssetType] = None): + def __init__(self, assets: List[AssetType] | None = None): super().__init__(assets) - def deserialize(self, path_or_json_str: Union[str, PandapowerRepair], report_container: ReportContainer = None) -> \ - PandapowerRepair: + def deserialize(self, path_or_json_str: Union[str, PandapowerRepair], report_container: ReportContainer | None = None) -> PandapowerRepair: # type: ignore[empty-body] pass class CIMRepair(CustomLogger): - def __init__(self, profiles: Dict[str, RepairMain] = None): + def __init__(self, profiles: Dict[str, RepairMain] | None = None): super().__init__() self.profiles = profiles def set_profile(self, profile: str, repair_main: RepairMain): pass - def repair(self, cim_dict: Dict[str, Dict[str, pd.DataFrame]], report_container: ReportContainer = None): + def repair(self, cim_dict: Dict[str, Dict[str, pd.DataFrame]], report_container: ReportContainer | None = None): pass - def deserialize(self, path_or_json_str: Union[str, CIMRepair], report_container: ReportContainer = None) -> \ - CIMRepair: + def deserialize(self, path_or_json_str: Union[str, CIMRepair], report_container: ReportContainer | None = None) -> CIMRepair: # type: ignore[empty-body] pass diff --git a/pandapower/converter/cim/other_classes.py b/pandapower/converter/cim/other_classes.py index 41ea954096..b86aa31f8a 100644 --- a/pandapower/converter/cim/other_classes.py +++ b/pandapower/converter/cim/other_classes.py @@ -1,14 +1,14 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- """ @author: mrichter """ from __future__ import annotations + import datetime import enum import json from typing import List, Dict + from . import interfaces @@ -53,7 +53,7 @@ def from_value(cls, value): class Report: - def __init__(self, message: str, code: ReportCode, level: LogLevel, timestamp: datetime.datetime = None): + def __init__(self, message: str, code: ReportCode, level: LogLevel, timestamp: datetime.datetime | None = None): self.timestamp = datetime.datetime.now() if timestamp is not None: self.timestamp = timestamp @@ -71,7 +71,7 @@ def to_dict(self) -> Dict: class ReportContainer(interfaces.ReportContainer): - def __init__(self, logs: List[Report] = None): + def __init__(self, logs: List[Report] | None = None): if logs is not None and isinstance(logs, List): self.logs = logs else: diff --git a/pandapower/converter/cim/pp_classes.py b/pandapower/converter/cim/pp_classes.py index 6b6af7ea07..7406f24dd2 100644 --- a/pandapower/converter/cim/pp_classes.py +++ b/pandapower/converter/cim/pp_classes.py @@ -1,5 +1,3 @@ -# -*- 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. import logging @@ -18,12 +16,12 @@ class PandapowerDiagnostic: :param diagnostic_: The pandapower diagnostic. If None a pp.diagnostic(net) will be run. Optional, default: None. :return: The pandapower diagnostic with CIM IDs. """ - def __init__(self, net: pandapowerNet, diagnostic_: Dict = None): + def __init__(self, net: pandapowerNet, diagnostic_: Dict | None = None): self.logger = logging.getLogger(self.__class__.__name__) self.net = net self.diagnostic = diagnostic_ - def _rec_replace_pp_diagnostic_with_cim_ids(self, input_obj, element_type: str = None): + def _rec_replace_pp_diagnostic_with_cim_ids(self, input_obj, element_type: str | None = None): sc = cim_tools.get_pp_net_special_columns_dict() element_mapping = { 'bus': 'bus', 'buses': 'bus', 'load': 'load', 'loads': 'load', 'sgen': 'sgen', 'sgens': 'sgen', @@ -37,7 +35,7 @@ def _rec_replace_pp_diagnostic_with_cim_ids(self, input_obj, element_type: str = 'impedance': 'impedance', 'impedances': 'impedance', 'dcline': 'dcline', 'dclines': 'dcline', 'ward': 'ward', 'wards': 'ward', 'xward': 'xward', 'xwards': 'xward'} if isinstance(input_obj, list): - return_obj = [] + return_obj: list = [] for one_input_obj in input_obj: if isinstance(one_input_obj, list) or isinstance(one_input_obj, dict): return_obj.append(self._rec_replace_pp_diagnostic_with_cim_ids(one_input_obj, element_type)) @@ -54,7 +52,7 @@ def _rec_replace_pp_diagnostic_with_cim_ids(self, input_obj, element_type: str = # default return_obj.append(one_input_obj) elif isinstance(input_obj, dict): - return_obj = {} + return_obj = {} # type: ignore[assignment] for key, item in input_obj.items(): if isinstance(item, list) or isinstance(item, dict) and key in element_mapping: element_type = element_mapping[key] diff --git a/pandapower/converter/cim/pp_tools.py b/pandapower/converter/cim/pp_tools.py index ab2978540c..9a26b951e1 100644 --- a/pandapower/converter/cim/pp_tools.py +++ b/pandapower/converter/cim/pp_tools.py @@ -1,5 +1,3 @@ -# -*- 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. import logging @@ -19,13 +17,13 @@ def _set_column_to_type(input_df: pd.DataFrame, column: str, data_type: str) -> None: try: - input_df[column] = input_df[column].astype(data_type) + input_df[column] = input_df[column].astype(data_type) # type: ignore[call-overload] except Exception as e: logger.error("Couldn't set data type %s for column %s!" % (data_type, column)) logger.exception(e) -def set_pp_col_types(net: Union[pandapowerNet, Dict], ignore_errors: bool = False) -> pandapowerNet: +def set_pp_col_types(net: pandapowerNet, ignore_errors: bool = False) -> pandapowerNet: """ Set the data types for some columns from pandapower assets. This mainly effects bus columns (to int, e.g. sgen.bus or line.from_bus) and in_service and other boolean columns (to bool, e.g. line.in_service or gen.slack). @@ -82,7 +80,7 @@ def set_pp_col_types(net: Union[pandapowerNet, Dict], ignore_errors: bool = Fals return net -def add_slack_and_lines_to_boundary_nodes(net: pandapowerNet, voltage_levels: List[int] = None): +def add_slack_and_lines_to_boundary_nodes(net: pandapowerNet, voltage_levels: List[int] | None = None): """ Add lines with low impedance and a slack to the boundary nodes with the highest voltage. :param net: The pandapower network @@ -109,8 +107,14 @@ def add_slack_and_lines_to_boundary_nodes(net: pandapowerNet, voltage_levels: Li name='virtual slack at voltage level ' + str(one_voltage_level)) logger.info("Added slack at bus ID: %s" % new_bus_id) new_bus_id_array = [new_bus_id for _ in busses_t.index.values] - create_lines(net, from_buses=busses_t.index.values, to_buses=new_bus_id_array, std_type='low_impedance_line', - name='virtual line to slack node with voltage level ' + str(one_voltage_level), length_km=1) + create_lines( + net, + from_buses=busses_t.index.to_list(), + to_buses=new_bus_id_array, + std_type="low_impedance_line", + name="virtual line to slack node with voltage level " + str(one_voltage_level), + length_km=1, + ) logger.info("Created %s low impedance lines." % len(busses_t.index.values)) del busses_t diff --git a/pandapower/converter/jao/from_jao.py b/pandapower/converter/jao/from_jao.py index 81813abdeb..853c2338f6 100644 --- a/pandapower/converter/jao/from_jao.py +++ b/pandapower/converter/jao/from_jao.py @@ -1,16 +1,16 @@ -# -*- coding: utf-8 -*-nt - # Copyright (c) 2016-2026 by University of Kassel and Fraunhofer Institute for Energy Economics # and Energy System Technology (IEE), Kassel. All rights reserved. -from copy import deepcopy import os +from copy import deepcopy import json from functools import reduce -from typing import Optional, Union +from typing import Optional, Union, Iterable + import numpy as np import pandas as pd from pandas.api.types import is_integer_dtype, is_object_dtype + from pandapower.network import pandapowerNet from pandapower.create import ( create_buses, create_lines_from_parameters, create_transformers_from_parameters @@ -109,7 +109,7 @@ def from_jao(excel_file_path: str, # --- manipulate data / data corrections if apply_data_correction: - html_str = _data_correction(data, html_str, max_i_ka_fillna) + html_str = _data_correction(data, html_str, max_i_ka_fillna) # type: ignore[assignment] # --- parse html_str to line_geo_data line_geo_data = None @@ -180,7 +180,7 @@ def _data_correction( cols.loc[cols[0].str.startswith("Unnamed:").astype(bool), 0] = None cols.loc[cols[1] == "Length_(km)", 0] = "Electrical Parameters" # might be wrong in # Tielines otherwise - data[key].columns = pd.MultiIndex.from_arrays(cols.values.T) + data[key].columns = pd.MultiIndex.from_arrays(cols.values.T.tolist()) # --- correct comma separation and cast to floats data[key][("Maximum Current Imax (A)", "Fixed")] = \ @@ -197,7 +197,7 @@ def _data_correction( ("Substation_2", "Full_name")]: data[key].loc[:, loc_name] = data[key].loc[:, loc_name].str.strip().apply( _multi_str_repl, repl=rename_locnames) - html_str = _multi_str_repl(html_str, rename_locnames) + html_str = _multi_str_repl(html_str, rename_locnames) # type: ignore[arg-type] # --- Transformer data -------------------------------- key = "Transformers" @@ -221,7 +221,7 @@ def _data_correction( data[key].loc[~nonnull, ("Phase Shifting Properties", "Taps used for RAO")] = "0;0" # --- phase shifter with double info - cols = ["Phase Regulation δu (%)", "Angle Regulation δu (%)"] + cols = ["Phase Regulation δu (%)", "Angle Regulation δu (%)"] # type: ignore[assignment] for col in cols: if is_object_dtype(data[key].loc[:, ("Phase Shifting Properties", col)]): tr_double = data[key].index[data[key].loc[:, ( @@ -249,8 +249,10 @@ def _filter_name(st: str) -> str: name_end = "" pos0 = st.find(name_start) + len(name_start) pos1 = st.find(name_end, pos0) - assert pos0 >= 0 - assert pos1 >= len(name_start) + if pos0 < 0: + raise AssertionError('pos0 < 0') + if pos1 < len(name_start): + raise AssertionError('pos1 < len(name_start)') return st[pos0:pos1] json_start_str = '