HvdcConverter.java

/**
 * Copyright (c) 2022, 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.powsybl.iidm.network.HvdcLine.ConvertersMode;
import com.powsybl.iidm.network.*;
import com.powsybl.powerfactory.converter.PowerFactoryImporter.ImportContext;
import com.powsybl.powerfactory.model.DataObject;
import com.powsybl.powerfactory.model.DataObjectRef;
import com.powsybl.powerfactory.model.PowerFactoryException;

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

/**
 * @author Luma Zamarre��o {@literal <zamarrenolm at aia.es>}
 * @author Jos�� Antonio Marqu��s {@literal <marquesja at aia.es>}
 */

class HvdcConverter extends AbstractConverter {

    private final Map<DataObject, List<DataObject>> elmTermsConnectedToVscs = new HashMap<>();
    private final List<Configuration> configurations = new ArrayList<>();
    private final Set<DataObject> dcElmLnes = new HashSet<>();
    private final Set<DataObject> dcElmTerms = new HashSet<>();
    private final Set<DataObject> usedVscs = new HashSet<>();

    HvdcConverter(ImportContext importContext, Network network) {
        super(importContext, network);
    }

    boolean isDcLink(DataObject elmLne) {
        return dcElmLnes.contains(elmLne);
    }

    boolean isDcNode(DataObject elmTerm) {
        return dcElmTerms.contains(elmTerm);
    }

    void computeConfigurations(List<DataObject> elmTerms, List<DataObject> elmVscs) {
        if (elmVscs.isEmpty()) {
            return;
        }
        computeElmTermsConnectedToVscs(elmTerms, elmVscs);
        for (DataObject elmVsc : elmVscs) {
            if (!usedVscs.contains(elmVsc)) {
                computeConfiguration(elmVsc).ifPresent(this::addConfiguration);
            }
        }
    }

    private void computeElmTermsConnectedToVscs(List<DataObject> elmTerms, List<DataObject> elmVscs) {
        for (DataObject elmTerm : elmTerms) {
            getDataObjectsConnectedToElmTerm(elmTerm)
                    .filter(elmVscs::contains).findFirst()
                    .ifPresent(elmVsc -> elmTermsConnectedToVscs.computeIfAbsent(elmVsc, k -> new ArrayList<>()).add(elmTerm));
        }
    }

    private void addConfiguration(Configuration c) {
        configurations.add(c);
        dcElmLnes.addAll(c.getElmLnes());
        dcElmTerms.addAll(c.getElmTerms());
        usedVscs.add(c.vsc0);
        usedVscs.add(c.vsc1);
    }

    private static Stream<DataObject> getDataObjectsConnectedToElmTerm(DataObject elmTerm) {
        return elmTerm.getChildren().stream()
                .map(staCubic -> staCubic.findObjectAttributeValue(DataAttributeNames.OBJ_ID)
                        .flatMap(DataObjectRef::resolve))
                .flatMap(Optional::stream);
    }

    private Optional<Configuration> computeConfiguration(DataObject elmVsc0) {
        List<DCLink> links = computeDcLinksConnectedToVsc(elmVsc0);
        Optional<DataObject> elmVsc1 = findVsc1(links);
        if (elmVsc1.isPresent()) {
            List<DataObject> configurationDcElmTerms = links.stream().flatMap(l -> Stream.of(l.elmTerm0, l.elmTerm1)).collect(Collectors.toList());
            Optional<DataObject> elmTermAc0 = findAcElmTerm(elmVsc0, configurationDcElmTerms);
            if (elmTermAc0.isPresent()) {
                Optional<DataObject> elmTermAc1 = findAcElmTerm(elmVsc1.get(), configurationDcElmTerms);
                if (elmTermAc1.isPresent()) {
                    return Optional.of(new Configuration(elmTermAc0.get(), elmVsc0, links, elmTermAc1.get(), elmVsc1.get()));
                }
            }
        }
        return Optional.empty();
    }

    private List<DCLink> computeDcLinksConnectedToVsc(DataObject elmVsc) {
        List<DCLink> dcLinks = new ArrayList<>();
        for (DataObject elmTerm : elmTermsConnectedToVscs.get(elmVsc)) {

            List<DataObject> elmLnes = getDataObjectsConnectedToElmTerm(elmTerm)
                .filter(dataObject -> dataObject.getDataClassName().equals("ElmLne"))
                .toList();

            for (DataObject elmLne : elmLnes) {
                otherElmTerm(elmTerm, elmLne, elmVsc)
                        .ifPresent(dataObject -> dcLinks.add(new DCLink(elmTerm, elmLne, dataObject)));
            }
        }
        return dcLinks;
    }

    private Optional<DataObject> otherElmTerm(DataObject elmTerm, DataObject elmLne, DataObject elmVsc) {
        // Search only in elmTerms connected to other VSCs
        return elmTermsConnectedToVscs.entrySet().stream()
                .filter(e -> !Objects.equals(elmVsc, e.getKey()))
                .map(Map.Entry::getValue)
                .flatMap(Collection::stream)
                // Recheck that elmTerm is different from the given one
            .filter(otherElmTerm -> !elmTerm.equals(otherElmTerm) && isElmTermConnectedToLne(otherElmTerm, elmLne))
            .findFirst();
    }

    private static boolean isElmTermConnectedToLne(DataObject elmTerm, DataObject elmLne) {
        return getDataObjectsConnectedToElmTerm(elmTerm).anyMatch(elmLne::equals);
    }

    private Optional<DataObject> findVsc1(List<DCLink> links) {
        if (!links.isEmpty()) {
            DataObject elmTerm1 = links.get(0).elmTerm1;
            Optional<DataObject> elmVsc1 = findTheOnlyOneDataObject(vscConnectedToElmTerm(elmTerm1));
            if (elmVsc1.isPresent()) {
                // Check other VSC is at end 1 for all the links in the HVDC configuration
                List<DataObject> elmTermsConnectedToOtherVsc = elmTermsConnectedToVscs.get(elmVsc1.get());
                if (links.stream().allMatch(l -> elmTermsConnectedToOtherVsc.contains(l.elmTerm1))) {
                    return elmVsc1;
                }
            }
        }
        return Optional.empty();
    }

    private static List<DataObject> vscConnectedToElmTerm(DataObject elmTerm) {
        return getDataObjectsConnectedToElmTerm(elmTerm)
            .filter(dataObject -> dataObject.getDataClassName().equals("ElmVsc"))
            .collect(Collectors.toList());
    }

    private Optional<DataObject> findAcElmTerm(DataObject elmVsc, List<DataObject> dcElmTerms) {
        List<DataObject> elmTermsAc = elmTermsConnectedToVscs.get(elmVsc).stream()
                .filter(elmTerm -> !dcElmTerms.contains(elmTerm))
                .toList();
        return findTheOnlyOneDataObject(elmTermsAc);
    }

    private static Optional<DataObject> findTheOnlyOneDataObject(List<DataObject> dataObjects) {
        if (dataObjects.isEmpty()) {
            return Optional.empty();
        } else if (dataObjects.size() > 1) {
            throw new PowerFactoryException("Unsupported Hvdc configuration");
        }
        return Optional.of(dataObjects.get(0));
    }

    private static final class Configuration {
        private final DataObject elmTermAc0;
        private final DataObject vsc0;
        private final List<DCLink> links;
        private final DataObject elmTermAc1;
        private final DataObject vsc1;

        private Configuration(DataObject elmTermAc0, DataObject vsc0, List<DCLink> links, DataObject elmTermAc1, DataObject vsc1) {
            this.elmTermAc0 = elmTermAc0;
            this.vsc0 = vsc0;
            this.links = links;
            this.elmTermAc1 = elmTermAc1;
            this.vsc1 = vsc1;
        }

        Collection<DataObject> getElmLnes() {
            return links.stream().map(l -> l.elmLne).collect(Collectors.toList());
        }

        Collection<DataObject> getElmTerms() {
            return links.stream()
                    .flatMap(l -> Stream.of(l.elmTerm0, l.elmTerm1))
                    .collect(Collectors.toList());
        }
    }

    private static final class DCLink {
        private final DataObject elmTerm0;
        private final DataObject elmLne;
        private final DataObject elmTerm1;

        private DCLink(DataObject elmTerm0, DataObject elmLne, DataObject elmTerm1) {
            this.elmTerm0 = elmTerm0;
            this.elmLne = elmLne;
            this.elmTerm1 = elmTerm1;

        }
    }

    void create() {
        configurations.forEach(this::create);
    }

    private void create(Configuration configuration) {

        VscModel vscModelR = VscModel.create(configuration.vsc0);
        DcLineModel dcLineModel = DcLineModel.create(configuration.links, configuration.vsc0);
        VscModel vscModelI = VscModel.create(configuration.vsc1);

        NodeRef nodeRefR = getNodeFromElmTerm(configuration.elmTermAc0);
        VoltageLevel voltageLevelR = getNetwork().getVoltageLevel(nodeRefR.voltageLevelId);

        // Always with internal connection
        int nodeR = voltageLevelR.getNodeBreakerView().getMaximumNodeIndex() + 1;
        createInternalConnection(voltageLevelR, nodeRefR.node, nodeR);

        VscConverterStationAdder adderR = voltageLevelR.newVscConverterStation()
            .setEnsureIdUnicity(true)
            .setId(configuration.vsc0.getLocName())
            .setNode(nodeR)
            .setLossFactor((float) vscModelR.lossFactor)
            .setVoltageSetpoint(vscModelR.voltageSetpoint)
            .setReactivePowerSetpoint(vscModelR.reactivePowerSetpoint)
            .setVoltageRegulatorOn(vscModelR.voltageRegulatorOn);
        VscConverterStation cR = adderR.add();

        NodeRef nodeRefI = getNodeFromElmTerm(configuration.elmTermAc1);
        VoltageLevel voltageLevelI = getNetwork().getVoltageLevel(nodeRefI.voltageLevelId);

        // Always with internal connection
        int nodeI = voltageLevelI.getNodeBreakerView().getMaximumNodeIndex() + 1;
        createInternalConnection(voltageLevelI, nodeRefI.node, nodeI);

        VscConverterStationAdder adderI = voltageLevelI.newVscConverterStation()
            .setEnsureIdUnicity(true)
            .setId(configuration.vsc1.getLocName())
            .setNode(nodeI)
            .setLossFactor((float) vscModelI.lossFactor)
            .setVoltageSetpoint(vscModelI.voltageSetpoint)
            .setReactivePowerSetpoint(vscModelI.reactivePowerSetpoint)
            .setVoltageRegulatorOn(vscModelI.voltageRegulatorOn);
        VscConverterStation cI = adderI.add();

        HvdcLineAdder adder = getNetwork().newHvdcLine()
            .setId(configuration.links.stream().findFirst().orElseThrow().elmLne.getLocName())
            .setR(dcLineModel.r)
            .setNominalV(dcLineModel.nominalV)
            .setActivePowerSetpoint(dcLineModel.activePowerSetpoint)
            .setMaxP(dcLineModel.maxP)
            .setConvertersMode(ConvertersMode.SIDE_1_RECTIFIER_SIDE_2_INVERTER)
            .setConverterStationId1(cR.getId())
            .setConverterStationId2(cI.getId());
        adder.add();
    }

    private static final class DcLineModel {
        private final double r;
        private final double nominalV;
        private final double activePowerSetpoint;
        private final double maxP;

        private DcLineModel(double r, double nominalV, double activePowerSetpoint, double maxP) {
            this.r = r;
            this.nominalV = nominalV;
            this.activePowerSetpoint = activePowerSetpoint;
            this.maxP = maxP;
        }

        private static DcLineModel create(List<DCLink> dcLnes, DataObject elmVscRectifier) {

            double g = 0.0;
            for (DCLink hvdcLne : dcLnes) {
                DataObject typLne = hvdcLne.elmLne.getObjectAttributeValue(DataAttributeNames.TYP_ID).resolve().orElseThrow();
                double gline = obtainRLine(hvdcLne.elmLne, typLne);
                g += gline == 0.0 ? 0.0 : 1 / gline;
            }
            double r = g == 0.0 ? 0.0 : 1 / g;

            double maxP = elmVscRectifier.getFloatAttributeValue("P_max");
            double activePowerSetpoint = elmVscRectifier.getFloatAttributeValue("psetp");
            return new DcLineModel(r, obtainNominalV(dcLnes), activePowerSetpoint, maxP);
        }

        private static double obtainNominalV(List<DCLink> dcLnes) {
            DCLink hvdcLne = dcLnes.stream().findFirst().orElseThrow();
            Optional<Float> unom = hvdcLne.elmLne.findFloatAttributeValue("Unom");
            if (unom.isPresent()) {
                return unom.get();
            } else {
                DataObject typLne = hvdcLne.elmLne.getObjectAttributeValue(DataAttributeNames.TYP_ID).resolve().orElseThrow();
                return typLne.getFloatAttributeValue("uline");
            }
        }

        private static double obtainRLine(DataObject elmLne, DataObject typLne) {
            float dline = elmLne.getFloatAttributeValue("dline");
            float rline = typLne.getFloatAttributeValue("rline");
            return rline * dline;
        }
    }

    private static final class VscModel {
        private final double lossFactor;
        private final double voltageSetpoint;
        private final double reactivePowerSetpoint;
        private final boolean voltageRegulatorOn;

        private VscModel(double lossFactor, double voltageSetpoint, double reactivePowerSetpoint, boolean voltageRegulatorOn) {
            this.lossFactor = lossFactor;
            this.voltageSetpoint = voltageSetpoint;
            this.reactivePowerSetpoint = reactivePowerSetpoint;
            this.voltageRegulatorOn = voltageRegulatorOn;
        }

        private static VscModel create(DataObject elmVsc) {
            double pnold = elmVsc.getFloatAttributeValue("Pnold");
            double unom = elmVsc.getFloatAttributeValue("Unom");
            double activeSetpoint = elmVsc.getFloatAttributeValue("psetp");
            double reactiveSetpoint = elmVsc.getFloatAttributeValue("qsetp");
            double voltageSetpoint = elmVsc.getFloatAttributeValue("usetp") * unom;

            double losses = pnold / 1000.0;
            double lossFactor = activeSetpoint != 0.0 ? losses / activeSetpoint * 100 : 0.0;
            return new VscModel(lossFactor, voltageSetpoint, reactiveSetpoint, false);
        }
    }
}