Spaces:
Sleeping
CLAUDE.md - Co-Study4Grid
Project Overview
Co-Study4Grid is a full-stack web application for power grid contingency analysis and N-1 planning. It provides an interface to the expert_op4grid_recommender library, allowing operators to simulate element disconnections, visualize network overflow graphs, and receive prioritized remedial action recommendations.
Architecture
Monorepo with two main components plus a standalone HTML mirror:
Co-Study4Grid/
├── CLAUDE.md # This file — project overview + standalone parity audit
├── README.md # User-facing project description + quick start
├── CHANGELOG.md # Per-release changelog (current: 0.9.0)
├── CONTRIBUTING.md # Contributor setup, code-quality gate
├── pyproject.toml # Python project metadata + ruff config (E9/F ruleset)
├── pytest.ini # Pytest config (testpaths = expert_backend/tests)
├── expert_backend/ # Python FastAPI backend
│ ├── CLAUDE.md # Backend-scoped guide (singletons, mixins, lifecycle)
│ ├── main.py # FastAPI app: endpoints, CORS, gzip helpers, NDJSON streaming
│ ├── test_backend.py # Ad-hoc integration script (not part of pytest)
│ ├── recommenders/ # Pluggable recommendation-model registry: registry.py,
│ │ │ # random_basic / random_overflow canonical examples,
│ │ │ # overflow_path_filter + network_existence sampling
│ │ │ # filters, synthetic_actions builders. Models are
│ │ │ # registered at package import; the service integration
│ │ │ # is EXPLICIT composition (RecommenderService inherits
│ │ │ # ModelSelectionMixin; AnalysisMixin.run_analysis_step2
│ │ │ # consumes the registry — no import-time patching).
│ │ │ # Full reference: docs/backend/recommender_models.md
│ ├── services/
│ │ ├── network_service.py # pypowsybl Network singleton + metadata queries
│ │ ├── recommender_service.py # Analysis orchestrator (composes the 4 mixins below)
│ │ ├── diagram_mixin.py # NAD/SLD orchestrator — delegates to services/diagram/
│ │ ├── analysis_mixin.py # Two-step analysis orchestrator (model-aware step-2
│ │ │ # dispatch through the recommenders registry) —
│ │ │ # delegates to services/analysis/
│ │ ├── simulation_mixin.py # Manual-action + superposition orchestrator
│ │ ├── model_selection_mixin.py # Active recommender model + overflow-graph toggle
│ │ ├── simulation_helpers.py # Stateless helpers extracted from simulation_mixin (PR #104)
│ │ ├── overflow_overlay.py # Pin / filter overlay injector for the interactive
│ │ │ # HTML overflow viewer (PR #116, 0.7.0)
│ │ ├── sanitize.py # NumPy → native-Python recursive coercion
│ │ ├── analysis/ # PR #104 decomposition — action_enrichment,
│ │ │ # mw_start_scoring, analysis_runner, pdf_watcher,
│ │ │ # overflow_geo_transform (PR #116 — geo-layout SVG
│ │ │ # transform for /api/regenerate-overflow-graph)
│ │ └── diagram/ # PR #104 decomposition — layout_cache, nad_params,
│ │ # nad_render, sld_render, overloads, flows, deltas,
│ │ # obs_prewarm (post-contingency obs cache prewarm
│ │ # that lets run_analysis_step1 skip the LF),
│ │ # action_patch (extracted action-variant patch
│ │ # pipeline — keeps diagram_mixin under the LoC
│ │ # ceiling)
│ └── tests/ # pytest suite — see tests/CLAUDE.md for the mock layer
├── frontend/ # React 19 + TypeScript 5.9 + Vite 7 frontend
│ ├── CLAUDE.md # Frontend-scoped guide (App.tsx hub, hooks, SVG levers)
│ ├── package.json, vite.config.ts, vite.config.standalone.ts,
│ │ # eslint.config.js, tsconfig*.json
│ └── src/
│ ├── App.tsx # State orchestration hub (~1400 lines)
│ ├── api.ts # Axios HTTP client (base URL: 127.0.0.1:8000)
│ ├── types.ts # All TypeScript interfaces (one file)
│ ├── styles/ # Design-token palette (PR #120, 0.7.0):
│ │ # tokens.css (CSS custom properties) +
│ │ # tokens.ts (typed colors / space / text /
│ │ # radius / pinColors* constants). Single
│ │ # source of truth for the entire UI; the
│ │ # code-quality gate enforces zero hex
│ │ # literals outside these two files.
│ ├── hooks/ # useSettings / useActions / useAnalysis / useDiagrams /
│ │ # useSession / useDetachedTabs / useTiedTabsSync /
│ │ # usePanZoom / useSldOverlay / useContingencyFetch (svgPatch fast-
│ │ # path + full fallback) / useDiagramHighlights (per-tab
│ │ # highlight pipeline + Flow/Impacts view-mode state) /
│ │ # useOverflowIframe (PR #116 — iframe lifecycle, layer
│ │ # toggles, postMessage bridge, pin overlay payload) /
│ │ # useSldTopologyEdit (interactive SLD switch-edit →
│ │ # manual action, 0.8.0) / useTheme (light/dark theme
│ │ # toggle + persistence, 0.8.0)
│ ├── components/ # Header, ActionFeed, ActionCard, ActionCardPopover,
│ │ # ActionSearchDropdown (editable Δ MW column
│ │ # for redispatch in score table),
│ │ # ActionTypeFilterChips,
│ │ # ActionFilterRings (shared severity + action-type +
│ │ # Max-loading ring strip, 0.8.0), ActionTypeIcon /
│ │ # SeverityIcon (action-type + severity pictograms),
│ │ # AdditionalLinesPicker,
│ │ # ActionOverviewDiagram, AppSidebar (collapsible
│ │ # shell — readability-feed PR), SidebarSummary
│ │ # (sticky strip — hosts Clear button + overload
│ │ # info bubble that absorbed the legacy
│ │ # OverloadPanel affordances),
│ │ # NotificationHost (typed toast store —
│ │ # severity/dismiss/aria-live, PR D5),
│ │ # VisualizationPanel, OverloadPanel
│ │ # (kept on disk for unit-test backwards-compat;
│ │ # no longer rendered from App.tsx),
│ │ # CombinedActionsModal, ComputedPairsTable,
│ │ # ExplorePairsTab, SldOverlay, SldEditPanel
│ │ # (interactive maneuver list, 0.8.0),
│ │ # SldInjectionPopover (SLD load/gen active-
│ │ # power editor bubble), DetachableTabHost,
│ │ # MemoizedSvgContainer, ErrorBoundary, NoticesPanel
│ │ # (PR #122 tier system), DiagramLegend (PR #122),
│ │ # InspectSearchField + DetachedPlaceholder (PR #116
│ │ # — extracted from VisualizationPanel)
│ │ # + modals/ (SettingsModal, ReloadSessionModal,
│ │ # ConfirmationDialog)
│ ├── game/ # Timed, scored Game Mode (0.8.0; active only
│ │ # with ?game=1) — GameShell / useGameSession /
│ │ # gameBridge / GameConfigScreen / GameHud /
│ │ # GameResults / scoring / gameLog / presets /
│ │ # types. See docs/features/game-mode-codabench.md
│ └── utils/ # svgUtils (barrel re-exporting utils/svg/*),
│ # svgPatch (DOM-recycling patch applier),
│ # overloadHighlights, sessionUtils, interactionLogger,
│ # popoverPlacement, mergeAnalysisResult, actionTypes
│ # (classifyActionType + DEFAULT_ACTION_OVERVIEW_FILTERS),
│ # inspectables (filterInspectables — match an element
│ # by its displayed name, not just its raw id),
│ # ndjsonStream (single NDJSON reader, D5),
│ # notifications (typed toast store, D5),
│ # fileRegistry (structure regression guard)
│ └── svg/ # PR #104 decomposition — idMap, metadataIndex,
│ # svgBoost, fitRect, deltaVisuals, actionPinData,
│ # actionPinRender, highlights, vlInteractions (VL-disk
│ # hover name / click-to-inspect / dbl-click SLD),
│ # overflowPinPayload
│ # + overflowOverlayRender + pinGlyph (PR #116 —
│ # iframe-overlay pin pipeline)
├── standalone_interface_legacy.html # DECOMMISSIONED 2026-04-20 — hand-maintained
│ # single-file mirror frozen at its last version and
│ # tracked here for reference only. Replaced by the
│ # auto-generated `frontend/dist-standalone/standalone.html`
│ # (`npm run build:standalone`). New UI changes land ONLY
│ # in `frontend/src/` — do NOT edit this file further.
├── docs/ # Design docs — organized into features/, performance/
│ # (+ history/), architecture/, proposals/, data/.
│ # See `docs/README.md` for the index.
├── data/ # Sample grids: bare_env_small_grid_test, pypsa_eur_fr400,
│ # pypsa_eur_fr225_400 (France 225/400 kV — its large
│ # network ships compressed as network.xiidm.zip,
│ # auto-decompressed by network_service on load)
├── benchmarks/ # Perf scripts (bench_load_study, _bench_common)
├── Overflow_Graph/ # Generated PDFs (created at runtime)
├── overrides.txt # Pinned versions for transitive Python deps
├── requirements_py310.txt # Python 3.10-pinned requirements superset
├── scripts/ # Integration / parity / build helpers —
│ # `check_standalone_parity.py`,
│ # `check_session_fidelity.py`,
│ # `check_gesture_sequence.py`,
│ # `check_invariants.py`, `check_code_quality.py`,
│ # `check_openapi_contract.py` (D2 — diffs the live
│ # app.openapi() against expert_backend/openapi.snapshot.json),
│ # `code_quality_report.py`, `profile_diagram_perf.py`,
│ # `test_code_quality_report.py`,
│ # `test_estimation_vs_simulation_small_grid.py`,
│ # `pypsa_eur/` (full PyPSA-EUR → XIIDM pipeline
│ # with its own pytest coverage; `build_pipeline.py`
│ # writes a per-bundle provenance.json, D8), and
│ # `game_mode/` (`e2e_game_session.py` — real-backend
│ # Game Mode replay; `scoring_program/score.py` —
│ # in-repo Codabench scorer twin of scoring.ts,
│ # pinned to `scoring_golden.json` by test_score.py
│ # + scoring.test.ts for cross-language parity, D8)
├── Dockerfile # Single-container HuggingFace Docker Space image —
│ # same-origin SPA + FastAPI on :7860, game mode on
├── .dockerignore
├── deploy/ # HuggingFace Space README + step-by-step SETUP.md
├── config.default.json # Bundled first-run settings (fr225_400 grid +
│ # per-action-type recommender minima)
├── .editorconfig # Cross-editor indent / EOL defaults
├── .env.example # Template for backend env vars (CORS, …)
├── .gitattributes # Git LFS tracking for *.zip / *.png / *.jpg(eg)
│ # (HuggingFace Space git endpoint requires LFS)
└── .gitignore # Excludes __pycache__/, *.pyc, *.pyo, node_modules/
Per-subtree docs
| File | Scope |
|---|---|
CLAUDE.md (this file) |
Project overview, API table, conventions, parity-audit pointer |
frontend/PARITY_AUDIT.md |
Full standalone-parity audit: feature inventory, mirror-status table, Layer 1–4 conformity findings, gap-priority list, deltas. Split out of this file 2026-04-20. |
expert_backend/CLAUDE.md |
Backend internals: singletons, mixin composition, state lifecycle, NDJSON streaming, gzip helpers, layout cache invariants, NAD prefetch |
expert_backend/tests/CLAUDE.md |
Test conventions, the conftest.py mock layer for pypowsybl / expert_op4grid_recommender, frontend Vitest patterns |
frontend/CLAUDE.md |
Frontend internals: hook split, data flow, state reset, SVG performance levers, detached/tied tabs, interaction logger contract |
docs/README.md |
Index of design/feature/perf/architecture/proposal docs. Start here for any docs/** lookup. |
docs/features/save-results.md |
Save / reload session contract (JSON schema, reload flow, regression-guard matrix) |
docs/features/adding-action-type.md |
Cross-cutting checklist for adding/upgrading a remedial-action type (lib → backend → frontend → save/log/reload triad → regression specs) |
docs/features/interaction-logging.md |
Replay-ready event log contract |
docs/features/sld-topology-edit.md |
Interactive SLD topology edit → manual action card |
docs/features/sld-diagram-feeder-labels.md |
SLD feeders relabelled by far-end VL name (+ parallel index), overload-halo friendly-name↔IIDM-id bridge, and the charging-current annotation explaining the "after" loading of a line opened at one end |
docs/features/vl-disk-interactions.md |
Interactive VL disks on the NAD (hover name / click → Inspect / double-click → SLD) + delegation performance contract |
docs/features/game-mode-codabench.md |
Timed, scored Game Mode (?game=1) + Codabench benchmark bundle |
deploy/huggingface/ |
HuggingFace Docker Space deployment (Space README + SETUP.md) |
Tech Stack
Backend
- Python with FastAPI + Uvicorn
- pypowsybl - Power system network loading, load flow, and diagram generation
- expert_op4grid_recommender - Domain-specific grid optimization recommendations
- grid2op / pandapower / lightsim2grid - Grid simulation backends
Frontend
- React 19 with TypeScript 5.9
- Vite 7 - Build tool and dev server
- axios - HTTP client
- react-select - Searchable dropdown for branch selection
- vite-plugin-singlefile - Auto-generated single-file standalone bundle
- Vitest + React Testing Library - Unit / integration tests
Development Workflow
Running the Backend
# From the project root:
python -m expert_backend.main
# Or:
uvicorn expert_backend.main:app --host 0.0.0.0 --port 8000
The backend serves on http://localhost:8000. It expects pypowsybl and expert_op4grid_recommender to be available in the Python environment.
Running the Frontend
cd frontend
npm install
npm run dev # Start Vite dev server with HMR
The frontend dev server proxies API calls to http://localhost:8000 (hardcoded in frontend/src/api.ts).
Build & Lint
cd frontend
npm run build # TypeScript compilation (tsc -b) + Vite production build
npm run lint # ESLint
npm run preview # Preview production build
Running Tests
Backend unit tests use pytest and run against the in-repo mock layer
(no live pypowsybl required):
pytest # Full backend suite
pytest expert_backend/tests/test_foo.py # Single file
Ad-hoc integration scripts live in scripts/ (and scripts/pypsa_eur/
for the PyPSA-EUR → XIIDM pipeline). The pipeline scripts carry their
own pytest coverage (scripts/pypsa_eur/test_*.py) alongside the
backend suite; the rest require a running backend with real data:
pytest scripts/pypsa_eur # Pipeline unit tests
python scripts/pypsa_eur/test_pipeline.py # End-to-end smoke test
python scripts/pypsa_eur/test_n1_calibration.py # N-1 flow calibration check
python scripts/pypsa_eur/test_grid_layout.py # Layout loading sanity check
python scripts/profile_diagram_perf.py # NAD rendering profiler
Frontend unit tests use Vitest:
cd frontend
npm run test # Run Vitest test suite
Code-Quality Checks (continuous reporting)
# Generate a full JSON + Markdown report (backend + frontend metrics)
python scripts/code_quality_report.py --output reports/code-quality.json \
--markdown reports/code-quality.md
# Gate a pull request: non-zero exit on threshold violation
python scripts/check_code_quality.py
Both scripts run in CI (.github/workflows/code-quality.yml). The gate
guards the reductions documented in
docs/architecture/code-quality-analysis.md
(no new print() / bare except, module-size ceilings, no any /
@ts-ignore in frontend source).
API Endpoints
| Method | Path | Description |
|---|---|---|
| GET | /api/user-config |
Read persisted user configuration (paths, recommender params) |
| POST | /api/user-config |
Persist user configuration |
| GET | /api/config-file-path |
Get the current user-config file path |
| POST | /api/config-file-path |
Set a custom user-config file path |
| POST | /api/config |
Set network path, action file path, and all recommender parameters (incl. model and compute_overflow_graph) |
| GET | /api/models |
List registered recommendation models with their params_spec() and capability flags |
| POST | /api/recommender-model |
Lightweight swap of the active recommender model (no network reload) — fired by the model dropdowns in Settings and above Analyze & Suggest |
| GET | /api/branches |
List disconnectable elements (lines + 2-winding transformers) |
| GET | /api/voltage-levels |
List voltage levels in the network |
| GET | /api/nominal-voltages |
Map voltage level IDs to nominal voltages (kV) |
| GET | /api/element-voltage-levels |
Resolve equipment ID to its voltage level IDs |
| GET | /api/voltage-level-substations |
Map voltage level IDs to their parent substation IDs (used by the SLD overlay and overflow pin pipeline) |
| POST | /api/run-analysis |
Run full N-1 contingency analysis (streaming NDJSON, legacy) |
| POST | /api/run-analysis-step1 |
Two-step analysis Part 1: detect overloads |
| POST | /api/run-analysis-step2 |
Two-step analysis Part 2: resolve with actions (streaming NDJSON) |
| GET | /api/network-diagram |
Get N-state network SVG diagram (NAD) |
| POST | /api/contingency-diagram |
Get post-contingency N-1 diagram with flow deltas |
| POST | /api/contingency-diagram-patch |
SVG-less per-branch delta for DOM-recycling fast path (PR #108) |
| POST | /api/action-variant-diagram |
Get network state after applying a remedial action |
| POST | /api/action-variant-diagram-patch |
Per-branch delta + VL-subtree splice for action DOM recycling |
| POST | /api/focused-diagram |
Generate NAD sub-diagram focused on a specific element |
| POST | /api/action-variant-focused-diagram |
Focused NAD for specific VL in post-action state |
| POST | /api/n-sld |
Single Line Diagram for voltage level in N state. Response includes switch_states (per-switch open/closed map), injections (per-load/generator active-power baseline) used by the interactive SLD-edit feature, and feeder_labels (per-branch {name, other_vl, label} — label = far-end VL name + parallel index, used to relabel feeders and bridge friendly-named overloads to the SLD cell). |
| POST | /api/contingency-sld |
Single Line Diagram in N-1 state (with flow deltas + switch_states + injections + feeder_labels). |
| POST | /api/action-variant-sld |
SLD in post-action state (with flow deltas, changed_switches, switch_states, injections, feeder_labels). |
| POST | /api/sld-topology-preview |
Target-topology preview SLD for the interactive SLD-edit feature: applies staged switch overrides on a throwaway variant and re-renders with topological colouring (no load flow; stale_flows: true). |
| GET | /api/actions |
Return all available action IDs and descriptions |
| POST | /api/regenerate-overflow-graph |
Regenerate (or serve from cache) the overflow graph in hierarchical / geo layout — drives the toggle on the Overflow Analysis tab |
| POST | /api/simulate-manual-action |
Simulate a specific action against a contingency. Accepts an optional voltage_level_id field used to auto-name switch-only user actions (interactive SLD-edit feature). |
| POST | /api/simulate-and-variant-diagram |
NDJSON stream: {type:"metrics"} then {type:"diagram"} so sidebar updates ahead of the SVG |
| POST | /api/compute-superposition |
Compute combined effect of two actions (superposition theorem) |
| POST | /api/save-session |
Save session folder with JSON snapshot + PDF copy |
| GET | /api/list-sessions |
List available session folders in a directory |
| POST | /api/load-session |
Load session JSON and restore PDFs |
| POST | /api/restore-analysis-context |
Restore analysis context from saved session |
| GET | /api/pick-path |
Open native OS file/directory picker (tkinter subprocess) |
| GET | /results/pdf/{filename} |
Serve generated overflow-graph files from Overflow_Graph/ — HTML (interactive viewer, current default via config.VISUALIZATION_FORMAT="html") or PDF (legacy sessions). URL path kept for backward compatibility. |
Key Patterns & Conventions
Backend
- Singleton services:
network_serviceandrecommender_serviceare module-level singleton instances - Streaming responses: Analysis uses
StreamingResponsewith NDJSON (application/x-ndjson), yielding{"type": "pdf", ...}then{"type": "result", ...}events - AC/DC fallback: Analysis first tries AC load flow; falls back to DC if AC does not converge
- Threaded analysis:
run_analysisruns the computation in a background thread and polls for PDF generation - JSON sanitization: NumPy types are recursively converted to native Python types via
sanitize_for_json() - Unified error contract (D2, 2026-07): every error is
{"detail", "code"}produced in one place (services/api_errors.py,install_error_handlers(app)). Raise a plainHTTPException(code derived from status: 400/404/409/422/500) orAppHTTPException(status, detail, code)when the frontend branches on the failure (e.g.ACTION_RESULT_UNAVAILABLE,STUDY_BUSY). Uncaught exceptions → generic logged 500 (neverstr(e)— it leaks paths). The frontend reads it viafrontend/src/utils/apiError.ts. - Machine-checked API contract (D2, 2026-07):
app.openapi()is snapshotted toexpert_backend/openapi.snapshot.jsonand diffed in CI (scripts/check_openapi_contract.py,test_openapi_contract.py). Regenerate on any deliberate endpoint / model / status change:python scripts/check_openapi_contract.py --write. - Concurrency ownership (D3, 2026-07): a service-level re-entrant lock (
services/service_lock.py,@with_network_lock[_stream]) serializes the variant-switching entry points on the sharedNetwork; overlapping study mutations (config load / analysis) get HTTP 409. Seedocs/architecture/shared-network-concurrency.md. - Mixin → helper-package decomposition (PR #104 / #106):
DiagramMixin,AnalysisMixinandSimulationMixinare thin orchestrators. Pure numerics live inservices/diagram/,services/analysis/andservices/simulation_helpers.pyrespectively — dependency-injected so existing@patchtests keep working. - SVG DOM recycling (PR #108): patch endpoints (
/api/contingency-diagram-patch,/api/action-variant-diagram-patch) return per-branch deltas + optional VL-subtree splices so the frontend can clone the already-mounted N-state SVG instead of re-downloading the full NAD (~80 % faster tab switches on large grids). - Interactive overflow viewer (PR #116, 0.7.0):
services/overflow_overlay.pyinjects a Co-Study4Grid pin / filter overlay (<style>+<script>block) into the upstreamexpert_op4grid_recommenderHTML viewer before serving it from/results/pdf/{filename}.services/analysis/overflow_geo_transform.pyis a pure lxml transform that rewrites the hierarchical-layout SVG to geographic coordinates for the/api/regenerate-overflow-graphtoggle; the geo cache is per-study and cleared onreset(). - Shared diagram helpers:
RecommenderServiceuses_load_network(),_load_layout(),_default_nad_parameters(), and_generate_diagram()to deduplicate diagram generation logic across endpoints - Focused diagrams: The
/api/focused-diagramendpoint resolves an element to its voltage levels and generates a sub-diagram with configurable depth, useful for inspecting specific parts of large grids - Ruff-gated:
pyproject.tomlconfigures a narrowE9+Fruleset (real bugs only); stylistic rules deliberately off
Frontend
- Strict TypeScript:
strict: true,noUnusedLocals,noUnusedParameters,noFallthroughCasesInSwitch - Functional components with React hooks; no external state management library
- Inline styles: Components use inline
styleobjects rather than CSS modules or utility classes - Design tokens (PR #120, 0.7.0): every colour / spacing / typography / radius value lives in
frontend/src/styles/tokens.{css,ts}. Inlinestyleobjects import the typedcolors/space/text/radiusconstants fromtokens.ts; stylesheet rules usevar(--…)fromtokens.css. Raw SVG attribute setters (element.setAttribute('fill', …)) import the hex-valuedpinColors/pinChromeconstants because browsers don't reliably resolvevar(--…)inside SVG presentation attributes. The code-quality gate enforces zero hex literals outside the two token files. - Light / dark theme (0.8.0): theming is a token swap, not per-component overrides — the
useThemehook flips a theme attribute that re-points thetokens.csscustom properties, with a tiny pre-mount script to avoid the first-paint flash. A legibility pass covers the pypowsybl NAD / SLD chrome and the injected overflow-viewer overlay. Seedocs/features/dark-mode.md. - Component architecture (Phase 2 hook extraction, PR #109):
App.tsx(~1400 lines) is the state orchestration hub — it wires all hooks together and handles cross-hook logic (e.g.,handleApplySettings). It should NOT contain large JSX blocks.- Presentational components live in
components/andcomponents/modals/. They receive data and callbacks via typed props; all business logic stays inApp.tsxor in hooks. hooks/useContingencyFetch.tsowns the N-1 diagram fetch pipeline (svgPatch fast-path +/api/contingency-diagramfallback + contingency-change confirm routing).hooks/useDiagramHighlights.tsowns the per-tab SVG highlight pipeline (overload halos, contingency highlight, action targets, delta visuals) + per-tab Flow/Impacts view-mode state.hooks/useOverflowIframe.ts(PR #116, 0.7.0) owns the interactive overflow viewer — iframe lifecycle, layer-toggle state, hierarchical ↔ geo layout switch, postMessage bridge to the host, and the action-pin overlay payload computation.hooks/useSldTopologyEdit.ts(0.8.0) owns the interactive SLD edit flow —editMode(implicit while the SLD is open — no toggle button; read-only on close), stagedpendingStates(switch toggles) +pendingInjections(load / generator active-power retunes),toggle/removeSwitch(es)/setInjection/removeInjection/focusedSwitchId— that turns clicked breakers AND injection retunes into one manual action card (SldInjectionPopoveris the active-power editor bubble). Seedocs/features/sld-topology-edit.md.useSettings.tsexposesSettingsState(all settings values + setters), which is passed wholesale toSettingsModalto avoid 30+ prop-drilling.
- SVG DOM recycling (PR #108):
utils/svgPatch.tsclones the already-mounted N-stateSVGSVGElementand patches only per-branch deltas on N-1 / action tab switches, saving a 12–28 MB SVG re-download and re-parse. - Props-based data flow: State lifted to
App.tsx, passed down via props - ESLint: Flat config (v9+) with
typescript-eslint,react-hooks, andreact-refreshplugins - Unit tests use Vitest + React Testing Library. Isolated component tests (no backend mocking needed) live alongside their components as
*.test.tsxfiles.
Data Flow
- User sets network path + action file path ->
POST /api/configloads the network - Frontend fetches disconnectable branches ->
GET /api/branches - User selects a contingency branch -> N-1 diagram fetched with overload highlighting
- User runs analysis (two-step flow):
- Step 1:
POST /api/run-analysis-step1detects overloads in N-1 state - User selects which overloads to resolve
- Step 2:
POST /api/run-analysis-step2streams PDF event + action results
- Step 1:
- Frontend displays overflow PDF and action cards in ActionFeed panel
- User can star/reject actions, manually simulate others, compute combined pairs
- Action selection triggers
POST /api/action-variant-diagram-> post-action diagram - Session save captures full snapshot to
session.json+ overflow PDF copy
Session Save/Load
- Save:
buildSessionResult()insessionUtils.tsserializes all state (config, contingency, actions with status tags, combined pairs) ->POST /api/save-sessionwrites to disk - Load:
POST /api/load-sessionreadssession.json-> frontend restores all state without re-simulating actions - Output folder:
<output_folder>/costudy4grid_session_<contingency>_<timestamp>/containssession.json+ overflow PDF - Interaction logging: Every user interaction is logged as a timestamped, replay-ready event via
interactionLogger. Saved asinteraction_log.jsonalongsidesession.json. - See
docs/features/save-results.mdfor session save/load,docs/features/interaction-logging.mdfor the replay contract, anddocs/features/action-overview-diagram.mdfor the Remedial Action overview (pin overlay on N-1 network)
SVG Visualization
Both the React frontend and the auto-generated
frontend/dist-standalone/standalone.html render pypowsybl
NAD/SLD payloads:
- Dynamic text scaling (
utils/svgUtils.ts:boostSvgForLargeGrid/ standaloneboostSvgForLargeGrid): font sizes for node labels, edge info, and legends scale proportionally to diagram size viasqrt(diagramSize / referenceSize), so text is readable when zoomed in and naturally invisible at full zoom-out. Engaged for grids ≥ 500 voltage levels. - Bus / transformer scaling: circle radii for bus nodes and transformer windings are boosted proportionally.
- Edge-info scaling: flow values and arrow glyphs are scaled via transform groups so they remain proportional to the line on which they sit.
- ViewBox zoom: auto-centers on selected contingency targets with adjustable padding.
- Pan/zoom: the
usePanZoomhook writes the SVGviewBoxdirectly (rAF-batched, cached CTM) — no pan/zoom library — in both the React dev build and the auto-generated standalone (they share the same source tree).
Dependencies
Backend (pyproject.toml + overrides.txt)
fastapi,uvicorn,python-multipartpypowsybl,expert_op4grid_recommender(expected in venv)pandas>=2.2.2,numpy>=2.0.0,grid2op>=1.12.2,pandapower>=2.14.0lightsim2grid>=0.12.0,matplotlib>=3.10.6,scipy>=1.16.0lxml>=6.0.0,contourpy>=1.2.0,tqdm>=4.65.0
Frontend (frontend/package.json)
- See
dependenciesanddevDependenciesinfrontend/package.json
File Conventions
- Network data files:
.xiidmformat (loaded by pypowsybl) - Action definitions:
.jsonfiles with action IDs mapping to descriptions - Generated outputs: PDF files in
Overflow_Graph/directory - Network layouts:
grid_layout.json(node ID -> [x, y] coordinates)
Notes for AI Assistants
- The backend API base URL defaults to
http://127.0.0.1:8000infrontend/src/api.ts(API_BASE_URL), overridable at build time viaVITE_API_BASE_URL— set it to""for same-origin hosting where the backend serves the SPA (the HuggingFace Docker Space), so requests become relative/api/... - CORS defaults to the local Vite dev/preview origins on loopback (
localhost/127.0.0.1on:5173/:4173); a wildcard is explicit opt-in and any other set is configurable via theCORS_ALLOWED_ORIGINSenv var (see.env.example) - Frontend architecture (Phase 2 hook extraction, PR #109):
App.tsxis the state orchestration hub; it must NOT contain large inline JSX blocks. Extracted presentational components live incomponents/andcomponents/modals/; cross-cutting state pipelines live inhooks/(notablyuseContingencyFetchanduseDiagramHighlights). When adding new UI sections, create a new component file (or hook for stateful pipelines) and wire it inApp.tsx. useSettingshook: Exposes aSettingsStateobject with all settings fields + setters. This is passed wholesale toSettingsModalto avoid excessive prop drilling. Adding a new setting means: (1) add touseSettings.ts, (2) add toSettingsModal.tsx. No manual standalone mirror is required — the legacy hand-maintained file has been decommissioned and the auto-generated bundle inherits from the React source automatically.- Standalone bundle (auto-generated):
npm run build:standaloneinfrontend/producesfrontend/dist-standalone/standalone.html— a single-file HTML with React + CSS inlined viavite-plugin-singlefile. This is the canonical distribution artifact replacing the formerstandalone_interface.html. The legacy file remains on disk asstandalone_interface_legacy.html(tracked as a frozen snapshot — do NOT edit). - Online deployment (HuggingFace Docker Space, 0.8.0):
Dockerfilebuilds the SPA withVITE_API_BASE_URL=""+VITE_GAME_MODE=1and serves it same-origin with the FastAPI backend on port 7860.main.pyoptionally mounts the built SPA viaCOSTUDY4GRID_FRONTEND_DIST(mounted LAST, after every/api/*and/results/*route; inert when the dist is absent, so local dev is unaffected). One Space instance serves one player (module-level singletons). Seedeploy/huggingface/. - Deployment lockdown (D7, 2026-07): the
DockerfilesetsCOSTUDY4GRID_LOCKDOWN=1, which disables the desktop-era filesystem RPCs (custom config-file path, session save/list/load, native file picker) with a403 {code: LOCKED_DOWN}— they assume a local operator and would otherwise expose the container filesystem to an anonymous Space visitor. The read-only app config stays available so the SPA boots; unset locally so dev is unaffected. The HF deploy is test-gated (workflow_runon a green Tests run) and tags each deploy on origin (space-deploy-*) as the rollback pointer. Seedocs/architecture/deployment-trust.md. - Game Mode (0.8.0): a timed, scored session shell in
frontend/src/game/, additive and inert unless?game=1—main.tsxmountsGameShellinstead ofApp; App integration is threegameBridge.isGameMode()-guarded touch points. Seedocs/features/game-mode-codabench.md. - Binary assets via Git LFS + transparent network decompression (0.8.0):
.gitattributestracks*.zip/*.png/*.jpgvia Git LFS (the HuggingFace Space git endpoint rejects non-LFS binaries); the large France 225/400 kV grid ships asnetwork.xiidm.zipandnetwork_service._resolve_network_file/_extract_network_zipdecompress it transparently on load. - CI pipelines: GitHub Actions only (
.github/workflows/:code-quality.yml,parity.yml,test.yml,canary.yml,deploy-huggingface.yml) run the code-quality gate, ruff, the pytest + Vitest suites, the data-pipeline + scorer-parity slice, and the parity scripts. CircleCI was removed (QW23 — GitHub Actions is a strict superset). The backend test + theDockerfileinstallexpert_op4grid_recommenderfromrecommender-pin.txt— one pinned, exact version (QW8) so an upstream release can't silently break a PR or a rebuild; the weeklycanary.ymlfloats to the latest release and flags regressions, so upgrades are a deliberate bump of that file. - Root
.gitignoreexcludes__pycache__/,*.pyc,*.pyo;frontend/.gitignorehandles frontend build artifacts - Integration helpers and parity scripts live under
scripts/. They are NOT part of the pytest suite — invoke them directly. The PyPSA-EUR pipeline scripts underscripts/pypsa_eur/DO carry pytest coverage (test_build_pipeline.py,test_calibrate_thermal_limits.py,test_generate_n1_overloads.py,test_regenerate_grid_layout.py). overrides.txtcontains pinned versions for transitive Python dependencies that need to be forced to specific versions- Frontend unit tests use Vitest + React Testing Library. Isolated component tests live as
*.test.tsxfiles next to their component. Run withcd frontend && npm run test. No backend mocking is needed for component tests since they only use mocked props. - The two-step analysis flow (step1: detect overloads, step2: resolve) is the primary user workflow; the single-step
/api/run-analysisis a legacy alternative - Session save/load is documented in
docs/features/save-results.md grid_layout.jsoncoordinate scale (2026-05-08): the on-disk layout MUST be in raw Mercator metres (span ≈ 1.4–1.6 M for the French grid). pypowsybl emits VL outer circles at a fixedr = 27.5user-space units, so any layout squashed below ~500 000 units forces overlap on dense regions (Paris/Lyon).scripts/pypsa_eur/regenerate_grid_layout.pydefaults to raw metres; the legacy--target-width 8000flag is preserved but warns. Full rationale + operator-vs-PyPSA comparison indocs/data/grid-layout-coordinate-scale.md.
Contributing & Pull Requests
Upstream is
ainetus;marotais the working fork. Development branches are pushed tomarota/Co-Study4Grid, but pull requests are opened directly against the upstreamainetus/Co-Study4Grid(base = its default branch, head =marota:<branch>) — not againstmarota. The sibling libraryExpert_op4grid_recommenderfollows the same rule againstainetus/Expert_op4grid_recommender.Load
ainetusas an initial source. A cross-fork PR intoainetuscan only be created from a session/tool context that has theainetusrepo in scope, so a new working session should be started withainetus/Co-Study4Gridandainetus/Expert_op4grid_recommenderas the initial sources (they should always be auto-loaded). A session rooted only atmarotacannot targetainetus(cross-tier adds are blocked) and the PR step fails with an access-denied error.Sync
marotawithainetusbefore starting new work. PRs merge intoainetus/main, but development happens onmarota, somarota/maindrifts behindainetus/mainafter every merged PR (this applies to both repos — Co-Study4Grid and Expert_op4grid_recommender). At the start of a dev session, bringmarota/mainup to date withainetus/main— GitHub "Sync fork", or locallygit fetch ainetus main && git merge --ff-only ainetus/mainthen pushmarota/main— and branch from there. Skipping this makes a new branch collide with the already-merged revisions when it is PR'd intoainetus. If the sync was missed and the PR already shows conflicts, mergeainetus/maininto the branch (or rebase onto it) and resolve, then force-with-lease push.DCO sign-off is required on every commit. The
ainetusrepos enforce the Developer Certificate of Origin: every commit must carry aSigned-off-by: <Name> <amarot91@gmail.com>trailer, and because the DCO check matches the sign-off against the commit author, the commit must also be authored under that same identity (author email =amarot91@gmail.com):git config user.name "<Name>" git config user.email "amarot91@gmail.com" git commit -s -m "..." # -s appends the Signed-off-by trailerTo sign off commits already made under a different identity, re-author and add the trailer (
git rebase --exec 'git commit --amend --no-edit --reset-author \ -s' <base>), then force-with-lease push.
Standalone Interface Parity Audit
The detailed audit — feature inventory, mirror-status table, Layer
1–4 conformity findings, regression-guard matrix, gap-priority list
and delta-vs-previous commits — lives in
frontend/PARITY_AUDIT.md. That
document is the working record of the parity project and is
updated as fixes land.
Quick status summary (2026-05-05):
- Canonical distribution is now the auto-generated
frontend/dist-standalone/standalone.html(npm run build:standalone). The hand-maintainedstandalone_interface.htmlhas been decommissioned and renamed tostandalone_interface_legacy.html— committed as a frozen snapshot of its last version (commit5d2b9d1content), do NOT edit further. Regenerate UI fromfrontend/src/vianpm run build:standaloneinstead. The standalone versioned snapshot was bumped to v0.7 onadae7acto include references to the new/api/*-diagram-patchendpoints. - Four parity layers run against the React source + the
standalone of choice:
- Layer 1 — static parity (
scripts/check_standalone_parity.py) - Layer 2 — session-reload fidelity (
scripts/check_session_fidelity.py) - Layer 3a — gesture-sequence static proxy (
scripts/check_gesture_sequence.py) - Layer 3b — behavioural E2E (
scripts/parity_e2e/e2e_parity.spec.ts) - Layer 4 — user-observable invariants (
scripts/check_invariants.py)
- Layer 1 — static parity (
- All parity scripts accept
COSTUDY4GRID_STANDALONE_PATHto re-target any artifact; they default to the auto-gen bundle and fall back to the legacy file when the auto-gen is not built.
See frontend/PARITY_AUDIT.md for
the full gap list, the session-fidelity regression record, the
honest-gap report of what each layer catches and misses, and the
2026-04-20 delta that documents the /api/restore-analysis-context
one-way API drift (now resolved) and the auto-generated-standalone
viability confirmation.