Spaces:
Running
Partage du pypowsybl Network avec le backend grid2op
Contexte
Après la mutualisation network_service ↔ recommender_service._base_network
(docs/performance/history/shared-network.md), le même .xiidm était encore chargé
deux fois pendant /api/config :
- ✅
network_service.load_network()— pypowsybl parse #1. - ✅
recommender_service._get_base_network()— mutualisé avec #1. - ⚠️
setup_environment_configs_pypowsybl()— pypowsybl parse #2 via grid2op : la lib upstreamexpert_op4grid_recommenderchargeait le fichier une seconde fois pour construire saSimulationEnvironment.
Sur les gros réseaux (PyPSA-EUR 400 kV) ce deuxième parse coûte ~1-5 s de CPU inutile.
Changement
Patch upstream (expert_op4grid_recommender 0.2.0.post1)
get_env_first_obs_pypowsybl()accepte un paramètrenetwork: Optional[pp.network.Network] = None. Quand non-None, la découverte.xiidm+pp.network.load()sont sautés ; l'instance injectée est passée directement àSimulationEnvironment(network=..., network_path=None).setup_environment_configs_pypowsybl()gagne le même paramètre et le transmet àget_env_first_obs_pypowsybl.- Les wrappers de compat (
setup_environment_configs,setup_environment_pypowsybl) tranmettent aussinetwork=— aucune rupture d'API. SimulationEnvironment.__init__etNetworkManager.__init__acceptaient déjànetwork=— seul le chaînage haut-niveau manquait. Voirpypowsybl_backend/simulation_env.py:46etpypowsybl_backend/network_manager.py:34.
Patch Co-Study4Grid
recommender_service.update_config() injecte self._base_network
(déjà pré-chauffé par prefetch_base_nad_async) dans l'appel upstream :
env, _obs, ... = setup_environment_configs_pypowsybl(
network=self._base_network # ← SKIP pp.network.load() côté grid2op
)
Sûreté de partage
Le même objet pypowsybl Network est maintenant référencé par trois détenteurs :
network_service.network— lectures seulement (get_lines,get_voltage_levels, …)recommender_service._base_network— lectures + variant switching dans_get_n_variant/_get_n1_variant, toujours restauré dans untry/finallySimulationEnvironment.network_manager.network— possédé par le backend grid2op, utilise intensivement le variant switching pendant les LF d'analyse et de simulation
Argumentaire de non-collision :
- Les LF grid2op ne tournent pas pendant
/api/config. Elles sont déclenchées parrun_analysis_step2,simulate_manual_action, etc. — postérieures au retour de la config. Quand le frontend fire les 4 XHRs parallèles post-config, seul le worker NAD et les lectures basiques tournent — pas de LF grid2op. - Le worker NAD termine avant
/api/config(confirmé par v9 : diagram XHR = 379 ms = pur transfert wire). Donc au moment où les actions commencent à se simuler, le thread NAD est déjà fini. - Un seul worker uvicorn en pratique — Co-Study4Grid est un outil mono-utilisateur, pas de sessions d'analyse concurrentes qui partagent l'état.
Échappatoire si problème : network.clone() (API pypowsybl) — la
SimulationEnvironment reçoit une deep-copy. Coût ~500 ms vs ~3 s de
parse frais — toujours un gain net si on doit y recourir un jour.
Gardes « variant-state » (commit suivant)
Le partage du Network introduit un risque théorique : si un endpoint d'analyse ou de simulation arrive pendant que le worker NAD est encore en train de modifier le variant (même brièvement), la lecture d'observation côté grid2op peut voir un état incohérent. Pour éliminer ce risque, chaque entrée d'analyse/simulation passe désormais par un garde adapté à la sémantique de l'endpoint :
_ensure_n_state_ready() — entrées qui démarrent en N
def _ensure_n_state_ready(self):
"""Drain the NAD prefetch worker and position the shared Network
on the N variant."""
self._drain_pending_base_nad_prefetch()
if self._base_network is None and config.ENV_PATH is None:
return # No study loaded yet; noop.
try:
n = self._get_base_network()
n.set_working_variant(self._get_n_variant())
except Exception as e:
logger.warning(f"Could not position N variant: {e}")
Appelé au tout début de :
run_analysis(full analysis legacy) — démarre en N, calcule le contingency.run_analysis_step1(overload detection) — idem.
_ensure_n1_state_ready(disconnected_element) — entrées qui démarrent en N-1
def _ensure_n1_state_ready(self, disconnected_element: str):
"""Drain the NAD prefetch worker and position the shared Network
on the N-1 variant for `disconnected_element`."""
self._drain_pending_base_nad_prefetch()
if self._base_network is None and config.ENV_PATH is None:
return
if not disconnected_element:
return # No contingency in hand — drain-only.
try:
n = self._get_base_network()
n.set_working_variant(self._get_n1_variant(disconnected_element))
except Exception as e:
logger.warning(f"Could not position N-1 variant for {disconnected_element!r}: {e}")
Appelé au tout début de :
simulate_manual_action(raw_action_id, disconnected_element, …)— simule une action sur un contingency connu : l'état d'entrée naturel est N-1.compute_superposition(action1_id, action2_id, disconnected_element)— idem.
Pas de garde
run_analysis_step2— hérite le_analysis_contextpositionné par step1 (observations déjà capturées, plus de lecture sur le réseau pypowsybl à faire). Le worker NAD a déjà été drainé par la garde de step1 dans la même session.
Sécurité anti-divergence LF
Complément : _get_n_variant et _get_n1_variant encapsulent maintenant
leur set_working_variant + AC load-flow dans un try/finally pour
garantir la restauration du variant d'origine même si le LF lève.
Avant, une divergence LF laissait le Network bloqué sur le variant
*_cached, ce que les autres consommateurs (network_service, grid2op)
voyaient silencieusement.
Invariants (testés)
Upstream (tests/test_environment_pypowsybl.py) :
test_get_env_first_obs_skips_file_load_when_network_is_injected— avec un Network injecté, aucun.xiidmn'est découvert ni chargé ;SimulationEnvironmentest construit avecnetwork=<injecté>etnetwork_path=None.test_get_env_first_obs_injected_network_still_looks_up_companion_files— le raccourci vaut uniquement pour le network lui-même ; les fichiers compagnons (thermal_limits.json, etc.) sont toujours cherchés.test_get_env_first_obs_injected_network_handles_direct_xiidm_env_name— quandenv_namepointe sur un chemin de fichier direct (/data/grid.xiidm), leenv_pathretombe sur le parent pour les compagnons.test_setup_environment_configs_forwards_injected_network— le wrapper de compat transmetnetwork=à la fonction inner.
Co-Study4Grid (test_recommender_service.py::TestUpdateConfigSharesNetworkWithGrid2op) :
test_update_config_injects_preloaded_network_into_upstream— assertion stricte quesetup_environment_configs_pypowsyblest appelée avecnetwork=<l'instance de self._base_network>.
Impact attendu
Baseline v9 (après mutualisation network_service ↔ recommender_service) :
| Segment | v9 mesuré |
|---|---|
/api/config |
16 596 ms |
/api/network-diagram |
379 ms |
| Total critical path | ~18 s |
Attendu v10 (avec partage grid2op) :
| Segment | v10 attendu |
|---|---|
/api/config |
~13-14 s (−3 s : plus de pp.network.load côté grid2op) |
/api/network-diagram |
~380 ms (inchangé, cache NAD toujours chaud) |
| Total critical path | ~15 s (−3 s vs v9, −6 s vs v7) |
Dépendance
Requiert expert_op4grid_recommender >= 0.2.0.post1. Si une version
antérieure est installée, l'appel setup_environment_configs_pypowsybl(network=...)
lève un TypeError explicite au démarrage. Pas de fallback silencieux
— ce serait masquer un problème de déploiement.
Ce qui n'est PAS changé
- Endpoints : comportement identique côté client.
- Frontend : aucun changement.
- Cas
_get_base_networkfallback (network_service.network is None) : préservé pour les tests et les appels directs àRecommenderServicehors/api/config. - Autres optimisations (cache disque NAD, streaming
/api/config) : orthogonales, à revisiter dans un commit ultérieur.
Fichiers modifiés
Upstream (expert_op4grid_recommender, commit 58fe2e44)
| Fichier | Changement |
|---|---|
expert_op4grid_recommender/environment_pypowsybl.py |
get_env_first_obs_pypowsybl et setup_environment_configs_pypowsybl gagnent network=None et le transmettent à SimulationEnvironment. Wrapper setup_environment_configs aussi. |
expert_op4grid_recommender/main.py |
setup_environment_pypowsybl gagne network=None passthrough. |
tests/test_environment_pypowsybl.py |
4 nouveaux tests sur le chemin d'injection + 1 ajustement du test compat wrapper. |
pyproject.toml |
0.2.0 → 0.2.0.post1. |
Co-Study4Grid
| Fichier | Changement |
|---|---|
expert_backend/services/recommender_service.py |
update_config passe network=self._base_network à setup_environment_configs_pypowsybl. |
expert_backend/tests/test_recommender_service.py |
Nouvelle classe TestUpdateConfigSharesNetworkWithGrid2op (1 test d'invariant). |