ANDES: fix run_eigenvalue_analysis + add PSS/E raw+dyr loading (integrates #52) - #58
Merged
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
…loading (#52) Merges elasticdotventures' #52, branched from 52deb67 (before #57's MCP 2 + sandbox migration), resolving the conflicts in ANDES/andes_mcp.py and the two READMEs against main. Conflict resolution keeps main's sandboxed structure (_prepare_run_dir, checked_path) and re-applies the PR's two features on top. ANDES/README.md keeps main's load_network_from_* lines, which document #57's operating_point / study_commit parameters that this branch predates. Review fixes applied on top of the author's work: - dyr_path goes through checked_path like its file_path sibling, and the .dyr copy into the run directory is a checked write. The PR predates containment and had neither, so it would have added an unchecked path argument to a function main had just finished sandboxing. - Sorting `modes` by damping left participation_factors and state_names in ANDES's native ordering, so modes[0] and participation_factors[0] described different eigenvalues -- breaking exactly the question the tool exists to answer. Each mode now carries its native `index`. - Real eigenvalues were hardcoded to 0% damping. ANDES's post_process applies -100*Re/|mu| to every mode, giving +/-100% for real ones; a real positive eigenvalue is monotonic instability, and reporting 0% sorted it into the middle of the list instead of first. Only a mode at the origin is special now. - dynamic_models_loaded reported whether the caller passed an argument rather than whether anything attached. It is now derived from the model count, which also covers inverter-based resources (RenGen, DG) instead of SynGen alone. - n_eigenvalues / eigenvalues are retained alongside the new n_modes / modes, so the change is purely additive for existing callers. Only eigenvectors and state_variables are gone -- they read attributes EIG has never had. - math.pi replaces a numpy import added for one constant. The andes_mcp fixture now points POWERMCP_HOME at a per-test tmp_path: run directories are named after the case stem, so the two ieee14 tests shared one directory and leaked the .dyr copy between them. Verified with andes 2.0.0 installed: 13 passed in tests/test_andes_server.py (they skip, not fail, without the extra). Full suite in an extras-free venv: 374 passed, 17 skipped. Co-Authored-By: Brian Horakh <elasticdotventures@users.noreply.github.com> Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Aug 22, 2026
feat(genx): GenX connector — capacity analysis, diurnal plots, SLURM submission (integrates #51)
#59
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Integrates #52 by @elasticdotventures, which branched from
52deb67— before #57 landed the MCP 2 migration andchecked_pathcontainment — and so conflicted withmaininANDES/andes_mcp.py,ANDES/README.md, andpowermcp/README.md. The author's commits are preserved as a parent of the merge.Conflict resolution
Kept main's sandboxed structure (
_prepare_run_dir,checked_path, the PowerIOresolve_solver_casebridge) and re-applied the PR's two features on top of it.ANDES/README.mdkeeps main'sload_network_from_*lines — they document #57'soperating_point/study_commitparameters, which this branch predates and would otherwise have silently dropped — and takes the PR'srun_power_flow/run_eigenvalue_analysisblocks and license note.Review fixes applied on top
dyr_pathbypassed the sandbox. The PR adds a second path argument to a function main had just finished containing. It now goes throughchecked_pathlike itsfile_pathsibling, and the.dyrcopy into the run directory is a checked write.modesdesynced them fromparticipation_factors.modes.sort(...)reordered only the mode list whilepfactors/x_namekept ANDES's native order, somodes[0]andparticipation_factors[0]described different eigenvalues — breaking exactly the question the tool exists to answer. Each mode now carries its nativeindex.post_process()applies-100*Re/|mu|to every mode, giving ±100% for real ones. A real positive eigenvalue is monotonic instability; reporting 0% sorted it into the middle of the list instead of first. Only a mode at the origin is special-cased now.dynamic_models_loadedreported the argument, not the result. ANDES's PSS/E dyr parser skips model types it does not support, so a supplied.dyrcan attach nothing. The flag is now derived from the model count, which also covers inverter-based resources (RenGen,DG) rather thanSynGenalone.n_eigenvalues/eigenvalueswere dropped. Restored alongside the newn_modes/modes, making the change purely additive for existing callers. Onlyeigenvectorsandstate_variablesare gone — they read attributesEIGhas never had, so they only ever returned[].math.pireplaces a numpy import added for one constant.andes_mcpfixture pointsPOWERMCP_HOMEat a per-testtmp_path. Run directories are named after the case stem, so the two ieee14 tests shared one directory and leaked the.dyrcopy between them.Verification
andes2.0.0 installed: 13 passed intests/test_andes_server.py, including four new tests covering the fixes above. They skip rather than fail without the extra.Closes #52.
🤖 Generated with Claude Code