Replace3TwoWindingsTransformersByThreeWindingsTransformers.java

/**
 * Copyright (c) 2024, 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.iidm.modification;

import com.powsybl.commons.report.ReportNode;
import com.powsybl.computation.ComputationManager;
import com.powsybl.iidm.modification.topology.NamingStrategy;
import com.powsybl.iidm.modification.util.RegulatedTerminalControllers;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.extensions.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

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

import static com.powsybl.iidm.modification.util.ModificationLogs.logOrThrow;
import static com.powsybl.iidm.modification.util.ModificationReports.*;
import static com.powsybl.iidm.modification.util.TransformerUtils.*;

/**
 * <p>This network modification is used to replace 3 twoWindingsTransformers by threeWindingsTransformers.</p>
 * <ul>
 *     <li>BusbarSections and the three TwoWindingsTransformers are the only connectable equipment allowed in the voltageLevel associated with the star bus.</li>
 *     <li>The three TwoWindingsTransformers must be connected to the star bus.</li>
 *     <li>The star terminals of the twoWindingsTransformers must not be regulated terminals for any controller.</li>
 *     <li>Each twoWindingsTransformer is well oriented if the star bus is located at the end 2.</li>
 *     <li>A new ThreeWindingsTransformer is created for replacing the three TwoWindingsTransformers.</li>
 *     <li>The following attributes are copied from each twoWindingsTransformer to the new associated leg:
 *         <ul>
 *             <li>Electrical characteristics, ratioTapChangers, and phaseTapChangers. Adjustments are required if the twoWindingsTransformer is not well oriented.</li>
 *             <li>Only the Operational Loading Limits  defined at the non-star end are copied to the leg.</li>
 *             <li>Active and reactive power at the non-star terminal are copied to the leg terminal.</li>
 *         </ul>
 *     </li>
 *     <li>Aliases:
 *         <ul>
 *             <li>Aliases for known CGMES identifiers (terminal, transformer end, ratio, and phase tap changer) are copied to the threeWindingsTransformer after adjusting the aliasType.</li>
 *             <li>Aliases that are not mapped are recorded in the functional log.</li>
 *         </ul>
 *     </li>
 *     <li>Properties:
 *         <ul>
 *             <li>Voltage and angle of the star bus are added as properties of the threeWindingsTransformer.</li>
 *             <li>Only the names of the transferred operational limits are copied as properties of the threeWindingsTransformer.</li>
 *             <li>All the properties of the first twoWindingsTransformer are transferred to the threeWindingsTransformer,
 *                 then those of the second that are not in the first, and finally, the properties of the third that are not in the first two.</li>
 *             <li>Properties that are not mapped are recorded in the functional log.</li>
 *         </ul>
 *     </li>
 *     <li>Extensions:
 *         <ul>
 *             <li>Only IIDM extensions are copied: TransformerFortescueData, PhaseAngleClock, and TransformerToBeEstimated.</li>
 *             <li>CGMES extensions can not be copied, as they cause circular dependencies.</li>
 *             <li>Extensions that are not copied are recorded in the functional log.</li>
 *         </ul>
 *     </li>
 *     <li>All the controllers using any of the twoWindingsTransformer terminals as regulated terminal are updated.</li>
 *     <li>New and removed equipment is also recorded in the functional log.</li>
 *     <li>Internal connections are created to manage the replacement.</li>
 *</ul>
 *
 * @author Luma Zamarre��o {@literal <zamarrenolm at aia.es>}
 * @author Jos�� Antonio Marqu��s {@literal <marquesja at aia.es>}
 */

public class Replace3TwoWindingsTransformersByThreeWindingsTransformers extends AbstractNetworkModification {

    private static final Logger LOG = LoggerFactory.getLogger(Replace3TwoWindingsTransformersByThreeWindingsTransformers.class);
    private static final String TWO_WINDINGS_TRANSFORMER = "TwoWindingsTransformer";
    private static final String WITH_FICTITIOUS_TERMINAL_USED_AS_REGULATED_TERMINAL = "with star terminal used as regulated terminal";
    private static final String CGMES_OPERATIONAL_LIMIT_SET = "CGMES.OperationalLimitSet_";

    private final List<String> transformersToBeReplaced;

    /**
     * <p>Used to replace all 3 twoWindingsTransformers by threeWindingsTransformers.</p>
     */
    public Replace3TwoWindingsTransformersByThreeWindingsTransformers() {
        this.transformersToBeReplaced = null;
    }

    /**
     * <p>Used to replace the 3 twoWindingsTransformers defined in the list by threeWindingsTransformers.
     *    To be selected, at least one of the three transformers must be included in the list.</p>
     */
    public Replace3TwoWindingsTransformersByThreeWindingsTransformers(List<String> transformersToBeReplaced) {
        this.transformersToBeReplaced = Objects.requireNonNull(transformersToBeReplaced);
    }

    @Override
    public String getName() {
        return "Replace3TwoWindingsTransformersByThreeWindingsTransformers";
    }

    @Override
    public void apply(Network network, NamingStrategy namingStrategy, boolean throwException, ComputationManager computationManager, ReportNode reportNode) {
        RegulatedTerminalControllers regulatedTerminalControllers = new RegulatedTerminalControllers(network);
        List<TwoR> twoWindingsTransformers = find3TwoWindingsTransformers(network, transformersToBeReplaced);
        twoWindingsTransformers.forEach(twoR -> replace3TwoWindingsTransformerByThreeWindingsTransformer(twoR, regulatedTerminalControllers, throwException, reportNode));
    }

    private static List<TwoR> find3TwoWindingsTransformers(Network network, List<String> transformersToBeReplaced) {
        Map<Bus, List<TransformerAndStarBusSide>> twoWindingTransformersByBus = new HashMap<>();
        network.getTwoWindingsTransformers().forEach(t2w -> {
            Bus bus1 = t2w.getTerminal1().getBusView().getBus();
            Bus bus2 = t2w.getTerminal2().getBusView().getBus();
            if (bus1 != null) {
                twoWindingTransformersByBus.computeIfAbsent(bus1, k -> new ArrayList<>()).add(new TransformerAndStarBusSide(t2w, TwoSides.ONE));
            }
            if (bus2 != null) {
                twoWindingTransformersByBus.computeIfAbsent(bus2, k -> new ArrayList<>()).add(new TransformerAndStarBusSide(t2w, TwoSides.TWO));
            }
        });
        return twoWindingTransformersByBus.keySet().stream()
                .filter(bus -> isStarBus(bus, twoWindingTransformersByBus.get(bus)))
                .sorted(Comparator.comparing(Identifiable::getId))
                .map(bus -> buildTwoR(bus, twoWindingTransformersByBus.get(bus)))
                .filter(twoR -> isGoingToBeReplaced(twoR, transformersToBeReplaced)).toList();
    }

    record TransformerAndStarBusSide(TwoWindingsTransformer transformer, TwoSides side) {
    }

    private static boolean isGoingToBeReplaced(TwoR twoR, List<String> transformersToBeReplaced) {
        return transformersToBeReplaced == null
                || transformersToBeReplaced.contains(twoR.t2w1.getId())
                || transformersToBeReplaced.contains(twoR.t2w2.getId())
                || transformersToBeReplaced.contains(twoR.t2w3.getId());
    }

    private static boolean isStarBus(Bus bus, List<TransformerAndStarBusSide> t2ws) {
        return t2ws.size() == 3 && bus.getConnectedTerminalStream().filter(connectedTerminal -> connectedTerminal.getConnectable().getType() != IdentifiableType.BUSBAR_SECTION).count() == 3;
    }

    private static TwoR buildTwoR(Bus starBus, List<TransformerAndStarBusSide> starBusT2ws) {
        List<TransformerAndStarBusSide> sortedStarBusT2ws = starBusT2ws.stream()
                .sorted(Comparator.comparingDouble((TransformerAndStarBusSide t2w) -> getNominalV(starBus, t2w.transformer()))
                        .reversed()
                        .thenComparing((TransformerAndStarBusSide t2w) -> t2w.transformer().getId()))
                .toList();

        return new TwoR(starBus, sortedStarBusT2ws.get(0), sortedStarBusT2ws.get(1), sortedStarBusT2ws.get(2));
    }

    private static double getNominalV(Bus bus, TwoWindingsTransformer t2w) {
        Bus terminalBus = t2w.getTerminal1().getBusView().getBus();
        return terminalBus != null && bus != null && terminalBus.getId().equals(bus.getId())
                ? t2w.getTerminal2().getVoltageLevel().getNominalV()
                : t2w.getTerminal1().getVoltageLevel().getNominalV();
    }

    private record TwoR(TwoWindingsTransformer t2w1, boolean isWellOrientedT2w1,
                        TwoWindingsTransformer t2w2, boolean isWellOrientedT2w2,
                        TwoWindingsTransformer t2w3, boolean isWellOrientedT2w3,
                        String starBusId, VoltageLevel starBusVoltageLevel,
                        double starBusV, double starBusAngle, List<Connectable> starBusConnectables) {
        TwoR(Bus starBus, TransformerAndStarBusSide t1, TransformerAndStarBusSide t2, TransformerAndStarBusSide t3) {
            this(t1.transformer(), isWellOriented(t1),
                    t2.transformer(), isWellOriented(t2),
                    t3.transformer(), isWellOriented(t3),
                    starBus.getId(), starBus.getVoltageLevel(),
                    starBus.getV(), starBus.getAngle(),
                    starBus.getConnectedTerminalStream().map(Terminal::getConnectable).toList());
        }

        private static boolean isWellOriented(TransformerAndStarBusSide transfoAndStarBusSide) {
            return transfoAndStarBusSide.side() == TwoSides.TWO;
        }

        public String starBusVoltageLevelId() {
            return starBusVoltageLevel.getId();
        }

        public double starBusNominalV() {
            return starBusVoltageLevel.getNominalV();
        }
    }

    // if the twoWindingsTransformer is not well oriented, and it has non-zero shunt admittance (G != 0 or B != 0)
    // the obtained model is not equivalent to the initial one as the shunt admittance must be moved to the other side
    private void replace3TwoWindingsTransformerByThreeWindingsTransformer(TwoR twoR, RegulatedTerminalControllers regulatedTerminalControllers, boolean throwException, ReportNode reportNode) {
        Substation substation = findSubstation(twoR, throwException);
        if (substation == null) {
            return;
        }
        if (anyTwoWindingsTransformerStarTerminalDefinedAsRegulatedTerminal(twoR, regulatedTerminalControllers, throwException)) {
            return;
        }
        double ratedU0 = twoR.starBusNominalV();

        ThreeWindingsTransformerAdder t3wAdder = substation.newThreeWindingsTransformer()
                .setEnsureIdUnicity(true)
                .setId(getId(twoR))
                .setName(getName(twoR))
                .setRatedU0(ratedU0);

        addLeg(t3wAdder.newLeg1(), twoR.t2w1, twoR.isWellOrientedT2w1, ratedU0);
        addLeg(t3wAdder.newLeg2(), twoR.t2w2, twoR.isWellOrientedT2w2, ratedU0);
        addLeg(t3wAdder.newLeg3(), twoR.t2w3, twoR.isWellOrientedT2w3, ratedU0);
        ThreeWindingsTransformer t3w = t3wAdder.add();

        // t3w is not considered in regulatedTerminalControllers (created later in the model)
        setLegData(t3w.getLeg1(), twoR.t2w1, twoR.isWellOrientedT2w1, regulatedTerminalControllers, twoR);
        setLegData(t3w.getLeg2(), twoR.t2w2, twoR.isWellOrientedT2w2, regulatedTerminalControllers, twoR);
        setLegData(t3w.getLeg3(), twoR.t2w3, twoR.isWellOrientedT2w3, regulatedTerminalControllers, twoR);

        copyStarBusVoltageAndAngle(twoR.starBusV(), twoR.starBusAngle(), t3w);
        List<PropertyR> lostProperties = new ArrayList<>();
        lostProperties.addAll(copyProperties(twoR.t2w1, t3w));
        lostProperties.addAll(copyProperties(twoR.t2w2, t3w));
        lostProperties.addAll(copyProperties(twoR.t2w3, t3w));

        List<ExtensionR> lostExtensions = copyExtensions(twoR, t3w);

        // copy necessary data before removing
        List<AliasR> t2wAliases = new ArrayList<>();
        t2wAliases.addAll(getAliases(twoR.t2w1, "1", getEnd1(twoR.isWellOrientedT2w1)));
        t2wAliases.addAll(getAliases(twoR.t2w2, "2", getEnd1(twoR.isWellOrientedT2w2)));
        t2wAliases.addAll(getAliases(twoR.t2w3, "3", getEnd1(twoR.isWellOrientedT2w3)));

        String t2w1Id = twoR.t2w1.getId();
        String t2w2Id = twoR.t2w2.getId();
        String t2w3Id = twoR.t2w3.getId();
        String starVoltageId = twoR.starBusVoltageLevelId();
        List<LimitsR> lostLimits = findLostLimits(twoR);

        remove(twoR);

        // after removing
        List<AliasR> lostAliases = copyAliases(t2wAliases, t3w);

        // warnings
        if (!lostProperties.isEmpty()) {
            lostProperties.forEach(propertyR -> LOG.warn("Property '{}' of twoWindingsTransformer '{}' was not transferred", propertyR.propertyName, propertyR.t2wId));
        }
        if (!lostExtensions.isEmpty()) {
            lostExtensions.forEach(extensionR -> LOG.warn("Extension '{}' of twoWindingsTransformer '{}' was not transferred", extensionR.extensionName, extensionR.t2wId));
        }
        if (!lostAliases.isEmpty()) {
            lostAliases.forEach(aliasR -> LOG.warn("Alias '{}' '{}' of twoWindingsTransformer '{}' was not transferred", aliasR.alias, aliasR.aliasType, aliasR.t2wId));
        }
        if (!lostLimits.isEmpty()) {
            lostLimits.forEach(limitsR -> LOG.warn("OperationalLimitsGroup '{}' of twoWindingsTransformer '{}' is lost", limitsR.operationalLimitsGroupName, limitsR.t2wId));
        }

        // report
        createReportNode(reportNode, t2w1Id, t2w2Id, t2w3Id, starVoltageId, lostProperties, lostExtensions, lostAliases, lostLimits, t3w.getId());
    }

    private static void addLeg(ThreeWindingsTransformerAdder.LegAdder legAdder, TwoWindingsTransformer t2w, boolean isWellOriented, double ratedU0) {
        legAdder.setVoltageLevel(findVoltageLevel(t2w, isWellOriented).getId())
                .setR(findImpedance(t2w.getR(), getStructuralRatio(t2w), isWellOriented))
                .setX(findImpedance(t2w.getX(), getStructuralRatio(t2w), isWellOriented))
                .setG(findAdmittance(t2w.getG(), getStructuralRatio(t2w), isWellOriented))
                .setB(findAdmittance(t2w.getB(), getStructuralRatio(t2w), isWellOriented))
                .setRatedU(getRatedU1(t2w, ratedU0, isWellOriented));
        connectAfterCreatingInternalConnection(legAdder, t2w, isWellOriented);
        legAdder.add();
    }

    private static void setLegData(ThreeWindingsTransformer.Leg leg, TwoWindingsTransformer t2w, boolean isWellOriented, RegulatedTerminalControllers regulatedTerminalControllers, TwoR twoR) {
        t2w.getOptionalRatioTapChanger().ifPresent(rtc -> copyOrMoveRatioTapChanger(leg.newRatioTapChanger(), rtc, isWellOriented));
        t2w.getOptionalPhaseTapChanger().ifPresent(ptc -> copyOrMovePhaseTapChanger(leg.newPhaseTapChanger(), ptc, isWellOriented));

        getOperationalLimitsGroups1(t2w, isWellOriented)
                .forEach(operationalLimitGroup -> copyOperationalLimitsGroup(leg.newOperationalLimitsGroup(operationalLimitGroup.getId()), operationalLimitGroup));

        regulatedTerminalControllers.replaceRegulatedTerminal(getTerminal1(t2w, isWellOriented), leg.getTerminal());
        replaceRegulatedTerminal(leg, twoR);

        copyTerminalActiveAndReactivePower(leg.getTerminal(), getTerminal1(t2w, isWellOriented));
    }

    private Substation findSubstation(TwoR twoR, boolean throwException) {
        Optional<Substation> substation = twoR.t2w1.getSubstation();
        if (substation.isEmpty()) {
            logOrThrow(throwException, TWO_WINDINGS_TRANSFORMER + "'" + twoR.t2w1.getId() + "' without substation");
            return null;
        } else {
            return substation.get();
        }
    }

    private boolean anyTwoWindingsTransformerStarTerminalDefinedAsRegulatedTerminal(TwoR twoR, RegulatedTerminalControllers regulatedTerminalControllers, boolean throwException) {
        if (regulatedTerminalControllers.usedAsRegulatedTerminal(getTerminal2(twoR.t2w1, twoR.isWellOrientedT2w1))) {
            logOrThrow(throwException, TWO_WINDINGS_TRANSFORMER + "'" + twoR.t2w1.getId() + "' " + WITH_FICTITIOUS_TERMINAL_USED_AS_REGULATED_TERMINAL);
            return true;
        }
        if (regulatedTerminalControllers.usedAsRegulatedTerminal(getTerminal2(twoR.t2w2, twoR.isWellOrientedT2w2))) {
            logOrThrow(throwException, TWO_WINDINGS_TRANSFORMER + "'" + twoR.t2w2.getId() + "' " + WITH_FICTITIOUS_TERMINAL_USED_AS_REGULATED_TERMINAL);
            return true;
        }
        if (regulatedTerminalControllers.usedAsRegulatedTerminal(getTerminal2(twoR.t2w3, twoR.isWellOrientedT2w3))) {
            logOrThrow(throwException, TWO_WINDINGS_TRANSFORMER + "'" + twoR.t2w3.getId() + "' " + WITH_FICTITIOUS_TERMINAL_USED_AS_REGULATED_TERMINAL);
            return true;
        }
        return false;
    }

    private static String getId(TwoR twoR) {
        return twoR.t2w1.getId() + "-" + twoR.t2w2.getId() + "-" + twoR.t2w3.getId();
    }

    private static String getName(TwoR twoR) {
        return twoR.t2w1.getNameOrId() + "-" + twoR.t2w2.getNameOrId() + "-" + twoR.t2w3.getNameOrId();
    }

    private static VoltageLevel findVoltageLevel(TwoWindingsTransformer t2w, boolean isWellOriented) {
        return isWellOriented ? t2w.getTerminal1().getVoltageLevel() : t2w.getTerminal2().getVoltageLevel();
    }

    private static double getStructuralRatio(TwoWindingsTransformer twt) {
        return twt.getRatedU1() / twt.getRatedU2();
    }

    private static double findImpedance(double impedance, double a, boolean isWellOriented) {
        return isWellOriented ? impedance : impedanceConversion(impedance, a);
    }

    private static double findAdmittance(double admittance, double a, boolean isWellOriented) {
        return isWellOriented ? admittance : admittanceConversion(admittance, a);
    }

    private static double getRatedU1(TwoWindingsTransformer t2w, double ratedU0, boolean isWellOriented) {
        return isWellOriented ? getStructuralRatio(t2w) * ratedU0 : ratedU0 / getStructuralRatio(t2w);
    }

    private static void connectAfterCreatingInternalConnection(ThreeWindingsTransformerAdder.LegAdder legAdder, TwoWindingsTransformer t2w, boolean isWellOriented) {
        Terminal terminal = getTerminal1(t2w, isWellOriented);
        if (terminal.getVoltageLevel().getTopologyKind() == TopologyKind.NODE_BREAKER) {
            int newNode = terminal.getVoltageLevel().getNodeBreakerView().getMaximumNodeIndex() + 1;
            terminal.getVoltageLevel().getNodeBreakerView()
                    .newInternalConnection()
                    .setNode1(terminal.getNodeBreakerView().getNode())
                    .setNode2(newNode).add();
            legAdder.setNode(newNode);
        } else {
            legAdder.setConnectableBus(terminal.getBusBreakerView().getConnectableBus().getId());
            Bus bus = terminal.getBusBreakerView().getBus();
            if (bus != null) {
                legAdder.setBus(bus.getId());
            }
        }
    }

    private static void copyOrMoveRatioTapChanger(RatioTapChangerAdder rtcAdder, RatioTapChanger rtc, boolean isWellOriented) {
        if (isWellOriented) {
            copyAndAddRatioTapChanger(rtcAdder, rtc);
        } else {
            copyAndMoveAndAddRatioTapChanger(rtcAdder, rtc);
        }
    }

    private static void copyOrMovePhaseTapChanger(PhaseTapChangerAdder ptcAdder, PhaseTapChanger ptc, boolean isWellOriented) {
        if (isWellOriented) {
            copyAndAddPhaseTapChanger(ptcAdder, ptc);
        } else {
            copyAndMoveAndAddPhaseTapChanger(ptcAdder, ptc);
        }
    }

    private static Collection<OperationalLimitsGroup> getOperationalLimitsGroups1(TwoWindingsTransformer t2w, boolean isWellOriented) {
        return isWellOriented ? t2w.getOperationalLimitsGroups1() : t2w.getOperationalLimitsGroups2();
    }

    private static Terminal getTerminal1(TwoWindingsTransformer t2w, boolean isWellOriented) {
        return isWellOriented ? t2w.getTerminal1() : t2w.getTerminal2();
    }

    private static Terminal getTerminal2(TwoWindingsTransformer t2w, boolean isWellOriented) {
        return isWellOriented ? t2w.getTerminal2() : t2w.getTerminal1();
    }

    private static String getEnd1(boolean isWellOriented) {
        return isWellOriented ? "1" : "2";
    }

    private static void replaceRegulatedTerminal(ThreeWindingsTransformer.Leg t3wLeg, TwoR twoR) {
        t3wLeg.getOptionalRatioTapChanger().ifPresent(rtc -> findNewRegulatedTerminal(rtc.getRegulationTerminal(), t3wLeg.getTransformer(), twoR).ifPresent(rtc::setRegulationTerminal));
        t3wLeg.getOptionalPhaseTapChanger().ifPresent(ptc -> findNewRegulatedTerminal(ptc.getRegulationTerminal(), t3wLeg.getTransformer(), twoR).ifPresent(ptc::setRegulationTerminal));
    }

    private static Optional<Terminal> findNewRegulatedTerminal(Terminal regulatedTerminal, ThreeWindingsTransformer t3w, TwoR twoR) {
        if (isRegulatedTerminalInTwoWindingsTransformer(regulatedTerminal, twoR.t2w1)) {
            return Optional.of(t3w.getTerminal(ThreeSides.ONE));
        } else if (isRegulatedTerminalInTwoWindingsTransformer(regulatedTerminal, twoR.t2w2)) {
            return Optional.of(t3w.getTerminal(ThreeSides.TWO));
        } else if (isRegulatedTerminalInTwoWindingsTransformer(regulatedTerminal, twoR.t2w3)) {
            return Optional.of(t3w.getTerminal(ThreeSides.THREE));
        } else {
            return Optional.empty();
        }
    }

    // we do not check the side, threeWindingsTransformers can only be controlled on the non-star side
    private static boolean isRegulatedTerminalInTwoWindingsTransformer(Terminal regulatedTerminal, TwoWindingsTransformer t2w) {
        return regulatedTerminal != null && regulatedTerminal.getConnectable().getId().equals(t2w.getId());
    }

    private static void copyStarBusVoltageAndAngle(double starBusV, double starBusAngle, ThreeWindingsTransformer t3w) {
        if (Double.isFinite(starBusV) && starBusV > 0.0 && Double.isFinite(starBusAngle)) {
            t3w.setProperty("v", String.valueOf(starBusV));
            t3w.setProperty("angle", String.valueOf(starBusAngle));
        }
    }

    private static List<PropertyR> copyProperties(TwoWindingsTransformer t2w, ThreeWindingsTransformer t3w) {
        List<PropertyR> lostProperties = new ArrayList<>();
        t2w.getPropertyNames().forEach(propertyName -> {
            boolean copied = copyProperty(propertyName, t2w.getProperty(propertyName), t3w);
            if (!copied) {
                lostProperties.add(new PropertyR(t2w.getId(), propertyName));
            }
        });
        return lostProperties;
    }

    private static boolean copyProperty(String propertyName, String property, ThreeWindingsTransformer t3w) {
        boolean copied = true;
        if (propertyName.startsWith(CGMES_OPERATIONAL_LIMIT_SET)) {
            if (t3w.getLeg1().getOperationalLimitsGroups().stream().anyMatch(operationalLimitsGroup -> propertyName.equals(CGMES_OPERATIONAL_LIMIT_SET + operationalLimitsGroup.getId()))) {
                t3w.setProperty(propertyName, property);
            } else if (t3w.getLeg2().getOperationalLimitsGroups().stream().anyMatch(operationalLimitsGroup -> propertyName.equals(CGMES_OPERATIONAL_LIMIT_SET + operationalLimitsGroup.getId()))) {
                t3w.setProperty(propertyName, property);
            } else if (t3w.getLeg3().getOperationalLimitsGroups().stream().anyMatch(operationalLimitsGroup -> propertyName.equals(CGMES_OPERATIONAL_LIMIT_SET + operationalLimitsGroup.getId()))) {
                t3w.setProperty(propertyName, property);
            } else {
                copied = false;
            }
        } else {
            if (t3w.getPropertyNames().contains(propertyName)) {
                copied = false;
            } else {
                t3w.setProperty(propertyName, property);
            }
        }
        return copied;
    }

    private record PropertyR(String t2wId, String propertyName) {
    }

    // TODO For now, only a few extensions are supported. But a wider mechanism should be developed to support custom extensions.
    private static List<ExtensionR> copyExtensions(TwoR twoR, ThreeWindingsTransformer t3w) {
        List<ExtensionR> extensions = new ArrayList<>();
        extensions.addAll(twoR.t2w1.getExtensions().stream().map(extension -> new ExtensionR(twoR.t2w1.getId(), extension.getName())).toList());
        extensions.addAll(twoR.t2w2.getExtensions().stream().map(extension -> new ExtensionR(twoR.t2w2.getId(), extension.getName())).toList());
        extensions.addAll(twoR.t2w3.getExtensions().stream().map(extension -> new ExtensionR(twoR.t2w3.getId(), extension.getName())).toList());

        List<ExtensionR> lostExtensions = new ArrayList<>();
        extensions.stream().map(extensionR -> extensionR.extensionName).collect(Collectors.toSet()).forEach(extensionName -> {
            boolean copied = copyExtension(extensionName, twoR, t3w);
            if (!copied) {
                lostExtensions.addAll(extensions.stream().filter(extensionR -> extensionR.extensionName.equals(extensionName)).toList());
            }
        });
        return lostExtensions;
    }

    private record ExtensionR(String t2wId, String extensionName) {
    }

    private static boolean copyExtension(String extensionName, TwoR twoR, ThreeWindingsTransformer t3w) {
        boolean copied = true;
        switch (extensionName) {
            case "twoWindingsTransformerFortescue" ->
                    copyAndAddFortescue(t3w.newExtension(ThreeWindingsTransformerFortescueAdder.class),
                            twoR.t2w1.getExtension(TwoWindingsTransformerFortescue.class), twoR.isWellOrientedT2w1,
                            twoR.t2w2.getExtension(TwoWindingsTransformerFortescue.class), twoR.isWellOrientedT2w2,
                            twoR.t2w3.getExtension(TwoWindingsTransformerFortescue.class), twoR.isWellOrientedT2w3);
            case "twoWindingsTransformerPhaseAngleClock" ->
                    copyAndAddPhaseAngleClock(t3w.newExtension(ThreeWindingsTransformerPhaseAngleClockAdder.class),
                            twoR.t2w2.getExtension(TwoWindingsTransformerPhaseAngleClock.class),
                            twoR.t2w3.getExtension(TwoWindingsTransformerPhaseAngleClock.class));
            case "twoWindingsTransformerToBeEstimated" ->
                    copyAndAddToBeEstimated(t3w.newExtension(ThreeWindingsTransformerToBeEstimatedAdder.class),
                            twoR.t2w1.getExtension(TwoWindingsTransformerToBeEstimated.class),
                            twoR.t2w2.getExtension(TwoWindingsTransformerToBeEstimated.class),
                            twoR.t2w3.getExtension(TwoWindingsTransformerToBeEstimated.class));
            default -> copied = false;
        }
        return copied;
    }

    private static List<AliasR> getAliases(TwoWindingsTransformer t2w, String leg, String end) {
        return t2w.getAliases().stream().map(alias -> new AliasR(t2w.getId(), alias, t2w.getAliasType(alias).orElse(""), leg, end)).toList();
    }

    private static List<AliasR> copyAliases(List<AliasR> t2wAliases, ThreeWindingsTransformer t3w) {
        List<AliasR> lostAliases = new ArrayList<>();
        t2wAliases.forEach(aliasR -> {
            boolean copied = copyAlias(aliasR.alias, aliasR.aliasType, aliasR.leg, aliasR.end, t3w);
            if (!copied) {
                lostAliases.add(aliasR);
            }
        });
        return lostAliases;
    }

    private static boolean copyAlias(String alias, String aliasType, String leg, String end, ThreeWindingsTransformer t3w) {
        boolean copied = true;
        if (aliasType.equals("CGMES.TransformerEnd" + end)) {
            t3w.addAlias(alias, "CGMES.TransformerEnd" + leg, true);
        } else if (aliasType.equals("CGMES.Terminal" + end)) {
            t3w.addAlias(alias, "CGMES.Terminal" + leg, true);
        } else if (aliasType.equals("CGMES.RatioTapChanger1")) {
            t3w.addAlias(alias, "CGMES.RatioTapChanger" + leg, true);
        } else if (aliasType.equals("CGMES.PhaseTapChanger1")) {
            t3w.addAlias(alias, "CGMES.PhaseTapChanger" + leg, true);
        } else {
            copied = false;
        }
        return copied;
    }

    private record AliasR(String t2wId, String alias, String aliasType, String leg, String end) {
    }

    private static void remove(TwoR twoR) {
        VoltageLevel voltageLevel = twoR.starBusVoltageLevel();
        twoR.starBusConnectables().forEach(Connectable::remove);
        voltageLevel.remove();
    }

    private static List<LimitsR> findLostLimits(TwoR twoR) {
        List<LimitsR> lostLimits = new ArrayList<>();
        getOperationalLimitsGroups2(twoR.t2w1, twoR.isWellOrientedT2w1).forEach(operationalLimitsGroup -> lostLimits.add(new LimitsR(twoR.t2w1.getId(), operationalLimitsGroup.getId())));
        getOperationalLimitsGroups2(twoR.t2w2, twoR.isWellOrientedT2w2).forEach(operationalLimitsGroup -> lostLimits.add(new LimitsR(twoR.t2w2.getId(), operationalLimitsGroup.getId())));
        getOperationalLimitsGroups2(twoR.t2w3, twoR.isWellOrientedT2w3).forEach(operationalLimitsGroup -> lostLimits.add(new LimitsR(twoR.t2w3.getId(), operationalLimitsGroup.getId())));
        return lostLimits;
    }

    private static Collection<OperationalLimitsGroup> getOperationalLimitsGroups2(TwoWindingsTransformer t2w, boolean isWellOriented) {
        return isWellOriented ? t2w.getOperationalLimitsGroups2() : t2w.getOperationalLimitsGroups1();
    }

    private record LimitsR(String t2wId, String operationalLimitsGroupName) {
    }

    private static void createReportNode(ReportNode reportNode, String t2w1Id, String t2w2Id, String t2w3Id, String starVoltageId,
                                         List<PropertyR> lostProperties, List<ExtensionR> lostExtensions, List<AliasR> lostAliases,
                                         List<LimitsR> lostLimits, String t3wId) {

        ReportNode reportNodeReplacement = replace3TwoWindingsTransformersByThreeWindingsTransformersReport(reportNode);

        removedTwoWindingsTransformerReport(reportNodeReplacement, t2w1Id);
        removedTwoWindingsTransformerReport(reportNodeReplacement, t2w2Id);
        removedTwoWindingsTransformerReport(reportNodeReplacement, t2w3Id);
        removedVoltageLevelReport(reportNodeReplacement, starVoltageId);

        if (!lostProperties.isEmpty()) {
            Set<String> t2wIds = lostProperties.stream().map(propertyR -> propertyR.t2wId).collect(Collectors.toSet());
            t2wIds.stream().sorted().forEach(t2wId -> {
                String properties = String.join(",", lostProperties.stream().filter(propertyR -> propertyR.t2wId.equals(t2wId)).map(propertyR -> propertyR.propertyName).toList());
                lostTwoWindingsTransformerProperties(reportNodeReplacement, properties, t2wId);
            });
        }
        if (!lostExtensions.isEmpty()) {
            Set<String> t2wIds = lostExtensions.stream().map(extensionR -> extensionR.t2wId).collect(Collectors.toSet());
            t2wIds.stream().sorted().forEach(t2wId -> {
                String extensions = String.join(",", lostExtensions.stream().filter(extensionR -> extensionR.t2wId.equals(t2wId)).map(extensionR -> extensionR.extensionName).toList());
                lostTwoWindingsTransformerExtensions(reportNodeReplacement, extensions, t2wId);
            });
        }
        if (!lostAliases.isEmpty()) {
            Set<String> t2wIds = lostAliases.stream().map(aliasR -> aliasR.t2wId).collect(Collectors.toSet());
            t2wIds.stream().sorted().forEach(t2wId -> {
                String aliases = lostAliases.stream().filter(aliasR -> aliasR.t2wId.equals(t2wId)).map(AliasR::alias).collect(Collectors.joining(","));
                lostTwoWindingsTransformerAliases(reportNodeReplacement, aliases, t2wId);
            });
        }
        if (!lostLimits.isEmpty()) {
            Set<String> t2wIds = lostLimits.stream().map(limitsR -> limitsR.t2wId).collect(Collectors.toSet());
            t2wIds.stream().sorted().forEach(t2wId -> {
                String limits = lostLimits.stream().filter(limitsR -> limitsR.t2wId.equals(t2wId)).map(LimitsR::operationalLimitsGroupName).collect(Collectors.joining(","));
                lostTwoWindingsTransformerOperationalLimitsGroups(reportNodeReplacement, limits, t2wId);
            });
        }

        createdThreeWindingsTransformerReport(reportNodeReplacement, t3wId);
    }
}