Skip to content

test: add test suite for ac validation after dc solver multi outage - #728

Open
spetznick-elia wants to merge 21 commits into
feat/dc-solver-multi-outage-importfrom
test/ac-dc-multi-outage-test
Open

spetznick-elia wants to merge 21 commits into
feat/dc-solver-multi-outage-importfrom
test/ac-dc-multi-outage-test

Conversation

@spetznick-elia

Copy link
Copy Markdown
Collaborator

This PR adds a test for AC validation after introducing proper multi-outage support in DC in #723

No changes in AC validation are required as the Powsybl backend already supports multi-outages.

Checklist

Please check if the PR fulfills these requirements:

  • PR Title follows conventional commit messages
  • Tests for the changes have been added (for bug fixes / features)
  • Docs have been added / updated (for bug fixes / features)
  • All commits in this PR are DCO signed-off (see CONTRIBUTING.md)

Does this PR already have an issue describing the problem?

Fixes #

What is the new behavior (if this is a feature change)?

Does this PR introduce a breaking change?

  • Yes
  • No

spetznick-elia and others added 21 commits September 10, 2026 10:33
… ones

An imported multi-outage that contains a bridging branch, or that is a cut
set in its own right, made the MODF denominator singular for every candidate
topology. The BSDF/LODF action filter then rejected every substation split, so
the topology optimizer was left with no search space at all. Reproduces on the
PandaPower oberrhein fixture with no complex import involved.

exclude_bridges_from_outage_masks now acts on whole contingencies: an imported
group is dropped and reported at warning level, while a synthesised trafo3w or
bus group -- which islands its own star node or busbar by construction -- is
repaired by sparing branches instead. This replaces the type-keyed
_zero_out_first_branch hack and its -1 sentinel.

Also removes multi_outage_nodes end to end. It was produced, serialised and
shape-asserted but never read by any computation; DC already models busbar
outages through preprocess_bb_outages. This drops the multi_outage_nodes_{idx}
datasets from static_information.hdf5: new code reads old files, but older code
cannot read new ones.

Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
Add load_nminus1_definition_from_file(), which resolves a business N-1
contingency list (grouped InterruptedComponents/OpenedSwitches outages plus
ClosedSwitches SPPS actions) against a Powsybl network into a shared
Nminus1Definition. A converted three-winding transformer referenced in
InterruptedComponents is expanded to its -Leg1/-Leg2/-Leg3 elements.

This is not wired into the production import pipeline (preprocessing.py /
powsybl_masks.py) yet, so convert_file() behavior, the DC solver, contingency
analysis and the topology optimizer are all unchanged: the parser is a fully
tested, standalone capability that a follow-up change will connect to
create_nminus1_definition() together with the DC/CA-side handling for grouped
outages and SPPS rules.

Adds the minimal Nminus1Definition/spps_parameters additions the parser needs:
- Nminus1Definition.source_schema to tag complex-imported definitions
- Condition.condition_limit_value now also accepts the string switch targets
  used by SPPS switching-state conditions
- a model validator enforcing that every SPPS scheme_name matches exactly one
  contingency id
- SppsConditionType.SWITCHING_STATE
- HVDC_LINE added to the shared GridElementType literal, needed because
  get_all_element_names() now inventories HVDC lines for element resolution

Fixture: data/complex_grid/contingency_list_complex.json (7 cases, 6 genuine
multi-outages) for the parser's own tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
… solver

Cut the Powsybl backend's dependency on *_for_nminus1 masks: outages are now
taken exclusively from nminus1_definition.json (or the DC-written
dc_nminus1_definition.json fallback for folders without an importer run), so
grouped/multi-element contingencies written by an importer (including the
complex parser added in a previous commit) reach the DC solver unchanged.
_get_definition_mask lost its mask fallback and the uses_dc_definition switch;
all six call sites now take outages from the definition only.
get_busbar_outage_map is built from kind=="bus" contingencies instead of the
busbar_for_nminus1 mask.

Because producers must now write the definition before load_grid() reads it,
save_nminus1_definition_from_masks() derives one from the masks a fixture
already wrote and is wired into the Powsybl example-grid builders and into
write_aux_data() as a non-overwriting provisioning step.

DC's own projection of the canonical definition -- exactly the contingencies
DC computes, in solver order -- is now written to dc_nminus1_definition.json
as an output only; it is never read back as input, which used to shrink the
busbar set on every run. NetworkData.contingency_ids now labels single
outages by their source contingency id
(BackendInterface.get_contingency_id_by_element_id, defaults to {} so
PandaPower is unaffected) instead of by element id, and the single-outage vs.
multi-outage split now keys off the *projected* arity instead of the source
arity, so a line plus its two breakers collapses to one branch outage instead
of a degenerate multi-outage row.

Fixes a multi-outage bug found while wiring this up: a live multi-outage that
islands the unsplit grid made the BSDF/LODF action filter reject every split
of every substation, because modf_success is conjoined over all multi-outages
and the MODF denominator is singular for any group that islands the base
grid. exclude_bridges_from_outage_masks now runs an islanding test over whole
multi-outage groups (find_islanding_branch_groups), not just single branches:
an *imported* group that contains a bridge or is a cut set in its own right is
dropped whole and reported at warning level (a curated contingency is
computed as declared or not at all); a *synthesised* group (trafo3w, bus -
one physical element that islands its own star node/busbar by construction)
is repaired instead by sparing the minimal set of branches that resolves the
islanding (find_branches_to_spare_from_groups), replacing the previous
unconditional "blank the first branch" hack. The gap between a declared and
a computed synthesised group is now recorded explicitly in
NetworkData.multi_outage_spared_branch_mask.

Also removes multi_outage_nodes end to end (BackendInterface method, both
backend implementations, NetworkData/DynamicInformation fields, HDF5
datasets): it was produced, reduced, padded, serialized and shape-asserted
but never read by any computation, since build_modf_matrices and
apply_modf_matrices only ever took branches.

Note: static_information.hdf5 no longer contains multi_outage_nodes_{idx}.
Old code reads new files fine (lookup by key), but older code cannot read
files written after this change.

New end-to-end regression:
tests/preprocessing/test_complex_contingency_end_to_end.py covers
contingency_list_complex.json -> convert_file -> canonical -> projection ->
PowsyblBackend -> NetworkData/StaticInformation and pins the switch-collapse
table (a line + its breakers projects to a single branch outage; a converted
3W transformer's three legs project to a 3-branch multi-outage; a group with
no DC-supported element is dropped).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
…pipeline

create_nminus1_definition() now dispatches on
importer_parameters.schema_format: for
"ContingencyImportSchemaComplex" it calls
load_nminus1_definition_from_file() (added in a previous commit) instead of
deriving the definition from masks, using the mask-derived definition only
for its monitored_elements and base_case. make_masks() becomes a no-op for
the complex schema, since a mask cannot express grouped N-k outages -- it
stays the importer's internal derivation step for every other input, never a
transport to DC.

save_shared_nminus1_definition() replaces the direct
save_pydantic_model_fs() call in convert_file() /
compute_network_masks_and_n_1_definition(): it persists the canonical
definition and immediately reloads it, asserting the round trip is
lossless, so a serialization gap can never silently change what DC and CA
read back.

BaseImporterParameters.schema_format gains the
"ContingencyImportSchemaComplex" literal alongside the existing PowerFactory
and CSV schemas.

New regression:
test_convert_file_complex_contingencies_persists_grouped_definition drives
convert_file() over data/complex_grid/contingency_list_complex.json end to
end and asserts the persisted definition keeps every grouped contingency,
its SPPS rules, and source_schema == "complex".

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
…evaluation

Neither AC backend may execute a complex list's SPPS rules in this work:
Pandapower would feed them to its SPPS engine and Powsybl has no engine for
them at all. get_ac_loadflow_results() now strips spps_rules from an
in-memory copy (copy_without_spps_rules) whenever
n_minus_1_definition.source_schema == "complex"; the canonical definition on
disk keeps its rules untouched.

For every other input, a mask-derived definition auto-generates one
contingency per switch in the N-1 area, so a node-breaker grid yields a
contingency for every disconnector -- these outage only the equipment behind
them, cannot converge, and used to make CA's ">len/2" non-convergence guard
abort every run once DC stopped silently shrinking the definition it
forwarded. get_ac_loadflow_results() and the AC optimizer's definition load
now both apply copy_without_switch_only_contingencies() to drop these,
gated on source_schema != "complex" so a curated list's deliberate
switch-only case is kept. Both call sites need the filter independently:
the topology optimizer reaches Powsybl CA through PowsyblRunner and bypasses
get_ac_loadflow_results() entirely.

Nminus1Definition gains the two copy helper functions used above
(copy_without_spps_rules, copy_without_switch_only_contingencies); the
source_schema field, the SPPS scheme-name validator and the widened
condition_limit_value type were added by the parser commit. BaseGrid gains a
sanitize_spps_rules flag for message-level callers.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
…notebook

Update docs/dc_solver/preprocessing.md, docs/dc_solver/quickstart.md and
docs/usage.md to describe nminus1_definition.json as the DC solver's outage
source instead of the *_for_nminus1 masks.

Add notebooks/example4_data_contracts.ipynb, walking through
importer -> canonical definition -> DC projection -> CA for a complex
grouped-contingency import.

Remove the unreferenced data/test_station.json fixture.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
Signed-off-by: Sascha Petznick <229719644+spetznick-elia@users.noreply.github.com>
@spetznick-elia
spetznick-elia added this pull request to stack #727 September 10, 2026 20:42
Copilot AI lite review requested due to automatic review settings September 10, 2026 20:42

Copilot AI 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.

🟡 Changes recommended

The AC coverage assertion can pass when unsupported contingencies are skipped.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Pull request overview

Adds end-to-end tests for AC validation after DC multi-outage support.

Changes:

  • Adds a two-line imported outage fixture.
  • Verifies DC scope and multi-outage metadata.
  • Compares AC security results with brute-force AC loadflows.
File summaries
File Description
packages/dc_solver_pkg/tests/postprocessing/test_ac_dc_multi_outage.py Adds multi-outage preprocessing, scope, and AC validation tests.
Review details
  • Files reviewed: 1/1 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +181 to +182
ac_converged = ac_results.converged.filter(pl.col("timestep") == 0).select("contingency").unique().collect()
ac_ids = set(ac_converged["contingency"].to_list())
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