Skip to content

LightSim2grid

This module provides the interface between a Network and lightsim2grid, used when settings.pf_solver is lightsim2grid. See Power flow solver.

convert_net

Convert a gridfm_datakit Network to lightsim2grid.

Parameters:

Name Type Description Default
network Network

The network to convert.

required

Returns:

Type Description
LoadedNetwork

The lightsim2grid LSGrid, the network itself and the index maps between the two.

Source code in gridfm_datakit/lightsim2grid/__init__.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
def convert_net(network: Network) -> LoadedNetwork:
    """Convert a gridfm_datakit Network to lightsim2grid.

    Args:
        network: The network to convert.

    Returns:
        The lightsim2grid LSGrid, the network itself and the index maps between the two.
    """
    conv = to_lightsim2grid(network)
    return LoadedNetwork(
        ls_net=conv.ls_net,
        gfm_net=network,
        mapping_l2g=conv.mapping_l2g,
    )

to_lightsim2grid

Build a lightsim2grid LSGrid from the current state of net.

Loads, generator set points, statuses and branch parameters are all read from net.buses / net.gens / net.branches, so the result is valid for one perturbed copy of the network only. Use :func:update_lightsim2grid to re-synchronise it with another one.

Parameters:

Name Type Description Default
net Network

The network to convert.

required

Returns:

Type Description
ConvertedNetwork

The LSGrid, its index maps to net and a snapshot of what it was built from.

Raises:

Type Description
ImportError

If lightsim2grid is not available.

Source code in gridfm_datakit/lightsim2grid/convert.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def to_lightsim2grid(net: Network) -> ConvertedNetwork:
    """Build a lightsim2grid LSGrid from the *current* state of ``net``.

    Loads, generator set points, statuses and branch parameters are all read
    from ``net.buses`` / ``net.gens`` / ``net.branches``, so the result is
    valid for one perturbed copy of the network only. Use
    :func:`update_lightsim2grid` to re-synchronise it with another one.

    Args:
        net: The network to convert.

    Returns:
        The LSGrid, its index maps to ``net`` and a snapshot of what it was built from.

    Raises:
        ImportError: If lightsim2grid is not available.
    """
    check_lightsim2grid_available()
    mpc = {
        "bus": net.buses,
        "gen": net.gens,
        "branch": net.branches,
        "baseMVA": float(net.baseMVA),
    }
    with warnings.catch_warnings():
        # e.g. BASE_KV == 0 everywhere: voltages are then reported in pu, which is what we want
        warnings.simplefilter("ignore")
        ls_net = lightsim2grid_network.init_from_matpower(mpc)
    _use_klu(ls_net)

    mapping = build_l2g_maps(net)
    assert len(ls_net.get_lines()) == len(mapping.line_rows)
    assert len(ls_net.get_trafos()) == len(mapping.trafo_rows)
    assert len(ls_net.get_generators()) == net.gens.shape[0]
    assert ls_net.total_bus() == net.buses.shape[0]
    return ConvertedNetwork(
        ls_net=ls_net,
        mapping_l2g=mapping,
        state=_snapshot(net),
    )

update_lightsim2grid

Return a lightsim2grid model in sync with net.

With no converted the LSGrid is built from scratch. Otherwise the LSGrid of converted is updated in place with what changed since it was last synchronised (branch parameters and statuses, generator statuses and set points, loads), which lets lightsim2grid keep its solver caches. It is rebuilt only if something it cannot update changed (topology, taps and shifts, shunts, which buses have a load, the slack generators). converted is modified and returned.

Parameters:

Name Type Description Default
net Network

The network the LSGrid has to match.

required
converted Optional[ConvertedNetwork]

The result of a previous call, or None to build the LSGrid.

None

Returns:

Type Description
ConvertedNetwork

converted updated in place, or a new :class:ConvertedNetwork if the LSGrid

ConvertedNetwork

had to be built or rebuilt.

Source code in gridfm_datakit/lightsim2grid/convert.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
def update_lightsim2grid(
    net: Network,
    converted: Optional[ConvertedNetwork] = None,
) -> ConvertedNetwork:
    """Return a lightsim2grid model in sync with ``net``.

    With no ``converted`` the LSGrid is built from scratch. Otherwise the
    LSGrid of ``converted`` is updated *in place* with what changed since it
    was last synchronised (branch parameters and statuses, generator statuses
    and set points, loads), which lets lightsim2grid keep its solver caches.
    It is rebuilt only if something it cannot update changed (topology, taps
    and shifts, shunts, which buses have a load, the slack
    generators). ``converted`` is modified and returned.

    Args:
        net: The network the LSGrid has to match.
        converted: The result of a previous call, or None to build the LSGrid.

    Returns:
        ``converted`` updated in place, or a new :class:`ConvertedNetwork` if the LSGrid
        had to be built or rebuilt.
    """
    if converted is None or not _same_structure(net, converted.state):
        return to_lightsim2grid(net)

    ls_net, mapping, old = converted.ls_net, converted.mapping_l2g, converted.state
    try:
        _update_in_place(ls_net, net, mapping, old)
    except AttributeError:
        # a released lightsim2grid without update_powerlines_parameters /
        # update_trafos_parameters: half-updated, nothing about the LSGrid can
        # be trusted anymore, fall back to rebuilding it from scratch
        return to_lightsim2grid(net)
    return converted

run_ls_pf

Run an AC (or DC) power flow with lightsim2grid and format the results.

The power flow starts from the voltages stored in net.buses (see :func:gridfm_datakit.lightsim2grid.convert.initial_voltage).

Parameters:

Name Type Description Default
ls_net Any

The lightsim2grid LSGrid, in sync with net.

required
net Network

The network the LSGrid was built from or updated with.

required
mapping_l2g MappingL2G

Index maps between net and ls_net.

required
dc bool

Run a DC power flow instead of an AC one.

False
max_iter int

Maximum number of iterations.

50
tol float

Convergence tolerance.

1e-08
as_arrays bool

Return the results as dense arrays under ["solution"]["arrays"] instead of per-element dicts (see :func:get_pf_res).

False

Returns:

Type Description
Dict[str, Any]

The power flow results in PowerModels' format (see :func:get_pf_res), with the

Dict[str, Any]

solving time in "solve_time".

Raises:

Type Description
ValueError

If the power flow did not converge.

Source code in gridfm_datakit/lightsim2grid/preprocess.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
def run_ls_pf(
    ls_net: Any,
    net: Network,
    mapping_l2g: MappingL2G,
    dc: bool = False,
    max_iter: int = 50,
    tol: float = 1e-8,
    as_arrays: bool = False,
) -> Dict[str, Any]:
    """Run an AC (or DC) power flow with lightsim2grid and format the results.

    The power flow starts from the voltages stored in ``net.buses`` (see
    :func:`gridfm_datakit.lightsim2grid.convert.initial_voltage`).

    Args:
        ls_net: The lightsim2grid LSGrid, in sync with ``net``.
        net: The network the LSGrid was built from or updated with.
        mapping_l2g: Index maps between ``net`` and ``ls_net``.
        dc: Run a DC power flow instead of an AC one.
        max_iter: Maximum number of iterations.
        tol: Convergence tolerance.
        as_arrays: Return the results as dense arrays under ``["solution"]["arrays"]``
            instead of per-element dicts (see :func:`get_pf_res`).

    Returns:
        The power flow results in PowerModels' format (see :func:`get_pf_res`), with the
        solving time in ``"solve_time"``.

    Raises:
        ValueError: If the power flow did not converge.
    """
    v_init = initial_voltage(net)
    start_time = time.perf_counter()
    v = (ls_net.dc_pf if dc else ls_net.ac_pf)(v_init, max_iter, tol)
    solve_time = time.perf_counter() - start_time
    return get_pf_res(
        ls_net,
        v,
        solve_time,
        net,
        mapping_l2g,
        as_arrays=as_arrays,
        with_ybus=as_arrays and not dc,
    )

get_pf_res

Format lightsim2grid power flow results for the pf_post_process function.

Parameters:

Name Type Description Default
ls_net Any

lightsim2grid LSGrid, on which a power flow was just run

required
v ndarray

complex voltage returned by the power flow (empty if it diverged)

required
solve_time float

power flow solving time

required
net Network

gridfm Network the LSGrid was built from

required
mapping_l2g MappingL2G

lightsim2grid-to-gridfm index maps

required
as_arrays bool

Skip the per-element dicts (which _solution_arrays would only turn back into arrays): ["solution"]["arrays"] then holds the (branch_flows, gen_pq, bus_vmva) triple that _solution_arrays would extract (in-service branches and generators, buses by bus index).

False
with_ybus bool

With as_arrays, also return the bus admittance matrix in ["solution"]["Ybus"] (see :func:get_ybus). Only valid right after an AC power flow, not after a DC one.

False

Returns:

Type Description
Dict[Any, Any]

Power flow results in a nested Dict format, similar to PowerModel's power flow results

Source code in gridfm_datakit/lightsim2grid/preprocess.py
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
def get_pf_res(
    ls_net: Any,
    v: np.ndarray,
    solve_time: float,
    net: Network,
    mapping_l2g: MappingL2G,
    as_arrays: bool = False,
    with_ybus: bool = False,
) -> Dict[Any, Any]:
    """Format lightsim2grid power flow results for the pf_post_process function.

    Args:
        ls_net: lightsim2grid LSGrid, on which a power flow was just run
        v: complex voltage returned by the power flow (empty if it diverged)
        solve_time: power flow solving time
        net: gridfm Network the LSGrid was built from
        mapping_l2g: lightsim2grid-to-gridfm index maps
        as_arrays: Skip the per-element dicts (which ``_solution_arrays`` would only turn
            back into arrays): ``["solution"]["arrays"]`` then holds the
            ``(branch_flows, gen_pq, bus_vmva)`` triple that ``_solution_arrays``
            would extract (in-service branches and generators, buses by bus index).
        with_ybus: With ``as_arrays``, also return the bus admittance matrix in
            ``["solution"]["Ybus"]`` (see :func:`get_ybus`). Only valid right after an
            AC power flow, not after a DC one.

    Returns:
        Power flow results in a nested Dict format, similar to PowerModel's power flow results
    """
    if v.shape[0] == 0:
        raise ValueError(
            "Power flow computation failed: lightsim2grid did not converge",
        )

    base_mva = float(net.baseMVA)
    n_branches = net.branches.shape[0]

    # Branch flows in pu, (n_branches, 4) as pf, qf, pt, qt. Powerline
    # and transformer results are scattered back to their branch rows.
    flows = np.zeros((n_branches, 4))
    for rows, res1, res2 in (
        (mapping_l2g.line_rows, ls_net.get_line_res1(), ls_net.get_line_res2()),
        (mapping_l2g.trafo_rows, ls_net.get_trafo_res1(), ls_net.get_trafo_res2()),
    ):
        flows[rows, 0] = res1[0]
        flows[rows, 1] = res1[1]
        flows[rows, 2] = res2[0]
        flows[rows, 3] = res2[1]
    flows /= base_mva

    gen_p, gen_q, _ = ls_net.get_gen_res()
    gen_p = np.asarray(gen_p) / base_mva
    gen_q = np.asarray(gen_q) / base_mva

    reverse = net.reverse_bus_index_mapping
    vm, va = np.abs(v), np.angle(v)

    if as_arrays:
        bus_vmva = np.empty((net.buses.shape[0], 2))
        bus_vmva[net.buses[:, BUS_I].astype(int)] = np.column_stack((vm, va))
        res = {
            "solution": {
                "baseMVA": base_mva,
                "per_unit": True,
                "pf": True,
                "arrays": (
                    flows[net.idx_branches_in_service],
                    np.column_stack((gen_p, gen_q))[net.idx_gens_in_service],
                    bus_vmva,
                ),
            },
            "solve_time": solve_time,
        }
        if with_ybus:
            res["solution"]["Ybus"] = get_ybus(ls_net, net)
        return res

    return {
        "solution": {
            "baseMVA": base_mva,
            "gen": {
                str(int(i) + 1): {"pg": gen_p[i], "qg": gen_q[i]}
                for i in net.idx_gens_in_service
            },
            "branch": {
                str(i + 1): {
                    "pf": flows[i, 0],
                    "qf": flows[i, 1],
                    "pt": flows[i, 2],
                    "qt": flows[i, 3],
                }
                for i in range(n_branches)
            },
            "bus": {
                str(reverse[int(net.buses[r, BUS_I])]): {"vm": vm[r], "va": va[r]}
                for r in range(net.buses.shape[0])
            },
            "per_unit": True,
            "pf": True,
        },
        "solve_time": solve_time,
    }

get_ybus

Bus admittance matrix of the LSGrid, indexed like :func:gridfm_datakit.network.makeYbus.

lightsim2grid builds it for its AC power flow, so this is much cheaper than rebuilding it from net. It must be called after an AC power flow, which is what updates it.

Parameters:

Name Type Description Default
ls_net Any

The lightsim2grid LSGrid, on which an AC power flow was just run.

required
net Network

The network the LSGrid is in sync with.

required

Returns:

Type Description
csr_matrix

The sparse Ybus (no explicit zeros, sorted indices), with row and column

csr_matrix

i for the bus whose BUS_I is i, like makeYbus. It matches

csr_matrix

makeYbus(net.baseMVA, net.buses, net.branches) up to the rounding of the

csr_matrix

summation order.

Source code in gridfm_datakit/lightsim2grid/preprocess.py
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
def get_ybus(ls_net: Any, net: Network) -> csr_matrix:
    """Bus admittance matrix of the LSGrid, indexed like :func:`gridfm_datakit.network.makeYbus`.

    lightsim2grid builds it for its AC power flow, so this is much cheaper than
    rebuilding it from ``net``. It must be called after an AC power flow, which is
    what updates it.

    Args:
        ls_net: The lightsim2grid LSGrid, on which an AC power flow was just run.
        net: The network the LSGrid is in sync with.

    Returns:
        The sparse Ybus (no explicit zeros, sorted indices), with row and column
        ``i`` for the bus whose ``BUS_I`` is ``i``, like ``makeYbus``. It matches
        ``makeYbus(net.baseMVA, net.buses, net.branches)`` up to the rounding of the
        summation order.
    """
    ybus = ls_net.get_Ybus()  # one row per lightsim2grid bus = per row of net.buses
    bus_i = net.buses[:, BUS_I].astype(int)
    n_buses = net.buses.shape[0]
    if np.array_equal(bus_i, np.arange(n_buses)):
        ybus = ybus.tocsr()
    else:
        ybus = ybus.tocoo()
        ybus = csr_matrix(
            (ybus.data, (bus_i[ybus.row], bus_i[ybus.col])),
            shape=(n_buses, n_buses),
        )
        ybus.sum_duplicates()
    ybus.sort_indices()
    ybus.eliminate_zeros()
    return ybus

build_l2g_maps

Build the index maps of the LSGrid that :func:to_lightsim2grid creates from net.

Parameters:

Name Type Description Default
net Network

The network the LSGrid is built from.

required

Returns:

Type Description
MappingL2G

The maps: which net.branches rows are powerlines and transformers in

MappingL2G

lightsim2grid, and the bus index of every lightsim2grid bus.

Source code in gridfm_datakit/lightsim2grid/mapping.py
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
def build_l2g_maps(net: Network) -> MappingL2G:
    """Build the index maps of the LSGrid that :func:`to_lightsim2grid` creates from ``net``.

    Args:
        net: The network the LSGrid is built from.

    Returns:
        The maps: which ``net.branches`` rows are powerlines and transformers in
        lightsim2grid, and the bus index of every lightsim2grid bus.
    """
    is_trafo = (net.branches[:, TAP] != 0) | (net.branches[:, SHIFT] != 0)
    return MappingL2G(
        line_rows=np.flatnonzero(~is_trafo),
        trafo_rows=np.flatnonzero(is_trafo),
        bus_index=net.buses[:, BUS_I].astype(int),
    )

MappingL2G

lightsim2grid-to-gridfm index maps.

Attributes

line_rows : np.ndarray line_rows[k] is the net.branches row of the k-th lightsim2grid powerline. trafo_rows : np.ndarray trafo_rows[k] is the net.branches row of the k-th lightsim2grid transformer. bus_index : np.ndarray bus_index[r] is the continuous bus index (BUS_I) of the r-th lightsim2grid bus (= r-th row of net.buses).

Source code in gridfm_datakit/lightsim2grid/mapping.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@dataclass
class MappingL2G:
    """lightsim2grid-to-gridfm index maps.

    Attributes
    ----------
    line_rows : np.ndarray
        ``line_rows[k]`` is the ``net.branches`` row of the k-th lightsim2grid powerline.
    trafo_rows : np.ndarray
        ``trafo_rows[k]`` is the ``net.branches`` row of the k-th lightsim2grid transformer.
    bus_index : np.ndarray
        ``bus_index[r]`` is the continuous bus index (``BUS_I``) of the r-th
        lightsim2grid bus (= r-th row of ``net.buses``).
    """

    line_rows: np.ndarray
    trafo_rows: np.ndarray
    bus_index: np.ndarray

ConvertedNetwork

A lightsim2grid LSGrid together with its maps to the gridfm Network.

Source code in gridfm_datakit/lightsim2grid/convert.py
34
35
36
37
38
39
40
41
@dataclass
class ConvertedNetwork:
    """A lightsim2grid LSGrid together with its maps to the gridfm Network."""

    ls_net: Any  # lightsim2grid.network.LSGrid
    mapping_l2g: MappingL2G
    # what was last pushed to ls_net (see _snapshot), to push only the differences next time
    state: Dict[str, Any] = field(default_factory=dict, repr=False)

LoadedNetwork

Bundles the lightsim2grid and gridfm_datakit representations of a network.

Source code in gridfm_datakit/lightsim2grid/__init__.py
35
36
37
38
39
40
41
@dataclass
class LoadedNetwork:
    """Bundles the lightsim2grid and gridfm_datakit representations of a network."""

    ls_net: Any  # lightsim2grid.network.LSGrid
    gfm_net: Network
    mapping_l2g: MappingL2G