Skip to content

Asymmetric three-phase modelling + per-load phase planning - #15

Merged
vfinel merged 20 commits into
vfinel:developfrom
harrysalmon:asymetric-loading-and-new-grid
Jun 7, 2026
Merged

Asymmetric three-phase modelling + per-load phase planning#15
vfinel merged 20 commits into
vfinel:developfrom
harrysalmon:asymetric-loading-and-new-grid

Conversation

@harrysalmon

@harrysalmon harrysalmon commented May 17, 2026

Copy link
Copy Markdown
Collaborator

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:

  1. io.load_geojson now accepts the round-tripped export schema
    (area_mm2, length_m, plugs_and_sockets_a) alongside the
    hand-authored input schema, and accepts list-valued phase for
    multi-phase loads (e.g. [1, 2] for a 2-leg load).
  2. io.load_geojson picks the right Cable subclass from JSON
    content (dispatching on plugs_and_sockets_a) instead of
    instantiating a base Cable with per-instance overrides. Cable
    types are a closed catalogue; the subclass IS the type. Unknown
    ratings fall back to Cable16A with a WARNING.
  3. models.normalise_phase_legs — the single source-of-truth
    helper for what each PowerNode.phase value means. Used by
    io.load_geojson, pp_interop._powerflow_3ph (both
    _phase_split and to_pandapower_3ph's routing) and
    pp_interop.phases._common._fixed_leg_seed. Non-canonical forms
    (legacy string markers, out-of-range ints, malformed lists) all
    coerce to balanced with a WARNING and consistent routing.
  4. pp_interop/phases/ — two phase-distribution strategies
    (greedy + round-robin) and a phase_geojson one-call wrapper for
    the load → plan → apply → write workflow. apply_assignment
    rebuilds node.power_per_phase from (power_watts, new_leg) so
    the planner's mutation keeps both coupled fields coherent.
  5. pp_interop/_powerflow_3ph.pyto_pandapower_3ph and
    compute_power_flow_3ph for runpp_3ph. Returns a distinct
    Pandapower3phGrid type so the dual pp.load / pp.asymmetric_load
    layout is encoded in the type, not in a comment. Independent of
    the balanced converter — no shared state, no mutation.
  6. PowerNode.to_geojson emits phase when non-None, so phase
    plans round-trip through the loader. Every polymorphic form
    (int, list, legacy string, None) is covered by a round-trip test.
  7. Diagnostic logging in io and pp_interop — legacy "U"/"Y"
    string phase markers, unknown-property-key typos, and impossible
    wirings (number_of_phases disagreeing with the picked subclass)
    surface as WARNING or DEBUG instead of being silently coerced.

Why

research/pandapower/10_three_phase_asymmetric_modelling.md is the
motivation: nopywer's tree walk and the balanced runpp agree on a
balanced 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-states
drop on lightly-loaded legs and under-states it on heavily-loaded
ones. Neither models the neutral conductor at all.

runpp_3ph is the right fix. It surfaces two quantities a balanced
solve structurally cannot:

  • per-leg voltage drop, so loads on the heavy leg of an
    imbalanced grid are sized against the leg they actually sit on;
  • neutral-conductor current, which decides whether a
    reduced-section-neutral cable (3G6+½N) is safe or the run needs
    full-section neutral (4G6).

Doc 12 (12_runpp_3ph_findings.md) is the first set of numbers from
this on the real 2026 east-grid fixture; the headline is that
balanced runpp is optimistic (~2.2× lower worst leg drop than
asymmetric at --load-factor 0.5), and that cable_15 carries
20.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):

uv sync --extra pandapower
uv run python -c '
from nopywer.pp_interop import config
from nopywer.pp_interop.phases import phase_geojson
phase_geojson(
    "tests/fixtures/2026-05-14_martin.geojson",
    "/tmp/martin_phased.geojson",
    usage_factor=config.DEFAULT_USAGE_FACTOR,
    strategy="greedy",
)
'

Run the asymmetric solve with a printable report:

# already-phased fixture at festival diversity (0.5x nameplate)
uv run python scripts/run_asymmetric.py \
    tests/fixtures/2026-05-14_martin_modified.geojson \
    --load-factor 0.5

# raw fixture with greedy phase planning
uv run python scripts/run_asymmetric.py \
    tests/fixtures/2026-05-14_martin.geojson \
    --plan greedy --load-factor 0.5

Reads the fixture, snaps cables, optionally plans phases, scales
loads by --load-factor, runs both the balanced and asymmetric
solves side-by-side, and prints the top worst-leg voltage drops and
worst-neutral cables.

The script has one user-facing knob--load-factor — that
applies 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's
pre-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, Cable125A are 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:

  • Tier facts live on the subclass as ClassVars (num_phases, max_current_a, tier_cost) or as dataclass-field defaults that aren't normally overridden (plugs_and_sockets_a, area_mm2). The base Cable is abstract — instantiating it directly gives you the 16 A defaults, which is what some tests use for type-hinting collections.
  • io.load_geojson picks the right subclass from JSON content via _pick_cable_class, dispatching on plugs_and_sockets_a. Unknown ratings fall back to Cable16A with a WARNING; an explicit number_of_phases that disagrees with the picked subclass also logs a WARNING (the subclass wins — the catalogue is authoritative).
  • Cable.to_geojson reads type(self).num_phases for the exported phase count and the cable_type string. 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.py and inventory.py read cable.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_phases as an instance field on the base Cable, duplicating the subclass num_phases ClassVar and quietly allowing impossible wirings. That has been removed; the subclass-as-type design is the source of truth.

2. PowerNode.phase is polymorphic; one helper normalises it everywhere

PowerNode.phase is the single hottest piece of polymorphism in the model. Every value it can take:

phase value Meaning
None Balanced — load is spread evenly across L1/L2/L3.
1, 2, 3 Single-leg — load sits entirely on that leg.
[1, 2] (any list of valid leg ints) Multi-phase — load is split evenly across the listed legs.
"U", "Y" Legacy sub-grid reporting markers. Treated as balanced (no electrical meaning), with a WARNING.
Anything else (out-of-range int, bool, malformed list, …) Treated as balanced, with a WARNING.

Three sites need to interpret this field — io.load_geojson, pp_interop._powerflow_3ph (both _phase_split and to_pandapower_3ph's routing), and pp_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 like phase=5 produced a balanced split everywhere but still got routed to net.asymmetric_load in the 3ph converter, mislabelling a balanced load as asymmetric.

The PR extracts a single normalise_phase_legs(phase, *, node_name) -> list[int] helper in models.py:

  • Canonical forms pass through silently. An int 1-3 returns [that int]; a list with at least one valid leg returns the list filtered to ints 1-3 (silent filter — operator intent is unambiguous); None returns [].
  • Non-canonical forms coerce to [] (balanced) and emit a WARNING that 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.
  • Routing falls out of the result: to_pandapower_3ph does legs = normalise_phase_legs(...); not legsnet.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_assignment also re-derives node.power_per_phase from (power_watts, new_phase) using the same rule, so the two coupled fields stay coherent after a planning run — analyze.py's tree walk reads power_per_phase directly and would otherwise walk stale balanced thirds for a load that just got assigned a single leg. Same fix is applied in scripts/run_asymmetric.py's _scale_loads_in_place for the same reason.

3. Parsing strategy — be liberal at read time, strict in the warning

io.load_geojson accepts both the hand-authored input schema and the round-tripped export schema (area_mm2area, length_mlength, plugs_and_sockets_aplugs&sockets). The aliasing table in _EXPORT_KEY_ALIASES is the one place that mapping lives.

The loader is intentionally permissive: legacy keys (cum_power instead of cum_power_watts), unknown property keys (typos, third-party metadata), and non-canonical phase forms all load — every coercion is logged at the right level (DEBUG for unknown keys that are typically benign noise from exported computed fields; WARNING for 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 by PowerGrid.__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.py and research/pandapower/, but
worth flagging here:

  • R0_OVER_R1 = X0_OVER_X1 = 4.0 — rule-of-thumb default for
    4-/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.py and research/pandapower/10.1 §
    "Picking R0/R1 and X0/X1 — what the ratios really mean".
  • SOURCE_X0X_MAX = 1.0, SOURCE_R0X0_MAX = 0.1 — assumes
    TN-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 festival
    diversity factor. Required at every phases call site (no
    function 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-factor flag, so library and CLI
    agree 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.md under "Modelling assumption —
    multi-phase loads self-balance evenly". Currently only matters
    for curious creatures in the 2026 fixture; flagged for any
    future genuinely-asymmetric multi-phase load.

Also worth a second pair of eyes:

  • The modified fixture tests/fixtures/2026-05-14_martin_modified.geojson
    was hand-edited to upsize four cables so the grid converges. The
    resize sweep is in research/pandapower/11; the original raw export
    is preserved as 2026-05-14_martin.geojson.
  • The phase = None semantics are explicit and important — None
    is the modelling choice "balanced three-phase draw", not "missing
    data". pp.load under runpp_3ph is 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 § "What phase = None means at
    each layer".

Required reading for the technical reviewer

  • src/nopywer/pp_interop/README.md — API surface, runnable examples,
    the balanced/asymmetric type distinction (PandapowerGrid vs
    Pandapower3phGrid — independent nets, no shared state), the
    "What phase = None means at each layer" per-layer table, and the
    logging surface.
  • research/pandapower/10_three_phase_asymmetric_modelling.md — what
    the work was and the shopping list it came from.
  • research/pandapower/10.1_three_phase_theory.md — physics of why
    single-phase loads unbalance the other two legs and how the
    zero-sequence parameters get picked.
  • research/pandapower/11_field_export_fixture_walkthrough.md — the
    real-fixture work that produced the modified fixture.
  • research/pandapower/12_runpp_3ph_findings.md — first numerical
    findings from runpp_3ph on the modified fixture.

CI coverage

CI now installs the pandapower extra (uv sync --frozen --extra pandapower) so the pp_interop tests actually execute. Previously they all pytest.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 for
    normalise_phase_legs across 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_count that
    pins the subclass-as-type design.
  • tests/test_pp_interop_phases.py — strategies, candidate selection,
    apply (with new power_per_phase coherence test), the round-trip
    through GeoJSON for every polymorphic phase form, the
    phase_geojson wrapper.
  • tests/test_pp_interop_powerflow_3ph.py_phase_split
    parametrised across every phase form; per-physics tests for
    balanced 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/X0 test that monkeypatches
    the config; NaN unbalance translation test; out-of-range int phase
    routes to the balanced table with a WARNING.
  • tests/test_io_logging.pycaplog-pinned warnings for the
    string-marker, unknown-key, out-of-range int, list-split, and
    unknown-plugs_and_sockets_a paths; subclass dispatch from JSON;
    warning when explicit number_of_phases disagrees with the picked
    subclass.

Diagnostic scripts shipped alongside

  • scripts/run_asymmetric.py — end-to-end runner described above.
  • scripts/sensitivity_sweep.py — sweeps X_OHM_PER_KM, PF, and
    GEN_XDSS_PU around their config defaults, prints AC-vs-tree-walk
    gap tables. Companion to research doc 09.
  • scripts/validate_optimiser_with_runpp.py — runs the optimiser on
    the 2025 fixture and compares the tree walk against runpp on the
    same grid.

These are diagnostic / research tools rather than user-facing CLI
surface; they live in scripts/ so they are out of the package
import path and don't need to be stable across releases.

Not in this PR (deliberately)

  • A compare_3ph_with_tree_walk. The tree walk collapses each node
    to a single scalar voltage; there is nothing to line up against
    runpp_3ph's three per-leg voltages. The findings doc (12) is the
    honest comparison.
  • A CLI subcommand. scripts/run_asymmetric.py is a thin standalone
    script; adding it to python -m nopywer would convert the existing
    no-subcommand CLI into a subcommand layout (a breaking UX change).
    Out of scope for this PR.
  • A unified plugs_and_sockets_a / area_mm2 cleanup. Same "type
    fact, not instance fact" logic applies but those are pre-existing
    instance/ClassVar inconsistencies left untouched in this PR;
    separate cleanup.
  • Wiring Pandapower3phGrid.source to receive per-leg writeback
    (mirroring compute_short_circuit's use of PandapowerGrid.source).
    Currently a back-reference for REPL inspection only.
  • Replacing the pandapower git pin (@472d8294…) with a PyPI version.
    Tracking issue: switch back once 3.4.1 ships.

harrysalmon and others added 18 commits May 14, 2026 22:02
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>
@harrysalmon
harrysalmon marked this pull request as draft May 17, 2026 20:10
…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>
@harrysalmon
harrysalmon force-pushed the asymetric-loading-and-new-grid branch from 2d871d6 to 2f36b4f Compare May 17, 2026 20:16
@harrysalmon
harrysalmon marked this pull request as ready for review May 24, 2026 14:30
@vfinel

vfinel commented Jun 7, 2026

Copy link
Copy Markdown
Owner

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.
Thanks again for your contribution !

runtime warning: invalid value encountered in divide

When running

# already-phased fixture at festival diversity (0.5x nameplate)
uv run python scripts/run_asymmetric.py \
    tests/fixtures/2026-05-14_martin_modified.geojson \
    --load-factor 0.5

as suggested, i get the following warning: nopywer/.venv/lib/python3.14/site-packages/pandapower/results_bus.py:71: RuntimeWarning: invalid value encountered in divide net["res_bus_3ph"]["unbalance_percent"] = np.abs(V012_pu[2, :]/V012_pu[1, :])*100

Maybe this is something worth looking at ?

convergence issue when running example with phase planning

when running the other example

# raw fixture with greedy phase planning
uv run python scripts/run_asymmetric.py \
    tests/fixtures/2026-05-14_martin.geojson \
    --plan greedy --load-factor 0.5

the power flow did not converged :

did NOT converge: LoadflowNotConverged: Power Flow nr did not converge after 30 iterations!

Maybe it would be worth to replace by a working example ? Reducing the load-factorto 0.1 does the trick.

@vfinel
vfinel merged commit 57c301e into vfinel:develop Jun 7, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants