Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions .claude/hooks/init-sync-check.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#!/usr/bin/env bash
#
# SessionStart init check — git sync verification for marota-fork sessions.
#
# Runs automatically at the start of every Claude Code session. It performs a
# REAL `git fetch` of the repository's default branch and reports whether the
# current working branch is in sync with it (ahead / behind counts), so a new
# session never assumes it is up to date based on a possibly-stale local
# `origin/<default>` ref.
#
# Design contract:
# * READ-ONLY — never mutates the working tree, index, or any branch
# (only `git fetch`, which updates remote-tracking refs).
# * NON-FATAL — any failure (no network, no git, detached HEAD, …) exits 0
# so it can never block or break session startup.
# * SCOPED — only emits the detailed check for `marota/*` origins, per
# the "starting a new session from a marota fork" request;
# a no-op for every other remote.
# * SYNCHRONOUS — fast (one time-boxed fetch); stdout is surfaced to the
# session as startup context.
#
set -uo pipefail

# Drain hook stdin (SessionStart passes a JSON payload we don't need) so the
# pipe closes cleanly.
cat >/dev/null 2>&1 || true

repo_root="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || true)}"
[ -z "${repo_root}" ] && exit 0
cd "${repo_root}" 2>/dev/null || exit 0

# Only act inside a git work tree.
git rev-parse --is-inside-work-tree >/dev/null 2>&1 || exit 0

origin_url="$(git remote get-url origin 2>/dev/null || true)"

# Guard: only run for marota forks. Everything else is a silent no-op.
case "${origin_url}" in
*marota/*) : ;;
*) exit 0 ;;
esac

# Resolve the default branch: origin/HEAD -> `git remote show` -> main.
default_branch="$(git symbolic-ref --quiet --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##')"
if [ -z "${default_branch}" ]; then
default_branch="$(git remote show origin 2>/dev/null | sed -n 's/.*HEAD branch: //p' | head -1)"
fi
[ -z "${default_branch}" ] && default_branch="main"

current_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null || echo '(detached)')"

# Do a REAL fetch — never trust the local origin ref. Time-boxed + non-fatal.
fetch_status="ok"
if command -v timeout >/dev/null 2>&1; then
timeout 60 git fetch --quiet origin "${default_branch}" 2>/dev/null || fetch_status="failed"
else
git fetch --quiet origin "${default_branch}" 2>/dev/null || fetch_status="failed"
fi

counts="$(git rev-list --left-right --count "origin/${default_branch}...HEAD" 2>/dev/null || true)"
behind="$(printf '%s' "${counts}" | awk '{print $1}')"
ahead="$(printf '%s' "${counts}" | awk '{print $2}')"

echo "── Session init: git sync check (marota fork) ─────────────────────────"
echo "repo: $(basename "${repo_root}")"
echo "current branch: ${current_branch}"
echo "default branch: origin/${default_branch}"
echo "origin fetch: ${fetch_status}"
if [ -n "${counts}" ]; then
echo "vs origin/${default_branch}: ${behind:-?} behind, ${ahead:-?} ahead"
if [ "${behind:-0}" -gt 0 ] 2>/dev/null; then
echo "ACTION: branch is BEHIND origin/${default_branch} by ${behind} commit(s)."
echo " Rebase onto the latest default before pushing follow-up work:"
echo " git fetch origin ${default_branch} && git rebase origin/${default_branch}"
elif [ "${ahead:-0}" -eq 0 ] 2>/dev/null; then
echo "STATUS: in sync — branch tip equals origin/${default_branch}"
echo " (fresh branch, no unmerged work). If a prior PR for this branch"
echo " was already merged, treat new work as a fresh change on this base."
else
echo "STATUS: ${ahead} local commit(s) ahead of origin/${default_branch}, 0 behind — up to date."
fi
else
echo "NOTE: could not compute ahead/behind (origin/${default_branch} missing or fetch failed)."
fi
echo "PRINCIPLE: always run a real 'git fetch' to confirm sync against the remote —"
echo " do not rely on a possibly-stale local origin ref."
echo "───────────────────────────────────────────────────────────────────────"

exit 0
14 changes: 14 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/init-sync-check.sh"
}
]
}
]
}
}
63 changes: 50 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,72 @@ permissions:

jobs:
build-and-test:
name: Build and test
name: Build and test (py${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Matches the classifiers (3.10–3.12). `numpy>=2.0,<3` in
# requirements.txt lets the resolver pick a 3.10-compatible numpy (2.3
# dropped 3.10). fail-fast:false so a surprise 3.10 wheel gap doesn't
# mask the 3.11/3.12 results.
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4

- name: Set up Python 3.12
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: "3.12"
python-version: ${{ matrix.python-version }}
cache: pip

- name: Install dependencies
# Mirrors the CircleCI "Install Dependencies" step: install runtime +
# test deps from requirements.txt, then numpy (which requirements.txt
# omits — CircleCI pinned it separately).
# requirements.txt now includes numpy (was an ad-hoc `pip install
# numpy==2.3.0` step here). pypowsybl2grid is deprecated & no longer
# listed (its numpy==1.26.4 pin was unsatisfiable against numpy>=2.0.0).
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install numpy==2.3.0

- name: Apply pypowsybl2grid patch
# Patches the installed pypowsybl2grid backend to fix zero-value
# handling (see CLAUDE.md gotcha #6). Must run before the tests import
# the backend. --debug prints the file-content analysis.
run: python scripts/patch_pypowsybl2grid_file.py --debug

- name: Run unit tests (exclude slow)
run: pytest -m "not slow"

grid2op-optional:
name: grid2op-optional contract (pypowsybl-only)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Python 3.12
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip

- name: Install base package, then remove the grid2op family
# expertop4grid pulls grid2op + lightsim2grid transitively, so a base
# install is NOT grid2op-free; remove the grid2op family to prove the
# pure-pypowsybl path imports and runs without it (enforces the PR #26
# optionality contract).
run: |
python -m pip install --upgrade pip
pip install -e .
pip uninstall -y grid2op lightsim2grid || true

- name: Assert grid2op is absent and the pypowsybl path imports without it
run: |
python - <<'PY'
import importlib.util, sys
assert importlib.util.find_spec("grid2op") is None, "grid2op should be uninstalled in this leg"
import expert_op4grid_recommender # package import must not need grid2op
from expert_op4grid_recommender.pipeline import run_analysis, run_analysis_step1 # noqa: F401
from expert_op4grid_recommender.backends import make_backend, Backend
b = make_backend(Backend.PYPOWSYBL)
assert type(b).__name__ == "PypowsyblBackend", type(b).__name__
assert "grid2op" not in sys.modules, "importing the pypowsybl path must not import grid2op"
print("grid2op-optional contract OK")
PY

quality:
name: Static analysis
# Static analysis only (ruff / interrogate / radon parse the code, they do
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,7 @@ venv*/
# artefacts non versionnés sur le Space (profiling, slides)
*.prof
*.pptx

# Runtime-generated overflow graphs (PDF/HTML/.dot written during an analysis;
# e.g. by scripts/benchmark_pipeline.py or an app run) — never a deliverable.
/Overflow_Graph/
79 changes: 79 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,85 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.3.0] - 2026-07-07

Follow-up slice of the 2026-07 full code review (findings M3–M6, M8, P3–P4,
and the R7 IHM promotion), on top of 0.2.9. Behaviour-preserving for the
analysis pipeline; the maneuver IHM gains a production serving path and the
dependency floors are reconciled. See `docs/release-notes/v0.3.0.md`.

### Added

- **Vendored, import-time pypowsybl integer-value patch (M5).**
`expert_op4grid_recommender/patched_backend.py` applies the
`update_integer_value` `0 → −1` fix on `pypowsybl.grid2op.Backend` via an
idempotent, version-guarded class patch at package import
(`apply_pypowsybl_integer_value_patch()`). No site-packages edit is required;
the standalone `scripts/patch_pypowsybl2grid_file.py` remains only as a manual
fallback. It is a no-op when pypowsybl is absent and self-disables if a future
upstream ships the fix. New coverage: `tests/test_patched_backend.py`. (The
`make_patched_pypowsybl_backend` factory depends on `pypowsybl2grid`, now
deprecated — see below.)
- **Maneuver IHM as a serveable app (R7 partial / M8).** `scripts/manoeuvre_ihm.py`
gains a `create_app()` application factory, a production `waitress` serving
path, and a `/healthz` endpoint, so the Flask IHM can be launched as a proper
WSGI app rather than only via the debug server.

### Changed

- **Observability: stop hijacking the root logger (M6).** The package no longer
reconfigures the root logger on import; it logs through its own named logger,
so importing `expert_op4grid_recommender` no longer changes a host
application's logging. `utils/action_rebuilder.py` now re-raises on failure
instead of swallowing the error and returning a partial result.
- **Test infrastructure hardening (M3).** `--strict-markers` plus a registered
`slow` marker (an unregistered mark now errors at collection instead of
silently running as a fast test), opt-in coverage config
(`pytest --cov=expert_op4grid_recommender`), and repair of two dead/broken
tests — including the `get_maintenance_timestep` mock test, which had never
run because it shared a name with the `@slow` real-env test that shadowed it.
- **Dependency declarations synced + grid2op-optional contract enforced (M4).**
`pyproject.toml` / `requirements.txt` floors reconciled (`expertop4grid >= 0.3.2`,
explicit `pydantic` / `pydantic-settings`), the Grid2Op backend's direct deps
moved to a `[grid2op]` extra, and a CI leg that removes the grid2op family and
asserts the pure-pypowsybl pipeline still imports/runs (the PR #26 optionality
contract).

### Deprecated / Removed

- **`pypowsybl2grid` deprecated and dropped as a dependency.** Its `0.3.0` wheel
pins `numpy==1.26.4`, which is unsatisfiable against this package's
`numpy>=2.0.0` and broke the install resolve (in CI and anywhere numpy 2 is
required). It is only used by the legacy grid2op+pypowsybl "assistant env"
bridge (`utils/make_assistant_env.create_pypowsybl_backend` →
`patched_backend.make_patched_pypowsybl_backend`); the pure-pypowsybl and
grid2op(lightsim2grid) analysis paths never needed it. It is removed from
`requirements.txt` and `pyproject.toml`, the CI `pypowsybl2grid` patch step is
dropped, the import-time version guard in `utils/make_env_utils.py` is softened
to a `DeprecationWarning`, and the two bridge entry points now emit a
`DeprecationWarning`. Install `pypowsybl2grid` manually into a `numpy<2`
environment if you still need that legacy path. The generic
`apply_pypowsybl_integer_value_patch()` (on `pypowsybl.grid2op.Backend`) is
unaffected.

### Performance

- **NetworkManager name-array caching + `set_thermal_limit` O(n²) fix (P4).**
The `name_line` / `name_sub` / … arrays are cached instead of rebuilt on every
property access, and `set_thermal_limit` no longer rescans all branches per
element.
- **Cached membership sets in action application (P3).** Repeated per-element
`in` scans over full element lists are replaced by precomputed sets in the
pypowsybl action-application path.

### Development

- **SessionStart git-sync init check.** `.claude/hooks/init-sync-check.sh`
(+ `.claude/settings.json`) is a read-only, non-fatal check that, when a
session starts on the `marota` fork, fetches and reports how far the branch is
from upstream and reminds that PRs target `ainetus`. Runtime-generated
`Overflow_Graph/` artifacts are now gitignored.

## [0.2.9] - 2026-07-06

### Changed
Expand Down
73 changes: 60 additions & 13 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ expert_op4grid_recommender/
│ # re-exported as module attrs for back-compat)
├── config_basic.py # One alternative config variant (Settings(...) with overrides)
├── exceptions.py # Domain exceptions (LoadFlowDivergedError)
├── patched_backend.py # Import-time, version-guarded fix for pypowsybl's
│ # grid2op backend update_integer_value (0→−1);
│ # replaces the site-packages patch script (M5)
├── environment.py # Grid2Op environment setup
├── environment_pypowsybl.py # Pure pypowsybl environment setup
├── data_loader.py # Load action dictionaries from JSON
Expand Down Expand Up @@ -479,19 +482,44 @@ pytest tests/test_ActionClassifier.py::test_specific # Single test

**Core:**
- `numpy >= 2.0.0`, `scipy >= 1.13.0`, `pandas`, `networkx`
- `pypowsybl >= 1.13.0`, `pypowsybl2grid >= 0.2.1`
- `expertop4grid >= 0.2.8` (contains alphaDeesp)
- `matplotlib >= 3.8.0`

**Test:**
- `pytest`
- `pypowsybl >= 1.13.0` (`pypowsybl2grid` is **deprecated** and no longer a
dependency as of 0.3.0 — its `numpy==1.26.4` pin conflicts with `numpy>=2.0.0`;
it is only needed by the legacy grid2op+pypowsybl assistant-env bridge, install
it manually in a `numpy<2` env if required)
- `expertop4grid >= 0.3.2` (contains alphaDeesp; pulls grid2op + lightsim2grid
transitively — so a base install is not grid2op-free at the wheel level)
- `matplotlib >= 3.8.0`, `pydantic >= 2.0`, `pydantic-settings >= 2.0`

**Extras:**
- `[grid2op]` — `grid2op >= 1.12.1`, `LightSim2Grid >= 0.10.3` (the Grid2Op
backend's *direct* deps, made explicit). `requirements.txt` pins the whole
set for reproducible CI; the CI `grid2op-optional` leg installs the base
package, removes the grid2op family, and asserts the pure-pypowsybl pipeline
still imports/runs (the PR #26 optionality contract).
- `[test]` — `pytest`; `[quality]` — ruff / interrogate / radon / vulture;
`[ihm]` — flask.

---

## Current Development Status

**Current version**: `0.2.9` (see `CHANGELOG.md` for full history)

**Current version**: `0.3.0` (see `CHANGELOG.md` for full history)

> **v0.3.0 highlights** (review follow-through on top of 0.2.9 — findings M3–M6, M8,
> P3–P4, R7-partial): the maneuver IHM (`scripts/manoeuvre_ihm.py`) becomes a serveable
> app (`create_app()` factory + `waitress` + `/healthz`, M8/R7); the pypowsybl
> integer-value `0 → −1` fix is a vendored, import-time, version-guarded class patch
> (`patched_backend.py`, M5 — no site-packages edit); the package stops reconfiguring the
> **root** logger and `action_rebuilder` re-raises instead of swallowing (M6); test infra
> hardens with `--strict-markers` + a registered `slow` marker + opt-in coverage, and two
> dead/broken tests are repaired (M3); dependency floors reconcile with a `[grid2op]` extra
> and a grid2op-optional CI leg (M4); and `NetworkManager` name-array caching + a
> `set_thermal_limit` O(n²) fix (P4) plus cached membership sets in action application (P3)
> trim the pypowsybl hot path. **`pypowsybl2grid` is deprecated and dropped as a
> dependency** — its `numpy==1.26.4` pin was unsatisfiable against `numpy>=2.0.0`; only the
> legacy grid2op+pypowsybl assistant-env bridge needs it (install manually in a `numpy<2`
> env). Behaviour-preserving for the analysis pipeline. See `docs/release-notes/v0.3.0.md`.
>
> **v0.2.9 highlights** (deep revisions R5 + A5 + R6-partial from the 2026-07 review, plus a
> container-aware reassessment fix): discovery restructured around data — one typed
> `FamilyResult` per family in `self.results` via a declarative `FAMILY_SPECS` registry (with
Expand Down Expand Up @@ -678,11 +706,30 @@ combined = action1 + action2

5. **alphaDeesp dependency**: `expertop4grid >= 0.2.8` is required for `AlphaDeesp_warmStart`.

6. **pypowsybl2grid backend patch**: The `PyPowSyBlBackend.update_integer_value` method has an issue with zero-value handling. The original code `changed[value == 0] = False` must be replaced with `value[value == 0] = -1`. **This patch must be applied to the installed package file before running tests**:
```bash
python scripts/patch_pypowsybl2grid_file.py
```
In CI, this is done automatically in the `build-and-test` job of `.github/workflows/ci.yml`. Runtime monkey-patching doesn't work because modules are imported before conftest.py runs.
6. **pypowsybl grid2op backend integer-value fix (0 → −1)**: the buggy method
is `update_integer_value` on **`pypowsybl.grid2op.Backend`** (the internal
delegate `pypowsybl2grid.PyPowSyBlBackend` instantiates as `self._grid`) —
*not* on `PyPowSyBlBackend` itself. It forwards the grid2op bus array to the
native `_pypowsybl.update_grid2op_integer_value`, but grid2op encodes the
disconnected/unset-bus sentinel as `0` while pypowsybl expects `-1`, so the
fix inserts `value[value == 0] = -1` before the native call.

As of M5 this is applied **at package import time** by
`expert_op4grid_recommender/__init__.py`, via an idempotent, version-guarded
class patch in `expert_op4grid_recommender/patched_backend.py`
(`apply_pypowsybl_integer_value_patch()`). It is a no-op when pypowsybl is
absent and self-disables if a future upstream already applies the fix. **No
site-packages edit is required.** `scripts/patch_pypowsybl2grid_file.py`
remains only as a manual fallback; the `.github/workflows/ci.yml` step that
ran it was removed in 0.3.0.

**`pypowsybl2grid` deprecated (0.3.0):** its wheel pins `numpy==1.26.4`,
unsatisfiable against `numpy>=2.0.0`, so it is dropped from the declared
dependencies. The generic patch above (on `pypowsybl.grid2op.Backend`) is
unaffected; only the `make_patched_pypowsybl_backend` factory + the
`make_assistant_env.create_pypowsybl_backend` bridge need it, and both now
emit a `DeprecationWarning`. Install `pypowsybl2grid` manually in a `numpy<2`
environment if you still use that legacy grid2op+pypowsybl path.

---

Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ WORKDIR /home/user/app
RUN pip install --no-cache-dir --upgrade pip \
&& pip install --no-cache-dir \
flask \
"waitress>=3.0" \
"pypowsybl>=1.13.0" \
networkx \
pandas
Expand Down
Loading
Loading