Co-Study4Grid / benchmarks /_bench_common.py
github-actions[bot]
Deploy 7688ef1
13d4e44
Raw
History Blame Contribute Delete
4.7 kB
# 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
"""Shared helpers for the Co-Study4Grid performance benchmarks.
Each script in this directory measures a distinct slice of the Load
Study critical path so that regressions can be caught on the real
PyPSA-EUR France 400 kV grid without standing up the full web stack.
Usage from each benchmark:
from _bench_common import bench, setup_service, NETWORK_PATH, ACTION_FILE
net = pn.load(NETWORK_PATH + "/grid.xiidm")
bench("my op", lambda: my_op(net))
"""
from __future__ import annotations
import os
import sys
import time
from pathlib import Path
from types import SimpleNamespace
from typing import Callable, Iterable
# Make `expert_backend` importable when running a benchmark directly.
_REPO_ROOT = Path(__file__).resolve().parent.parent
if str(_REPO_ROOT) not in sys.path:
sys.path.insert(0, str(_REPO_ROOT))
# Default reference grid — overridable via env var so benchmarks can
# also run on smaller test cases in CI.
NETWORK_PATH = os.environ.get(
"BENCH_NETWORK_PATH",
"/home/marotant/dev/Expert_op4grid_recommender/data/bare_env_20240828T0100Z",
)
ACTION_FILE = os.environ.get(
"BENCH_ACTION_FILE",
"/home/marotant/dev/Expert_op4grid_recommender/data/action_space/"
"reduced_model_actions_20240828T0100Z_dijon.json",
)
def bench(label: str, fn: Callable, reps: int = 5, width: int = 60) -> object:
"""Time `fn` over `reps` runs and print median/min.
Returns the last call's return value so the caller can assert
semantic equivalence across variants.
"""
dts: list[float] = []
ret = None
for _ in range(reps):
t0 = time.perf_counter()
ret = fn()
dts.append((time.perf_counter() - t0) * 1000)
dts.sort()
med = dts[len(dts) // 2]
mn = dts[0]
print(f" {label:<{width}} median={med:>7.1f} ms min={mn:>7.1f}")
return ret
def setup_service(
network_path: str = NETWORK_PATH,
action_file: str = ACTION_FILE,
wait_for_nad_prefetch: bool = True,
) -> tuple[object, object, float]:
"""Prepare `network_service` + `recommender_service` with a
realistic config, mimicking `/api/config` from the UI.
Returns `(network_service, recommender_service, dt_setup_ms)`.
"""
from expert_backend.services.network_service import network_service
from expert_backend.services.recommender_service import recommender_service
settings = SimpleNamespace(
network_path=network_path,
action_file_path=action_file,
layout_path=f"{network_path}/grid_layout.json",
min_line_reconnections=2.0,
min_close_coupling=3.0,
min_open_coupling=2.0,
min_line_disconnections=3.0,
n_prioritized_actions=10,
monitoring_factor=0.95,
pre_existing_overload_threshold=0.02,
ignore_reconnections=False,
pypowsybl_fast_mode=True,
min_pst=1.5,
min_load_shedding=2.5,
min_renewable_curtailment_actions=1,
lines_monitoring_path=None,
do_visualization=True,
)
t0 = time.perf_counter()
recommender_service.reset()
network_service.load_network(network_path)
recommender_service.update_config(settings)
if wait_for_nad_prefetch:
ev = getattr(recommender_service, "_prefetched_base_nad_event", None)
if ev is not None:
ev.wait(timeout=30)
dt_setup = (time.perf_counter() - t0) * 1000
return network_service, recommender_service, dt_setup
def timed(name: str, orig: Callable, store: dict) -> Callable:
"""Wrap `orig` so every call appends its duration to `store[name]`.
Used to instrument a live service method without changing its code:
steps = {}
mixin._generate_diagram = timed("generate", mixin._generate_diagram, steps)
mixin.get_n1_diagram(...)
print(steps)
"""
def wrapped(*a, **kw):
t0 = time.perf_counter()
try:
return orig(*a, **kw)
finally:
store.setdefault(name, []).append((time.perf_counter() - t0) * 1000)
return wrapped
def print_step_summary(steps: dict, header: str = "Step timings") -> None:
print(f"\n--- {header} ---")
for k, dts in steps.items():
if not dts:
continue
total = sum(dts)
last = dts[-1]
print(f" {k:<55} last={last:>8.1f} ms count={len(dts)} total={total:>8.1f} ms")