feat: pandapower comparison tests + sensitivity analysis (stacked on PR #13) - #1
feat: pandapower comparison tests + sensitivity analysis (stacked on PR #13)#1harrysalmon wants to merge 16 commits into
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.
feat: pandapower interop — IEC 60909 short-circuit + AC validation of the optimiser
|
the vfinel#13 has been merged, as suggested, the base of this PR can flip to |
|
I reviewed the change, it is all good to me. Can't wait to see whatever comes next, the research documentation is teasing ! The merge can be done when you're ready. Thanks for your work ! |
|
Also, be aware that the concept of "Y" and "R" phases, that was used to distinguish the "yellow" and "red" sub-grids, is going to be removed as it is not relevant anymore. I did the deletion in the |
vfinel
left a comment
There was a problem hiding this comment.
I reviewed the change, it is all good to me. Can't wait to see whatever comes next, the research documentation is teasing ! The merge can be done when you're ready. Thanks for your work !
NB: note that the concept of "Y" and "R" phases, that was used to distinguish the "yellow" and "red" sub-grids, is going to be removed as it is not relevant anymore. I did the deletion in the qgis_plugin branch, but that wasn't merged yet. So you could already remove the mention of those concepts in the documentation if you feel like it.
|
Superseded by the parent-repo PR against develop: vfinel#14 |
Stacked on top of PR #13
This PR sits on top of `feat/pandapower-interop` (the base PR going into vfinel:develop). Review vfinel#13 first — its conversion layer + AC validator are what these tests / docs build on. Once vfinel#13 lands, the base of this PR can flip to `develop`.
Summary
Eight commits adding comparison tests, topology coverage, sensitivity analysis, and a refactor of `RHO_COPPER` into traceable components.
```
4ea2057 refactor(constants): decompose RHO_COPPER into traceable components
5bc2729 docs(research): sensitivity to pp_interop config assumptions
b6b9d39 chore(scripts): sensitivity sweep over high-impact pp_interop config
347172d test(pp_interop): topology comparison tests for three festival shapes
d6cd989 docs(test): explain what each pp_interop test is comparing and why
beb3393 docs(research): why tree walk and pandapower AC flow disagree
488f99b test(pp_interop): side-by-side comparison vs analyze on each fixture
f160e1f test(pp_interop): parity with analyze on the canonical voltage-drop fixture
```
What's in here
Tests (7 new tests, 0 modified, 51/51 pass total)
Scripts
Docs (research/pandapower/)
Refactor
`src/nopywer/constants.py` — `RHO_COPPER` now decomposed into:
Each component independently overridable. The "Rich's notes" provenance is preserved in the comment as the historical origin.
Test plan
Why draft
Sitting in draft until PR #13 is reviewed. Once vfinel#13 lands, this PR's base will flip to develop and it can come out of draft.
🤖 Generated with Claude Code