Skip to content

feat(twin): declare topology, street layer and slack setpoint in the grid config - #50

Merged
nilsonfh merged 3 commits into
mainfrom
feat/topology-rebase-config
Sep 15, 2026
Merged

nilsonfh merged 3 commits into
mainfrom
feat/topology-rebase-config

Conversation

@nilsonfh

Copy link
Copy Markdown
Contributor

First half of the single topology re-base (bd 4os.7, 4os.14). Mechanism only: a config that declares none of this builds exactly what it built before. Measured on the real footprints through this branch: the undeclared SDK and flagship configs give today's numbers (43 / 134.6% / 2342 buses < 0.95; 209 / 180.7% / 2283), and #42's explicit arguments give #42's numbers.

What a grid config can now declare

  • topology

    • lv_assignment: kmeans or capacitated
    • max_customers_per_transformer: a limit a study can cite, which wins over the sized count
    • block_penalty_km2
    • snap_transformers_to_streets
    • street_layer {path, sha256}: path relative to the footprints file's directory

    Unknown keys and contradictory options are refused by name, before any IO. Explicit builder arguments still override the config.

  • external_grid.vm_pu: the slack setpoint. It defaults to 1.0 pu and is bounded to [0.9, 1.1]. _build_uniform_pandapower_network copied only part of the config and would have dropped this block silently; the offline build test caught that.

Streets as a pinned input

A live OSM fetch is not reproducible. write_street_snapshot (and tools/snapshot_streets.py) writes the layer once, deterministically: the same bytes under any file name, and list-valued OSM fields flattened so OGR does not skip them on read. load_street_snapper_from_file reads it with no network and refuses a digest mismatch. Because the digest lives in the config, a changed layer also changes the config hash every topology cache keys on.

Report

  • The validation report gains topology_options only when the config declares topology or external_grid, or an argument overrides them.
  • lv_assignment gains capacity_source (declared or sized_count).

Why the limit is declared: the Quebec evidence (bd 4os.7)

  • Hydro-Québec's own guide: a 100 kVA unit "pourra desservir jusqu'à dix maisons".
  • Metered Hydro-Québec data, 573 electric-heated homes, hourly coincident annual peak: 100 kVA at nameplate holds 9 (P95) to 11 (P50) homes.
  • Province-wide: about 680,000 aerial transformers (VGQ 2022) for 4.6 M subscriptions (HQ 2024).

The flagship already sizes each unit's kVA from a ladder in-study, so its partition limit is the largest residential unit's count, not its 75 kVA placeholder. The config values and the flagship re-base come in a follow-up PR.

Checks

  • 287 passed across the new tests/test_topology_config.py (offline, hand-built streets), capacitated assignment, street siting, the generator, network adapters, builder relocation, flagship topology, hygiene, doc, import-hygiene, layer-direction and twin test files.
  • black 24.3.0 / isort 5.13.2 / flake8 7.0.0 with bugbear and docstrings: clean.
  • mypy gridalyn 120 = baseline.

🤖 Generated with Claude Code

nilsonfh and others added 3 commits September 15, 2026 19:29
…grid config

The single topology re-base (bd 4os.7, 4os.14) needs a study to pin its
partition, siting and slack setpoint as data, and its streets as a file. This
commit adds the mechanism only: a config that declares none of it builds
exactly what it built before.

- config["topology"]: lv_assignment, max_customers_per_transformer (a cited
  limit that wins over the sized count), block_penalty_km2,
  snap_transformers_to_streets, street_layer {path, sha256}. Unknown keys and
  contradictory options are refused by name before any IO. Explicit builder
  arguments still override the config.
- config["external_grid"]["vm_pu"]: the slack setpoint, 1.0 pu when absent,
  bounded to [0.9, 1.1]. _build_uniform_pandapower_network copied only part of
  the config and would have dropped this block; the offline build test caught
  that.
- Street layer on disk: write_street_snapshot / tools/snapshot_streets.py write
  the OSM layer once; load_street_snapper_from_file reads it with no network and
  refuses a digest mismatch. Because the digest sits in the config, a changed
  layer changes the config hash every topology cache keys on.
- The validation report gains topology_options only when the config declares
  topology/external_grid or an argument overrides it; lv_assignment gains
  capacity_source (declared or sized_count).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JSON has no comments, and a declared per-transformer limit should say where it
comes from. The key carries no behaviour; a test pins that options with and
without a note resolve equal.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
PR CI found two tests (test_real_geojson, test_with_geojson_generator) whose
configs carried an older external_grid shape: voltage_kv, va_degree, p_mw,
q_mvar. Nothing in the repository reads any of them; the builder always created
the slack at 1.0 pu. The new validator refused them as unknown keys.

They stay refused, since accepting keys that configure nothing is the trap the
validator exists for, but the error now names them as never read, so a config
in that shape learns it can delete them. The two test configs drop the dead
block, which changes nothing they build: the slack stays at 1.0 pu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@nilsonfh
nilsonfh force-pushed the feat/topology-rebase-config branch from 3a31c16 to 7b78363 Compare September 15, 2026 19:30
@nilsonfh
nilsonfh merged commit fe50cd3 into main Sep 15, 2026
7 checks passed
nilsonfh added a commit that referenced this pull request Sep 16, 2026
…it moves (#52)

* chore(rebase): declare the Quebec-grounded topology on the SDK and flagship configs

The data half of the single topology re-base (bd 4os.7, 4os.14, 4os.6). The
flagship's pins and CALIBRATION entry follow in this branch after the full run;
nothing here is re-pinned yet.

- ev_hosting_flex synthetic_network_config.json:
  - capacitated LV assignment, at most 10 homes per MV/LV unit;
  - block penalty 0.005 and street siting, both from a pinned street layer;
  - external_grid.vm_pu 1.04.
  The limit comes from Hydro-Quebec's own guide (100 kVA serves up to ten
  houses), cross-checked on metered HQ electric-heated homes (100 kVA at
  nameplate: 9 at P95, 11 at P50). The study rates each unit from its kVA
  ladder, so the partition limit is the largest residential unit's. 1.04 is the
  slackVmPu the study already declares.
- inputs/streets.geojson: 5103 drive_service segments written by
  tools/snapshot_streets.py, sha256 declared in the config, with ODbL
  attribution in inputs/README.md and a streetLayer entry in project.yaml.
- configs/grid/config.json (SDK default):
  - capacitated, at most 20 per 210 kVA unit (20 metered HQ electric-heated
    homes peak at 190.6 kW P95 against 205.8 kW usable);
  - external_grid 1.04;
  - no street layer, since the config serves arbitrary footprints.
  Removes sizing_capacity_kva and mv_hv.count, which nothing reads (bd 4os.6).
- Tests that copy either config for an unrelated purpose now strip its
  topology block, so they keep testing what they tested.

Measured through PR #50's API on the real footprints:
- flagship: homes per unit 1-10 (mean 5.99, unchanged), forced block crossings
  308 -> 38, 503 of 540 clusters inside one block, every transformer on a street;
- SDK default at 1.04 pu: 0 transformers above 100%, 0 LV buses below 0.95.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(ev_hosting_flex): stop declaring outputs/json as an output of prepare_workspace

prepare_workspace runs `gridalyn project prepare-workspace`, which creates the
SDK's default output directories (data, figures, manifests, operations,
reports, cache). outputs/json is not one of them. The stage declared it anyway,
and since the runner enforces declared outputs, a run on a clean tree fails at
stage 1: "stage 'prepare_workspace' exited 0 but did not produce outputs/json".

Every run so far used a checkout where an earlier run had already left
outputs/json behind, so none of them saw the failure. The first cold run in a fresh
worktree did (re-base run, eff31e8b). Nothing reads the directory before a stage
writes into it: every writer goes through ProjectScript.write_json, which
creates the parent. The declaration was false, so it is removed rather than
made true by creating an empty directory.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* chore(ev_hosting_flex): re-base 42 pins on the topology re-base run, and record why

One cold full run from committed code: 25 stages, no stage filter, manifest
completed, git_commit 6de591bd, 2026-09-15 19:02-22:26Z, in a dedicated
worktree. Regression is 94/94 valid against the re-based pins, the flagship
sweep is 157 passed, and check_calibration_claims gates 33 claims OK.

WHAT MOVED (42 of 94). Not the hosting headlines: annual.*, cred.*, insurance.*,
coldcoupling.* and flexincentive.* are value-identical, because the worked
example is still a 6-home 71.25 kW physical twin (idx-0 now, idx-10 before).
What moved is everything computed over the fleet and over the whole network:
pf 6, netchar 5, cluster 6, perf 2, congestion 3, phase 2, voltage 2,
voltage_net 4, nonwires 4, fleet 8.

The two numbers to carry:
  voltage_net.p_undervolt_at_ref     0.0 -> 0.11273   (first risk 1.563 -> 0.943 EV/home)
  fleet.n_at_risk_at_1ev_static      500 -> 506

WHY. The four model changes of bd 4os.7 / 4os.14, declared in the study's grid
config: a capacity-limited LV partition at 10 homes per unit (Hydro-Quebec's
subdivision guide, cross-checked on metered HQ data), transformers sited on the
street instead of on the building they serve, a block penalty that cuts
partition-forced block crossings 308 -> 38, and a slack declared at 1.04 pu
instead of an implicit 1.0. The undervoltage move is the honest consequence of
modelling service drops as conductor rather than as 1 m stubs.

RECORDED. CALIBRATION.md carries a dated section with the evidence, the
per-family moves and the limits (including that per-change attribution was NOT
measured: the four moved together in one run). REBASE_LOG.md carries the entry
and the new digest. README findings that quoted moved numbers are updated.

tests/test_ev_hosting_flex_phase.py asserted a balanced min voltage <= 1.01 pu,
which pinned the old implicit 1.0 slack. It now bounds by the declared setpoint.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant