From 2fb7b5d5fce2d5482e6c9d09286436259aa5b1f5 Mon Sep 17 00:00:00 2001 From: Bertrand Rix Date: Thu, 18 Jan 2024 16:13:13 +0100 Subject: [PATCH] Support for operator strategies in security analysis API (#621) Signed-off-by: Bertrand Rix --- cpp/src/bindings.cpp | 69 +++++- cpp/src/pypowsybl-api.h | 24 ++ cpp/src/pypowsybl.cpp | 53 +++++ cpp/src/pypowsybl.h | 29 ++- docs/user_guide/security.rst | 72 ++++-- .../com/powsybl/python/commons/CTypeUtil.java | 17 +- .../python/commons/PyPowsyblApiHeader.java | 57 ++++- .../java/com/powsybl/python/commons/Util.java | 25 +++ .../powsybl/python/network/Dataframes.java | 22 +- .../python/security/BranchResultContext.java | 9 +- .../python/security/BusResultContext.java | 9 +- .../security/SecurityAnalysisCFunctions.java | 212 +++++++++++++++++- .../security/SecurityAnalysisContext.java | 22 +- pypowsybl/_pypowsybl.pyi | 71 +++++- pypowsybl/security/impl/security.py | 33 ++- .../security/impl/security_analysis_result.py | 42 +++- pypowsybl/security/impl/util.py | 10 +- tests/test_security_analysis.py | 95 +++++++- 18 files changed, 809 insertions(+), 62 deletions(-) diff --git a/cpp/src/bindings.cpp b/cpp/src/bindings.cpp index 74178e3540..5202c280fd 100644 --- a/cpp/src/bindings.cpp +++ b/cpp/src/bindings.cpp @@ -20,6 +20,12 @@ void bindArray(py::module_& m, const std::string& className) { }) .def("__iter__", [](T& a) { return py::make_iterator(a.begin(), a.end()); + }, py::keep_alive<0, 1>()) + .def("__getitem__", [](T& a, size_t index) { + if (index >= a.length()) { + throw pypowsybl::PyPowsyblError("Index out of bounds."); + } + return *(a.begin() + index); }, py::keep_alive<0, 1>()); } @@ -509,16 +515,64 @@ PYBIND11_MODULE(_pypowsybl, m) { m.def("add_contingency", &pypowsybl::addContingency, "Add a contingency to a security analysis or sensitivity analysis", py::arg("analysis_context"), py::arg("contingency_id"), py::arg("elements_ids")); + m.def("add_load_active_power_action", &pypowsybl::addLoadActivePowerAction, "Add a load active power remedial action", + py::arg("analysis_context"), py::arg("action_id"), py::arg("load_id"), py::arg("is_relative"), py::arg("active_power")); + + m.def("add_load_reactive_power_action", &pypowsybl::addLoadReactivePowerAction, "Add a load reactive power remedial action", + py::arg("analysis_context"), py::arg("action_id"), py::arg("load_id"), py::arg("is_relative"), py::arg("reactive_power")); + + m.def("add_generator_active_power_action", &pypowsybl::addGeneratorActivePowerAction, "Add a generator active power remedial action", + py::arg("analysis_context"), py::arg("action_id"), py::arg("generator_id"), py::arg("is_relative"), py::arg("active_power")); + + m.def("add_switch_action", &pypowsybl::addSwitchAction, "Add a switch action", + py::arg("analysis_context"), py::arg("action_id"), py::arg("switch_id"), py::arg("open")); + + m.def("add_phase_tap_changer_position_action", &pypowsybl::addPhaseTapChangerPositionAction, "Add a phase tap changer position action", + py::arg("analysis_context"), py::arg("action_id"), py::arg("transformer_id"), py::arg("is_relative"), py::arg("tap_position")); + + m.def("add_ratio_tap_changer_position_action", &pypowsybl::addRatioTapChangerPositionAction, "Add a ratio tap changer position action", + py::arg("analysis_context"), py::arg("action_id"), py::arg("transformer_id"), py::arg("is_relative"), py::arg("tap_position")); + + m.def("add_shunt_compensator_position_action", &pypowsybl::addShuntCompensatorPositionAction, "Add a shunt compensator position action", + py::arg("analysis_context"), py::arg("action_id"), py::arg("shunt_id"), py::arg("section_count")); + + m.def("add_operator_strategy", &pypowsybl::addOperatorStrategy, "Add an operator strategy", + py::arg("analysis_context"), py::arg("operator_strategy_id"), py::arg("contingency_id"), py::arg("action_ids"), + py::arg("condition_type"), py::arg("subject_ids"), py::arg("violation_types")); + py::enum_(m, "LimitType") + .value("ACTIVE_POWER", pypowsybl::LimitType::ACTIVE_POWER) + .value("APPARENT_POWER", pypowsybl::LimitType::APPARENT_POWER) .value("CURRENT", pypowsybl::LimitType::CURRENT) .value("LOW_VOLTAGE", pypowsybl::LimitType::LOW_VOLTAGE) - .value("HIGH_VOLTAGE", pypowsybl::LimitType::HIGH_VOLTAGE); + .value("HIGH_VOLTAGE", pypowsybl::LimitType::HIGH_VOLTAGE) + .value("LOW_VOLTAGE_ANGLE", pypowsybl::LimitType::LOW_VOLTAGE_ANGLE) + .value("HIGH_VOLTAGE_ANGLE", pypowsybl::LimitType::HIGH_VOLTAGE_ANGLE) + .value("LOW_SHORT_CIRCUIT_CURRENT", pypowsybl::LimitType::LOW_SHORT_CIRCUIT_CURRENT) + .value("HIGH_SHORT_CIRCUIT_CURRENT", pypowsybl::LimitType::HIGH_SHORT_CIRCUIT_CURRENT) + .value("OTHER", pypowsybl::LimitType::OTHER); py::enum_(m, "Side") .value("NONE", pypowsybl::Side::NONE) .value("ONE", pypowsybl::Side::ONE) .value("TWO", pypowsybl::Side::TWO); + py::enum_(m, "ViolationType") + .value("ACTIVE_POWER", violation_type::ACTIVE_POWER) + .value("APPARENT_POWER", violation_type::APPARENT_POWER) + .value("CURRENT", violation_type::CURRENT) + .value("LOW_VOLTAGE", violation_type::LOW_VOLTAGE) + .value("HIGH_VOLTAGE", violation_type::HIGH_VOLTAGE) + .value("LOW_SHORT_CIRCUIT_CURRENT", violation_type::LOW_SHORT_CIRCUIT_CURRENT) + .value("HIGH_SHORT_CIRCUIT_CURRENT", violation_type::HIGH_SHORT_CIRCUIT_CURRENT) + .value("OTHER", violation_type::OTHER); + + py::enum_(m, "ConditionType") + .value("TRUE_CONDITION", condition_type::TRUE_CONDITION) + .value("ALL_VIOLATION_CONDITION", condition_type::ALL_VIOLATION_CONDITION) + .value("ANY_VIOLATION_CONDITION", condition_type::ANY_VIOLATION_CONDITION) + .value("AT_LEAST_ONE_VIOLATION_CONDITION", condition_type::AT_LEAST_ONE_VIOLATION_CONDITION); + py::class_>(m, "NetworkMetadata") .def_property_readonly("id", [](const network_metadata& att) { return att.id; @@ -579,6 +633,18 @@ PYBIND11_MODULE(_pypowsybl, m) { }); bindArray(m, "PostContingencyResultArray"); + py::class_(m, "OperatorStrategyResult") + .def_property_readonly("operator_strategy_id", [](const operator_strategy_result& r) { + return r.operator_strategy_id; + }) + .def_property_readonly("status", [](const operator_strategy_result& r) { + return static_cast(r.status); + }) + .def_property_readonly("limit_violations", [](const operator_strategy_result& r) { + return pypowsybl::LimitViolationArray((array *) & r.limit_violations); + }); + bindArray(m, "OperatorStrategyResultArray"); + py::class_(m, "PreContingencyResult") .def_property_readonly("status", [](const pre_contingency_result& r) { return static_cast(r.status); @@ -718,6 +784,7 @@ PYBIND11_MODULE(_pypowsybl, m) { m.def("get_post_contingency_results", &pypowsybl::getPostContingencyResults, "get post contingency results of a security analysis", py::arg("result")); m.def("get_pre_contingency_result", &pypowsybl::getPreContingencyResult, "get pre contingency result of a security analysis", py::arg("result")); + m.def("get_operator_strategy_results", &pypowsybl::getOperatorStrategyResults, "get operator strategy results of a security analysis", py::arg("result")); m.def("get_node_breaker_view_nodes", &pypowsybl::getNodeBreakerViewNodes, "get all nodes for a voltage level", py::arg("network"), py::arg("voltage_level")); m.def("get_node_breaker_view_internal_connections", &pypowsybl::getNodeBreakerViewInternalConnections, "get all internal connections for a voltage level", py::arg("network"), py::arg("voltage_level")); diff --git a/cpp/src/pypowsybl-api.h b/cpp/src/pypowsybl-api.h index f5e599b069..83eed6ad44 100644 --- a/cpp/src/pypowsybl-api.h +++ b/cpp/src/pypowsybl-api.h @@ -120,6 +120,12 @@ typedef struct pre_contingency_result_struct { array limit_violations; } pre_contingency_result; +typedef struct operator_strategy_result_struct { + char* operator_strategy_id; + int status; + array limit_violations; +} operator_strategy_result; + typedef enum { BUS = 0, LINE, @@ -166,6 +172,24 @@ typedef enum { TWTS3W, } validation_type; +typedef enum { + ACTIVE_POWER = 0, + APPARENT_POWER, + CURRENT, + LOW_VOLTAGE, + HIGH_VOLTAGE, + LOW_SHORT_CIRCUIT_CURRENT, + HIGH_SHORT_CIRCUIT_CURRENT, + OTHER, +} violation_type; + +typedef enum { + TRUE_CONDITION = 0, + ALL_VIOLATION_CONDITION, + ANY_VIOLATION_CONDITION, + AT_LEAST_ONE_VIOLATION_CONDITION, +} condition_type; + typedef enum { EQUIPMENT = 0, STEADY_STATE_HYPOTHESIS, diff --git a/cpp/src/pypowsybl.cpp b/cpp/src/pypowsybl.cpp index 7876ba117e..d20d3bd5d4 100644 --- a/cpp/src/pypowsybl.cpp +++ b/cpp/src/pypowsybl.cpp @@ -131,6 +131,11 @@ Array::~Array() { callJava<>(::freeContingencyResultArrayPointer, delegate_); } +template<> +Array::~Array() { + callJava<>(::freeOperatorStrategyResultArrayPointer, delegate_); +} + template<> Array::~Array() { // already freed by contingency_result @@ -843,6 +848,50 @@ JavaHandle createSensitivityAnalysis() { return callJava(::createSensitivityAnalysis); } +void addLoadActivePowerAction(const JavaHandle& analysisContext, const std::string& actionId, const std::string& loadId, bool relativeValue, double activePower) { + callJava(::addLoadActivePowerAction, analysisContext, (char*) actionId.data(), (char*) loadId.data(), relativeValue, activePower); +} + +void addLoadReactivePowerAction(const JavaHandle& analysisContext, const std::string& actionId, const std::string& loadId, bool relativeValue, double reactivePower) { + callJava(::addLoadReactivePowerAction, analysisContext, (char*) actionId.data(), (char*) loadId.data(), relativeValue, reactivePower); +} + +void addGeneratorActivePowerAction(const JavaHandle& analysisContext, const std::string& actionId, const std::string& generatorId, bool relativeValue, double activePower) { + callJava(::addGeneratorActivePowerAction, analysisContext, (char*) actionId.data(), (char*) generatorId.data(), relativeValue, activePower); +} + +void addSwitchAction(const JavaHandle& analysisContext, const std::string& actionId, const std::string& switchId, bool open) { + callJava(::addSwitchAction, analysisContext, (char*) actionId.data(), (char*) switchId.data(), open); +} + +void addPhaseTapChangerPositionAction(const JavaHandle& analysisContext, const std::string& actionId, const std::string& transformerId, + bool isRelative, int tapPosition) { + callJava(::addPhaseTapChangerPositionAction, analysisContext, (char*) actionId.data(), (char*) transformerId.data(), isRelative, tapPosition); +} + +void addRatioTapChangerPositionAction(const JavaHandle& analysisContext, const std::string& actionId, const std::string& transformerId, + bool isRelative, int tapPosition) { + callJava(::addRatioTapChangerPositionAction, analysisContext, (char*) actionId.data(), (char*) transformerId.data(), isRelative, tapPosition); +} + +void addShuntCompensatorPositionAction(const JavaHandle& analysisContext, const std::string& actionId, const std::string& shuntId, + int sectionCount) { + callJava(::addShuntCompensatorPositionAction, analysisContext, (char*) actionId.data(), (char*) shuntId.data(), sectionCount); +} + +void addOperatorStrategy(const JavaHandle& analysisContext, std::string operatorStrategyId, std::string contingencyId, const std::vector& actionsIds, + condition_type conditionType, const std::vector& subjectIds, const std::vector& violationTypesFilters) { + ToCharPtrPtr actionsPtr(actionsIds); + ToCharPtrPtr subjectIdsPtr(subjectIds); + std::vector violationTypes; + for(int i = 0; i < violationTypesFilters.size(); ++i) { + violationTypes.push_back(violationTypesFilters[i]); + } + ToIntPtr violationTypesPtr(violationTypes); + callJava(::addOperatorStrategy, analysisContext, (char*) operatorStrategyId.data(), (char*) contingencyId.data(), actionsPtr.get(), actionsIds.size(), + conditionType, subjectIdsPtr.get(), subjectIds.size(), violationTypesPtr.get(), violationTypesFilters.size()); +} + ::zone* createZone(const std::string& id, const std::vector& injectionsIds, const std::vector& injectionsShiftKeys) { auto z = new ::zone; z->id = copyStringToCharPtr(id); @@ -975,6 +1024,10 @@ PostContingencyResultArray* getPostContingencyResults(const JavaHandle& security return new PostContingencyResultArray(callJava(::getPostContingencyResults, securityAnalysisResult)); } +OperatorStrategyResultArray* getOperatorStrategyResults(const JavaHandle& securityAnalysisResult) { + return new OperatorStrategyResultArray(callJava(::getOperatorStrategyResults, securityAnalysisResult)); +} + pre_contingency_result* getPreContingencyResult(const JavaHandle& securityAnalysisResult) { return callJava(::getPreContingencyResult, securityAnalysisResult); } diff --git a/cpp/src/pypowsybl.h b/cpp/src/pypowsybl.h index c62fb6cfb8..43d47d445f 100644 --- a/cpp/src/pypowsybl.h +++ b/cpp/src/pypowsybl.h @@ -73,6 +73,7 @@ class Array { typedef Array LoadFlowComponentResultArray; typedef Array PostContingencyResultArray; +typedef Array OperatorStrategyResultArray; typedef Array LimitViolationArray; typedef Array SeriesArray; @@ -111,9 +112,16 @@ enum class PostContingencyComputationStatus { }; enum LimitType { - CURRENT = 0, + ACTIVE_POWER = 0, + APPARENT_POWER, + CURRENT, LOW_VOLTAGE, HIGH_VOLTAGE, + LOW_VOLTAGE_ANGLE, + HIGH_VOLTAGE_ANGLE, + LOW_SHORT_CIRCUIT_CURRENT, + HIGH_SHORT_CIRCUIT_CURRENT, + OTHER }; enum Side { @@ -414,6 +422,23 @@ JavaHandle runSecurityAnalysis(const JavaHandle& securityAnalysisContext, const JavaHandle createSensitivityAnalysis(); +void addLoadActivePowerAction(const JavaHandle& analysisContext, const std::string& actionId, const std::string& loadId, bool relativeValue, double activePower); + +void addLoadReactivePowerAction(const JavaHandle& analysisContext, const std::string& actionId, const std::string& loadId, bool relativeValue, double reactivePower); + +void addGeneratorActivePowerAction(const JavaHandle& analysisContext, const std::string& actionId, const std::string& generatorId, bool relativeValue, double activePower); + +void addSwitchAction(const JavaHandle& analysisContext, const std::string& actionId, const std::string& switchId, bool open); + +void addPhaseTapChangerPositionAction(const JavaHandle& analysisContext, const std::string& actionId, const std::string& transformerId, bool isRelative, int tapPosition); + +void addRatioTapChangerPositionAction(const JavaHandle& analysisContext, const std::string& actionId, const std::string& transformerId, bool isRelative, int tapPosition); + +void addShuntCompensatorPositionAction(const JavaHandle& analysisContext, const std::string& actionId, const std::string& shuntId, int sectionCount); + +void addOperatorStrategy(const JavaHandle& analysisContext, std::string operatorStrategyId, std::string contingencyId, const std::vector& actionsIds, + condition_type conditionType, const std::vector& subjectIds, const std::vector& violationTypesFilters); + void setZones(const JavaHandle& sensitivityAnalysisContext, const std::vector<::zone*>& zones); void addFactorMatrix(const JavaHandle& sensitivityAnalysisContext, std::string matrixId, const std::vector& branchesIds, @@ -456,6 +481,8 @@ SeriesArray* getLimitViolations(const JavaHandle& securityAnalysisResult); PostContingencyResultArray* getPostContingencyResults(const JavaHandle& securityAnalysisResult); +OperatorStrategyResultArray* getOperatorStrategyResults(const JavaHandle& securityAnalysisResult); + pre_contingency_result* getPreContingencyResult(const JavaHandle& securityAnalysisResult); SeriesArray* getBranchResults(const JavaHandle& securityAnalysisResult); diff --git a/docs/user_guide/security.rst b/docs/user_guide/security.rst index edc449aba0..5071f960ca 100644 --- a/docs/user_guide/security.rst +++ b/docs/user_guide/security.rst @@ -7,6 +7,7 @@ Running a security analysis import pandas as pd pd.options.display.max_columns = None pd.options.display.expand_frame_repr = False + from pypowsybl._pypowsybl import ConditionType You can use the module :mod:`pypowsybl.security` in order to perform a security analysis on a network. Please check out the examples below. @@ -70,18 +71,18 @@ Information can be obtained on buses, branches and three windings transformers. >>> security_analysis.add_precontingency_monitored_elements(branch_ids=['NHV1_NHV2_2']) >>> results = security_analysis.run_ac(network) >>> results.bus_results - v_mag v_angle - contingency_id voltage_level_id bus_id - VLHV2 NHV2 389.95 -3.51 - NGEN_NHV1 VLHV2 NHV2 569.04 -1.71 - NHV1_NHV2_1 VLHV2 NHV2 366.58 -7.50 + v_mag v_angle + contingency_id operator_strategy_id voltage_level_id bus_id + VLHV2 NHV2 389.95 -3.51 + NGEN_NHV1 VLHV2 NHV2 569.04 -1.71 + NHV1_NHV2_1 VLHV2 NHV2 366.58 -7.50 >>> results.branch_results - p1 q1 i1 p2 q2 i2 flow_transfer - contingency_id branch_id - NHV1_NHV2_2 302.44 98.74 456.77 -300.43 -137.19 488.99 NaN - NGEN_NHV1 NHV1_NHV2_1 301.06 0.00 302.80 -300.19 -116.60 326.75 NaN - NHV1_NHV2_2 301.06 0.00 302.80 -300.19 -116.60 326.75 NaN - NHV1_NHV2_1 NHV1_NHV2_2 610.56 334.06 1,008.93 -601.00 -285.38 1,047.83 NaN + p1 q1 i1 p2 q2 i2 flow_transfer + contingency_id operator_strategy_id branch_id + NHV1_NHV2_2 302.44 98.74 456.77 -300.43 -137.19 488.99 NaN + NGEN_NHV1 NHV1_NHV2_1 301.06 0.00 302.80 -300.19 -116.60 326.75 NaN + NHV1_NHV2_2 301.06 0.00 302.80 -300.19 -116.60 326.75 NaN + NHV1_NHV2_1 NHV1_NHV2_2 610.56 334.06 1,008.93 -601.00 -285.38 1,047.83 NaN @@ -98,13 +99,50 @@ It also possible to get flow transfer on monitored branches in case of N-1 branc >>> sa.add_monitored_elements(branch_ids=['NHV1_NHV2_1', 'NHV1_NHV2_2']) >>> sa_result = sa.run_ac(n) >>> sa_result.branch_results - p1 q1 i1 p2 q2 i2 flow_transfer - contingency_id branch_id - NHV1_NHV2_2 302.444049 98.740275 456.768978 -300.433895 -137.188493 488.992798 NaN - NHV1_NHV2_1 302.444049 98.740275 456.768978 -300.433895 -137.188493 488.992798 NaN - NHV1_NHV2_2 NHV1_NHV2_1 610.562154 334.056272 1008.928788 -600.996156 -285.379147 1047.825769 1.018761 - NHV1_NHV2_1 NHV1_NHV2_2 610.562154 334.056272 1008.928788 -600.996156 -285.379147 1047.825769 1.018761 + p1 q1 i1 p2 q2 i2 flow_transfer + contingency_id operator_strategy_id branch_id + NHV1_NHV2_2 302.444049 98.740275 456.768978 -300.433895 -137.188493 488.992798 NaN + NHV1_NHV2_1 302.444049 98.740275 456.768978 -300.433895 -137.188493 488.992798 NaN + NHV1_NHV2_2 NHV1_NHV2_1 610.562154 334.056272 1008.928788 -600.996156 -285.379147 1047.825769 1.018761 + NHV1_NHV2_1 NHV1_NHV2_2 610.562154 334.056272 1008.928788 -600.996156 -285.379147 1047.825769 1.018761 .. testcleanup:: security.monitored_elements pd.options.display.float_format = None + +Operator strategies and remedial actions +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Pypowsybl security analysis support operator strategies and remedial actions definition. +You can define several types of actions by calling the add_XXX_action API. +All actions need a unique id to be referenced later at the operator strategy creation stage. +The following example define a switch closing action with id 'SwitchAction' on the switch with id 'S4VL1_BBS_LD6_DISCONNECTOR'. + +.. doctest:: + :options: +NORMALIZE_WHITESPACE + + >>> n = pp.network.create_four_substations_node_breaker_network() + >>> sa = pp.security.create_analysis() + >>> sa.add_switch_action(action_id='SwitchAction', switch_id='S4VL1_BBS_LD6_DISCONNECTOR', open=False) + +To enable the application of the action you need to define an operator strategy and add the action to it. +An operator strategy is a set of actions to be applied after the simulation of a contingency. +It is defined with an unique id, a reference to the id of the contingency, a list action ids and a condition. +The following operator strategy define the application of the switch action 'SwitchAction' after 'Breaker contingency' with the 'True' condition (always applied) : + +.. doctest:: + :options: +NORMALIZE_WHITESPACE + + >>> n = pp.network.create_four_substations_node_breaker_network() + >>> sa = pp.security.create_analysis() + >>> sa.add_single_element_contingency(element_id='S4VL1_BBS_LD6_DISCONNECTOR', contingency_id='Breaker contingency') + >>> sa.add_switch_action(action_id='SwitchAction', switch_id='S4VL1_BBS_LD6_DISCONNECTOR', open=False) + >>> sa.add_operator_strategy(operator_strategy_id='OperatorStrategy1', contingency_id='Breaker contingency', action_ids=['SwitchAction'], condition_type=ConditionType.TRUE_CONDITION) + >>> sa.add_monitored_elements(branch_ids=['LINE_S3S4']) + >>> sa_result = sa.run_ac(n) + >>> df = sa_result.branch_results + >>> #Get the detailed results post operator strategy + >>> df.loc['Breaker contingency', 'OperatorStrategy1', 'LINE_S3S4']['p1'] + 240.00360040333226 + +Results for the post remedial action state are available in the branch results indexed with the operator strategy unique id. \ No newline at end of file diff --git a/java/src/main/java/com/powsybl/python/commons/CTypeUtil.java b/java/src/main/java/com/powsybl/python/commons/CTypeUtil.java index 12b775e4c1..329b668ed7 100644 --- a/java/src/main/java/com/powsybl/python/commons/CTypeUtil.java +++ b/java/src/main/java/com/powsybl/python/commons/CTypeUtil.java @@ -18,9 +18,8 @@ import org.graalvm.word.WordFactory; import java.nio.charset.StandardCharsets; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; +import java.util.*; +import java.util.function.IntFunction; import java.util.stream.Collectors; import java.util.stream.IntStream; @@ -101,6 +100,18 @@ public static List toIntegerList(CIntPointer intPointer, int length) { return ints; } + /** + * Convert an int list to a set of enum using the specified converter + */ + public static Set toEnumSet(CIntPointer intPointer, int length, IntFunction converter) { + Set enumSet = new HashSet<>(); + for (int i = 0; i < length; i++) { + T value = converter.apply(intPointer.read(i)); + enumSet.add(value); + } + return enumSet; + } + public static Map toStringMap(CCharPointerPointer keysPointer, int keysCount, CCharPointerPointer valuesPointer, int valuesCount) { List keys = toStringList(keysPointer, keysCount); diff --git a/java/src/main/java/com/powsybl/python/commons/PyPowsyblApiHeader.java b/java/src/main/java/com/powsybl/python/commons/PyPowsyblApiHeader.java index 267674ad33..b45a58c387 100644 --- a/java/src/main/java/com/powsybl/python/commons/PyPowsyblApiHeader.java +++ b/java/src/main/java/com/powsybl/python/commons/PyPowsyblApiHeader.java @@ -14,10 +14,7 @@ import org.graalvm.nativeimage.c.constant.CEnumLookup; import org.graalvm.nativeimage.c.constant.CEnumValue; import org.graalvm.nativeimage.c.struct.*; -import org.graalvm.nativeimage.c.type.CCharPointer; -import org.graalvm.nativeimage.c.type.CCharPointerPointer; -import org.graalvm.nativeimage.c.type.CDoublePointer; -import org.graalvm.nativeimage.c.type.VoidPointer; +import org.graalvm.nativeimage.c.type.*; import org.graalvm.word.PointerBase; /** @@ -543,6 +540,27 @@ public interface PostContingencyResultPointer extends PointerBase { PostContingencyResultPointer addressOf(int index); } + @CStruct("operator_strategy_result") + public interface OperatorStrategyResultPointer extends PointerBase { + + @CField("operator_strategy_id") + CCharPointer getOperatorStrategyId(); + + @CField("operator_strategy_id") + void setOperatorStrategyId(CCharPointer contingencyId); + + @CField("status") + int getStatus(); + + @CField("status") + void setStatus(int status); + + @CFieldAddress("limit_violations") + ArrayPointer limitViolations(); + + OperatorStrategyResultPointer addressOf(int index); + } + @CEnum("element_type") public enum ElementType { BUS, @@ -623,6 +641,23 @@ public enum NetworkModificationType { public static native NetworkModificationType fromCValue(int value); } + @CEnum("violation_type") + public enum LimitViolationType { + ACTIVE_POWER, + APPARENT_POWER, + CURRENT, + LOW_VOLTAGE, + HIGH_VOLTAGE, + LOW_SHORT_CIRCUIT_CURRENT, + HIGH_SHORT_CIRCUIT_CURRENT, + OTHER; + @CEnumValue + public native int getCValue(); + + @CEnumLookup + public static native LimitViolationType fromCValue(int value); + } + @CEnum("remove_modification_type") public enum RemoveModificationType { REMOVE_FEEDER, @@ -1092,6 +1127,20 @@ public enum BranchSide { public static native BranchSide fromCValue(int value); } + @CEnum("condition_type") + public enum ConditionType { + TRUE_CONDITION, + ALL_VIOLATION_CONDITION, + ANY_VIOLATION_CONDITION, + AT_LEAST_ONE_VIOLATION_CONDITION; + + @CEnumValue + public native int getCValue(); + + @CEnumLookup + public static native ConditionType fromCValue(int value); + } + @CStruct("shortcircuit_analysis_parameters") public interface ShortCircuitAnalysisParametersPointer extends PointerBase { diff --git a/java/src/main/java/com/powsybl/python/commons/Util.java b/java/src/main/java/com/powsybl/python/commons/Util.java index 3a9fe157e5..dc54e7c457 100644 --- a/java/src/main/java/com/powsybl/python/commons/Util.java +++ b/java/src/main/java/com/powsybl/python/commons/Util.java @@ -6,6 +6,7 @@ */ package com.powsybl.python.commons; +import com.powsybl.commons.PowsyblException; import com.powsybl.commons.datasource.CompressionFormat; import com.powsybl.contingency.ContingencyContextType; import com.powsybl.dataframe.DataframeElementType; @@ -19,6 +20,7 @@ import com.powsybl.python.commons.PyPowsyblApiHeader.VoltageInitializerObjective; import com.powsybl.python.commons.PyPowsyblApiHeader.VoltageInitializerStatus; import com.powsybl.python.dataframe.CDataframeHandler; +import com.powsybl.security.LimitViolationType; import com.powsybl.sensitivity.SensitivityFunctionType; import com.powsybl.sensitivity.SensitivityVariableType; import org.graalvm.nativeimage.UnmanagedMemory; @@ -376,4 +378,27 @@ public static Optional detectCompressionFormat(ByteBuffer buf return Optional.empty(); } } + + public static LimitViolationType convert(PyPowsyblApiHeader.LimitViolationType violationType) { + switch (violationType) { + case ACTIVE_POWER: + return LimitViolationType.ACTIVE_POWER; + case APPARENT_POWER: + return LimitViolationType.APPARENT_POWER; + case CURRENT: + return LimitViolationType.CURRENT; + case LOW_VOLTAGE: + return LimitViolationType.LOW_VOLTAGE; + case HIGH_VOLTAGE: + return LimitViolationType.HIGH_VOLTAGE; + case LOW_SHORT_CIRCUIT_CURRENT: + return LimitViolationType.LOW_SHORT_CIRCUIT_CURRENT; + case HIGH_SHORT_CIRCUIT_CURRENT: + return LimitViolationType.HIGH_SHORT_CIRCUIT_CURRENT; + case OTHER: + return LimitViolationType.OTHER; + default: + throw new PowsyblException("Unknown limit violation type: " + violationType); + } + } } diff --git a/java/src/main/java/com/powsybl/python/network/Dataframes.java b/java/src/main/java/com/powsybl/python/network/Dataframes.java index 2911916ca8..996ab396da 100644 --- a/java/src/main/java/com/powsybl/python/network/Dataframes.java +++ b/java/src/main/java/com/powsybl/python/network/Dataframes.java @@ -161,11 +161,17 @@ public static DataframeMapper>> fee private static List getBranchResults(SecurityAnalysisResult result) { List branchResults = result.getPreContingencyResult().getNetworkResult() .getBranchResults().stream() - .map(branchResult -> new BranchResultContext(branchResult, null)) + .map(branchResult -> new BranchResultContext(branchResult, null, null)) .collect(Collectors.toList()); result.getPostContingencyResults().forEach(postContingencyResult -> { postContingencyResult.getNetworkResult().getBranchResults() - .forEach(branchResult -> branchResults.add(new BranchResultContext(branchResult, postContingencyResult.getContingency().getId()))); + .forEach(branchResult -> branchResults.add(new BranchResultContext(branchResult, postContingencyResult.getContingency().getId(), null))); + }); + result.getOperatorStrategyResults().forEach(operatorStrategyResult -> { + operatorStrategyResult.getNetworkResult().getBranchResults() + .forEach(branchResult -> branchResults.add(new BranchResultContext(branchResult, + operatorStrategyResult.getOperatorStrategy().getContingencyContext().getContingencyId(), + operatorStrategyResult.getOperatorStrategy().getId()))); }); return branchResults; } @@ -174,6 +180,7 @@ private static DataframeMapper createBranchResultsMapper return new DataframeMapperBuilder() .itemsProvider(Dataframes::getBranchResults) .stringsIndex("contingency_id", BranchResultContext::getContingencyId) + .stringsIndex("operator_strategy_id", BranchResultContext::getOperatorStrategyId) .stringsIndex("branch_id", BranchResultContext::getBranchId) .doubles("p1", BranchResultContext::getP1) .doubles("q1", BranchResultContext::getQ1) @@ -189,11 +196,17 @@ private static List getBusResults(SecurityAnalysisResult resul List busResults = result.getPreContingencyResult() .getNetworkResult() .getBusResults().stream() - .map(busResult -> new BusResultContext(busResult, null)) + .map(busResult -> new BusResultContext(busResult, null, null)) .collect(Collectors.toList()); result.getPostContingencyResults().forEach(postContingencyResult -> { postContingencyResult.getNetworkResult().getBusResults() - .forEach(busResult -> busResults.add(new BusResultContext(busResult, postContingencyResult.getContingency().getId()))); + .forEach(busResult -> busResults.add(new BusResultContext(busResult, postContingencyResult.getContingency().getId(), null))); + }); + result.getOperatorStrategyResults().forEach(operatorStrategyResult -> { + operatorStrategyResult.getNetworkResult().getBusResults() + .forEach(busResult -> busResults.add(new BusResultContext(busResult, + operatorStrategyResult.getOperatorStrategy().getContingencyContext().getContingencyId(), + operatorStrategyResult.getOperatorStrategy().getId()))); }); return busResults; } @@ -202,6 +215,7 @@ private static DataframeMapper createBusResultsMapper() return new DataframeMapperBuilder() .itemsProvider(Dataframes::getBusResults) .stringsIndex("contingency_id", BusResultContext::getContingencyId) + .stringsIndex("operator_strategy_id", BusResultContext::getOperatorStrategyId) .stringsIndex("voltage_level_id", BusResultContext::getVoltageLevelId) .stringsIndex("bus_id", BusResultContext::getBusId) .doubles("v_mag", BusResultContext::getV) diff --git a/java/src/main/java/com/powsybl/python/security/BranchResultContext.java b/java/src/main/java/com/powsybl/python/security/BranchResultContext.java index c66edd04f3..37b21d5af6 100644 --- a/java/src/main/java/com/powsybl/python/security/BranchResultContext.java +++ b/java/src/main/java/com/powsybl/python/security/BranchResultContext.java @@ -15,13 +15,20 @@ public class BranchResultContext extends BranchResult { private final String contingencyId; - public BranchResultContext(BranchResult branchResult, String contingency) { + private final String operatorStrategyId; + + public BranchResultContext(BranchResult branchResult, String contingency, String operatorStrategy) { super(branchResult.getBranchId(), branchResult.getP1(), branchResult.getQ1(), branchResult.getI1(), branchResult.getP2(), branchResult.getQ2(), branchResult.getI2(), branchResult.getFlowTransfer()); this.contingencyId = contingency; + this.operatorStrategyId = operatorStrategy; } public String getContingencyId() { return contingencyId; } + + public String getOperatorStrategyId() { + return operatorStrategyId; + } } diff --git a/java/src/main/java/com/powsybl/python/security/BusResultContext.java b/java/src/main/java/com/powsybl/python/security/BusResultContext.java index 0af9ac5077..d9ed0f0add 100644 --- a/java/src/main/java/com/powsybl/python/security/BusResultContext.java +++ b/java/src/main/java/com/powsybl/python/security/BusResultContext.java @@ -15,12 +15,19 @@ public class BusResultContext extends BusResult { private final String contingencyId; - public BusResultContext(BusResult busResults, String contingency) { + private final String operatorStrategyId; + + public BusResultContext(BusResult busResults, String contingency, String operatorStrategyId) { super(busResults.getVoltageLevelId(), busResults.getBusId(), busResults.getV(), busResults.getAngle()); this.contingencyId = contingency; + this.operatorStrategyId = operatorStrategyId; } public String getContingencyId() { return contingencyId; } + + public String getOperatorStrategyId() { + return operatorStrategyId; + } } diff --git a/java/src/main/java/com/powsybl/python/security/SecurityAnalysisCFunctions.java b/java/src/main/java/com/powsybl/python/security/SecurityAnalysisCFunctions.java index 89922fd341..bdc0e9868a 100644 --- a/java/src/main/java/com/powsybl/python/security/SecurityAnalysisCFunctions.java +++ b/java/src/main/java/com/powsybl/python/security/SecurityAnalysisCFunctions.java @@ -16,13 +16,14 @@ import com.powsybl.python.loadflow.LoadFlowCFunctions; import com.powsybl.python.loadflow.LoadFlowCUtils; import com.powsybl.python.network.Dataframes; -import com.powsybl.security.LimitViolation; -import com.powsybl.security.SecurityAnalysisParameters; -import com.powsybl.security.SecurityAnalysisProvider; -import com.powsybl.security.SecurityAnalysisResult; +import com.powsybl.security.*; +import com.powsybl.security.action.*; +import com.powsybl.security.condition.*; import com.powsybl.security.monitor.StateMonitor; +import com.powsybl.security.results.OperatorStrategyResult; import com.powsybl.security.results.PostContingencyResult; import com.powsybl.security.results.PreContingencyResult; +import com.powsybl.security.strategy.OperatorStrategy; import org.graalvm.nativeimage.IsolateThread; import org.graalvm.nativeimage.ObjectHandle; import org.graalvm.nativeimage.ObjectHandles; @@ -32,6 +33,7 @@ import org.graalvm.nativeimage.c.struct.SizeOf; import org.graalvm.nativeimage.c.type.CCharPointer; import org.graalvm.nativeimage.c.type.CCharPointerPointer; +import org.graalvm.nativeimage.c.type.CIntPointer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -146,6 +148,16 @@ private static void setPostContingencyResultInSecurityAnalysisResultPointer(PyPo contingencyPtr.limitViolations().setPtr(limitViolationPtr); } + private static void setOperatorStrategyResultInSecurityAnalysisResultPointer(PyPowsyblApiHeader.OperatorStrategyResultPointer operatorStrategyPtr, OperatorStrategyResult result) { + operatorStrategyPtr.setOperatorStrategyId(CTypeUtil.toCharPtr(result.getOperatorStrategy().getId())); + operatorStrategyPtr.setStatus(result.getStatus().ordinal()); + List limitViolations = result.getLimitViolationsResult().getLimitViolations(); + PyPowsyblApiHeader.LimitViolationPointer limitViolationPtr = UnmanagedMemory.calloc(limitViolations.size() * SizeOf.get(PyPowsyblApiHeader.LimitViolationPointer.class)); + createLimitViolationPtr(limitViolationPtr, limitViolations); + operatorStrategyPtr.limitViolations().setLength(limitViolations.size()); + operatorStrategyPtr.limitViolations().setPtr(limitViolationPtr); + } + private static void setPreContingencyResultInSecurityAnalysisResultPointer(PyPowsyblApiHeader.PreContingencyResultPointer contingencyPtr, PreContingencyResult preContingencyResult) { contingencyPtr.setStatus(preContingencyResult.getStatus().ordinal()); List limitViolations = preContingencyResult.getLimitViolationsResult().getLimitViolations(); @@ -188,6 +200,17 @@ private static PyPowsyblApiHeader.ArrayPointer createOperatorStrategyResultsArrayPointer(SecurityAnalysisResult result) { + int resultCount = result.getOperatorStrategyResults().size(); + PyPowsyblApiHeader.OperatorStrategyResultPointer strategyPtr = UnmanagedMemory.calloc(resultCount * SizeOf.get(PyPowsyblApiHeader.OperatorStrategyResultPointer.class)); + for (int i = 0; i < result.getOperatorStrategyResults().size(); i++) { + OperatorStrategyResult resultOp = result.getOperatorStrategyResults().get(i); + PyPowsyblApiHeader.OperatorStrategyResultPointer operatorStrategyPlus = strategyPtr.addressOf(i); + setOperatorStrategyResultInSecurityAnalysisResultPointer(operatorStrategyPlus, resultOp); + } + return allocArrayPointer(strategyPtr, resultCount); + } + private static SecurityAnalysisProvider getProvider(String name) { String actualName = name.isEmpty() ? PyPowsyblConfiguration.getDefaultSecurityAnalysisProvider() : name; return SecurityAnalysisProvider.findAll().stream() @@ -221,6 +244,14 @@ public static PyPowsyblApiHeader.ArrayPointer getOperatorStrategyResults(IsolateThread thread, ObjectHandle securityAnalysisResultHandle, PyPowsyblApiHeader.ExceptionHandlerPointer exceptionHandlerPtr) { + return doCatch(exceptionHandlerPtr, () -> { + SecurityAnalysisResult result = ObjectHandles.getGlobal().get(securityAnalysisResultHandle); + return createOperatorStrategyResultsArrayPointer(result); + }); + } + @CEntryPoint(name = "getPreContingencyResult") public static PyPowsyblApiHeader.PreContingencyResultPointer getPreContingencyResult(IsolateThread thread, ObjectHandle securityAnalysisResultHandle, PyPowsyblApiHeader.ExceptionHandlerPointer exceptionHandlerPtr) { return doCatch(exceptionHandlerPtr, () -> { @@ -256,6 +287,25 @@ public static void freeContingencyResultArrayPointer(IsolateThread thread, PyPow }); } + @CEntryPoint(name = "freeOperatorStrategyResultArrayPointer") + public static void freeOperatorStrategyResultArrayPointer(IsolateThread thread, PyPowsyblApiHeader.ArrayPointer operatorStrategyResultArrayPtr, + PyPowsyblApiHeader.ExceptionHandlerPointer exceptionHandlerPtr) { + doCatch(exceptionHandlerPtr, () -> { + for (int i = 0; i < operatorStrategyResultArrayPtr.getLength(); i++) { + PyPowsyblApiHeader.OperatorStrategyResultPointer strategyResultPtrPlus = operatorStrategyResultArrayPtr.getPtr().addressOf(i); + UnmanagedMemory.free(strategyResultPtrPlus.getOperatorStrategyId()); + for (int l = 0; l < strategyResultPtrPlus.limitViolations().getLength(); l++) { + PyPowsyblApiHeader.LimitViolationPointer violation = strategyResultPtrPlus.limitViolations().getPtr().addressOf(l); + UnmanagedMemory.free(violation.getSubjectId()); + UnmanagedMemory.free(violation.getSubjectName()); + UnmanagedMemory.free(violation.getLimitName()); + } + UnmanagedMemory.free(strategyResultPtrPlus.limitViolations().getPtr()); + } + freeArrayPointer(operatorStrategyResultArrayPtr); + }); + } + @CEntryPoint(name = "freeSecurityAnalysisParameters") public static void freeSecurityAnalysisParameters(IsolateThread thread, SecurityAnalysisParametersPointer parameters, PyPowsyblApiHeader.ExceptionHandlerPointer exceptionHandlerPtr) { @@ -290,4 +340,158 @@ public static PyPowsyblApiHeader.ArrayPointer getProviderPa return Util.createCharPtrArray(SecurityAnalysisCUtils.getSecurityAnalysisProvider(providerStr).getSpecificParametersNames()); }); } + + @CEntryPoint(name = "addLoadActivePowerAction") + public static void addLoadActivePowerAction(IsolateThread thread, ObjectHandle securityAnalysisContextHandle, + CCharPointer actionId, CCharPointer loadId, boolean relativeValue, + double activePowerValue, + PyPowsyblApiHeader.ExceptionHandlerPointer exceptionHandlerPtr) { + doCatch(exceptionHandlerPtr, () -> { + SecurityAnalysisContext analysisContext = ObjectHandles.getGlobal().get(securityAnalysisContextHandle); + String actionIdStr = CTypeUtil.toString(actionId); + String loadIdStr = CTypeUtil.toString(loadId); + LoadAction action = new LoadActionBuilder().withId(actionIdStr) + .withLoadId(loadIdStr) + .withRelativeValue(relativeValue) + .withActivePowerValue(activePowerValue) + .build(); + analysisContext.addAction(action); + }); + } + + @CEntryPoint(name = "addLoadReactivePowerAction") + public static void addLoadReactivePowerAction(IsolateThread thread, ObjectHandle securityAnalysisContextHandle, + CCharPointer actionId, CCharPointer loadId, boolean relativeValue, + double reactivePowerValue, + PyPowsyblApiHeader.ExceptionHandlerPointer exceptionHandlerPtr) { + doCatch(exceptionHandlerPtr, () -> { + SecurityAnalysisContext analysisContext = ObjectHandles.getGlobal().get(securityAnalysisContextHandle); + String actionIdStr = CTypeUtil.toString(actionId); + String loadIdStr = CTypeUtil.toString(loadId); + LoadAction action = new LoadActionBuilder().withId(actionIdStr) + .withLoadId(loadIdStr) + .withRelativeValue(relativeValue) + .withReactivePowerValue(reactivePowerValue) + .build(); + analysisContext.addAction(action); + }); + } + + @CEntryPoint(name = "addGeneratorActivePowerAction") + public static void addGeneratorActivePowerAction(IsolateThread thread, ObjectHandle securityAnalysisContextHandle, + CCharPointer actionId, CCharPointer generatorId, boolean relativeValue, double activePower, + PyPowsyblApiHeader.ExceptionHandlerPointer exceptionHandlerPtr) { + doCatch(exceptionHandlerPtr, () -> { + SecurityAnalysisContext analysisContext = ObjectHandles.getGlobal().get(securityAnalysisContextHandle); + String actionIdStr = CTypeUtil.toString(actionId); + String generatorIdStr = CTypeUtil.toString(generatorId); + GeneratorActionBuilder builder = new GeneratorActionBuilder().withId(actionIdStr) + .withGeneratorId(generatorIdStr); + if (relativeValue) { + builder.withActivePowerRelativeValue(relativeValue); + } + builder.withActivePowerValue(activePower); + analysisContext.addAction(builder.build()); + }); + } + + @CEntryPoint(name = "addSwitchAction") + public static void addSwitchAction(IsolateThread thread, ObjectHandle securityAnalysisContextHandle, + CCharPointer actionId, CCharPointer switchId, boolean open, + PyPowsyblApiHeader.ExceptionHandlerPointer exceptionHandlerPtr) { + doCatch(exceptionHandlerPtr, () -> { + SecurityAnalysisContext analysisContext = ObjectHandles.getGlobal().get(securityAnalysisContextHandle); + String actionIdStr = CTypeUtil.toString(actionId); + String switchIdStr = CTypeUtil.toString(switchId); + SwitchAction action = new SwitchAction(actionIdStr, switchIdStr, open); + analysisContext.addAction(action); + }); + } + + @CEntryPoint(name = "addPhaseTapChangerPositionAction") + public static void addPhaseTapChangerPositionAction(IsolateThread thread, ObjectHandle securityAnalysisContextHandle, + CCharPointer actionId, CCharPointer transformerId, boolean isRelative, + int tapPosition, PyPowsyblApiHeader.ExceptionHandlerPointer exceptionHandlerPtr) { + doCatch(exceptionHandlerPtr, () -> { + SecurityAnalysisContext analysisContext = ObjectHandles.getGlobal().get(securityAnalysisContextHandle); + String actionIdStr = CTypeUtil.toString(actionId); + String transformerIdStr = CTypeUtil.toString(transformerId); + PhaseTapChangerTapPositionAction pstAction = new PhaseTapChangerTapPositionAction(actionIdStr, transformerIdStr, isRelative, tapPosition); + analysisContext.addAction(pstAction); + }); + } + + @CEntryPoint(name = "addRatioTapChangerPositionAction") + public static void addRatioTapChangerPositionAction(IsolateThread thread, ObjectHandle securityAnalysisContextHandle, + CCharPointer actionId, CCharPointer transformerId, boolean isRelative, + int tapPosition, PyPowsyblApiHeader.ExceptionHandlerPointer exceptionHandlerPtr) { + doCatch(exceptionHandlerPtr, () -> { + SecurityAnalysisContext analysisContext = ObjectHandles.getGlobal().get(securityAnalysisContextHandle); + String actionIdStr = CTypeUtil.toString(actionId); + String transformerIdStr = CTypeUtil.toString(transformerId); + RatioTapChangerTapPositionAction ratioTapChangerAction = new RatioTapChangerTapPositionAction(actionIdStr, transformerIdStr, isRelative, tapPosition); + analysisContext.addAction(ratioTapChangerAction); + }); + } + + @CEntryPoint(name = "addShuntCompensatorPositionAction") + public static void addShuntCompensatorPositionAction(IsolateThread thread, ObjectHandle securityAnalysisContextHandle, + CCharPointer actionId, CCharPointer shuntCompensatorId, int sectionCount, + PyPowsyblApiHeader.ExceptionHandlerPointer exceptionHandlerPtr) { + doCatch(exceptionHandlerPtr, () -> { + SecurityAnalysisContext analysisContext = ObjectHandles.getGlobal().get(securityAnalysisContextHandle); + String actionIdStr = CTypeUtil.toString(actionId); + String shuntCompensatorIdStr = CTypeUtil.toString(shuntCompensatorId); + ShuntCompensatorPositionActionBuilder builder = new ShuntCompensatorPositionActionBuilder(); + ShuntCompensatorPositionAction action = builder.withId(actionIdStr) + .withShuntCompensatorId(shuntCompensatorIdStr) + .withSectionCount(sectionCount) + .build(); + analysisContext.addAction(action); + }); + } + + @CEntryPoint(name = "addOperatorStrategy") + public static void addOperatorStrategy(IsolateThread thread, ObjectHandle securityAnalysisContextHandle, + CCharPointer operationStrategyId, CCharPointer contingencyId, + CCharPointerPointer actions, int actionCount, + PyPowsyblApiHeader.ConditionType conditionType, + CCharPointerPointer subjectIds, int subjectIdsCount, + CIntPointer violationTypes, int violationTypesCount, + PyPowsyblApiHeader.ExceptionHandlerPointer exceptionHandlerPtr) { + doCatch(exceptionHandlerPtr, () -> { + SecurityAnalysisContext analysisContext = ObjectHandles.getGlobal().get(securityAnalysisContextHandle); + String operationStrategyIdStr = CTypeUtil.toString(operationStrategyId); + String contingencyIdStr = CTypeUtil.toString(contingencyId); + List actionsStrList = CTypeUtil.toStringList(actions, actionCount); + + Condition condition = buildCondition(conditionType, subjectIds, subjectIdsCount, violationTypes, violationTypesCount); + + OperatorStrategy op = new OperatorStrategy(operationStrategyIdStr, + ContingencyContext.specificContingency(contingencyIdStr), condition, actionsStrList); + analysisContext.addOperatorStrategy(op); + }); + } + + private static Condition buildCondition(PyPowsyblApiHeader.ConditionType conditionType, + CCharPointerPointer subjectIds, int subjectIdsCount, + CIntPointer violationTypes, int violationTypesCount) { + List subjectIdsStrList = CTypeUtil.toStringList(subjectIds, subjectIdsCount); + Set violationTypesC = CTypeUtil.toEnumSet( + violationTypes, violationTypesCount, PyPowsyblApiHeader.LimitViolationType::fromCValue); + Set violationTypesFilter = violationTypesC.stream().map(Util::convert).collect(Collectors.toSet()); + + switch (conditionType) { + case TRUE_CONDITION : + return new TrueCondition(); + case ALL_VIOLATION_CONDITION : + return new AllViolationCondition(subjectIdsStrList, violationTypesFilter); + case ANY_VIOLATION_CONDITION : + return new AnyViolationCondition(violationTypesFilter); + case AT_LEAST_ONE_VIOLATION_CONDITION : + return new AtLeastOneViolationCondition(subjectIdsStrList, violationTypesFilter); + default: + throw new PowsyblException("Unsupported condition type " + conditionType); + } + } } diff --git a/java/src/main/java/com/powsybl/python/security/SecurityAnalysisContext.java b/java/src/main/java/com/powsybl/python/security/SecurityAnalysisContext.java index 53f9be1419..cb4114aed4 100644 --- a/java/src/main/java/com/powsybl/python/security/SecurityAnalysisContext.java +++ b/java/src/main/java/com/powsybl/python/security/SecurityAnalysisContext.java @@ -12,18 +12,22 @@ import com.powsybl.python.commons.CommonObjects; import com.powsybl.python.contingency.ContingencyContainerImpl; import com.powsybl.security.*; +import com.powsybl.security.action.*; import com.powsybl.security.detectors.DefaultLimitViolationDetector; import com.powsybl.security.monitor.StateMonitor; +import com.powsybl.security.strategy.OperatorStrategy; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; +import java.util.*; /** * @author Geoffroy Jamgotchian {@literal } */ class SecurityAnalysisContext extends ContingencyContainerImpl { + private final List actions = new ArrayList<>(); + + private final List operatorStrategies = new ArrayList<>(); + private final List monitors = new ArrayList<>(); SecurityAnalysisResult run(Network network, SecurityAnalysisParameters securityAnalysisParameters, String provider, Reporter reporter) { @@ -38,14 +42,22 @@ SecurityAnalysisResult run(Network network, SecurityAnalysisParameters securityA new LimitViolationFilter(), new DefaultLimitViolationDetector(), Collections.emptyList(), - Collections.emptyList(), - Collections.emptyList(), + operatorStrategies, + actions, monitors, (reporter == null) ? Reporter.NO_OP : reporter ); return report.getResult(); } + void addAction(Action action) { + actions.add(action); + } + + void addOperatorStrategy(OperatorStrategy strategy) { + operatorStrategies.add(strategy); + } + void addMonitor(StateMonitor monitor) { monitors.add(monitor); } diff --git a/pypowsybl/_pypowsybl.pyi b/pypowsybl/_pypowsybl.pyi index e2c3297f68..ffac201f0a 100644 --- a/pypowsybl/_pypowsybl.pyi +++ b/pypowsybl/_pypowsybl.pyi @@ -108,6 +108,14 @@ class PostContingencyResult: @property def status(self) -> PostContingencyComputationStatus: ... +class OperatorStrategyResult: + @property + def operator_strategy_id(self) -> str: ... + @property + def limit_violations(self) -> LimitViolationArray: ... + @property + def status(self) -> PostContingencyComputationStatus: ... + class PreContingencyResult: @property def limit_violations(self) -> LimitViolationArray: ... @@ -117,6 +125,12 @@ class PreContingencyResult: class PostContingencyResultArray: def __iter__(self) -> Iterator: ... def __len__(self) -> int: ... + def __getitem__(self) -> PostContingencyResult: ... + +class OperatorStrategyResultArray: + def __iter__(self) -> Iterator: ... + def __len__(self) -> int: ... + def __getitem__(self) -> OperatorStrategyResult: ... class ElementType: __members__: ClassVar[Dict[str, ElementType]] = ... # read-only @@ -249,9 +263,16 @@ class NadParameters: class LimitType: __members__: ClassVar[Dict[str, LimitType]] = ... # read-only + ACTIVE_POWER: ClassVar[LimitType] = ... + APPARENT_POWER: ClassVar[LimitType] = ... CURRENT: ClassVar[LimitType] = ... - HIGH_VOLTAGE: ClassVar[LimitType] = ... LOW_VOLTAGE: ClassVar[LimitType] = ... + HIGH_VOLTAGE: ClassVar[LimitType] = ... + LOW_VOLTAGE_ANGLE: ClassVar[LimitType] = ... + HIGH_VOLTAGE_ANGLE: ClassVar[LimitType] = ... + LOW_SHORT_CIRCUIT_CURRENT: ClassVar[LimitType] = ... + HIGH_SHORT_CIRCUIT_CURRENT: ClassVar[LimitType] = ... + OTHER: ClassVar[LimitType] = ... def __init__(self, arg0: int) -> None: ... def __eq__(self, arg0: object) -> bool: ... def __getstate__(self) -> int: ... @@ -286,6 +307,7 @@ class LimitViolation: class LimitViolationArray: def __iter__(self) -> Iterator: ... def __len__(self) -> int: ... + def __getitem__(self) -> LimitViolation: ... class LoadFlowComponentResult: @property @@ -306,6 +328,7 @@ class LoadFlowComponentResult: class LoadFlowComponentResultArray: def __iter__(self) -> Iterator: ... def __len__(self) -> int: ... + def __getitem__(self) -> LoadFlowComponentResult: ... class LoadFlowComponentStatus: __members__: ClassVar[Dict[str, LoadFlowComponentStatus]] = ... # read-only @@ -419,6 +442,7 @@ class Series: class SeriesArray: def __iter__(self) -> Iterator: ... def __len__(self) -> int: ... + def __getitem__(self) -> Series: ... class SeriesMetadata: def __init__(self, arg0: str, arg1: int, arg2: bool, arg3: bool, arg4: bool) -> None: ... @@ -574,6 +598,44 @@ class DefaultXnecProvider: @property def name(self) -> str: ... +class ConditionType: + __members__: ClassVar[Dict[str, ConditionType]] = ... # read-only + TRUE_CONDITION: ClassVar[ConditionType] = ... + ALL_VIOLATION_CONDITION: ClassVar[ConditionType] = ... + ANY_VIOLATION_CONDITION: ClassVar[ConditionType] = ... + AT_LEAST_ONE_VIOLATION_CONDITION: ClassVar[ConditionType] = ... + def __init__(self, arg0: int) -> None: ... + def __eq__(self, arg0: object) -> bool: ... + def __getstate__(self) -> int: ... + def __hash__(self) -> int: ... + def __index__(self) -> int: ... + def __int__(self) -> int: ... + def __ne__(self, arg0: object) -> bool: ... + def __setstate__(self, arg0: int) -> None: ... + @property + def name(self) -> str: ... + +class ViolationType: + __members__: ClassVar[Dict[str, ViolationType]] = ... # read-only + ACTIVE_POWER: ClassVar[ViolationType] = ... + APPARENT_POWER: ClassVar[ViolationType] = ... + CURRENT: ClassVar[ViolationType] = ... + LOW_VOLTAGE: ClassVar[ViolationType] = ... + HIGH_VOLTAGE: ClassVar[ViolationType] = ... + LOW_SHORT_CIRCUIT_CURRENT: ClassVar[ViolationType] = ... + HIGH_SHORT_CIRCUIT_CURRENT: ClassVar[ViolationType] = ... + OTHER: ClassVar[ViolationType] = ... + def __init__(self, arg0: int) -> None: ... + def __eq__(self, arg0: object) -> bool: ... + def __getstate__(self) -> int: ... + def __hash__(self) -> int: ... + def __index__(self) -> int: ... + def __int__(self) -> int: ... + def __ne__(self, arg0: object) -> bool: ... + def __setstate__(self, arg0: int) -> None: ... + @property + def name(self) -> str: ... + class ShortCircuitStudyType: __members__: ClassVar[Dict[str, ShortCircuitStudyType]] = ... # read-only SUB_TRANSIENT: ClassVar[ShortCircuitStudyType] = ... @@ -596,9 +658,13 @@ class ShortCircuitAnalysisParameters: provider_parameters_values: List[str] def __init__(self) -> None: ... - def add_contingency(analysis_context: JavaHandle, contingency_id: str, elements_ids: List[str]) -> None: ... def add_monitored_elements(security_analysis_context: JavaHandle, contingency_context_type: ContingencyContextType, branch_ids: List[str], voltage_level_ids: List[str], three_windings_transformer_ids: List[str], contingency_ids: List[str]) -> None: ... +def add_load_active_power_action(security_analysis_context: JavaHandle, action_id: str, load_id: str, is_relative: bool, active_power: float) -> None: ... +def add_load_reactive_power_action(security_analysis_context: JavaHandle, action_id: str, load_id: str, is_relative: bool, reactive_power: float) -> None: ... +def add_generator_active_power_action(security_analysis_context: JavaHandle, action_id: str, generator_id: str, is_relative: bool, active_power: float) -> None: ... +def add_switch_action(security_analysis_context: JavaHandle, action_id: str, switch_id: str, open: bool) -> None: ... +def add_operator_strategy(security_analysis_context: JavaHandle, operator_strategy_id: str, contingency_id: str, action_ids: List[str], condition_type: ConditionType, violation_subject_ids: List[str], violation_types: List[ViolationType]) -> None: ... def clone_variant(network: JavaHandle, src: str, variant: str, may_overwrite: bool) -> None: ... def create_dataframe(columns_values: list, columns_names: List[str], columns_types: List[int], is_index: List[bool]) -> Dataframe: ... def create_element(network: JavaHandle, dataframes: List[Optional[Dataframe]], element_type: ElementType) -> None: ... @@ -639,6 +705,7 @@ def get_node_breaker_view_nodes(network: JavaHandle, voltage_level: str) -> Seri def get_node_breaker_view_switches(network: JavaHandle, voltage_level: str) -> SeriesArray: ... def get_reference_matrix(sensitivity_analysis_result_context: JavaHandle, matrix_id: str, contingency_id: str) -> Matrix: ... def get_post_contingency_results(result: JavaHandle) -> PostContingencyResultArray: ... +def get_operator_strategy_results(result: JavaHandle) -> OperatorStrategyResultArray: ... def get_pre_contingency_result(result: JavaHandle) -> PreContingencyResult: ... def get_network_elements_dataframe_metadata(element_type: ElementType) -> List[SeriesMetadata]: ... def get_network_elements_creation_dataframes_metadata(element_type: ElementType) -> List[List[SeriesMetadata]]: ... diff --git a/pypowsybl/security/impl/security.py b/pypowsybl/security/impl/security.py index d386ff228d..a353baa3f9 100644 --- a/pypowsybl/security/impl/security.py +++ b/pypowsybl/security/impl/security.py @@ -7,7 +7,7 @@ from typing import Union, List import pypowsybl.loadflow from pypowsybl import _pypowsybl -from pypowsybl._pypowsybl import ContingencyContextType +from pypowsybl._pypowsybl import ContingencyContextType, ConditionType, ViolationType from pypowsybl._pypowsybl import PostContingencyComputationStatus as ComputationStatus from pypowsybl.network import Network from pypowsybl.report import Reporter @@ -135,3 +135,34 @@ def add_postcontingency_monitored_elements(self, contingency_ids: Union[List[str """ return self.add_monitored_elements(ContingencyContextType.SPECIFIC, contingency_ids, branch_ids, voltage_level_ids, three_windings_transformer_ids) + + def add_load_active_power_action(self, action_id: str, load_id: str, is_relative: bool, active_power: float) -> None: + """ + """ + _pypowsybl.add_load_active_power_action(self._handle, action_id, load_id, is_relative, active_power) + + def add_load_reactive_power_action(self, action_id: str, load_id: str, is_relative: bool, reactive_power: float) -> None: + """ + """ + _pypowsybl.add_load_reactive_power_action(self._handle, action_id, load_id, is_relative, reactive_power) + + def add_generator_active_power_action(self, action_id: str, generator_id: str, is_relative: bool, active_power: float) -> None: + """ + """ + _pypowsybl.add_generator_active_power_action(self._handle, action_id, generator_id, is_relative, active_power) + + def add_switch_action(self, action_id: str, switch_id: str, open: bool) -> None: + """ + """ + _pypowsybl.add_switch_action(self._handle, action_id, switch_id, open) + + def add_operator_strategy(self, operator_strategy_id: str, contingency_id: str, action_ids: List[str], + condition_type: ConditionType = ConditionType.TRUE_CONDITION, violation_subject_ids: List[str] = None, + violation_types: List[ViolationType] = None) -> None: + """ + """ + if violation_types is None: + violation_types = [] + if violation_subject_ids is None: + violation_subject_ids = [] + _pypowsybl.add_operator_strategy(self._handle, operator_strategy_id, contingency_id, action_ids, condition_type, violation_subject_ids, violation_types) diff --git a/pypowsybl/security/impl/security_analysis_result.py b/pypowsybl/security/impl/security_analysis_result.py index fd4ea26db2..9fc91eef1d 100644 --- a/pypowsybl/security/impl/security_analysis_result.py +++ b/pypowsybl/security/impl/security_analysis_result.py @@ -8,7 +8,7 @@ import pandas as pd from prettytable import PrettyTable from pypowsybl import _pypowsybl -from pypowsybl._pypowsybl import PreContingencyResult, PostContingencyResult +from pypowsybl._pypowsybl import PreContingencyResult, PostContingencyResult, OperatorStrategyResult, LimitViolationArray from pypowsybl.utils import create_data_frame_from_series_array @@ -21,10 +21,14 @@ def __init__(self, handle: _pypowsybl.JavaHandle): self._handle = handle self._pre_contingency_result = _pypowsybl.get_pre_contingency_result(self._handle) post_contingency_results = _pypowsybl.get_post_contingency_results(self._handle) + operator_strategy_results = _pypowsybl.get_operator_strategy_results(self._handle) self._post_contingency_results = {} + self._operator_strategy_results = {} for result in post_contingency_results: if result.contingency_id: self._post_contingency_results[result.contingency_id] = result + for result in operator_strategy_results: + self._operator_strategy_results[result.operator_strategy_id] = result self._limit_violations = create_data_frame_from_series_array(_pypowsybl.get_limit_violations(self._handle)) @property @@ -53,14 +57,33 @@ def find_post_contingency_result(self, contingency_id: str) -> PostContingencyRe raise KeyError(f'Contingency {contingency_id} not found') return result + @property + def operator_strategy_results(self) -> Dict[str, OperatorStrategyResult]: + """ + Results for the operator strategies, as a dictionary operator strategy ID -> result. + """ + + return self._operator_strategy_results + + def find_operator_strategy_results(self, operator_strategy_id: str) -> OperatorStrategyResult: + """ + Result for the specified operator strategy + + Returns: + Result for the specified operator strategy. + """ + result = self._operator_strategy_results[operator_strategy_id] + if not result: + raise KeyError(f'Operator strategy {operator_strategy_id} not found') + return result + def get_table(self) -> PrettyTable: table = PrettyTable() - table.field_names = ["Contingency ID", "Status", "Equipment ID", "Equipment name", "Limit type", "Limit", + table.field_names = ["Contingency ID", "Operator strategy ID", "Status", "Equipment ID", "Equipment name", "Limit type", "Limit", "Limit name", "Acceptable duration", "Limit reduction", "Value", "Side"] - for contingency_id, post_contingency_result in self._post_contingency_results.items(): - table.add_row([contingency_id, post_contingency_result.status.name, '', '', '', '', '', '', '', '', '']) - for limit_violation in post_contingency_result.limit_violations: - table.add_row(['', '', + def print_limit_violation(limit_violations: LimitViolationArray) -> None: + for limit_violation in limit_violations: + table.add_row(['', '', '', limit_violation.subject_id, limit_violation.subject_name, limit_violation.limit_type.name, @@ -70,6 +93,13 @@ def get_table(self) -> PrettyTable: limit_violation.limit_reduction, f'{limit_violation.value:.1f}', limit_violation.side.name]) + for contingency_id, post_contingency_result in self._post_contingency_results.items(): + table.add_row([contingency_id, '', post_contingency_result.status.name, '', '', '', '', '', '', '', '', '']) + print_limit_violation(post_contingency_result.limit_violations) + + for operator_strategy_id, operator_strategy_result in self._operator_strategy_results.items(): + table.add_row(['', operator_strategy_id, operator_strategy_result.status.name, '', '', '', '', '', '', '', '', '']) + print_limit_violation(operator_strategy_result.limit_violations) return table @property diff --git a/pypowsybl/security/impl/util.py b/pypowsybl/security/impl/util.py index d79d765c8c..2f76c4812a 100644 --- a/pypowsybl/security/impl/util.py +++ b/pypowsybl/security/impl/util.py @@ -7,7 +7,7 @@ from typing import List from pypowsybl import _pypowsybl -from pypowsybl._pypowsybl import LimitViolation, PreContingencyResult, PostContingencyResult +from pypowsybl._pypowsybl import LimitViolation, PreContingencyResult, PostContingencyResult, OperatorStrategyResult from .security import SecurityAnalysis @@ -103,5 +103,13 @@ def _limit_violation_repr(self: LimitViolation) -> str: f", side={self.side.name}" \ f")" +def _operator_strategy_result_repr(self: OperatorStrategyResult) -> str: + return f"{self.__class__.__name__}(" \ + f"operator_strategy_id={self.operator_strategy_id!r}" \ + f", status={self.status.name}" \ + f", limit_violations=[{len(self.limit_violations)}]" \ + f")" + +OperatorStrategyResult.__repr__ = _operator_strategy_result_repr # type: ignore LimitViolation.__repr__ = _limit_violation_repr # type: ignore diff --git a/tests/test_security_analysis.py b/tests/test_security_analysis.py index 6acd24f364..a7e4c94d32 100644 --- a/tests/test_security_analysis.py +++ b/tests/test_security_analysis.py @@ -9,6 +9,7 @@ import pypowsybl as pp import pandas as pd import pypowsybl.report as rp +from pypowsybl._pypowsybl import ConditionType @pytest.fixture(autouse=True) @@ -90,20 +91,20 @@ def test_monitored_elements(): bus_results = sa_result.bus_results branch_results = sa_result.branch_results - assert bus_results.index.to_frame().columns.tolist() == ['contingency_id', 'voltage_level_id', 'bus_id'] + assert bus_results.index.to_frame().columns.tolist() == ['contingency_id', 'operator_strategy_id', 'voltage_level_id', 'bus_id'] assert bus_results.columns.tolist() == ['v_mag', 'v_angle'] assert len(bus_results) == 3 - assert bus_results.loc['', 'VLHV2', 'NHV2']['v_mag'] == pytest.approx(389.95, abs=1e-2) - assert bus_results.loc['NGEN_NHV1', 'VLHV2', 'NHV2']['v_mag'] == pytest.approx(569.038987, abs=1e-2) - assert bus_results.loc['NHV1_NHV2_1', 'VLHV2', 'NHV2']['v_mag'] == pytest.approx(366.58, abs=1e-2) + assert bus_results.loc['', '', 'VLHV2', 'NHV2']['v_mag'] == pytest.approx(389.95, abs=1e-2) + assert bus_results.loc['NGEN_NHV1', '', 'VLHV2', 'NHV2']['v_mag'] == pytest.approx(569.038987, abs=1e-2) + assert bus_results.loc['NHV1_NHV2_1', '', 'VLHV2', 'NHV2']['v_mag'] == pytest.approx(366.58, abs=1e-2) - assert branch_results.index.to_frame().columns.tolist() == ['contingency_id', 'branch_id'] + assert branch_results.index.to_frame().columns.tolist() == ['contingency_id', 'operator_strategy_id', 'branch_id'] assert branch_results.columns.tolist() == ['p1', 'q1', 'i1', 'p2', 'q2', 'i2', 'flow_transfer'] assert len(branch_results) == 4 - assert branch_results.loc['', 'NHV1_NHV2_2']['p1'] == pytest.approx(302.44, abs=1e-2) - assert branch_results.loc['NGEN_NHV1', 'NHV1_NHV2_1']['p1'] == pytest.approx(301.05, abs=1e-2) - assert branch_results.loc['NGEN_NHV1', 'NHV1_NHV2_2']['p1'] == pytest.approx(301.05, abs=1e-2) - assert branch_results.loc['NHV1_NHV2_1', 'NHV1_NHV2_2']['p1'] == pytest.approx(610.56, abs=1e-2) + assert branch_results.loc['', '', 'NHV1_NHV2_2']['p1'] == pytest.approx(302.44, abs=1e-2) + assert branch_results.loc['NGEN_NHV1', '', 'NHV1_NHV2_1']['p1'] == pytest.approx(301.05, abs=1e-2) + assert branch_results.loc['NGEN_NHV1', '', 'NHV1_NHV2_2']['p1'] == pytest.approx(301.05, abs=1e-2) + assert branch_results.loc['NHV1_NHV2_1', '', 'NHV1_NHV2_2']['p1'] == pytest.approx(610.56, abs=1e-2) def test_flow_transfer(): @@ -113,8 +114,8 @@ def test_flow_transfer(): sa.add_monitored_elements(branch_ids=['NHV1_NHV2_1', 'NHV1_NHV2_2']) sa_result = sa.run_ac(n) branch_results = sa_result.branch_results - assert branch_results.loc['NHV1_NHV2_1', 'NHV1_NHV2_2']['flow_transfer'] == pytest.approx(1.01876, abs=1e-5) - assert branch_results.loc['NHV1_NHV2_2', 'NHV1_NHV2_1']['flow_transfer'] == pytest.approx(1.01876, abs=1e-5) + assert branch_results.loc['NHV1_NHV2_1', '', 'NHV1_NHV2_2']['flow_transfer'] == pytest.approx(1.01876, abs=1e-5) + assert branch_results.loc['NHV1_NHV2_2', '', 'NHV1_NHV2_1']['flow_transfer'] == pytest.approx(1.01876, abs=1e-5) def test_dc_analysis(): @@ -267,6 +268,78 @@ def test_different_equipment_contingency(): assert 'Twt contingency' in sa_result.post_contingency_results.keys() assert 'Switch contingency' in sa_result.post_contingency_results.keys() +def test_load_action(): + n = pp.network.create_eurostag_tutorial_example1_network() + sa = pp.security.create_analysis() + sa.add_single_element_contingency('NHV1_NHV2_1', 'Line contingency') + sa.add_load_active_power_action('LoadAction1', 'LOAD', False, 750.0) + sa.add_operator_strategy('OperatorStrategy1', 'Line contingency', ['LoadAction1']) + sa_result = sa.run_ac(n) + assert 'Line contingency' in sa_result.post_contingency_results.keys() + assert len(sa_result.find_post_contingency_result('Line contingency').limit_violations) == 2 + assert 'OperatorStrategy1' in sa_result.operator_strategy_results.keys() + assert len(sa_result.find_operator_strategy_results('OperatorStrategy1').limit_violations) == 3 + assert str(sa_result.get_table()) == """+------------------+----------------------+-----------+--------------+----------------+-------------+--------+------------+---------------------+-----------------+--------+------+ +| Contingency ID | Operator strategy ID | Status | Equipment ID | Equipment name | Limit type | Limit | Limit name | Acceptable duration | Limit reduction | Value | Side | ++------------------+----------------------+-----------+--------------+----------------+-------------+--------+------------+---------------------+-----------------+--------+------+ +| Line contingency | | CONVERGED | | | | | | | | | | +| | | | NHV1_NHV2_2 | | CURRENT | 500.0 | permanent | 2147483647 | 1.0 | 1047.8 | TWO | +| | | | VLHV1 | | LOW_VOLTAGE | 400.0 | | 2147483647 | 1.0 | 398.3 | NONE | +| | OperatorStrategy1 | CONVERGED | | | | | | | | | | +| | | | NHV1_NHV2_2 | | CURRENT | 1200.0 | 20' | 60 | 1.0 | 1316.2 | ONE | +| | | | NHV1_NHV2_2 | | CURRENT | 500.0 | permanent | 2147483647 | 1.0 | 1355.4 | TWO | +| | | | VLHV1 | | LOW_VOLTAGE | 400.0 | | 2147483647 | 1.0 | 394.1 | NONE | ++------------------+----------------------+-----------+--------------+----------------+-------------+--------+------------+---------------------+-----------------+--------+------+""" + +def test_load_action_with_any_violation_condition(): + n = pp.network.create_four_substations_node_breaker_network() + sa = pp.security.create_analysis() + sa.add_single_element_contingency('LINE_S3S4', 'Line contingency') + sa.add_load_active_power_action('LoadAction1', 'LD6', False, 750.0) + + #LINE_S3S4 does not generate limit violations, OperatorStrategy1 should be applied but not OperatorStrategy2 + sa.add_operator_strategy('OperatorStrategy1', 'Line contingency', ['LoadAction1'], ConditionType.TRUE_CONDITION) + sa.add_operator_strategy('OperatorStrategy2', 'Line contingency', ['LoadAction1'], ConditionType.ANY_VIOLATION_CONDITION) + sa_result = sa.run_ac(n) + assert 'Line contingency' in sa_result.post_contingency_results.keys() + assert len(sa_result.find_post_contingency_result('Line contingency').limit_violations) == 0 + + assert 'OperatorStrategy1' in sa_result.operator_strategy_results.keys() + assert 'OperatorStrategy2' not in sa_result.operator_strategy_results.keys() + +def test_load_action_with_all_violation_condition(): + n = pp.network.create_eurostag_tutorial_example1_network() + sa = pp.security.create_analysis() + sa.add_single_element_contingency('NHV1_NHV2_1', 'Line contingency') + sa.add_load_active_power_action('LoadAction1', 'LOAD', False, 750.0) + + #OperatorStrategy1 will be applied because we have a violation on NHV1_NHV2_2 after contingency + #OperatorStrategy2 should not be applied because there is not violation on UnknownLine + sa.add_operator_strategy('OperatorStrategy1', 'Line contingency', ['LoadAction1'], ConditionType.ALL_VIOLATION_CONDITION , ['NHV1_NHV2_2']) + sa.add_operator_strategy('OperatorStrategy2', 'Line contingency', ['LoadAction1'], ConditionType.ALL_VIOLATION_CONDITION, ['NHV1_NHV2_2', 'UnknownLine']) + sa_result = sa.run_ac(n) + assert 'Line contingency' in sa_result.post_contingency_results.keys() + assert len(sa_result.find_post_contingency_result('Line contingency').limit_violations) == 2 + assert 'OperatorStrategy1' in sa_result.operator_strategy_results.keys() + assert len(sa_result.find_operator_strategy_results('OperatorStrategy1').limit_violations) == 3 + assert 'OperatorStrategy2' not in sa_result.operator_strategy_results.keys() + +def test_switch_action(): + n = pp.network.create_four_substations_node_breaker_network() + sa = pp.security.create_analysis() + sa.add_single_element_contingency(element_id='S4VL1_BBS_LD6_DISCONNECTOR', contingency_id='Breaker contingency') + sa.add_switch_action(action_id='SwitchAction', switch_id='S4VL1_BBS_LD6_DISCONNECTOR', open=False) + sa.add_operator_strategy(operator_strategy_id='OperatorStrategy1', contingency_id='Breaker contingency', action_ids=['SwitchAction'], condition_type=ConditionType.TRUE_CONDITION) + sa.add_monitored_elements(branch_ids=['LINE_S3S4']) + sa_result = sa.run_ac(n) + df = sa_result.branch_results + + #Contingency open a switch, then action close it + #Check p1 on line is the same pre contingency and post remedial action + assert df.loc['', '', 'LINE_S3S4']['p1'] == pytest.approx(2.400036e+02, abs=1e-2) + assert df.loc['Breaker contingency', '', 'LINE_S3S4']['p1'] == pytest.approx(0.0, abs=1e-2) + assert df.loc['Breaker contingency', 'OperatorStrategy1', 'LINE_S3S4']['p1'] == pytest.approx(2.400036e+02, abs=1e-2) + def test_tie_line_contingency(): n = pp.network._create_network("eurostag_tutorial_example1_with_tie_line") sa = pp.security.create_analysis()