Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
86dd105
Route the solver boundary through PowerIO 0.11.2 modules
samtalki Sep 16, 2026
ecade21
Take PowerIO IR in every solver adapter
samtalki Sep 16, 2026
645ebc6
Require powerio 0.11.2 across the repository
samtalki Sep 16, 2026
c6eff09
Name the roots the suite and the doctor expect
samtalki Sep 16, 2026
7e09667
Cover PowerIO IR at the solver boundary
samtalki Sep 16, 2026
55cdbf5
Document the PowerIO IR boundary
samtalki Sep 16, 2026
8f071ad
Keep the MCP stdio channel clean and state the sys.path constraint
samtalki Sep 16, 2026
d1a8d91
Drop the Rust toolchain step and cancel superseded runs
samtalki Sep 16, 2026
574a052
Run the native Tellegen CLI as an MCP server
samtalki Sep 16, 2026
04f2982
Check the Tellegen CLI from the doctor
samtalki Sep 16, 2026
fd5b129
Cover the Tellegen adapter without a compiled CLI
samtalki Sep 16, 2026
d3afd30
Document the Tellegen server
samtalki Sep 16, 2026
6f22768
Report every GenX tool failure through one shape
samtalki Sep 16, 2026
d3e652c
Report every PowerFactory tool result through one shape
samtalki Sep 16, 2026
3617939
Report every PLEXOSDB tool failure through one shape
samtalki Sep 16, 2026
b8c80ba
Load the OpenDSS engine on first use
samtalki Sep 16, 2026
39c6ca9
Keep the LTSpice server importable without its engines
samtalki Sep 16, 2026
ecfd327
Name the one tool result shape in the top-level docs
samtalki Sep 16, 2026
88420d6
Adopt the powerio 0.11.3 names
samtalki Sep 16, 2026
1211444
Retrigger CI now that PyPI lists powerio 0.11.3
samtalki Sep 17, 2026
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
8 changes: 5 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,18 @@ on:
branches: [main]
pull_request:

concurrency:
group: tests-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
test:
name: pytest (py${{ matrix.python-version }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# floor and ceiling of requires-python
# floor of requires-python, and the highest version the classifiers claim
python-version: ["3.10", "3.12"]
steps:
- uses: actions/checkout@v4
Expand All @@ -22,8 +26,6 @@ jobs:
python-version: ${{ matrix.python-version }}
cache: pip

- uses: dtolnay/rust-toolchain@stable

- name: Install package
run: |
python -m pip install --upgrade pip
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Environments
.venv/
.venv-*/
env/
venv/
ENV/
Expand Down
4 changes: 2 additions & 2 deletions ANDES/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,8 @@ Configure in your MCP client (e.g., Cursor, Claude Desktop):
- `success`: whether ANDES's `EIG.run()` reported success.
- `n_eigenvalues`, `eigenvalues`: the pre-0.3.0 fields, retained so existing callers keep working. The old `eigenvectors` and `state_variables` fields are gone: they read attributes the `EIG` routine has never had, so they only ever returned `[]`.
- **get_system_info()**: Get information about the currently loaded power system.
- **load_network_from_any(...)**: Convert any PowerIO-readable case or one selected `.pio.json` package state into the ANDES run format.
- **load_network_from_json(...)**: Convert PowerIO model JSON or one selected `.pio.json` package state without staging the source input.
- **load_network_from_any(...)**: Convert any PowerIO-readable case, or one selected entry of a PowerIO IR collection, into the ANDES run format.
- **load_network_from_json(...)**: Convert serialized PowerIO IR, or one selected entry of it, without staging the source input.

## License note

Expand Down
100 changes: 79 additions & 21 deletions ANDES/andes_mcp.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
checked_path,
checked_read_tree,
ensure_checked_directory,
staged_file_write,
)
finally:
if _repo_root_added:
Expand All @@ -44,6 +45,19 @@ def _ensure_andes_runs_dir() -> str:
)


def _write_case_file(destination: str, text: str) -> None:
"""Install one MATPOWER case at ``destination`` only once it is complete.

The text lands in a private staging file first, so a write that fails part
way leaves any file already at ``destination`` as it was.
"""
staged_file_write(
destination,
True,
lambda staging: Path(staging).write_text(text, encoding="utf-8"),
)


def _prepare_run_dir(name: str, purpose: str) -> str:
run_dir = checked_path(
os.path.join(_ensure_andes_runs_dir(), name),
Expand Down Expand Up @@ -493,12 +507,18 @@ def get_system_info() -> Dict[str, Any]:

@mcp.tool()
def load_network_from_json(
network_json: str,
out_path: str,
network_json: str = "",
out_path: str = "",
operating_point: Optional[int] = None,
study_commit: Optional[int] = None,
time_index: Optional[int] = None,
scenario_id: Optional[str] = None,
powerio_ir: str = "",
edits: str = "",
to_balanced: bool = False,
base_mva: float = 100.0,
) -> Dict[str, Any]:
"""Stage PowerIO model JSON or one package state as MATPOWER for ANDES.
"""Stage a selected PowerIO IR module as MATPOWER for ANDES.

Accepts the ``json`` string returned by the powerio server's parse tool.
Converts the network to MATPOWER format, writes it to out_path (use a .m
Expand All @@ -509,12 +529,26 @@ def load_network_from_json(
Args:
network_json: The JSON transport string from powerio
out_path: Destination for the MATPOWER case file (.m)
operating_point: Optional package operating-point index to materialize
study_commit: Optional package study-commit index to materialize
operating_point: Compatibility alias for time_index
study_commit: Retired package selector; export a Tellegen Study state as IR
time_index: Explicit TimeSeries index
scenario_id: Explicit ScenarioSet identifier
powerio_ir: Serialized PowerIO IR from the powerio server (the
preferred spelling; network_json is its alias)
edits: JSON list of typed what-if edits PowerIO applies before the
conversion, in list order, for example
[{"op": "set_load_active_power", "load": "loads:0", "mw": 91.5}]
Consecutive updates of one class apply as one atomic batch, and a
bus load reallocation sees the values the edits before it produced.
to_balanced: Authorize the multiconductor to balanced transformation;
the response carries its readiness report as `lowering`
base_mva: System base for that transformation

Returns:
Dict with status, case_file path, component counts, and fidelity warnings
"""
if not out_path:
return {"status": "error", "message": "out_path is required"}
try:
out_path = checked_path(out_path, purpose="out_path", for_write=True)
except PathNotAllowed as exc:
Expand All @@ -524,12 +558,17 @@ def load_network_from_json(
network_json=network_json,
operating_point=operating_point,
study_commit=study_commit,
time_index=time_index,
scenario_id=scenario_id,
powerio_ir=powerio_ir,
edits=edits,
to_balanced=to_balanced,
base_mva=base_mva,
)
case = prepared.network
conv = case.to_format("matpower")
conv = prepared.emit("matpower")
abs_out = os.path.abspath(out_path)
with open(abs_out, "w") as fh:
fh.write(conv.text)
_write_case_file(abs_out, conv.text)
except Exception as e:
return {"status": "error", "message": str(e)}
return {
Expand All @@ -539,10 +578,9 @@ def load_network_from_json(
"info": {
"buses": case.n_buses,
"branches": case.n_branches,
"generators": case.n_gens,
"generators": case.n_generators,
},
"warnings": list(prepared.warnings) + list(conv.warnings),
**({"package": prepared.package} if prepared.package is not None else {}),
**prepared.response_fields(conv),
}


Expand All @@ -553,6 +591,11 @@ def load_network_from_any(
source_format: Optional[str] = None,
operating_point: Optional[int] = None,
study_commit: Optional[int] = None,
time_index: Optional[int] = None,
scenario_id: Optional[str] = None,
edits: str = "",
to_balanced: bool = False,
base_mva: float = 100.0,
) -> Dict[str, Any]:
"""Stage any powerio readable case as a MATPOWER file for ANDES.

Expand All @@ -566,8 +609,18 @@ def load_network_from_any(
out_path: Destination for the MATPOWER case file (.m)
source_format: Input format name (matpower, powermodels-json, egret-json,
psse, powerworld); inferred from the file extension when omitted
operating_point: Optional package operating-point index to materialize
study_commit: Optional package study-commit index to materialize
operating_point: Compatibility alias for time_index
study_commit: Retired package selector; export a Tellegen Study state as IR
time_index: Explicit TimeSeries index
scenario_id: Explicit ScenarioSet identifier
edits: JSON list of typed what-if edits PowerIO applies before the
conversion, in list order, for example
[{"op": "set_load_active_power", "load": "loads:0", "mw": 91.5}]
Consecutive updates of one class apply as one atomic batch, and a
bus load reallocation sees the values the edits before it produced.
to_balanced: Authorize the multiconductor to balanced transformation;
the response carries its readiness report as `lowering`
base_mva: System base for that transformation

Returns:
Dict with status, case_file path, component counts, and fidelity warnings
Expand All @@ -586,12 +639,16 @@ def load_network_from_any(
source_format=source_format,
operating_point=operating_point,
study_commit=study_commit,
time_index=time_index,
scenario_id=scenario_id,
edits=edits,
to_balanced=to_balanced,
base_mva=base_mva,
)
case = prepared.network
conv = case.to_format("matpower")
conv = prepared.emit("matpower")
abs_out = os.path.abspath(out_path)
with open(abs_out, "w") as fh:
fh.write(conv.text)
_write_case_file(abs_out, conv.text)
except FileNotFoundError:
return {"status": "error", "message": f"File not found: {file_path}"}
except Exception as e:
Expand All @@ -603,14 +660,15 @@ def load_network_from_any(
"info": {
"buses": case.n_buses,
"branches": case.n_branches,
"generators": case.n_gens,
"generators": case.n_generators,
},
"warnings": list(prepared.warnings) + list(conv.warnings),
**({"package": prepared.package} if prepared.package is not None else {}),
**prepared.response_fields(conv),
}


if __name__ == "__main__":
print(f"Starting ANDES MCP Server")
print(f"Using storage directory: {_andes_runs_dir()}")
# stdout carries JSON-RPC once the server runs, so startup notes go to the
# logger, which writes to stderr.
logger.info("Starting ANDES MCP Server")
logger.info("Using storage directory: %s", _andes_runs_dir())
mcp.run(transport="stdio")
2 changes: 1 addition & 1 deletion ANDES/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
andes
mcp>=2,<3
powerio[mcp,matrix]>=0.9.0,<1
powerio[mcp,matrix]>=0.11.3,<0.12
4 changes: 2 additions & 2 deletions Egret/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ Configure in your MCP client (e.g., Cursor, Claude Desktop):
- **solve_unit_commitment_problem(case_file, solver, mipgap, timelimit)**: Solve a unit commitment problem with custom solver, MIP gap, and time limits.
- **solve_ac_opf(case_file, solver, return_results)**: Run AC Optimal Power Flow on Matpower or Egret JSON case files.
- **solve_dc_opf(case_file, solver, return_results)**: Run DC Optimal Power Flow on Matpower or Egret JSON case files.
- **load_model_from_any(...)**: Convert any PowerIO-readable case or one selected `.pio.json` package state into an Egret model.
- **load_model_from_json(...)**: Convert PowerIO model JSON or one selected `.pio.json` package state without staging the source input.
- **load_model_from_any(...)**: Convert any PowerIO-readable case, or one selected entry of a PowerIO IR collection, into an Egret model.
- **load_model_from_json(...)**: Convert serialized PowerIO IR, or one selected entry of it, without staging the source input.

## Prompt Example

Expand Down
Loading
Loading