Co-Study4Grid / docs /data /grid-layout-coordinate-scale.md
github-actions[bot]
Deploy 7688ef1
13d4e44
|
Raw
History Blame Contribute Delete
6.55 kB

Grid-layout coordinate scale

TL;DR: grid_layout.json MUST be in raw Mercator metres (span ≈ 1.4–1.6 M for the French grid). Pypowsybl emits VL outer circles at a fixed r = 27.5 user-space units; if the layout span gets squashed to e.g. 8 000, then 27.5 stops being negligible and every neighbour overlaps the rendered circle. This document explains the math and records the 2026-05-08 fix.

Symptom

On pypsa_eur_fr225_400 the Network (N) tab rendered as an unreadable dark blob in dense urban regions (Paris, Lyon, Lille). VL circles visibly overlapped one another even at maximum zoom-in. Branch lines, flow arrows, and labels were all crammed inside the blob. Frontend SVG post-processing (svgBoost) could mitigate but never fully clear the overlap.

Diagnosis

Pypowsybl's NAD generator (powsybl-network-area-diagram) emits each VL bus node as a <circle r="27.5"> in user-space SVG coordinates, regardless of the surrounding layout scale. Other glyph sizes are similarly fixed (line stroke 5, arrow 10×10, edge-info font 20). The viewBox→screen transform applies uniformly to all of them, so the relative size of nodes vs inter-node distance is determined entirely by the on-disk coordinate scale.

We compared two layout files for the French grid:

Layout Source Entries x-span (units) Median nearest-neighbour Median NN ÷ r=27.5
Operator reference (RTE study format) Internal RTE export 18 141 1 643 372 1 776 64.6 ×
Pre-fix regenerate_grid_layout.py OSM → Mercator, ÷ 8000/x_range 1 196 8 000 26 0.95 ×

A median NN of 0.95 × r means adjacent VL circles overlap by definition — the circle's diameter is roughly the distance between its centre and the next centre over. There is no clipping, alpha, or zoom tweak that can recover whitespace from this geometry.

The operator file's 65 × ratio is comfortable: the next neighbour sits 65 r away, leaving > 100 × visual whitespace per node. That is the geometry pypowsybl was designed for.

Where the rescale came from

scripts/pypsa_eur/regenerate_grid_layout.py (originally cribbed from Step 9 of convert_pypsa_to_xiidm.py) projected raw (lon, lat) to Web Mercator, then rescaled every coordinate by 8000 / (max_x − min_x) to fit a fixed 8 000-unit target width. The constant TARGET_WIDTH = 8000 was inherited from the conversion pipeline. It made smaller test grids look "right-sized" at hand-curated viewBoxes but broke for any production-scale dataset.

D8 (2026-07-09): the source of the constant — convert_pypsa_to_xiidm.py's step_write_metadata — still carried the same TARGET_WIDTH = 8_000 rescale, so a pipeline rebuild silently re-emitted the forbidden layout. It now writes grid_layout.json in raw Mercator metres directly (re-centred, no rescale), matching regenerate_grid_layout.py's default. test_grid_layout.py::test_coordinate_spans_in_reasonable_range was corrected from the stale [5000, 20000] assertion (which encoded the forbidden scale) to the raw-metres regime.

Fix (2026-05-08)

regenerate_grid_layout.py:

  • Default behaviour now writes raw Mercator metres (centred on the bounding-box midpoint). For the French grid this gives x_span ≈ 1.4 M, matching the operator reference scale.
  • The --target-width N flag is preserved for backward compatibility but defaults to None. When set below 500 000 the script logs a warning explaining the readability-vs-rescale trade-off.
  • The regeneration prints a median NN / r ratio at the end so you can see at a glance whether the output will read cleanly.

Both bundled layouts were regenerated:

Bundle Old span New span Old median NN/r New median NN/r
data/pypsa_eur_fr225_400/ 8 000 1 401 015 0.95 × 166.8 ×
data/pypsa_eur_fr400/ 8 000 1 307 946 varied 1 090 ×

(The fr400 ratio is unusually high because that dataset only carries 400 kV transmission VLs — 190 entries spread across the same physical area — so neighbours are kilometres apart.)

The pre-fix files are kept as grid_layout.json.bak.8000width siblings so the rescale can be reverted with a single cp if a downstream consumer turns out to depend on the old span.

How to regenerate

# Default (recommended): raw Mercator metres
python scripts/pypsa_eur/regenerate_grid_layout.py --network data/pypsa_eur_fr225_400

# Legacy: rescale to 8 000-unit width (NOT recommended for dense grids)
python scripts/pypsa_eur/regenerate_grid_layout.py --network data/pypsa_eur_fr225_400 --target-width 8000

Regression guards

  • scripts/pypsa_eur/test_regenerate_grid_layout.py::TestCoordinateSanity::test_x_span_matches_france_mercator asserts the on-disk span is in the operator-clean range [1.3 M, 1.7 M].
  • The test docstring explains the failure mode if someone reverts the script.

Why this also retires the svgBoost band-aids

The branch's earlier work (utils/svg/svgBoost.ts) added several frontend SVG transforms — node-circle shrinking, edge-info downscale, polyline kink-drop, line-extension to the new circle edge, and flow-indicator projection. All of them were trying to compensate for the squashed coordinate scale. With the layout fixed at the source, those transforms are no-ops on the operator-clean default (median NN/r = 65–166 ×, well above the threshold the boost code triggers on). The boost code can stay as a safety net for any caller that intentionally uses a small --target-width, but the canonical path no longer needs it.

Cross-references

  • scripts/pypsa_eur/regenerate_grid_layout.py — module docstring carries the same math summary.
  • CHANGELOG.md[Unreleased] "Fixed" section.
  • frontend/src/utils/svg/svgBoost.ts — frontend mitigations that the layout fix retires.
  • voltage-level-separation.md — the follow-on fix for substations whose several voltage levels share one location; run separate_voltage_levels.py after any regeneration here.