Asymmetric three-phase modelling + per-load phase planning - #15
Conversation
io.load_geojson now accepts the plugin export schema (area_mm2, plugs_and_sockets_a, length_m) alongside the input schema, and handles list-valued phase to model multi-phase loads (e.g. phase=[1,2] splits power evenly across L1 and L2). Adds Martin's 2026 east-grid field export as a fixture plus a modified copy with resized trunk/branch cables, and research doc 11 walking through plugging the export into the pandapower code. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Documents the iterative cable-resize exercise on the modified fixture: worst-bus trace, the resize sweep matrix (config x load scale -> max vdrop), and the finding that convergence is a far lower bar than the 5% drop threshold. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New pp_interop/phases package synthesises a leg (L1/L2/L3) for single-phase loads on grids that ship without phase data — the prerequisite for asymmetric runpp_3ph work (research doc 10). Two strategies in separate modules: round-robin (positional, cyclic legs) and greedy (size-aware, heaviest load onto the lightest leg). Both share candidate selection, a fixed-leg seed, and the balance metric in _common. Strategies are pure; commit a plan via apply_assignment. Applies the greedy assignment to the modified fixture and regenerates its distro in/out fields after the earlier cable resizes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds Strategy D (greedy least-loaded leg) alongside the existing three, documents the shared usage-factor assumption, and records the B-vs-D comparison on the 2026 fixture. Headline finding: phase balancing and voltage-drop relief are decoupled — greedy cuts leg imbalance 9.5%->0.6% but the worst node's drop is unchanged. Marks the phase-synthesis work done (pp_interop/phases) and renumbers the future findings doc to 12 (doc 11 is now the field-export walkthrough). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
New _powerflow_3ph.py runs pandapower's unbalanced solve — the path that sees per-leg voltage drop and neutral-conductor current, neither of which a balanced runpp can show. to_pandapower_3ph augments the balanced net (zero-sequence line and source params, asymmetric loads swapped in for phased loads) rather than branching inside _conversion.py, leaving the balanced path untouched. compute_power_flow_3ph returns per-leg bus voltages, per-leg and neutral line currents, and the voltage unbalance factor. Zero-sequence and source-earthing defaults added to config.py. Updates doc 10 to mark the code layers done. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Every test now documents what it asserts and why it matters — the bug class it guards or the physics/contract it pins. Flags the two wiring-regression guards as such (not physics checks), and records why the single-phase test does not assert L2 == L3. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The physics behind doc 10: why a balanced solve can collapse three phases to one, symmetrical components, why the zero sequence is the neutral return, and a full walkthrough of why a single-phase load unbalances the two legs it does not touch — including a verification experiment showing the L2/L3 split has a precise physical on/off switch (Z0 = Z1) and is not a solver artefact. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
First run of the asymmetric solver on the 2026 modified fixture with both phase strategies and across load scales. Headlines: balanced runpp under-states the worst drop ~2.2x at 0.5x usage and *converges* at full load where runpp_3ph has no AC operating point; phase strategy buys convergence margin but does not bring drops near spec; one 2.5mm² cable carries 20A on its neutral under a plausible phase plan -- the cable-spec question doc 10 raised, with a real number against it for the first time. Updates doc 10 status and the README index. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ng assumptions - Require `usage_factor` (keyword-only, no default) on every public function in `pp_interop.phases` so the choice is always visible at the call site. - Move `DEFAULT_USAGE_FACTOR` from `phases/_common.py` to `pp_interop/config.py` (re-exported via `phases`) so all modelling tunables live in one place. - Expand `R0_OVER_R1` / `X0_OVER_X1` config comments to spell out the cable build (3P + full-N + full-PE) and TN-S earthing assumed by the 4.0 default, with an override matrix for other topologies. - Add a new "Picking R0/R1 and X0/X1 — what the ratios really mean" section to research doc 10.1 with the derivation, override table, and sources (IEC 60909-2, Roeper). - Document the "multi-phase loads self-balance evenly" assumption (e.g. phase=[1, 2] splits 50/50) in research docs 10 and 11, flagging when it would be wrong and where the fix would live. - Add a README at `src/nopywer/pp_interop/` covering the code flow, inputs, outputs, and runnable examples for the balanced and asymmetric solver paths. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…n docs
- Refactor `to_pandapower_3ph` to build an independent net via a
shared `_build_net_skeleton` helper, instead of mutating the
balanced converter's output. The two converters now produce
fully independent `pandapowerNet`s.
- Introduce `Pandapower3phGrid` as a distinct return type for the
asymmetric path, with separate `balanced_load_idx` and
`asymmetric_load_idx` maps so the dual-table layout is encoded
in the type rather than in a comment. `compute_power_flow_3ph`
now takes `Pandapower3phGrid` specifically.
- Add `phase_geojson` one-call wrapper for the common "load,
plan, apply, write" script/CLI workflow.
- Emit `PowerNode.phase` from `PowerNode.to_geojson` (omitted when
None) so phase plans round-trip through GeoJSON.
- Documentation pass on the pp_interop README and module docstrings:
- Add a "Two converters, two grid types" section explaining the
balanced/asymmetric split and what it means for callers.
- Fix factually-wrong README claims (3ph doesn't require phases
to be populated; not all config knobs are exposed as kwargs).
- Fix stale references in `phases/_common.py` module docstring
(usage factor now in config, strategy signature now requires
`usage_factor`, typo "fill that gap in for").
- Clarify `Pandapower3phGrid.source` doc, `_phase_split` `None`
branch is now defensive only, `_round_robin` references
`config.DEFAULT_USAGE_FACTOR` consistently with sibling.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three silent coercions in the data path now emit log records so the
operator has a paper trail when a fixture carries borderline data:
- `nopywer.io.load_geojson`:
- WARNING when a node's `phase` is a string (legacy "U"/"Y"
sub-grid markers) — coerced to balanced as before.
- DEBUG when a feature carries property keys not in the loader's
known set — catches typos (`powr` for `power`) without being
noisy on legitimate computed export fields.
- `nopywer.pp_interop._powerflow_3ph.to_pandapower_3ph`:
- WARNING when a string-marker phase appears; now also routes
such loads to the balanced `pp.load` table (previously sent
through `_phase_split` and stored as a balanced
`pp.asymmetric_load`, which was semantically misleading).
- `nopywer.pp_interop.phases._common._fixed_leg_seed`:
- WARNING when a string-marker phase appears in the leg seed.
Each warning is pinned by a focused test (`caplog`).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Older nopywer exports (pre-2026) write `cum_power` instead of the
current `cum_power_watts`. The 2026-05-14 martin fixture is in this
shape — reloading it triggered one DEBUG record per node ("unknown
key cum_power") which buried the actually-useful diagnostics.
Add `cum_power` to the known-feature-keys allowlist so a round-tripped
legacy export reloads silently.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add a Logging section listing the four log records the module emits (string-marker WARNINGS in io / 3ph converter / leg seed, unknown-key DEBUG in io) and the operator action for each. Cross-reference from the Per-load phase row in the Inputs table. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
`scripts/run_asymmetric.py` wraps the load → snap → (optional plan) → convert → solve → report pipeline so a reviewer can exercise the asymmetric path on any fixture without writing Python. Runs the balanced solve side-by-side so the "balanced says X, asymmetric says Y" gap documented in doc 12 is visible. Also adds a "What `phase = None` means at each layer" section to the pp_interop README, walking through every layer's handling of unphased loads — explicitly framing `None` as the modelling choice "balanced three-phase draw", not "missing data". Mirrors the expanded help text on the script's `--plan` flag. PR_DESCRIPTION.md drafts the message for the upstream review: what landed, why, how to try it, the four modelling assumptions that need a second look, and the research docs that motivate them. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…oad-factor The earlier interface had two knobs that were conceptually adjacent but did different things — `--usage-factor` affected only phase planning, never the solve. Reviewers reading the output had no way to know which numbers the solver actually saw. Confusing in practice and the cautious-engineering "plan for X, stress at Y" workflow that two knobs would enable is rare enough not to deserve CLI surface. The script now exposes **one** knob, `--load-factor`, applied to both phase planning AND the pre-solve scaling pass. Order of operations: load -> snap -> plan (uses unmodified nameplate × factor) -> apply -> scale (mutates power_watts *= factor) -> convert -> solve. A pre-flight summary block prints the nameplate total, the load factor, the effective total demand the solvers actually see, and the chosen plan strategy, so the reader knows exactly what is being modelled before any results scroll past. Failure-mode hints now say "try a lower --load-factor" so the relationship to convergence is explicit. The script's module docstring carries the full rationale (when to use 1.0 / 0.5 / 0.2, why one knob and not two). README and PR_DESCRIPTION reference the script and the one-knob design. Library-level `usage_factor` parameter is unchanged — it's still required at every `phases` call site. Only the CLI surface changes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Three small fixes from the final review pass: - 3ph load-count summary lines align numerically (right-justified 3-digit width). - `--load-factor` rejects <= 0 and > 5 with actionable error messages, catching operator typos before they reach the solver. - PR_DESCRIPTION uses --load-factor consistently and notes that the library DEFAULT_USAGE_FACTOR is the CLI default too. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…r_3ph `to_pandapower_3ph` was refactored earlier in the branch to build an independent net via the shared `_build_net_skeleton`, but two descriptive docstrings still described the old mutation-based shape: - research/pandapower/10's status-summary footer - tests/test_pp_interop_powerflow_3ph.py module docstring Updated both to match what the code now does. Also expanded PR description's reference to the README to call out the per-layer `phase = None` table explicitly — it's the clearest single piece of behavioural documentation a reviewer can read. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2d871d6 to
2f36b4f
Compare
|
hey ! First of all, thank you for the PR. It is a very promising move towards better analyses ! I tried to run the code as suggested, and have some remarks. However, the code is running fine (and the tests are passing) so i'll proceed with the PR. runtime warning: invalid value encountered in divideWhen running as suggested, i get the following warning: Maybe this is something worth looking at ? convergence issue when running example with phase planningwhen running the other example the power flow did not converged :
Maybe it would be worth to replace by a working example ? Reducing the |
Asymmetric three-phase modelling + per-load phase planning
What this PR adds
A pandapower-backed asymmetric three-phase power flow path for
festival grids, with the per-load phase planning needed to feed
it. Seven user-facing pieces:
io.load_geojsonnow accepts the round-tripped export schema(
area_mm2,length_m,plugs_and_sockets_a) alongside thehand-authored input schema, and accepts list-valued
phaseformulti-phase loads (e.g.
[1, 2]for a 2-leg load).io.load_geojsonpicks the rightCablesubclass from JSONcontent (dispatching on
plugs_and_sockets_a) instead ofinstantiating a base
Cablewith per-instance overrides. Cabletypes are a closed catalogue; the subclass IS the type. Unknown
ratings fall back to
Cable16Awith aWARNING.models.normalise_phase_legs— the single source-of-truthhelper for what each
PowerNode.phasevalue means. Used byio.load_geojson,pp_interop._powerflow_3ph(both_phase_splitandto_pandapower_3ph's routing) andpp_interop.phases._common._fixed_leg_seed. Non-canonical forms(legacy string markers, out-of-range ints, malformed lists) all
coerce to balanced with a
WARNINGand consistent routing.pp_interop/phases/— two phase-distribution strategies(greedy + round-robin) and a
phase_geojsonone-call wrapper forthe
load → plan → apply → writeworkflow.apply_assignmentrebuilds
node.power_per_phasefrom(power_watts, new_leg)sothe planner's mutation keeps both coupled fields coherent.
pp_interop/_powerflow_3ph.py—to_pandapower_3phandcompute_power_flow_3phforrunpp_3ph. Returns a distinctPandapower3phGridtype so the dualpp.load/pp.asymmetric_loadlayout is encoded in the type, not in a comment. Independent of
the balanced converter — no shared state, no mutation.
PowerNode.to_geojsonemitsphasewhen non-None, so phaseplans round-trip through the loader. Every polymorphic form
(int, list, legacy string, None) is covered by a round-trip test.
ioandpp_interop— legacy"U"/"Y"string phase markers, unknown-property-key typos, and impossible
wirings (
number_of_phasesdisagreeing with the picked subclass)surface as
WARNINGorDEBUGinstead of being silently coerced.Why
research/pandapower/10_three_phase_asymmetric_modelling.mdis themotivation: nopywer's tree walk and the balanced
runppagree on abalanced grid, but disagree exactly when balance matters (single-
phase loads piling onto one leg). The balanced solve averages the
imbalance away; the tree walk's
max(I_per_phase)over-statesdrop on lightly-loaded legs and under-states it on heavily-loaded
ones. Neither models the neutral conductor at all.
runpp_3phis the right fix. It surfaces two quantities a balancedsolve structurally cannot:
imbalanced grid are sized against the leg they actually sit on;
reduced-section-neutral cable (
3G6+½N) is safe or the run needsfull-section neutral (
4G6).Doc 12 (
12_runpp_3ph_findings.md) is the first set of numbers fromthis on the real 2026 east-grid fixture; the headline is that
balanced
runppis optimistic (~2.2× lower worst leg drop thanasymmetric at
--load-factor 0.5), and thatcable_15carries20.4 A on its neutral under the greedy phase plan, which would have
been invisible in any balanced analysis.
How a reviewer can try it
Phase a fixture (load → plan → apply → write, one call):
Run the asymmetric solve with a printable report:
Reads the fixture, snaps cables, optionally plans phases, scales
loads by
--load-factor, runs both the balanced and asymmetricsolves side-by-side, and prints the top worst-leg voltage drops and
worst-neutral cables.
The script has one user-facing knob —
--load-factor— thatapplies to both phase planning and the solver inputs. See its module
docstring for the full rationale; the short version is "the fraction
of nameplate this analysis is modelling" (
1.0= worst case;0.5= festival diversity;0.2= quiet period). The script'spre-flight summary always prints the nameplate total, the load
factor, and the effective demand the solvers actually saw, so
reviewers can never be confused about which numbers fed which
result.
Design approach — Cable types, phase representation, and parsing
The PR makes three opinionated design decisions worth surfacing for review. All three were aligned on during PR-review iteration; the rationale is repeated here so the diff is reviewable without scrolling the conversation.
1. Cable types are a closed catalogue; the subclass IS the type
Cable16A,Cable32A,Cable63A,Cable125Aare the only cable types nopywer knows about. A cable's phase count, plug-and-socket rating, conductor area, max current, and tier cost are type-level facts — they never vary per instance. There is no such thing as "a 16 A cable wired 3-phase"; that's a different physical cable, which is a different subclass.So:
num_phases,max_current_a,tier_cost) or as dataclass-field defaults that aren't normally overridden (plugs_and_sockets_a,area_mm2). The baseCableis abstract — instantiating it directly gives you the 16 A defaults, which is what some tests use for type-hinting collections.io.load_geojsonpicks the right subclass from JSON content via_pick_cable_class, dispatching onplugs_and_sockets_a. Unknown ratings fall back toCable16Awith aWARNING; an explicitnumber_of_phasesthat disagrees with the picked subclass also logs aWARNING(the subclass wins — the catalogue is authoritative).Cable.to_geojsonreadstype(self).num_phasesfor the exported phase count and thecable_typestring. The"number_of_phases"JSON key is preserved for round-trip compatibility, but the in-memory representation no longer carries a redundant instance attribute.analyze.pyandinventory.pyreadcable.num_phases(ClassVar accessible on the instance), so loaded cables and optimiser-built cables behave identically.An earlier version of this branch had introduced
number_of_phasesas an instance field on the baseCable, duplicating the subclassnum_phasesClassVar and quietly allowing impossible wirings. That has been removed; the subclass-as-type design is the source of truth.2.
PowerNode.phaseis polymorphic; one helper normalises it everywherePowerNode.phaseis the single hottest piece of polymorphism in the model. Every value it can take:phasevalueNone1,2,3[1, 2](any list of valid leg ints)"U","Y"WARNING.WARNING.Three sites need to interpret this field —
io.load_geojson,pp_interop._powerflow_3ph(both_phase_splitandto_pandapower_3ph's routing), andpp_interop.phases._common._fixed_leg_seed. Before this PR they each had their own copy of the rule and had already drifted: an out-of-range int likephase=5produced a balanced split everywhere but still got routed tonet.asymmetric_loadin the 3ph converter, mislabelling a balanced load as asymmetric.The PR extracts a single
normalise_phase_legs(phase, *, node_name) -> list[int]helper inmodels.py:[that int]; a list with at least one valid leg returns the list filtered to ints 1-3 (silent filter — operator intent is unambiguous);Nonereturns[].[](balanced) and emit aWARNINGthat names the node, the offending value, and the canonical alternatives. The warning fires once per call site per offending node — noisier than centralising at the loader only, but it means a programmer-introduced bad value is caught even if it never went through JSON.to_pandapower_3phdoeslegs = normalise_phase_legs(...);not legs→net.load(balanced table); non-empty →net.asymmetric_load. The cold-eyes-flagged "phase=5 ends up in the asymmetric table with three equal P's" path is structurally impossible now.apply_assignmentalso re-derivesnode.power_per_phasefrom(power_watts, new_phase)using the same rule, so the two coupled fields stay coherent after a planning run —analyze.py's tree walk readspower_per_phasedirectly and would otherwise walk stale balanced thirds for a load that just got assigned a single leg. Same fix is applied inscripts/run_asymmetric.py's_scale_loads_in_placefor the same reason.3. Parsing strategy — be liberal at read time, strict in the warning
io.load_geojsonaccepts both the hand-authored input schema and the round-tripped export schema (area_mm2↔area,length_m↔length,plugs_and_sockets_a↔plugs&sockets). The aliasing table in_EXPORT_KEY_ALIASESis the one place that mapping lives.The loader is intentionally permissive: legacy keys (
cum_powerinstead ofcum_power_watts), unknown property keys (typos, third-party metadata), and non-canonical phase forms all load — every coercion is logged at the right level (DEBUGfor unknown keys that are typically benign noise from exported computed fields;WARNINGfor legacy string phase markers and impossible wirings). The principle is "we shouldn't fail to load a fixture, but we shouldn't silently coerce either"; the warning channel is the operator's signal that a coercion happened and where to fix the source.Two things are not tolerated and raise loudly: cables with missing endpoints (caught by
_build_net_skeleton) and grids with no generator or more than one (caught byPowerGrid.__post_init__). Those are structural rather than data-coercion errors.What I want a closer look at
Four modelling assumptions live inline and are operator-tunable. All
documented in
pp_interop/config.pyandresearch/pandapower/, butworth flagging here:
R0_OVER_R1 = X0_OVER_X1 = 4.0— rule-of-thumb default for4-/5-core LV flex with full-section neutral, TN-S earthing
(genset star-point bond only). Every neutral-current number in
doc 12 moves linearly with this. Multi-point N-PE bonding would
drop it to ~2.5. See
config.pyandresearch/pandapower/10.1§"Picking
R0/R1andX0/X1— what the ratios really mean".SOURCE_X0X_MAX = 1.0,SOURCE_R0X0_MAX = 0.1— assumesTN-S genset with solidly-grounded star point (Yn). An
isolated-neutral (IT) genset would have no zero-sequence return
at all and these need overriding.
DEFAULT_USAGE_FACTOR = 0.5— single conservative festivaldiversity factor. Required at every
phasescall site (nofunction default — the choice is event-specific and we don't
want it disappearing into a default). Also the default for the
run_asymmetric.py--load-factorflag, so library and CLIagree on the project-wide reference value.
phase = [1, 2]splits power evenly across the listed legs —the multi-phase-loads-self-balance-evenly assumption. Documented
in
research/pandapower/10.mdunder "Modelling assumption —multi-phase loads self-balance evenly". Currently only matters
for
curious creaturesin the 2026 fixture; flagged for anyfuture genuinely-asymmetric multi-phase load.
Also worth a second pair of eyes:
tests/fixtures/2026-05-14_martin_modified.geojsonwas hand-edited to upsize four cables so the grid converges. The
resize sweep is in
research/pandapower/11; the original raw exportis preserved as
2026-05-14_martin.geojson.phase = Nonesemantics are explicit and important —Noneis the modelling choice "balanced three-phase draw", not "missing
data".
pp.loadunderrunpp_3phis the balanced-draw primitive,so unphased loads need no special handling and don't contribute to
neutral current. Full per-layer behaviour table in
src/nopywer/pp_interop/README.md§ "Whatphase = Nonemeans ateach layer".
Required reading for the technical reviewer
src/nopywer/pp_interop/README.md— API surface, runnable examples,the balanced/asymmetric type distinction (
PandapowerGridvsPandapower3phGrid— independent nets, no shared state), the"What
phase = Nonemeans at each layer" per-layer table, and thelogging surface.
research/pandapower/10_three_phase_asymmetric_modelling.md— whatthe work was and the shopping list it came from.
research/pandapower/10.1_three_phase_theory.md— physics of whysingle-phase loads unbalance the other two legs and how the
zero-sequence parameters get picked.
research/pandapower/11_field_export_fixture_walkthrough.md— thereal-fixture work that produced the modified fixture.
research/pandapower/12_runpp_3ph_findings.md— first numericalfindings from
runpp_3phon the modified fixture.CI coverage
CI now installs the
pandapowerextra (uv sync --frozen --extra pandapower) so thepp_interoptests actually execute. Previously they allpytest.importorskipped and silently passed, leaving the bulk of this branch's surface uncovered by the green checkmark.Test count
151 tests; all pass. Notable additions:
tests/test_models.py— extensive parametrised tests fornormalise_phase_legsacross every input form (canonical, out-of-range int, mixed list, malformed list, empty list, legacy strings,
bool), and a
test_cable_to_geojson_uses_subclass_phase_countthatpins the subclass-as-type design.
tests/test_pp_interop_phases.py— strategies, candidate selection,apply (with new
power_per_phasecoherence test), the round-tripthrough GeoJSON for every polymorphic phase form, the
phase_geojsonwrapper.tests/test_pp_interop_powerflow_3ph.py—_phase_splitparametrised across every
phaseform; per-physics tests forbalanced and single-phase loads under
runpp_3ph; new[1, 2]multi-phase integration test; no-shared-state test between balanced
and 3ph converters; non-tautological
R0/X0test that monkeypatchesthe config; NaN unbalance translation test; out-of-range int phase
routes to the balanced table with a
WARNING.tests/test_io_logging.py—caplog-pinned warnings for thestring-marker, unknown-key, out-of-range int, list-split, and
unknown-
plugs_and_sockets_apaths; subclass dispatch from JSON;warning when explicit
number_of_phasesdisagrees with the pickedsubclass.
Diagnostic scripts shipped alongside
scripts/run_asymmetric.py— end-to-end runner described above.scripts/sensitivity_sweep.py— sweepsX_OHM_PER_KM,PF, andGEN_XDSS_PUaround their config defaults, prints AC-vs-tree-walkgap tables. Companion to research doc 09.
scripts/validate_optimiser_with_runpp.py— runs the optimiser onthe 2025 fixture and compares the tree walk against
runppon thesame grid.
These are diagnostic / research tools rather than user-facing CLI
surface; they live in
scripts/so they are out of the packageimport path and don't need to be stable across releases.
Not in this PR (deliberately)
compare_3ph_with_tree_walk. The tree walk collapses each nodeto a single scalar voltage; there is nothing to line up against
runpp_3ph's three per-leg voltages. The findings doc (12) is thehonest comparison.
scripts/run_asymmetric.pyis a thin standalonescript; adding it to
python -m nopywerwould convert the existingno-subcommand CLI into a subcommand layout (a breaking UX change).
Out of scope for this PR.
plugs_and_sockets_a/area_mm2cleanup. Same "typefact, not instance fact" logic applies but those are pre-existing
instance/ClassVar inconsistencies left untouched in this PR;
separate cleanup.
Pandapower3phGrid.sourceto receive per-leg writeback(mirroring
compute_short_circuit's use ofPandapowerGrid.source).Currently a back-reference for REPL inspection only.
@472d8294…) with a PyPI version.Tracking issue: switch back once 3.4.1 ships.