Skip to content

Cosimulation that passes information from ExaGO to GridKit - #144

Merged
pelesh merged 2 commits into
slaven/agentigrid-refactorfrom
suggestion_gridkit_from_exago_eve
Oct 15, 2026
Merged

pelesh merged 2 commits into
slaven/agentigrid-refactorfrom
suggestion_gridkit_from_exago_eve

Conversation

@tsybinae

@tsybinae tsybinae commented Oct 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

After #135 GridKit ran only from the operating point stored in its own case files. If something was solved by ExaGO, it would not be transferred to GridKit. This PR completes the folder strucutre and adds a GridKit fault screen start from the most recent ExaGO steady state. A transient study can run on top of the latest ExaGO result. A screen can also run all of its faults in one GridKit ContingencyAnalysis process.

Old approach

config        exago: data: llm: search: output:          (GridKit values hard-coded in Python)
ExaGO run     workdir/iter_*/case.m                       (solution printed, parsed, not saved)
GridKit run   .case.json ── its own operating point ── DynamicSimulation, one process per fault

New approach

config        exago: data: llm: search: output: gridkit:
ExaGO run     workdir/exago/iter_*/case.m + opflowout.m / pflowout.m   (solved case saved)

GridKit run   .case.json ──┬── "start_from": "case"                 its own operating point
                           └── "start_from": "latest_steady_state"  newest ExaGO solution
                                  → its last simulation → solved .m
                                  → voltages, angles, dispatch, outages, network applied
                                    to the case's dynamic models
                                  → no-fault run must stay steady before any fault
              ── "application": "DynamicSimulation"     one process per fault (default)
                                "ContingencyAnalysis"   one process for all faults of the screen
  • The GridKit case still supplies the dynamic models (machines, governors, exciters); a MATPOWER case has none. The two cases must describe the same network. If AgentiGrid sees a case for which it does not have a case with models, it will report an error.

Changes

ExaGO folders and output

  • engine/ → exago_engine/, parsers/ → exago_parsers/ (files unchanged). Imports, patch("agentigrid.engine...") strings in tests and logger names updated in 76 files; logger names follow agentigrid.exago_engine..., as GridKit's do. Docs updated: README.md, tests/README.md, agentigrid_architecture.md, launcher/ARCHITECTURE.md.
  • cli.py: exago_output() puts ExaGO runs in workdir/exago/ and logs in logs/exago/ (GridKit already used gridkit/ subfolders). The steering save command (was hard-coded to workdir/) and regenerate-report follow it.
  • exago_engine/executor.py: OPFLOW and PFLOW run with -save_output, which writes the solved case (opflowout.m / pflowout.m) into the run folder. Other applications are unchanged.

One config for both tools (config.py, configs/*.template)

  • New gridkit: section: binary_dir, dynamicsimulation_binary, timeout; study: (tmax, dt_monitor, rel_tol, abs_tol, dt_fixed, max_steps, max_order), written one-to-one into each run's solver .json; fault: start. LLM, iteration limit, worker count and output folders stay shared.
  • Defaults equal the values the code used before (tmax 15 s, timeout 600 s, fault start 1 s), so behaviour and the LLM prompt text are unchanged with defaults.
  • PJM pass/fail limits are not configurable; they stay in gridkit_engine/criteria.py with their sources.
  • GridKit ignores misspelled solver keys without a message, so the loader warns about unknown keys; it also warns when max_order is outside 1-5, dt_monitor <= 0, or the fault starts at or after tmax.
  • gridkit_engine/settings.py, executor.py, agent_loop.py, commands.py, schema_description.py read the configured values.

Start from the latest ExaGO steady state (new gridkit_engine/from_exago.py)

  • find_latest_steady_state: newest journal_*.json in workdir/exago/, its last simulation, the run folder from the recorded -netfile. Error if that simulation did not converge, was a sweep, was not OPFLOW/PFLOW, or has no saved solution.
  • apply_steady_state matches the solved MATPOWER case to the GridKit case:
    • buses by number: init Vr, Vi from the solved Vm, Va
    • generators to machines by bus, then unit order: p0, q0 from the solved Pg, Qg; machines of generators that are off in ExaGO are removed with every device on their signals (governor, exciter, stabilizer), and unused signals are removed
    • loads: one constant-impedance LoadZIP per bus from Pd, Qd; shunts: a separate constant-impedance LoadZIP from Gs, Bs
    • branches rebuilt from the online ExaGO branches (R, X, B, tap on the bus-1 side, phase in radians)
    • errors: bus numbers differ, isolated buses, generator without a machine model, unit counts that cannot be matched at a bus
  • PFLOW's saved file always has converged = 0 (ExaGO writes the OPFLOW flag there), so convergence is read from the journal.
  • commands.py: new fault-screen field start_from ("case" default, "latest_steady_state"). Each start is built once per session and must pass its own no-fault run first; the converted case is saved as from_exago.case.json in the session folder.
  • agent_loop.py: if the goal asks for the most recent steady state and the LLM sends "start_from": "case", the screen is rejected and the LLM is told to set it. The journal, verdict line and final summary name the ExaGO iteration and file used.
  • cli.py: --from-exago uses the ExaGO start for the whole session.

ContingencyAnalysis (gridkit_engine/executor.py, modifier.py, gridkit_parsers/status_parser.py)

  • New fault-screen field application: "DynamicSimulation" (default) or "ContingencyAnalysis". run_contingency runs all faults of a screen in one process, writes output_<fault index>.csv per fault in the screen's folder, and splits the shared console output into a status per fault. Timeout = per-run timeout × number of faults.
  • ContingencyAnalysis runs every fault device in the file, so its case copy keeps only the requested faults (fault devices shipped with the case, such as the demo fault at bus 149 in ACTIVSg200, are left out). With DynamicSimulation they stay in the copy but are never switched on.
  • The status parser also reads ContingencyAnalysis's completion line (Complete in 1.91939s).

Prompt (prompts/gridkit/system_prompt.py)

  • A fault or connection request at a bus means that bus only ("buses": [<bus>]). The POI screen with neighbours ("poi", "hops") is used only when the goal asks for nearby buses, a depth or the PJM POI screen.
  • start_from and application are set only when the goal asks for them; the answer states the operating point used.

Tests

  • New in tests/gridkit/: test_config.py (gridkit section, defaults, warnings, solver keys in the .json), test_from_exago.py (journal lookup, no fallback, conversion on ACTIVSg200, generator without model, bus mismatch, -save_output only for OPFLOW/PFLOW), test_screen_options.py (start_from and application, live on IEEE39 and ACTIVSg200 when GridKit is linked). test_status_parser.py covers the ContingencyAnalysis completion line.

Testing

  • Fast suite: 1231 passed, 5 skipped (python -m pytest -m "not slow"). The 46 new GridKit tests pass, including the live runs.
  • Live runs with OpenAI gpt-4o, OPFLOW with Ipopt/MUMPS, GridKit at cdb77263. Fault: bus short circuit to ground at 1 s, cleared after 6.25 cycles (0.104 s), 15 s simulated. The ExaGO study removed the generator at the bus; the GridKit study then faulted the same bus.
Case ExaGO: generator out GridKit: fault at the same bus
ACTIVSg200, bus 135 (446 MW unit) feasible, V 1.058-1.100 pu, $27,558 → $24,635/h TAMU operating point: passes (angle spread 83.5°, Vmin 0.986 pu at 2.5 s, damping 5.3%)
latest ExaGO steady state, DynamicSimulation: does NOT pass, damping 2.5% < 3% (spread 45.4°)
latest ExaGO steady state, ContingencyAnalysis: same results, 0.3 s
ACTIVSg2000, bus 5360 (1008 MW unit) feasible, V 1.015-1.100 pu, $1,228,892 → $1,227,382/h TAMU operating point, GridKit with Enzyme: does NOT pass, damping 2.2% < 3% (spread 105.0°); base run 2.6 s, fault run 34 s
  • From the ExaGO start, the starting voltages equal the solved ones on all 200 buses (largest difference 2e-16 pu, 4e-15°), and the no-fault run moves angles 0.000° and voltages 0.0000 pu.
  • Cost drops when the unit is removed: in the base OPF the unit at bus 5360 ran at its 302.4 MW minimum (of 1008 MW), i.e. it was uneconomic. The unit at bus 135 is at its 133.9 MW minimum in the case file; its base OPF dispatch was not checked.
  • ACTIVSg2000 needs GridKit built with Enzyme: without it GridKit falls back to dense Jacobians, and the no-fault run hit the 600 s timeout with no time step on a 3.7 GB machine. Enzyme 0.0.206 + LLVM 16, RelWithDebInfo (as in GridKit's CI); the build is selected with gridkit.binary_dir.

Known issues

  • final_voltage fails a bus that the OPF holds exactly at its 1.1 pu limit when GridKit ends 1e-6 pu above it (bus 100 in the ACTIVSg200 runs: 1.100000 → 1.100001 pu). This is a false failure; a small tolerance would fix it. Not changed here.
  • The ExaGO start needs a GridKit case for the same network; AgentiGrid does not search for one or generate dynamic data. On ACTIVSg2000, 432 online generators face 334 machine models, so the ExaGO start will stop with the "no dynamic model" error there.
  • Upstream GridKit 0e6814ab (#607) removes BusFault (replaced by Bus::setFault); AgentiGrid's fault runs use BusFault devices and need adapting before moving past cdb77263.
  • GridKit with Enzyme does not build with Release (-O3): ReecbEnzyme.cpp fails with Enzyme errors; RelWithDebInfo (-O2) builds and passes ctest (70/70).
  • Every OPFLOW/PFLOW run now saves its solved case, sweep runs included, which adds disk use on large sweeps.
  • Remaining skips: two e2e tests need a machine-specific configs/env_setup.sh; one SOPFLOW test needs output from a real EMPAR run; two backend tests need ANTHROPIC_API_KEY / OPENAI_API_KEY.

Produced with help of Claude Code, but not exclusively by Claude.

… ExaGO steady state

- ExaGO code moved to exago_engine/ and exago_parsers/ (imports, test patch
  targets and logger names updated); ExaGO output in workdir/exago/ and
  logs/exago/; OPFLOW and PFLOW save their solved case (-save_output).
- One shared config: new gridkit: section (binary, timeout, solver .json
  values, default fault start), read by the GridKit engine; warnings for
  unknown solver keys and invalid values. Defaults keep the old behaviour.
- New gridkit_engine/from_exago.py: a fault screen with
  "start_from": "latest_steady_state" takes voltages, angles, dispatch,
  generator outages, loads, shunts and branches from the last simulation of
  the newest ExaGO journal and applies them to the GridKit case's dynamic
  models; error for online generators without a machine model. --from-exago
  forces it for a session.
- Fault screens can run with GridKit ContingencyAnalysis (one process for
  all faults of a screen).
- GridKit prompt: a fault at a bus means that bus only, unless the goal asks
  for nearby buses.
- New tests: tests/gridkit/test_config.py, test_from_exago.py,
  test_screen_options.py.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@tsybinae tsybinae self-assigned this Oct 8, 2026
@tsybinae tsybinae added enhancement New feature or request ai Support for agentic AI capabilities labels Oct 8, 2026

@superwhiskers superwhiskers left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ran test suite, all passed. replicated live run on my machine, same fault clear duration of ~.104s, simulated 15s total. same overall statistics (angle spread, damping)

@samimk samimk left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test suite passes, with GridKit linked locally. I also ran AgentiGrid with GridKit end to end from the console and it works.

A few comments:

  1. Launcher output folders

Launcher is not updated, and launcher/session_manager.py still uses plain load_config, so launcher studies still write to workdir/. That will be corrected when we update launcher and make it use exago_output().

  1. -save_output on every solve

OPFLOW/PFLOW now save the solved case on every run, including sweep and contingency solves. On large cases that’s a full .m file per solve. Could it be limited to single solves, or made configurable?

@pelesh

pelesh commented Oct 13, 2026

Copy link
Copy Markdown
Collaborator
  1. -save_output on every solve

OPFLOW/PFLOW now save the solved case on every run, including sweep and contingency solves. On large cases that’s a full .m file per solve. Could it be limited to single solves, or made configurable?

We do want to solve .m files for each solve because they contain the needed information. I agree it would be helpful to have ability to switch output off at instances.

Answers the #144 review: OPFLOW/PFLOW saved a solved case on every solve,
including sweep and contingency solves. New config key exago.save_solutions
(also --set exago.save_solutions=<value>):

- best (default): single solves keep their solution; an action with many
  solves keeps only its best (cheapest feasible sweep candidate or best by
  the sweep's metric, largest hosting capacity for boundary, Pareto-best
  explore variant), or the most recent if it has no best (contingency,
  reserve)
- newest: multi-solve actions keep only the most recent solve
- all: every solution kept (previous behaviour)
- none: OPFLOW/PFLOW write no solution (-save_output not passed)

Only solution files are removed; run folders and input cases stay. After an
explore, pruning waits for the next action so a selected variant keeps its
solution. New tests: tests/runner/test_save_solutions.py.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@tsybinae

Copy link
Copy Markdown
Collaborator Author
  1. -save_output on every solve

OPFLOW/PFLOW now save the solved case on every run, including sweep and contingency solves. On large cases that’s a full .m file per solve. Could it be limited to single solves, or made configurable?

We do want to solve .m files for each solve because they contain the needed information. I agree it would be helpful to have ability to switch output off at instances.

Here is what we are doing. config now has a flag what to keep ("all", "best", "newest", "none"), the normal flag is "best". If there is no best solution, it keeps the most recent. If you are reluctant to save any outputs, then you can set "none". Alternatively, tell the agent not to save any data outputs.

@pelesh
pelesh merged commit 774743e into slaven/agentigrid-refactor Oct 15, 2026
10 checks passed
pelesh pushed a commit that referenced this pull request Oct 15, 2026
* AgentiGrid: one config for ExaGO and GridKit, GridKit from the latest ExaGO steady state

- ExaGO code moved to exago_engine/ and exago_parsers/ (imports, test patch
  targets and logger names updated); ExaGO output in workdir/exago/ and
  logs/exago/; OPFLOW and PFLOW save their solved case (-save_output).
- One shared config: new gridkit: section (binary, timeout, solver .json
  values, default fault start), read by the GridKit engine; warnings for
  unknown solver keys and invalid values. Defaults keep the old behaviour.
- New gridkit_engine/from_exago.py: a fault screen with
  "start_from": "latest_steady_state" takes voltages, angles, dispatch,
  generator outages, loads, shunts and branches from the last simulation of
  the newest ExaGO journal and applies them to the GridKit case's dynamic
  models; error for online generators without a machine model. --from-exago
  forces it for a session.
- Fault screens can run with GridKit ContingencyAnalysis (one process for
  all faults of a screen).
- GridKit prompt: a fault at a bus means that bus only, unless the goal asks
  for nearby buses.
- New tests: tests/gridkit/test_config.py, test_from_exago.py,
  test_screen_options.py.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

* AgentiGrid: exago.save_solutions limits the solved cases kept per action

Answers the #144 review: OPFLOW/PFLOW saved a solved case on every solve,
including sweep and contingency solves. New config key exago.save_solutions
(also --set exago.save_solutions=<value>):

- best (default): single solves keep their solution; an action with many
  solves keeps only its best (cheapest feasible sweep candidate or best by
  the sweep's metric, largest hosting capacity for boundary, Pareto-best
  explore variant), or the most recent if it has no best (contingency,
  reserve)
- newest: multi-solve actions keep only the most recent solve
- all: every solution kept (previous behaviour)
- none: OPFLOW/PFLOW write no solution (-save_output not passed)

Only solution files are removed; run folders and input cases stay. After an
explore, pruning waits for the next action so a selected variant keeps its
solution. New tests: tests/runner/test_save_solutions.py.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: tsybinae <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
pelesh added a commit that referenced this pull request Oct 20, 2026
* AgentiGrid: substation-based N-1/N-2 contingency screen, verdict, fast-only relief (#134)

* AgentiGrid: GridKit transient-stability engine, first build (#135)

* Cosimulation that passes information from ExaGO to GridKit (#144)

* AgentiGrid: one config for ExaGO and GridKit, GridKit from the latest ExaGO steady state

* AgentiGrid: exago.save_solutions limits the solved cases kept per action

---------

Co-authored-by: tsybinae <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: tsybinae <[email protected]>
Co-authored-by: tsybinae <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai Support for agentic AI capabilities enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants