Spaces:
Sleeping
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 | |
| ```bash | |
| # 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 | |
| ```bash | |
| 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 | |
| ```bash | |
| 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): | |
| ```bash | |
| 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: | |
| ```bash | |
| 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: | |
| ```bash | |
| cd frontend | |
| npm run test # Run Vitest test suite | |
| ``` | |
| ### Code-Quality Checks (continuous reporting) | |
| ```bash | |
| # 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`](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_service` and `recommender_service` are module-level singleton instances | |
| - **Streaming responses**: Analysis uses `StreamingResponse` with 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_analysis` runs 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 plain `HTTPException` (code derived from status: 400/404/409/422/500) or `AppHTTPException(status, detail, code)` when the frontend branches on the failure (e.g. `ACTION_RESULT_UNAVAILABLE`, `STUDY_BUSY`). Uncaught exceptions → generic logged 500 (never `str(e)` — it leaks paths). The frontend reads it via `frontend/src/utils/apiError.ts`. | |
| - **Machine-checked API contract (D2, 2026-07)**: `app.openapi()` is snapshotted to `expert_backend/openapi.snapshot.json` and 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 shared `Network`; overlapping study mutations (config load / analysis) get **HTTP 409**. See [`docs/architecture/shared-network-concurrency.md`](docs/architecture/shared-network-concurrency.md). | |
| - **Mixin → helper-package decomposition (PR #104 / #106)**: `DiagramMixin`, `AnalysisMixin` and `SimulationMixin` are thin orchestrators. Pure numerics live in `services/diagram/`, `services/analysis/` and `services/simulation_helpers.py` respectively — dependency-injected so existing `@patch` tests 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.py` injects a Co-Study4Grid pin / filter overlay (`<style>` + `<script>` block) into the upstream `expert_op4grid_recommender` HTML viewer before serving it from `/results/pdf/{filename}`. `services/analysis/overflow_geo_transform.py` is a pure lxml transform that rewrites the hierarchical-layout SVG to geographic coordinates for the `/api/regenerate-overflow-graph` toggle; the geo cache is per-study and cleared on `reset()`. | |
| - **Shared diagram helpers**: `RecommenderService` uses `_load_network()`, `_load_layout()`, `_default_nad_parameters()`, and `_generate_diagram()` to deduplicate diagram generation logic across endpoints | |
| - **Focused diagrams**: The `/api/focused-diagram` endpoint 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.toml` configures a narrow `E9` + `F` ruleset (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 `style` objects 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}`. Inline `style` objects import the typed `colors` / `space` / `text` / `radius` constants from `tokens.ts`; stylesheet rules use `var(--…)` from `tokens.css`. Raw SVG attribute setters (`element.setAttribute('fill', …)`) import the hex-valued `pinColors` / `pinChrome` constants because browsers don't reliably resolve `var(--…)` 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 `useTheme` hook flips a theme attribute that re-points the `tokens.css` custom 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. See [`docs/features/dark-mode.md`](docs/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/` and `components/modals/`. They receive data and callbacks via typed props; all business logic stays in `App.tsx` or in hooks. | |
| - `hooks/useContingencyFetch.ts` owns the N-1 diagram fetch pipeline (svgPatch fast-path + `/api/contingency-diagram` fallback + contingency-change confirm routing). | |
| - `hooks/useDiagramHighlights.ts` owns 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), staged `pendingStates` (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 (`SldInjectionPopover` is the active-power editor bubble). See [`docs/features/sld-topology-edit.md`](docs/features/sld-topology-edit.md). | |
| - `useSettings.ts` exposes `SettingsState` (all settings values + setters), which is passed wholesale to `SettingsModal` to avoid 30+ prop-drilling. | |
| - **SVG DOM recycling (PR #108)**: `utils/svgPatch.ts` clones the already-mounted N-state `SVGSVGElement` and 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`, and `react-refresh` plugins | |
| - **Unit tests** use Vitest + React Testing Library. Isolated component tests (no backend mocking needed) live alongside their components as `*.test.tsx` files. | |
| ### Data Flow | |
| 1. User sets network path + action file path -> `POST /api/config` loads the network | |
| 2. Frontend fetches disconnectable branches -> `GET /api/branches` | |
| 3. User selects a contingency branch -> N-1 diagram fetched with overload highlighting | |
| 4. User runs analysis (two-step flow): | |
| - Step 1: `POST /api/run-analysis-step1` detects overloads in N-1 state | |
| - User selects which overloads to resolve | |
| - Step 2: `POST /api/run-analysis-step2` streams PDF event + action results | |
| 5. Frontend displays overflow PDF and action cards in ActionFeed panel | |
| 6. User can star/reject actions, manually simulate others, compute combined pairs | |
| 7. Action selection triggers `POST /api/action-variant-diagram` -> post-action diagram | |
| 8. Session save captures full snapshot to `session.json` + overflow PDF copy | |
| ### Session Save/Load | |
| - **Save**: `buildSessionResult()` in `sessionUtils.ts` serializes all state (config, contingency, actions with status tags, combined pairs) -> `POST /api/save-session` writes to disk | |
| - **Load**: `POST /api/load-session` reads `session.json` -> frontend restores all state without re-simulating actions | |
| - **Output folder**: `<output_folder>/costudy4grid_session_<contingency>_<timestamp>/` contains `session.json` + overflow PDF | |
| - **Interaction logging**: Every user interaction is logged as a timestamped, replay-ready event via `interactionLogger`. Saved as `interaction_log.json` alongside `session.json`. | |
| - See `docs/features/save-results.md` for session save/load, `docs/features/interaction-logging.md` for the replay contract, and `docs/features/action-overview-diagram.md` for 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` / | |
| standalone `boostSvgForLargeGrid`): font sizes for node labels, edge | |
| info, and legends scale proportionally to diagram size via | |
| `sqrt(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 `usePanZoom` hook writes the SVG `viewBox` | |
| directly (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-multipart` | |
| - `pypowsybl`, `expert_op4grid_recommender` (expected in venv) | |
| - `pandas>=2.2.2`, `numpy>=2.0.0`, `grid2op>=1.12.2`, `pandapower>=2.14.0` | |
| - `lightsim2grid>=0.12.0`, `matplotlib>=3.10.6`, `scipy>=1.16.0` | |
| - `lxml>=6.0.0`, `contourpy>=1.2.0`, `tqdm>=4.65.0` | |
| ### Frontend (`frontend/package.json`) | |
| - See `dependencies` and `devDependencies` in `frontend/package.json` | |
| ## File Conventions | |
| - Network data files: `.xiidm` format (loaded by pypowsybl) | |
| - Action definitions: `.json` files 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:8000` in `frontend/src/api.ts` (`API_BASE_URL`), overridable at build time via `VITE_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.1` on `:5173` / `:4173`); a wildcard is explicit opt-in and any other set is configurable via the `CORS_ALLOWED_ORIGINS` env var (see `.env.example`) | |
| - **Frontend architecture (Phase 2 hook extraction, PR #109)**: `App.tsx` is the state orchestration hub; it must NOT contain large inline JSX blocks. Extracted presentational components live in `components/` and `components/modals/`; cross-cutting state pipelines live in `hooks/` (notably `useContingencyFetch` and `useDiagramHighlights`). When adding new UI sections, create a new component file (or hook for stateful pipelines) and wire it in `App.tsx`. | |
| - **`useSettings` hook**: Exposes a `SettingsState` object with all settings fields + setters. This is passed wholesale to `SettingsModal` to avoid excessive prop drilling. Adding a new setting means: (1) add to `useSettings.ts`, (2) add to `SettingsModal.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:standalone` in `frontend/` produces `frontend/dist-standalone/standalone.html` — a single-file HTML with React + CSS inlined via `vite-plugin-singlefile`. This is the canonical distribution artifact replacing the former `standalone_interface.html`. The legacy file remains on disk as `standalone_interface_legacy.html` (tracked as a frozen snapshot — do NOT edit). | |
| - **Online deployment (HuggingFace Docker Space, 0.8.0)**: `Dockerfile` builds the SPA with `VITE_API_BASE_URL=""` + `VITE_GAME_MODE=1` and serves it **same-origin** with the FastAPI backend on port 7860. `main.py` optionally mounts the built SPA via `COSTUDY4GRID_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). See `deploy/huggingface/`. | |
| - **Deployment lockdown (D7, 2026-07)**: the `Dockerfile` sets `COSTUDY4GRID_LOCKDOWN=1`, which disables the desktop-era filesystem RPCs (custom config-file path, session save/list/load, native file picker) with a `403 {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_run` on a green Tests run) and **tags each deploy** on origin (`space-deploy-*`) as the rollback pointer. See [`docs/architecture/deployment-trust.md`](docs/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.tsx` mounts `GameShell` instead of `App`; App integration is three `gameBridge.isGameMode()`-guarded touch points. See `docs/features/game-mode-codabench.md`. | |
| - **Binary assets via Git LFS + transparent network decompression (0.8.0)**: `.gitattributes` tracks `*.zip` / `*.png` / `*.jpg` via Git LFS (the HuggingFace Space git endpoint rejects non-LFS binaries); the large France 225/400 kV grid ships as `network.xiidm.zip` and `network_service._resolve_network_file` / `_extract_network_zip` decompress 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 + the `Dockerfile` install `expert_op4grid_recommender` from **`recommender-pin.txt`** — one pinned, exact version (QW8) so an upstream release can't silently break a PR or a rebuild; the weekly `canary.yml` floats to the latest release and flags regressions, so upgrades are a deliberate bump of that file. | |
| - Root `.gitignore` excludes `__pycache__/`, `*.pyc`, `*.pyo`; `frontend/.gitignore` handles 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 under `scripts/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.txt` contains 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.tsx` files next to their component. Run with `cd 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-analysis` is a legacy alternative | |
| - Session save/load is documented in `docs/features/save-results.md` | |
| - **`grid_layout.json` coordinate 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 *fixed* `r = 27.5` user-space units, so any layout squashed below ~500 000 units forces overlap on dense regions (Paris/Lyon). `scripts/pypsa_eur/regenerate_grid_layout.py` defaults to raw metres; the legacy `--target-width 8000` flag is preserved but warns. Full rationale + operator-vs-PyPSA comparison in [`docs/data/grid-layout-coordinate-scale.md`](docs/data/grid-layout-coordinate-scale.md). | |
| --- | |
| ## Contributing & Pull Requests | |
| - **Upstream is `ainetus`; `marota` is the working fork.** Development branches | |
| are pushed to `marota/Co-Study4Grid`, but **pull requests are opened directly | |
| against the upstream `ainetus/Co-Study4Grid`** (base = its default branch, | |
| head = `marota:<branch>`) — *not* against `marota`. The sibling library | |
| `Expert_op4grid_recommender` follows the same rule against | |
| `ainetus/Expert_op4grid_recommender`. | |
| - **Load `ainetus` as an initial source.** A cross-fork PR into `ainetus` can | |
| only be created from a session/tool context that has the `ainetus` repo in | |
| scope, so a new working session should be started with | |
| **`ainetus/Co-Study4Grid` and `ainetus/Expert_op4grid_recommender` as the | |
| initial sources** (they should always be auto-loaded). A session rooted only | |
| at `marota` cannot target `ainetus` (cross-tier adds are blocked) and the PR | |
| step fails with an access-denied error. | |
| - **Sync `marota` with `ainetus` before starting new work.** PRs merge into | |
| `ainetus/main`, but development happens on `marota`, so `marota/main` drifts | |
| behind `ainetus/main` after every merged PR (this applies to **both** repos — | |
| Co-Study4Grid and Expert_op4grid_recommender). **At the start of a dev session, | |
| bring `marota/main` up to date with `ainetus/main`** — GitHub "Sync fork", or | |
| locally `git fetch ainetus main && git merge --ff-only ainetus/main` then push | |
| `marota/main` — and branch from there. Skipping this makes a new branch collide | |
| with the already-merged revisions when it is PR'd into `ainetus`. If the sync | |
| was missed and the PR already shows conflicts, merge `ainetus/main` into the | |
| branch (or rebase onto it) and resolve, then force-with-lease push. | |
| - **DCO sign-off is required on every commit.** The `ainetus` repos enforce the | |
| [Developer Certificate of Origin](https://developercertificate.org/): every | |
| commit must carry a `Signed-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`): | |
| ```bash | |
| git config user.name "<Name>" | |
| git config user.email "amarot91@gmail.com" | |
| git commit -s -m "..." # -s appends the Signed-off-by trailer | |
| ``` | |
| To 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`](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-maintained | |
| `standalone_interface.html` has been decommissioned and | |
| renamed to `standalone_interface_legacy.html` — committed as | |
| a frozen snapshot of its last version (commit `5d2b9d1` content), | |
| do NOT edit further. Regenerate UI from `frontend/src/` via | |
| `npm run build:standalone` instead. The standalone versioned | |
| snapshot was bumped to v0.7 on `adae7ac` to include references | |
| to the new `/api/*-diagram-patch` endpoints. | |
| - 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`) | |
| - All parity scripts accept `COSTUDY4GRID_STANDALONE_PATH` to | |
| 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`](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. | |