Skip to content

ContingencyAnalysisCPP: connected N-1 rows fail after 1 iteration with a remote voltage regulator (depends on nb_thread) #240

Open

Description

@BDonnot

Environment

  • Grid2op version: not used
  • lightsim2grid version: 1.2.0, master at fe623bf, built from source
  • pypowsybl version: 1.16.1
  • System: Linux. Seen in the gpusim2grid CI (ubuntu 22.04, Python 3.10) and reproduced in a separate container (Python 3.13).

Bug description

When a generator regulates a remote bus, ContingencyAnalysisCPP reports several N-1 rows as not converged after a single iteration. Their voltages come back as all 0 and get_row_nb_iter() is 1. Yet for each of these rows:

  • the grid stays connected (is_grid_connected_after_contingency() is 1);
  • a single LSGrid.ac_pf of the same contingency, started from the base-case voltages, converges in 3 to 4 iterations;
  • without the remote regulation (plain IEEE 14), every connected row converges.

Which rows fail depends on nb_thread. The result is deterministic for a given value: e.g. branch 5 fails with 1 or 8 threads but converges with 4. It does not depend on Vinit, init_from_n_powerflow, reuse_base_case or keep_vinit_at_group_controlled_buses. A single contingency on its own (add_n1(5) alone) fails too.

Because the outcome changes with the way rows are split across threads, some per-thread solver state seems to carry over from one row to the next, or to be set up differently from ac_pf. The VoltageControl state of the remote regulator is the obvious suspect.

How to reproduce

Code snippet

import numpy as np
import pypowsybl as pp
from lightsim2grid.network import init_from_pypowsybl
from lightsim2grid.contingencyAnalysis import ContingencyAnalysisCPP

# IEEE 14, with B6-G regulating the 12 kV bus of a load further away (a remote regulator)
net = pp.network.create_ieee14()
loads = net.get_loads(attributes=["voltage_level_id"])
load = loads.index[loads["voltage_level_id"] == "VL12"][0]
net.update_generators(id="B6-G", regulated_element_id=load, target_v=14.2, min_q=-9999., max_q=9999.)
grid = init_from_pypowsybl(net, gen_slack_id="B1-G", sort_index=True)
V0 = grid.ac_pf(np.ones(grid.total_bus(), dtype=complex), 30, 1e-11)
assert V0.shape[0] > 0

n_lines = len(grid.get_lines())
for nb_thread in (1, 4, 8):
    ca = ContingencyAnalysisCPP(grid)
    ca.nb_thread = nb_thread
    ca.add_all_n1()
    ca.compute(V0.copy(), 30, 1e-11)
    connected = np.asarray(ca.is_grid_connected_after_contingency()).ravel().astype(bool)
    failed = np.flatnonzero(connected & ~np.asarray(ca.converged_mask()))
    print(f"nb_thread={nb_thread}: connected rows not converged {failed.tolist()}, "
          f"iterations {np.asarray(ca.get_row_nb_iter())[failed].tolist()}")

# the same contingencies, one LSGrid.ac_pf each, from the base-case voltages
for b in (2, 3, 4, 5, 16, 17):
    g = grid.copy()
    g.deactivate_powerline(b) if b < n_lines else g.deactivate_trafo(b - n_lines)
    V = g.ac_pf(V0.copy(), 30, 1e-11)
    print(f"branch {b}: single ac_pf converged = {V.shape[0] > 0}")

# a single contingency alone fails too
ca = ContingencyAnalysisCPP(grid)
ca.add_n1(5)
ca.compute(V0.copy(), 30, 1e-11)
print("add_n1(5) alone: converged", ca.converged_mask(), "iterations", ca.get_row_nb_iter())

Current output

nb_thread=1: connected rows not converged [2, 3, 4, 5, 16, 17], iterations [1, 1, 1, 1, 1, 1]
nb_thread=4: connected rows not converged [2, 3, 4, 16, 17], iterations [1, 1, 1, 1, 1]
nb_thread=8: connected rows not converged [2, 5, 17], iterations [1, 1, 1]
branch 2: single ac_pf converged = True
branch 3: single ac_pf converged = True
branch 4: single ac_pf converged = True
branch 5: single ac_pf converged = True
branch 16: single ac_pf converged = True
branch 17: single ac_pf converged = True
add_n1(5) alone: converged [False] iterations [1]

(Row 13, L7-8-1, islands bus 8 and is excluded above as expected. The failing branches are L10-11-1, L12-13-1, L13-14-1, L2-3-1, L9-14-1 and T4-7-1.)

Expected output

Every connected row converges, whatever nb_thread, as the single ac_pf of each contingency does.

For instance, for branch 5 (L2-3-1) the single solve gives B6-G's own bus at 14.430316 kV, which is what gpusim2grid's batched GPU contingency analysis also finds for that row. This came up in Grid2op/gpusim2grid#21, whose parity test against ContingencyAnalysisCPP now compares each row to the single ac_pf solve, and to ContingencyAnalysisCPP only where it converged.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions