Skip to content

Extend PowerFactory contingency analysis tools - #84

Merged
qian-harvard merged 3 commits into
Power-Agent:mainfrom
aswinkrishnapoyil:feat/powerfactory-contingency-modes
Sep 23, 2026
Merged

qian-harvard merged 3 commits into
Power-Agent:mainfrom
aswinkrishnapoyil:feat/powerfactory-contingency-modes

Conversation

@aswinkrishnapoyil

Copy link
Copy Markdown
Contributor

Summary

Extends the PowerFactory contingency workflow added in #78, on top of the fixes merged in #83.

Configuration and execution

  • Add get_contingency_configuration for read-only inspection of the active ComSimoutage settings.
  • Extend run_contingency_analysis with validated calculation modes:
    • ac
    • dc
    • ac_linearised
    • linearised_screening
  • Only write the supported PowerFactory iopt_Linear setting.

Contingency definitions

  • Add create_contingency for creating idempotent EvtSwitch-based fault cases.
  • Support target lookup, open/close actions, event time, event name, and fault-case folder selection.
  • Extend list_contingencies to report both EvtOutage and EvtSwitch events, including target metadata and enabled state.

Result inspection

  • Extend get_contingency_results with:
    • bounded AC/DC result retrieval;
    • result-column object metadata;
    • b:i_obj resolution to ComOutage objects;
    • unavailable cells represented as null with PowerFactory error codes.
  • Add read-only get_contingency_summary for:
    • contingency identity;
    • affected elements;
    • convergence status;
    • voltage violations;
    • overloads;
    • recorded voltage and loading extrema;
    • base-case results.

PowerFactory contingency result files are sparse, so interpreted measurements and extrema cover cells recorded in the existing ElmRes. The inspection tools do not execute calculations.

Result contract

All tools return the existing PowerFactory JSON result shape:

  • Successful calls include "success": true.
  • Failed calls include "success": false and a human-readable "message".
  • Calculation calls also return the execution code, command identity, and applied settings.
  • Read-only inspection calls never run a calculation.
  • Result reads are bounded to prevent accidentally returning an entire large ElmRes.

Validation

  • PowerFactory suite: 36 passed, 7 subtests passed.
  • Broader repository suite: 2166 passed, 25 skipped.
  • The two Git Bash-dependent GenX tests were rerun with bash.exe on PATH: 2 passed.
  • git diff --check passed.

Live PowerFactory validation

Validated against study case Case 1:

  • AC, DC, AC-linearised, and linearised-screening execution.
  • Configuration readback for every calculation mode.
  • Existing-event reuse and new contingency creation.
  • Switch and outage-event listing with resolved target objects.
  • AC/DC bounded result retrieval.
  • Result-row and result-column object resolution.
  • Interpreted summary for MCP N-1 Line Test.
  • Affected element resolved as Line 01 - 02.
  • Contingency converged successfully.
  • Expected Line 06 - 07 overload reported at approximately 81.06%.

@qian-harvard qian-harvard left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Substantial piece of work, and the parts I can check hold up: rebased on main, green on all four interpreters, 36 in the PowerFactory suite and 451 across the full CI set locally. The idempotency design in create_contingency is the right shape — match on name, verify the settings agree, report created: false rather than silently diverging — and the rollback deleting a case it created is the kind of thing that is easy to skip.

Up front: I have no PowerFactory here, so the vendor semantics (i_switch 0/1, iopt_Linear 0–3, b:i_obj, ComOutage resolution) rest on your live validation, not on anything I verified. Everything below is from the code and the fakes.

The one I would fix before merging: the mode selection is written into the user's project and never put back

Agent_DIgSILENT.py writes iopt_Linear through _set_and_verify_attributes and leaves it there. So the setting is not scoped to the call — it is a permanent edit to the study case:

user's configured iopt_Linear at start: 0   (AC)
after calculation_method='dc'         : 1   <- written to the project
then calculation_method='configured'  : 1   -> now reports DC

After one dc call, configured no longer means what the user configured. It means whatever this tool last wrote. That is the part that worries me: a model exploring modes (ac, then dc, then back to configured to "restore normal") ends up silently leaving the project on DC and believing it restored it. The damage is quiet and persists after the session.

The docstring does not mention it either — it says the tool "uses the active study case's existing contingency definitions, filters, and result selection", which reads as non-mutating, and #78 stated the same intent explicitly. Changing that intent is entirely reasonable for a follow-up, but it needs to be visible.

Cleanest fix is to restore the previous value once Execute returns, so the mode is per-call:

previous = attribute("iopt_Linear")
...
finally:
    if modes[mode] is not None and previous is not None:
        cls._set_and_verify_attributes(command, {"iopt_Linear": previous}, "Contingency Analysis")

If you would rather it persist, that is a defensible choice too — but then say so in the docstring, because configured is the wrong name for "whatever was last written".

Good news: the configured path itself does not write. I checked; SetAttribute is only called when a mode is named.

Worth fixing

affected_elements scans a magic 100 and hides truncation (MCP_PowerFactory.py):

for index in range(100):
    try:
        element = contingency.GetObject(index)
    except Exception:
        break

Two problems. A contingency affecting more than 100 elements is silently cut with nothing in the payload saying so — every other bound in these tools reports total_count / returned_count / truncated, and this one should match. And the bare except Exception is doing double duty as the loop terminator, so a genuine PowerFactory error mid-scan is indistinguishable from "ran off the end" and yields a short list that looks complete.

list_contingencies narrowed without saying so. The new parent check means only IntEvt sitting directly in an IntFltcases folder is listed:

3 IntEvt in the project (parents: IntFltcases / IntCase / IntPrjfolder)
  total_count: 1   names: ['InFolder']

I think this is the right call — contingency cases do live in IntFltcases, and it stops study-case event folders being reported as contingencies. But it is the reverse direction from #83, which we just merged specifically so configured-but-invisible cases became visible, and the docstring still just says "List configured fault cases and outage/switch events, including empty cases". Worth a clause saying which cases qualify, since that string is what the model reasons from.

The (status, value) decode is now in four places. Lines 1060, 1081, 1188, 1198. #83 added the explanatory comment to exactly one of them (1057), so the three copies this PR adds carry the assumption without the reasoning. A module-level _decode_cell(raw) returning (error_code, value) would collapse all four and give the comment one home.

Smaller

  • Rollback can lose the real error. In create_contingency, created_event.Delete() and created_case.Delete() run inside the except unguarded; if Delete itself raises, the caller gets that instead of the failure that triggered the rollback. Wrapping each in its own try would keep the original message.
  • No test for the paths that matter most on a write tool — the rollback, and the "event exists with different settings" refusal. The idempotency test covers create-then-reuse, which is the happy path of the same feature. Those two are where a bug would actually cost someone their project state.
  • Scope note. #78 said creating definitions and modifying ComSimoutage settings were deliberately out of scope; this PR does both. Fine as a follow-up — just worth a line in PowerFactory/README.md so the older statement is not left standing as the current contract.

Happy to look again once the mode-persistence question is settled either way; the rest is polish and none of it is load-bearing.

@aswinkrishnapoyil

Copy link
Copy Markdown
Contributor Author

Addressed the review feedback in 3427504:

  • Explicit calculation modes are now restored in finally, including after execution errors.
  • The result still reports the mode actually used for the run.
  • Affected elements now expose total, returned, and truncation metadata.
  • Removed silent exception handling while enumerating affected elements.
  • Centralized PowerFactory result-cell decoding.
  • Rollback failures no longer hide the original contingency-creation error.
  • Added rollback, mismatched-event, mode-restoration, and truncation coverage.
  • Clarified fault-case discovery and the expanded contingency scope in the documentation.

Validation:

  • PowerFactory suite: 39 passed, including 7 subtests.
  • Full repository suite: 2168 passed, 25 skipped.
  • Live mode-restoration check:
    • Before DC run: linear_method = 0
    • DC execution: linear_method = 1, execution code 0
    • After execution: restored to linear_method = 0

@qian-harvard qian-harvard left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything from the last round is addressed, and the mode-restoration design is better than what I suggested — capturing the result inside the try so settings.linear_method still reports the mode the run actually used, then restoring in finally, is the right split. Verified rather than read:

1) successful run, calculation_method='dc'
     reported settings.linear_method: 1   (the mode used)
     project iopt_Linear afterwards : 0   (restored)

2) failed run (Execute -> 2)
     success: False   execution_code: 2   reported linear_method: 1
     project afterwards: 0                (restored)

3) calculation_method='configured'
     SetAttribute calls: 0                (still never writes)

So configured means what the user configured again, which was the substance of the last review.

The rest checks out too. affected_elements has lost the bare except and now reports total_affected_elements / returned_affected_elements / affected_elements_truncated with a max_affected_elements knob, matching the bounding convention the other tools use. _decode_cell collapses all four decode sites — grep finds no inline copies left, only the helper's own body — so the assumption has one home and one comment. The rollback preserves the original error, and test_create_contingency_preserves_error_when_rollback_fails pins it. list_contingencies now says "List direct IntEvt children of fault-case folders", which is what it does. README covers the expanded scope and the restore.

You also fixed something I missed: setdefault was evaluating affected_elements(result_object) on every row, not just the first, so the enumeration ran once per result row per contingency. The if key not in contingencies guard removes that. Good catch.

39 in the PowerFactory suite, 454 across full CI locally, rebased, green on all four.

One left, and it is the same shape as the rollback you just fixed

If the restore in finally raises, it discards the result that was already built:

Execute returned 0, i.e. the run genuinely succeeded
  tool reports success  : False
  tool reports message  : PowerFactory refused the restore
  execution_code present: False
  project left at iopt_Linear = 1   (DC, not restored)

Two things go wrong at once. A successful analysis is reported as a failure, and execution_code — the field we added in #78 specifically so a caller would not have to read prose — is gone. And the caller is told about the restore rather than about the run, so it learns nothing about the thing it asked for, while the project is quietly left on the explicit mode.

create_contingency already handles exactly this: rollback failures are caught, logged, and do not replace the original error. The restore wants the same treatment — catch and log, keep result, and ideally say so in the payload so a caller can tell the project was left modified:

finally:
    if selected_method is not None:
        try:
            cls._set_and_verify_attributes(
                command, {"iopt_Linear": previous_method}, "Contingency Analysis"
            )
        except Exception as restore_error:
            log.error(f"Could not restore iopt_Linear: {restore_error}")
            result["settings"]["mode_restored"] = False

test_run_contingency_analysis_restores_method_after_exception covers Execute raising, which is the more likely failure — this is the narrower case where the restore itself is what fails.

One minor

affected_elements is now while True until GetObject returns None. Dropping the bare except was right, but range(100) also guaranteed the loop terminated, and nothing does now — if a build ever returns something other than None past the end, it spins. max_affected_elements bounds the output, not the walk: every element is collected and then sliced. A generous cap on the loop itself would get the termination guarantee back without reintroducing silent truncation, since you already have the flag to report it.

Neither of these is load-bearing for the normal path. Happy to merge once the restore guard is in — or say the word and I will add it myself rather than send you round again.

@aswinkrishnapoyil

Copy link
Copy Markdown
Contributor Author

Thank you for the review and feedback! If it looks good to you, could you please add those changes and merge PR #84 on your end? I'm currently tied up with another issue and prepping a follow-up pull request, so having you handle the merge would be a huge help.

An explicit calculation_method is written before the run and put back in
finally. If putting it back raised, the exception escaped the finally and
discarded the result that had already been built, so a run that returned
execution code 0 was reported as {"success": false} carrying the restore
error instead of the outcome -- and without execution_code, the field Power-Agent#78
added so a caller would not have to read prose.

The restore is now caught and logged, the result stands, and
settings.mode_restored says whether the study case was left on the
explicit mode. It is true when nothing was written, so the field answers
"is the calculation method as I left it" in every case.

Catching here also stops a restore failure replacing an exception the try
block is already propagating: when Execute raises and the restore fails
too, the caller now gets the native failure rather than the restore.

This matches how create_contingency already treats a failed rollback.

Verified across all five paths:

  run OK, restore OK         success=True   code=0   restored=True   project=0
  run fails (2), restore OK  success=False  code=2   restored=True   project=0
  run OK, restore fails      success=True   code=0   restored=False  project=1
  Execute raises + restore   success=False  message="native failure"
  configured                 no writes at all        restored=True

40 in the PowerFactory suite, 455 across full CI.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@qian-harvard

Copy link
Copy Markdown
Contributor

Pushed the restore guard to this branch as b8feede (maintainer edit) rather than sending you round a third time.

The restore is now caught and logged, the result stands, and settings.mode_restored reports whether the study case was left on the explicit mode. It is true when nothing was written, so the field answers "is the calculation method as I left it" in every case rather than only when something went wrong.

Catching it there also fixes a second-order case I had not called out separately: when Execute raises and the restore fails, the restore exception used to replace the one already propagating. The caller now gets the native failure.

Wrote the test first — it fails on the previous commit with False is not true : PowerFactory refused the restore, which is exactly the symptom. Verified all five paths:

run OK, restore OK         success=True   code=0   restored=True   project=0
run fails (2), restore OK  success=False  code=2   restored=True   project=0
run OK, restore fails      success=True   code=0   restored=False  project=1
Execute raises + restore   success=False  message="native failure"
configured                 no writes at all        restored=True

40 in the PowerFactory suite, 455 across full CI locally; green on all four interpreters here.

Merging. Two rounds of substantive rework on a 900-line change, and the mode-restoration design you landed on — capturing the result inside the try so linear_method still reports the mode the run used — is better than what I proposed. Thanks for that.

One thing I left alone, since it is yours to weigh and not worth another round: affected_elements is now while True until GetObject returns None. Dropping the bare except was right, but range(100) also guaranteed the loop terminated and nothing does now, and max_affected_elements bounds the output rather than the walk. A generous cap on the loop itself would get termination back without reintroducing silent truncation, since you already have the flag to report it.

@qian-harvard
qian-harvard merged commit a694e7c into Power-Agent:main Sep 23, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants