Skip to content

simplenet.generators

Move external generators to their nearest internal bus.

Port of matlab/NetworkReduction2/MoveExGen.m. The MATLAB code builds a sparse network from the "second" reduction (where only the non-generator externals are eliminated) and runs a layered shortest electrical-distance search to attach every external generator to its closest retained bus. We achieve the same result by collapsing parallel lines and running multi-source Dijkstra (scipy.sparse.csgraph).

Inputs to :func:move_external_generators:

case_with_gens The :class:PowerCase that resulted from the second reduction - its bus set is internal U external-with-generator. internal_bus_ids Bus IDs (original numbering) that are truly internal (retained) and serve as the multi-source set. ac_flag If True use |z| = sqrt(r^2 + x^2) as the edge weight, else |x|. The MATLAB reduction_test.m always passes 0, but we expose it for flexibility.

GenMoveResult dataclass

GenMoveResult(new_gen_bus: ndarray, link: ndarray, islanded: ndarray)

Output of :func:move_external_generators.

Attributes:

Name Type Description
new_gen_bus ndarray

Array of length case.n_gen() with the post-move bus id for every generator (in the case's original bus numbering).

link ndarray

2-D array of shape (case.n_bus(), 2) whose first column is the original bus id and whose second column is the bus id that bus is "mapped to" (self for internal buses; nearest internal for external-with-generator buses).

islanded ndarray

Bus IDs that could not reach any internal bus; their generators are dropped from new_gen_bus (mapped to None via a sentinel of -1).

move_external_generators

move_external_generators(case_with_gens: PowerCase, internal_bus_ids: ndarray, *, ac_flag: bool = False) -> GenMoveResult

Find the closest internal bus for every external generator.

Parameters:

Name Type Description Default
case_with_gens PowerCase

The :class:PowerCase that resulted from the second Kron reduction. Its bus set must contain every internal bus plus every external bus that hosts a generator.

required
internal_bus_ids ndarray

Original-numbered bus IDs that should be treated as retained (multi-source set for the Dijkstra search).

required
ac_flag bool

When True use sqrt(r^2 + x^2) as the edge weight. When False (default, matching reduction_test.m) use |x|.

False

Returns:

Type Description
GenMoveResult

new_gen_bus field is suitable for use as the first column of the reduced case's gen matrix.

Source code in src/simplenet/generators.py
def move_external_generators(
    case_with_gens: PowerCase,
    internal_bus_ids: np.ndarray,
    *,
    ac_flag: bool = False,
) -> GenMoveResult:
    """Find the closest internal bus for every external generator.

    Parameters
    ----------
    case_with_gens
        The :class:`PowerCase` that resulted from the *second* Kron
        reduction. Its bus set must contain every internal bus plus
        every external bus that hosts a generator.
    internal_bus_ids
        Original-numbered bus IDs that should be treated as
        retained (multi-source set for the Dijkstra search).
    ac_flag
        When ``True`` use ``sqrt(r^2 + x^2)`` as the edge weight.
        When ``False`` (default, matching ``reduction_test.m``)
        use ``|x|``.

    Returns
    -------
    GenMoveResult
        ``new_gen_bus`` field is suitable for use as the first column
        of the reduced case's ``gen`` matrix.
    """

    bus_ids = case_with_gens.bus[:, 0].astype(np.int64, copy=False)
    n = bus_ids.size
    id_to_idx = {int(b): i for i, b in enumerate(bus_ids)}

    internal_bus_ids = np.asarray(internal_bus_ids).astype(np.int64, copy=False).ravel()
    internal_idx = np.array(
        [id_to_idx[int(b)] for b in internal_bus_ids if int(b) in id_to_idx],
        dtype=np.int64,
    )

    if case_with_gens.n_branch():
        if ac_flag:
            r = case_with_gens.branch[:, BR_R]
            x = case_with_gens.branch[:, BR_X]
            weight = np.sqrt(r * r + x * x)
        else:
            weight = np.abs(case_with_gens.branch[:, BR_X])
        keys, w = _collapse_parallel_lines(case_with_gens.branch, weight)
    else:
        keys = np.zeros((0, 2), dtype=np.int64)
        w = np.zeros(0)

    rows: list[int] = []
    cols: list[int] = []
    data: list[float] = []
    for (a_id, b_id), wt in zip(keys, w, strict=False):
        if not np.isfinite(wt):
            continue
        if int(a_id) not in id_to_idx or int(b_id) not in id_to_idx:
            continue
        ai = id_to_idx[int(a_id)]
        bi = id_to_idx[int(b_id)]
        if ai == bi:
            continue
        rows.append(ai)
        cols.append(bi)
        data.append(float(wt))
        rows.append(bi)
        cols.append(ai)
        data.append(float(wt))

    graph = sp.csr_matrix((data, (rows, cols)), shape=(n, n)) if rows else sp.csr_matrix((n, n))

    if internal_idx.size and graph.nnz:
        dist, _pred, sources = csg.dijkstra(
            graph,
            indices=internal_idx,
            return_predecessors=True,
            directed=False,
            min_only=True,
        )
    elif internal_idx.size:
        dist = np.full(n, np.inf)
        dist[internal_idx] = 0.0
        sources = np.full(n, -9999, dtype=np.int64)
        sources[internal_idx] = internal_idx
    else:
        dist = np.full(n, np.inf)
        sources = np.full(n, -9999, dtype=np.int64)

    link = np.empty((n, 2), dtype=np.int64)
    link[:, 0] = bus_ids
    islanded: list[int] = []
    for i in range(n):
        if np.isinf(dist[i]):
            link[i, 1] = -1
            islanded.append(int(bus_ids[i]))
        else:
            link[i, 1] = int(bus_ids[int(sources[i])])

    if case_with_gens.n_gen():
        gen_bus = case_with_gens.gen[:, 0].astype(np.int64, copy=False)
        bus_pos = {int(b): i for i, b in enumerate(bus_ids)}
        new_gen_bus = np.empty(gen_bus.size, dtype=np.int64)
        for i, gb in enumerate(gen_bus):
            pos = bus_pos.get(int(gb))
            new_gen_bus[i] = -1 if pos is None else int(link[pos, 1])
    else:
        new_gen_bus = np.zeros(0, dtype=np.int64)

    return GenMoveResult(new_gen_bus=new_gen_bus, link=link, islanded=np.asarray(islanded, dtype=np.int64))