ContainersMappingHelper.java

/**
 * Copyright (c) 2021, 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.powerfactory.converter;

import com.google.common.primitives.Ints;
import com.powsybl.iidm.network.util.ContainersMapping;
import com.powsybl.powerfactory.model.DataObject;
import com.powsybl.powerfactory.model.DataObjectIndex;
import com.powsybl.powerfactory.model.DataObjectRef;
import com.powsybl.powerfactory.model.PowerFactoryException;

import java.util.*;
import java.util.stream.Collectors;

/**
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 * @author Luma Zamarre��o {@literal <zamarrenolm at aia.es>}
 * @author Jos�� Antonio Marqu��s {@literal <marquesja at aia.es>}
 */
final class ContainersMappingHelper {

    private ContainersMappingHelper() {
    }

    private static final class Edge {

        private final DataObject obj1;

        private final DataObject obj2;

        private final boolean transformer;

        private final boolean zeroImpedance;

        private Edge(DataObject obj1, DataObject obj2, boolean transformer, boolean zeroImpedance) {
            this.obj1 = Objects.requireNonNull(obj1);
            this.obj2 = Objects.requireNonNull(obj2);
            this.transformer = transformer;
            this.zeroImpedance = zeroImpedance;
        }

        private DataObject getObj1() {
            return obj1;
        }

        private DataObject getObj2() {
            return obj2;
        }

        private boolean isTransformer() {
            return transformer;
        }

        private boolean isZeroImpedance() {
            return zeroImpedance;
        }
    }

    private static final class BusesToVoltageLevelId {

        private final DataObjectIndex index;

        private BusesToVoltageLevelId(DataObjectIndex index) {
            this.index = index;
        }

        private double getNominalVoltage(Integer id) {
            DataObject elmTerm = index.getDataObjectById(id).orElseThrow(() -> new PowerFactoryException("One ElemTerm was expected"));
            return NodeConverter.getNominalVoltage(elmTerm);
        }

        private String getVoltageLevelId(Set<Integer> ids) {
            String voltageLevelId = getPowerFactoryVoltageLevelId(ids);
            // automatic naming
            return Objects.requireNonNullElseGet(voltageLevelId, () -> "VL" + ids.stream().sorted().findFirst()
                .orElseThrow(() -> new PowerFactoryException("Unexpected empty ids set")));
        }

        private String getSubstationId(Set<Integer> ids) {
            String substationId = getPowerFactorySubstationId(ids);
            // automatic naming
            return Objects.requireNonNullElseGet(substationId, () -> "S" + ids.stream().sorted().findFirst()
                .orElseThrow(() -> new PowerFactoryException("Unexpected empty ids set")));
        }

        // Find an ElmSite with same ElmSubstats as defined by the ids argument
        private String getPowerFactorySubstationId(Set<Integer> ids) {

            Set<DataObject> elmTerms = elmTermsAssociatedTo(ids);
            Set<DataObject> elmSubstats = elmSubstatsAssociatedTo(elmTerms);
            Set<DataObject> elmSites = elmSitesAssociatedTo(elmSubstats);

            if (elmSites.size() != 1) {
                return null;
            }
            DataObject elmSite = elmSites.iterator().next();

            Set<DataObject> dataObjects = elmSite.getChildren().stream()
                .filter(dataObject -> dataObject.getDataClassName().equals(DataAttributeNames.ELMSUBSTAT))
                .collect(Collectors.toSet());

            return elmSubstats.equals(dataObjects) ? elmSite.getLocName() : null;
        }

        // Find an ElmSubstat with same ElmTerms (Nodes) as defined by the ids argument
        private String getPowerFactoryVoltageLevelId(Set<Integer> ids) {

            Set<DataObject> elmTerms = elmTermsAssociatedTo(ids);
            Set<DataObject> elmSubstats = elmSubstatsAssociatedTo(elmTerms);

            if (elmSubstats.size() != 1) {
                return null;
            }
            DataObject elmSubstat = elmSubstats.iterator().next();

            Set<DataObject> dataObjects = elmSubstat.getChildren().stream()
                .filter(dataObject -> dataObject.getDataClassName().equals(DataAttributeNames.ELMTERM))
                .collect(Collectors.toSet());

            return elmTerms.equals(dataObjects) ? elmSubstat.getLocName() : null;
        }

        private Set<DataObject> elmTermsAssociatedTo(Set<Integer> ids) {
            return ids.stream()
                .flatMap(id -> index.getDataObjectById(id).stream())
                .filter(dataObject -> dataObject.getDataClassName().equals(DataAttributeNames.ELMTERM))
                .collect(Collectors.toSet());
        }

        private Set<DataObject> elmSubstatsAssociatedTo(Set<DataObject> elmTerms) {
            return elmTerms.stream().map(DataObject::getParent)
                .filter(dataObject -> dataObject.getDataClassName().equals(DataAttributeNames.ELMSUBSTAT))
                .collect(Collectors.toSet());
        }

        private Set<DataObject> elmSitesAssociatedTo(Set<DataObject> elmSubstats) {
            return elmSubstats.stream().map(DataObject::getParent)
                .filter(dataObject -> dataObject.getDataClassName().equals(DataAttributeNames.ELMSITE))
                .collect(Collectors.toSet());
        }
    }

    private static boolean isConnectedElm(DataObject connectedObj) {
        if (connectedObj == null) {
            return false;
        }
        String dataClassName = connectedObj.getDataClassName();
        return dataClassName.equals("ElmTr2")
                || dataClassName.equals("ElmTr3")
                || dataClassName.equals("ElmLne")
                || dataClassName.equals("ElmCoup")
                || dataClassName.equals("ElmZpu")
                || dataClassName.equals("ElmVsc");
    }

    private static void createNodes(List<DataObject> elmTerms, List<DataObject> nodes,
        Map<DataObject, List<DataObject>> connectedElmByElmTermId) {
        for (DataObject elmTerm : elmTerms) {
            nodes.add(elmTerm);
            for (DataObject staCubic : elmTerm.getChildrenByClass("StaCubic")) {
                DataObject connectedObj = staCubic.findObjectAttributeValue(DataAttributeNames.OBJ_ID)
                        .flatMap(DataObjectRef::resolve)
                        .orElse(null);
                if (isConnectedElm(connectedObj)) {
                    connectedElmByElmTermId.computeIfAbsent(connectedObj, k -> new ArrayList<>()).add(elmTerm);
                }
            }
        }
    }

    // we do not have to consider the exact orientation of the element
    private static void createEdges(List<Edge> edges, Map<DataObject, List<DataObject>> connectedElmByElmTermId) {
        for (Map.Entry<DataObject, List<DataObject>> e : connectedElmByElmTermId.entrySet()) {
            DataObject connectedObj = e.getKey();
            List<DataObject> elmTerms = e.getValue();
            if (elmTerms.size() == 2) {
                switch (connectedObj.getDataClassName()) {
                    case "ElmTr2":
                        // All transformers are considered with impedance
                        edges.add(new Edge(elmTerms.get(0), elmTerms.get(1), true, false));
                        break;
                    case "ElmLne", "ElmZpu":
                        // All lines are considered with impedance, only zero impedance lines are necessary
                        break;
                    case "ElmCoup":
                        edges.add(new Edge(elmTerms.get(0), elmTerms.get(1), false, true));
                        break;
                    default:
                        throw new IllegalStateException("Unexpected object class: " + connectedObj.getDataClassName());
                }
            } else if (elmTerms.size() == 3) {
                if (connectedObj.getDataClassName().equals("ElmTr3")) {
                    edges.add(new Edge(elmTerms.get(0), elmTerms.get(1), true, false));
                    edges.add(new Edge(elmTerms.get(0), elmTerms.get(2), true, false));
                } else if (connectedObj.getDataClassName().equals("ElmVsc")) {
                    edges.add(new Edge(elmTerms.get(0), elmTerms.get(1), false, true));
                    edges.add(new Edge(elmTerms.get(0), elmTerms.get(2), false, true));
                } else {
                    throw new IllegalStateException("Unexpected object class: " + connectedObj.getDataClassName());
                }
            }
        }
    }

    static ContainersMapping create(DataObjectIndex index, List<DataObject> elmTerms) {
        List<DataObject> nodes = new ArrayList<>();
        List<Edge> edges = new ArrayList<>();
        Map<DataObject, List<DataObject>> connectedElmByElmTermId = new HashMap<>();

        createNodes(elmTerms, nodes, connectedElmByElmTermId);
        createEdges(edges, connectedElmByElmTermId);

        BusesToVoltageLevelId busesToVoltageLevelId = new BusesToVoltageLevelId(index);

        return ContainersMapping.create(nodes, edges,
            obj -> Ints.checkedCast(obj.getId()),
            edge -> Ints.checkedCast(edge.getObj1().getId()),
            edge -> Ints.checkedCast(edge.getObj2().getId()),
            Edge::isZeroImpedance,
            Edge::isTransformer,
            busesToVoltageLevelId::getNominalVoltage,
            busesToVoltageLevelId::getVoltageLevelId,
            busesToVoltageLevelId::getSubstationId);
    }
}