powergrid_synth.transmission

Transmission grid synthesis pipeline (CLC model).

Submodules

Package Contents

class powergrid_synth.transmission.BusTypeAllocator(graph, entropy_model=0, bus_type_ratio=None)[source]

Assigns bus types (Generator, Load, Connection) to a raw power grid topology using an Artificial Immune System (AIS) optimization algorithm.

The method, from Elyas and Wang (2016), exploits the observed non-trivial correlations between bus types and topology metrics (node degree, clustering coefficient) in realistic grids. A bus type entropy measure quantifies these correlations, and a target entropy \(W^*\) is derived from a scaling property fitted to real-world systems. The AIS then searches for an assignment whose entropy matches \(W^*\).

The pipeline is:

  1. Determine target bus type ratios \((r_G, r_L, r_C)\) from network size.

  2. Estimate \(W^*\) via Monte Carlo sampling of random assignments and the scaling relation \(W^* = \mu + \sigma \cdot d(N)\).

  3. Run AIS optimization (clonal selection, hypermutation, receptor editing) to find an assignment \(\mathbb{T}\) such that \(|W(\mathbb{T}) - W^*| < \epsilon\).

Two entropy definitions are supported:

  • Model 0 (\(W_0\)): standard entropy of bus/link type ratios. \(\mu\) is stable across network sizes.

  • Model 1 (\(W_1\)): generalized entropy weighted by \(N\) and \(M\). \(\mu\) grows with network size, giving better discrimination in large grids.

Parameters:
  • graph (nx.Graph) – NetworkX graph representing the grid topology (nodes and edges only; no bus type attributes required yet).

  • entropy_model (int) – Selects the entropy definition: 0 for \(W_0\), 1 for \(W_1\).

  • bus_type_ratio (list of float or None) – Optional target ratios [Gen, Load, Conn]. Values are normalized to sum to 1. If None, default ratios are chosen based on N.

References

_calculate_bus_ratios(assignment)[source]

Compute actual bus type ratios from an assignment vector.

Parameters:

assignment (np.ndarray) – Bus type vector of shape (N,) with values in {1, 2, 3}.

Returns:

[r_G, r_L, r_C] — actual ratios computed from the assignment.

Return type:

list of float

_calculate_entropy_score(bus_ratios, link_ratios)[source]

Compute the bus type entropy \(W\) for a given assignment.

Model 0 (\(W_0\)):

\[W_0 = -\sum_{k=1}^{3} r_k \ln r_k -\sum_{i,j=1}^{3} R_{ij} \ln R_{ij}\]

Model 1 (\(W_1\)):

\[W_1 = -\sum_{k=1}^{3} \ln(r_k) \cdot N_k -\sum_{i,j=1}^{3} \ln(R_{ij}) \cdot M_{ij}\]

where \(N_k = r_k \cdot N\) and \(M_{ij} = R_{ij} \cdot M\).

Parameters:
  • bus_ratios (list of float) – [r_G, r_L, r_C] — bus type ratios (should reflect the actual assignment, not necessarily the target ratios).

  • link_ratios (list of float) – [R_GG, R_LL, R_CC, R_GL, R_GC, R_LC].

Returns:

Entropy score \(W\).

Return type:

float

Compute ratios of the 6 link (edge) types for a given assignment.

Each edge is classified by the sorted pair of its endpoint types: GG (1,1), LL (2,2), CC (3,3), GL (1,2), GC (1,3), LC (2,3). The ratio is \(R_{ij} = M_{ij} / M\).

Parameters:

assignment (np.ndarray) – Bus type vector of shape (N,).

Returns:

[R_GG, R_LL, R_CC, R_GL, R_GC, R_LC].

Return type:

list of float

_estimate_w_star(monte_carlo_iters=2000)[source]

Estimate the target entropy \(W^*\) via Monte Carlo sampling.

Generates monte_carlo_iters random bus type assignments (with fixed target ratios), computes their entropies, and fits a normal distribution \(\mathcal{N}(\mu, \sigma^2)\). The target entropy is then:

\[W^* = \mu + \sigma \cdot d(N)\]

where \(d(N)\) is the piecewise scaling function from _get_d_parameter().

Parameters:

monte_carlo_iters (int, optional) – Number of random samples (default 2000).

Returns:

  • w_star (float) – Target entropy value.

  • std_w (float) – Standard deviation of the Monte Carlo entropy samples.

Return type:

Tuple[float, float]

_generate_random_assignment()[source]

Generate a random bus type assignment vector of length \(N\).

Types: 1 = Generator, 2 = Load, 3 = Connection. The counts are determined by self.ratio_types. Connection buses are preferentially placed on non-leaf nodes (degree > 1) to reflect the hub-like role of connection buses in realistic grids.

Returns:

Integer array of shape (N,) with values in {1, 2, 3}.

Return type:

np.ndarray

_get_d_parameter(n)[source]

Compute the normalized distance parameter \(d(N)\) for estimating the target entropy \(W^*\).

The piecewise scaling functions were fitted to realistic grids by Elyas and Wang (2016). For entropy model 0:

\[\begin{split}d_0(N) = \begin{cases} -1.39 \ln N + 6.79 & \text{if } \ln N \le 8 \\ -6.003 \times 10^{-14} (\ln N)^{15.48} & \text{if } \ln N > 8 \end{cases}\end{split}\]

For entropy model 1:

\[\begin{split}d_1(N) = \begin{cases} -1.748 \ln N + 8.576 & \text{if } \ln N \le 8 \\ -6.053 \times 10^{-22} (\ln N)^{24.1} & \text{if } \ln N > 8 \end{cases}\end{split}\]

Note

The \(d_0\) linear-segment coefficients here (-1.39, 6.79) follow the MATLAB SynGrid implementation and differ from the values in the paper (-1.721, 8).

Parameters:

n (int) – Network size.

Returns:

The distance parameter \(d(N)\).

Return type:

float

_get_ratios(n)[source]

Return default bus type ratios [Gen, Load, Conn] based on network size, derived from realistic reference systems.

  • \(N < 2000\)[0.23, 0.55, 0.22] (IEEE-300-like)

  • \(2000 \le N < 10000\)[0.33, 0.44, 0.23] (NYISO-like)

  • \(N \ge 10000\)[0.20, 0.40, 0.40] (WECC-like)

Parameters:

n (int) – Network size (number of buses).

Returns:

[r_G, r_L, r_C] summing to 1.0.

Return type:

list of float

_normalize_ratio(ratio)[source]

Normalize a user-supplied bus type ratio to sum to 1.0.

Parameters:

ratio (list of float) – Three values [Gen, Load, Conn].

Returns:

Normalized ratios summing to 1.0.

Return type:

list of float

Raises:

ValueError – If ratio does not contain exactly 3 positive values.

allocate(max_iter=100, population_size=20)[source]

Run the AIS optimization to find a bus type assignment.

Implements the Clonal Selection Principle:

  1. Monte Carlo estimation of \(W^*\) and convergence threshold \(\epsilon\) (see _estimate_w_star()).

  2. Initialize a population of K random assignments.

  3. Iterate until max_iter or best_error < epsilon:

    1. Evaluate fitness \(|W^* - W(\mathbb{T})|\) for each individual.

    2. Clonal selection — top-ranked individuals produce more clones: \(N_c = \text{round}(\beta \cdot s / r)\) where r is the rank.

    3. Hypermutation — clones are mutated with intensity proportional to their parent’s rank (worse → more mutations).

    4. Receptor editing — inject 10% fresh random solutions to maintain diversity and avoid local optima.

    5. Selection — combine elite, mutated clones, and fresh solutions; keep the top K.

  4. Return the best assignment found.

Parameters:
  • max_iter (int, optional) – Maximum number of AIS iterations (default 100).

  • population_size (int, optional) – Number of individuals surviving each generation (default 20).

Returns:

Mapping from node ID to bus type label ('Gen', 'Load', or 'Conn').

Return type:

dict[int, str]

plot_entropy_pdf(figsize=(10, 6))[source]

Plot the empirical PDF of entropy samples from the Monte Carlo estimation, with a fitted normal distribution overlay.

Must be called after allocate() or _estimate_w_star() so that self.w_samples is populated.

Parameters:

figsize (tuple of int, optional) – Figure size (width, height) in inches.

class powergrid_synth.transmission.CapacityAllocator(graph, ref_sys_id=1)[source]

Assigns generation capacities (PgMax) to generator buses in the grid.

Implements the three-stage methodology from Elyas et al. (2017):

  1. Total generation: Compute the aggregate generation capacity from a scaling law fitted to realistic grids: log Pg_tot(N) = -0.21 * log^2(N) + 2.06 * log(N) + 0.66.

  2. Individual capacities: Sample N_g individual capacities from an exponential distribution (with ~1% super-large outliers to capture the observed heavy tail).

  3. Correlated assignment: Assign capacities to generator buses using a 14x14 empirical 2D probability table (Tab_2D_Pgmax) that encodes the joint distribution of normalized capacity and normalized nodal degree. This preserves the observed Pearson correlation rho(P_bar, k_bar) in [0.25, 0.5] between capacity and degree.

Reference systems with pre-computed Tab_2D_Pgmax tables are available for NYISO-2935 (id=1), WECC-16944 (id=2), and a third system (id=3). A heuristic diagonal-bias table (id=0) is provided as a fallback.

Parameters:
  • graph (nx.Graph) – NetworkX graph with 'bus_type' node attributes already set (by BusTypeAllocator).

  • ref_sys_id (int) – Reference system for statistical tables (0=heuristic, 1=NYISO-2935, 2=WECC-16944, 3=additional reference).

References

_assignment_logic(norm_deg_pairs, norm_caps, tab_2d)[source]

Assign normalized capacities to buses via 2D-binning (Stage 3).

This implements the correlated assignment algorithm from Elyas et al. (2017) that preserves the empirical joint distribution of normalized capacity and normalized nodal degree. The steps are:

  1. Scale 2D table to counts: multiply tab_2d by N_g and round to integer targets n_rc for each (capacity-class r, degree-class c) cell. Adjust rounding so that the total equals N_g exactly.

  2. Compute marginals: column sums give degree-bin targets; row sums give capacity-bin targets.

  3. Bin by degree: sort generators by normalized degree, partition into 14 degree bins according to column-sum targets.

  4. Bin by capacity: sort normalized capacities, partition into 14 capacity bins according to row-sum targets.

  5. Nested assignment loop: for each degree bin c = 1..14 and each capacity bin r = 14..1 (high to low): randomly sample n_rc capacities from capacity bin r (without replacement) and assign them to unassigned generators in degree bin c.

  6. Reassemble all bins into the output array.

Parameters:
  • norm_deg_pairs (np.ndarray) – Shape (N_g, 2) — columns are [NodeID, NormalizedDegree].

  • norm_caps (np.ndarray) – Shape (N_g,) — normalized capacity values in [0, 1].

  • tab_2d (np.ndarray) – A 14x14 joint-probability matrix (rows=capacity classes, columns=degree classes).

Returns:

Shape (N_g, 3) — columns are [NodeID, NormalizedDegree, NormalizedCapacity].

Return type:

np.ndarray

_generate_heuristic_tab_2d()[source]

Generate a heuristic 14x14 joint-probability table as a fallback.

When no reference system data is selected (ref_sys_id=0), this method produces a synthetic table using a Gaussian-decay diagonal bias: weight(r, c) = exp(-0.5 * |r - c|), where rows represent capacity classes and columns represent degree classes (both ordered low-to-high). The matrix is normalized to sum to 1.

This simulates the positive correlation between nodal degree and generation capacity observed in realistic grids, without relying on empirical data from a specific reference system.

Returns:

A 14x14 probability matrix (sums to 1).

Return type:

np.ndarray

_get_default_tab_2d()[source]

Return the Tab_2D_Pgmax table for the selected reference system.

The table is a 14x14 matrix representing the empirical joint PDF Pr((P_bar_gn_max, k_bar_n) in A) discretized into 14 capacity classes (rows, low-to-high) and 14 nodal-degree classes (columns, low-to-high). Tables for real reference systems are loaded from reference_data.get_reference_stats() (ported from the MATLAB SynGrid file sg_refsys_stat.m).

Returns:

A 14x14 probability matrix.

Return type:

np.ndarray

_initial_generation_distribution(total_gen)[source]

Sample individual generation capacities from the empirical distribution.

Following Elyas et al. (2017), more than 99% of generation units in realistic grids follow an exponential distribution. About 1% have extremely large capacities that fall outside the exponential range.

The procedure is:

  1. Draw N_g samples from Exp(mu) where mu = total_gen / N_g.

  2. Replace ~1% of the samples with “super-large” values drawn uniformly from [max(P), 3 * max(P)].

  3. If the sum deviates more than 5% above or 10% below total_gen, rescale all values proportionally.

  4. Normalize by the maximum capacity.

Parameters:

total_gen (float) – Target total generation capacity (MW).

Returns:

  • p_caps (np.ndarray) – Raw (possibly rescaled) capacity values, shape (N_g,).

  • max_r_pgmax (float) – Maximum capacity value, used for normalization.

  • normalized_r_pgmax (np.ndarray) – Capacities normalized to [0, 1] by dividing by max_r_pgmax.

Return type:

Tuple[numpy.ndarray, float, numpy.ndarray]

allocate(tab_2d=None)[source]

Run the full generation-capacity allocation pipeline.

Executes the three-stage methodology from Elyas et al. (2017):

  1. Compute total generation capacity from the scaling law: Pg_tot = 10^(-0.21 * log10(N)^2 + 2.06 * log10(N) + 0.66).

  2. Sample individual capacities, normalize them and the generator nodal degrees by their respective maxima so that both lie in [0, 1]: P_bar = P / max(P), k_bar = k / max(k).

  3. Assign normalized capacities to generator buses via 2D binning using Tab_2D_Pgmax.

  4. Denormalize: Pg_max = P_bar * max(P).

Parameters:

tab_2d (np.ndarray or None, optional) – Custom 14x14 probability matrix. If None, the default table for the selected ref_sys_id is used.

Returns:

Mapping from generator node ID to its assigned capacity Pg_max (MW).

Return type:

dict[int, float]

class powergrid_synth.transmission.GenerationDispatcher(graph, ref_sys_id=1)[source]

Assign active-power dispatch to each generator bus.

The algorithm follows Sadeghian et al. (2018) (Section III):

  1. Uncommitted units (10–20 %): \(\alpha = 0\), selected via targets drawn from Uniform[0, 0.6].

  2. Partially committed units (40–50 %): selected via exponential distribution on capacity; dispatch factors assigned through a 2-D bin-matching table Tab_2D_Pg (\(14 \times 10\)).

  3. Fully committed units (remainder): \(\alpha = 1\).

  4. Balancing loop: iteratively adjusts dispatch to match total load within 1 % tolerance.

Parameters:
  • graph (networkx.Graph) – Power grid graph. Generator nodes must have 'bus_type' == 'Gen' and 'pg_max' (MW) attributes. Load nodes must have 'pl' (MW).

  • ref_sys_id (int, optional) – Reference system for statistical tables (1 = NYISO-2935, 2 = WECC-16994, 3 = additional reference). Default is 1.

alpha_mod

Loading-level flag from the reference system. When 0 all alphas are Uniform[0, 1]; otherwise 0.5 % receive negative dispatch (e.g., pumped-storage hydro).

Type:

int

mu_committed

Exponential-distribution parameter for committed-unit capacities.

Type:

float

tab_2d_pg

2-D empirical PMF table (14 capacity bins × 10 alpha bins).

Type:

numpy.ndarray

_assign_alphas(units, alphas)[source]

Assign dispatch factors to committed units via 2-D bin matching.

Units are sorted by normalised capacity and alphas by value, then distributed into bins defined by Tab_2D_Pg (14 capacity bins \(\times\) 10 alpha bins). Within each bin, units and alphas are paired randomly (high-to-low bin traversal). Any leftovers are paired sequentially as a fallback.

This reproduces the empirical joint distribution \(f(\bar{P}_{g}^{\max}, \alpha)\) from the reference system (Sadeghian et al., 2018, Table I).

Parameters:
  • units (numpy.ndarray, shape (n, 2)) – [bus_id, normalised_capacity].

  • alphas (numpy.ndarray, shape (n, 1)) – Dispatch-factor values from _generate_alphas().

Returns:

[bus_id, normalised_capacity, alpha].

Return type:

numpy.ndarray, shape (m, 3)

_generate_alphas(n_comm)[source]

Generate dispatch factors for partially committed units.

When alpha_mod == 0 (e.g. NYISO), all \(\alpha\) values are drawn from Uniform[0, 1]. When alpha_mod != 0 (e.g. WECC), 99.5 % are Uniform[0, 1] and 0.5 % are negative, representing reverse dispatch such as pumped-storage hydro.

Parameters:

n_comm (int) – Number of committed units requiring \(\alpha\) values.

Returns:

Dispatch-factor values.

Return type:

numpy.ndarray, shape (n_comm, 1)

_select_committed(norm_pg_max, total_units_count)[source]

Select generators to be partially committed (\(0 < \alpha < 1\)).

Selects 40–50 % of total generator count. 99 % of these are chosen by matching to targets drawn from an exponential distribution with parameter \(\mu_{\text{committed}}\); the remaining 1 % are drawn from the extreme tail Uniform[0.5, 1.0], capturing super-large units (Sadeghian et al., 2018, Sec. III-A).

Parameters:
  • norm_pg_max (numpy.ndarray, shape (n, 2)) – Remaining units after uncommitted selection: [bus_id, normalised_capacity].

  • total_units_count (int) – Original total number of generator units (before any selection).

Returns:

  • committed (numpy.ndarray, shape (m, 2)) – Committed units: [bus_id, norm_cap].

  • remaining (numpy.ndarray, shape (n-m, 2)) – Units not selected (will become fully committed).

Return type:

Tuple[numpy.ndarray, numpy.ndarray]

_select_uncommitted(norm_pg_max)[source]

Select generators to be uncommitted (\(\alpha = 0\)).

Randomly selects 10–20 % of total generator units. Target capacities are drawn from Uniform[0, 0.6] and the unit whose normalised capacity is closest to each target is selected. This reproduces the empirical observation that uncommitted units tend to be small or medium-size (Sadeghian et al., 2018, Sec. III).

Parameters:

norm_pg_max (numpy.ndarray, shape (n, 2)) – Array with columns [bus_id, normalised_capacity].

Returns:

  • uncommitted (numpy.ndarray, shape (m, 3)) – Uncommitted units: [bus_id, norm_cap, alpha=0].

  • remaining (numpy.ndarray, shape (n-m, 2)) – Units not selected.

Return type:

Tuple[numpy.ndarray, numpy.ndarray]

dispatch()[source]

Run the full generation dispatch pipeline.

Implements the algorithm of Sadeghian et al. (2018):

  1. Collect generator buses and normalise capacities by \(P^{\max}_{g_{\max}}\).

  2. Partition generators into uncommitted (\(\alpha = 0\)), partially committed (\(0 < \alpha < 1\)), and fully committed (\(\alpha = 1\)).

  3. Assign dispatch factors to partially committed units via the 2-D bin-matching table Tab_2D_Pg.

  4. Iteratively balance total generation against total load (1 % tolerance, up to 50 iterations) by scaling committed \(\alpha\) values and toggling uncommitted / full-load units on or off.

  5. Convert normalised dispatch back to MW: \(P_{g_i} = \alpha_i \cdot \bar{P}_{g_i}^{\max} \cdot P^{\max}_{g_{\max}}\).

Returns:

Mapping of generator bus ID to dispatched active power (MW).

Return type:

dict

class powergrid_synth.transmission.InputConfigurator(seed=None)[source]

Generate detailed input sequences for PowerGridGenerator from high-level parameters.

This is “operation mode II”, where the user specifies only the number of nodes, average degree, diameter, and distribution type for each voltage level, rather than providing explicit degree sequences. The configurator uses DegreeDistributionOptimizer to fit distribution parameters and then samples degree sequences.

See Section 6 of Aksoy et al. (2018) for the synthetic input generation guidelines.

Parameters:

seed (int or None, optional) – Random seed for reproducibility. Default is None.

_generate_optimized_degrees(n_nodes, avg_degree, max_degree, dist_type)[source]

Generate a degree sequence by optimizing DGLN or DPL parameters.

First calls DegreeDistributionOptimizer.optimize() to find the distribution parameters matching avg_degree and max_degree, then samples n_nodes degrees from the resulting PDF.

Parameters:
  • n_nodes (int) – Number of nodes in the same-voltage subgraph.

  • avg_degree (float) – Target average degree \(\bar{d}\).

  • max_degree (int) – Maximum degree \(d_{\max}\) (PDF support is 1..max_degree).

  • dist_type (str) – 'dgln' for generalized log-normal or 'dpl' for power law.

Returns:

Sampled degree sequence of length n_nodes.

Return type:

list of int

_generate_poisson_degrees(n_nodes, avg_degree)[source]

Generate a degree sequence from a Poisson distribution.

Parameters:
  • n_nodes (int) – Number of nodes.

  • avg_degree (float) – Mean of the Poisson distribution \(\lambda = \bar{d}\).

Returns:

Sampled degree sequence of length n_nodes.

Return type:

list of int

_generate_transformer_simple(n_nodes, prob)[source]

Generate binary transformer degrees (simple probabilistic model).

Each node independently gets transformer degree 0 or 1 with probability 1 - prob and prob, respectively.

Parameters:
  • n_nodes (int) – Number of nodes.

  • prob (float) – Probability that a node participates in a transformer edge.

Returns:

Transformer degree list (0 or 1 per node).

Return type:

list of int

_generate_transformer_stars(n_i, n_j, c=0.174, gamma=4.15)[source]

Generate transformer degrees using the disjoint k-stars model.

Follows Section 6.2 of Aksoy et al. (2018):

  1. Number of star centres: \(h(n_i, n_j) = c \cdot \min(n_i, n_j)\) with \(c \approx 0.174\).

  2. Star sizes sampled from a discrete power law \(P(k) \propto k^{-\gamma}\) with \(\gamma \approx 4.15\).

  3. Each star is randomly assigned a centre in level i or j.

  4. Degree lists are padded with zeros and shuffled.

Parameters:
  • n_i (int) – Number of nodes in the two voltage levels.

  • n_j (int) – Number of nodes in the two voltage levels.

  • c (float, optional) – Coefficient for the number of active star centres. Default is 0.174 (paper optimum).

  • gamma (float, optional) – Power-law exponent for star sizes. Default is 4.15 (paper optimum, Section 6.2).

Returns:

(t_i, t_j) — transformer degree sequences for levels i and j, each of length n_i and n_j respectively.

Return type:

tuple of (list of int, list of int)

create_params(levels, inter_connections)[source]

Generate the full parameter set for PowerGridGenerator.generate_grid().

Parameters:
  • levels (list of dict) –

    One dict per voltage level with keys:

    • 'n' (int): number of nodes.

    • 'avg_k' (float): target average degree.

    • 'diam' (int): target diameter.

    • 'dist_type' (str): 'dgln', 'dpl', or 'poisson'.

    • 'max_k' (int, optional): maximum degree (default: min(n - 1, 50)). For 'dpl' the paper suggests \(d_{\max} \approx 1.517\,n^{1/4}\); for 'dgln' \(\bar{d} \approx 2.425 \pm 0.185\) is consistent across subgraphs (Section 6.1 of Aksoy et al., 2018).

  • inter_connections (dict) –

    Mapping (i, j) -> config for transformer connections. Config is either:

    • {'type': 'k-stars', 'c': 0.174, 'gamma': 4.15}

    • {'type': 'simple', 'p_i_j': float, 'p_j_i': float}

Returns:

Keys 'degrees_by_level', 'diameters_by_level', and 'transformer_degrees', ready to be unpacked into PowerGridGenerator.generate_grid().

Return type:

dict

class powergrid_synth.transmission.LoadAllocator(graph, ref_sys_id=1)[source]

Assigns active power loads (PL) to load buses in the grid.

Implements the load-setting methodology from Elyas et al. (2017), which mirrors the generation-capacity approach in CapacityAllocator:

  1. Total load: Compute an aggregate load target, either from a deterministic scaling formula or as a fraction (light / medium / heavy) of the total installed generation capacity.

  2. Individual loads: Sample N_l individual loads from an exponential distribution (with ~1% super-large outliers).

  3. Correlated assignment: Assign loads to load buses using a 14x14 empirical 2D probability table (Tab_2D_load) that encodes the joint distribution of normalized load demand and normalized nodal degree.

Reference systems with pre-computed Tab_2D_load tables are available for NYISO-2935 (id=1), WECC-16944 (id=2), and a third system (id=3). A heuristic diagonal-bias table (id=0) is provided as a fallback.

Parameters:
  • graph (nx.Graph) – NetworkX graph with 'bus_type' and (for non-deterministic loading levels) 'pg_max' node attributes already set.

  • ref_sys_id (int) – Reference system for statistical tables (0=heuristic, 1=NYISO-2935, 2=WECC-16944, 3=additional reference).

References

_assignment_logic(norm_deg_pairs, norm_vals, tab_2d)[source]

Assign normalized values to buses via 2D-binning.

Shared logic with CapacityAllocator._assignment_logic(). See that method’s docstring for the full algorithm description. In brief:

  1. Scale the 14x14 probability table to integer target counts.

  2. Derive degree-bin (column sums) and value-bin (row sums) targets.

  3. Sort buses by normalized degree, partition into 14 bins.

  4. Sort normalized values, partition into 14 bins.

  5. For each (degree bin, value bin) pair—iterating degree bins 1→14, value bins 14→1—randomly assign the target count of values to unassigned buses.

Parameters:
  • norm_deg_pairs (np.ndarray) – Shape (N, 2) — columns are [NodeID, NormalizedDegree].

  • norm_vals (np.ndarray) – Shape (N,) — normalized load values in [0, 1].

  • tab_2d (np.ndarray) – A 14x14 joint-probability matrix (rows=value classes, columns=degree classes).

Returns:

Shape (N, 3) — columns are [NodeID, NormalizedDegree, NormalizedValue].

Return type:

np.ndarray

_calculate_total_load(loading_level, total_gen_capacity)[source]

Compute the total system load target.

Four strategies are supported, following Elyas et al. (2017):

  • 'D'Deterministic: scaling formula fitted to realistic grids: Pl_tot = 10^(-0.2 * log10(N)^2 + 1.98 * log10(N) + 0.58).

  • 'L'Light loading: 30–40% of total generation capacity.

  • 'M'Medium loading: 50–60% of total generation capacity.

  • 'H'Heavy loading: 70–80% of total generation capacity.

Parameters:
  • loading_level (str) – One of 'D', 'L', 'M', 'H'.

  • total_gen_capacity (float) – Sum of all generator pg_max values (MW). Only used when loading_level is not 'D'.

Returns:

Total system load target (MW).

Return type:

float

Raises:

ValueError – If loading_level is not one of the four valid options.

_get_tab_2d_load()[source]

Return the Tab_2D_load table for the selected reference system.

The table is a 14x14 matrix representing the empirical joint PDF Pr((P_bar_ln, k_bar_n) in A) discretized into 14 load-demand classes (rows, low-to-high) and 14 nodal-degree classes (columns, low-to-high). When ref_sys_id=0, a heuristic table with Gaussian-decay diagonal bias exp(-0.5 * |r - c|) is generated instead.

Returns:

A 14x14 probability matrix (sums to 1).

Return type:

np.ndarray

_initial_load_distribution(total_load)[source]

Sample individual load demands from the empirical distribution.

Mirrors CapacityAllocator._initial_generation_distribution(). More than 99% of load demands follow an exponential distribution; ~1% are replaced by super-large outliers drawn uniformly from [max(P), 3 * max(P)]. If the sum deviates more than 5% above or 10% below total_load, all values are rescaled proportionally.

Parameters:

total_load (float) – Target aggregate load (MW).

Returns:

  • p_loads (np.ndarray) – Raw (possibly rescaled) load values, shape (N_l,).

  • max_r_pl (float) – Maximum load value, used for normalization.

  • normalized_r_pl (np.ndarray) – Loads normalized to [0, 1] by dividing by max_r_pl.

Return type:

Tuple[numpy.ndarray, float, numpy.ndarray]

allocate(loading_level='H')[source]

Run the full load-allocation pipeline.

Executes the three-stage methodology (analogous to CapacityAllocator.allocate()):

  1. Compute total system load from loading_level strategy.

  2. Sample individual loads, normalize them and the load-bus nodal degrees by their respective maxima: P_bar = P / max(P), k_bar = k / max(k).

  3. Assign normalized loads to load buses via 2D binning using Tab_2D_load.

  4. Denormalize: PL = P_bar * max(P).

Parameters:

loading_level (str, optional) –

Loading strategy (default 'H'):

  • 'D' — deterministic scaling formula.

  • 'L' — light (30–40% of generation capacity).

  • 'M' — medium (50–60% of generation capacity).

  • 'H' — heavy (70–80% of generation capacity).

Returns:

Mapping from load node ID to its assigned active power load (MW).

Return type:

dict[int, float]

allocate_reactive(active_loads, pf_min=0.85, pf_max=0.97)[source]

Derive reactive power loads from active loads using power factors.

For each load bus, a power factor is sampled uniformly from [pf_min, pf_max] (lagging) and the reactive load is computed as ql = pl * tan(arccos(pf)).

Theory reference: https://www.phasetophase.nl/book/book_2_9.html#_9.5.2

Parameters:
  • active_loads (dict[int, float]) – Mapping from load node ID to active power load (MW), as returned by allocate().

  • pf_min (float, optional) – Minimum power factor (default 0.85).

  • pf_max (float, optional) – Maximum power factor (default 0.97).

Returns:

Mapping from load node ID to reactive power load (Mvar).

Return type:

dict[int, float]

class powergrid_synth.transmission.PowerGridGenerator(seed=None)[source]

Generative model for an entire power grid graph on k voltage levels.

Implements Algorithm 4 (CLCStars) from Aksoy et al. (2018). Phase 1 generates each same-voltage subgraph via the CLC model, and Phase 2 inserts transformer edges via the random-star model.

Parameters:

seed (int or None, optional) – Random seed for reproducibility. Default is None.

_generate_subgraphs(k, degrees_by_level, diameters_by_level, level_offsets, level_node_counts, all_edges)[source]

Generate same-voltage subgraphs (Phase 1) for all k levels.

For each voltage level, runs Preprocessor.run_setup() followed by EdgeCreator.generate_edges(), converting local node IDs to global IDs using cumulative offsets.

Parameters:
  • k (int) – Number of voltage levels.

  • degrees_by_level (list of list of int) – Desired degree sequences, one per voltage level.

  • diameters_by_level (list of int) – Desired diameters, one per voltage level.

  • level_offsets (list of int) – Mutated in place — filled with the global node-ID offset for each level.

  • level_node_counts (list of int) – Mutated in place — filled with the actual node count (after degree-sequence inflation) for each level.

  • all_edges (dict) – Mutated in place{(u, v): {'type': 'line'}} entries are added for each generated edge.

Example

1# Level 0 generated (5 nodes) -> Local IDs: 0, 1, 2, 3, 4
2level_offsets[0] = 0
3current_global_offset becomes 5
4
5# Level 1 generated (3 nodes) -> Local IDs: 0, 1, 2
6# We shift them by current_global_offset (5):
7# Global IDs: 5, 6, 7
8level_offsets[1] = 5
9current_global_offset becomes 8
_generate_transformer_connections(k, transformer_degrees, level_node_counts, level_offsets, all_edges)[source]

Insert transformer edges between voltage levels (Phase 2).

For each pair of levels (i, j), runs TransformerConnector.generate_transformer_edges() and converts local node IDs to global IDs.

Parameters:
  • k (int) – Number of voltage levels.

  • transformer_degrees (dict) – Mapping (i, j) -> (t_i_j, t_j_i) of transformer degree lists.

  • level_node_counts (list of int) – Actual node counts per level (from Phase 1).

  • level_offsets (list of int) – Global node-ID offsets per level (from Phase 1).

  • all_edges (dict) – Mutated in place{(u, v): {'type': 'transformer'}} entries are added.

generate_grid(degrees_by_level, diameters_by_level, transformer_degrees, keep_lcc=False)[source]

Generate a multi-level power grid graph (CLCStars, Algorithm 4).

Runs Phase 1 (CLC for each voltage level) and Phase 2 (random-star transformer edges for each pair of levels), then combines results.

Parameters:
  • degrees_by_level (list of list of int) – Desired degree sequences \(\mathbf{d}^{X_1},\dots,\mathbf{d}^{X_k}\), one per voltage level.

  • diameters_by_level (list of int) – Desired diameters \(\delta^{X_1},\dots,\delta^{X_k}\), one per voltage level.

  • transformer_degrees (dict) – Mapping (i, j) -> (t_i_j, t_j_i) where t_i_j is the transformer degree list for level-i nodes toward level j, and t_j_i is the reverse.

  • keep_lcc (bool, optional) – If True, return only the largest connected component with contiguous node IDs. Default is False.

Returns:

The generated grid graph with voltage_level node attributes and type ('line' or 'transformer') edge attributes.

Return type:

PowerGridGraph

class powergrid_synth.transmission.TransmissionLineAllocator(graph, ref_sys_id=1)[source]

Allocate impedance and capacity limits to transmission lines.

The algorithm follows Sadeghian et al. (2018) and the SynGrid MATLAB toolbox (sg_line.m, sg_flow_lim.m):

  1. Impedance generation — magnitudes \(Z\) from LogNormal(\(\mu\), \(\sigma\)), angles \(\varphi\) from a Lévy stable distribution \(S(\alpha_s, \beta_s, \gamma_s, \delta_s)\). Then \(X = Z \sin\varphi\), \(R = Z \cos\varphi\).

  2. DCPF-based swapping — sort impedances ascending and flows descending, then randomly swap ~20–30 % of assignments to introduce variance while preserving the negative correlation between impedance and flow.

  3. (Optional) Topology refinement — iteratively add low-impedance lines between max angle-difference bus pairs and remove weak high-\(X\) lines until the angle spread is below a size-dependent threshold.

  4. Capacity assignment — gauge ratios \(\beta_l = F_l / F_l^{\max}\) from Exponential(\(\mu_\beta\)) with overload injection; assigned via 2-D table Tab_2D_FlBeta. Capacity limits: \(F_l^{\max} = F_l / \beta_l\).

Parameters:
  • graph (networkx.Graph) – Power grid graph with nodal generation/load attributes.

  • ref_sys_id (int, optional) – Reference system (1 = NYISO-2935, 2 = WECC-16994). Default 1.

stab_params

Lévy stable parameters \([\alpha_s, \beta_s, \gamma_s, \delta_s]\).

Type:

list of float

tab_fl_beta

2-D empirical PMF table for flow–beta assignment.

Type:

numpy.ndarray

mu_beta

Mean of the exponential distribution for \(\beta\).

Type:

float

overload_b

Fraction of lines assigned overload (\(\beta > 1\)).

Type:

float

_assign_betas(flows, betas)[source]

Assign gauge ratios to lines via 2-D bin matching.

Uses the empirical PMF Tab_2D_FlBeta to reproduce the joint distribution \(f(\bar{F}_l, \beta_l)\) from the reference system. Lines are sorted by normalised flow and betas by value, then paired randomly within matching bins (high-to-low traversal).

Parameters:
  • flows (numpy.ndarray, shape (m, 2)) – [line_index, normalised_flow].

  • betas (numpy.ndarray, shape (m,)) – Sorted gauge-ratio values from _generate_beta().

Returns:

[line_index, normalised_flow, beta].

Return type:

numpy.ndarray, shape (m, 3)

_generate_beta(num_lines)[source]

Generate gauge ratios from an exponential distribution.

Draws \(\beta \sim \mathrm{Exp}(\mu_\beta)\) and resamples values exceeding 1.0. A fraction overload_b of lines are then injected with \(\beta \in (1.0, 1.2]\) to model bottleneck / overloaded lines (Sadeghian et al., 2018, Sec. IV).

Parameters:

num_lines (int) – Number of transmission lines.

Returns:

Sorted gauge-ratio values.

Return type:

numpy.ndarray, shape (num_lines,)

_generate_phi(num_lines)[source]

Generate line angles from a Lévy stable distribution.

Draws \(\varphi \sim S(\alpha_s, \beta_s, \gamma_s, \delta_s)\) and clips to \([0.01, 89.99]\) degrees. Out-of-range samples are resampled up to 20 times before a hard clip.

Parameters:

num_lines (int) – Number of transmission lines.

Returns:

Line angle in degrees for each branch.

Return type:

numpy.ndarray, shape (num_lines,)

_refine_topology()[source]

Refine grid topology to reduce phase-angle spread.

Iteratively tightens the electrical diameter of the network (ported from sg_flow_lim.m):

  1. Compute the DCPF and measure \(\Delta\theta_{\max} = \max(\theta) - \min(\theta)\).

  2. If \(\Delta\theta_{\max} > TT + 2\) with \(TT = 10^{0.3196 \log_{10} N + 0.8324}\), add a low-impedance edge between the bus pair with the largest angle difference.

  3. Remove a random high-\(X\) edge (top 20 %) whose end-point degrees are both \(\ge 3\), preserving graph connectivity.

  4. Repeat for up to 10 iterations.

allocate(refine_topology=False)[source]

Run the full transmission-line allocation pipeline.

Executes the seven-step procedure:

  1. Draw impedance magnitudes \(Z \sim \text{LogNormal}(\mu, \sigma)\), clipped to [0.001, 0.5] p.u.

  2. Generate angles \(\varphi\) (Lévy stable), compute \(X = Z\sin\varphi\), \(R = Z\cos\varphi\).

  3. Iterative DCPF swapping: sort \(Z\) ascending / flows descending, randomly swap ~20–30 % of assignments.

  4. (Optional) Topology refinement via _refine_topology().

  5. Final DCPF to obtain converged flows.

  6. Generate and assign gauge ratios (\(\beta\)) via _generate_beta() and _assign_betas().

  7. Set capacity limits: \(F_l^{\max} = F_l / \beta_l\) with a minimum-capacity fallback (5 + 100 · rand MW when \(\le 2\)).

Parameters:

refine_topology (bool, optional) – If True, run topology refinement after step 3. Default is False.

Returns:

Mapping (u, v) edge tuple to capacity limit (MW).

Return type:

dict