diff --git a/java/src/main/java/com/powsybl/dataframe/dynamic/DynamicModelSeriesUtils.java b/java/src/main/java/com/powsybl/dataframe/dynamic/DynamicModelSeriesUtils.java index 6e2cb190b..624d655c9 100644 --- a/java/src/main/java/com/powsybl/dataframe/dynamic/DynamicModelSeriesUtils.java +++ b/java/src/main/java/com/powsybl/dataframe/dynamic/DynamicModelSeriesUtils.java @@ -11,10 +11,7 @@ import com.powsybl.dataframe.update.StringSeries; import com.powsybl.dataframe.update.UpdatingDataframe; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.function.Consumer; /** @@ -22,10 +19,13 @@ */ public final class DynamicModelSeriesUtils { - private DynamicModelSeriesUtils(){ + private DynamicModelSeriesUtils() { } public static Map> createIdMap(UpdatingDataframe dataframe, String indexColumn, String idColumn) { + if (dataframe.getRowCount() == 0) { + return Collections.emptyMap(); + } StringSeries joinIds = dataframe.getStrings(indexColumn); if (joinIds == null) { throw new PowsyblException("Join dataframe: %s column is not set".formatted(indexColumn)); diff --git a/java/src/main/java/com/powsybl/dataframe/dynamic/adders/DynamicModelDataframeConstants.java b/java/src/main/java/com/powsybl/dataframe/dynamic/adders/DynamicModelDataframeConstants.java index b6b0fbf12..c3524f088 100644 --- a/java/src/main/java/com/powsybl/dataframe/dynamic/adders/DynamicModelDataframeConstants.java +++ b/java/src/main/java/com/powsybl/dataframe/dynamic/adders/DynamicModelDataframeConstants.java @@ -26,7 +26,7 @@ public final class DynamicModelDataframeConstants { public static final String GENERATOR = "generator"; public static final String TRANSFORMER = "transformer"; public static final String SIDE = "side"; - public static final String U_MEASUREMENTS = "u_measurements"; + public static final String MEASUREMENT_POINT_ID = "measurement_point_id"; public static final String TRANSFORMER_ID = "transformer_id"; public static final String START_TIME = "start_time"; public static final String DISCONNECT_ONLY = "disconnect_only"; diff --git a/java/src/main/java/com/powsybl/dataframe/dynamic/adders/TapChangerBlockingAutomationSystemAdder.java b/java/src/main/java/com/powsybl/dataframe/dynamic/adders/TapChangerBlockingAutomationSystemAdder.java index c2247fa9c..062fad9c2 100644 --- a/java/src/main/java/com/powsybl/dataframe/dynamic/adders/TapChangerBlockingAutomationSystemAdder.java +++ b/java/src/main/java/com/powsybl/dataframe/dynamic/adders/TapChangerBlockingAutomationSystemAdder.java @@ -9,9 +9,6 @@ import com.powsybl.commons.report.ReportNode; import com.powsybl.dataframe.SeriesMetadata; -import com.powsybl.dataframe.dynamic.PersistentStringSeries; -import com.powsybl.dataframe.network.adders.SeriesUtils; -import com.powsybl.dataframe.update.StringSeries; import com.powsybl.dataframe.update.UpdatingDataframe; import com.powsybl.dynawo.builders.ModelInfo; import com.powsybl.dynawo.models.automationsystems.TapChangerBlockingAutomationSystemBuilder; @@ -32,14 +29,25 @@ public class TapChangerBlockingAutomationSystemAdder implements DynamicMappingAd private static final List METADATA = List.of( SeriesMetadata.stringIndex(DYNAMIC_MODEL_ID), SeriesMetadata.strings(PARAMETER_SET_ID), - SeriesMetadata.strings(MODEL_NAME), - SeriesMetadata.strings(U_MEASUREMENTS)); + SeriesMetadata.strings(MODEL_NAME)); private static final List TRANSFORMER_METADATA = List.of( SeriesMetadata.stringIndex(DYNAMIC_MODEL_ID), SeriesMetadata.strings(TRANSFORMER_ID)); - private static final List> METADATA_LIST = List.of(METADATA, TRANSFORMER_METADATA); + private static final List U_MEASUREMENT_METADATA = List.of( + SeriesMetadata.stringIndex(DYNAMIC_MODEL_ID), + SeriesMetadata.strings(MEASUREMENT_POINT_ID)); + + private static final List> METADATA_LIST = List.of(METADATA, + TRANSFORMER_METADATA, + U_MEASUREMENT_METADATA, + U_MEASUREMENT_METADATA, + U_MEASUREMENT_METADATA, + U_MEASUREMENT_METADATA, + U_MEASUREMENT_METADATA); + + private static final int MAX_MEASUREMENTS = 5; @Override public List> getMetadata() { @@ -53,12 +61,17 @@ public Collection getSupportedModels() { @Override public void addElements(PythonDynamicModelsSupplier modelMapping, List dataframes) { - if (dataframes.size() != 2) { - throw new IllegalArgumentException("Expected 2 dataframes: one for TCB, one for transformers."); + if (dataframes.size() < 3) { + throw new IllegalArgumentException("Expected at least 3 dataframes: one for TCB, one for transformers and one for measurement points."); } UpdatingDataframe dataframe = dataframes.get(0); - UpdatingDataframe tfo_dataframe = dataframes.get(1); - DynamicModelSeries series = new TapChangerBlockingSeries(dataframe, tfo_dataframe); + UpdatingDataframe tfoDataframe = dataframes.get(1); + List measurementDataframe = new ArrayList<>(MAX_MEASUREMENTS); + for (int i = 2; i < dataframes.size(); i++) { + measurementDataframe.add(dataframes.get(i)); + } + + DynamicModelSeries series = new TapChangerBlockingSeries(dataframe, tfoDataframe, measurementDataframe); for (int row = 0; row < dataframe.getRowCount(); row++) { modelMapping.addModel(series.getModelSupplier(row)); } @@ -66,21 +79,25 @@ public void addElements(PythonDynamicModelsSupplier modelMapping, List { - private final StringSeries uMeasurements; private final Map> transformers; + private final List>> uMeasurements = new ArrayList<>(MAX_MEASUREMENTS); - TapChangerBlockingSeries(UpdatingDataframe tcbDataframe, UpdatingDataframe tfoDataframe) { + TapChangerBlockingSeries(UpdatingDataframe tcbDataframe, UpdatingDataframe tfoDataframe, List measurementDataframe) { super(tcbDataframe); - this.uMeasurements = PersistentStringSeries.copyOf(tcbDataframe, U_MEASUREMENTS); this.transformers = createIdMap(tfoDataframe, DYNAMIC_MODEL_ID, TRANSFORMER_ID); + measurementDataframe.forEach(mdf -> uMeasurements.add(createIdMap(mdf, DYNAMIC_MODEL_ID, MEASUREMENT_POINT_ID))); } @Override protected void applyOnBuilder(int row, TapChangerBlockingAutomationSystemBuilder builder) { super.applyOnBuilder(row, builder); - SeriesUtils.applyIfPresent(uMeasurements, row, builder::uMeasurements); String tcbId = dynamicModelIds.get(row); applyIfPresent(transformers, tcbId, builder::transformers); + List> id2dList = new ArrayList<>(); + uMeasurements.forEach(idMap -> applyIfPresent(idMap, tcbId, id2dList::add)); + if (!id2dList.isEmpty()) { + builder.uMeasurements(id2dList.toArray(new Collection[0])); + } } @Override diff --git a/java/src/test/java/com/powsybl/dataframe/dynamic/adders/DynamicModelsAdderTest.java b/java/src/test/java/com/powsybl/dataframe/dynamic/adders/DynamicModelsAdderTest.java index debc27dce..305b4755c 100644 --- a/java/src/test/java/com/powsybl/dataframe/dynamic/adders/DynamicModelsAdderTest.java +++ b/java/src/test/java/com/powsybl/dataframe/dynamic/adders/DynamicModelsAdderTest.java @@ -11,6 +11,7 @@ import com.powsybl.dataframe.update.TestStringSeries; import com.powsybl.dynawo.models.AbstractPureDynamicBlackBoxModel; import com.powsybl.dynawo.models.TransformerSide; +import com.powsybl.dynawo.models.automationsystems.TapChangerBlockingAutomationSystem; import com.powsybl.iidm.network.Network; import com.powsybl.iidm.network.TwoSides; import com.powsybl.iidm.network.test.EurostagTutorialExample1Factory; @@ -18,6 +19,7 @@ import com.powsybl.iidm.network.test.SvcTestCaseFactory; import com.powsybl.python.commons.PyPowsyblApiHeader; import com.powsybl.python.dynamic.PythonDynamicModelsSupplier; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -77,7 +79,8 @@ void testAutomationSystemAdders(PyPowsyblApiHeader.DynamicMappingType mappingTyp assertThat(dynamicModelsSupplier.get(network)).satisfiesExactly( model1 -> assertThat(model1).hasFieldOrPropertyWithValue("dynamicModelId", dynamicModelId) .isInstanceOf(AbstractPureDynamicBlackBoxModel.class), - model2 -> assertThat(model2).hasFieldOrPropertyWithValue("dynamicModelId", dynamicModelId + DEFAULT_SUFFIX)); + model2 -> assertThat(model2).hasFieldOrPropertyWithValue("dynamicModelId", dynamicModelId + DEFAULT_SUFFIX) + .isInstanceOf(AbstractPureDynamicBlackBoxModel.class)); } @Test @@ -88,17 +91,30 @@ void testTapChangerBlockingAdders() { String defaultDynamicModelId = dynamicModelId + DEFAULT_SUFFIX; // Setup Tcb df setupDataFrame(dataframe, dynamicModelId, expectedModelName); - dataframe.addSeries(U_MEASUREMENTS, false, createTwoRowsSeries("NHV1")); // Setup Tfo df DefaultUpdatingDataframe tfoDataFrame = new DefaultUpdatingDataframe(3); tfoDataFrame.addSeries(DYNAMIC_MODEL_ID, true, new TestStringSeries(dynamicModelId, dynamicModelId, defaultDynamicModelId)); tfoDataFrame.addSeries(TRANSFORMER_ID, false, new TestStringSeries("NGEN_NHV1", "NHV2_NLOAD", "NHV2_NLOAD")); - DynamicMappingHandler.addElements(TAP_CHANGER_BLOCKING, dynamicModelsSupplier, List.of(dataframe, tfoDataFrame)); + // Setup measurement points df + DefaultUpdatingDataframe m1DataFrame = new DefaultUpdatingDataframe(3); + m1DataFrame.addSeries(DYNAMIC_MODEL_ID, true, new TestStringSeries(dynamicModelId, dynamicModelId, defaultDynamicModelId)); + m1DataFrame.addSeries(MEASUREMENT_POINT_ID, false, new TestStringSeries("BBS_NGEN", "NGEN", "NHV1")); + DefaultUpdatingDataframe m2DataFrame = new DefaultUpdatingDataframe(2); + m2DataFrame.addSeries(DYNAMIC_MODEL_ID, true, new TestStringSeries(dynamicModelId, dynamicModelId)); + m2DataFrame.addSeries(MEASUREMENT_POINT_ID, false, new TestStringSeries("OLD_NLOAD_ID", "NLOAD")); + DynamicMappingHandler.addElements(TAP_CHANGER_BLOCKING, dynamicModelsSupplier, List.of(dataframe, tfoDataFrame, m1DataFrame, m2DataFrame)); assertThat(dynamicModelsSupplier.get(network)).satisfiesExactly( model1 -> assertThat(model1).hasFieldOrPropertyWithValue("dynamicModelId", dynamicModelId) - .isInstanceOf(AbstractPureDynamicBlackBoxModel.class), - model2 -> assertThat(model2).hasFieldOrPropertyWithValue("dynamicModelId", defaultDynamicModelId)); + .isInstanceOf(TapChangerBlockingAutomationSystem.class) + .extracting("uMeasurements") + .asInstanceOf(InstanceOfAssertFactories.LIST) + .containsExactly(network.getBusBreakerView().getBus("NGEN"), network.getBusBreakerView().getBus("NLOAD")), + model2 -> assertThat(model2).hasFieldOrPropertyWithValue("dynamicModelId", defaultDynamicModelId) + .isInstanceOf(TapChangerBlockingAutomationSystem.class) + .extracting("uMeasurements") + .asInstanceOf(InstanceOfAssertFactories.LIST) + .containsExactly(network.getBusBreakerView().getBus("NHV1"))); } @Test diff --git a/pypowsybl/dynamic/__init__.py b/pypowsybl/dynamic/__init__.py index 280a7f077..f58c84c15 100644 --- a/pypowsybl/dynamic/__init__.py +++ b/pypowsybl/dynamic/__init__.py @@ -8,4 +8,4 @@ from .impl.event_mapping import EventMapping, EventMappingType from .impl.simulation_result import SimulationResult from .impl.simulation import Simulation -from .impl.model_mapping import ModelMapping, DynamicMappingType \ No newline at end of file +from .impl.model_mapping import ModelMapping, DynamicMappingType diff --git a/pypowsybl/dynamic/impl/event_mapping.py b/pypowsybl/dynamic/impl/event_mapping.py index 8cc287530..1bf790f54 100644 --- a/pypowsybl/dynamic/impl/event_mapping.py +++ b/pypowsybl/dynamic/impl/event_mapping.py @@ -4,9 +4,9 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. # SPDX-License-Identifier: MPL-2.0 # +from typing import Optional from numpy.typing import ArrayLike from pandas import DataFrame -from typing import Optional from pypowsybl import _pypowsybl as _pp from pypowsybl._pypowsybl import EventMappingType # pylint: disable=protected-access from pypowsybl.utils import _add_index_to_kwargs, \ diff --git a/pypowsybl/dynamic/impl/model_mapping.py b/pypowsybl/dynamic/impl/model_mapping.py index 3f991842c..b979e80fa 100644 --- a/pypowsybl/dynamic/impl/model_mapping.py +++ b/pypowsybl/dynamic/impl/model_mapping.py @@ -837,27 +837,35 @@ def add_tap_changer_automation_system(self, df: DataFrame = None, **kwargs: Arra """ self._add_all_dynamic_mappings(DynamicMappingType.TAP_CHANGER, [df], **kwargs) - def add_tap_changer_blocking_automation_system(self, df: DataFrame, tfo_df: DataFrame) -> None: + def add_tap_changer_blocking_automation_system(self, df: DataFrame, tfo_df: DataFrame, mp1_df: DataFrame, + mp2_df: DataFrame = None, mp3_df: DataFrame = None, + mp5_df: DataFrame = None, mp4_df: DataFrame = None) -> None: """ Add a dynamic tap changer blocking automation system (not link to a network element) :Args: df: Primary attributes as a dataframe. tfo_df: Dataframe for transformer data. + mpN_df: Dataframes for a measurement point data, the automation system can handle up to 5 measurement points, + at least 1 measurement point is expected. For each measurement point dataframe, alternative points can be input + (for example bus or busbar section) the first energized element found in the network will be used Notes: - Valid attributes for thr primary dataframes are: + Valid attributes for the primary dataframes are: - **dynamic_model_id**: id of the tap changer blocking automation system - **parameter_set_id**: id of the parameter for this model given in the dynawo configuration - - **u_measurements**: id of the bus or busbar section used for the voltage measurement - **model_name**: name of the model used for the mapping (if none the default model will be used) - Valid attributes for thr primary dataframes are: + Valid attributes for the transformer dataframes are: - **dynamic_model_id**: id of the tap changer blocking automation system - **transformer_id**: id of a transformer controlled by the automation system + Valid attributes for the measurement point dataframes are: + - **dynamic_model_id**: id of the tap changer blocking automation system + - **measurement_point_id**: id of the bus or busbar section used for the voltage measurement + Examples: We need to provide 2 dataframes, 1 for tap changer blocking automation system basic data, and one for transformer data: @@ -874,9 +882,19 @@ def add_tap_changer_blocking_automation_system(self, df: DataFrame, tfo_df: Data data=[('DM_TCB', 'TFO1'), ('DM_TCB', 'TFO2'), ('DM_TCB', 'TFO3')]) - model_mapping.add_tap_changer_blocking_automation_system(df, tfo_df) + measurement1_df = pd.DataFrame.from_records( + index='dynamic_model_id', + columns=['dynamic_model_id', 'measurement_point_id'], + data=[('DM_TCB', 'B1'), + ('DM_TCB', 'BS1')]) + measurement2_df = pd.DataFrame.from_records( + index='dynamic_model_id', + columns=['dynamic_model_id', 'measurement_point_id'], + data=[('DM_TCB', 'B4')]) + model_mapping.add_tap_changer_blocking_automation_system(df, tfo_df, measurement1_df, measurement2_df) """ - self._add_all_dynamic_mappings(DynamicMappingType.TAP_CHANGER_BLOCKING, [df, tfo_df]) + dfs = [df, tfo_df, mp1_df, mp2_df, mp3_df, mp4_df, mp5_df] + self._add_all_dynamic_mappings(DynamicMappingType.TAP_CHANGER_BLOCKING, [DataFrame() if df is None else df for df in dfs]) def _add_all_dynamic_mappings(self, mapping_type: DynamicMappingType, mapping_dfs: List[Optional[DataFrame]], **kwargs: ArrayLike) -> None: metadata = _pp.get_dynamic_mappings_meta_data(mapping_type) diff --git a/tests/test_dynamic.py b/tests/test_dynamic.py index 0fb192271..ae225547d 100644 --- a/tests/test_dynamic.py +++ b/tests/test_dynamic.py @@ -16,9 +16,6 @@ def set_up(): def test_add_mapping(): - static_id = 'test_id' - dynamic_id = 'test_dynamic_id' - parameter_id = 'test_parameter' model_mapping = dyn.ModelMapping() # Equipments model_mapping.add_base_load(static_id='LOAD', parameter_set_id='lab', dynamic_model_id='DM_LOAD', model_name='LoadPQ') @@ -57,9 +54,6 @@ def test_add_mapping(): phase_shifter_id='PSI', model_name='PhaseShifterBlockingI') model_mapping.add_tap_changer_automation_system(dynamic_model_id='DM_TC', parameter_set_id='tc', static_id='LOAD', side='HIGH_VOLTAGE', model_name='TapChangerAutomaton') - model_mapping.add_tap_changer_blocking_automation_system(dynamic_model_id='DM_TCB', parameter_set_id='tcb', - transformers='TRA', u_measurements='BUS', - model_name='TapChangerBlockingAutomaton') # Equipment with default model name and dynamic id model_mapping.add_base_load(static_id='LOAD', parameter_set_id='lab') # Equipment model from Supported models @@ -91,15 +85,24 @@ def test_dynamic_dataframe(): tcb_df = pd.DataFrame.from_records( index='dynamic_model_id', - columns=['dynamic_model_id', 'parameter_set_id', 'u_measurements', 'model_name'], - data=[('DM_TCB', 'tcb', 'BUS', 'TapChangerBlockingAutomaton')]) + columns=['dynamic_model_id', 'parameter_set_id', 'model_name'], + data=[('DM_TCB', 'tcb', 'TapChangerBlockingAutomaton')]) tfo_df = pd.DataFrame.from_records( index='dynamic_model_id', columns=['dynamic_model_id', 'transformer_id'], data=[('DM_TCB', 'TFO1'), ('DM_TCB', 'TFO2'), ('DM_TCB', 'TFO3')]) - model_mapping.add_tap_changer_blocking_automation_system(tcb_df, tfo_df) + measurement1_df = pd.DataFrame.from_records( + index='dynamic_model_id', + columns=['dynamic_model_id', 'measurement_point_id'], + data=[('DM_TCB', 'B1'), + ('DM_TCB', 'BS1')]) + measurement2_df = pd.DataFrame.from_records( + index='dynamic_model_id', + columns=['dynamic_model_id', 'measurement_point_id'], + data=[('DM_TCB', 'B4')]) + model_mapping.add_tap_changer_blocking_automation_system(tcb_df, tfo_df, measurement1_df, measurement2_df) def test_add_event():