ANDES: fix run_eigenvalue_analysis + add PSS/E raw+dyr dynamic-model loading - #52
ANDES: fix run_eigenvalue_analysis + add PSS/E raw+dyr dynamic-model loading#52elasticdotventures wants to merge 2 commits into
Conversation
ss.EIG.vectors and ss.EIG.state_desc do not exist on ANDES's real EIG
routine object; the hasattr() guards around them meant eigenvectors
and state_variables have silently returned empty lists since this
tool was written. Verified against andes/routines/eig.py's source and
a live andes 2.0.0 install run against the bundled Kundur case
(ANDES/kundur_full.json):
ss.EIG.mu eigenvalues (complex array)
ss.EIG.N, .W right/left eigenvector matrices
ss.EIG.pfactors participation factors
ss.EIG.x_name state labels
run_eigenvalue_analysis now reads these directly and computes
frequency_hz/damping_ratio_pct per mode using the same formula
EIG.post_process() uses internally for its own text report:
freq_hz = |Im(mu)| / (2*pi)
damping_pct = -100 * Re(mu) / |mu|
Modes are returned sorted least-damped (most concerning) first.
Also:
- Factor tests/test_powerio_server.py's ANDES-bootstrap helper into a
shared andes_mcp fixture in tests/conftest.py.
- Add tests/test_andes_server.py exercising run_power_flow,
get_system_info, run_time_domain_simulation, and the fixed
run_eigenvalue_analysis against the bundled Kundur case. Guarded by
pytest.importorskip("andes") like the existing ANDES bridge tests,
so they skip (not fail) in CI, which installs no extras. Verified
locally with a real andes 2.0.0 install: all pass.
- Document the new run_eigenvalue_analysis return shape and the
load_network_from_json/load_network_from_any tools (already in the
code, missing from the README) in ANDES/README.md; add a GPL-3.0
license note there and fill in powermcp/README.md's andes extras
row, which had an empty Notes column.
No CI workflow changes; PSS/E raw+dyr dynamic-model loading is
explicitly out of scope here (tracked separately).
Part of #1
run_power_flow(file_path) only loaded static topology -- no way to attach
a PSS/E .dyr dynamic-model file (generators, exciters, governors) to a
.raw case, so run_time_domain_simulation/run_eigenvalue_analysis had no
real dynamics to work with except on the one bundled all-JSON
ANDES/kundur_full.json fixture.
Adds an optional dyr_path: Optional[str] = None parameter, following the
function's own existing scaffolding exactly: resolved to an absolute
path and existence-checked before any run-dir/chdir work (same
error shape as the existing file_path check), copied into run_dir
alongside the main input via shutil.copy2 (same pattern), and passed as
addfile=<copied path> to andes.run(...) only when supplied -- the no-dyr
call path is byte-identical to before.
Adds two additive fields to the power_flow result:
- dynamic_models_loaded: dyr_path is not None
- n_dynamic_generators: ss.groups["SynGen"].n, read defensively via
getattr(..., 0) -- deliberately not get_system_info's num_generators
field, which sums PV.idx + GENROU.idx and is already non-zero from
static PV buses alone even with no .dyr loaded.
Tests (tests/test_andes_server.py) resolve ANDES's own bundled ieee14
PSS/E raw+dyr example case via andes.get_case(...) at test-call time
(inside each test body, after the andes_mcp fixture's
pytest.importorskip("andes") has already run) -- no new fixture file
is authored or vendored into this repo. andes.get_case() is a real,
documented top-level function (verified live against andes 2.0.0) that
resolves paths under the installed package's own andes/cases/
directory, declared as package-data in andes's own pyproject.toml.
Verified live (andes 2.0.0, real pip install):
- ieee14.raw alone: ss.groups['SynGen'].n == 0
- ieee14.raw + addfile=ieee14.dyr: ss.groups['SynGen'].n == 5
- All 9 tests in tests/test_andes_server.py pass (5 pre-existing + 4 new)
- Full suite in a clean venv with no extras: new tests skip cleanly,
106 passed / 13 skipped, no regressions
- Manually inspected the actual returned dict shape with and without
dyr_path, and for a missing dyr_path
Stacked on fix/andes-eigenvalue-analysis (PR #3): depends on its
run_eigenvalue_analysis fix and tests/conftest.py andes_mcp fixture.
Closes #2
|
Reviewed and integrated in #58 — thanks for this, the eigenvalue bug was a real one and well diagnosed. Since this branch was cut from Conflict resolutionKept main's sandboxed structure ( Findings addressed in #58
Minor: Your test file and the Closing in favour of #58. |
ANDES: fix run_eigenvalue_analysis + add PSS/E raw+dyr loading (integrates #52)
Summary
Two combined fixes/additions to the ANDES MCP server (
ANDES/andes_mcp.py), motivated by wanting to use ANDES's small-signal/eigenvalue analysis for oscillation-risk review (per AEMO's 2026 General Power System Risk Review, which names small-signal stability and inter-area oscillation damping as active industry concerns, and separately notes system operators moving off proprietary small-signal tooling).1. Fix
run_eigenvalue_analysis(real bug, not a new feature)run_eigenvalue_analysisreadss.EIG.vectorsandss.EIG.state_descafter callingss.EIG.run(). Neither attribute exists on ANDES's realEIGroutine object — the code's ownhasattr()guards meanteigenvectors/state_variableshave silently returned[]since the tool was written, with no error and no test coverage catching it.Verified two ways: read
andes/routines/eig.pydirectly (GitHub + a local install), and installedandes2.0.0 into a venv and printedhasattr(ss.EIG, ...)for old vs. new attribute names directly. Real attributes:mu(eigenvalues, complex array),N/W(right/left eigenvector matrices),pfactors(participation factors),x_name(state labels).Fix: reads the real attributes and derives
frequency_hz/damping_ratio_pctper mode using the exact formula ANDES's ownEIG.post_process()uses internally for its text report:New return shape (documented in
ANDES/README.md):n_modes,modes(list of{eigenvalue: [re, im], frequency_hz, damping_ratio_pct, is_oscillatory}, sorted least-damped/most-concerning first),participation_factors,state_names,success.2. Add PSS/E raw+dyr dynamic-model loading to
run_power_flowrun_power_flowpreviously only loaded static topology — no way to attach a PSS/E.dyrdynamic-model file (generators, exciters, governors) to a.rawcase, sorun_time_domain_simulation/run_eigenvalue_analysishad no real dynamics to operate on except via the one bundled all-JSONANDES/kundur_full.jsonfixture.Added an optional
dyr_path: Optional[str] = Noneparameter, following the function's existing scaffolding exactly (abspath/exists-check, copy into the run directory, conditionaladdfilekwarg toandes.run(...)only when supplied — the no-dyr call path is byte-identical to before). Two new, purely additive return fields:dynamic_models_loadedandn_dynamic_generators(from ANDES'sSynGenmodel group, notget_system_info's existingnum_generators, which is already non-zero from staticPVbuses alone even with no.dyrloaded).No new fixture file is vendored into this repo. Tests resolve ANDES's own bundled
ieee14PSS/E example case at test-call time viaandes.get_case("ieee14/ieee14.raw")/andes.get_case("ieee14/ieee14.dyr")— a real, documented top-level function inandes2.0.0 (declared as package-data inandes's ownpyproject.toml), confirmed live:ss.groups["SynGen"].nis0loadingieee14.rawalone,5loading it withaddfile=ieee14.dyr. This avoids authoring a new.dyrfile by hand, which carries real risk given ANDES's own PSS/E dyr parser has known real-world compatibility gaps on some model types.Tests
tests/test_andes_server.py(new), using a sharedandes_mcpfixture intests/conftest.py(factored out oftests/test_powerio_server.py's prior ad hoc bootstrap): power flow convergence, system info, time-domain simulation, and the eigenvalue-analysis regression check (non-emptystate_names/numericfrequency_hz/damping_ratio_pctper mode — would have been empty before the fix) onkundur_full.json; plus four raw+dyr tests on ANDES's bundledieee14case (dynamics absent withoutdyr_path, present with it, enabling time-domain simulation, and a missing-file error path).All tests are
pytest.importorskip("andes")-guarded, matching this repo's existing convention for the optionalandesextra — they run and pass against a realandesinstall (verified locally: 9/9 passed) and skip cleanly (not fail) whenandesisn't installed (verified locally in a clean venv with no extras: 106 passed, 13 skipped, 0 failed).Docs
ANDES/README.md: documented both new/changed tool signatures and return shapes, added a license note (ANDES is GPL-3.0; installed only as an optional pip extra, never vendored into this MIT-licensed repo — including the raw+dyr test fixture, which is referenced from the installed package, not vendored), added a raw+dyr prompt example.powermcp/README.md: filled in the previously-empty Notes column for theandesextras-table row.