Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 24 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,25 @@ license = "GPL-3.0-or-later"
dependencies = [
"fastapi>=0.135",
"networkx>=3.6",
"numpy>=2.4",
"numpy>=1.26",
"odfpy>=1.4",
"pandas>=3.0",
"pandas>=2.3",
"pyproj>=3.7",
"typer>=0.24",
"uvicorn>=0.41",
]

[project.optional-dependencies]
pandapower = [
# Pinned to the merge commit of e2nIEE/pandapower#2860 ("Prepare for
# Pandas 3.0") which lands the CoW / read-only fix needed by `runpp`
# under pandas >= 2.3. Released 3.4.0 (2026-02-09) is one day too old.
# Switch back to a PyPI version constraint once 3.4.1 ships.
# Background: https://github.com/e2nIEE/pandapower/pull/2860
"pandapower @ git+https://github.com/e2nIEE/pandapower.git@472d8294be2ad56fdb62e439046e802609478bd1",
]
modern = ["pandas>=3.0", "numpy>=2.4"]

[dependency-groups]
dev = [
"httpx",
Expand All @@ -31,6 +42,17 @@ nopywer-server = "nopywer.api:run"
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.uv]
conflicts = [
[
{ extra = "modern" },
{ extra = "pandapower" },
],
]

[tool.hatch.metadata]
allow-direct-references = true

[tool.hatch.build]
include = ["src/nopywer/frontend/**/*"]

Expand Down
117 changes: 117 additions & 0 deletions research/pandapower/01_pandapower_overview.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# pandapower at a glance

## What it is

Pandapower is a Python toolkit for static and quasi-static analysis and
optimization of balanced (and, optionally, three-phase asymmetric) power
systems. It was first published in 2017 (Thurner et al., IEEE Transactions on
Power Systems) and is maintained by the Fraunhofer IEE / e2nIEE group on
GitHub. It is BSD-licensed.

The library positions itself as the bridge between commercial tools like
DIgSILENT PowerFactory and the open-source MATPOWER / PYPOWER stack, with
its component models validated against commercial software.

## Core abstractions

A pandapower network (`pandapowerNet`) is a collection of pandas DataFrames,
one per element type:

| DataFrame | Represents |
|---|---|
| `net.bus` | Buses (nodes), with nominal voltage `vn_kv` (line-to-line for 3-phase) |
| `net.line` | Distribution / transmission lines and cables |
| `net.trafo`, `net.trafo3w` | Two- and three-winding transformers |
| `net.ext_grid` | Slack / external grid connection (sets reference voltage and angle) |
| `net.gen`, `net.sgen` | Synchronous and static (PV / PQ) generators |
| `net.load`, `net.asymmetric_load` | Symmetric and per-phase loads |
| `net.switch` | Switches between buses or on lines |
| `net.shunt`, `net.impedance`, `net.ward`, `net.xward` | Equivalent impedances |

Results from a power flow are written into parallel `net.res_*` DataFrames
(`res_bus`, `res_line`, `res_trafo`, …).

## Calculations supported

| Function | Purpose |
|---|---|
| `runpp(net)` | Balanced AC power flow. Newton-Raphson by default; PYPOWER, lightsim2grid, or PowerGridModel solvers also pluggable. |
| `runpp_3ph(net)` | Asymmetric three-phase power flow using the sequence-frame method (NR for positive sequence, current injection for zero/negative). Requires zero-sequence line parameters. |
| `rundcpp(net)` | DC approximation. |
| `runopp(net)` / `rundcopp(net)` | AC / DC optimal power flow via PYPOWER or PowerModels.jl. |
| `estimate(net)` | Weighted-least-squares state estimation with bad-data detection. |
| `pp.shortcircuit.calc_sc(net)` | IEC 60909 short-circuit calculation, including converter-fed networks. |
| `pp.topology.create_nxgraph(net)` | Convert the net to a NetworkX (Multi)Graph for connectivity / radiality / cycle / component searches. |

## Line / cable model

`pandapower.create_line_from_parameters(net, from_bus, to_bus, length_km,
r_ohm_per_km, x_ohm_per_km, c_nf_per_km, max_i_ka, …)`

Required electrical parameters per line:

- `r_ohm_per_km` — resistance.
- `x_ohm_per_km` — reactance.
- `c_nf_per_km` — line-to-earth capacitance.
- `max_i_ka` — thermal current limit, used for loading-percent reporting.
- For 3-phase asymmetric flow: zero-sequence equivalents `r0_ohm_per_km`,
`x0_ohm_per_km`, `c0_nf_per_km`.

Constraints worth knowing:

- `length_km` must be > 0 (zero leads to division-by-zero in the solver).
- Very low impedance can cause Newton-Raphson convergence problems — pandapower
recommends bridging via `net.switch` or `net.impedance` instead of an
artificially short line.
- Pandapower ships a **standard-type library** (e.g. the `NAYY 4×50 SE` LV
cable) that auto-fills all parameters from a name string.

## Voltage convention

- Bus voltage `vn_kv` is **line-to-line** for three-phase systems
(European LV → 0.4 kV, not 0.23 kV).
- Per-unit results in `res_bus.vm_pu` are referenced to the bus's `vn_kv`.
- For runpp_3ph, results are reported per phase (a, b, c).

## Three-phase asymmetric flow specifics

`runpp_3ph` is the relevant entry point for any unbalanced LV analysis.
Constraints to be aware of:

- Only earthed transformer vector groups are supported: Dyn, Yyn, YNyn, Yzn.
- Loads need to live in `net.asymmetric_load` (with `p_a_mw`, `p_b_mw`,
`p_c_mw`, etc.) for true per-phase modelling.
- Voltage-dependent loads, Q-limit enforcement, and temperature-dependent
resistance are **not** confirmed-tested in the 3-phase path.
- Lines need their zero-sequence parameters populated.

## Dependencies

Core: `numpy`, `scipy`, `pandas`, `networkx`, `packaging`, `tqdm`, `deepdiff`.
Optional: `numba` (JIT, big speedup for `runpp`), `lightsim2grid`,
`PowerGridModel`, `PowerModels.jl` (Julia, for OPF / advanced features),
`matplotlib` / `plotly` (plotting).

Installation footprint with `pip install pandapower[all]` is non-trivial
(numba pulls in LLVM). The minimum core is moderate.

## Version 3.4.0 highlights (Feb 2026)

- `enforce_p_lims` argument on `runpp` for `gen` / `sgen` active-power limits.
- New toolbox helper `get_all_elements()` returning every element table as a
combined DataFrame.
- Renamed `tap_dependent_impedance` → `tap_dependency_table` for transformers.
- Build moved from `setup.py` to `pyproject.toml`.

## What pandapower is *not*

- Not a transient / dynamic simulator. For RMS / EMT, use packages like
`andes`, `dynawo`, `assimulo`, or commercial tools.
- Not a cable-routing / layout / planning tool — it has no concept of
geographic distance, cable-tier cost, plug standards, or inventory.
- Not designed around radial-only networks; it can solve them, but it
doesn't know that "radial" is a special case worth optimising for.
- Not a North American distribution tool — pandapower's three-phase model
assumes European-style symmetric connections and cannot represent the
asymmetric feeder-with-neutral patterns used in NA distribution. (This is
not a constraint nopywer cares about.)
130 changes: 130 additions & 0 deletions research/pandapower/02_nopywer_power_calculations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
# What nopywer's power calculations actually do

Read of `src/nopywer/analyze.py`, `models.py`, `optimize.py`, and `constants.py`
as of commit on branch `develop` (April 2026).

## Domain

Festival / event temporary power distribution. European LV (230 V phase-to-neutral,
50 Hz implicit), single generator, radial cable layout, IEC plug-and-socket
form factors (16 A / 32 A / 63 A / 125 A).

## Inputs

A GeoJSON FeatureCollection containing:

- **Point features** → loads or the generator. Properties: `name`, `power`
(watts), `phase` (1, 2, 3, "U", "Y", or null). The generator is identified
by the substring `"generator"` in its name.
- **LineString features** → cables with the two coordinate endpoints.
Properties: `length` (m, optional — geodesic distance is used otherwise),
`area` (mm², default 2.5), `plugs&sockets` (A, default 16), `phase`.

There is **no** transformer, no shunt capacitance, no reactive component, no
external grid, no impedance element, no switch, and no concept of
line-to-line voltage. Everything is single-stage radial.

## Power flow algorithm (`analyze.py`)

The algorithm is not a Newton-Raphson AC power flow. It is a four-pass
tree walk:

1. **`_snap_cables_to_nodes`** — Snap any unattached cable endpoint to the
nearest node within `CONNECTION_THRESHOLD_M = 5 m`.
2. **`_build_tree`** — Recursively assign a parent / children / depth to each
node using a simple DFS rooted at the generator. Cycles raise an error.
3. **`_cumulate_current`** — Walk the tree from leaves back to the root,
accumulating `power_per_phase` (a length-3 numpy array) into each
ancestor. The cable to a node's parent gets
`current_per_phase = cum_power / V0 / PF`. There is **no Kirchhoff /
Newton iteration** — the answer is obtained in a single bottom-up sweep
because the graph is a tree.
4. **`_compute_voltage_drop`** — Walk the tree top-down. For each child cable:

```
cable.vdrop_volts = cable.resistance * max(cable.current_per_phase)
node.voltage = parent.voltage - cable.vdrop_volts
```

`cable.resistance` is `RHO_COPPER * length_m / area_mm2`.

`RHO_COPPER = 1/26 Ω·mm²/m`, deliberately ~2.2× the textbook copper
resistivity (1/58). The constant comes from "Rich's old notes" and is a
conservative field-margin factor.

## What this model captures

- Per-phase real-power balance through any tree topology.
- Single-phase loads pinned to one specific phase (1, 2, or 3) cause that
phase to carry more cumulative current upstream.
- Voltage drop is approximately correct for resistive cables with PF = 0.9.
- Phase imbalance is reported as `100 * std(cum_power_per_phase) / mean(...)`
at the generator.

## What this model elides

| Effect | Treated as |
|---|---|
| Cable reactance (X) | Zero. Festival cables have `X / R` in the 0.05–0.15 range, so this is a few-percent error in voltage drop magnitude. |
| Cable capacitance (C) | Zero. Acceptable at festival scales (km-class capacitance is irrelevant under 1 km of LV cable). |
| Reactive power (Q) | Power factor is folded into `PF = 0.9` once at conversion. There is no concept of `Q` per load. |
| Voltage-dependent load | All loads are constant-power. |
| Line-to-line vs line-to-neutral | The generator output is treated as 230 V phase-to-neutral always; there is no Δ vs Y wiring. |
| Neutral / earth current | Not modelled. Per-phase imbalance produces neutral current in reality; nopywer reports imbalance but does not size the neutral. |
| Mutual coupling between phases | Not modelled. |
| Loop topologies | Forbidden — `_assign_children` raises if a cycle is detected. |
| Multiple generators | Forbidden — `PowerGrid.__post_init__` raises if `len(generators) > 1`. |
| Solver convergence | N/A — the tree walk is direct, so there is nothing to converge. |

## Sizing logic (`models.py`)

Four hardcoded `Cable` subclasses:

| Class | Phases | Max current (A) | Cross-section (mm²) | Tier cost |
|---|---|---|---|---|
| `Cable16A` | 1 | 16 | 2.5 | 1.0 |
| `Cable32A` | 3 | 32 | 6.0 | 3.0 |
| `Cable63A` | 3 | 63 | 16.0 | 8.0 |
| `Cable125A` | 3 | 125 | 35.0 | 20.0 |

`pick_cable_for(power_watts)` picks the smallest tier such that
`power / (num_phases * V0 * PF) ≤ max_current_a`.

The thresholds (3 313 W, 19 873 W, 39 124 W) are pinned in
`tests/test_models.py`.

## Layout optimization (`optimize.py`)

Independent of `analyze.py`. Steps:

1. Build a complete distance graph between all node pairs (geodesic distance
via projection to EPSG:32630).
2. Compute the NetworkX MST → minimum-total-length tree.
3. Root by BFS at the generator.
4. **`_reduce_cable_cost`** — local-search rewires: for each non-root node,
try the 15 nearest neighbours, the generator, and a few ancestors as
candidate parents. Accept any rewire that drops total
`Σ length × pick_cable_for(cum_power).tier_cost` by ≥ 1 %.
5. Convert the final tree into typed cables and recompute power flow.

This is a domain-specific layout planner. There is no equivalent in
pandapower.

## Other supporting logic

- **`inventory.py`** — Match required cables / distros against an Excel
inventory using a greedy combinatorial search over available cable
lengths. Pure pandas + itertools.
- **`api.py`** — Single FastAPI endpoint `/api/v1/optimize` that takes a
nodes GeoJSON and returns the optimized layout as cables GeoJSON.
- **`io.py`** — GeoJSON ↔ `PowerGrid` round-trip plus a `print_grid_info`
reporter that prints per-depth tabular grid info to logs.

## Key numbers to keep in mind

- `V0 = 230 V` (phase-to-neutral)
- `PF = 0.9`
- `RHO_COPPER = 1/26 Ω·mm²/m` (conservative)
- `VDROP_THRESHOLD_PERCENT = 5 %` (NF C 15-100 / IEC 60364)
- `EXTRA_CABLE_LENGTH_M = 10 m` (slack added to every cable)
- `CONNECTION_THRESHOLD_M = 5 m` (snap distance)
Loading
Loading