Skip to main content
Pre-release

This release is a pre-release and may not be stable for production use.

gdm-reduce

Phase-aware Kron reduction for distribution systems in the NLR Distribution Suite.

This alpha implements exact source-conditioned electrical reduction and phase-aware Opti-KRON candidate search with radial numerical equivalents. It uses existing A/B/C/S1/S2 bus-phase terminals, retains whole buses, preserves phase coupling, and reconstructs eliminated voltages using sparse factorized solves.

This is a prescribed-current numerical equivalent, not an aggregated nonlinear power-flow model or an OPF feasibility certificate. Supported radial equivalents can now be exported as separate GDM equipment models, with independent nonlinear validation. GDM metadata and fixed-configuration reference-scenario ingestion are available through the optional gdm-flow integration.

Install

The executable Jupyter Book / MyST documentation covers equations, paper references, model preparation, API and an original-versus-reduced GDM comparison. Install .[docs] and run jupyter-book build --html --strict from docs.

Python 3.11+ and grid-data-models 2.4.0+ are required. Imports use the canonical gdm.systems.distribution namespace. This is an alpha source release. From a repository checkout, install the core package or the optional GDM power-flow integration:

python -m pip install .
python -m pip install '.[flow]'

Exact Reduction

import numpy as np
from gdm_reduce import ElectricalNetwork, kron_reduce

network = ElectricalNetwork(
    admittance=np.array([[10, -10, 0], [-10, 20, -10], [0, -10, 10]]),
    labels=(("source", "A"), ("middle", "A"), ("load", "A")),
    source_voltages={("source", "A"): 120},
)
currents = np.array([0, -1, -2], dtype=complex)
result = kron_reduce(network, retained_buses={"source", "load"})
retained_voltage = result.solve(currents)
full_voltage = result.reconstruct(currents, retained_voltage)

Admittance is in siemens, voltages in volts, and currents in amperes, positive into the passive network. Negative currents represent consumption. Currents always use the original terminal indexing, even when passed to a reduced result. Source current entries are ignored by voltage solves: source injections are determined by the fixed voltages and can be obtained from network.admittance @ full_voltage. Multiple scenarios use a (terminals, scenarios) current array.

Every fixed-source bus must be retained. Disconnected terminals, singular or excessively ill-conditioned blocks, nonfinite inputs, and explicit neutral labels are rejected. Inputs are copied and reduction does not mutate them. Kron equivalents can have significant fill-in even when the input is sparse.

GDM Scenario Ingestion

python examples/ingest_gdm.py /path/to/model.json
from gdm.systems.distribution import DistributionSystem
from gdm_reduce import build_gdm_scenario_library

system = DistributionSystem.from_json("model.json")
library = build_gdm_scenario_library(system, load_scales=[0.5, 1.0])

The library stores original terminal indexing, nominal phase-voltage bases, bus/component UUID lineage, configuration fingerprints, and solver provenance. Each scenario must converge, share the same source state and passive matrix, and satisfy the voltage/current KCL consistency tolerance. The generator copies the model and rejects backend hardware-state mutations. It uses the backend's constant-P/Q semantics; ZIP evaluation and independent whole-feeder OpenDSS baseline validation are not established by this check. Supported grounded split-phase line-to-line loads are described below. Time-series profiles are not interpreted by the scenario generator.

from_gdm_flow_results(system, results) accepts a mapping of names to solved results; callers must supply their original model. Matrices are taken from the results, not independently restamped. Fingerprints include the full static GDM component serialization, so models differing in UUIDs or static load settings are conservatively rejected when mixing references. Copied DiTTo test circuits and their provenance are described in the model catalog.

The documentation regression tests use copied DiTTo circuit fixtures without network downloads or runtime conversion. Older external IEEE-13 integration tests discover sibling gdm-flow data or GDM_REDUCE_TEST_DATA; the new copied fixture tests do not depend on that discovery. CI installs Flow and OpenDSS.

The supplied IEEE-13 model is exercised end to end. IEEE-123 must include its referenced ieee_123_node_time_series companion storage before GDM can load it. Center taps are treated as grounded under the assumption below. The updated stamper includes nonnegative no-load core losses and accepts complete zero- or one-based winding-pair indices. These features do not by themselves establish reference convergence or independent baseline fidelity for every feeder.

GDM 2.4 Station Equipment

With the updated gdm-flow backend, series reactors and supported two-winding station transformers participate in AC references. Reactor, transformer, and switch terminals are protected during candidate selection. Export retains these devices, their concrete equipment types, UUIDs, and nameplate metadata; only plain matrix-impedance line paths are synthesized or contracted. Station breaker, disconnector, and earthing equipment retain their static switch states. Protection trips, thermal behavior, and unsupported transformer layouts are not simulated.

Ground-Referenced Split Phase

S1/S2 references require the updated companion gdm-flow center-tap stamper and connection-aware power-flow solver. Install the updated sibling during development with python -m pip install -e ../gdm-flow; its version number alone does not guarantee the required split-phase capabilities.

By default, build_gdm_scenario_library() assumes all supported center taps are grounded, including when GDM winding flags say otherwise. Only the solver's deep copy is normalized; input models and files remain unchanged. The assumption is recorded as assumed-grounded in scenario provenance, service-domain fingerprints, and result bundles. It is a modeling assumption, not verified grounding evidence. Pass assume_center_taps_grounded=False, or CLI --require-grounding-metadata, to require the original secondary winding grounding flags instead.

Supported services have one A/B/C primary and two single-phase STAR secondary windings sharing a center-tap bus. Explicit N, floating neutrals, ambiguous service connections and unsupported device connections remain rejected. No-load core loss uses a tap-adjusted winding-two conductance, checked against OpenDSS. Secondary winding order and S2/S1 bus order are supported; missing legs are not padded. Split line-to-line bus ratings are divided by two, not square root of three. Transformer/primary UUIDs and primary phase identify each service domain; current pooling cannot cross domains or nominal bases.

Grounded STAR loads use per-leg constant P/Q. A two-leg DELTA load uses the sum of its GDM phase-load powers as one S1-to-S2 device, with opposite currents computed from the actual differential voltage. ZIP coefficients are not evaluated. Independent OpenDSS service tests cover winding primitives, mutual line coupling, unequal legs, leg-to-leg loads, heavy loading, and reverse power. They do not certify an arbitrary feeder or reduced nonlinear device model.

Selection and holdouts enforce complex S1-minus-S2 voltage error as well as leg magnitude limits. Differential voltage uses the sum of the leg bases. Reports include differential magnitude/complex error and signed leg-imbalance differences. audit_split_phase_grounding(system) inspects original flags without modifying them, even when the generator uses the grounded assumption.

python examples/search_gdm.py /path/to/model.json
from gdm_reduce import get_gdm_search_constraints, optimize_kron_reduction

constraints = get_gdm_search_constraints(system, library)
selected = optimize_kron_reduction(
    library,
    constraints.bus_edges,
    protected_buses=constraints.protected_buses,
    voltage_error_limit_pu=0.001,
    target_reduction_fraction=0.5,
)
retained_voltage = selected.equivalent.solve(selected.aggregated_currents)

Search enumerates adjacent whole-bus merges on a connected radial input graph. The removed bus's phases must exist at the receiver, with matching nominal phase-voltage bases and split-phase service domains. GDM topology comes from active physical equipment, not off-diagonal admittance entries. Sources, transformer/regulator/switch boundaries, DER, capacitors, and controller measurement buses are conservatively protected. Caller-specified protected buses can be added to this set. Configuration checks reject a GDM model changed since reference generation.

Candidates pool the accumulated cluster currents at the receiver. Algorithm 1's SMICE objective sums cluster-wise maximum magnitude errors over scenarios; a separate maximum original-terminal error enforces feasibility. Every step checks all original terminals against the original references, not just surviving buses. Candidate solves reuse one sparse LU in bounded batches. Ties within 1e-12 pu use receiver/removed bus-name order. This is exhaustive one-merge search, not a globally optimal partition or a held-out/nonlinear accuracy guarantee.

selected contains clusters, complete name and UUID mappings, original-index pooled currents, represented training voltages, step diagnostics, a stop reason, and a KronReductionResult. It is not a GDM DistributionSystem. Use the separate equipment exporter after radialization and independently validate its devices. Use selected.aggregated_currents, rather than the original library currents, to solve the selected current-pooled equivalent.

Radialization

from gdm_reduce import radialize_reduction

radial = radialize_reduction(selected, constraints.bus_edges)
retained_voltage = radial.equivalent.solve(selected.aggregated_currents)

Eliminating a branching junction can turn a tree into a meshed Schur equivalent. Radialization prunes non-root leaves from the original physical tree, restores non-root branching junctions in the subtree connecting selected roots, and suppresses degree-two paths. This is minimal structural restoration for that tree, not a claim of globally minimal equipment reconstruction.

Restored junctions have zero pooled injection; they do not become new cluster roots or receive reassigned devices. Original cluster/UUID mappings, currents, and training scores remain in radial.selection. radial.retained_buses includes both selected roots and restored junctions, so radial.reduction_fraction can be smaller than the search reduction. The example reports both percentages.

Both original and radial matrix bus couplings must match their physical trees. No matrix entries are discarded to conceal mesh edges. Selected-terminal complex voltages must agree within 1e-7 V absolute and 1e-9 relative tolerance by default; the result reports their maximum difference. Coupled and missing A/B/C/S1/S2 phases are preserved. This remains a numerical equivalent, not GDM feeder equipment or nonlinear validation.

Reduced GDM Export

from gdm_reduce import build_reduced_gdm, save_reduced_gdm, validate_reduced_gdm

exported = build_reduced_gdm(system, radial, library)
validation = validate_reduced_gdm(exported, holdout, split="holdout")
save_reduced_gdm(exported, "results/reduced.json")

The exporter deep-copies the original system, retains existing bus UUIDs and protected transformers/regulators/switches/DER, prunes removed line stubs, and replaces degree-two matrix-impedance paths with reciprocal passive series-line equipment. New lines have deterministic UUIDs, summed path length, conservative minimum ampacity and original-line UUID provenance. It independently restamps the exported model and rejects a Y-bus mismatch with the radial equivalent (default absolute tolerance 1e-6 S, relative tolerance 1e-9).

Loads move to their assigned roots without merging devices: UUIDs, phases, individual P/Q, ZIP coefficients and attached time-series profiles survive the GDM serialization round trip. Original models are not modified. Center-tap grounding assumptions are applied only to the exported copy and recorded. Profiles or supplemental metadata attached to removed buses/lines are rejected rather than silently discarded. Contracted geometry/sequence lines, parallel paths, nonreciprocal/nonpassive equivalents and non-realizable Schur shunts are not supported.

Nonzero charging on contracted/pruned lines is not silently dropped. An explicit allow_series_only=True acknowledges its omission, consistent with the series-only reference workflow. The exported Y-bus must still match; this option cannot turn a charged Schur equivalent into a pure series model. This limitation matters for feeders with nonzero capacitance. Uncontracted equipment remains unchanged.

validate_reduced_gdm() solves the exported devices at recorded static load and solar scales and compares original-terminal representative voltages separately from current-pooled errors. It records convergence, magnitude/complex errors and S1-S2 complex error in the export report. Missing scale provenance is rejected. Its backend is gdm-flow's constant-P/Q solver; this is not independent OpenDSS, ZIP, profile-driven time-series, thermal or OPF validation. A failed solve or voltage limit fails validation. Programmatic callers own the training/holdout split; use explicit split labels when recording multiple evaluations.

The model is canonical GDM JSON, including its companion time-series storage. Assumptions, mappings, path provenance and validation are saved separately as reduced.export-report.json; neither file is overwritten by default.

gdm-reduce model.json --target-reduction 0.5 \
  --output results/numerical.json --export-gdm results/reduced.json

For an explicitly authorized series-only export of charged contracted lines, add --export-series-only. The CLI independently validates exported-device training and holdout snapshots, saves failing reports and exits 1 if either numerical or exported-device validation fails. Input/model/report paths must be distinct, even with --overwrite.

Held-Out Validation

from gdm_reduce import evaluate_reduction

holdout = build_gdm_scenario_library(system, load_scales=[0.75, 1.25])
report = evaluate_reduction(radial, holdout, voltage_error_limit_pu=0.001)
print(report.passed, report.max_voltage_magnitude_error_pu)

Evaluation freezes the selected clusters, applies them to new reference currents, and checks the configuration fingerprint before solving. It does not re-optimize on holdouts. Per-scenario reports separate magnitude, complex-voltage and wrapped angle errors, full-network reference consistency, backend convergence mismatch, and KCL residual. An angle error is None if a representative voltage is zero. Power diagnostics use electrical reconstruction and actual YV currents, not the ignored pooled source entries. Signed power differences are reduced minus reference; passive active absorption includes every passive element in the matrix, not just line losses. These are diagnostics, not thermal or nonlinear certificates.

The evaluator accepts training or held-out references; callers own the split. Tests exercise unseen heavier demand, reverse flow and phase-specific changes, including a stress case that passes training and fails the held-out threshold.

CLI And Result Bundles

gdm-reduce /path/to/model.json \
  --train-load-scales 0.5 1.0 --holdout-load-scales 0.75 1.25 \
  --voltage-error-limit-pu 0.001 --target-reduction 0.5 \
  --output results/numerical.json --export-gdm results/reduced.json

--target-reduction accepts one fraction between 0 and 1, not a percentage list. Search is accuracy-limited; structural protections and restored junctions can make actual radial bus removal smaller than the requested target. Batch percentage export and automatic primary-only preprocessing are not CLI features.

The CLI rejects overlapping training/holdout scales. It saves a failed validation report rather than silently changing the clusters. Exit codes: 0 when both numerical reports and any requested exported-device validation pass the magnitude and applicable S1-S2 complex limits, 1 for a validation failure, 2 for invalid arguments, incompatible configurations, solver failures or file errors. Without --output, the JSON bundle is printed to stdout. Existing files require explicit --overwrite. python -m gdm_reduce.cli is also available.

The gdm-reduce-result-v1 bundle stores the reduced CSR matrix, fixed source phasors, original/retained indexing, clusters and UUID mappings, restored junctions, step history, reference and pooled currents, separate reports, and provenance. It is explicitly labeled radial-prescribed-current-equivalent, with gdm_model_exported: false unless a separate CLI GDM export was saved. It is not accepted as a GDM feeder model, even when it references that export. Split-phase bundles use gdm-reduce-result-v2, adding service domains, the ground-reference contract, and grounding-policy provenance. Existing ABC v1 bundles remain readable; split-phase data without v2 metadata is rejected.

import json
from pathlib import Path
import numpy as np
from gdm_reduce import network_from_result_bundle, solve_voltages

bundle = json.loads(Path("results/numerical.json").read_text())
network = network_from_result_bundle(bundle)
values = bundle["holdout"]["pooled_retained_currents"]
currents = np.array(values["real"]) + 1j * np.array(values["imag"])
retained_voltage = solve_voltages(network, currents)

build_result_bundle() and save_result_bundle() provide the same artifact API for programmatic workflows. The decoder validates the numerical equivalent; it does not certify manually edited reports or reconstruct original equipment.

Per-stage wall-clock timings, package/Python/platform versions, CPU count and BLAS/OpenMP thread environment settings are recorded for reproducible CPU runs. Compare repeated runs on matching hardware and thread settings; these are single-run measurements, not hardware-independent performance guarantees. Current dense-first gdm-flow assembly still prevents utility-scale claims.

IEEE-13 is exercised with 0.5/1.0 training and 0.75/1.25 holdout snapshots. Exported constant-P/Q devices are separately solved and checked; independent whole-feeder OpenDSS validation and operational certification remain outside these checks.

Development

python -m pip install -e '.[dev,flow,opendss]'
pre-commit install
pytest --cov=gdm_reduce --cov-report=term-missing
ruff check .
ruff format --check .
python -m build
python examples/exact_reduction.py

See the numerical contract for source-boundary and current-transformation semantics, and scope and validation for implemented features and remaining limits. GDM and gdm-flow compatibility does not imply support for all devices or successful reference-solver validation.

Research Basis

  • Chevalier and Almassalkhi (2022), Towards Optimal Kron-based Reduction Of Networks (Opti-KRON) for the Electric Power Grid, DOI: 10.1109/CDC51059.2022.9992730.
  • Mokhtari, Chevalier, and Almassalkhi (2026), Optimal Kron-based Reduction of Networks (Opti-KRON) for Three-phase Distribution Feeders, arXiv:2510.19608v3.

The implementation is independently written from the mathematical formulation. The papers and private utility datasets are not distributed with this package.

License

BSD-3-Clause. See LICENSE.

Metadata

Release files for gdm-reduce 0.1.0a2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gdm-reduce 0.1.0a2
File Size Uploaded
gdm_reduce-0.1.0a2.tar.gz 85.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for gdm-reduce 0.1.0a2
File Interpreter ABI Platform
gdm_reduce-0.1.0a2-py3-none-any.whl Python 3 none any Details

Total release size: 127.5 kB

Release files / gdm_reduce-0.1.0a2.tar.gz

Download URL gdm_reduce-0.1.0a2.tar.gz
Size 85.0 kB
Tags Source
SHA-256 checksum
How to use checksums
5088ca931114f6b1ab978bc9d59bb3e2ece2cc4f46e655eb8765291e6c6d6299
BLAKE2b-256 checksum
How to use checksums
e88224ca61d8a7e5adef3c31a34476cf6139861bacba737b38ca119b696aa8f9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.

Transparency log

Release files / gdm_reduce-0.1.0a2-py3-none-any.whl

Download URL gdm_reduce-0.1.0a2-py3-none-any.whl
Size 42.5 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ffb3fd823c8aa1f30f20dd3643bb3e2fb5777f2f78541ce7ff19085be74583dc
BLAKE2b-256 checksum
How to use checksums
33c81b555f6e8da875d2bd689de34d700d4a3ed65569a16111b5ce570d747ef6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 13, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.1.0a2 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page