#!/usr/bin/env python3 # Copyright (c) 2025-2026, RTE (https://www.rte-france.com) # This Source Code Form is subject to the terms of the Mozilla Public License, version 2.0. # If a copy of the Mozilla Public License, version 2.0 was not distributed with this file, # you can obtain one at http://mozilla.org/MPL/2.0/. # SPDX-License-Identifier: MPL-2.0 """Per-load-flow micro-benchmark: what makes "slow mode" 6x slower than "fast". Isolates a SINGLE ``run_ac`` on the post-contingency (N-1) state and times it under each load-flow parameter variant, so the cost of the per-action reassessment (`utils/reassessment.py` re-simulates every prioritized action with one AC LF each) can be attributed to a specific knob rather than guessed. Key result on the full France grid (`bare_env_20240828T0100Z`, contingency ``P.SAOL31RONCI`` → overload ``BEON L31CPVAN``): the entire slow/fast gap is the **transformer tap-changer voltage-control mode**. The provider default (incremental outer loop) needs ~57 Newton iterations/LF; switching to ``AFTER_GENERATOR_VOLTAGE_CONTROL`` converges in ~20 for the same branch current (<0.5 % delta). Disabling tap control entirely (the *old* fast mode) is only marginally faster still but changes the currents. See ``docs/performance/history/reassessment-fast-mode-tap-control.md``. Usage (from repo root, backend venv active):: python benchmarks/bench_load_flow_modes.py python benchmarks/bench_load_flow_modes.py --contingency P.SAOL31RONCI \ --overload "BEON L31CPVAN" --reps 3 Override the grid with ``BENCH_NETWORK_PATH`` (see ``_bench_common``). """ from __future__ import annotations import argparse import time import pypowsybl as pp import pypowsybl.loadflow as lf from _bench_common import NETWORK_PATH def _base_params(**override): """The recommender's slow-mode default (NetworkManager._create_default_lf_parameters). ``override`` accepts any ``lf.Parameters`` attribute plus a special ``provider`` dict that is merged into ``provider_parameters``. """ provider = { "useActiveLimits": "true", "svcVoltageMonitoring": "false", "voltageRemoteControl": "false", "writeReferenceTerminals": "false", "slackBusSelectionMode": "MOST_MESHED", "maxOuterLoopIterations": "100", } provider.update(override.pop("provider", {})) p = lf.Parameters( read_slack_bus=False, write_slack_bus=False, voltage_init_mode=override.pop("voltage_init_mode", lf.VoltageInitMode.PREVIOUS_VALUES), transformer_voltage_control_on=override.pop("transformer_voltage_control_on", True), use_reactive_limits=override.pop("use_reactive_limits", True), shunt_compensator_voltage_control_on=override.pop("shunt_compensator_voltage_control_on", True), phase_shifter_regulation_on=override.pop("phase_shifter_regulation_on", True), distributed_slack=True, dc_use_transformer_ratio=False, twt_split_shunt_admittance=True, provider_parameters=provider, ) for k, v in override.items(): setattr(p, k, v) return p def main() -> None: ap = argparse.ArgumentParser(description=__doc__) ap.add_argument("--contingency", default="P.SAOL31RONCI") ap.add_argument("--overload", default="BEON L31CPVAN") ap.add_argument("--reps", type=int, default=3) args = ap.parse_args() net = pp.network.load(f"{NETWORK_PATH}/grid.xiidm") # Seed a valid state (DC_VALUES init — no previous voltages exist yet), # then apply the contingency on a fresh variant and seed it too, so every # timed config below starts from a converged PREVIOUS_VALUES baseline. seed = _base_params(voltage_init_mode=lf.VoltageInitMode.DC_VALUES) lf.run_ac(net, parameters=seed) net.clone_variant(net.get_variant_ids()[0], "n1") net.set_working_variant("n1") try: net.update_lines(id=args.contingency, connected1=False, connected2=False) except Exception: net.update_2_windings_transformers(id=args.contingency, connected1=False, connected2=False) lf.run_ac(net, parameters=seed) def run(label: str, params) -> None: ts = [] res = None for _ in range(args.reps): t = time.perf_counter() try: res = lf.run_ac(net, parameters=params) except Exception as e: # noqa: BLE001 print(f" {label:<44s} FAILED ({str(e)[:50]})") return ts.append(time.perf_counter() - t) r0 = res[0] try: ln = net.get_lines(attributes=["i1", "i2"]).loc[args.overload] imax = f"{max(abs(ln.i1), abs(ln.i2)):.1f}A" except Exception: imax = "n/a" print(f" {label:<44s} {min(ts):6.3f}s {r0.status.name:<10s} " f"nr={getattr(r0, 'iteration_count', '?'):>3} I={imax}") print(f"Grid={NETWORK_PATH} contingency={args.contingency} overload={args.overload}") print(f"Per-LF run_ac ({args.reps} reps, min):") run("SLOW default (incremental tap control)", _base_params()) run("FAST-old (transformer+shunt ctrl OFF)", _base_params( transformer_voltage_control_on=False, shunt_compensator_voltage_control_on=False)) run("FAST-new (tap ctrl AFTER_GENERATOR)", _base_params( provider={"transformerVoltageControlMode": "AFTER_GENERATOR_VOLTAGE_CONTROL"})) run("slow, shunt ctrl OFF only", _base_params(shunt_compensator_voltage_control_on=False)) run("slow, transformer ctrl OFF only", _base_params(transformer_voltage_control_on=False)) run("slow, reactive_limits OFF", _base_params(use_reactive_limits=False)) run("slow, phase_shifter_reg OFF", _base_params(phase_shifter_regulation_on=False)) if __name__ == "__main__": main()