DCConversion.java

/**
 * 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.cgmes.conversion.elements.dc;

import com.powsybl.cgmes.conversion.CgmesReports;
import com.powsybl.cgmes.conversion.Context;
import com.powsybl.cgmes.model.CgmesModel;
import com.powsybl.commons.PowsyblException;
import com.powsybl.triplestore.api.PropertyBag;
import com.powsybl.triplestore.api.PropertyBags;

import java.util.*;
import java.util.function.Predicate;

import static com.powsybl.cgmes.model.CgmesNames.*;

/**
 * @author Romain Courtier {@literal <romain.courtier at rte-france.com>}
 */
public class DCConversion {

    private final Context context;

    private PropertyBags cgmesDcTerminalNodes;
    private PropertyBags cgmesAcDcConverters;
    private PropertyBags cgmesDcLineSegments;
    private PropertyBags cgmesDcSwitches;
    private PropertyBags cgmesDcGrounds;

    private final Map<String, String> dcTerminalNodes = new HashMap<>();
    private final Set<DCEquipment> dcEquipments = new HashSet<>();
    private final Set<DCIslandEnd> dcIslandEnds = new HashSet<>();
    private final Set<DCIsland> dcIslands = new HashSet<>();

    public DCConversion(CgmesModel cgmesModel, Context context) {
        this.context = Objects.requireNonNull(context);
        cacheCgmesData(cgmesModel);
        computeDcData();
        convert();
    }

    private void cacheCgmesData(CgmesModel cgmesModel) {
        cgmesDcTerminalNodes = cgmesModel.dcTerminals();
        cgmesAcDcConverters = cgmesModel.acDcConverters();
        cgmesDcLineSegments = cgmesModel.dcLineSegments();
        cgmesDcSwitches = cgmesModel.dcSwitches();
        cgmesDcGrounds = cgmesModel.dcGrounds();
    }

    private void computeDcData() {
        computeDcEquipments();
        computeDcIslandEnds();
        computeDcIslands();
    }

    private void computeDcEquipments() {
        // Store the CGMES terminal to CGMES node association.
        String node = context.nodeBreaker() ? DC_NODE : DC_TOPOLOGICAL_NODE;
        cgmesDcTerminalNodes.forEach(t -> dcTerminalNodes.put(t.getId(DC_TERMINAL), t.getId(node)));

        // Store the CGMES DCEquipment base data: id, type, node1, node2
        cgmesAcDcConverters.forEach(c -> addDcEquipment(c, ACDC_CONVERTER));
        cgmesDcLineSegments.forEach(l -> addDcEquipment(l, DC_LINE_SEGMENT));
        cgmesDcSwitches.forEach(s -> addDcEquipment(s, DC_SWITCH));
        cgmesDcGrounds.forEach(g -> addDcEquipment(g, DC_GROUND));
    }

    private void addDcEquipment(PropertyBag propertyBag, String type) {
        dcEquipments.add(new DCEquipment(
                propertyBag.getId(type),
                ACDC_CONVERTER.equals(type) ? propertyBag.getLocal("type") : type,
                terminalNode(propertyBag.getId(DC_TERMINAL1)),
                DC_GROUND.equals(type) ? null : terminalNode(propertyBag.getId(DC_TERMINAL2))
        ));
    }

    private String terminalNode(String terminalId) {
        // Get the CGMES node of a CGMES terminal.
        if (!dcTerminalNodes.containsKey(terminalId)) {
            throw new PowsyblException("DCTerminal not found");
        }
        return dcTerminalNodes.get(terminalId);
    }

    private void computeDcIslandEnds() {
        // Compute DCIslandEnd: a collection of DCEquipment connected together on the same side of a DCLineSegment.
        // This comprises DCLineSegment, so a specific DCLineSegment shall be present in 2 DCIslandEnds.
        // The exploration starts with ACDCConverters, this ensures that an island end has at least one converter.
        Set<DCEquipment> visitedDcEquipments = new HashSet<>();
        dcEquipments.stream().filter(DCEquipment::isConverter)
                .forEach(acDcConverter -> {
                    if (!visitedDcEquipments.contains(acDcConverter)) {
                        Set<DCEquipment> dcIslandEnd = new HashSet<>();
                        getAdjacentDcEquipments(acDcConverter, dcIslandEnd);
                        visitedDcEquipments.addAll(dcIslandEnd);
                        dcIslandEnds.add(new DCIslandEnd(dcIslandEnd));
                    }
                });

        // Log not visited DCEquipment.
        Set<DCEquipment> notVisitedDcEquipments = new HashSet<>(dcEquipments);
        notVisitedDcEquipments.removeAll(visitedDcEquipments);
        notVisitedDcEquipments.forEach(dcEquipment ->
                CgmesReports.notVisitedDcEquipmentReport(context.getReportNode(), dcEquipment.id()));
    }

    private void getAdjacentDcEquipments(DCEquipment dcEquipment, Set<DCEquipment> dcIslandEnd) {
        // Recursively get all adjacent DCEquipment.
        // DCLineSegment are included but not traversed.
        if (!dcIslandEnd.contains(dcEquipment)) {
            dcIslandEnd.add(dcEquipment);
            if (!dcEquipment.isLine()) {
                dcEquipments.stream()
                        .filter(e -> e != dcEquipment && e.isAdjacentTo(dcEquipment) && !dcIslandEnd.contains(e))
                        .forEach(e -> getAdjacentDcEquipments(e, dcIslandEnd));
            }
        }
    }

    private void computeDcIslands() {
        // Compute DCIsland: a collection of DCIslandEnd connected together via shared DCLineSegment(s).
        Set<DCIslandEnd> visitedDcIslandEnds = new HashSet<>();
        dcIslandEnds.forEach(dcIslandEnd -> {
            if (!visitedDcIslandEnds.contains(dcIslandEnd)) {
                Set<DCIslandEnd> dcIsland = new HashSet<>();
                getAdjacentDcIslandEnds(dcIslandEnd, dcIsland);
                visitedDcIslandEnds.addAll(dcIsland);
                dcIslands.add(new DCIsland(dcIsland));
            }
        });
    }

    private void getAdjacentDcIslandEnds(DCIslandEnd dcIslandEnd, Set<DCIslandEnd> dcIsland) {
        // Recursively get all adjacent DCIslandEnd.
        if (!dcIsland.contains(dcIslandEnd)) {
            dcIsland.add(dcIslandEnd);
            dcIslandEnds.stream()
                    .filter(end -> end != dcIslandEnd && end.isAdjacentTo(dcIslandEnd) && !dcIsland.contains(end))
                    .forEach(end -> getAdjacentDcIslandEnds(end, dcIsland));
        }
    }

    private void convert() {
        for (DCIsland dcIsland : dcIslands) {
            if (dcIsland.valid(context)) {
                convertDcLinks(dcIsland);
            }
        }
    }

    private void convertDcLinks(DCIsland dcIsland) {
        // Get island poles.
        List<DCPole> dcPoles = getDcPoles(dcIsland);
        List<DCLink> dcLinks = new ArrayList<>();
        for (DCPole dcPole : dcPoles) {
            // Create 1 or 2 DCLink per pole, depending on the number of bridges per pole.
            PropertyBag converter1 = getConverterBag(dcPole.getConverter1A());
            PropertyBag converter2 = getConverterBag(dcPole.getConverter2A());
            PropertyBag dcLine1 = getDcLineSegmentBag(dcPole.getDcLine1());
            PropertyBag dcLine2 = null;
            if (dcPole.getDcLine2() != null) {
                dcLine2 = getDcLineSegmentBag(dcPole.getDcLine2());
                if (dcPole.isHalfOfBipole()) {
                    // The Dedicated Metallic Return line of a bipole is retrieved solely to keep its id as an alias.
                    // Its resistance should not be taken into account since there is no flow through it in nominal cases.
                    dcLine2.put("r", "0");
                }
            }
            if (dcPole.getConverter1B() == null) {
                dcLinks.add(new DCLink(converter1, converter2, dcLine1, dcLine2));
            } else {
                PropertyBag converter1B = getConverterBag(dcPole.getConverter1B());
                PropertyBag converter2B = getConverterBag(dcPole.getConverter2B());
                PropertyBag dcLine1B = splitDcLineSegmentBag(dcLine1);
                dcLinks.add(new DCLink(converter1, converter2, dcLine1, dcLine2));
                dcLinks.add(new DCLink(converter1B, converter2B, dcLine1B, null));
            }
        }

        // Convert DCLink.
        dcLinks.forEach(dcLink -> {
            new HvdcConverterConversion(dcLink.getConverter1(), dcLink.getLossFactor1(), context).convert();
            new HvdcConverterConversion(dcLink.getConverter2(), dcLink.getLossFactor2(), context).convert();
            new HvdcLineConversion(dcLink, context).convert();
        });
    }

    private List<DCPole> getDcPoles(DCIsland dcIsland) {
        // Sort island ends.
        // The first island end is the one that has the more dc line segments node 1.
        // In case of equality, it is the one with the lowest converter id.
        List<DCIslandEnd> islandEnds = dcIsland.dcIslandEnds().stream()
                .sorted(Comparator.<DCIslandEnd>comparingLong(e -> e.getDcLineSegments().stream()
                                .filter(l -> e.dcEquipments().stream()
                                        .anyMatch(eq -> !eq.equals(l) && eq.isConnectedTo(l.node1())))
                                .count())
                        .reversed()
                        .thenComparing(e -> e.getAcDcConverters().stream()
                                .map(DCEquipment::id)
                                .min(Comparator.naturalOrder())
                                .orElseThrow()))
                .toList();

        // Retrieve ACDCConverter and DCLineSegment.
        List<DCEquipment> converters1 = islandEnds.get(0).getAcDcConverters();
        List<DCEquipment> dcLineSegments = islandEnds.get(0).getDcLineSegments();
        boolean isBipole = converters1.size() > 1 && dcLineSegments.size() > 1;

        // Get energized lines and DMR line (if any).
        DCEquipment dMRLine = null;
        List<DCEquipment> energizedLines = new ArrayList<>(dcLineSegments);
        if (hasDMRLine(converters1.size(), dcLineSegments.size())) {
            dMRLine = getDMRLine(dcIsland, dcLineSegments);
            energizedLines.remove(dMRLine);
        }

        // For each energized line, find the nearest converter on each side.
        List<DCPole> dcPoles = new ArrayList<>();
        Set<DCEquipment> usedConverters1 = new HashSet<>();
        Set<DCEquipment> usedConverters2 = new HashSet<>();
        for (DCEquipment dcLineSegment : energizedLines) {
            Predicate<DCEquipment> eligibleConverter1 = e -> e.isConverter() && !usedConverters1.contains(e);
            DCEquipment converter1 = islandEnds.get(0).getNearestConverter(dcLineSegment, eligibleConverter1);
            usedConverters1.add(converter1);

            Predicate<DCEquipment> eligibleConverter2 = e -> e.type().equals(converter1.type()) && !usedConverters2.contains(e);
            DCEquipment converter2 = islandEnds.get(1).getNearestConverter(dcLineSegment, eligibleConverter2);
            usedConverters2.add(converter2);

            dcPoles.add(new DCPole(converter1, converter2, dcLineSegment, isBipole));
        }

        // In case of multiple converters (bridges) per pole, add the second converter pair to an existing pole.
        Set<DCEquipment> unmappedConverters1 = new HashSet<>(converters1);
        Set<DCEquipment> eligibleConverters1A = new HashSet<>(usedConverters1);
        unmappedConverters1.removeAll(usedConverters1);
        for (DCEquipment converter1B : unmappedConverters1) {
            Predicate<DCEquipment> eligibleConverter1A = e -> e.type().equals(converter1B.type()) && eligibleConverters1A.contains(e);
            DCEquipment converter1A = islandEnds.get(0).getNearestConverter(converter1B, eligibleConverter1A);
            eligibleConverters1A.remove(converter1A);

            DCPole dcPole = dcPoles.stream().filter(p -> p.getConverter1A().equals(converter1A)).findFirst().orElseThrow();
            DCEquipment converter2A = dcPole.getConverter2A();

            Predicate<DCEquipment> eligibleConverter2B = e -> e.type().equals(converter1B.type()) && !usedConverters2.contains(e);
            DCEquipment converter2B = islandEnds.get(1).getNearestConverter(converter2A, eligibleConverter2B);
            usedConverters2.add(converter2B);

            dcPole.addSecondBridge(converter1B, converter2B);
        }

        // Add DMR line to the first pole.
        dcPoles.get(0).addMetallicReturnLine(dMRLine);

        return dcPoles;
    }

    private boolean hasDMRLine(int numberOfConverters, int numberOfLines) {
        return numberOfConverters == 1 && numberOfLines > 1 || numberOfLines > 2;
    }

    private DCEquipment getDMRLine(DCIsland dcIsland, List<DCEquipment> dcLineSegments) {
        // Either the DMR line is clearly identifiable since it's the only grounded line of the island.
        // Or it's not, and the most central line (the last one since they are sorted) is considered to be the DMR.
        List<DCEquipment> groundedLines = dcLineSegments.stream()
                .filter(dcIsland::isGrounded)
                .toList();
        if (groundedLines.size() == 1) {
            return groundedLines.get(0);
        }
        return dcLineSegments.get(dcLineSegments.size() - 1);
    }

    private PropertyBag splitDcLineSegmentBag(PropertyBag dcLineSegment) {
        // Divide the original resistance by 2, and make the identifiers of the copy unique.
        double r = dcLineSegment.asDouble("r");
        r = Double.isNaN(r) ? 0.1 : r / 2;
        dcLineSegment.put("r", Double.toString(r));
        PropertyBag otherDcLineSegment = (PropertyBag) dcLineSegment.clone();
        otherDcLineSegment.put(DC_LINE_SEGMENT, otherDcLineSegment.get(DC_LINE_SEGMENT) + "-1");
        otherDcLineSegment.put(DC_TERMINAL1, otherDcLineSegment.get(DC_TERMINAL1) + "-1");
        otherDcLineSegment.put(DC_TERMINAL2, otherDcLineSegment.get(DC_TERMINAL2) + "-1");
        otherDcLineSegment.put("name", otherDcLineSegment.get("name") + "-1");
        return otherDcLineSegment;
    }

    private PropertyBag getConverterBag(DCEquipment acDcConverter) {
        return getPropertyBag(acDcConverter, cgmesAcDcConverters, ACDC_CONVERTER);
    }

    private PropertyBag getDcLineSegmentBag(DCEquipment dcLineSegment) {
        return getPropertyBag(dcLineSegment, cgmesDcLineSegments, DC_LINE_SEGMENT);
    }

    private PropertyBag getPropertyBag(DCEquipment dcEquipment, PropertyBags cachedPropertyBags, String propertyKey) {
        return cachedPropertyBags.stream()
                .filter(b -> b.getId(propertyKey).equals(dcEquipment.id()))
                .findFirst()
                .orElseThrow();
    }

}