feat: pandapower comparison tests and sensitivity analysis - #14
Merged
harrysalmon merged 16 commits intoMay 15, 2026
Merged
Conversation
…ixture Mirrors test_compute_voltage_drop_uses_phase_voltage_reference (test_analyze.py) but routes the same hand-crafted fixture through pandapower's runpp and asserts both produce 220 V / 4.35 %. Same fixture: 26 m of 1 mm² cable, single-phase 10 A. nopywer's RHO_COPPER = 1/26 makes R = 1.0 Ω exactly, so ΔV = R · I = 10 V. Key insight: nopywer's tree walk computes I = P / V0 / PF once at nominal voltage and uses that current everywhere — that's exactly pandapower's `const_i_p_percent=100` constant-current load model. Under matched load assumptions (and PF=1, no cable X) the two tools land on the same answer to within 0.05 V / 0.02 % vdrop. This is the conversion-correctness test. Any future divergence on real fixtures (test_pp_interop_comparison.py) is then known to come from physics nopywer simplifies away — load feedback, reactive power, line reactance — not from a translation bug.
Pin the tree-walk-vs-AC divergence on every existing analyze fixture so future changes to either side are visible. Three fixtures cover the three "shapes" of disagreement: 1. analyze_named_generator (small balanced load) — both tools agree to within rounding (<0.1 V / 0.05 %). The conversion is faithful in the regime where physics simplifications don't bite. 2. analyze_input (unbalanced single-phase loads) — nopywer's `max(current_per_phase)` rule pushes the tree walk's reported drop ABOVE pandapower's balanced runpp. At load_b: tree walk says 8.13 %, AC says 3.12 % — tree walk overstates by ~5 pp because it's conservative-by-design, while our converter currently collapses single-phase loads to balanced (runpp_3ph is opportunity §3 in 06_extended_opportunities.md). 3. input_nodes (the 2025 event, optimised) — constant-power feedback at low local voltage means more current, more drop. Tree walk computes I at nominal V0 once and never iterates, so it UNDERSTATES drop by ~12 pp at the worst node. This is the headline finding from 07_optimiser_validation_findings.md, captured here as a regression check. The two effects are physically real and pull in opposite directions. Tests assert direction-of-disagreement plus pinned numbers to ±2 pp, so any improvement to either tool's modelling will require a conscious update to these expectations.
Power-systems theory walkthrough of the divergence captured by tests/test_pp_interop_comparison.py. Covers: - The 230 V P-N vs 400 V L-L convention (and where it bites in interop) - The ZIP load model (constant-Z, constant-I, constant-P) - Why nopywer's tree walk is implicitly a constant-current solver - Why pandapower's runpp is constant-power and iterates to equilibrium Then the three mechanisms that make the two tools disagree, in detail: 1. Small balanced load → both shortcuts inactive → tools agree to rounding (the conversion-correctness check). 2. Unbalanced single-phase → nopywer's max(I_per_phase) rule over-states drop while pandapower's balanced collapse under-states it. Neither is right; truth needs runpp_3ph. 3. High-stress balanced → nopywer's constant-I shortcut means it misses the V-drops-→-more-current-→-more-drops feedback that AC iterates to equilibrium. Tree walk under-states by ~12 pp. Closes the loop on the comparison tests by explaining WHY each direction shows up.
Adds module-level docs and per-test docstrings across the three
pp_interop test files. Each test now spells out:
- "What this is comparing" — the two sides being lined up
- "What this is testing" — which property is being asserted
- "Real-world scenario" — the festival situation the test models
The three files now read as a deliberate three-layer story:
test_pp_interop.py
Smoke / conversion / API tests on a trivial 1-cable grid.
Festival mapping: any layout would touch this code, so any
silent unit/convention bug would show here first.
test_pp_interop_parity.py
Mirrors the canonical analyze unit test through pandapower
with matched assumptions (constant-current load, no X). Proves
the conversion is faithful — any divergence elsewhere is
physics, not a bug.
test_pp_interop_comparison.py
Three tests, three festival scenarios, three "shapes" of
tree-walk-vs-AC disagreement:
1. small distro near the generator → both tools agree
2. unbalanced single-phase distro → nopywer over-states
3. long radial trunk to heavy load → nopywer under-states
Tests unchanged; assertions identical. Pure documentation.
Adds Group A from the coverage gap analysis: synthetic fixtures that isolate the effect of grid SHAPE on the tree-walk-vs-AC gap, with load type and config defaults held constant. Three tests, three layouts a planner regularly sketches: - Deep linear chain (gen→A→B→C→D, each tapping 5 kW through a 32 A cable). Tests constant-power feedback compounding with depth: the gap grows monotonically (A=+0.02, B=+0.07, C=+0.10, D=+0.12 pp). Models a back-of-house corridor: kitchen → dressing → green room → workshop, each distro tapping its load. - Wide star (gen → 5 parallel branches × 5 kW each). Tests branch independence: with no chaining there's no feedback compounding. All branches identical (tw 1.78 % / ac 1.65 % / gap -0.13 pp at every leaf). Models a central distro hub feeding stages radially — the case where the planner can trust the tree walk. - Bottleneck (thin 16 A trunk → distro → three 32 A spurs). Tests drop attribution: even when magnitudes diverge, both tools should agree the trunk dominates. They do — trunk carries 96 % of the total drop to any leaf, in both views. Models the case where a planner reuses an existing thin cable run and fans out from there. Each test pins direction-of-disagreement plus magnitude bounds (±0.5 pp on the gap, >75 % attribution share). Synthetic fixtures are built in code via two small helpers (_node, _cable) that populate `power_per_phase` the way io.load_geojson would. Module docstring frames all three as "sensible planner shapes" and links to research/pandapower/08 for the underlying physics.
Adds scripts/sensitivity_sweep.py — three tight sweeps around the
defaults in pp_interop/config.py, each printing a table showing how
the AC vs tree-walk gap responds.
Findings on the deep-chain fixture (4 cables × 50 m, 5 kW × 4 loads):
X_OHM_PER_KM 0.05 → 0.12 (default 0.08)
AC drop at d: 11.312 % → 11.373 % (span 0.06 pp)
essentially negligible at festival scales — the 0.08
default is fine even if individual cables differ ±50 %.
PF 0.80 → 1.00 (default 0.9)
AC drop at d: 11.543 % → 11.151 % (span 0.39 pp)
gap (AC − tree): +0.323 → −0.069 pp
crosses zero around PF ≈ 0.97. Real festival kitchens at
PF ≈ 0.7 would push the gap further open; this is the
knob worth varying per-load if/when load_type lands.
GEN_XDSS_PU 0.08 → 0.18 (default 0.12)
Isc at gen: 1.811 kA → 0.805 kA (2.2× range)
biggest lever overall. Choice of generator impedance
materially affects breaker breaking-capacity sizing.
Run with:
uv run python scripts/sensitivity_sweep.py
Output is reproducible (no randomness). Not a test because the
useful artifact is the printed table, not a pass/fail; pinning
specific numbers as test assertions would obscure the gradient
we actually care about.
Captures the findings from running scripts/sensitivity_sweep.py: X_OHM_PER_KM 0.05 → 0.12 → AC drop moves 0.06 pp (negligible) PF 0.80 → 1.00 → AC drop moves 0.39 pp (gap flips at PF≈0.97) GEN_XDSS_PU 0.08 → 0.18 → Isc moves 2.2× (1.81 → 0.81 kA) Verdict per knob: X is fine to leave at 0.08 (festival cables are short and resistive enough that the X·sin φ term barely contributes). PF is a modest lever and the most useful per-load knob if/when load_type lands. GEN_XDSS_PU is the one to override per-event — ignoring the actual hired generator's X''d can mis-size breakers by a factor of 2. Cross-cutting: at festival scales the AC vs tree-walk gap is dominated by cable R and topology, not by these per-cable / per-load constants. RHO_COPPER deliberately not swept because both tools share it (gap invariant; absolute numbers would scale). Reproducible: re-run scripts/sensitivity_sweep.py after any change to pp_interop/config.py defaults to refresh the numbers in this doc.
The historical RHO_COPPER = 1/26 ("Rich's notes") bundled together
three distinct effects that were previously opaque. Split them out
without changing the numerical default:
RHO_COPPER = RHO_COPPER_20C × k_temp(COPPER_TEMP_C) × FIELD_MARGIN
≈ (1/58) × 1.197 × 1.864
≈ 0.0385 ≈ 1/26
Each component is now independently overridable:
- RHO_COPPER_20C = 1/58 — annealed copper at 20 °C (IACS standard).
Physics; not really tunable.
- ALPHA_COPPER = 0.00393 — temperature coefficient at 20 °C.
- COPPER_TEMP_C = 70 — assumed steady-state conductor T under
festival load. Lower for cool events; raise for coiled-drum-
in-the-sun scenarios.
- FIELD_MARGIN = 1.864 — empirical catch-all for connector contact
resistance, bundling, ageing, etc. Calibrated to reproduce the
legacy 1/26 figure at the default temperature.
The product at the defaults reproduces 1/26 to within 0.02 %, well
below modelling noise. All 51 tests pass.
One test (`test_to_pandapower_topology`) was hardcoding `1000/26` for
the expected r_ohm_per_km; updated to use `RHO_COPPER` directly so
it tracks the actual constant rather than the legacy literal.
The "Rich's notes" provenance is preserved in the comment, framed
as the historical origin of the figure rather than its current
definition.
Documents what would be needed to move from balanced runpp to
asymmetric runpp_3ph — the right fix for mechanism 2 (the unbalanced
single-phase divergence captured in 08).
Three categories of input pandapower needs:
1. Per-load phase assignment (already in PowerNode.phase, just
not always populated in fixtures)
2. Per-cable zero-sequence params (r0, x0, c0 — engineering
defaults of r0/r1 ≈ x0/x1 ≈ 4 for 4-core LV flex)
3. Source vector group + zero-sequence source impedance
(most festival diesel gen-sets are TN-S star-grounded, Yn)
Probed the existing fixtures for what's already there:
- input_nodes.geojson (2025 production fixture, 51 nodes):
every load has phase=None. Modelled balanced not because the
festival was balanced but because the design doc was created
before phase assignment was decided.
- analyze_input.geojson (small synthetic): has phase=1, phase=2
on its two loads. Could be modelled three-phase today.
Three strategies for filling in the missing 2025-fixture phase
data: historical reconstruction (most realistic, requires field
records), round-robin synthesis (cheap and reproducible — the
recommended default), or all-on-L1 worst-case stress.
Implementation cost estimate: ~5 hours total across data,
config, code, tests, and findings doc. Would land as
feat/pandapower-runpp-3ph stacked on this PR.
Updates README index to include doc 10.
Removes both kinds of time guesses across the research docs: - Implementation-effort estimates (hours/minutes per layer in doc 10's "Estimated cost" table, plus the "~5 hours total" summary). The per-layer LINE counts are kept since those are scope indicators, not effort guesses. Table renamed "Scope summary". - Reading-time estimates (per-doc and per-file minutes in REVIEWING.md, plus the "~25 minutes / ~15 more" preamble). The README's reference to doc 10's "5-hour cost estimate" is updated to "per-layer scope breakdown" to match. Effort guesses are usually wrong and tend to be quoted as if they were measurements; dropping them avoids anchoring future implementers to a number that has no basis.
Closed
3 tasks
Collaborator
Author
|
Reviewed in Original review: harrysalmon#1 (review) Merging |
Owner
|
awesome. i'll proceed to delete the branch. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Now based on
developThis PR was originally stacked on top of PR #13. PR #13 has now landed, this branch has merged
origin/develop, and this parent-repo PR targetsdevelopdirectly.Supersedes the fork-local PR: harrysalmon#1
Summary
Eight original commits adding comparison tests, topology coverage, sensitivity analysis, and a refactor of
RHO_COPPERinto traceable components.Since the original PR review, this branch also:
origin/developafter PR feat: pandapower interop — IEC 60909 short-circuit + AC validation of the optimiser #13 landeddevelopexecution-time reporting changes fromscripts/validate_optimiser_with_runpp.py/pp_interop/_powerflow.pyWhat's in here
Tests (7 new tests, 0 modified, 51/51 pass total)
test_pp_interop_parity.py— mirror Vincent's canonicaltest_compute_voltage_drop_uses_phase_voltage_referencethrough pandapower with matched assumptions (constant-current load). Proves the conversion is faithful.test_pp_interop_comparison.py— three side-by-side comparisons on the existing analyze fixtures. Captures the three shapes of disagreement: low-drop balanced (parity), unbalanced single-phase (nopywer over-states), high-drop balanced (nopywer under-states).test_pp_interop_topology.py— three synthetic festival layouts (deep chain, wide star, bottleneck) isolating the effect of grid shape on the gap.Scripts
scripts/sensitivity_sweep.py— tight sweeps around the high-impactpp_interop/config.pydefaults (X_OHM_PER_KM,PF,GEN_XDSS_PU). Prints tables. Findings:Docs (
research/pandapower/)08_why_tree_walk_and_ac_disagree.md— power-systems theory walkthrough (V_PN vs V_LL conventions, ZIP load model, why the gap runs in opposite directions on different fixtures).09_sensitivity_to_config_assumptions.md— sweep findings with per-knob takeaways and verdicts.Refactor
src/nopywer/constants.py—RHO_COPPERnow decomposed into:Each component independently overridable. The "Rich's notes" provenance is preserved in the comment as the historical origin.
Carried-over review context
From the superseded fork-local PR:
develop. Done here: this PR targetsvfinel/nopywer:develop.qgis_plugin. Addressed here by removing those concepts from the pandapower research docs and config docstring.Original review: harrysalmon#1 (review)
Test plan
uv run ruff check .uv run pytest— 51/51 passuv run python scripts/sensitivity_sweep.py— three tables print, deterministicuv run python scripts/validate_optimiser_with_runpp.py— still works against the 2025 fixture (numbers match feat: pandapower interop — IEC 60909 short-circuit + AC validation of the optimiser #13's findings)