diff --git a/PowerFactory/Agent_DIgSILENT.py b/PowerFactory/Agent_DIgSILENT.py index a1f9876..f87a5bd 100644 --- a/PowerFactory/Agent_DIgSILENT.py +++ b/PowerFactory/Agent_DIgSILENT.py @@ -931,6 +931,7 @@ def export_active_grid_pdf( open_digsilent: bool = True, ) -> tuple[bool, str]: """Export one single-line diagram from the active project as PDF.""" + restore_warning = "" try: file_path = checked_path( file_path, @@ -1025,19 +1026,23 @@ def export_active_grid_pdf( ) finally: if previous_diagram is not None: - restore_code = graphics_board.Show(previous_diagram) - if restore_code not in (None, 0): - raise RuntimeError( - f"Could not restore previous diagram: {restore_code}" + try: + restore_code = graphics_board.Show(previous_diagram) + if restore_code not in (None, 0): + raise RuntimeError(f"error code {restore_code}") + except Exception as restore_exc: + restore_warning = ( + f" Warning: could not restore previous diagram: {restore_exc}" ) log.ok(f"Grid diagram exported to PDF → {file_path}") return True, ( f"Grid diagram '{selected_name}' exported to {file_path}" + + restore_warning ) except Exception as exc: log.error(f"Grid PDF export failed: {exc}") - return False, str(exc) + return False, str(exc) + restore_warning @classmethod def import_project(cls, file_path: str, open_digsilent: bool = True) -> tuple[bool, str]: @@ -1371,6 +1376,36 @@ def failure(message, **extra): ) reused_buses.append(existing.GetFullName()) + data_folder = None + model_folder = None + destination_plan = None + if grid is None: + data_folders = cls._find_named_contents( + project, "Network Data", "IntPrjfolder", + ) + if len(data_folders) > 1: + raise RuntimeError("Multiple Network Data folders were found") + data_folder = data_folders[0] if data_folders else None + if data_folder is None: + model_folders = cls._find_named_contents( + project, "Network Model", "IntPrjfolder", + ) + if len(model_folders) > 1: + raise RuntimeError("Multiple Network Model folders were found") + model_folder = model_folders[0] if model_folders else None + destination_plan = { + "data_folder": ( + data_folder.GetFullName() if data_folder is not None else None + ), + "parent": ( + data_folder.GetFullName() if data_folder is not None + else model_folder.GetFullName() if model_folder is not None + else project.GetFullName() + ), + "create_network_model": data_folder is None and model_folder is None, + "create_network_data": data_folder is None, + } + required_confirmation = cls._creation_confirmation_token( confirmation_prefix, { @@ -1384,6 +1419,7 @@ def failure(message, **extra): "grid_will_be_activated": not grid_is_active, "buses_to_create": pending_buses, "reused_buses": reused_buses, + "destination_plan": destination_plan, }, ) @@ -1403,6 +1439,7 @@ def failure(message, **extra): definition["name"] for definition in pending_buses ], "reused_buses": reused_buses, + "destination_plan": destination_plan, "confirmation_required": required_confirmation, "message": ( "Preview only; no grid, folder, or bus was created." @@ -1422,27 +1459,7 @@ def failure(message, **extra): return result if grid is None: - data_folders = cls._find_named_contents( - project, - "Network Data", - "IntPrjfolder", - ) - if len(data_folders) > 1: - raise RuntimeError("Multiple Network Data folders were found") - data_folder = data_folders[0] if data_folders else None if data_folder is None: - model_folders = cls._find_named_contents( - project, - "Network Model", - "IntPrjfolder", - ) - if len(model_folders) > 1: - raise RuntimeError( - "Multiple Network Model folders were found" - ) - model_folder = ( - model_folders[0] if model_folders else None - ) if model_folder is None: model_folder = project.CreateObject( "IntPrjfolder", diff --git a/PowerFactory/MCP_PowerFactory.py b/PowerFactory/MCP_PowerFactory.py index b98ce97..7b069d6 100644 --- a/PowerFactory/MCP_PowerFactory.py +++ b/PowerFactory/MCP_PowerFactory.py @@ -27,17 +27,6 @@ export_active_grid_pdf Export an active single-line diagram to PDF. create_evemtbench_component Create any supported EvEMTBench component. list_evemtbench_components Inspect the versioned equipment database. - create_evemtbench_transformer Create a new Table 11 TypTr2 and ElmTr2. - list_evemtbench_line_profiles Inspect the overhead-line profile database. - create_evemtbench_line Create a new Table 7 TypLne and ElmLne. - list_evemtbench_cable_profiles Inspect the underground-cable database. - create_evemtbench_cable Create a new Table 6 TypLne and ElmLne. - list_evemtbench_external_grid_profiles Inspect the Table 4 external-grid database. - create_evemtbench_external_grid Create a new Table 4 ElmXnet. - list_evemtbench_load_profiles Inspect the Table 5 load-power database. - create_evemtbench_load Create a new Table 5 ElmLod. - list_evemtbench_synchronous_machine_profiles - Inspect the Table 13 machine database. create_grid_with_buses Bootstrap an ElmNet and initial ElmTerm objects. import_project Import a .pfd file and activate it in PowerFactory. create_study_case Create/activate a study case by name (no simulation run). @@ -1156,7 +1145,8 @@ def export_active_grid_pdf( The active project and study case are required. If the Graphics Board has more than one single-line diagram, provide its exact name. Existing files - are never overwritten. + are never overwritten. Failure to restore the previous diagram is appended + as a warning in the message without changing the export's success flag. Parameters ---------- @@ -1191,21 +1181,24 @@ def export_active_grid_pdf( @mcp.tool() def list_evemtbench_components( - voltage_class: str = "", - rated_power_mva: float | None = None, + component_type: str, + filters: dict[str, Any] | None = None, ) -> str: - """List executable EvEMTBench transformer data without opening PowerFactory. - - Leave both filters empty to list every supported fixed-voltage transformer - class. Set ``voltage_class`` for one class, and optionally select an exact - published ``rated_power_mva``. The response includes Table 10 zero-sequence - rules and Table 12 saturation data even though those fields are not yet - mapped to PowerFactory attributes. + """Inspect one EvEMTBench catalog and discover its creation parameters. + + component_type: transformer, overhead_line, underground_cable, + external_grid, load, or synchronous_machine. Omit filters to see the full + selected catalog. Allowed filters: transformer={voltage_class,rated_power_mva}, + overhead_line={voltage_kv,profile}, underground_cable={profile}, + external_grid={voltage_kv}, load={voltage_kv}, + synchronous_machine={machine_type}. Profile choices: p05, p50, p95. + Returns source data, mapping limits, available_filters and creation_parameters + (names, types, required flags and defaults). No PowerFactory connection is opened. """ _, DIgSILENTAgent = _load_modules() return _to_json(DIgSILENTAgent.list_evemtbench_components( - voltage_class, - rated_power_mva, + component_type, + filters, )) @@ -1218,8 +1211,9 @@ def create_evemtbench_component( ``component_type`` must be ``transformer``, ``overhead_line``, ``underground_cable``, ``external_grid``, ``load``, or - ``synchronous_machine``. Put the corresponding specialized creator's - arguments in ``parameters``. Use an empty ``confirmation`` for the + ``synchronous_machine``. First call list_evemtbench_components for the + selected type to discover its creation_parameters and catalog choices. + Put those inputs in ``parameters``. Use an empty ``confirmation`` for the read-only preview, then repeat the same request with the exact returned ``confirmation_required`` value. Validation, creation, verification, and rollback are delegated unchanged to the existing specialized creator. @@ -1232,339 +1226,6 @@ def create_evemtbench_component( ) -@mcp.tool() -def create_evemtbench_transformer( - voltage_class: str, - rated_power_mva: float, - nominal_frequency_hz: float, - vector_group: str, - clock_number: int, - transformer_name: str, - type_name: str, - high_voltage_bus_name: str, - low_voltage_bus_name: str, - grid_name: str = "", - seed: int = 0, - out_of_service: bool = False, - confirmation: str = "", - open_digsilent: bool = True, -) -> str: - """Preview or create a new transformer type and connected transformer. - - No existing transformer or type is used as a template. Table 11 supplies - the sampled electrical values and winding-connection family. The caller - explicitly supplies the clock number because the paper does not select one - for every transformer. Nominal frequency must explicitly be 50 or 60 Hz; - it is not inferred from the grid. Existing high- and low-voltage buses are required. - An empty ``confirmation`` performs a read-only preview; a confirmed call - creates the ``TypTr2``, connected ``ElmTr2``, cubicles, and closed switches. - The new type is rolled back if element creation fails. Tables 10 and 12 are - stored in the database but are not applied until their PowerFactory - attribute mappings have been validated. - """ - return _agent_result( - "create_evemtbench_transformer", - voltage_class, - rated_power_mva, - nominal_frequency_hz, - vector_group, - clock_number, - transformer_name, - type_name, - high_voltage_bus_name, - low_voltage_bus_name, - grid_name, - seed, - out_of_service, - confirmation, - open_digsilent, - structured=True, - ) - - -@mcp.tool() -def list_evemtbench_line_profiles( - voltage_kv: float | None = None, - profile: str = "", -) -> str: - """List executable EvEMTBench overhead-line sequence profiles. - - The database exposes the paper's 5th, 50th and 95th percentile R/X/C - values per kilometre for 20, 110 and 345 kV overhead lines, plus the - corresponding Table 3 length ranges. This is a read-only catalog tool and - does not open PowerFactory. - """ - _, DIgSILENTAgent = _load_modules() - return _to_json(DIgSILENTAgent.list_evemtbench_line_profiles( - voltage_kv, - profile, - )) - - -@mcp.tool() -def create_evemtbench_line( - voltage_kv: float, - nominal_frequency_hz: float, - rated_current_ka: float, - profile: str, - length_km: float, - line_name: str, - type_name: str, - bus1_name: str, - bus2_name: str, - grid_name: str = "", - out_of_service: bool = False, - confirmation: str = "", - open_digsilent: bool = True, -) -> str: - """Preview or create a new paper-backed TypLne and connected ElmLne. - - The caller selects a published ``p05``, ``p50`` or ``p95`` per-kilometre - electrical profile and supplies the engineering current rating. Both buses - must already exist at the selected 20, 110 or 345 kV voltage level, and the - requested length must remain inside the paper's range for that voltage. - Capacitances are converted from nF/km to PowerFactory's uF/km fields. - - An empty confirmation performs a read-only preview. A confirmed call - creates a new ``TypLne``, connected ``ElmLne``, cubicles and closed circuit - breakers, rolling the new objects back if creation fails. This is a sequence - equivalent; it does not reproduce the paper's geometric ULM model. - """ - return _agent_result( - "create_evemtbench_line", - voltage_kv, - nominal_frequency_hz, - rated_current_ka, - profile, - length_km, - line_name, - type_name, - bus1_name, - bus2_name, - grid_name, - out_of_service, - confirmation, - open_digsilent, - structured=True, - ) - - -@mcp.tool() -def list_evemtbench_cable_profiles( - profile: str = "", -) -> str: - """List executable 20 kV EvEMTBench cable sequence profiles. - - The database exposes Table 6's 5th, 50th and 95th percentile R/X/C values - per kilometre and the Table 3 cable-length range. Construction and - installation data are returned for inspection but are not written. This - read-only catalog tool does not open PowerFactory. - """ - _, DIgSILENTAgent = _load_modules() - return _to_json(DIgSILENTAgent.list_evemtbench_cable_profiles(profile)) - - -@mcp.tool() -def create_evemtbench_cable( - nominal_frequency_hz: float, - rated_current_ka: float, - profile: str, - length_km: float, - cable_name: str, - type_name: str, - bus1_name: str, - bus2_name: str, - grid_name: str = "", - out_of_service: bool = False, - confirmation: str = "", - open_digsilent: bool = True, -) -> str: - """Preview or create a 20 kV Table 6 cable sequence equivalent. - - The caller selects a published ``p05``, ``p50`` or ``p95`` profile, - supplies the engineering current rating, and selects 50 or 60 Hz. Both - existing buses must be 20 kV, and length must be 0.5 to 3.75 km. An empty - confirmation is read-only; a confirmed call creates a new ``TypLne``, - connected ``ElmLne``, cubicles, and closed circuit breakers. Failed - creation is rolled back. The tool does not reproduce the paper's geometric - ULM cable model. - """ - return _agent_result( - "create_evemtbench_cable", - nominal_frequency_hz, - rated_current_ka, - profile, - length_km, - cable_name, - type_name, - bus1_name, - bus2_name, - grid_name, - out_of_service, - confirmation, - open_digsilent, - structured=True, - ) - - -@mcp.tool() -def list_evemtbench_external_grid_profiles( - voltage_kv: float | None = None, -) -> str: - """List executable EvEMTBench Table 4 external-grid profiles. - - Leave ``voltage_kv`` empty to return the 110 kV and combined 380/345 kV - classes. An exact 110, 345, or 380 kV filter returns the applicable source - ranges. This read-only catalog tool does not open PowerFactory. - """ - _, DIgSILENTAgent = _load_modules() - return _to_json( - DIgSILENTAgent.list_evemtbench_external_grid_profiles(voltage_kv) - ) - - -@mcp.tool() -def create_evemtbench_external_grid( - voltage_kv: float, - external_grid_name: str, - bus_name: str, - grid_name: str = "", - seed: int = 0, - out_of_service: bool = False, - confirmation: str = "", - open_digsilent: bool = True, -) -> str: - """Preview or create a seeded Table 4 external-grid equivalent. - - The selected existing bus must be exactly 110, 345, or 380 kV. Table 4 - supplies uniformly sampled short-circuit power, positive- and zero-sequence - ratios, voltage setpoint, and initial phase angle. An empty confirmation is - read-only; a confirmed call creates one connected slack ``ElmXnet`` plus a - cubicle and closed circuit breaker. Failed creation is rolled back. - """ - return _agent_result( - "create_evemtbench_external_grid", - voltage_kv, - external_grid_name, - bus_name, - grid_name, - seed, - out_of_service, - confirmation, - open_digsilent, - structured=True, - ) - - -@mcp.tool() -def list_evemtbench_load_profiles( - voltage_kv: float | None = None, -) -> str: - """List executable EvEMTBench Table 5 load-power profiles. - - Leave ``voltage_kv`` empty to return the 20, 110 and 345 kV profiles. - The catalog also returns the paper's ZIP coefficients, which remain - read-only until their PowerFactory attributes are live-validated. - """ - _, DIgSILENTAgent = _load_modules() - return _to_json( - DIgSILENTAgent.list_evemtbench_load_profiles(voltage_kv) - ) - - -@mcp.tool() -def create_evemtbench_load( - voltage_kv: float, - load_name: str, - bus_name: str, - grid_name: str = "", - seed: int = 0, - out_of_service: bool = False, - confirmation: str = "", - open_digsilent: bool = True, -) -> str: - """Preview or create a seeded Table 5 balanced load. - - At 110 and 345 kV, active and reactive powers are sampled directly. At - 20 kV, apparent power and a reactive-power angle spanning the paper's - 0.95 capacitive to 0.8 inductive limits are sampled, then converted to P - and Q. An empty confirmation is read-only; a confirmed call creates one - connected ``ElmLod`` plus a cubicle and closed circuit breaker. Failed - creation is rolled back. - """ - return _agent_result( - "create_evemtbench_load", - voltage_kv, - load_name, - bus_name, - grid_name, - seed, - out_of_service, - confirmation, - open_digsilent, - structured=True, - ) - - -@mcp.tool() -def list_evemtbench_synchronous_machine_profiles( - machine_type: str = "", -) -> str: - """List EvEMTBench Table 13 synchronous-machine profiles. - - Use ``salient_pole`` or ``round_rotor`` to filter the catalog. Leave the - value empty to return both. This tool is read-only and does not open - PowerFactory. Round-rotor creation is available separately; salient-pole - creation remains disabled because Table 13 omits q-axis transient data. - """ - _, DIgSILENTAgent = _load_modules() - return _to_json( - DIgSILENTAgent.list_evemtbench_synchronous_machine_profiles( - machine_type - ) - ) - - -@mcp.tool() -def create_evemtbench_synchronous_machine( - machine_type: str, - rated_power_mva: float, - machine_name: str, - type_name: str, - bus_name: str, - grid_name: str = "", - seed: int = 0, - active_power_mw: float = 0.0, - reactive_power_mvar: float = 0.0, - out_of_service: bool = False, - confirmation: str = "", - open_digsilent: bool = True, -) -> str: - """Preview or create a seeded Table 13 round-rotor machine. - - The MVA rating must be 25 to 1200. Its rated voltage is derived from the - paper's rating rule, so the selected existing bus must be at 6.3, 10.5, - 21, or 27 kV as applicable. An empty confirmation is read-only; a confirmed - call creates a new ``TypSym``, connected ``ElmSym``, cubicle, and closed - circuit breaker. New objects are rolled back on failure. ``salient_pole`` - remains catalog-only because Table 13 does not provide xq transient data. - """ - return _agent_result( - "create_evemtbench_synchronous_machine", - machine_type, - rated_power_mva, - machine_name, - type_name, - bus_name, - grid_name, - seed, - active_power_mw, - reactive_power_mvar, - out_of_service, - confirmation, - open_digsilent, - structured=True, - ) @mcp.tool() diff --git a/PowerFactory/README.md b/PowerFactory/README.md index 3b74300..cfa9f1a 100644 --- a/PowerFactory/README.md +++ b/PowerFactory/README.md @@ -1,305 +1,82 @@ # DIgSILENT PowerFactory MCP Agent -A Model Context Protocol (MCP) server that exposes DIgSILENT PowerFactory simulation capabilities to AI assistants such as Claude. The agent automates RMS transient stability simulations, load flow and short-circuit calculations, parameter modifications, and result exports — all through a clean, tool-callable interface. - ---- +Connect an AI assistant to DIgSILENT PowerFactory to inspect models, run +calculations and fault simulations, build equipment, and export results. ## Architecture -``` +```text Claude / AI assistant - │ MCP protocol + │ MCP protocol ▼ -MCP_PowerFactory.py ← MCP Python SDK 2 server - │ Python calls +MCP_PowerFactory.py MCP Python SDK 2 server + │ Python calls ▼ -Agent_DIgSILENT.py ← Simulation engine - │ +Agent_DIgSILENT.py Simulation and shared PowerFactory helpers + │ + ├── Inherits EvEMTBenchMixin from evemtbench.py + │ ├── Reads data/evemtbench_*.json + │ ├── Lists catalogs, filters and creation parameters + │ └── Plans, creates, verifies and rolls back equipment + │ + │ Simulation, inspection, export and equipment-creation calls ▼ -DIgSILENT PowerFactory +DIgSILENT PowerFactory │ ▼ -Output folder: CSV results, PNG plots, optional .pfd export +Outputs: CSV results, PNG plots, diagram PDF, optional .pfd export ``` +`evemtbench.py` is part of the agent layer, not a separate MCP server. +Catalog listing returns JSON data without opening PowerFactory. Equipment +creation and simulation require a PowerFactory connection. + ## Features -- **Full simulation pipeline** — connect, activate study case, load flow, RMS transient simulation, CSV export, plot generation, optional PFD export in one call. -- **Custom fault scenarios** — define bus faults, line faults, and generator switching events at call time without editing config files. -- **Parameter modification** — update any PowerFactory attribute on any object via the MCP interface. -- **Model inspection and discovery** — inspect the active project and study case, read selected attributes, and discover objects using raw queries or friendly component categories. -- **Guarded component management** — create or safely preview and delete buses, loads, generators, lines, and two-winding transformers. -- **Diagram and switchgear synchronization** — optionally update the active single-line diagram and create one closed circuit breaker in each generated connection cubicle. -- **Load flow and short-circuit** — run Load flow and Short-Circuit calculation. -- **Automatic plot generation** — bus voltage magnitudes and generator speeds plotted from exported CSV results. -- **Study case management** — create, copy, and activate study cases with idempotency support (replay-safe via `request_id`). -- **Project import/export** — import `.pfd` files and export the active project back to `.pfd`. -- **Grid diagram export** — export a selected active-project single-line diagram to PDF. -- **Greenfield equipment types** — create paper-backed transformer, overhead-line, and underground-cable types without requiring project templates. -- **CSV result reading** — auto-discovers the latest result file or reads a specified path. -- **JSON configuration** — all simulation parameters controlled through `simulation_config.json`. - ---- +- Model inspection, parameter changes, and study-case management. +- Load flow, short-circuit, contingency analysis, and RMS fault simulations. +- Grid bootstrap, component management, and six EvEMTBench equipment catalogs. +- CSV results, PNG plots, single-line PDF export, and optional project export. ## Implemented Functions -### MCP Tools (`MCP_PowerFactory.py`) - -| Tool | Description | -|------|-------------| -| `ping` | Health check — returns `"pong"`. | -| `close_digsilent` | Closes the PowerFactory session. | -| `get_config` | Returns the active `simulation_config.json` as a JSON string. | -| `get_active_project` | Returns the active PowerFactory project without modifying it. | -| `get_active_study_case` | Returns the active PowerFactory study case without modifying it. | -| `get_parameters` | Reads selected attributes from matching calculation-relevant objects, with opt-in metadata and active-project library inspection. | -| `list_objects` | Lists calculation-relevant objects using a raw PowerFactory query. | -| `list_components` | Lists objects using friendly equipment categories. | -| `list_study_cases` | Lists study cases and identifies the active case. | -| `list_contingencies` | Lists configured fault-library cases and their `EvtOutage`, `EvtSwitch`, and `EvtShc` events without modifying the project. | -| `create_contingency` | Creates or reuses an idempotent switch-based contingency definition. | -| `get_contingency_configuration` | Reads the active Contingency Analysis command settings without running a calculation. | -| `configure_contingency_screening` | Persists selected DC/AC-linearised screening criteria without running a calculation. | -| `add_contingency_result_variables` | Adds variables to the configured AC or DC contingency result recording selection without running a calculation. | -| `remove_contingency_result_variables` | Removes variables from matching AC or DC result recording selections without running a calculation. | -| `export_active_grid_pdf` | Exports one active-project single-line diagram to a new PDF file. | -| `create_evemtbench_component` | Routes one nested parameter object to any validated EvEMTBench component creator. | -| `list_evemtbench_components` | Reads the versioned EvEMTBench transformer database without opening PowerFactory. | -| `create_evemtbench_transformer` | Previews or creates a new database-backed `TypTr2` and connected `ElmTr2` without a template. | -| `list_evemtbench_line_profiles` | Reads the versioned 20/110/345 kV overhead-line sequence profiles without opening PowerFactory. | -| `create_evemtbench_line` | Previews or creates a new profile-backed `TypLne` and connected `ElmLne` without a template. | -| `list_evemtbench_cable_profiles` | Reads the versioned 20 kV underground-cable sequence profiles without opening PowerFactory. | -| `create_evemtbench_cable` | Previews or creates a new Table 6 cable-equivalent `TypLne` and connected `ElmLne`. | -| `list_evemtbench_external_grid_profiles` | Reads the versioned EvEMTBench Table 4 external-grid ranges without opening PowerFactory. | -| `create_evemtbench_external_grid` | Previews or creates a seeded Table 4 `ElmXnet` on an existing bus. | -| `list_evemtbench_load_profiles` | Reads the versioned EvEMTBench Table 5 load-power ranges without opening PowerFactory. | -| `create_evemtbench_load` | Previews or creates a seeded Table 5 `ElmLod` on an existing bus. | -| `list_evemtbench_synchronous_machine_profiles` | Reads the versioned EvEMTBench Table 13 synchronous-machine ranges without opening PowerFactory. | -| `create_evemtbench_synchronous_machine` | Previews or creates a seeded round-rotor `TypSym` and connected `ElmSym`. | -| `create_grid_with_buses` | Previews or bootstraps an `ElmNet` and initial `ElmTerm` objects in the active project. | -| `import_project` | Imports a `.pfd` project file and activates it. | -| `create_study_case` | Creates (or activates) a study case by name, copying from a base case when needed. Supports `request_id` for idempotency. | -| `modify_parameter` | Sets one attribute on all PowerFactory objects matching a query string. Auto-casts string values to the correct type. | -| `add_component` | Creates a bus, load, generator, line, or two-winding transformer with validation, rollback, circuit breakers, and optional diagram insertion. | -| `delete_component` | Previews or confirms exact-name component deletion, including generated cubicles, circuit breakers, and optional diagram cleanup. | -| `run_loadflow` | Runs a standalone load flow calculation (ComLdf). | -| `run_short_circuit` | Runs a standalone short-circuit calculation (ComShc). | -| `run_contingency_analysis` | Runs ComSimoutage using its configured method or an explicit AC, DC, AC-linearised, or linearised-screening mode. | -| `get_contingency_results` | Reads a bounded, optionally variable-filtered slice of the configured AC or DC contingency result file using bulk or sparse access. | -| `get_contingency_summary` | Interprets existing contingency results as affected elements, convergence, overloads, and voltage violations. | -| `run_simulation` | Executes the full pipeline defined in `simulation_config.json`. | -| `run_custom_case` | Runs one fault simulation with parameters supplied at call time (no config file edit required). | -| `read_results_csv` | Reads an RMS result CSV; auto-discovers the latest file if no path is given. | - -The contingency tools can create switch-based definitions and inspect or run -the active `ComSimoutage` command. An explicit calculation mode applies only to -that run; the study case's previous mode is restored afterward. - -Read-only contingency tools search the active study case for an existing -`ComSimoutage`; they do not call `GetFromStudyCase`, which can create a missing -command. `get_contingency_results` accepts exact variable identifiers such as -`["c:loading"]`, returns each column's original index and object name/class when -PowerFactory provides one, and automatically includes `b:i_obj` for row-to-case -mapping. It uses `ElmRes.GetColumnValues` with a temporary, deleted `IntVec`, -falling back to PowerFactory's sparse iterator. Filtered reads resolve columns -from `IntMon` selections and `FindColumn`, avoiding a full `ElmRes` metadata -scan. `max_read_seconds` supplies a cooperative read budget; narrow `variables`, -`max_rows`, or `max_columns` for large files. - -`configure_contingency_screening` intentionally persists selected screening -settings. It can choose DC or AC-linearised screening; enable the simple and -combined loading criteria; set their loading and relative-change thresholds; -ignore components already overloaded in the base case; and restrict screening -to recorded elements. It returns the previous and resulting settings without -executing the analysis. - -Result recording is read from `IntMon` selections, never from stale result -columns. Both recording tools report each unreadable monitor once and keep -the record of successful changes to other objects. Class selections identified -by `IntMon.className` count as recorded for objects of that exact class. -Object-specific removal of a class-recorded variable is reported as failed, -without changing either its class or object selections. Other variables can -still be removed. Unidentified targetless selections remain errors; additions -and absence claims are skipped when their state is unknown. - -Removal reports `failed_variables` separately from `already_absent_variables`. -A variable can appear in both `removed` and `failed` when different monitors -produce a partial change; rerun the analysis whenever `removed_variables` is -non-zero. `configured_objects` counts objects with readable matching selections, -and `objects_without_selection` counts objects with no known selection. -The tool schemas require strings; blank or whitespace-only variables are also -rejected. Missing screening flags are returned as `null`, rather than disabled. - -PowerFactory 2026's User Manual, section 19.3.1.1, explicitly supports class-name -variable selections for contingency analysis. These tools report them rather -than attempting to remove a class-wide variable for an individual object. -Its Python Reference, section 5.6.26, documents `IntMon.RemoveVar` return codes: -`0` means removed, `1` means not found; other codes or exceptions are failures. - -### Simulation Engine (`Agent_DIgSILENT.py`) - -| Class / Method | Description | -|----------------|-------------| -| `SimulationConfig` | Dataclass holding all simulation parameters. `from_json()` loads from a JSON file. | -| `Logger` | Timestamped console logging with `info`, `ok`, `warn`, `error`, `section`. | -| `DIgSILENTAgent.__init__` | Initialises the agent from a `SimulationConfig`. | -| `DIgSILENTAgent.connect` | Connects to PowerFactory and activates the configured project. | -| `DIgSILENTAgent.activate_study_case` | Activates or creates the target study case. | -| `DIgSILENTAgent.run_loadflow` | Executes ComLdf. | -| `DIgSILENTAgent.run_contingency_analysis` | Executes ComSimoutage without changing its contingency filters and optionally selects a validated calculation mode. | -| `DIgSILENTAgent.run_rms_simulation` | Applies fault events, then runs ComInc + ComSim. | -| `DIgSILENTAgent._apply_fault_event` | Builds the event sequence for bus faults, line faults, or generator switches. | -| `DIgSILENTAgent.addSwitchEvent` | Creates a PowerFactory `EvtSwitch` event. | -| `DIgSILENTAgent.export_results_to_csv` | Exports the RMS result object to CSV via ComRes. | -| `DIgSILENTAgent.export_active_grid_pdf` | Exports a selected Graphics Board single-line diagram to PDF via ComWr. | -| `DIgSILENTAgent.create_evemtbench_component` | Validates and routes a unified request to an existing specialized component creator. | -| `DIgSILENTAgent.list_evemtbench_components` | Validates and filters the versioned transformer database. | -| `DIgSILENTAgent.create_evemtbench_transformer` | Samples, previews, creates, connects, verifies, and rolls back a new transformer type and element. | -| `DIgSILENTAgent.list_evemtbench_line_profiles` | Validates and filters the versioned overhead-line sequence profiles. | -| `DIgSILENTAgent.create_evemtbench_line` | Previews, creates, connects, verifies, and rolls back a new overhead-line type and element. | -| `DIgSILENTAgent.list_evemtbench_cable_profiles` | Validates and filters the versioned underground-cable sequence profiles. | -| `DIgSILENTAgent.create_evemtbench_cable` | Previews, creates, connects, verifies, and rolls back a new cable type and element. | -| `DIgSILENTAgent.list_evemtbench_external_grid_profiles` | Validates and filters the external-grid catalog. | -| `DIgSILENTAgent.create_evemtbench_external_grid` | Samples, previews, creates, connects, verifies, and rolls back a new external grid. | -| `DIgSILENTAgent.create_grid_with_buses` | Previews, creates or reuses, verifies, and rolls back a grid and its initial buses. | -| `DIgSILENTAgent.export_project_to_pfd` | Exports the active project to `.pfd` via ComPfdexport. | -| `DIgSILENTAgent.generate_standard_plots` | Reads the exported CSV and produces voltage and generator speed PNG plots. | -| `DIgSILENTAgent.import_project` | Imports a `.pfd` file into the running PowerFactory session. | -| `DIgSILENTAgent.create_study_case` | Creates or reuses a study case by exact name. | -| `DIgSILENTAgent.modify_parameter` | Sets an attribute on all objects returned by `GetCalcRelevantObjects`. | -| `DIgSILENTAgent.add_component` | Creates and verifies supported network components and their connections. | -| `DIgSILENTAgent.delete_component` | Performs guarded exact-name component deletion and cleanup. | -| `DIgSILENTAgent.short_circuit` | Standalone ComShc execution. | -| `DIgSILENTAgent.run_pipeline` | Orchestrates the full workflow and returns a structured status report. | -| `DIgSILENTAgent.close` | Shuts down PowerFactory and clears shared handles. | - -## Greenfield Grid Bootstrap - -`create_grid_with_buses` removes the requirement for a pre-existing -calculation grid and buses. It requires an active PowerFactory project and -study case. It can create the standard `Network Model` / `Network Data` folder -hierarchy, a new `ElmNet`, and one to 100 `ElmTerm` objects without using a -template. A confirmed call activates the grid and verifies that PowerFactory -reports it as calculation-relevant. - -```text -create_grid_with_buses( - grid_name="Research Grid", - nominal_frequency_hz=50, - buses=[ - {"name": "HV Bus", "nominal_voltage_kv": 110}, - { - "name": "MV Bus", - "nominal_voltage_kv": 20, - "out_of_service": true - } - ], - confirmation="" -) -``` - -The first call is a read-only preview. Repeat it with the exact returned -`confirmation_required` value to apply the plan. An existing grid or bus is -reused only when its frequency, voltage, and service state match exactly; -conflicting objects are reported without modification. If creation fails, all -objects created by that call are removed. The tool does not create a project, -study case, single-line diagram, equipment type, or calculation command. - -The bootstrap currently accepts 50 or 60 Hz so its grid is compatible with the -EvEMTBench transformer and overhead-line creators. Once the buses exist, pass -their names and the grid name directly to those tools. - -## Component Management - -### Create a component - -```text -add_component( - component_type, - component_name, - parameters, - grid_name="", - out_of_service=false, - open_digsilent=true, - update_graphics=false -) -``` - -Supported component parameters: - -| Type | Required parameters | Optional parameters | +| Task | Main tools | Guide | |---|---|---| -| `bus` | `nominal_voltage_kv` | — | -| `load` | `bus_name`, `active_power_mw` | `reactive_power_mvar` | -| `generator` | `bus_name`, `template_generator`, `active_power_mw` | `reactive_power_mvar` | -| `line` | `bus1_name`, `bus2_name`, `template_line`, `length_km` | — | -| `transformer` | `high_voltage_bus_name`, `low_voltage_bus_name`, `template_transformer` | — | +| Inspect models | `list_components`, `list_objects`, `get_parameters` | [Tool reference](references/tool_guide.md#implemented-functions) | +| Manage study cases and imports | `list_study_cases`, `create_study_case`, `import_project` | [Examples](references/tool_guide.md#prompt-examples) | +| Run calculations | `run_loadflow`, `run_short_circuit` | [Tool reference](references/tool_guide.md#implemented-functions) | +| Simulate faults and read results | `run_custom_case`, `run_simulation`, `read_results_csv` | [Configuration and outputs](references/tool_guide.md#configuration) | +| Run contingency analysis | `list_contingencies`, `run_contingency_analysis`, `get_contingency_summary` | [Contingency tools](references/tool_guide.md#implemented-functions) | +| Bootstrap grids and buses | `create_grid_with_buses` | [Bootstrap](references/tool_guide.md#greenfield-grid-bootstrap) | +| Add or delete equipment | `add_component`, `delete_component` | [Component management](references/tool_guide.md#component-management) | +| Build paper-backed equipment | `list_evemtbench_components`, `create_evemtbench_component` | [EvEMTBench](#evemtbench-equipment) | +| Export a diagram | `export_active_grid_pdf` | [Export behavior](references/tool_guide.md#automated-tests-and-validation-reports) | -Component names are limited to 40 characters, matching PowerFactory's -`loc_name` limit. +The [detailed guide](references/tool_guide.md) contains full tool and agent-method +inventories, parameter examples, and prompt examples. -An explicit `null` for optional reactive power is treated as an omitted value -and defaults to zero. Boolean values are not accepted for numeric parameters. +## EvEMTBench Equipment -Each generated connection cubicle contains one closed circuit breaker -(`StaSwitch`, `aUsage="cbk"`, `on_off=1`). Set `update_graphics=true` to -request insertion into the active single-line diagram. +Two public MCP tools cover six component families: -Graphical updates use the Diagram Layout Tool's automatic insertion mode so -existing diagram objects are not re-laid out as a K-neighbourhood. +- `list_evemtbench_components(component_type, filters)` reads catalog data and + returns source information, available filters, and creation parameters. +- `create_evemtbench_component(component_type, parameters)` previews or creates + equipment using the selected catalog. -If graphical insertion fails, the network component remains created and the -tool returns `success=false` with the graphical error. Check the returned -message before retrying to avoid creating a duplicate. +Supported types: `transformer`, `overhead_line`, `underground_cable`, +`external_grid`, `load`, and `synchronous_machine`. -PowerFactory may place an isolated bus far from the existing network when it -is inserted directly into the diagram. For a better anchored result, create -the bus with `update_graphics=false`, then create its first connecting line -with `update_graphics=true`. +Discover a line profile without opening PowerFactory: -### Delete a component - -```text -delete_component( - component_type, - component_name, - grid_name="", - confirmation="", - open_digsilent=true, - update_graphics=false +```python +list_evemtbench_components( + component_type="overhead_line", + filters={"voltage_kv": 110, "profile": "p50"}, ) ``` -Calling `delete_component` without confirmation returns a preview and the -required confirmation phrase: - -```text -DELETE -``` - -Name lookup is case-insensitive. The confirmation phrase contains the full -PowerFactory object path, binds the confirmation to the previewed grid, and -must be copied exactly from the preview. - -Confirmed deletion removes the exact component. A connection cubicle is removed -only when its generated name and sole generated circuit breaker both match; -cubicles containing relays, instrument transformers, or other objects are -preserved. A bus cannot be deleted while it still has connected cubicles. Set -`update_graphics=true` to remove matching diagram objects from every single-line -diagram in the active project and rebuild the active view. - -The response reports component deletion separately from cleanup. In particular, -`success=false` with `deleted=true` means the network component was removed but -some graphical or cubicle cleanup failed; do not retry the component deletion. - ---- - -## Unified EvEMTBench Component Creator - -`create_evemtbench_component` is the common write entry point for the six -validated component families: `transformer`, `overhead_line`, -`underground_cable`, `external_grid`, `load`, and `synchronous_machine`. -Supply the selected specialized creator's arguments inside `parameters`: +Preview a load in an existing grid with a compatible bus: ```python create_evemtbench_component( @@ -312,481 +89,109 @@ create_evemtbench_component( "seed": 42, "out_of_service": True, "confirmation": "", - "open_digsilent": True, }, ) ``` -An empty `confirmation` keeps the request in preview mode. Repeat the same -request with the exact returned `confirmation_required` value to apply it. -The unified tool delegates validation, seeded sampling, creation, retained-value -checks, and rollback to the existing specialized creator without changing its -behavior. The specialized tools remain available for backward compatibility -and for clients that benefit from their explicit per-component schemas. - ---- - -## EvEMTBench Transformer Database and Creator - -The versioned database at -[`data/evemtbench_transformers.json`](data/evemtbench_transformers.json) -stores the fixed-voltage transformer classes from EvEMTBench Tables 10-12. -Use `list_evemtbench_components` to inspect classes, ratings, ranges, vector -groups, zero-sequence rules, and saturation data without opening PowerFactory. - -`create_evemtbench_transformer` creates a new `TypTr2` directly; it does not -copy or require an existing transformer. It also creates an `ElmTr2`, two -cubicles, and closed circuit breakers between existing buses. The type is -stored in the project library's `Transformer Types` folder; a confirmed call -creates that folder when it is absent from a greenfield project. The first -call is a read-only preview when `confirmation` is empty: - -```text -create_evemtbench_transformer( - voltage_class="110/20", - rated_power_mva=20, - nominal_frequency_hz=60, - vector_group="YNd", - clock_number=11, - transformer_name="Research Transformer", - type_name="Research Transformer Type", - high_voltage_bus_name="HV Bus", - low_voltage_bus_name="LV Bus", - seed=42, - confirmation="", - open_digsilent=true -) -``` - -The preview validates the grid, buses, nominal voltages, names, rating, vector -group, clock number, and an explicit nominal frequency of 50 or 60 Hz, then -returns the exact confirmation phrase. Frequency is not inferred from the grid -because existing projects can contain types with a different `frnom`. A -confirmed call writes three-phase technology (`nt2ph`), frequency (`frnom`), -the impedance modes, `strn`, `utrn_h`, `utrn_l`, `uktr`, `pcutr`, `pfe`, -`curmg`, `tr2cn_h`, `tr2cn_l`, and `nt2ag`. Structural settings and nameplate -ratings are committed before the performance values so PowerFactory can -refresh dependent type fields. Copper loss and short-circuit voltage are -converted to the equivalent `r1pu` and `x1pu` values before assignment, then -verified again through PowerFactory's derived `pcutr` and `uktr` attributes. -The performance values use PowerFactory's native Python property assignment; -each value is checked immediately and again after the database write. If element -creation fails, the new type, element, cubicles, and switches are rolled back. - -Table 10 zero-sequence and Table 12 saturation data are stored and returned by -the catalog tool, but are not written until their PowerFactory attribute -mappings are validated. The `Block` class and other component families remain -catalog-only for the same reason. The caller must explicitly choose a clock -number because Table 11 specifies vector-group families rather than one clock -number for every generated transformer. - -See [references/evemtbench.md](references/evemtbench.md) for the source mapping -and limitations. - ---- - -## EvEMTBench Overhead-Line Database and Creator - -[`data/evemtbench_overhead_lines.json`](data/evemtbench_overhead_lines.json) -stores the published 5th, 50th and 95th percentile sequence parameters from -Table 7 for 20, 110 and 345 kV overhead lines, together with the corresponding -Table 3 length ranges. `list_evemtbench_line_profiles` inspects this catalog -without opening PowerFactory. - -`create_evemtbench_line` creates a new `TypLne` directly and connects a new -`ElmLne` between two existing equal-voltage buses. The caller selects `p05`, -`p50` or `p95`, supplies the actual current rating, and chooses 50 or 60 Hz. -The first call is a read-only preview: - -```text -create_evemtbench_line( - voltage_kv=110, - nominal_frequency_hz=50, - rated_current_ka=1, - profile="p50", - length_km=10, - line_name="Research Line", - type_name="Research Line Type", - bus1_name="Bus A", - bus2_name="Bus B", - confirmation="", - open_digsilent=true -) -``` - -The preview checks the voltage class, frequency, paper length range, existing -buses, names, and exact confirmation phrase. A confirmed call creates the -project library's `Line Types` folder when needed, writes `uline`, `sline`, -`frnom`, `rline`, `xline`, `cline`, `rline0`, `xline0` and `cline0`, and then -creates the line, two cubicles and closed circuit breakers. Published -capacitances are converted from nF/km to PowerFactory's uF/km attributes. New -objects are rolled back when creation fails. - -This tool deliberately implements a three-phase `TypLne` sequence equivalent, -not the paper's frequency-dependent geometric ULM model. The three published -percentiles are discrete profiles rather than endpoints of an invented -sampling distribution. Tower and conductor geometry remains catalog-only. - ---- - -## EvEMTBench Underground-Cable Database and Creator - -[`data/evemtbench_underground_cables.json`](data/evemtbench_underground_cables.json) -stores Table 6's published 5th, 50th and 95th percentile sequence parameters -for 20 kV underground cables and Table 3's 0.5 to 3.75 km length range. -`list_evemtbench_cable_profiles` returns those profiles plus the paper's -construction and installation data without opening PowerFactory. - -`create_evemtbench_cable` creates a new `TypLne` sequence equivalent and -connects a new `ElmLne` between two existing 20 kV buses. The caller chooses a -published percentile, 50 or 60 Hz, and an engineering current rating because -Table 6 does not publish cable ampacity. The first call is a read-only preview: - -```text -create_evemtbench_cable( - nominal_frequency_hz=50, - rated_current_ka=0.4, - profile="p50", - length_km=2, - cable_name="Research Cable", - type_name="Research Cable Type", - bus1_name="MV Bus A", - bus2_name="MV Bus B", - grid_name="Research Grid", - out_of_service=true, - confirmation="", - open_digsilent=true -) -``` - -Repeat the call with the exact returned `confirmation_required` value to -create the type, cable element, two cubicles, and closed circuit breakers. -Published capacitances are converted from nF/km to PowerFactory's uF/km -attributes, retained values are verified, and new objects are rolled back if -creation fails. +These are MCP tool-call examples. An empty confirmation creates nothing. +Inspect the preview, then repeat the same request with the exact returned +`confirmation_required` string to apply it. If inputs or resolved project +state change, obtain a fresh preview. -The created `TypLne` is a three-phase sequence equivalent suitable for initial -network construction; it is not the paper's geometric ULM cable model. The -catalog preserves insulation, conductor, sheath, arrangement, burial-depth, -spacing, and earth-resistivity data for a future geometric implementation. +Choose the catalog class or profile explicitly; a paper table alone does not +select equipment. See the [equipment examples](references/tool_guide.md#evemtbench-transformer-database-and-creator), +[source mappings](references/evemtbench.md), and +[component catalog](references/evemtbench_component_catalog.md). ---- +### Scope and safety -## EvEMTBench External-Grid Database and Creator +- Bootstrap requires an active project and study case. It does not create a + project or a single-line diagram. +- Lines and cables use sequence equivalents, not geometric ULM models. + Supply a suitable engineering current rating. +- Transformer zero-sequence and saturation mappings, and load ZIP coefficients, + remain catalog-only. Synchronous-machine creation supports round rotors only. +- Sampled research inputs need engineering review. Retained-value checks do not + establish a complete dynamic model or suitability for an energized network. +- Preview/confirmation applies to EvEMTBench creation, bootstrap, and deletion. + Other tools, including `add_component` and `modify_parameter`, can change + the model immediately. Inspect returned status and partial-change warnings. -[`data/evemtbench_external_grids.json`](data/evemtbench_external_grids.json) -stores the Table 4 ranges for 110 kV and combined 380/345 kV external-grid -equivalents. `list_evemtbench_external_grid_profiles` inspects the ranges -without opening PowerFactory. - -`create_evemtbench_external_grid` uses a supplied seed to sample every Table 4 -interval uniformly and connects a new slack `ElmXnet` to an existing bus. The -bus must be exactly 110, 345, or 380 kV. The first call is a read-only preview: - -```text -create_evemtbench_external_grid( - voltage_kv=110, - external_grid_name="Research External Grid", - bus_name="HV Bus", - grid_name="Research Grid", - seed=42, - out_of_service=true, - confirmation="", - open_digsilent=true -) -``` - -Repeat the call with the exact returned `confirmation_required` value to create -the external grid, cubicle, and closed circuit breaker. The tool writes `snss`, -`rntxn`, `x0tx1`, `r0tx0`, `usetp`, and `phiini`, sets `bustp="SL"`, verifies -the retained values, and rolls back new objects if creation fails. The sampled -values are research inputs and must still be checked for suitability in the -target network. The attribute mapping and retained values were validated on -PowerFactory 2026 SP1; SP4 validation remains pending. - ---- - -## EvEMTBench Load Database and Creator - -[`data/evemtbench_loads.json`](data/evemtbench_loads.json) stores the Table 5 -load-power ranges for 20, 110, and 345 kV systems. The read-only -`list_evemtbench_load_profiles` tool returns these ranges without opening -PowerFactory. - -`create_evemtbench_load` samples active and reactive power directly at 110 and -345 kV. At 20 kV it samples apparent power and a reactive-power angle between -the published 0.95 capacitive and 0.8 inductive limits, then calculates P and -Q. The first call is a read-only preview: - -```text -create_evemtbench_load( - voltage_kv=20, - load_name="Research Load", - bus_name="MV Bus", - grid_name="Research Grid", - seed=42, - out_of_service=true, - confirmation="", - open_digsilent=true -) -``` - -Repeat the call with the exact returned `confirmation_required` value to create -the balanced `ElmLod`, cubicle, and closed circuit breaker. The tool verifies -`plini`, `qlini`, the bus connection, and the service state and rolls back new -objects if creation fails. The Table 5 ZIP coefficients remain catalog-only -until their PowerFactory attribute mapping is live-validated. - ---- - -## EvEMTBench Synchronous-Machine Database and Creator - -[`data/evemtbench_synchronous_machines.json`](data/evemtbench_synchronous_machines.json) -stores the salient-pole and round-rotor ranges from Table 13, including the -rated-voltage selection rule and parameter dependencies. Use -`list_evemtbench_synchronous_machine_profiles` to inspect both profiles or to -filter by `salient_pole` or `round_rotor`. - -`create_evemtbench_synchronous_machine` supports the round-rotor profile. The -caller selects a rating from 25 to 1200 MVA and an existing bus. The tool -derives the rated voltage using Table 13 (6.3, 10.5, 21, or 27 kV), samples the -remaining ranges reproducibly from `seed`, and validates all published -reactance and saturation dependencies. The requested P/Q operating point must -fit the MVA rating and the paper's reactive-power limits. - -```python -create_evemtbench_synchronous_machine( - machine_type="round_rotor", - rated_power_mva=100, - machine_name="Research Generator", - type_name="Research Generator Type", - bus_name="Generator Bus 10.5", - grid_name="Research Grid", - seed=42, - active_power_mw=60, - reactive_power_mvar=5, - out_of_service=true, - confirmation="", - open_digsilent=true -) -``` - -Repeat the call with the exact returned `confirmation_required` string to -create the `TypSym`, `ElmSym`, cubicle, and closed circuit breaker. The type is -created without a template, and all new objects are rolled back if creation -fails. Table 13's short-circuit time constants are converted to PowerFactory's -writable open-circuit inputs and checked again through the derived `tds`, -`tqs`, `tdss`, and `tqss` values. The attribute relationships, `iturbo=1` -round-rotor mode, confirmed creation, and structured response were validated -on PowerFactory 2026 SP1. Salient-pole creation remains disabled because Table -13 does not provide `x'q` or `T'q`; the profile continues to be available -through the read-only catalog. - ---- +When upgrading from specialized MCP tools, migrate to the two unified tools +and refresh the client's tool list. Internal Python helpers remain available. +See the [detailed guide](references/tool_guide.md) for per-family constraints. ## Configuration -Copy `simulation_config.example.json` to `simulation_config.json` and fill in your paths and parameters: - -```json -{ - "project_path": "\\\\\\.IntPrj", - "study_case": "Case 1", - "base_study_case": "0. Base", - "output_dir": "C:\\path\\to\\output", - "run_label": "Test_1", - "result_name": "All calculations.ElmRes", - "cases": [ - { - "case_name": "Fault_1", - "fault_type": "bus", - "fault_element": "Bus 25.ElmTerm", - "t_start": 0.0, "t_fault": 1.0, "t_clear": 1.08, "t_end": 10.0, "dt_rms": 0.01 - } - ] -} -``` - -Supported `fault_type` values: `"bus"`, `"line"`, `"gen_switch"`. - - ---- +Copy [simulation_config.example.json](simulation_config.example.json) to +`simulation_config.json`. Set the project, study case, output paths, and fault +cases. Do not commit this machine-specific file. Configure +`POWERFACTORY_PYTHON_PATH` or `PYTHONPATH` for the PowerFactory Python module. +See the [configuration guide](references/tool_guide.md#configuration) for details. ## Output Structure -Each simulation run creates a dedicated subfolder: - -``` -output_dir/ -└── run_label/ - ├── run_label_RMS.csv - ├── run_label_voltages.png - ├── run_label_gen_speeds.png - └── run_label.pfd (optional) -``` - ---- +Each run writes CSV results and PNG plots beneath `output_dir/run_label/`, +with an optional `.pfd` project export. See the +[output example](references/tool_guide.md#output-structure). ## Requirements -See [requirements.txt](requirements.txt) and [INSTALL.txt](INSTALL.txt) for full details. - -- Python 3.10+ -- DIgSILENT PowerFactory 2023 or later (with Python interface enabled) -- PowerFactory Python module path configured through `POWERFACTORY_PYTHON_PATH` or `PYTHONPATH` -- MCP Python SDK 2.x -- numpy >= 1.26 -- matplotlib >= 3.8 -- pandas >= 1.5 - ---- +A licensed Windows installation of PowerFactory with its Python interface is +required for simulations and model changes. Catalog listing works offline. +Use a Python version supported by your PowerFactory installation. +See [INSTALL.txt](INSTALL.txt) and [requirements.txt](requirements.txt) for setup +and dependencies. ## Quick Start -1. Install Python dependencies: `pip install -r requirements.txt` -2. Add the PowerFactory Python path to your environment (see [INSTALL.txt](INSTALL.txt)). -3. Copy and edit the config: `cp simulation_config.example.json simulation_config.json` -4. Start the MCP server: `python MCP_PowerFactory.py` -5. Connect your AI assistant to the server using the MCP protocol. - ---- - -## Prompt Examples +From the `PowerFactory` directory, with your virtual environment active: -Below are example prompts you can send to an AI assistant connected to this MCP server. - -**Health check** -``` -Ping the PowerFactory server and confirm it's reachable. -``` - -**Run a load flow** -``` -Run a load flow on the active study case and tell me if it converged. -``` - -**Run a transient fault simulation** -``` -Simulate a three-phase bus fault on Bus 25 starting at t=1.0 s, cleared at t=1.08 s, -with a total simulation window of 10 s and a time step of 0.01 s. Export the results -and generate the voltage and generator speed plots. -``` - -**Modify a parameter before running** -``` -Set the generator G 01.ElmSym out of service (e:outserv = 1), -then run a load flow and report the result. -``` - -**Inspect the active context** -``` -Return the active PowerFactory project and study case, then list the first ten -buses without modifying anything. -``` - -**Create and connect a bus** -``` -Create an in-service 110 kV bus named "MCP Example Bus" in Grid without a -graphical update. Connect Bus 01 to it with a 1 km line based on -"Line 01 - 02.ElmLne", request a graphical update for the line, and run a load -flow. Stop if any operation fails. -``` - -**Preview and delete a component** -``` -Preview deletion of the load "MCP Example Load" in Grid. Show me the required -confirmation phrase and do not perform the confirmed deletion yet. -``` - -**Create a new study case and run a custom fault** -``` -Create a study case called "Scenario_A" based on "0. Base", -then run a line fault on "Line 12-34.ElmLne" from t=0.5 s to t=0.58 s -in a 15 s window. Save the CSV and plots to C:\Results\Scenario_A. -``` - -**Short-circuit calculation** -``` -Run a short-circuit calculation on the active study case and return the results. -``` - -**Read the latest results** -``` -Read the most recent RMS results CSV and summarise the peak voltage deviations -and minimum generator speed recorded during the simulation. +```powershell +python -m pip install -r requirements.txt +Copy-Item simulation_config.example.json simulation_config.json +python MCP_PowerFactory.py --transport stdio ``` -**Import a project and run the full pipeline** -``` -Import the project at C:\Projects\IEEE39.pfd, activate it, -then run the full simulation pipeline defined in the config file. -``` +Before starting the server, configure your project and output paths as described +above. See [INSTALL.txt](INSTALL.txt) for MCP client configuration. -**Export an active grid diagram to PDF** -``` -Export the single-line diagram named "Summary Grid" from the active -PowerFactory project to C:\Projects\exports\summary-grid.pdf. -Do not overwrite an existing file. -``` +Once connected, start with `ping`, then inspect the context using +`get_active_project` and `get_active_study_case` before requesting changes. -**Inspect and preview an EvEMTBench transformer** -``` -List the available 110/20 kV EvEMTBench transformer ratings. Then preview a -20 MVA YNd11 transformer type named "Research Transformer Type" and an element -named "Research Transformer" between "HV Bus" and "LV Bus", using seed 42. -Return the sampled values and confirmation phrase without creating anything. -``` +## Prompt Examples ---- +> Inspect the active project and study case, then list ten buses without changes. -## Research provenance and affiliation +> Preview a 20 kV EvEMTBench load on MV Bus in Research Grid using seed 42. +> Return the sampled values and confirmation phrase without creating anything. -PowerFactory development and validation contributions by Aswin Krishna Poyil -are carried out at the **Institute of Electrical Energy Systems, -Friedrich-Alexander-Universität Erlangen-Nürnberg (FAU)** under the -supervision of Tobias Lorz and Timon Conrad, whose support provided the -opportunity to pursue this work independently. +See [more prompts](references/tool_guide.md#prompt-examples) for calculations, +fault simulations, component management, and exports. -The transformer-type library generator is informed by Georg Kordowich, -Jonathan Loebel, Julian Oelhaf, Andreas Maier, Siming Bayer, Christian Bergler, -and Johann Jaeger, *A simulation based dataset of faults and events for machine -learning in power systems*, arXiv:2608.19777 (2026). The paper documents the -EvEMTBench element models and parameter ranges used to construct realistic -synthetic grids. See [references/evemtbench.md](references/evemtbench.md) for -the implementation notes and source links. +## EvEMTBench Data Source ---- +Equipment catalogs use [EvEMTBench (Kordowich et al., 2026)](https://arxiv.org/abs/2608.19777). +See [source mappings and limitations](references/evemtbench.md). ## License MIT -## Automated tests and validation reports +## Automated Tests and Validation Reports -The EvEMTBench catalogs and builders are implemented in `evemtbench.py` and -inherited by `DIgSILENTAgent`; the public MCP tool names and arguments are unchanged. -Equipment-type rollback and confirmation hashing share helpers in that module. - -Grid-bootstrap confirmation is bound to the active project and study case, -the resolved grid, and the create/reuse/activation decisions. If that state -changes, request a fresh preview before confirming, including for repeated calls. - -PDF export restores a previously active single-line diagram in a `finally` -block after success or failure. If no current diagram is reported, there is -no diagram to restore. Restoration failure is reported as a failed export; -the PDF may already have been written. Successful single-line restoration was -verified live on SP1 in [FT-26](evidence/FT-26_pdf_diagram_restoration_sp1.txt). -Failure-path restoration has automated coverage; active plot pages are not covered. -Bootstrap stale-token rejection after creation was verified live in -[FT-25](evidence/FT-25_bootstrap_confirmation_binding_sp1.txt). - -Run the PowerFactory unit and catalog tests from the repository root: +From the repository root: ```powershell python -m pytest PowerFactory/tests -q ``` -The installed-wheel smoke test, `tests/check_evemtbench_wheel.py`, runs separately -in CI against an installed package outside the checkout. -Validation records and the contribution and function-test reports are in -[`evidence/`](evidence/README.md). +The installed-wheel smoke check, +[`check_evemtbench_wheel.py`](tests/check_evemtbench_wheel.py), runs separately +in CI against an installed package outside the checkout. Automated tests do +not replace licensed-runtime validation. + +Live records and contribution and function-test reports are indexed in +[`evidence/`](evidence/README.md). See the +[validation notes](references/tool_guide.md#automated-tests-and-validation-reports) +for confirmation binding and PDF diagram restoration limitations. diff --git a/PowerFactory/evemtbench.py b/PowerFactory/evemtbench.py index 03e03ec..f00af5a 100644 --- a/PowerFactory/evemtbench.py +++ b/PowerFactory/evemtbench.py @@ -5,10 +5,79 @@ import json import math import random -from typing import Any, Optional +from typing import Any, Optional, get_args class EvEMTBenchMixin: + _evemtbench_creators = { + "transformer": "create_evemtbench_transformer", + "overhead_line": "create_evemtbench_line", + "underground_cable": "create_evemtbench_cable", + "external_grid": "create_evemtbench_external_grid", + "load": "create_evemtbench_load", + "synchronous_machine": "create_evemtbench_synchronous_machine", + } + + @staticmethod + def _evemtbench_parameter_schema(method): + schema = {} + types = {str: "string", float: "number", int: "integer", bool: "boolean"} + for name, parameter in inspect.signature(method).parameters.items(): + if name.startswith("_"): + continue + annotation = parameter.annotation + arguments = get_args(annotation) + nullable = type(None) in arguments + if nullable: + annotation = next(arg for arg in arguments if arg is not type(None)) + entry = { + "type": types.get(annotation, str(annotation)), + "required": parameter.default is inspect.Parameter.empty, + } + if nullable: + entry["nullable"] = True + if parameter.default is not inspect.Parameter.empty: + entry["default"] = parameter.default + schema[name] = entry + return schema + + @classmethod + def list_evemtbench_components(cls, component_type: str, filters=None): + """List one component catalog and its creation inputs without PowerFactory.""" + listings = { + "transformer": "list_evemtbench_transformer_profiles", + "overhead_line": "list_evemtbench_line_profiles", + "underground_cable": "list_evemtbench_cable_profiles", + "external_grid": "list_evemtbench_external_grid_profiles", + "load": "list_evemtbench_load_profiles", + "synchronous_machine": "list_evemtbench_synchronous_machine_profiles", + } + kind = str(component_type or "").strip().lower().replace("-", "_") + if kind not in listings: + return { + "success": False, + "component_type": kind, + "supported_component_types": list(listings), + "message": "component_type must be one of: " + ", ".join(listings), + } + listing = getattr(cls, listings[kind]) + metadata = { + "component_type": kind, + "available_filters": cls._evemtbench_parameter_schema(listing), + "creation_parameters": cls._evemtbench_parameter_schema( + getattr(cls, cls._evemtbench_creators[kind]) + ), + } + if filters is None: + filters = {} + if not isinstance(filters, dict): + return {"success": False, "message": "filters must be an object", **metadata} + try: + inspect.signature(listing).bind(**filters) + except TypeError as exc: + return {"success": False, "message": f"Invalid {kind} filters: {exc}", **metadata} + return {**listing(**filters), **metadata} + @classmethod def _rollback_equipment_type(cls, equipment_folder, created_type, created_folder, type_name, type_class, folder_name): @@ -1191,7 +1260,7 @@ def sampled(interval): } @classmethod - def list_evemtbench_components( + def list_evemtbench_transformer_profiles( cls, voltage_class: str = "", rated_power_mva: Optional[float] = None, @@ -1382,14 +1451,7 @@ def create_evemtbench_component( parameters: dict[str, Any], ) -> dict[str, Any]: """Route one component request to its validated EvEMTBench creator.""" - creators = { - "transformer": "create_evemtbench_transformer", - "overhead_line": "create_evemtbench_line", - "underground_cable": "create_evemtbench_cable", - "external_grid": "create_evemtbench_external_grid", - "load": "create_evemtbench_load", - "synchronous_machine": "create_evemtbench_synchronous_machine", - } + creators = cls._evemtbench_creators kind = str(component_type or "").strip().lower().replace("-", "_") method_name = creators.get(kind) supported = ", ".join(creators) diff --git a/PowerFactory/evidence/FT-27_unified_evemtbench_catalog_mcp.txt b/PowerFactory/evidence/FT-27_unified_evemtbench_catalog_mcp.txt new file mode 100644 index 0000000..032433f --- /dev/null +++ b/PowerFactory/evidence/FT-27_unified_evemtbench_catalog_mcp.txt @@ -0,0 +1,158 @@ +Evidence ID: FT-27 +Title: Unified EvEMTBench MCP exposure and filtered catalog listing +Date: 15 October 2026 +Classification: Compiled evidence record +Source: Contributor-supplied VS Code MCP client output from the development PC +Revision: PR #122 commit 5993fc8 plus patch 0107, before commit + +Scope and provenance: +The contributor restarted the powerFactoryUnified MCP server using the +PowerMCP-tests-layout checkout and supplied the client's tool-exposure report +and catalog response. No independent tool-registry capture or byte-for-byte +running-source verification was supplied. +This read-only catalog call does not open PowerFactory or require a project. + +The client reported only these exposed EvEMTBench tools: +- list_evemtbench_components +- create_evemtbench_component +No specialized EvEMTBench create/list tools were reported visible. + +Call: +list_evemtbench_components( + component_type="overhead_line", + filters={"voltage_kv": 110, "profile": "p50"} +) + +Selected response fields (extract from supplied JSON): +success: true +component_type: overhead_line +catalog_id: evemtbench-overhead-lines-tables-3-7-9-v1 +source.tables: [3, 7, 9] +affiliation: Institute of Electrical Energy Systems, Friedrich-Alexander-Universität Erlangen-Nürnberg (FAU) +returned_count: 1 +results[0].voltage_class: "110" +results[0].nominal_voltage_kv: 110.0 +results[0].length_km: {"min": 1.0, "max": 32.0} +results[0].electrical_profiles: +{ + "p50": { + "r1_ohm_per_km": 0.06, + "x1_ohm_per_km": 0.29, + "c1_nf_per_km": 12.58, + "r0_ohm_per_km": 0.31, + "x0_ohm_per_km": 1.07, + "c0_nf_per_km": 5.59 + } +} +powerfactory.source_capacitance_unit: nF/km +powerfactory.powerfactory_capacitance_unit: uF/km +powerfactory.capacitance_conversion_factor: 0.001 +catalog_only.geometry.reason: +Tower and conductor geometry is retained in the human-readable catalog but +is not written by the TypLne sequence-equivalent creator. + +Filter discovery: +voltage_kv: number, optional, nullable, default null +profile: string, optional, default "" + +Creation-parameter discovery: +voltage_kv, nominal_frequency_hz, rated_current_ka, length_km: + number, required +profile, line_name, type_name, bus1_name, bus2_name: + string, required +grid_name: string, optional, default "" +out_of_service: boolean, optional, default false +confirmation: string, optional, default "" +open_digsilent: boolean, optional, default true + +Related automated validation: +Development-PC output: 166 tests and 85 subtests passed in 6.90 seconds. +The focused tests cover all six unified catalog routes, filter routing, +parameter discovery, invalid filters, and the two-tool MCP registration surface. +Local wheel build and six installed unified catalog listings also passed. + +Incidental client behavior: +The client first attempted PowerMCP-PSCAD-list_documentation, which failed. +This is outside the requested PowerFactory test and is not validation evidence +for the unified tools. + +Conclusion and limits: +The first supplied response verifies filtered overhead-line listing and +creation-input discovery through the refreshed MCP interface. The additional +MCP-client checks below extend coverage to all six catalog types. No +PowerFactory objects were created or modified. Confirmed creation was not +retested during this catalog validation. + +Additional MCP-client checks, 15 October 2026: +Revision reported by contributor: 6b14337, published in PR #122. +The following results are compiled from contributor-supplied client summaries. +The original large responses were read through the client's file-reading tool; +their temporary files were not supplied to the repository or independently +parsed during preparation of this record. These are not verbatim JSON captures. + +For each additional call the client reported success=true, matching +component_type, returned_count=1, and presence of catalog_id, source, +available_filters and creation_parameters. + +1. transformer + filters: {"voltage_class": "345/25", "rated_power_mva": 800} + catalog_id: evemtbench-transformers-tables-10-12-v1 + available_filters: voltage_class, rated_power_mva + required creation inputs: voltage_class, rated_power_mva, + nominal_frequency_hz, vector_group, clock_number, transformer_name, + type_name, high_voltage_bus_name, low_voltage_bus_name + Returned limitations: unmapped_source_tables=[10,12]; Table 12 saturation + mapping pending; Block catalog-only pending an explicit voltage rule; + other component families catalog-only pending validated mappings. + The client initially stopped on display truncation. A later read of the + saved response recovered the missing fields and verified the checks. + +2. underground_cable + filters: {"profile": "p50"} + catalog_id: evemtbench-underground-cables-tables-3-6-v1 + available_filters: profile + required creation inputs: nominal_frequency_hz, rated_current_ka, profile, + length_km, cable_name, type_name, bus1_name, bus2_name + Returned limitations: geometric ULM model remains catalog-only; cable + ampacity is not provided and requires a validated engineering rating. + +3. external_grid + filters: {"voltage_kv": 110} + catalog_id: evemtbench-external-grids-table-4-v1 + available_filters: voltage_kv + required creation inputs: voltage_kv, external_grid_name, bus_name + No catalog-only or unsupported mapping limitation was reported returned. + This does not establish unrestricted engineering applicability. + +4. load + filters: {"voltage_kv": 20} + catalog_id: evemtbench-loads-table-5-v1 + available_filters: voltage_kv + required creation inputs: voltage_kv, load_name, bus_name + Returned limitation: load-model coefficients are stored but not written + pending live validation of PowerFactory ZIP attributes. + +5. synchronous_machine + filters: {"machine_type": "round_rotor"} + catalog_id: evemtbench-synchronous-machines-table-13-v1 + available_filters: machine_type + required creation inputs: machine_type, rated_power_mva, machine_name, + type_name, bus_name + Returned status text: corrected open-circuit time-input mapping awaits a + creation retest; salient-pole creation disabled because xq transient data + is omitted by the source. + The creation-retest wording predates FT-23, which already records live + round-rotor creation and time-constant validation. This record preserves + the client's reported catalog text without treating it as current evidence + that the earlier creation validation is missing. + +The client reported reading saved transformer, cable and machine responses +in sections. Other response fields and unviewed portions are not asserted here. +The initial transformer sequence also attempted an unrelated PSCAD documentation +call, which failed; it is excluded from PowerFactory validation. + +Final scope: +All six types now have contributor-reported MCP-client catalog coverage in +addition to automated and installed-wheel coverage. This validates catalog +routing and parameter discovery, not physical model suitability or a fresh +creation/calculation run. No new CI result is claimed for this evidence update. diff --git a/PowerFactory/evidence/README.md b/PowerFactory/evidence/README.md index cb326d9..35e46a0 100644 --- a/PowerFactory/evidence/README.md +++ b/PowerFactory/evidence/README.md @@ -2,6 +2,7 @@ Latest follow-up records: +- [FT-27](FT-27_unified_evemtbench_catalog_mcp.txt): unified MCP tool exposure and filtered overhead-line listing with creation-parameter discovery. - [FT-25](FT-25_bootstrap_confirmation_binding_sp1.txt): bootstrap stale-token rejection after the create/reuse plan changed. - [FT-26](FT-26_pdf_diagram_restoration_sp1.txt): restoration of the previously active single-line diagram after successful PDF export. diff --git a/PowerFactory/functions_overview.txt b/PowerFactory/functions_overview.txt index 58ba687..f68dc74 100644 --- a/PowerFactory/functions_overview.txt +++ b/PowerFactory/functions_overview.txt @@ -17,18 +17,7 @@ Implemented MCP tools - get_config: returns the active configuration file. - export_active_grid_pdf: exports one active-project single-line diagram to a new PDF file. - create_evemtbench_component: routes a component type and parameter object to any validated EvEMTBench creator while preserving preview, confirmation, verification, and rollback behavior. -- list_evemtbench_components: validates and filters the versioned EvEMTBench transformer database without opening PowerFactory. -- create_evemtbench_transformer: previews or creates a new database-backed TypTr2 and connected ElmTr2 without an existing template. -- list_evemtbench_line_profiles: validates and filters the versioned 20/110/345 kV overhead-line sequence profiles without opening PowerFactory. -- create_evemtbench_line: previews or creates a new profile-backed TypLne and connected ElmLne without an existing template. -- list_evemtbench_cable_profiles: validates and filters the versioned 20 kV underground-cable sequence profiles without opening PowerFactory. -- create_evemtbench_cable: previews or creates a Table 6 cable-equivalent TypLne and connected ElmLne without an existing template. -- list_evemtbench_external_grid_profiles: validates and filters the versioned Table 4 external-grid ranges without opening PowerFactory. -- create_evemtbench_external_grid: previews or creates a seeded Table 4 ElmXnet on an existing 110, 345, or 380 kV bus. -- list_evemtbench_load_profiles: validates and filters the versioned Table 5 load-power ranges without opening PowerFactory. -- create_evemtbench_load: previews or creates a seeded Table 5 ElmLod on an existing 20, 110, or 345 kV bus. -- list_evemtbench_synchronous_machine_profiles: validates and filters the versioned Table 13 salient-pole and round-rotor ranges without opening PowerFactory. -- create_evemtbench_synchronous_machine: previews or creates a seeded round-rotor TypSym and connected ElmSym without an existing template. +- list_evemtbench_components: lists any of the six equipment catalogs with component-specific filters and creation-parameter discovery, without opening PowerFactory. - create_grid_with_buses: previews or bootstraps and activates an ElmNet with one to 100 initial ElmTerm objects in the active project and study case. - import_project: imports a .pfd project and activates it. - create_study_case: creates or activates a study case with optional idempotency support. @@ -70,7 +59,8 @@ Implemented simulation engine behavior - DIgSILENTAgent.export_results_to_csv: exports the RMS results to CSV via ComRes. - DIgSILENTAgent.export_active_grid_pdf: exports a selected Graphics Board single-line diagram to PDF via ComWr. - DIgSILENTAgent.create_evemtbench_component: validates and routes unified component requests to the existing specialized creator. -- DIgSILENTAgent.list_evemtbench_components: validates and filters the transformer catalog. +- DIgSILENTAgent.list_evemtbench_components: routes unified catalog requests and returns filter/creation parameter metadata. +- DIgSILENTAgent.list_evemtbench_transformer_profiles: validates and filters the transformer catalog internally. - DIgSILENTAgent.create_evemtbench_transformer: samples, previews, creates, connects, verifies, and rolls back a transformer type and element. - DIgSILENTAgent.list_evemtbench_line_profiles: validates and filters the overhead-line profile catalog. - DIgSILENTAgent.create_evemtbench_line: previews, creates, connects, verifies, and rolls back an overhead-line type and element. diff --git a/PowerFactory/references/tool_guide.md b/PowerFactory/references/tool_guide.md new file mode 100644 index 0000000..d2596d9 --- /dev/null +++ b/PowerFactory/references/tool_guide.md @@ -0,0 +1,752 @@ +# PowerFactory tool and workflow guide + +[Back to the README](../README.md) + +Detailed tool inventories, equipment examples, configuration, and validation notes. + +## Implemented Functions + +### MCP Tools (`MCP_PowerFactory.py`) + +| Tool | Description | +|------|-------------| +| `ping` | Health check — returns `"pong"`. | +| `close_digsilent` | Closes the PowerFactory session. | +| `get_config` | Returns the active `simulation_config.json` as a JSON string. | +| `get_active_project` | Returns the active PowerFactory project without modifying it. | +| `get_active_study_case` | Returns the active PowerFactory study case without modifying it. | +| `get_parameters` | Reads selected attributes from matching calculation-relevant objects, with opt-in metadata and active-project library inspection. | +| `list_objects` | Lists calculation-relevant objects using a raw PowerFactory query. | +| `list_components` | Lists objects using friendly equipment categories. | +| `list_study_cases` | Lists study cases and identifies the active case. | +| `list_contingencies` | Lists configured fault-library cases and their `EvtOutage`, `EvtSwitch`, and `EvtShc` events without modifying the project. | +| `create_contingency` | Creates or reuses an idempotent switch-based contingency definition. | +| `get_contingency_configuration` | Reads the active Contingency Analysis command settings without running a calculation. | +| `configure_contingency_screening` | Persists selected DC/AC-linearised screening criteria without running a calculation. | +| `add_contingency_result_variables` | Adds variables to the configured AC or DC contingency result recording selection without running a calculation. | +| `remove_contingency_result_variables` | Removes variables from matching AC or DC result recording selections without running a calculation. | +| `export_active_grid_pdf` | Exports one active-project single-line diagram to a new PDF file. | +| `create_evemtbench_component` | Routes one nested parameter object to any validated EvEMTBench component creator. | +| `list_evemtbench_components` | Lists any of six EvEMTBench catalogs, filters and required creation parameters without opening PowerFactory. | +| `create_grid_with_buses` | Previews or bootstraps an `ElmNet` and initial `ElmTerm` objects in the active project. | +| `import_project` | Imports a `.pfd` project file and activates it. | +| `create_study_case` | Creates (or activates) a study case by name, copying from a base case when needed. Supports `request_id` for idempotency. | +| `modify_parameter` | Sets one attribute on all PowerFactory objects matching a query string. Auto-casts string values to the correct type. | +| `add_component` | Creates a bus, load, generator, line, or two-winding transformer with validation, rollback, circuit breakers, and optional diagram insertion. | +| `delete_component` | Previews or confirms exact-name component deletion, including generated cubicles, circuit breakers, and optional diagram cleanup. | +| `run_loadflow` | Runs a standalone load flow calculation (ComLdf). | +| `run_short_circuit` | Runs a standalone short-circuit calculation (ComShc). | +| `run_contingency_analysis` | Runs ComSimoutage using its configured method or an explicit AC, DC, AC-linearised, or linearised-screening mode. | +| `get_contingency_results` | Reads a bounded, optionally variable-filtered slice of the configured AC or DC contingency result file using bulk or sparse access. | +| `get_contingency_summary` | Interprets existing contingency results as affected elements, convergence, overloads, and voltage violations. | +| `run_simulation` | Executes the full pipeline defined in `simulation_config.json`. | +| `run_custom_case` | Runs one fault simulation with parameters supplied at call time (no config file edit required). | +| `read_results_csv` | Reads an RMS result CSV; auto-discovers the latest file if no path is given. | + +The contingency tools can create switch-based definitions and inspect or run +the active `ComSimoutage` command. An explicit calculation mode applies only to +that run; the study case's previous mode is restored afterward. + +Read-only contingency tools search the active study case for an existing +`ComSimoutage`; they do not call `GetFromStudyCase`, which can create a missing +command. `get_contingency_results` accepts exact variable identifiers such as +`["c:loading"]`, returns each column's original index and object name/class when +PowerFactory provides one, and automatically includes `b:i_obj` for row-to-case +mapping. It uses `ElmRes.GetColumnValues` with a temporary, deleted `IntVec`, +falling back to PowerFactory's sparse iterator. Filtered reads resolve columns +from `IntMon` selections and `FindColumn`, avoiding a full `ElmRes` metadata +scan. `max_read_seconds` supplies a cooperative read budget; narrow `variables`, +`max_rows`, or `max_columns` for large files. + +`configure_contingency_screening` intentionally persists selected screening +settings. It can choose DC or AC-linearised screening; enable the simple and +combined loading criteria; set their loading and relative-change thresholds; +ignore components already overloaded in the base case; and restrict screening +to recorded elements. It returns the previous and resulting settings without +executing the analysis. + +Result recording is read from `IntMon` selections, never from stale result +columns. Both recording tools report each unreadable monitor once and keep +the record of successful changes to other objects. Class selections identified +by `IntMon.className` count as recorded for objects of that exact class. +Object-specific removal of a class-recorded variable is reported as failed, +without changing either its class or object selections. Other variables can +still be removed. Unidentified targetless selections remain errors; additions +and absence claims are skipped when their state is unknown. + +Removal reports `failed_variables` separately from `already_absent_variables`. +A variable can appear in both `removed` and `failed` when different monitors +produce a partial change; rerun the analysis whenever `removed_variables` is +non-zero. `configured_objects` counts objects with readable matching selections, +and `objects_without_selection` counts objects with no known selection. +The tool schemas require strings; blank or whitespace-only variables are also +rejected. Missing screening flags are returned as `null`, rather than disabled. + +PowerFactory 2026's User Manual, section 19.3.1.1, explicitly supports class-name +variable selections for contingency analysis. These tools report them rather +than attempting to remove a class-wide variable for an individual object. +Its Python Reference, section 5.6.26, documents `IntMon.RemoveVar` return codes: +`0` means removed, `1` means not found; other codes or exceptions are failures. + +### Simulation Engine (`Agent_DIgSILENT.py`) + +| Class / Method | Description | +|----------------|-------------| +| `SimulationConfig` | Dataclass holding all simulation parameters. `from_json()` loads from a JSON file. | +| `Logger` | Timestamped console logging with `info`, `ok`, `warn`, `error`, `section`. | +| `DIgSILENTAgent.__init__` | Initialises the agent from a `SimulationConfig`. | +| `DIgSILENTAgent.connect` | Connects to PowerFactory and activates the configured project. | +| `DIgSILENTAgent.activate_study_case` | Activates or creates the target study case. | +| `DIgSILENTAgent.run_loadflow` | Executes ComLdf. | +| `DIgSILENTAgent.run_contingency_analysis` | Executes ComSimoutage without changing its contingency filters and optionally selects a validated calculation mode. | +| `DIgSILENTAgent.run_rms_simulation` | Applies fault events, then runs ComInc + ComSim. | +| `DIgSILENTAgent._apply_fault_event` | Builds the event sequence for bus faults, line faults, or generator switches. | +| `DIgSILENTAgent.addSwitchEvent` | Creates a PowerFactory `EvtSwitch` event. | +| `DIgSILENTAgent.export_results_to_csv` | Exports the RMS result object to CSV via ComRes. | +| `DIgSILENTAgent.export_active_grid_pdf` | Exports a selected Graphics Board single-line diagram to PDF via ComWr. | +| `DIgSILENTAgent.create_evemtbench_component` | Validates and routes a unified request to an existing specialized component creator. | +| `DIgSILENTAgent.list_evemtbench_components` | Routes catalog requests for all six types and returns parameter metadata. | +| `DIgSILENTAgent.list_evemtbench_transformer_profiles` | Internal transformer catalog implementation. | +| `DIgSILENTAgent.create_evemtbench_transformer` | Samples, previews, creates, connects, verifies, and rolls back a new transformer type and element. | +| `DIgSILENTAgent.list_evemtbench_line_profiles` | Validates and filters the versioned overhead-line sequence profiles. | +| `DIgSILENTAgent.create_evemtbench_line` | Previews, creates, connects, verifies, and rolls back a new overhead-line type and element. | +| `DIgSILENTAgent.list_evemtbench_cable_profiles` | Validates and filters the versioned underground-cable sequence profiles. | +| `DIgSILENTAgent.create_evemtbench_cable` | Previews, creates, connects, verifies, and rolls back a new cable type and element. | +| `DIgSILENTAgent.list_evemtbench_external_grid_profiles` | Validates and filters the external-grid catalog. | +| `DIgSILENTAgent.create_evemtbench_external_grid` | Samples, previews, creates, connects, verifies, and rolls back a new external grid. | +| `DIgSILENTAgent.create_grid_with_buses` | Previews, creates or reuses, verifies, and rolls back a grid and its initial buses. | +| `DIgSILENTAgent.export_project_to_pfd` | Exports the active project to `.pfd` via ComPfdexport. | +| `DIgSILENTAgent.generate_standard_plots` | Reads the exported CSV and produces voltage and generator speed PNG plots. | +| `DIgSILENTAgent.import_project` | Imports a `.pfd` file into the running PowerFactory session. | +| `DIgSILENTAgent.create_study_case` | Creates or reuses a study case by exact name. | +| `DIgSILENTAgent.modify_parameter` | Sets an attribute on all objects returned by `GetCalcRelevantObjects`. | +| `DIgSILENTAgent.add_component` | Creates and verifies supported network components and their connections. | +| `DIgSILENTAgent.delete_component` | Performs guarded exact-name component deletion and cleanup. | +| `DIgSILENTAgent.short_circuit` | Standalone ComShc execution. | +| `DIgSILENTAgent.run_pipeline` | Orchestrates the full workflow and returns a structured status report. | +| `DIgSILENTAgent.close` | Shuts down PowerFactory and clears shared handles. | + +## Greenfield Grid Bootstrap + +`create_grid_with_buses` removes the requirement for a pre-existing +calculation grid and buses. It requires an active PowerFactory project and +study case. It can create the standard `Network Model` / `Network Data` folder +hierarchy, a new `ElmNet`, and one to 100 `ElmTerm` objects without using a +template. A confirmed call activates the grid and verifies that PowerFactory +reports it as calculation-relevant. + +```text +create_grid_with_buses( + grid_name="Research Grid", + nominal_frequency_hz=50, + buses=[ + {"name": "HV Bus", "nominal_voltage_kv": 110}, + { + "name": "MV Bus", + "nominal_voltage_kv": 20, + "out_of_service": true + } + ], + confirmation="" +) +``` + +The first call is a read-only preview. Repeat it with the exact returned +`confirmation_required` value to apply the plan. An existing grid or bus is +reused only when its frequency, voltage, and service state match exactly; +conflicting objects are reported without modification. If creation fails, all +objects created by that call are removed. The tool does not create a project, +study case, single-line diagram, equipment type, or calculation command. + +The bootstrap currently accepts 50 or 60 Hz so its grid is compatible with the +EvEMTBench transformer and overhead-line creators. Once the buses exist, pass +their names and the grid name directly to those tools. + +## Component Management + +### Create a component + +```text +add_component( + component_type, + component_name, + parameters, + grid_name="", + out_of_service=false, + open_digsilent=true, + update_graphics=false +) +``` + +Supported component parameters: + +| Type | Required parameters | Optional parameters | +|---|---|---| +| `bus` | `nominal_voltage_kv` | — | +| `load` | `bus_name`, `active_power_mw` | `reactive_power_mvar` | +| `generator` | `bus_name`, `template_generator`, `active_power_mw` | `reactive_power_mvar` | +| `line` | `bus1_name`, `bus2_name`, `template_line`, `length_km` | — | +| `transformer` | `high_voltage_bus_name`, `low_voltage_bus_name`, `template_transformer` | — | + +Component names are limited to 40 characters, matching PowerFactory's +`loc_name` limit. + +An explicit `null` for optional reactive power is treated as an omitted value +and defaults to zero. Boolean values are not accepted for numeric parameters. + +Each generated connection cubicle contains one closed circuit breaker +(`StaSwitch`, `aUsage="cbk"`, `on_off=1`). Set `update_graphics=true` to +request insertion into the active single-line diagram. + +Graphical updates use the Diagram Layout Tool's automatic insertion mode so +existing diagram objects are not re-laid out as a K-neighbourhood. + +If graphical insertion fails, the network component remains created and the +tool returns `success=false` with the graphical error. Check the returned +message before retrying to avoid creating a duplicate. + +PowerFactory may place an isolated bus far from the existing network when it +is inserted directly into the diagram. For a better anchored result, create +the bus with `update_graphics=false`, then create its first connecting line +with `update_graphics=true`. + +### Delete a component + +```text +delete_component( + component_type, + component_name, + grid_name="", + confirmation="", + open_digsilent=true, + update_graphics=false +) +``` + +Calling `delete_component` without confirmation returns a preview and the +required confirmation phrase: + +```text +DELETE +``` + +Name lookup is case-insensitive. The confirmation phrase contains the full +PowerFactory object path, binds the confirmation to the previewed grid, and +must be copied exactly from the preview. + +Confirmed deletion removes the exact component. A connection cubicle is removed +only when its generated name and sole generated circuit breaker both match; +cubicles containing relays, instrument transformers, or other objects are +preserved. A bus cannot be deleted while it still has connected cubicles. Set +`update_graphics=true` to remove matching diagram objects from every single-line +diagram in the active project and rebuild the active view. + +The response reports component deletion separately from cleanup. In particular, +`success=false` with `deleted=true` means the network component was removed but +some graphical or cubicle cleanup failed; do not retry the component deletion. + +--- + +## Unified EvEMTBench Component Creator + +`create_evemtbench_component` is the common write entry point for the six +validated component families: `transformer`, `overhead_line`, +`underground_cable`, `external_grid`, `load`, and `synchronous_machine`. +Use the creation inputs returned by `list_evemtbench_components` inside +`parameters`: + +```python +create_evemtbench_component( + component_type="load", + parameters={ + "voltage_kv": 20, + "load_name": "Research Load", + "bus_name": "MV Bus", + "grid_name": "Research Grid", + "seed": 42, + "out_of_service": True, + "confirmation": "", + "open_digsilent": True, + }, +) +``` + +An empty `confirmation` keeps the request in preview mode. Repeat the same +request with the exact returned `confirmation_required` value to apply it. +The unified tool delegates validation, seeded sampling, creation, retained-value +checks, and rollback to the existing specialized creator without changing its +behavior. Only the unified create and list tools are exposed through MCP. +Specialized methods remain internal Python implementation details. + +### Catalog discovery and migration + +Call `list_evemtbench_components(component_type, filters)` before creating equipment. +Omit `filters` to inspect the complete selected catalog. The response includes +`available_filters` and `creation_parameters`, with names, types, required flags, +and defaults. Catalog-only data and mapping limits remain in the response. + +| component_type | Optional filters | +|---|---| +| `transformer` | `voltage_class`, `rated_power_mva` (requires voltage_class) | +| `overhead_line` | `voltage_kv`, `profile` | +| `underground_cable` | `profile` | +| `external_grid` | `voltage_kv` | +| `load` | `voltage_kv` | +| `synchronous_machine` | `machine_type` | + +```python +list_evemtbench_components( + component_type="overhead_line", + filters={"voltage_kv": 110, "profile": "p50"}, +) +``` + +This intentionally changes the MCP interface. Replace previous specialized +create calls with `create_evemtbench_component` and place their arguments inside +`parameters`. Replace specialized list calls with `list_evemtbench_components` +and place their filters inside `filters`. The old transformer-only listing now +requires `component_type="transformer"`; move voltage_class/rated_power_mva into +`filters`. Restart the server and refresh the client's tool list after updating. +The examples below use the new public interface. + +--- + +## EvEMTBench Transformer Database and Creator + +The versioned database at +[`data/evemtbench_transformers.json`](../data/evemtbench_transformers.json) +stores the fixed-voltage transformer classes from EvEMTBench Tables 10-12. +Use `list_evemtbench_components` to inspect classes, ratings, ranges, vector +groups, zero-sequence rules, and saturation data without opening PowerFactory. + +`create_evemtbench_component(component_type="transformer", parameters=...)` creates a new `TypTr2` directly; it does not +copy or require an existing transformer. It also creates an `ElmTr2`, two +cubicles, and closed circuit breakers between existing buses. The type is +stored in the project library's `Transformer Types` folder; a confirmed call +creates that folder when it is absent from a greenfield project. The first +call is a read-only preview when `confirmation` is empty: + +```text +create_evemtbench_component( + component_type="transformer", + parameters={ + "voltage_class": "110/20", + "rated_power_mva": 20, + "nominal_frequency_hz": 60, + "vector_group": "YNd", + "clock_number": 11, + "transformer_name": "Research Transformer", + "type_name": "Research Transformer Type", + "high_voltage_bus_name": "HV Bus", + "low_voltage_bus_name": "LV Bus", + "seed": 42, + "confirmation": "", + "open_digsilent": True, + }, +) +``` + +The preview validates the grid, buses, nominal voltages, names, rating, vector +group, clock number, and an explicit nominal frequency of 50 or 60 Hz, then +returns the exact confirmation phrase. Frequency is not inferred from the grid +because existing projects can contain types with a different `frnom`. A +confirmed call writes three-phase technology (`nt2ph`), frequency (`frnom`), +the impedance modes, `strn`, `utrn_h`, `utrn_l`, `uktr`, `pcutr`, `pfe`, +`curmg`, `tr2cn_h`, `tr2cn_l`, and `nt2ag`. Structural settings and nameplate +ratings are committed before the performance values so PowerFactory can +refresh dependent type fields. Copper loss and short-circuit voltage are +converted to the equivalent `r1pu` and `x1pu` values before assignment, then +verified again through PowerFactory's derived `pcutr` and `uktr` attributes. +The performance values use PowerFactory's native Python property assignment; +each value is checked immediately and again after the database write. If element +creation fails, the new type, element, cubicles, and switches are rolled back. + +Table 10 zero-sequence and Table 12 saturation data are stored and returned by +the catalog tool, but are not written until their PowerFactory attribute +mappings are validated. The `Block` class and other component families remain +catalog-only for the same reason. The caller must explicitly choose a clock +number because Table 11 specifies vector-group families rather than one clock +number for every generated transformer. + +See [references/evemtbench.md](evemtbench.md) for the source mapping +and limitations. + +--- + +## EvEMTBench Overhead-Line Database and Creator + +[`data/evemtbench_overhead_lines.json`](../data/evemtbench_overhead_lines.json) +stores the published 5th, 50th and 95th percentile sequence parameters from +Table 7 for 20, 110 and 345 kV overhead lines, together with the corresponding +Table 3 length ranges. `list_evemtbench_components(component_type="overhead_line")` inspects this catalog +without opening PowerFactory. + +`create_evemtbench_component(component_type="overhead_line", parameters=...)` creates a new `TypLne` directly and connects a new +`ElmLne` between two existing equal-voltage buses. The caller selects `p05`, +`p50` or `p95`, supplies the actual current rating, and chooses 50 or 60 Hz. +The first call is a read-only preview: + +```text +create_evemtbench_component( + component_type="overhead_line", + parameters={ + "voltage_kv": 110, + "nominal_frequency_hz": 50, + "rated_current_ka": 1, + "profile": "p50", + "length_km": 10, + "line_name": "Research Line", + "type_name": "Research Line Type", + "bus1_name": "Bus A", + "bus2_name": "Bus B", + "confirmation": "", + "open_digsilent": True, + }, +) +``` + +The preview checks the voltage class, frequency, paper length range, existing +buses, names, and exact confirmation phrase. A confirmed call creates the +project library's `Line Types` folder when needed, writes `uline`, `sline`, +`frnom`, `rline`, `xline`, `cline`, `rline0`, `xline0` and `cline0`, and then +creates the line, two cubicles and closed circuit breakers. Published +capacitances are converted from nF/km to PowerFactory's uF/km attributes. New +objects are rolled back when creation fails. + +This tool deliberately implements a three-phase `TypLne` sequence equivalent, +not the paper's frequency-dependent geometric ULM model. The three published +percentiles are discrete profiles rather than endpoints of an invented +sampling distribution. Tower and conductor geometry remains catalog-only. + +--- + +## EvEMTBench Underground-Cable Database and Creator + +[`data/evemtbench_underground_cables.json`](../data/evemtbench_underground_cables.json) +stores Table 6's published 5th, 50th and 95th percentile sequence parameters +for 20 kV underground cables and Table 3's 0.5 to 3.75 km length range. +`list_evemtbench_components(component_type="underground_cable")` returns those profiles plus the paper's +construction and installation data without opening PowerFactory. + +`create_evemtbench_component(component_type="underground_cable", parameters=...)` creates a new `TypLne` sequence equivalent and +connects a new `ElmLne` between two existing 20 kV buses. The caller chooses a +published percentile, 50 or 60 Hz, and an engineering current rating because +Table 6 does not publish cable ampacity. The first call is a read-only preview: + +```text +create_evemtbench_component( + component_type="underground_cable", + parameters={ + "nominal_frequency_hz": 50, + "rated_current_ka": 0.4, + "profile": "p50", + "length_km": 2, + "cable_name": "Research Cable", + "type_name": "Research Cable Type", + "bus1_name": "MV Bus A", + "bus2_name": "MV Bus B", + "grid_name": "Research Grid", + "out_of_service": True, + "confirmation": "", + "open_digsilent": True, + }, +) +``` + +Repeat the call with the exact returned `confirmation_required` value to +create the type, cable element, two cubicles, and closed circuit breakers. +Published capacitances are converted from nF/km to PowerFactory's uF/km +attributes, retained values are verified, and new objects are rolled back if +creation fails. + +The created `TypLne` is a three-phase sequence equivalent suitable for initial +network construction; it is not the paper's geometric ULM cable model. The +catalog preserves insulation, conductor, sheath, arrangement, burial-depth, +spacing, and earth-resistivity data for a future geometric implementation. + +--- + +## EvEMTBench External-Grid Database and Creator + +[`data/evemtbench_external_grids.json`](../data/evemtbench_external_grids.json) +stores the Table 4 ranges for 110 kV and combined 380/345 kV external-grid +equivalents. `list_evemtbench_components(component_type="external_grid")` inspects the ranges +without opening PowerFactory. + +`create_evemtbench_component(component_type="external_grid", parameters=...)` uses a supplied seed to sample every Table 4 +interval uniformly and connects a new slack `ElmXnet` to an existing bus. The +bus must be exactly 110, 345, or 380 kV. The first call is a read-only preview: + +```text +create_evemtbench_component( + component_type="external_grid", + parameters={ + "voltage_kv": 110, + "external_grid_name": "Research External Grid", + "bus_name": "HV Bus", + "grid_name": "Research Grid", + "seed": 42, + "out_of_service": True, + "confirmation": "", + "open_digsilent": True, + }, +) +``` + +Repeat the call with the exact returned `confirmation_required` value to create +the external grid, cubicle, and closed circuit breaker. The tool writes `snss`, +`rntxn`, `x0tx1`, `r0tx0`, `usetp`, and `phiini`, sets `bustp="SL"`, verifies +the retained values, and rolls back new objects if creation fails. The sampled +values are research inputs and must still be checked for suitability in the +target network. The attribute mapping and retained values were validated on +PowerFactory 2026 SP1; SP4 validation remains pending. + +--- + +## EvEMTBench Load Database and Creator + +[`data/evemtbench_loads.json`](../data/evemtbench_loads.json) stores the Table 5 +load-power ranges for 20, 110, and 345 kV systems. The read-only +`list_evemtbench_components(component_type="load")` tool returns these ranges without opening +PowerFactory. + +`create_evemtbench_component(component_type="load", parameters=...)` samples active and reactive power directly at 110 and +345 kV. At 20 kV it samples apparent power and a reactive-power angle between +the published 0.95 capacitive and 0.8 inductive limits, then calculates P and +Q. The first call is a read-only preview: + +```text +create_evemtbench_component( + component_type="load", + parameters={ + "voltage_kv": 20, + "load_name": "Research Load", + "bus_name": "MV Bus", + "grid_name": "Research Grid", + "seed": 42, + "out_of_service": True, + "confirmation": "", + "open_digsilent": True, + }, +) +``` + +Repeat the call with the exact returned `confirmation_required` value to create +the balanced `ElmLod`, cubicle, and closed circuit breaker. The tool verifies +`plini`, `qlini`, the bus connection, and the service state and rolls back new +objects if creation fails. The Table 5 ZIP coefficients remain catalog-only +until their PowerFactory attribute mapping is live-validated. + +--- + +## EvEMTBench Synchronous-Machine Database and Creator + +[`data/evemtbench_synchronous_machines.json`](../data/evemtbench_synchronous_machines.json) +stores the salient-pole and round-rotor ranges from Table 13, including the +rated-voltage selection rule and parameter dependencies. Use +`list_evemtbench_components(component_type="synchronous_machine")` to inspect both profiles or to +filter by `salient_pole` or `round_rotor`. + +`create_evemtbench_component(component_type="synchronous_machine", parameters=...)` supports the round-rotor profile. The +caller selects a rating from 25 to 1200 MVA and an existing bus. The tool +derives the rated voltage using Table 13 (6.3, 10.5, 21, or 27 kV), samples the +remaining ranges reproducibly from `seed`, and validates all published +reactance and saturation dependencies. The requested P/Q operating point must +fit the MVA rating and the paper's reactive-power limits. + +```python +create_evemtbench_component( + component_type="synchronous_machine", + parameters={ + "machine_type": "round_rotor", + "rated_power_mva": 100, + "machine_name": "Research Generator", + "type_name": "Research Generator Type", + "bus_name": "Generator Bus 10.5", + "grid_name": "Research Grid", + "seed": 42, + "active_power_mw": 60, + "reactive_power_mvar": 5, + "out_of_service": True, + "confirmation": "", + "open_digsilent": True, + }, +) +``` + +Repeat the call with the exact returned `confirmation_required` string to +create the `TypSym`, `ElmSym`, cubicle, and closed circuit breaker. The type is +created without a template, and all new objects are rolled back if creation +fails. Table 13's short-circuit time constants are converted to PowerFactory's +writable open-circuit inputs and checked again through the derived `tds`, +`tqs`, `tdss`, and `tqss` values. The attribute relationships, `iturbo=1` +round-rotor mode, confirmed creation, and structured response were validated +on PowerFactory 2026 SP1. Salient-pole creation remains disabled because Table +13 does not provide `x'q` or `T'q`; the profile continues to be available +through the read-only catalog. + +--- + +## Configuration + +Copy `simulation_config.example.json` to `simulation_config.json` and fill in your paths and parameters: + +```json +{ + "project_path": "\\\\\\.IntPrj", + "study_case": "Case 1", + "base_study_case": "0. Base", + "output_dir": "C:\\path\\to\\output", + "run_label": "Test_1", + "result_name": "All calculations.ElmRes", + "cases": [ + { + "case_name": "Fault_1", + "fault_type": "bus", + "fault_element": "Bus 25.ElmTerm", + "t_start": 0.0, "t_fault": 1.0, "t_clear": 1.08, "t_end": 10.0, "dt_rms": 0.01 + } + ] +} +``` + +Supported `fault_type` values: `"bus"`, `"line"`, `"gen_switch"`. + + +--- + +## Output Structure + +Each simulation run creates a dedicated subfolder: + +``` +output_dir/ +└── run_label/ + ├── run_label_RMS.csv + ├── run_label_voltages.png + ├── run_label_gen_speeds.png + └── run_label.pfd (optional) +``` + +--- + +## Prompt Examples + +Below are example prompts you can send to an AI assistant connected to this MCP server. + +**Health check** +``` +Ping the PowerFactory server and confirm it's reachable. +``` + +**Run a load flow** +``` +Run a load flow on the active study case and tell me if it converged. +``` + +**Run a transient fault simulation** +``` +Simulate a three-phase bus fault on Bus 25 starting at t=1.0 s, cleared at t=1.08 s, +with a total simulation window of 10 s and a time step of 0.01 s. Export the results +and generate the voltage and generator speed plots. +``` + +**Modify a parameter before running** +``` +Set the generator G 01.ElmSym out of service (e:outserv = 1), +then run a load flow and report the result. +``` + +**Inspect the active context** +``` +Return the active PowerFactory project and study case, then list the first ten +buses without modifying anything. +``` + +**Create and connect a bus** +``` +Create an in-service 110 kV bus named "MCP Example Bus" in Grid without a +graphical update. Connect Bus 01 to it with a 1 km line based on +"Line 01 - 02.ElmLne", request a graphical update for the line, and run a load +flow. Stop if any operation fails. +``` + +**Preview and delete a component** +``` +Preview deletion of the load "MCP Example Load" in Grid. Show me the required +confirmation phrase and do not perform the confirmed deletion yet. +``` + +**Create a new study case and run a custom fault** +``` +Create a study case called "Scenario_A" based on "0. Base", +then run a line fault on "Line 12-34.ElmLne" from t=0.5 s to t=0.58 s +in a 15 s window. Save the CSV and plots to C:\Results\Scenario_A. +``` + +**Short-circuit calculation** +``` +Run a short-circuit calculation on the active study case and return the results. +``` + +**Read the latest results** +``` +Read the most recent RMS results CSV and summarise the peak voltage deviations +and minimum generator speed recorded during the simulation. +``` + +**Import a project and run the full pipeline** +``` +Import the project at C:\Projects\IEEE39.pfd, activate it, +then run the full simulation pipeline defined in the config file. +``` + +**Export an active grid diagram to PDF** +``` +Export the single-line diagram named "Summary Grid" from the active +PowerFactory project to C:\Projects\exports\summary-grid.pdf. +Do not overwrite an existing file. +``` + +**Inspect and preview an EvEMTBench transformer** +``` +List the available 110/20 kV EvEMTBench transformer ratings. Then preview a +20 MVA YNd11 transformer type named "Research Transformer Type" and an element +named "Research Transformer" between "HV Bus" and "LV Bus", using seed 42. +Return the sampled values and confirmation phrase without creating anything. +``` + +--- + +## Automated tests and validation reports + +The EvEMTBench catalogs and builders are implemented in `evemtbench.py` and +inherited by `DIgSILENTAgent`; the public MCP tool names and arguments are unchanged. +Equipment-type rollback and confirmation hashing share helpers in that module. + +Grid-bootstrap confirmation is bound to the active project and study case, +the resolved grid, the destination folder for a new grid, and the +create/reuse/activation decisions. The preview returns the destination folder +and required folder-creation decisions in `destination_plan`. If that state +changes, request a fresh preview before confirming, including for repeated calls. + +PDF export restores a previously active single-line diagram in a `finally` +block after success or failure. If no current diagram is reported, there is +no diagram to restore. Restoration failure adds a warning to the result message: +a completed export still reports success, while a failed export retains its +original error. Successful single-line restoration was +verified live on SP1 in [FT-26](../evidence/FT-26_pdf_diagram_restoration_sp1.txt). +Failure-path restoration has automated coverage; active plot pages are not covered. +Bootstrap stale-token rejection after creation was verified live in +[FT-25](../evidence/FT-25_bootstrap_confirmation_binding_sp1.txt). + +Run the PowerFactory unit and catalog tests from the repository root: + +```powershell +python -m pytest PowerFactory/tests -q +``` + +The installed-wheel smoke test, `tests/check_evemtbench_wheel.py`, runs separately +in CI against an installed package outside the checkout. +Validation records and the contribution and function-test reports are in +[`evidence/`](../evidence/README.md). diff --git a/PowerFactory/tests/check_evemtbench_wheel.py b/PowerFactory/tests/check_evemtbench_wheel.py index 77bb0cc..ef5cae9 100644 --- a/PowerFactory/tests/check_evemtbench_wheel.py +++ b/PowerFactory/tests/check_evemtbench_wheel.py @@ -58,22 +58,20 @@ agent_module = importlib.util.module_from_spec(agent_spec) agent_spec.loader.exec_module(agent_module) agent = agent_module.DIgSILENTAgent -listing_calls = ( - ("transformers", agent.list_evemtbench_components), - ("overhead lines", agent.list_evemtbench_line_profiles), - ("underground cables", agent.list_evemtbench_cable_profiles), - ("external grids", agent.list_evemtbench_external_grid_profiles), - ("loads", agent.list_evemtbench_load_profiles), - ("synchronous machines", agent.list_evemtbench_synchronous_machine_profiles), +component_types = ( + "transformer", "overhead_line", "underground_cable", + "external_grid", "load", "synchronous_machine", ) -for label, listing in listing_calls: - result = listing() +for label in component_types: + result = agent.list_evemtbench_components(label) if not result.get("success"): raise SystemExit(f"installed-wheel {label} listing failed: {result.get('message')}") if result.get("returned_count", 0) < 1: raise SystemExit(f"installed-wheel {label} listing returned no catalog entries") + if not result.get("creation_parameters") or "available_filters" not in result: + raise SystemExit(f"installed-wheel {label} listing omitted parameter discovery") print( f"Wheel catalog smoke test passed: {len(CATALOGS)} JSON catalogs loaded " - f"and {len(listing_calls)} read-only listing APIs succeeded from {catalog_root}" + f"and {len(component_types)} unified catalog listings succeeded from {catalog_root}" ) diff --git a/PowerFactory/tests/test_evemtbench_component_factory.py b/PowerFactory/tests/test_evemtbench_component_factory.py index 7e9ac9b..366cf5b 100644 --- a/PowerFactory/tests/test_evemtbench_component_factory.py +++ b/PowerFactory/tests/test_evemtbench_component_factory.py @@ -1,8 +1,75 @@ import unittest +from unittest.mock import patch import Agent_DIgSILENT as agent_module +class UnifiedEvEMTBenchListingTest(unittest.TestCase): + def test_lists_all_six_catalogs_without_powerfactory(self): + agent = agent_module.DIgSILENTAgent + with patch.object(agent, "_get_application", side_effect=AssertionError("must stay offline")): + for kind in agent._evemtbench_creators: + with self.subTest(component_type=kind): + result = agent.list_evemtbench_components(kind) + self.assertTrue(result["success"], result.get("message")) + self.assertEqual(result["component_type"], kind) + self.assertGreater(result["returned_count"], 0) + self.assertIn("source", result) + self.assertIn("catalog_id", result) + inputs = result["creation_parameters"] + self.assertEqual(inputs["confirmation"], + {"type": "string", "required": False, "default": ""}) + self.assertTrue(inputs["open_digsilent"]["default"]) + self.assertFalse(any(name.startswith("_") for name in inputs)) + transformer = agent.list_evemtbench_components("transformer") + self.assertEqual(transformer["creation_parameters"]["clock_number"], + {"type": "integer", "required": True}) + line = agent.list_evemtbench_components("overhead_line") + self.assertTrue(line["available_filters"]["voltage_kv"]["nullable"]) + self.assertFalse(line["available_filters"]["voltage_kv"]["required"]) + + def test_routes_filters_and_preserves_catalog_limits(self): + agent = agent_module.DIgSILENTAgent + cases = ( + ("transformer", {"voltage_class": "345/25", "rated_power_mva": 800}), + ("overhead_line", {"voltage_kv": 110, "profile": "p50"}), + ("underground_cable", {"profile": "p95"}), + ("external_grid", {"voltage_kv": 110}), + ("load", {"voltage_kv": 20}), + ("synchronous_machine", {"machine_type": "salient_pole"}), + ) + for kind, filters in cases: + with self.subTest(component_type=kind): + original = dict(filters) + result = agent.list_evemtbench_components(kind, filters) + self.assertTrue(result["success"], result.get("message")) + self.assertEqual(result["returned_count"], 1) + self.assertEqual(filters, original) + self.assertEqual(result["component_type"], kind) + transformer = agent.list_evemtbench_components(*cases[0]) + self.assertEqual(transformer["unmapped_source_tables"], [10, 12]) + self.assertEqual(transformer["results"][0]["ratings"][0]["rated_power_mva"], 800) + line = agent.list_evemtbench_components(*cases[1]) + self.assertEqual(set(line["results"][0]["electrical_profiles"]), {"p50"}) + machine = agent.list_evemtbench_components(*cases[-1]) + self.assertIsNone(machine["results"][0]["parameters"]["xq_transient_pu"]) + self.assertIn("salient-pole creation remains disabled", machine["powerfactory"]["status"]) + + def test_rejects_bad_types_filters_and_private_options(self): + agent = agent_module.DIgSILENTAgent + unknown = agent.list_evemtbench_components("unsupported") + self.assertFalse(unknown["success"]) + self.assertEqual(len(unknown["supported_component_types"]), 6) + for filters in ([], {"_database_loader": "override"}, {"voltage_class": "20"}, {"open_digsilent": True}): + with self.subTest(filters=filters): + result = agent.list_evemtbench_components("load", filters) + self.assertFalse(result["success"]) + self.assertIn("available_filters", result) + invalid = agent.list_evemtbench_components("overhead_line", {"profile": "made-up"}) + self.assertFalse(invalid["success"]) + self.assertIn("p50", invalid["message"]) + + class UnifiedEvEMTBenchCreatorTest(unittest.TestCase): class FakeAgent(agent_module.DIgSILENTAgent): calls = [] diff --git a/PowerFactory/tests/test_grid_bootstrap.py b/PowerFactory/tests/test_grid_bootstrap.py index 12d0ebb..3cc0863 100644 --- a/PowerFactory/tests/test_grid_bootstrap.py +++ b/PowerFactory/tests/test_grid_bootstrap.py @@ -149,6 +149,48 @@ def test_confirmation_rejects_changed_create_reuse_plan(self): self.assertIn("confirmation must exactly match", result["message"]) self.assertEqual(len(grid["ElmTerm"]), 1) + def test_confirmation_rejects_changed_destination_folder_plan(self): + for existing_model, add_data_folder in ((False, False), (False, True), (True, True)): + with self.subTest(existing_model=existing_model, add_data_folder=add_data_folder): + self.setUp() + model = None + if existing_model: + model = self.project.CreateObject("IntPrjfolder", "Network Model") + preview = self.call() + self.assertTrue(preview["success"], preview.get("message")) + if model is None: + model = self.project.CreateObject("IntPrjfolder", "Network Model") + if add_data_folder: + model.CreateObject("IntPrjfolder", "Network Data") + + rejected = self.call(confirmation=preview["confirmation_required"]) + self.assertFalse(rejected["success"]) + self.assertIn("confirmation must exactly match", rejected["message"]) + self.assertEqual(self.project.GetContents("*.ElmNet", 1), []) + fresh_preview = self.call() + applied = self.call(confirmation=fresh_preview["confirmation_required"]) + self.assertTrue(applied["success"], applied.get("message")) + self.assertEqual(len(model["IntPrjfolder"][0]["ElmNet"]), 1) + + def test_confirmation_rejects_relocated_destination_folder(self): + model = self.project.CreateObject("IntPrjfolder", "Network Model") + model.CreateObject("IntPrjfolder", "Network Data") + preview = self.call() + model.SetAttribute("loc_name", "Renamed Model") + rejected = self.call(confirmation=preview["confirmation_required"]) + self.assertFalse(rejected["success"]) + self.assertIn("confirmation must exactly match", rejected["message"]) + self.assertEqual(self.project.GetContents("*.ElmNet", 1), []) + + def test_ambiguous_destination_fails_during_preview_without_creating_objects(self): + for name in ("First", "Second"): + parent = self.project.CreateObject("IntPrjfolder", name) + parent.CreateObject("IntPrjfolder", "Network Data") + preview = self.call() + self.assertFalse(preview["success"]) + self.assertIn("Multiple Network Data folders", preview["message"]) + self.assertEqual(self.project.GetContents("*.ElmNet", 1), []) + def test_confirmation_creates_folder_grid_and_buses(self): preview = self.call() diff --git a/PowerFactory/tests/test_grid_pdf_export.py b/PowerFactory/tests/test_grid_pdf_export.py index 7858484..415f333 100644 --- a/PowerFactory/tests/test_grid_pdf_export.py +++ b/PowerFactory/tests/test_grid_pdf_export.py @@ -146,13 +146,26 @@ def test_restores_previous_diagram_after_success_and_failure(self): def test_reports_restoration_failure(self): selected = FakePage("Export Grid") - app = FakeApplication([selected]) - app.current_diagram = FakePage("Previously open diagram") - with tempfile.TemporaryDirectory(dir=os.path.dirname(__file__)) as directory: - with patch.object(app.board, "Show", side_effect=[0, 1]): - success, message = self.run_export(app, os.path.join(directory, "grid.pdf")) - self.assertFalse(success) - self.assertIn("restore previous diagram", message) + for export_fails in (False, True): + for restore_failure in (1, RuntimeError("restore raised")): + with self.subTest(export_fails=export_fails, restore=restore_failure): + app = FakeApplication([selected]) + app.current_diagram = FakePage("Previously open diagram") + with tempfile.TemporaryDirectory(dir=os.path.dirname(__file__)) as directory: + output = os.path.join(directory, "grid.pdf") + with patch.object(app.board, "Show", side_effect=[0, restore_failure]): + if export_fails: + with patch.object(app.command, "Execute", side_effect=RuntimeError("original export failure")): + success, message = self.run_export(app, output) + else: + success, message = self.run_export(app, output) + self.assertEqual(os.path.isfile(output), not export_fails) + self.assertEqual(success, not export_fails, message) + self.assertIn("Warning: could not restore previous diagram", message) + if export_fails: + self.assertTrue(message.startswith("original export failure"), message) + else: + self.assertTrue(message.startswith("Grid diagram 'Export Grid' exported"), message) def test_requires_name_when_multiple_diagrams_are_available(self): app = FakeApplication([FakePage("Overview"), FakePage("Summary Grid")]) diff --git a/PowerFactory/tests/test_state_inspection.py b/PowerFactory/tests/test_state_inspection.py index 7766b48..ca39abc 100644 --- a/PowerFactory/tests/test_state_inspection.py +++ b/PowerFactory/tests/test_state_inspection.py @@ -9,10 +9,13 @@ class FakeFastMCP: def __init__(self, *args, **kwargs): - pass + self.registered_tools = [] def tool(self): - return lambda function: function + def register(function): + self.registered_tools.append(function.__name__) + return function + return register class FakeAgent: @@ -1246,6 +1249,21 @@ def test_unified_evemtbench_tool_uses_structured_agent_result(self): structured=True, ) + def test_only_unified_evemtbench_tools_are_registered(self): + self.assertEqual( + {name for name in mcp_module.mcp.registered_tools if "evemtbench" in name}, + {"list_evemtbench_components", "create_evemtbench_component"}, + ) + + def test_unified_listing_passes_filters_without_opening_powerfactory(self): + filters = {"voltage_kv": 110} + with patch.object(FakeAgent, "list_evemtbench_components", + return_value={"success": True, "component_type": "load"}, + create=True) as listing: + result = json.loads(mcp_module.list_evemtbench_components("load", filters)) + self.assertTrue(result["success"]) + listing.assert_called_once_with("load", filters) + def test_get_parameters_serializes_object_lists(self): reference = FakeObject( "Bus 02", diff --git a/PowerFactory/tests/test_transformer_type_library.py b/PowerFactory/tests/test_transformer_type_library.py index 5710824..f8b5bc8 100644 --- a/PowerFactory/tests/test_transformer_type_library.py +++ b/PowerFactory/tests/test_transformer_type_library.py @@ -315,7 +315,7 @@ def call(self, app, **kwargs): def test_database_validates_and_lists_supported_classes(self): database = agent_module.DIgSILENTAgent._load_evemtbench_database() - result = agent_module.DIgSILENTAgent.list_evemtbench_components() + result = agent_module.DIgSILENTAgent.list_evemtbench_transformer_profiles() self.assertEqual(database["schema_version"], 1) self.assertTrue(result["success"], result.get("message")) @@ -328,7 +328,7 @@ def test_database_validates_and_lists_supported_classes(self): self.assertIn("Institute of Electrical Energy Systems", result["affiliation"]) def test_catalog_filters_an_exact_rating(self): - result = agent_module.DIgSILENTAgent.list_evemtbench_components( + result = agent_module.DIgSILENTAgent.list_evemtbench_transformer_profiles( "345/25", 800.0, )