Conversion.java

/**
 * Copyright (c) 2017-2018, 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;

import com.powsybl.cgmes.conversion.elements.*;
import com.powsybl.cgmes.conversion.elements.hvdc.CgmesDcConversion;
import com.powsybl.cgmes.conversion.elements.transformers.ThreeWindingsTransformerConversion;
import com.powsybl.cgmes.conversion.elements.transformers.TwoWindingsTransformerConversion;
import com.powsybl.cgmes.conversion.naming.NamingStrategy;
import com.powsybl.cgmes.extensions.*;
import com.powsybl.cgmes.model.*;
import com.powsybl.cgmes.model.triplestore.CgmesModelTripleStore;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.util.Identifiables;
import com.powsybl.triplestore.api.PropertyBag;
import com.powsybl.triplestore.api.PropertyBags;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.Duration;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.stream.Stream;

import static com.powsybl.cgmes.conversion.Conversion.Config.StateProfile.SSH;
import static java.util.stream.Collectors.groupingBy;

/**
 * TwoWindingsTransformer Interpretation
 * <p>
 * Ratio and Phase Interpretation (Xfmr2RatioPhaseInterpretationAlternative) <br>
 * END1. All tapChangers (ratioTapChanger and phaseTapChanger) are considered at end1 (before transmission impedance) <br>
 * END2. All tapChangers (ratioTapChanger and phaseTapChanger) are considered at end2 (after transmission impedance) <br>
 * END1_END2. TapChangers (ratioTapChanger and phaseTapChanger) are considered at the end where they are defined in Cgmes <br>
 * X. If x1 == 0 all tapChangers (ratioTapChanger and phaseTapChanger) are considered at the end1 otherwise they are considered at end2
 * <p>
 * Shunt Admittance Interpretation (Xfmr2ShuntInterpretationAlternative) <br>
 * END1. All shunt admittances to ground (g, b) at end1 (before transmission impedance) <br>
 * END2. All shunt admittances to ground (g, b) at end2 (after transmission impedance) <br>
 * END1_END2. Shunt admittances to ground (g, b) at the end where they are defined in Cgmes model <br>
 * SPLIT. Split shunt admittances to ground (g, b) between end1 and end2. <br>
 * <p>
 * Structural Ratio (Xfmr2StructuralRatioInterpretationAlternative) <br>
 * END1. Structural ratio always at end1 (before transmission impedance) <br>
 * END2. Structural ratio always at end2 (after transmission impedance) <br>
 * X. If x1 == 0 structural ratio at end1, otherwise at end2
 * <p>
 * ThreeWindingsTransformer Interpretation.
 * <p>
 * Ratio and Phase Interpretation.  (Xfmr3RatioPhaseInterpretationAlternative) <br>
 * NETWORK_SIDE. All tapChangers (ratioTapChanger and phaseTapChanger) at the network side. <br>
 * STAR_BUS_SIDE. All tapChangers (ratioTapChanger and phaseTapChanger) at the star bus side.
 * <p>
 * Shunt Admittance Interpretation (Xfmr3ShuntInterpretationAlternative) <br>
 * NETWORK_SIDE. Shunt admittances to ground at the network side (end1 of the leg) <br>
 * STAR_BUS_SIDE. Shunt admittances to ground at the start bus side (end2 of the leg) <br>
 * SPLIT. Split shunt admittances to ground between two ends of the leg
 * <p>
 * Structural Ratio Interpretation (Xfmr3StructuralRatioInterpretationAlternative) <br>
 * STAR_BUS_SIDE. Structural ratio at the star bus side of all legs and RatedU0 = RatedU1 <br>
 * NETWORK_SIDE. Structural ratio at the network side of all legs. RatedU0 = 1 kv <br>
 * END1. Structural ratio at the network side of legs 2 and 3. RatedU0 = RatedU1 <br>
 * END2. Structural ratio at the network side of legs 1 and 3. RatedU0 = RatedU2 <br>
 * END3. Structural ratio at the network side of legs 1 and 2. RatedU0 = RatedU2 <br>
 * <p>
 * @author Luma Zamarre��o {@literal <zamarrenolm at aia.es>}
 * @author Jos�� Antonio Marqu��s {@literal <marquesja at aia.es>}
 *
 */
public class Conversion {

    public enum Xfmr2RatioPhaseInterpretationAlternative {
        END1, END2, END1_END2, X
    }

    public enum Xfmr2ShuntInterpretationAlternative {
        END1, END2, END1_END2, SPLIT
    }

    public enum Xfmr2StructuralRatioInterpretationAlternative {
        END1, END2, X
    }

    public enum Xfmr3RatioPhaseInterpretationAlternative {
        NETWORK_SIDE, STAR_BUS_SIDE
    }

    public enum Xfmr3ShuntInterpretationAlternative {
        NETWORK_SIDE, STAR_BUS_SIDE, SPLIT
    }

    public enum Xfmr3StructuralRatioInterpretationAlternative {
        NETWORK_SIDE, STAR_BUS_SIDE, END1, END2, END3
    }

    public Conversion(CgmesModel cgmes) {
        this(cgmes, new Config());
    }

    public Conversion(CgmesModel cgmes, Conversion.Config config) {
        this(cgmes, config, Collections.emptyList(), Collections.emptyList());
    }

    public Conversion(CgmesModel cgmes, Conversion.Config config, List<CgmesImportPostProcessor> postProcessors) {
        this(cgmes, config, Collections.emptyList(), postProcessors, NetworkFactory.findDefault());
    }

    public Conversion(CgmesModel cgmes, Conversion.Config config, List<CgmesImportPreProcessor> preProcessors, List<CgmesImportPostProcessor> postProcessors) {
        this(cgmes, config, preProcessors, postProcessors, NetworkFactory.findDefault());
    }

    public Conversion(CgmesModel cgmes, Config config, List<CgmesImportPreProcessor> activatedPreProcessors, List<CgmesImportPostProcessor> activatedPostProcessors, NetworkFactory networkFactory) {
        this.cgmes = Objects.requireNonNull(cgmes);
        this.config = Objects.requireNonNull(config);
        this.preProcessors = Objects.requireNonNull(activatedPreProcessors);
        this.postProcessors = Objects.requireNonNull(activatedPostProcessors);
        this.networkFactory = Objects.requireNonNull(networkFactory);
    }

    public void report(Consumer<String> out) {
        new ReportTapChangers(cgmes, out).report();
    }

    public Network convert() {
        return convert(ReportNode.NO_OP);
    }

    public Network convert(ReportNode reportNode) {
        // Check presence of report node for functional logs and EQ profile in the data source
        Objects.requireNonNull(reportNode);
        if (!cgmes.hasEquipmentCore()) {
            throw new CgmesModelException("Data source does not contain EquipmentCore data");
        }

        // Apply preprocessors, which mainly create missing containers
        ReportNode preProcessorsNode = CgmesReports.applyingPreprocessorsReport(reportNode);
        for (CgmesImportPreProcessor preProcessor : preProcessors) {
            CgmesReports.applyingProcessorReport(preProcessorsNode, preProcessor.getName());
            preProcessor.process(cgmes);
        }
        if (LOG.isTraceEnabled() && cgmes.baseVoltages() != null) {
            LOG.trace("{}{}{}", "BaseVoltages", System.lineSeparator(), cgmes.baseVoltages().tabulate());
        }

        // Create base network with metadata information
        Network network = createNetwork();
        Context context = createContext(network, reportNode);
        assignNetworkProperties(context);
        addMetadataModels(network, context);
        addCimCharacteristics(network);

        // Build mappings
        context.pushReportNode(CgmesReports.buildingMappingsReport(reportNode));
        context.nodeContainerMapping().build();
        BaseVoltageMappingAdder bvAdder = network.newExtension(BaseVoltageMappingAdder.class);
        cgmes.baseVoltages().forEach(bv -> bvAdder.addBaseVoltage(bv.getId("BaseVoltage"), bv.asDouble("nominalVoltage"), isBoundaryBaseVoltage(bv.getLocal("graph"))));
        bvAdder.add();
        cgmes.computedTerminals().forEach(t -> context.terminalMapping().buildTopologicalNodeCgmesTerminalsMapping(t));
        cgmes.regulatingControls().forEach(p -> context.regulatingControlMapping().cacheRegulatingControls(p));
        context.popReportNode();

        // Convert containers
        convert(cgmes.substations(), CgmesNames.SUBSTATION, context);
        convert(cgmes.voltageLevels(), CgmesNames.VOLTAGE_LEVEL, context);
        createFictitiousVoltageLevelsForLineContainers(context);

        // Convert topology
        PropertyBags nodes = context.nodeBreaker() ? cgmes.connectivityNodes() : cgmes.topologicalNodes();
        if (context.nodeBreaker()) {
            convert(nodes, CgmesNames.CONNECTIVITY_NODE, context);
        } else {
            convert(nodes, CgmesNames.TOPOLOGICAL_NODE, context);
        }
        if (!context.config().createBusbarSectionForEveryConnectivityNode()) {
            convert(cgmes.busBarSections(), CgmesNames.BUSBAR_SECTION, context);
        }

        // Convert single terminal equipments
        convert(cgmes.grounds(), CgmesNames.GROUND, context);
        convert(cgmes.energyConsumers(), CgmesNames.ENERGY_CONSUMER, context);
        convert(cgmes.energySources(), CgmesNames.ENERGY_SOURCE, context);
        convert(cgmes.equivalentInjections(), CgmesNames.EQUIVALENT_INJECTION, context);
        convert(cgmes.externalNetworkInjections(), CgmesNames.EXTERNAL_NETWORK_INJECTION, context);
        convert(cgmes.shuntCompensators(), CgmesNames.SHUNT_COMPENSATOR, context);
        convert(cgmes.equivalentShunts(), CgmesNames.EQUIVALENT_SHUNT, context);
        convert(cgmes.staticVarCompensators(), CgmesNames.STATIC_VAR_COMPENSATOR, context);
        convert(cgmes.asynchronousMachines(), CgmesNames.ASYNCHRONOUS_MACHINE, context);
        convert(cgmes.synchronousMachinesAll(), CgmesNames.SYNCHRONOUS_MACHINE, context);

        // Convert multiple terminals equipments
        // We will delay the conversion of some lines/switches that have an end at boundary
        // They have to be processed after all lines/switches have been reviewed
        // FIXME(Luma) store delayedBoundaryNodes in context
        Set<String> delayedBoundaryNodes = new HashSet<>();
        convertSwitches(context, delayedBoundaryNodes);
        convertACLineSegmentsToLines(context, delayedBoundaryNodes);
        convertEquivalentBranchesToLines(context, delayedBoundaryNodes);
        convert(cgmes.seriesCompensators(), CgmesNames.SERIES_COMPENSATOR, context);
        convertTransformers(context, delayedBoundaryNodes);
        context.pushReportNode(CgmesReports.convertingElementTypeReport(reportNode, "equipments at boundaries"));
        delayedBoundaryNodes.forEach(node -> convertEquipmentAtBoundaryNode(context, node));
        context.popReportNode();

        // Convert DC equipments, limits, SV injections, control areas, regulating controls
        context.pushReportNode(CgmesReports.convertingElementTypeReport(reportNode, "DC network"));
        CgmesDcConversion cgmesDcConversion = new CgmesDcConversion(cgmes, context);
        cgmesDcConversion.convert();
        clearUnattachedHvdcConverterStations(network, context);
        context.popReportNode();

        convert(cgmes.operationalLimits(), CgmesNames.OPERATIONAL_LIMIT, context);
        context.loadingLimitsMapping().addAll();
        setSelectedOperationalLimitsGroup(context);

        if (config.convertSvInjections()) {
            convert(cgmes.svInjections(), CgmesNames.SV_INJECTION, context);
        }

        if (config.importControlAreas()) {
            convert(cgmes.controlAreas(), CgmesNames.CONTROL_AREA, context);
            convert(cgmes.tieFlows(), CgmesNames.TIE_FLOW, context);
        }

        context.pushReportNode(CgmesReports.convertingElementTypeReport(reportNode, CgmesNames.REGULATING_CONTROL));
        context.regulatingControlMapping().setAllRegulatingControls(network);
        context.popReportNode();

        // Fix dangling lines issues
        context.pushReportNode(CgmesReports.fixingDanglingLinesIssuesReport(reportNode));
        handleDangingLineDisconnectedAtBoundary(network, context);
        adjustMultipleUnpairedDanglingLinesAtSameBoundaryNode(network, context);
        context.popReportNode();

        // Set voltages and angles
        context.pushReportNode(CgmesReports.settingVoltagesAndAnglesReport(reportNode));
        voltageAngles(nodes, context);
        completeVoltagesAndAngles(network);
        context.popReportNode();

        // Save/store data for debug or external validation
        if (config.debugTopology()) {
            debugTopology(context);
        }
        if (config.storeCgmesModelAsNetworkExtension()) {
            network.newExtension(CgmesModelExtensionAdder.class).withModel(cgmes).add();
        }
        if (config.storeCgmesConversionContextAsNetworkExtension()) {
            network.newExtension(CgmesConversionContextExtensionAdder.class).withContext(context).add();
        }

        ReportNode postProcessorsNode = CgmesReports.applyingPostprocessorsReport(reportNode);
        for (CgmesImportPostProcessor postProcessor : postProcessors) {
            // FIXME generic cgmes models may not have an underlying triplestore
            // TODO maybe pass the properties to the post processors
            CgmesReports.applyingProcessorReport(postProcessorsNode, postProcessor.getName());
            postProcessor.process(network, cgmes.tripleStore());
        }

        CgmesReports.importedCgmesNetworkReport(reportNode, network.getId());
        return network;
    }

    /**
     * Retrieve the Collection of OperationalLimitGroups for identifiable that have flow limits
     * (branch, dangling line, 3w-transformer).
     * If the collection has only one element, it gets to be the identifiable's selectedGroup.
     * If there is more than one element in the collection, don't set any as selected.
     * @param context The conversion's Context.
     */
    private void setSelectedOperationalLimitsGroup(Context context) {
        // Set selected limits group for branches
        context.network().getBranchStream().map(b -> (Branch<?>) b).forEach(branch -> {
            // Side 1
            Collection<OperationalLimitsGroup> limitsHolder1 = branch.getOperationalLimitsGroups1();
            if (limitsHolder1.size() == 1) {
                branch.setSelectedOperationalLimitsGroup1(limitsHolder1.iterator().next().getId());
            }
            // Side 2
            Collection<OperationalLimitsGroup> limitsHolder2 = branch.getOperationalLimitsGroups2();
            if (limitsHolder2.size() == 1) {
                branch.setSelectedOperationalLimitsGroup2(limitsHolder2.iterator().next().getId());
            }
        });

        // Set selected limits group for Dangling lines
        context.network().getDanglingLineStream().forEach(dl -> {
            Collection<OperationalLimitsGroup> limitsHolder = dl.getOperationalLimitsGroups();
            if (limitsHolder.size() == 1) {
                dl.setSelectedOperationalLimitsGroup(limitsHolder.iterator().next().getId());
            }
        });

        // Set selected limits group for 3w transformers legs
        context.network().getThreeWindingsTransformerStream().flatMap(ThreeWindingsTransformer::getLegStream).forEach(leg -> {
            Collection<OperationalLimitsGroup> limitsHolder = leg.getOperationalLimitsGroups();
            if (limitsHolder.size() == 1) {
                leg.setSelectedOperationalLimitsGroup(limitsHolder.iterator().next().getId());
            }
        });
    }

    private void handleDangingLineDisconnectedAtBoundary(Network network, Context context) {
        if (config.disconnectNetworkSideOfDanglingLinesIfBoundaryIsDisconnected()) {
            for (DanglingLine dl : network.getDanglingLines()) {
                String terminalBoundaryId = dl.getAliasFromType(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + "Terminal_Boundary").orElse(null);
                if (terminalBoundaryId == null) {
                    LOG.warn("Dangling line {}: alias for terminal at boundary is missing", dl.getId());
                } else {
                    disconnectDanglingLineAtBounddary(dl, terminalBoundaryId, context);
                }
            }
        }
    }

    private void disconnectDanglingLineAtBounddary(DanglingLine dl, String terminalBoundaryId, Context context) {
        CgmesTerminal terminalBoundary = cgmes.terminal(terminalBoundaryId);
        if (terminalBoundary == null) {
            LOG.warn("Dangling line {}: terminal at boundary with id {} is not found in CGMES model", dl.getId(), terminalBoundaryId);
        } else {
            if (!terminalBoundary.connected() && dl.getTerminal().isConnected()) {
                LOG.warn("DanglingLine {} was connected at network side and disconnected at boundary side. It has been disconnected also at network side.", dl.getId());
                CgmesReports.danglingLineDisconnectedAtBoundaryHasBeenDisconnectedReport(context.getReportNode(), dl.getId());
                dl.getTerminal().disconnect();
            }
        }
    }

    private void adjustMultipleUnpairedDanglingLinesAtSameBoundaryNode(Network network, Context context) {
        network.getDanglingLineStream(DanglingLineFilter.UNPAIRED)
                .filter(dl -> dl.getTerminal().isConnected())
                .collect(groupingBy(Conversion::getDanglingLineBoundaryNode))
                .values().stream()
                // Only perform adjustment for the groups with more than one connected dangling line
                .filter(dls -> dls.size() > 1)
                .forEach(dls -> adjustMultipleUnpairedDanglingLinesAtSameBoundaryNode(dls, context));
    }

    private void adjustMultipleUnpairedDanglingLinesAtSameBoundaryNode(List<DanglingLine> dls, Context context) {
        // All dangling lines will have same value for p0, q0. Take it from the first one
        double p0 = dls.get(0).getP0();
        double q0 = dls.get(0).getQ0();
        // Divide this value between all connected dangling lines
        // This method is called only if there is more than 1 connected dangling line
        long count = dls.size();
        final double p0Adjusted = p0 / count;
        final double q0Adjusted = q0 / count;
        dls.forEach(dl -> {
            LOG.warn("Multiple unpaired DanglingLines were connected at the same boundary side. Adjusted original injection from ({}, {}) to ({}, {}) for dangling line {}.", p0, q0, p0Adjusted, q0Adjusted, dl.getId());
            CgmesReports.multipleUnpairedDanglingLinesAtSameBoundaryReport(context.getReportNode(), dl.getId(), p0, q0, p0Adjusted, q0Adjusted);
            dl.setP0(p0Adjusted);
            dl.setQ0(q0Adjusted);
        });
    }

    public static String getDanglingLineBoundaryNode(DanglingLine dl) {
        String node;
        node = dl.getProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.CONNECTIVITY_NODE_BOUNDARY);
        if (node == null) {
            node = dl.getProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.TOPOLOGICAL_NODE_BOUNDARY);
        }
        if (node == null) {
            LOG.warn("Dangling line {} does not have a boundary node identifier.", dl.getId());
            node = "unknown";
        }
        return node;
    }

    private Source isBoundaryBaseVoltage(String graph) {
        //There are unit tests where the boundary file contains the sequence "EQBD" and others "EQ_BD"
        return graph.contains("EQ") && graph.contains("BD") ? Source.BOUNDARY : Source.IGM;
    }

    private static void completeVoltagesAndAngles(Network network) {

        // Voltage and angle in starBus as properties
        network.getThreeWindingsTransformers()
            .forEach(ThreeWindingsTransformerConversion::calculateVoltageAndAngleInStarBus);

        // Voltage and angle in boundary buses
        network.getDanglingLineStream(DanglingLineFilter.UNPAIRED)
            .forEach(AbstractConductingEquipmentConversion::calculateVoltageAndAngleInBoundaryBus);
        network.getTieLines().forEach(tieLine -> AbstractConductingEquipmentConversion.calculateVoltageAndAngleInBoundaryBus(tieLine.getDanglingLine1(), tieLine.getDanglingLine2()));
    }

    private void convert(PropertyBags elements, String elementType, Context context) {
        context.pushReportNode(CgmesReports.convertingElementTypeReport(context.getReportNode(), elementType));
        for (PropertyBag element : elements) {
            if (LOG.isTraceEnabled()) {
                LOG.trace(element.tabulateLocals(elementType));
            }
            AbstractObjectConversion c = switch (elementType) {
                case CgmesNames.SUBSTATION -> new SubstationConversion(element, context);
                case CgmesNames.VOLTAGE_LEVEL -> new VoltageLevelConversion(element, context);
                case CgmesNames.CONNECTIVITY_NODE, CgmesNames.TOPOLOGICAL_NODE -> new NodeConversion(elementType, element, context);
                case CgmesNames.BUSBAR_SECTION -> new BusbarSectionConversion(element, context);
                case CgmesNames.GROUND -> new GroundConversion(element, context);
                case CgmesNames.ENERGY_CONSUMER -> new EnergyConsumerConversion(element, context);
                case CgmesNames.ENERGY_SOURCE -> new EnergySourceConversion(element, context);
                case CgmesNames.EQUIVALENT_INJECTION -> new EquivalentInjectionConversion(element, context);
                case CgmesNames.EXTERNAL_NETWORK_INJECTION -> new ExternalNetworkInjectionConversion(element, context);
                case CgmesNames.SHUNT_COMPENSATOR -> new ShuntConversion(element, context);
                case CgmesNames.EQUIVALENT_SHUNT -> new EquivalentShuntConversion(element, context);
                case CgmesNames.STATIC_VAR_COMPENSATOR -> new StaticVarCompensatorConversion(element, context);
                case CgmesNames.ASYNCHRONOUS_MACHINE -> new AsynchronousMachineConversion(element, context);
                case CgmesNames.SYNCHRONOUS_MACHINE -> new SynchronousMachineConversion(element, context);
                case CgmesNames.SERIES_COMPENSATOR -> new SeriesCompensatorConversion(element, context);
                case CgmesNames.OPERATIONAL_LIMIT -> new OperationalLimitConversion(element, context);
                case CgmesNames.SV_INJECTION -> new SvInjectionConversion(element, context);
                case CgmesNames.CONTROL_AREA -> new ControlAreaConversion(element, context);
                case CgmesNames.TIE_FLOW -> new TieFlowConversion(element, context);
                default -> throw new IllegalArgumentException("Invalid elementType.");
            };
            if (c.insideBoundary()) {
                c.convertInsideBoundary();
            } else if (c.valid()) {
                c.convert();
            }
        }
        context.popReportNode();
    }

    private Network createNetwork() {
        String networkId = cgmes.modelId();
        String sourceFormat = "CGMES";
        return networkFactory.createNetwork(networkId, sourceFormat);
    }

    private Context createContext(Network network, ReportNode reportNode) {
        Context context = new Context(cgmes, config, network, reportNode);
        context.dc().initialize();
        return context;
    }

    private void assignNetworkProperties(Context context) {
        context.network().setProperty(NETWORK_PS_CGMES_MODEL_DETAIL,
                context.nodeBreaker()
                        ? NETWORK_PS_CGMES_MODEL_DETAIL_NODE_BREAKER
                        : NETWORK_PS_CGMES_MODEL_DETAIL_BUS_BRANCH);
        PropertyBags modelProfiles = context.cgmes().modelProfiles();
        String fullModel = "FullModel";
        modelProfiles.sort(Comparator.comparing(p -> p.getId(fullModel)));
        for (PropertyBag modelProfile : modelProfiles) { // Import of profiles ID as properties TODO import them in a dedicated extension
            if (modelProfile.getId(fullModel).equals(context.network().getId())) {
                continue;
            }
            String profile = CgmesNamespace.getProfile(modelProfile.getId("profile"));
            if (profile != null && !"EQ_OP".equals(profile) && !"SV".equals(profile)) { // don't import EQ_OP and SV profiles as they are not used for CGMES export
                context.network()
                        .setProperty(Identifiables.getUniqueId(CGMES_PREFIX_ALIAS_PROPERTIES + profile + "_ID", property -> context.network().hasProperty(property)),
                                modelProfile.getId(fullModel));
            }
        }
        ZonedDateTime modelScenarioTime = cgmes.scenarioTime();
        ZonedDateTime modelCreated = cgmes.created();
        long forecastDistance = Duration.between(modelCreated, modelScenarioTime).toMinutes();
        context.network().setForecastDistance(forecastDistance >= 0 ? (int) forecastDistance : 0);
        context.network().setCaseDate(modelScenarioTime);
        LOG.info("cgmes scenarioTime       : {}", modelScenarioTime);
        LOG.info("cgmes modelCreated       : {}", modelCreated);
        LOG.info("network caseDate         : {}", context.network().getCaseDate());
        LOG.info("network forecastDistance : {}", context.network().getForecastDistance());
    }

    /**
     * Read the model header (the FullModel node) that holds metadata information.
     * The metadata will be stored in the {@link CgmesMetadataModels} extension.
     * @param network The network described by the model header and that will hold the extension.
     * @param context The conversion context.
     */
    private void addMetadataModels(Network network, Context context) {
        PropertyBags ps = cgmes.fullModels();
        if (ps.isEmpty()) {
            return;
        }
        CgmesMetadataModelsAdder modelsAdder = network.newExtension(CgmesMetadataModelsAdder.class);
        for (PropertyBag p : ps) {
            CgmesMetadataModelsAdder.ModelAdder modelAdder = modelsAdder.newModel()
                .setId(p.getId("FullModel"))
                .setSubset(subsetFromGraph(p.getLocal("graph")))
                .setDescription(p.getId("description"))
                .setVersion(readVersion(p, context))
                .setModelingAuthoritySet(p.getId("modelingAuthoritySet"));
            addMetadataModelReferences(p, "profileList", modelAdder::addProfile);
            addMetadataModelReferences(p, "dependentOnList", modelAdder::addDependentOn);
            addMetadataModelReferences(p, "supersedesList", modelAdder::addSupersedes);
            modelAdder.add();
        }
        modelsAdder.add();
    }

    /**
     * Add references (profiles, dependencies, supersedes) to the {@link CgmesMetadataModel} being created by the adder.
     * @param p The property bag holding the references.
     * @param refsProperty The property name to look for in the property bag.
     *                     The property value must be split to retrieve the complete list of references.
     * @param adder The method in the adder that will add the references to the model.
     */
    private void addMetadataModelReferences(PropertyBag p, String refsProperty, Function<String, CgmesMetadataModelsAdder.ModelAdder> adder) {
        String refs = p.get(refsProperty);
        if (refs != null && !refs.isEmpty()) {
            for (String ref : refs.split(" ")) {
                adder.apply(ref);
            }
        }
    }

    /**
     * Retrieve the subset from the graph.
     * @param graph The file name. It shall contain the subset identifier.
     * @return The {@link CgmesSubset} corresponding to the graph.
     */
    private CgmesSubset subsetFromGraph(String graph) {
        return Stream.of(CgmesSubset.values())
                .filter(subset -> subset.isValidName(graph))
                .findFirst()
                .orElse(CgmesSubset.UNKNOWN);
    }

    /**
     * Retrieve the version number from the property bag.
     * @param propertyBag The bag where to look for a version property.
     * @param context The conversion context.
     * @return The version number if found and is a proper integer, else the default value: 1.
     */
    private int readVersion(PropertyBag propertyBag, Context context) {
        try {
            return propertyBag.asInt("version");
        } catch (NumberFormatException e) {
            context.fixed("Version", "The version is expected to be an integer: " + propertyBag.get("version") + ". Fixed to 1");
            return 1;
        }
    }

    private void addCimCharacteristics(Network network) {
        if (cgmes instanceof CgmesModelTripleStore cgmesModelTripleStore) {
            network.newExtension(CimCharacteristicsAdder.class)
                    .setTopologyKind(cgmes.isNodeBreaker() ? CgmesTopologyKind.NODE_BREAKER : CgmesTopologyKind.BUS_BRANCH)
                    .setCimVersion(cgmesModelTripleStore.getCimVersion())
                    .add();
        }
    }

    private void convertACLineSegmentsToLines(Context context, Set<String> delayedBoundaryNodes) {
        context.pushReportNode(CgmesReports.convertingElementTypeReport(context.getReportNode(), CgmesNames.AC_LINE_SEGMENT));
        for (PropertyBag line : cgmes.acLineSegments()) {
            if (LOG.isTraceEnabled()) {
                LOG.trace(line.tabulateLocals(CgmesNames.AC_LINE_SEGMENT));
            }
            ACLineSegmentConversion c = new ACLineSegmentConversion(line, context);
            if (c.valid()) {
                String node = c.boundaryNode();
                if (node != null && !context.config().convertBoundary()) {
                    context.boundary().addAcLineSegmentAtNode(line, node);
                    delayedBoundaryNodes.add(node);
                } else {
                    c.convert();
                }
            }
        }
        context.popReportNode();
    }

    // Fictitious voltageLevels for Line and Substation(when it includes nodes) containers
    private void createFictitiousVoltageLevelsForLineContainers(Context context) {

        context.nodeContainerMapping().getFictitiousVoltageLevelsForLineContainersToBeCreated().forEach(fictitiousVoltageLevelId -> {
            String containerId = context.nodeContainerMapping().getContainerId(fictitiousVoltageLevelId).orElseThrow();
            String containerName = context.nodeContainerMapping().getContainerName(fictitiousVoltageLevelId).orElseThrow();
            String referenceVoltageLevelId = context.nodeContainerMapping().getReferenceVoltageLevelId(fictitiousVoltageLevelId).orElseThrow();

            if (context.network().getVoltageLevel(fictitiousVoltageLevelId) == null) {
                VoltageLevel referenceVoltageLevel = context.network().getVoltageLevel(referenceVoltageLevelId);
                if (referenceVoltageLevel == null) {
                    throw new ConversionException("VoltageLevel not found for voltageLevelId: " + referenceVoltageLevelId);
                }
                createFictitiousVoltageLevelsForLineContainer(context, fictitiousVoltageLevelId, containerId, containerName, referenceVoltageLevel);
            }
        });
    }

    private void createFictitiousVoltageLevelsForLineContainer(Context context, String fictitiousVoltageLevelId, String containerId, String containerName, VoltageLevel vlref) {
        LOG.warn("Fictitious Voltage Level {} created for Line container {} name {}", fictitiousVoltageLevelId, containerId, containerName);
        // Nominal voltage and low/high limits are copied from the reference voltage level, if it is given
        VoltageLevel vl = context.network().newVoltageLevel()
                .setNominalV(vlref.getNominalV())
                .setTopologyKind(
                        context.nodeBreaker()
                                ? TopologyKind.NODE_BREAKER
                                : TopologyKind.BUS_BREAKER)
                .setLowVoltageLimit(vlref.getLowVoltageLimit())
                .setHighVoltageLimit(vlref.getHighVoltageLimit())
                .setId(fictitiousVoltageLevelId)
                .setFictitious(true)
                .setName(containerName)
                .setEnsureIdUnicity(context.config().isEnsureIdAliasUnicity())
                .add();
        vl.setProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + "LineContainerId", containerId);
    }

    private void convertSwitches(Context context, Set<String> delayedBoundaryNodes) {
        context.pushReportNode(CgmesReports.convertingElementTypeReport(context.getReportNode(), CgmesNames.SWITCH));
        for (PropertyBag sw : cgmes.switches()) {
            if (LOG.isTraceEnabled()) {
                LOG.trace(sw.tabulateLocals("Switch"));
            }
            SwitchConversion c = new SwitchConversion(sw, context);
            if (c.valid()) {
                String node = c.boundaryNode();
                if (node != null && !context.config().convertBoundary()) {
                    context.boundary().addSwitchAtNode(sw, node);
                    delayedBoundaryNodes.add(node);
                } else {
                    c.convert();
                }
            }
        }
        context.popReportNode();
    }

    private void convertEquivalentBranchesToLines(Context context, Set<String> delayedBoundaryNodes) {
        context.pushReportNode(CgmesReports.convertingElementTypeReport(context.getReportNode(), CgmesNames.EQUIVALENT_BRANCH));
        for (PropertyBag equivalentBranch : cgmes.equivalentBranches()) {
            if (LOG.isTraceEnabled()) {
                LOG.trace(equivalentBranch.tabulateLocals("EquivalentBranch"));
            }
            EquivalentBranchConversion c = new EquivalentBranchConversion(equivalentBranch, context);
            if (c.valid()) {
                String node = c.boundaryNode();
                if (node != null && !context.config().convertBoundary()) {
                    context.boundary().addEquivalentBranchAtNode(equivalentBranch, node);
                    delayedBoundaryNodes.add(node);
                } else {
                    c.convert();
                }
            }
        }
        context.popReportNode();
    }

    private void convertTransformers(Context context, Set<String> delayedBoundaryNodes) {
        context.pushReportNode(CgmesReports.convertingElementTypeReport(context.getReportNode(), CgmesNames.POWER_TRANSFORMER));
        cgmes.transformers().stream()
                .map(t -> context.transformerEnds(t.getId("PowerTransformer")))
                .forEach(ends -> {
                    String transformerId = ends.get(0).getId("PowerTransformer");
                    if (LOG.isTraceEnabled()) {
                        LOG.trace("Transformer {}, {}-winding", transformerId, ends.size());
                        ends.forEach(e -> LOG.trace(e.tabulateLocals("TransformerEnd")));
                    }
                    if (ends.size() == 2) {
                        convertTwoWindingsTransformers(context, ends, delayedBoundaryNodes);
                    } else if (ends.size() == 3) {
                        convertThreeWindingsTransformers(context, ends);
                    } else {
                        String what = "PowerTransformer " + transformerId;
                        Supplier<String> reason = () -> String.format("Has %d ends. Only 2 or 3 ends are supported", ends.size());
                        context.invalid(what, reason);
                    }
                });
        context.popReportNode();
    }

    private static void convertTwoWindingsTransformers(Context context, PropertyBags ends, Set<String> delayedBoundaryNodes) {
        AbstractConductingEquipmentConversion c = new TwoWindingsTransformerConversion(ends, context);
        if (c.valid()) {
            String node = c.boundaryNode();
            if (node != null && !context.config().convertBoundary()) {
                context.boundary().addTransformerAtNode(ends, node);
                delayedBoundaryNodes.add(node);
            } else {
                c.convert();
            }
        }
    }

    private static void convertThreeWindingsTransformers(Context context, PropertyBags ends) {
        AbstractConductingEquipmentConversion c = new ThreeWindingsTransformerConversion(ends, context);
        if (c.valid()) {
            c.convert();
        }
    }

    // Supported conversions:
    // Only one Line (--> create dangling line)
    // Only one Switch (--> create dangling line with z0)
    // Only one Transformer (--> create dangling line)
    // Only one EquivalentBranch (--> create dangling line)
    // Any combination of Line, Switch, Transformer and EquivalentBranch

    private void convertEquipmentAtBoundaryNode(Context context, String node) {
        // At least each delayed boundary node should have one equipment attached to it
        // Currently supported equipment at boundary are lines and switches
        List<BoundaryEquipment> beqs = context.boundary().boundaryEquipmentAtNode(node);
        if (LOG.isDebugEnabled()) {
            LOG.debug("Delayed boundary node {} with {} equipment at it", node, beqs.size());
            beqs.forEach(BoundaryEquipment::log);
        }
        int numEquipmentsAtNode = beqs.size();
        if (numEquipmentsAtNode == 1) {
            beqs.get(0).createConversion(context).convertAtBoundary();
        } else if (numEquipmentsAtNode == 2) {
            convertTwoEquipmentsAtBoundaryNode(context, node, beqs.get(0), beqs.get(1));
        } else if (numEquipmentsAtNode > 2) {
            // In some TYNDP there are three acLineSegments at the boundary node,
            // one of them disconnected. The two connected acLineSegments are imported.
            List<BoundaryEquipment> connectedBeqs = beqs.stream()
                .filter(beq -> !beq.isAcLineSegmentDisconnected(context)).toList();
            if (connectedBeqs.size() == 2) {
                convertTwoEquipmentsAtBoundaryNode(context, node, connectedBeqs.get(0), connectedBeqs.get(1));
                // There can be multiple disconnected ACLineSegment to the same X-node (for example, for planning purposes)
                beqs.stream().filter(beq -> !connectedBeqs.contains(beq)).toList()
                    .forEach(beq -> {
                        context.fixed("convertEquipmentAtBoundaryNode",
                                String.format("Multiple AcLineSegments at boundary %s. Disconnected AcLineSegment %s is imported as a dangling line.", node, beq.getAcLineSegmentId()));
                        beq.createConversion(context).convertAtBoundary();
                    });
            } else {
                // This case should not happen and will not result in an equivalent network at the end of the conversion
                context.fixed(node, "More than two connected AcLineSegments at boundary: only dangling lines are created." +
                        " Please note that the converted IIDM network will probably not be equivalent to the CGMES network.");
                beqs.forEach(beq -> beq.createConversion(context).convertAtBoundary());
            }
        }
    }

    private static void convertTwoEquipmentsAtBoundaryNode(Context context, String node, BoundaryEquipment beq1, BoundaryEquipment beq2) {
        EquipmentAtBoundaryConversion conversion1 = beq1.createConversion(context);
        EquipmentAtBoundaryConversion conversion2 = beq2.createConversion(context);

        conversion1.convertAtBoundary();
        Optional<DanglingLine> dl1 = conversion1.getDanglingLine();
        conversion2.convertAtBoundary();
        Optional<DanglingLine> dl2 = conversion2.getDanglingLine();

        if (dl1.isPresent() && dl2.isPresent()) {
            // there can be several dangling lines linked to same x-node in one IGM for planning purposes
            // in this case, we don't merge them
            // please note that only one of them should be connected
            String regionName1 = obtainRegionName(dl1.get().getTerminal().getVoltageLevel());
            String regionName2 = obtainRegionName(dl2.get().getTerminal().getVoltageLevel());

            String pairingKey1 = dl1.get().getPairingKey();
            String pairingKey2 = dl2.get().getPairingKey();

            if (!(pairingKey1 != null && pairingKey1.equals(pairingKey2))) {
                context.ignored(node, "Both dangling lines do not have the same pairingKey: we do not consider them as a merged line");
            } else if (regionName1 != null && regionName1.equals(regionName2)) {
                context.ignored(node, "Both dangling lines are in the same voltage level: we do not consider them as a merged line");
            } else if (dl2.get().getId().compareTo(dl1.get().getId()) >= 0) {
                ACLineSegmentConversion.convertToTieLine(context, dl1.get(), dl2.get());
            } else {
                ACLineSegmentConversion.convertToTieLine(context, dl2.get(), dl1.get());
            }
        }
    }

    private static String obtainRegionName(VoltageLevel voltageLevel) {
        return voltageLevel.getSubstation().map(s -> s.getProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + "regionName")).orElse(null);
    }

    private void voltageAngles(PropertyBags nodes, Context context) {
        if (context.nodeBreaker()) {
            // TODO(Luma): we create again one conversion object for every node
            // In node-breaker conversion,
            // set (voltage, angle) values after all nodes have been created and connected
            for (PropertyBag n : nodes) {
                NodeConversion nc = new NodeConversion(CgmesNames.CONNECTIVITY_NODE, n, context);
                if (!nc.insideBoundary() || nc.insideBoundary() && context.config().convertBoundary()) {
                    nc.setVoltageAngleNodeBreaker();
                }
            }
        }
    }

    private void clearUnattachedHvdcConverterStations(Network network, Context context) {
        // In case of faulty CGMES files, remove HVDC Converter Stations without HVDC lines
        network.getHvdcConverterStationStream()
                .filter(converter -> converter.getHvdcLine() == null)
                .forEach(converter -> {
                    CgmesReports.removingUnattachedHvdcConverterStationReport(context.getReportNode(), converter.getId());
                    context.ignored("HVDC Converter Station " + converter.getId(), "No correct linked HVDC line found.");
                    converter.remove();
                });
    }

    private void debugTopology(Context context) {
        context.network().getVoltageLevels().forEach(vl -> {
            String name = vl.getSubstation().map(s -> s.getNameOrId() + "-").orElse("") + vl.getNameOrId();
            name = name.replace('/', '-');
            Path file = Paths.get(System.getProperty("java.io.tmpdir"), "temp-cgmes-" + name + ".dot");
            try {
                vl.exportTopology(file);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        });
    }

    public static class Config {

        public enum StateProfile {
            SSH,
            SV
        }

        public List<String> substationIdsExcludedFromMapping() {
            return Collections.emptyList();
        }

        public boolean debugTopology() {
            return false;
        }

        public boolean importNodeBreakerAsBusBreaker() {
            return importNodeBreakerAsBusBreaker;
        }

        public Config setImportNodeBreakerAsBusBreaker(boolean importNodeBreakerAsBusBreaker) {
            this.importNodeBreakerAsBusBreaker = importNodeBreakerAsBusBreaker;
            return this;
        }

        public boolean convertBoundary() {
            return convertBoundary;
        }

        public Config setConvertBoundary(boolean convertBoundary) {
            this.convertBoundary = convertBoundary;
            return this;
        }

        public boolean computeFlowsAtBoundaryDanglingLines() {
            return true;
        }

        public boolean createBusbarSectionForEveryConnectivityNode() {
            return createBusbarSectionForEveryConnectivityNode;
        }

        public Config setCreateBusbarSectionForEveryConnectivityNode(boolean b) {
            createBusbarSectionForEveryConnectivityNode = b;
            return this;
        }

        public boolean convertSvInjections() {
            return convertSvInjections;
        }

        public Config setConvertSvInjections(boolean convertSvInjections) {
            this.convertSvInjections = convertSvInjections;
            return this;
        }

        public StateProfile getProfileForInitialValuesShuntSectionsTapPositions() {
            return profileForInitialValuesShuntSectionsTapPositions;
        }

        public Config setProfileForInitialValuesShuntSectionsTapPositions(String profileForInitialValuesShuntSectionsTapPositions) {
            String forInitialValuesShuntSectionsTapPositions = Objects.requireNonNull(profileForInitialValuesShuntSectionsTapPositions);
            if (forInitialValuesShuntSectionsTapPositions.equals("SSH") || forInitialValuesShuntSectionsTapPositions.equals("SV")) {
                this.profileForInitialValuesShuntSectionsTapPositions = StateProfile.valueOf(profileForInitialValuesShuntSectionsTapPositions);
            } else {
                throw new CgmesModelException("Unexpected profile used for shunt sections / tap positions state hypothesis: " + profileForInitialValuesShuntSectionsTapPositions);
            }
            return this;
        }

        public boolean storeCgmesModelAsNetworkExtension() {
            return storeCgmesModelAsNetworkExtension;
        }

        public Config setStoreCgmesModelAsNetworkExtension(boolean storeCgmesModelAsNetworkExtension) {
            this.storeCgmesModelAsNetworkExtension = storeCgmesModelAsNetworkExtension;
            return this;
        }

        public boolean storeCgmesConversionContextAsNetworkExtension() {
            return storeCgmesConversionContextAsNetworkExtension;
        }

        public Config setStoreCgmesConversionContextAsNetworkExtension(boolean storeCgmesTerminalMappingAsNetworkExtension) {
            this.storeCgmesConversionContextAsNetworkExtension = storeCgmesTerminalMappingAsNetworkExtension;
            return this;
        }

        public boolean createActivePowerControlExtension() {
            return createActivePowerControlExtension;
        }

        public Config setCreateActivePowerControlExtension(boolean createActivePowerControlExtension) {
            this.createActivePowerControlExtension = createActivePowerControlExtension;
            return this;
        }

        public boolean isEnsureIdAliasUnicity() {
            return ensureIdAliasUnicity;
        }

        public Config setEnsureIdAliasUnicity(boolean ensureIdAliasUnicity) {
            this.ensureIdAliasUnicity = ensureIdAliasUnicity;
            return this;
        }

        public boolean importControlAreas() {
            return importControlAreas;
        }

        public Config setImportControlAreas(boolean importControlAreas) {
            this.importControlAreas = importControlAreas;
            return this;
        }

        public NamingStrategy getNamingStrategy() {
            return namingStrategy;
        }

        public Config setNamingStrategy(NamingStrategy namingStrategy) {
            this.namingStrategy = Objects.requireNonNull(namingStrategy);
            return this;
        }

        public Xfmr2RatioPhaseInterpretationAlternative getXfmr2RatioPhase() {
            return xfmr2RatioPhase;
        }

        public void setXfmr2RatioPhase(Xfmr2RatioPhaseInterpretationAlternative alternative) {
            xfmr2RatioPhase = alternative;
        }

        public Xfmr2ShuntInterpretationAlternative getXfmr2Shunt() {
            return xfmr2Shunt;
        }

        public void setXfmr2Shunt(Xfmr2ShuntInterpretationAlternative alternative) {
            xfmr2Shunt = alternative;
        }

        public Xfmr2StructuralRatioInterpretationAlternative getXfmr2StructuralRatio() {
            return xfmr2StructuralRatio;
        }

        public void setXfmr2StructuralRatio(Xfmr2StructuralRatioInterpretationAlternative alternative) {
            xfmr2StructuralRatio = alternative;
        }

        public Xfmr3RatioPhaseInterpretationAlternative getXfmr3RatioPhase() {
            return xfmr3RatioPhase;
        }

        public void setXfmr3RatioPhase(Xfmr3RatioPhaseInterpretationAlternative alternative) {
            this.xfmr3RatioPhase = alternative;
        }

        public Xfmr3ShuntInterpretationAlternative getXfmr3Shunt() {
            return xfmr3Shunt;
        }

        public void setXfmr3Shunt(Xfmr3ShuntInterpretationAlternative alternative) {
            xfmr3Shunt = alternative;
        }

        public Xfmr3StructuralRatioInterpretationAlternative getXfmr3StructuralRatio() {
            return xfmr3StructuralRatio;
        }

        public void setXfmr3StructuralRatio(Xfmr3StructuralRatioInterpretationAlternative alternative) {
            xfmr3StructuralRatio = alternative;
        }

        public double getMissingPermanentLimitPercentage() {
            return missingPermanentLimitPercentage;
        }

        public Config setMissingPermanentLimitPercentage(double missingPermanentLimitPercentage) {
            if (missingPermanentLimitPercentage < 0 || missingPermanentLimitPercentage > 100) {
                throw new IllegalArgumentException("Missing permanent limit percentage must be between 0 and 100.");
            }
            this.missingPermanentLimitPercentage = missingPermanentLimitPercentage;
            return this;
        }

        public CgmesImport.FictitiousSwitchesCreationMode getCreateFictitiousSwitchesForDisconnectedTerminalsMode() {
            return createFictitiousSwitchesForDisconnectedTerminalsMode;
        }

        public Config createFictitiousSwitchesForDisconnectedTerminalsMode(CgmesImport.FictitiousSwitchesCreationMode createFictitiousSwitchesForDisconnectedTerminalsMode) {
            this.createFictitiousSwitchesForDisconnectedTerminalsMode = createFictitiousSwitchesForDisconnectedTerminalsMode;
            return this;
        }

        public Config setDisconnectNetworkSideOfDanglingLinesIfBoundaryIsDisconnected(boolean b) {
            disconnectNetworkSideOfDanglingLinesIfBoundaryIsDisconnected = b;
            return this;
        }

        public boolean disconnectNetworkSideOfDanglingLinesIfBoundaryIsDisconnected() {
            return disconnectNetworkSideOfDanglingLinesIfBoundaryIsDisconnected;
        }

        public boolean getCreateFictitiousVoltageLevelsForEveryNode() {
            return createFictitiousVoltageLevelsForEveryNode;
        }

        public Config setCreateFictitiousVoltageLevelsForEveryNode(boolean b) {
            createFictitiousVoltageLevelsForEveryNode = b;
            return this;
        }

        private boolean convertBoundary = false;

        private boolean createBusbarSectionForEveryConnectivityNode = false;
        private boolean convertSvInjections = true;
        private StateProfile profileForInitialValuesShuntSectionsTapPositions = SSH;
        private boolean storeCgmesModelAsNetworkExtension = true;
        private boolean storeCgmesConversionContextAsNetworkExtension = false;
        private boolean createActivePowerControlExtension = false;

        private CgmesImport.FictitiousSwitchesCreationMode createFictitiousSwitchesForDisconnectedTerminalsMode = CgmesImport.FictitiousSwitchesCreationMode.ALWAYS;

        private boolean ensureIdAliasUnicity = false;
        private boolean importControlAreas = true;
        private boolean importNodeBreakerAsBusBreaker = false;
        private boolean disconnectNetworkSideOfDanglingLinesIfBoundaryIsDisconnected = true;

        private NamingStrategy namingStrategy = new NamingStrategy.Identity();

        // Default interpretation.
        private Xfmr2RatioPhaseInterpretationAlternative xfmr2RatioPhase = Xfmr2RatioPhaseInterpretationAlternative.END1_END2;
        private Xfmr2ShuntInterpretationAlternative xfmr2Shunt = Xfmr2ShuntInterpretationAlternative.END1_END2;
        private Xfmr2StructuralRatioInterpretationAlternative xfmr2StructuralRatio = Xfmr2StructuralRatioInterpretationAlternative.X;

        private Xfmr3RatioPhaseInterpretationAlternative xfmr3RatioPhase = Xfmr3RatioPhaseInterpretationAlternative.NETWORK_SIDE;
        private Xfmr3ShuntInterpretationAlternative xfmr3Shunt = Xfmr3ShuntInterpretationAlternative.NETWORK_SIDE;
        private Xfmr3StructuralRatioInterpretationAlternative xfmr3StructuralRatio = Xfmr3StructuralRatioInterpretationAlternative.STAR_BUS_SIDE;

        private double missingPermanentLimitPercentage = 100;
        private boolean createFictitiousVoltageLevelsForEveryNode = true;
    }

    private final CgmesModel cgmes;
    private final Config config;
    private final List<CgmesImportPostProcessor> postProcessors;
    private final List<CgmesImportPreProcessor> preProcessors;
    private final NetworkFactory networkFactory;

    private static final Logger LOG = LoggerFactory.getLogger(Conversion.class);

    public static final String NETWORK_PS_CGMES_MODEL_DETAIL = "CGMESModelDetail";
    public static final String NETWORK_PS_CGMES_MODEL_DETAIL_BUS_BRANCH = "bus-branch";
    public static final String NETWORK_PS_CGMES_MODEL_DETAIL_NODE_BREAKER = "node-breaker";

    public static final String CGMES_PREFIX_ALIAS_PROPERTIES = "CGMES.";
    public static final String PROPERTY_IS_CREATED_FOR_DISCONNECTED_TERMINAL = CGMES_PREFIX_ALIAS_PROPERTIES + "isCreatedForDisconnectedTerminal";
    public static final String PROPERTY_IS_EQUIVALENT_SHUNT = CGMES_PREFIX_ALIAS_PROPERTIES + "isEquivalentShunt";
    public static final String PROPERTY_HYDRO_PLANT_STORAGE_TYPE = CGMES_PREFIX_ALIAS_PROPERTIES + "hydroPlantStorageKind";
    public static final String PROPERTY_FOSSIL_FUEL_TYPE = CGMES_PREFIX_ALIAS_PROPERTIES + "fuelType";
    public static final String PROPERTY_WIND_GEN_UNIT_TYPE = CGMES_PREFIX_ALIAS_PROPERTIES + "windGenUnitType";
    public static final String PROPERTY_CGMES_ORIGINAL_CLASS = CGMES_PREFIX_ALIAS_PROPERTIES + "originalClass";
    public static final String PROPERTY_BUSBAR_SECTION_TERMINALS = CGMES_PREFIX_ALIAS_PROPERTIES + "busbarSectionTerminals";
    public static final String PROPERTY_CGMES_GOVERNOR_SCD = CGMES_PREFIX_ALIAS_PROPERTIES + "governorSCD";
    public static final String PROPERTY_CGMES_SYNCHRONOUS_MACHINE_TYPE = CGMES_PREFIX_ALIAS_PROPERTIES + "synchronousMachineType";
    public static final String PROPERTY_CGMES_SYNCHRONOUS_MACHINE_OPERATING_MODE = CGMES_PREFIX_ALIAS_PROPERTIES + "synchronousMachineOperatingMode";
    public static final String PROPERTY_OPERATIONAL_LIMIT_SET_IDENTIFIERS = CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.OPERATIONAL_LIMIT_SET + "_identifiers";
    public static final String PROPERTY_REGULATING_CONTROL = CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.REGULATING_CONTROL;
}