Repository: powsybl/pypowsybl
Pull request #975: Add DiscreteMeasurements extension dataframe
Author: login: geofjamg
Created: 2025-03-27T09:39:25Z | Merged: 2025-03-27T13:07:26Z

**Please check if the PR fulfills these requirements**
<!-- please use `'[x]'` to check the checkboxes, or submit the PR and then click the checkboxes -->
- [x] The commit message follows our guidelines
- [x] Tests for the changes have been added (for bug fixes / features)
- [ ] Docs have been added / updated (for bug fixes / features)
- [ ] A PR or issue has been opened in all impacted repositories (if any)


**Does this PR already have an issue describing the problem?**
<!-- If so, link to this issue using `'Fixes #XXX'` and skip the rest -->
No


**What kind of change does this PR introduce?**
<!-- Bug fix, feature, docs update, ... -->
Feature


**What is the current behavior?**
<!-- You can also link to an open issue here -->
We only have access to Measurements extension and not DiscreteMeasurements


**What is the new behavior (if this is a feature change)?**
We also can get DiscreteMeasurements data


**Does this PR introduce a breaking change or deprecate an API?**
- [ ] Yes
- [x] No

**If yes, please check if the following requirements are fulfilled**
<!-- If no breaking changes or API deprecations were introduced, delete this section -->
- [ ] The *Breaking Change* or *Deprecated* label has been added
- [ ] The migration steps are described in the following section

**What changes might users need to make in their application due to this PR? (migration steps)**
<!-- If this PR introduces a breaking change, describe the migration steps -->
<!-- The content of this section will be copied in the migration guide -->



**Other information**:
<!-- if any of the questions/checkboxes don't apply, please delete them entirely -->



Diff:
diff --git a/java/pypowsybl/src/main/java/com/powsybl/dataframe/network/extensions/DiscreteMeasurementsDataframeAdder.java b/java/pypowsybl/src/main/java/com/powsybl/dataframe/network/extensions/DiscreteMeasurementsDataframeAdder.java
new file mode 100644
index 0000000000..b5666a21ff
--- /dev/null
+++ b/java/pypowsybl/src/main/java/com/powsybl/dataframe/network/extensions/DiscreteMeasurementsDataframeAdder.java
@@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.dataframe.network.extensions;
+
+import com.powsybl.commons.PowsyblException;
+import com.powsybl.dataframe.SeriesMetadata;
+import com.powsybl.dataframe.network.adders.AbstractSimpleAdder;
+import com.powsybl.dataframe.network.adders.SeriesUtils;
+import com.powsybl.dataframe.update.IntSeries;
+import com.powsybl.dataframe.update.StringSeries;
+import com.powsybl.dataframe.update.UpdatingDataframe;
+import com.powsybl.iidm.network.Identifiable;
+import com.powsybl.iidm.network.Network;
+import com.powsybl.iidm.network.extensions.DiscreteMeasurement;
+import com.powsybl.iidm.network.extensions.DiscreteMeasurementAdder;
+import com.powsybl.iidm.network.extensions.DiscreteMeasurements;
+import com.powsybl.iidm.network.extensions.DiscreteMeasurementsAdder;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
+ */
+public class DiscreteMeasurementsDataframeAdder extends AbstractSimpleAdder {
+
+    private static final List<SeriesMetadata> METADATA = List.of(
+            SeriesMetadata.stringIndex(DiscreteMeasurementsDataframeProvider.ELEMENT_ID),
+            SeriesMetadata.strings(DiscreteMeasurementsDataframeProvider.ID),
+            SeriesMetadata.strings(DiscreteMeasurementsDataframeProvider.TYPE),
+            SeriesMetadata.strings(DiscreteMeasurementsDataframeProvider.TAP_CHANGER),
+            SeriesMetadata.strings(DiscreteMeasurementsDataframeProvider.VALUE_TYPE),
+            SeriesMetadata.strings(DiscreteMeasurementsDataframeProvider.VALUE),
+            SeriesMetadata.booleans(DiscreteMeasurementsDataframeProvider.VALID)
+    );
+
+    private static class DiscreteMeasurementsSeries {
+        private final StringSeries ids;
+        private final StringSeries elementIds;
+        private final StringSeries types;
+        private final StringSeries tapChangers;
+        private final StringSeries valueTypes;
+        private final StringSeries values;
+        private final IntSeries valids;
+        private final Map<String, DiscreteMeasurements<?>> measurementsByElementId = new HashMap<>();
+
+        DiscreteMeasurementsSeries(UpdatingDataframe dataframe) {
+            this.elementIds = dataframe.getStrings(DiscreteMeasurementsDataframeProvider.ELEMENT_ID);
+            this.ids = dataframe.getStrings(DiscreteMeasurementsDataframeProvider.ID);
+            this.types = dataframe.getStrings(DiscreteMeasurementsDataframeProvider.TYPE);
+            this.tapChangers = dataframe.getStrings(DiscreteMeasurementsDataframeProvider.TAP_CHANGER);
+            this.valueTypes = dataframe.getStrings(DiscreteMeasurementsDataframeProvider.VALUE_TYPE);
+            this.values = dataframe.getStrings(DiscreteMeasurementsDataframeProvider.VALUE);
+            this.valids = dataframe.getInts(DiscreteMeasurementsDataframeProvider.VALID);
+        }
+
+        void create(int row) {
+            String elementId = this.elementIds.get(row);
+            DiscreteMeasurements<?> measurements = measurementsByElementId.get(elementId);
+            DiscreteMeasurementAdder adder = measurements.newDiscreteMeasurement();
+            SeriesUtils.applyIfPresent(ids, row, adder::setId);
+            SeriesUtils.applyIfPresent(types, row, type -> adder.setType(DiscreteMeasurement.Type.valueOf(type)));
+            SeriesUtils.applyIfPresent(tapChangers, row, tapChanger -> adder.setTapChanger(DiscreteMeasurement.TapChanger.valueOf(tapChanger)));
+            SeriesUtils.applyIfPresent(values, row, value -> {
+                switch (DiscreteMeasurement.ValueType.valueOf(valueTypes.get(row))) {
+                    case BOOLEAN -> adder.setValue(Boolean.parseBoolean(value));
+                    case INT -> adder.setValue(Integer.parseInt(value));
+                    case STRING -> adder.setValue(value);
+                }
+            });
+            SeriesUtils.applyBooleanIfPresent(valids, row, adder::setValid);
+            adder.add();
+        }
+
+        @SuppressWarnings("unchecked")
+        <I extends Identifiable<I>> DiscreteMeasurements<I> createDiscreteMeasurement(Identifiable<I> identifiable) {
+            DiscreteMeasurementsAdder<I> adder = identifiable.newExtension(DiscreteMeasurementsAdder.class);
+            return adder.add();
+        }
+
+        void removeAndInitialize(Network network, int row) {
+            String elementId = this.elementIds.get(row);
+            if (!measurementsByElementId.containsKey(elementId)) {
+                Identifiable<?> identifiable = network.getIdentifiable(elementId);
+                if (identifiable == null) {
+                    throw new PowsyblException("Invalid element id '" + elementId + "'");
+                }
+                DiscreteMeasurementsDataframeProvider.removeDiscreteMeasurements(identifiable);
+                measurementsByElementId.put(elementId, createDiscreteMeasurement(identifiable));
+            }
+        }
+    }
+
+    @Override
+    public void addElements(Network network, UpdatingDataframe dataframe) {
+        DiscreteMeasurementsSeries series = new DiscreteMeasurementsSeries(dataframe);
+        for (int row = 0; row < dataframe.getRowCount(); row++) {
+            series.removeAndInitialize(network, row);
+        }
+        for (int row = 0; row < dataframe.getRowCount(); row++) {
+            series.create(row);
+        }
+    }
+
+    @Override
+    public List<List<SeriesMetadata>> getMetadata() {
+        return Collections.singletonList(METADATA);
+    }
+}
diff --git a/java/pypowsybl/src/main/java/com/powsybl/dataframe/network/extensions/DiscreteMeasurementsDataframeProvider.java b/java/pypowsybl/src/main/java/com/powsybl/dataframe/network/extensions/DiscreteMeasurementsDataframeProvider.java
new file mode 100644
index 0000000000..28cfe476a3
--- /dev/null
+++ b/java/pypowsybl/src/main/java/com/powsybl/dataframe/network/extensions/DiscreteMeasurementsDataframeProvider.java
@@ -0,0 +1,145 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.dataframe.network.extensions;
+
+import com.google.auto.service.AutoService;
+import com.powsybl.dataframe.network.ExtensionInformation;
+import com.powsybl.dataframe.network.NetworkDataframeMapper;
+import com.powsybl.dataframe.network.NetworkDataframeMapperBuilder;
+import com.powsybl.dataframe.network.adders.NetworkElementAdder;
+import com.powsybl.iidm.network.Identifiable;
+import com.powsybl.iidm.network.Network;
+import com.powsybl.iidm.network.extensions.DiscreteMeasurement;
+import com.powsybl.iidm.network.extensions.DiscreteMeasurements;
+import org.jgrapht.alg.util.Pair;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.stream.Stream;
+
+/**
+ * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
+ */
+@AutoService(NetworkExtensionDataframeProvider.class)
+public class DiscreteMeasurementsDataframeProvider extends AbstractSingleDataframeNetworkExtension {
+
+    public static final String ELEMENT_ID = "element_id";
+    public static final String ID = "id";
+    public static final String TYPE = "type";
+    public static final String TAP_CHANGER = "tap_changer";
+    public static final String VALUE_TYPE = "value_type";
+    public static final String VALUE = "value";
+    public static final String VALID = "valid";
+
+    @Override
+    public String getExtensionName() {
+        return DiscreteMeasurements.NAME;
+    }
+
+    @Override
+    public ExtensionInformation getExtensionInformation() {
+        return new ExtensionInformation(DiscreteMeasurements.NAME, "Provides discrete measurements about a specific equipment",
+                "index : element_id (str),id (str), type (str), tap_changer (str), value_type (str), value (str), valid (bool)");
+    }
+
+    @Override
+    public NetworkDataframeMapper createMapper() {
+        return NetworkDataframeMapperBuilder.ofStream(this::itemsStream)
+                .stringsIndex(ELEMENT_ID, DiscreteMeasurementInformation::getElementId)
+                .strings(ID, DiscreteMeasurementInformation::getId)
+                .strings(TYPE, info -> info.getType().toString())
+                .strings(TAP_CHANGER, info -> info.getTapChanger() == null ? null : info.getTapChanger().toString())
+                .strings(VALUE_TYPE, info -> info.getValueType().toString())
+                .strings(VALUE, DiscreteMeasurementInformation::getValue)
+                .booleans(VALID, DiscreteMeasurementInformation::isValid)
+                .build();
+    }
+
+    @SuppressWarnings("unchecked")
+    static void removeDiscreteMeasurements(Identifiable<?> identifiable) {
+        identifiable.removeExtension(DiscreteMeasurements.class);
+    }
+
+    @Override
+    public void removeExtensions(Network network, List<String> ids) {
+        ids.stream().filter(Objects::nonNull)
+                .map(network::getIdentifiable)
+                .filter(Objects::nonNull)
+                .forEach(DiscreteMeasurementsDataframeProvider::removeDiscreteMeasurements);
+    }
+
+    static <I extends Identifiable<I>> DiscreteMeasurements<I> getDiscreteMeasurements(Identifiable<I> identifiable) {
+        return identifiable.getExtension(DiscreteMeasurements.class);
+    }
+
+    private Stream<DiscreteMeasurementInformation> itemsStream(Network network) {
+        return network.getIdentifiables().stream()
+                .map(identifiable -> Pair.of(identifiable.getId(), getDiscreteMeasurements(identifiable)))
+                .filter(pair -> pair.getSecond() != null)
+                .flatMap(pair -> pair.getSecond().getDiscreteMeasurements().stream()
+                        .map(measurement -> new DiscreteMeasurementInformation(measurement, pair.getFirst())));
+    }
+
+    @Override
+    public NetworkElementAdder createAdder() {
+        return new DiscreteMeasurementsDataframeAdder();
+    }
+
+    private static class DiscreteMeasurementInformation {
+        private final String elementId;
+        private final String id;
+        private final DiscreteMeasurement.Type type;
+        private final DiscreteMeasurement.TapChanger tapChanger;
+        private final DiscreteMeasurement.ValueType valueType;
+        private final String value;
+        private final boolean valid;
+
+        public DiscreteMeasurementInformation(DiscreteMeasurement measurement, String elementId) {
+            this.elementId = elementId;
+            this.id = measurement.getId();
+            this.type = measurement.getType();
+            this.tapChanger = measurement.getTapChanger();
+            this.valueType = measurement.getValueType();
+            // FIXME make object directly available in DiscreteMeasurement interface
+            this.value = switch (measurement.getValueType()) {
+                case BOOLEAN -> Boolean.toString(measurement.getValueAsBoolean());
+                case INT -> Integer.toString(measurement.getValueAsInt());
+                case STRING -> measurement.getValueAsString();
+            };
+            this.valid = measurement.isValid();
+        }
+
+        public String getId() {
+            return id;
+        }
+
+        public String getElementId() {
+            return elementId;
+        }
+
+        public DiscreteMeasurement.Type getType() {
+            return type;
+        }
+
+        public DiscreteMeasurement.TapChanger getTapChanger() {
+            return tapChanger;
+        }
+
+        public DiscreteMeasurement.ValueType getValueType() {
+            return valueType;
+        }
+
+        public String getValue() {
+            return value;
+        }
+
+        public boolean isValid() {
+            return valid;
+        }
+    }
+}
diff --git a/java/pypowsybl/src/main/java/com/powsybl/dataframe/network/extensions/MeasurementsDataframeProvider.java b/java/pypowsybl/src/main/java/com/powsybl/dataframe/network/extensions/MeasurementsDataframeProvider.java
index b2b5db784f..1de5e01217 100644
--- a/java/pypowsybl/src/main/java/com/powsybl/dataframe/network/extensions/MeasurementsDataframeProvider.java
+++ b/java/pypowsybl/src/main/java/com/powsybl/dataframe/network/extensions/MeasurementsDataframeProvider.java
@@ -44,7 +44,7 @@ public String getExtensionName() {
 
     @Override
     public ExtensionInformation getExtensionInformation() {
-        return new ExtensionInformation(Measurements.NAME, "Provides measurement about a specific equipment",
+        return new ExtensionInformation(Measurements.NAME, "Provides measurements about a specific equipment",
                 "index : element_id (str),id (str), type (str), standard_deviation (float), value (float), valid (bool)");
     }
 
diff --git a/tests/test_network_extensions.py b/tests/test_network_extensions.py
index 0290413835..93d6c7a380 100644
--- a/tests/test_network_extensions.py
+++ b/tests/test_network_extensions.py
@@ -222,6 +222,20 @@ def test_measurements():
     assert n.get_extensions(extension_name).empty
 
 
+def test_discrete_measurements():
+    n = pn.create_four_substations_node_breaker_network()
+    extension_name = 'discreteMeasurements'
+    extensions = n.get_extensions(extension_name)
+    assert extensions.empty
+    n.create_extensions(extension_name, id='TWT_TAP_TS', element_id='TWT', type='TAP_POSITION',
+                        tap_changer='PHASE_TAP_CHANGER', value_type='INT', value='17', valid=True)
+    extensions = n.get_extensions(extension_name)
+    expected = pd.DataFrame(index=pd.Series(name='element_id', data=['TWT']),
+                            columns=['id', 'type', 'tap_changer', 'value_type', 'value', 'valid'],
+                            data=[['TWT_TAP_TS', 'TAP_POSITION', 'PHASE_TAP_CHANGER', 'INT', '17', True]])
+    pd.testing.assert_frame_equal(expected, extensions, check_dtype=False)
+
+
 def test_injection_observability():
     n = pn.create_four_substations_node_breaker_network()
     extension_name = 'injectionObservability'
@@ -533,7 +547,7 @@ def test_get_extensions_information():
     assert extensions_information.loc['cgmesMetadataModels']['attributes'] == ('index : id (str), cgmes_subset (str), description (str), ' \
                                                                             'version (int), modeling_authority_set (str), profiles (str), ' \
                                                                             'dependent_on (str), supersedes (str) ')
-    assert extensions_information.loc['measurements']['detail'] == 'Provides measurement about a specific equipment'
+    assert extensions_information.loc['measurements']['detail'] == 'Provides measurements about a specific equipment'
     assert extensions_information.loc['measurements']['attributes'] == 'index : element_id (str),id (str), type (str), ' \
                                                                        'standard_deviation (float), value (float), valid (bool)'
     assert extensions_information.loc['branchObservability']['detail'] == 'Provides information about the observability of a branch'


Reviews:
user: login: flo-dup
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T11:56:21Z
user: login: geofjamg
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-27T12:33:35Z
user: login: flo-dup
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-27T13:01:34Z

Review comments:
diff_hunk: @@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.dataframe.network.extensions;
+
+import com.powsybl.commons.PowsyblException;
+import com.powsybl.dataframe.SeriesMetadata;
+import com.powsybl.dataframe.network.adders.AbstractSimpleAdder;
+import com.powsybl.dataframe.network.adders.SeriesUtils;
+import com.powsybl.dataframe.update.IntSeries;
+import com.powsybl.dataframe.update.StringSeries;
+import com.powsybl.dataframe.update.UpdatingDataframe;
+import com.powsybl.iidm.network.Identifiable;
+import com.powsybl.iidm.network.Network;
+import com.powsybl.iidm.network.extensions.DiscreteMeasurement;
+import com.powsybl.iidm.network.extensions.DiscreteMeasurementAdder;
+import com.powsybl.iidm.network.extensions.DiscreteMeasurements;
+import com.powsybl.iidm.network.extensions.DiscreteMeasurementsAdder;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
+ */
+public class DiscreteMeasurementsDataframeAdder extends AbstractSimpleAdder {
+
+    private static final List<SeriesMetadata> METADATA = List.of(
+            SeriesMetadata.stringIndex(DiscreteMeasurementsDataframeProvider.ELEMENT_ID),
+            SeriesMetadata.strings(DiscreteMeasurementsDataframeProvider.ID),
+            SeriesMetadata.strings(DiscreteMeasurementsDataframeProvider.TYPE),
+            SeriesMetadata.strings(DiscreteMeasurementsDataframeProvider.TAP_CHANGER),
+            SeriesMetadata.strings(DiscreteMeasurementsDataframeProvider.VALUE_TYPE),
+            SeriesMetadata.strings(DiscreteMeasurementsDataframeProvider.VALUE),
+            SeriesMetadata.booleans(DiscreteMeasurementsDataframeProvider.VALID)
+    );
+
+    private static class DiscreteMeasurementsSeries {
+        private final StringSeries ids;
+        private final StringSeries elementIds;
+        private final StringSeries types;
+        private final StringSeries tapChangers;
+        private final StringSeries valueTypes;
+        private final StringSeries values;
+        private final IntSeries valids;
+        private final Map<String, DiscreteMeasurements<?>> measurementsByElementId = new HashMap<>();
+
+        DiscreteMeasurementsSeries(UpdatingDataframe dataframe) {
+            this.elementIds = dataframe.getStrings(DiscreteMeasurementsDataframeProvider.ELEMENT_ID);
+            this.ids = dataframe.getStrings(DiscreteMeasurementsDataframeProvider.ID);
+            this.types = dataframe.getStrings(DiscreteMeasurementsDataframeProvider.TYPE);
+            this.tapChangers = dataframe.getStrings(DiscreteMeasurementsDataframeProvider.TAP_CHANGER);
+            this.valueTypes = dataframe.getStrings(DiscreteMeasurementsDataframeProvider.VALUE_TYPE);
+            this.values = dataframe.getStrings(DiscreteMeasurementsDataframeProvider.VALUE);
+            this.valids = dataframe.getInts(DiscreteMeasurementsDataframeProvider.VALID);
+        }
+
+        void create(int row) {
+            String elementId = this.elementIds.get(row);
+            DiscreteMeasurements<?> measurements = measurementsByElementId.get(elementId);
+            DiscreteMeasurementAdder adder = measurements.newDiscreteMeasurement();
+            SeriesUtils.applyIfPresent(ids, row, adder::setId);
+            SeriesUtils.applyIfPresent(types, row, type -> adder.setType(DiscreteMeasurement.Type.valueOf(type)));
+            SeriesUtils.applyIfPresent(tapChangers, row, tapChanger -> adder.setTapChanger(DiscreteMeasurement.TapChanger.valueOf(tapChanger)));
+            SeriesUtils.applyIfPresent(values, row, value -> {
+                switch (DiscreteMeasurement.ValueType.valueOf(valueTypes.get(row))) {
+                    case BOOLEAN -> adder.setValue(Boolean.parseBoolean(value));
+                    case INT -> adder.setValue(Integer.parseInt(value));
+                    case STRING -> adder.setValue(value);
+                }
+            });
+            SeriesUtils.applyBooleanIfPresent(valids, row, adder::setValid);
+            adder.add();
+        }
+
+        @SuppressWarnings("unchecked")
path: java/pypowsybl/src/main/java/com/powsybl/dataframe/network/extensions/DiscreteMeasurementsDataframeAdder.java
user: login: flo-dup
body: you don't need the annotation anymore, do you?
created_at: 2025-03-27T11:56:20Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,116 @@
+/**
+ * Copyright (c) 2025, RTE (http://www.rte-france.com)
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ * SPDX-License-Identifier: MPL-2.0
+ */
+package com.powsybl.dataframe.network.extensions;
+
+import com.powsybl.commons.PowsyblException;
+import com.powsybl.dataframe.SeriesMetadata;
+import com.powsybl.dataframe.network.adders.AbstractSimpleAdder;
+import com.powsybl.dataframe.network.adders.SeriesUtils;
+import com.powsybl.dataframe.update.IntSeries;
+import com.powsybl.dataframe.update.StringSeries;
+import com.powsybl.dataframe.update.UpdatingDataframe;
+import com.powsybl.iidm.network.Identifiable;
+import com.powsybl.iidm.network.Network;
+import com.powsybl.iidm.network.extensions.DiscreteMeasurement;
+import com.powsybl.iidm.network.extensions.DiscreteMeasurementAdder;
+import com.powsybl.iidm.network.extensions.DiscreteMeasurements;
+import com.powsybl.iidm.network.extensions.DiscreteMeasurementsAdder;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
+ */
+public class DiscreteMeasurementsDataframeAdder extends AbstractSimpleAdder {
+
+    private static final List<SeriesMetadata> METADATA = List.of(
+            SeriesMetadata.stringIndex(DiscreteMeasurementsDataframeProvider.ELEMENT_ID),
+            SeriesMetadata.strings(DiscreteMeasurementsDataframeProvider.ID),
+            SeriesMetadata.strings(DiscreteMeasurementsDataframeProvider.TYPE),
+            SeriesMetadata.strings(DiscreteMeasurementsDataframeProvider.TAP_CHANGER),
+            SeriesMetadata.strings(DiscreteMeasurementsDataframeProvider.VALUE_TYPE),
+            SeriesMetadata.strings(DiscreteMeasurementsDataframeProvider.VALUE),
+            SeriesMetadata.booleans(DiscreteMeasurementsDataframeProvider.VALID)
+    );
+
+    private static class DiscreteMeasurementsSeries {
+        private final StringSeries ids;
+        private final StringSeries elementIds;
+        private final StringSeries types;
+        private final StringSeries tapChangers;
+        private final StringSeries valueTypes;
+        private final StringSeries values;
+        private final IntSeries valids;
+        private final Map<String, DiscreteMeasurements<?>> measurementsByElementId = new HashMap<>();
+
+        DiscreteMeasurementsSeries(UpdatingDataframe dataframe) {
+            this.elementIds = dataframe.getStrings(DiscreteMeasurementsDataframeProvider.ELEMENT_ID);
+            this.ids = dataframe.getStrings(DiscreteMeasurementsDataframeProvider.ID);
+            this.types = dataframe.getStrings(DiscreteMeasurementsDataframeProvider.TYPE);
+            this.tapChangers = dataframe.getStrings(DiscreteMeasurementsDataframeProvider.TAP_CHANGER);
+            this.valueTypes = dataframe.getStrings(DiscreteMeasurementsDataframeProvider.VALUE_TYPE);
+            this.values = dataframe.getStrings(DiscreteMeasurementsDataframeProvider.VALUE);
+            this.valids = dataframe.getInts(DiscreteMeasurementsDataframeProvider.VALID);
+        }
+
+        void create(int row) {
+            String elementId = this.elementIds.get(row);
+            DiscreteMeasurements<?> measurements = measurementsByElementId.get(elementId);
+            DiscreteMeasurementAdder adder = measurements.newDiscreteMeasurement();
+            SeriesUtils.applyIfPresent(ids, row, adder::setId);
+            SeriesUtils.applyIfPresent(types, row, type -> adder.setType(DiscreteMeasurement.Type.valueOf(type)));
+            SeriesUtils.applyIfPresent(tapChangers, row, tapChanger -> adder.setTapChanger(DiscreteMeasurement.TapChanger.valueOf(tapChanger)));
+            SeriesUtils.applyIfPresent(values, row, value -> {
+                switch (DiscreteMeasurement.ValueType.valueOf(valueTypes.get(row))) {
+                    case BOOLEAN -> adder.setValue(Boolean.parseBoolean(value));
+                    case INT -> adder.setValue(Integer.parseInt(value));
+                    case STRING -> adder.setValue(value);
+                }
+            });
+            SeriesUtils.applyBooleanIfPresent(valids, row, adder::setValid);
+            adder.add();
+        }
+
+        @SuppressWarnings("unchecked")
path: java/pypowsybl/src/main/java/com/powsybl/dataframe/network/extensions/DiscreteMeasurementsDataframeAdder.java
user: login: geofjamg
body: Needed
created_at: 2025-03-27T12:33:35Z
author_association: MEMBER

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-27T12:32:07Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=powsybl_pypowsybl-java&pullRequest=975) **Quality Gate passed for 'pypowsybl-parent'**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [1 New issue](https://sonarcloud.io/project/issues?id=powsybl_pypowsybl-java&pullRequest=975&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=powsybl_pypowsybl-java&pullRequest=975&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=powsybl_pypowsybl-java&pullRequest=975&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=powsybl_pypowsybl-java&pullRequest=975&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=powsybl_pypowsybl-java&pullRequest=975&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=powsybl_pypowsybl-java&pullRequest=975)


author_association: NONE
user: login: sonarqubecloud[bot]
created_at: 2025-03-27T12:41:55Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=powsybl_pypowsybl&pullRequest=975) **Quality Gate passed for 'pypowsybl'**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=powsybl_pypowsybl&pullRequest=975&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=powsybl_pypowsybl&pullRequest=975&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=powsybl_pypowsybl&pullRequest=975&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=powsybl_pypowsybl&pullRequest=975&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=powsybl_pypowsybl&pullRequest=975&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=powsybl_pypowsybl&pullRequest=975)


author_association: NONE