Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/workflows/pre_commit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ jobs:
- uses: actions/checkout@v3
with:
ref: ${{ github.event.pull_request.head.ref }}
- uses: actions/setup-python@v3
- uses: actions/setup-python@v3.14
- uses: pre-commit/[email protected]
- uses: EndBug/[email protected]
# Only need to try and commit if the action failed
Expand Down
4 changes: 3 additions & 1 deletion agentigrid/agentigrid/exago_parsers/matpower_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,9 @@ def parse_matpower(path: Path) -> MATNetwork:
if not path.exists():
raise FileNotFoundError(f"MATPOWER file not found: {path}")

text = path.read_text(encoding="utf-8")
# Exported cases may carry non-UTF-8 bytes in comments (e.g. Windows curly
# quotes in citations); replace them rather than reject the file.
text = path.read_text(encoding="utf-8", errors="replace")

# --- Function name ---
m = _FUNC_RE.search(text)
Expand Down
24 changes: 15 additions & 9 deletions agentigrid/agentigrid/gridkit_engine/agent_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -213,21 +213,27 @@ def _run_base_case(self, case: dict, emap: dict, run_dir: Path,
dt_monitor_s=self._settings.dt_monitor_s)
self._print(f"[Iter {iteration}] Base case: {what}, {self._settings.tmax_s:g} s ...")
res = run_one(job, self._settings)
rows = []
if res.status.solved:
series = read_results(res.csv_path, emap)
steady = criteria.check_steady_state(
series.t, series.device_variable("delta", MACHINE_CLASSES), series.bus_variable("Vm"),
)
ok = steady.status == "pass"
text = f"GridKit {res.status.reason}. Steady state: {steady.status} - {steady.detail}."
rows = [{"bus": None, "status": steady.status, "solved": True, "run": res.status.reason,
"checks": {steady.name: {"status": steady.status, "value": steady.value,
"limit": steady.limit, "detail": steady.detail,
"data": steady.data}},
"run_dir": str(run_dir)}]
else:
ok = False
text = f"GridKit base case did not solve: {res.status.reason}."
if not ok:
text += " Fault screens are not meaningful until the base case solves and stays steady."
self._print(f"[Iter {iteration}] {text}")
self._journal.add(JournalEntry(
iteration, "base_case", what, verdict=text, result_text=text,
iteration, "base_case", what, verdict=text, result_text=text, rows=rows,
elapsed_s=res.elapsed_s, run_dir=str(run_dir),
))
return text, ok
Expand Down Expand Up @@ -347,7 +353,7 @@ def _handle_fault_screen(self, iteration: int, data: dict) -> tuple[Optional[str

cycles = study.clearing_cycles + study.margin_cycles
note = (f"bus short circuit to ground at {study.start_s:g} s, cleared after {cycles:g} cycles "
f"({study.duration_s():.3f} s), R = {study.R:g} pu, X = {study.X:g} pu, "
f"({criteria.num(study.duration_s(), '.3f', ' s')}), R = {study.R:g} pu, X = {study.X:g} pu, "
f"{study.tmax_s:g} s simulated, GridKit {study.application}, "
f"starting from {start.label}")
text = screen_verdict(self._subject(study), outcomes, note)
Expand Down Expand Up @@ -420,18 +426,18 @@ def _exago_label(source: SteadyStateSource) -> str:

def _outcome_table(outcomes: list[FaultOutcome]) -> str:
"""Every fault, one row: result and the main check values."""
def val(o: FaultOutcome, name: str, fmt: str) -> str:
def val(o: FaultOutcome, name: str, fmt: str, unit: str = "", scale: float = 1.0) -> str:
c = next((c for c in o.checks if c.name == name), None)
if c is None or c.value is None:
return "-" if c is None else c.status
return fmt.format(c.value)
return criteria.num(scale * c.value, fmt, unit)

lines = ["ALL BUS FAULTS:",
f"{'Bus':>6} | {'Result':<10} | {'Angle spread':>12} | {'Vmin 2.5s':>9} | {'Damping':>8}",
"-" * 58]
lines = ["ALL BUS FAULTS (rounded value, full value in brackets):",
f"{'Bus':>6} | {'Result':<10} | {'Angle spread':>32} | {'Vmin 2.5s':>28} | {'Damping':>28}",
"-" * 116]
for o in sorted(outcomes, key=lambda o: o.bus):
lines.append(
f"{o.bus:>6} | {o.status:<10} | {val(o, 'angle_stability', '{:.1f} deg'):>12} | "
f"{val(o, 'voltage_recovery', '{:.3f}'):>9} | {val(o, 'damping', '{:.1%}'):>8}"
f"{o.bus:>6} | {o.status:<10} | {val(o, 'angle_stability', '.1f', ' deg'):>32} | "
f"{val(o, 'voltage_recovery', '.3f', ' pu'):>28} | {val(o, 'damping', '.1f', '%', 100):>28}"
)
return "\n".join(lines)
34 changes: 24 additions & 10 deletions agentigrid/agentigrid/gridkit_engine/criteria.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from __future__ import annotations

import math
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import Mapping, Optional, Sequence

# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -105,12 +105,19 @@ class CheckResult:
limit: Optional[float]
detail: str
source: str
data: dict = field(default_factory=dict) # every number in *detail*, unrounded (journal)

@property
def passed(self) -> bool:
return self.status == "pass"


def num(x: float, fmt: str, unit: str = "") -> str:
"""*x* rounded with *fmt* for reading, then its full value in brackets, so a
rounded 1.100 that is really 1.10036 stays visible."""
return f"{x:{fmt}}{unit} [{float(x)!r}{unit}]"


def _series_after(t: Sequence[float], x: Sequence[float], t0: float) -> list[tuple[float, float]]:
return [(ti, xi) for ti, xi in zip(t, x) if ti >= t0]

Expand All @@ -135,9 +142,10 @@ def check_angle_stability(
ok = worst <= limit_deg
return CheckResult(
"angle_stability", "pass" if ok else "fail", worst, limit_deg,
f"largest machine angle spread {worst:.1f} deg at t = {worst_t:g} s"
f"largest machine angle spread {num(worst, '.1f', ' deg')} at t = {worst_t:g} s"
+ ("" if ok else " (loss of synchronism)"),
src,
{"spread_deg": worst, "t_s": worst_t},
)


Expand Down Expand Up @@ -165,9 +173,10 @@ def check_voltage_recovery(
ok = worst_v >= min_pu
return CheckResult(
"voltage_recovery", "pass" if ok else "fail", worst_v, min_pu,
f"lowest voltage from {after_s:g} s after clearing: {worst_v:.3f} pu "
f"lowest voltage from {after_s:g} s after clearing: {num(worst_v, '.3f', ' pu')} "
f"at bus {worst_bus}, t = {worst_t:g} s",
src,
{"lowest_pu": worst_v, "bus": worst_bus, "t_s": worst_t},
)


Expand Down Expand Up @@ -196,8 +205,9 @@ def check_voltage_envelope(
bus, ti, vi, vmin = worst
return CheckResult(
name, "pass" if ok else "fail", vi, vmin,
f"closest to envelope: bus {bus} at t = {ti:g} s, {vi:.3f} pu vs minimum {vmin:.2f} pu",
f"closest to envelope: bus {bus} at t = {ti:g} s, {num(vi, '.3f', ' pu')} vs minimum {vmin:.2f} pu",
source,
{"v_pu": vi, "bus": bus, "t_s": ti, "envelope_min_pu": vmin, "margin_pu": worst_margin},
)


Expand All @@ -216,9 +226,11 @@ def check_final_voltage(
ok = finals[lo_bus] >= vmin and finals[hi_bus] <= vmax
return CheckResult(
"final_voltage", "pass" if ok else "fail", finals[lo_bus], vmin,
f"end of run: lowest {finals[lo_bus]:.3f} pu (bus {lo_bus}), "
f"highest {finals[hi_bus]:.3f} pu (bus {hi_bus}), band {vmin}-{vmax} pu",
f"end of run: lowest {num(finals[lo_bus], '.3f', ' pu')} (bus {lo_bus}), "
f"highest {num(finals[hi_bus], '.3f', ' pu')} (bus {hi_bus}), band {vmin}-{vmax} pu",
src,
{"lowest_pu": finals[lo_bus], "lowest_bus": lo_bus,
"highest_pu": finals[hi_bus], "highest_bus": hi_bus, "vmin_pu": vmin, "vmax_pu": vmax},
)


Expand Down Expand Up @@ -302,11 +314,12 @@ def check_damping(
f"all swings below {min_amplitude_deg} deg after clearing", src)
zeta, nm = worst
ok = zeta >= min_ratio and not unmeasured
detail = f"lowest damping ratio {100 * zeta:.1f}% (machine {nm})"
detail = f"lowest damping ratio {num(100 * zeta, '.1f', '%')} (machine {nm})"
if unmeasured:
detail += f"; not measurable for {', '.join(unmeasured[:5])}"
status = "pass" if ok else ("fail" if zeta < min_ratio else "not_tested")
return CheckResult("damping", status, zeta, min_ratio, detail, src)
return CheckResult("damping", status, zeta, min_ratio, detail, src,
{"ratio": zeta, "machine": nm, "unmeasured": unmeasured})


# Pre-fault steady state: the no-fault run of the case copy must stay still,
Expand Down Expand Up @@ -341,9 +354,10 @@ def check_steady_state(
ok = angle_move <= angle_tol_deg and v_move <= v_tol_pu
return CheckResult(
"steady_state", "pass" if ok else "fail", angle_move, angle_tol_deg,
f"without a fault: angles move {angle_move:.3f} deg (limit {angle_tol_deg}), "
f"voltages move {v_move:.4f} pu (limit {v_tol_pu})",
f"without a fault: angles move {num(angle_move, '.3f', ' deg')} (limit {angle_tol_deg}), "
f"voltages move {num(v_move, '.4f', ' pu')} (limit {v_tol_pu})",
src,
{"angle_move_deg": angle_move, "v_move_pu": v_move},
)

# PJM tests the agent does not run, because GridKit lacks the element they
Expand Down
Loading
Loading