Skip to content
Closed
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
24 changes: 24 additions & 0 deletions agentigrid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,28 @@ Beyond the default cost metric and standard V-band/loading feasibility, a sweep
- **`metric: "max_delta_v"`** — ranks buses by the worst *system-wide* voltage step `max_b |V_candidate[b] − V_base[b]|` caused by switching in a load block (`search.switched_load_mw`) at the candidate bus. A power-quality flag; the largest step need not be at the switched bus. Requires (and triggers) a one-time base-case solve for the reference voltages.
- **`feasibility_predicate: "reactive_adequacy"`** — replaces standard feasibility with a reactive-headroom test: a feasible OPF must exist with the added unit forced to `(P = Pmax, Q = Qmax)` (the sweep pins `Qmin = Qmax`). Returns the limiting quantity when it fails.

## Transient Stability (GridKit)

`--tool gridkit` runs transient-stability studies with GridKit's `DynamicSimulation`
on a GridKit case (`*.case.json`). The LLM turns the goal into one fault screen
(one bus short circuit to ground per run: given buses, every bus, or a POI and the
buses one branch away); AgentiGrid adds the fault devices to a copy of the case,
runs GridKit, and judges each run with PJM checks (angle stability, voltage recovery
0.70 pu at 2.5 s, 3% damping, final voltage). PJM tests that need elements GridKit
does not have (line/generator trips, reclosing, relays, load ride-through) are
listed as SKIPPED, never passed.

```bash
agentigrid --tool gridkit --config configs/local_config.yaml \
data/gridkit/examples/IEEE39.case.json \
"Assume a short circuit fault at each bus of the system, tell me if the system can survive without blackout."
```

Link the GridKit binaries and cases first (see [applications/gridkit/README.md](applications/gridkit/README.md)
and [data/gridkit/README.md](data/gridkit/README.md)). Sessions are written to
`workdir/gridkit/session_<time>/` (element map, case copies, one folder per fault,
`journal.json`); logs to `logs/gridkit/`.

## Search Modes

- **Boundary finding** — "Find the maximum load scaling factor before infeasibility"
Expand Down Expand Up @@ -410,6 +432,8 @@ pip install -e .

Copy or symlink ExaGO binaries into `applications/exago/` (see [applications/exago/README.md](applications/exago/README.md)) and place your own network data files in `data/exago/datafiles/`; ExaGO's example data is linked into `data/exago/examples/` (see [data/exago/README.md](data/exago/README.md)).

For transient studies, link GridKit's binaries into `applications/gridkit/` and its example cases into `data/gridkit/examples/` (see the READMEs in those folders).

## Configuration

Edit `configs/default_config.yaml` or pass `--config path/to/config.yaml`. CLI arguments override config file values.
Expand Down
50 changes: 50 additions & 0 deletions agentigrid/agentigrid/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,13 @@ def build_parser() -> argparse.ArgumentParser:
help="Override any config value via dotted key, e.g. "
"--set report.network_summary_max_generators=40 (repeatable)",
)
parser.add_argument(
"--tool",
choices=["exago", "gridkit"],
default="exago",
help="Simulation tool: exago (steady state, .m case) or gridkit "
"(transient stability, .case.json case). Default: exago",
)
parser.add_argument(
"--version",
action="version",
Expand Down Expand Up @@ -393,6 +400,45 @@ def run_search(cfg: AppConfig, goal: str, quiet: bool = False) -> None:
controller.run(cfg.search.base_case, goal)


def run_gridkit(cfg: AppConfig, args: argparse.Namespace) -> None:
"""Run a GridKit transient-stability study (``--tool gridkit``)."""
from agentigrid.gridkit_engine.agent_loop import GridkitAgentLoop
from agentigrid.gridkit_engine.settings import GridkitSettings

if args.resume:
raise SystemExit("--resume is not available with --tool gridkit yet")
settings = GridkitSettings.from_config(cfg)
setup_logging(settings.logs_dir, verbose=cfg.output.verbose)
case_file = Path(args.base_case) if args.base_case else None
if case_file is None or args.goal is None:
raise SystemExit("base_case (GridKit .case.json) and goal are required")
if not case_file.exists():
logger.error("Case file does not exist: %s", case_file)
sys.exit(1)
if args.dry_run:
print(f"GridKit settings: {settings}")
return
quiet = getattr(args, "quiet", False) or False
if not quiet:
print("=" * 60)
print(" AgentiGrid — transient stability with GridKit")
print("=" * 60)
print(f" Backend: {cfg.llm.backend} ({cfg.llm.model})")
print(f" Case: {case_file}")
print(f" Goal: {args.goal}")
print(f" Max iterations: {settings.max_iterations}")
print("=" * 60)
from agentigrid.gridkit_parsers.case_parser import CaseFileError

try:
session = GridkitAgentLoop(cfg, settings=settings, quiet=quiet).run(case_file, args.goal)
except (FileNotFoundError, CaseFileError) as exc:
logger.error("%s", exc)
sys.exit(1)
if session.termination_reason in ("llm_error", "base_case_failed", "parse_failures"):
sys.exit(1)


def run_regenerate_report(argv: list[str] | None = None) -> None:
"""Offline `regenerate-report` subcommand: rebuild analysis+PDF from a journal.

Expand Down Expand Up @@ -459,6 +505,10 @@ def main(argv: list[str] | None = None) -> None:
overrides = _cli_overrides(args)
cfg = load_config(Path(args.config), cli_overrides=overrides)

if args.tool == "gridkit":
run_gridkit(cfg, args)
return

# Set up logging
verbose = cfg.output.verbose
setup_logging(cfg.output.logs_dir, verbose=verbose)
Expand Down
4 changes: 4 additions & 0 deletions agentigrid/agentigrid/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@
"load_factor": None,
"relief_tap_steps": [0.90, 0.95, 1.00, 1.05, 1.10],
"relief_curtail_tol_mw": 1.0,
"relief_shed_max_fraction": 0.10,
"relief_shed_max_depth": 3,
"relief_max_solves": 2000,
"reserve_max_solves": 4000,
},
Expand Down Expand Up @@ -182,6 +184,8 @@ class SearchConfig:
# Contingency relief (C.7) — bounds for the per-failure relief-measure search
relief_tap_steps: list = field(default_factory=lambda: [0.90, 0.95, 1.00, 1.05, 1.10]) # transformer tap ratios tried
relief_curtail_tol_mw: float = 1.0 # Load-curtailment bisection stops below this MW gap
relief_shed_max_fraction: float = 0.10 # Max share of each bus's load that relief may shed
relief_shed_max_depth: int = 3 # Load shedding grows tier by tier around the outage up to this depth
relief_max_solves: int = 2000 # Total relief solves budget across all failed contingencies
# Reserve minimization (C.8-revised Path A) — solve budget for the greedy de-commitment search
reserve_max_solves: int = 4000 # Total OPF solves budget across the greedy reserve-minimization pass
Expand Down
Loading
Loading