Extend PowerFactory contingency analysis tools - #84
qian-harvard merged 3 commits into
Conversation
qian-harvard
left a comment
There was a problem hiding this comment.
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:
breakTwo 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()andcreated_case.Delete()run inside theexceptunguarded; ifDeleteitself 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
ComSimoutagesettings were deliberately out of scope; this PR does both. Fine as a follow-up — just worth a line inPowerFactory/README.mdso 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.
|
Addressed the review feedback in
Validation:
|
qian-harvard
left a comment
There was a problem hiding this comment.
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"] = Falsetest_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.
|
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>
|
Pushed the restore guard to this branch as The restore is now caught and logged, the result stands, and Catching it there also fixes a second-order case I had not called out separately: when Wrote the test first — it fails on the previous commit with 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 One thing I left alone, since it is yours to weigh and not worth another round: |
Summary
Extends the PowerFactory contingency workflow added in #78, on top of the fixes merged in #83.
Configuration and execution
get_contingency_configurationfor read-only inspection of the activeComSimoutagesettings.run_contingency_analysiswith validated calculation modes:acdcac_linearisedlinearised_screeningiopt_Linearsetting.Contingency definitions
create_contingencyfor creating idempotentEvtSwitch-based fault cases.list_contingenciesto report bothEvtOutageandEvtSwitchevents, including target metadata and enabled state.Result inspection
get_contingency_resultswith:b:i_objresolution toComOutageobjects;nullwith PowerFactory error codes.get_contingency_summaryfor: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:
"success": true."success": falseand a human-readable"message".ElmRes.Validation
bash.exeonPATH: 2 passed.git diff --checkpassed.Live PowerFactory validation
Validated against study case
Case 1:MCP N-1 Line Test.Line 01 - 02.Line 06 - 07overload reported at approximately 81.06%.