CgmesExportContext.java

/**
 * Copyright (c) 2020, 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.export;

import com.fasterxml.uuid.Generators;
import com.google.common.collect.BiMap;
import com.google.common.collect.HashBiMap;
import com.powsybl.cgmes.conversion.CgmesExport.ExportParameters;
import com.powsybl.cgmes.conversion.Conversion;
import com.powsybl.cgmes.conversion.naming.CgmesObjectReference;
import com.powsybl.cgmes.conversion.naming.NamingStrategy;
import com.powsybl.cgmes.conversion.naming.NamingStrategyFactory;
import com.powsybl.cgmes.extensions.*;
import com.powsybl.cgmes.model.*;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.Identifiable;
import com.powsybl.iidm.network.extensions.RemoteReactivePowerControl;
import com.powsybl.triplestore.api.PropertyBag;
import org.apache.commons.lang3.tuple.Pair;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.DecimalFormat;
import java.time.ZonedDateTime;
import java.util.*;
import java.util.stream.Collectors;

import static com.powsybl.cgmes.conversion.export.CgmesExportUtil.obtainSynchronousMachineKind;
import static com.powsybl.cgmes.conversion.export.EquipmentExport.hasDifferentTNsAtBothEnds;
import static com.powsybl.cgmes.conversion.naming.CgmesObjectReference.*;
import static com.powsybl.cgmes.conversion.naming.CgmesObjectReference.Part.*;
import static com.powsybl.cgmes.conversion.naming.CgmesObjectReference.ref;
import static com.powsybl.cgmes.conversion.naming.CgmesObjectReference.refGeneratingUnit;

/**
 * @author Miora Ralambotiana {@literal <miora.ralambotiana at rte-france.com>}
 */
public class CgmesExportContext {

    private static final String GENERATING_UNIT = "GeneratingUnit";

    private static final String DCNODE = "DCNode";
    private static final String DCTERMINAL = "DCTerminal";
    private static final String ACDCCONVERTERDCTERMINAL = "ACDCConverterDCTerminal";

    private static final String TERMINAL_BOUNDARY = "Terminal_Boundary";
    private static final String REGION_ID = "regionId";
    private static final String REGION_NAME = "regionName";
    private static final String DEFAULT_REGION = "default region";
    public static final String SUB_REGION_ID = "subRegionId";
    private static final String BOUNDARY_EQ_ID_PROPERTY = Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + "EQ_BD_ID";
    private static final String BOUNDARY_TP_ID_PROPERTY = Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + "TP_BD_ID";

    private CgmesNamespace.Cim cim = CgmesNamespace.CIM_16;
    private CgmesTopologyKind topologyKind = CgmesTopologyKind.NODE_BREAKER;
    private ZonedDateTime scenarioTime = ZonedDateTime.now();
    private ReportNode reportNode = ReportNode.NO_OP;
    private String businessProcess = DEFAULT_BUSINESS_PROCESS;
    private NamingStrategy namingStrategy = new NamingStrategy.Identity();
    private String modelingAuthoritySet = null;
    private String modelDescription = null;
    private String modelVersion = null;
    private String boundaryEqId = null;
    private String boundaryTpId = null;
    private List<String> profiles = null;
    private String baseName = null;
    public static final boolean CGM_EXPORT_VALUE = false;
    public static final boolean EXPORT_BOUNDARY_POWER_FLOWS_DEFAULT_VALUE = true;
    public static final boolean EXPORT_POWER_FLOWS_FOR_SWITCHES_DEFAULT_VALUE = true;
    public static final boolean EXPORT_TRANSFORMERS_WITH_HIGHEST_VOLTAGE_AT_END1_DEFAULT_VALUE = false;
    public static final boolean ENCODE_IDS_DEFAULT_VALUE = true;
    public static final boolean EXPORT_LOAD_FLOW_STATUS_DEFAULT_VALUE = true;
    public static final boolean EXPORT_ALL_LIMITS_GROUP_DEFAULT_VALUE = true;
    public static final boolean EXPORT_GENERATORS_IN_LOCAL_REGULATION_MODE_DEFAULT_VALUE = false;
    // From QoCDC 3.3.1 rules IGMConvergence, KirchhoffsFirstLaw, ... that refer to SV_INJECTION_LIMIT=0.1
    public static final double MAX_P_MISMATCH_CONVERGED_DEFAULT_VALUE = 0.1;
    public static final double MAX_Q_MISMATCH_CONVERGED_DEFAULT_VALUE = 0.1;
    public static final boolean EXPORT_SV_INJECTIONS_FOR_SLACKS_DEFAULT_VALUE = true;
    public static final String DEFAULT_MODELING_AUTHORITY_SET_VALUE = "powsybl.org";
    public static final UUID DEFAULT_UUID_NAMESPACE = Generators.nameBasedGenerator().generate(DEFAULT_MODELING_AUTHORITY_SET_VALUE);
    public static final String DEFAULT_BUSINESS_PROCESS = "1D";
    public static final boolean UPDATE_DEPENDENCIES_DEFAULT_VALUE = true;

    private boolean exportBoundaryPowerFlows = EXPORT_BOUNDARY_POWER_FLOWS_DEFAULT_VALUE;
    private boolean exportFlowsForSwitches = EXPORT_POWER_FLOWS_FOR_SWITCHES_DEFAULT_VALUE;
    private boolean exportTransformersWithHighestVoltageAtEnd1 = EXPORT_TRANSFORMERS_WITH_HIGHEST_VOLTAGE_AT_END1_DEFAULT_VALUE;
    private boolean exportLoadFlowStatus = EXPORT_LOAD_FLOW_STATUS_DEFAULT_VALUE;
    private boolean exportAllLimitsGroup = EXPORT_ALL_LIMITS_GROUP_DEFAULT_VALUE;
    private boolean exportGeneratorsInLocalRegulationMode = EXPORT_GENERATORS_IN_LOCAL_REGULATION_MODE_DEFAULT_VALUE;
    private double maxPMismatchConverged = MAX_P_MISMATCH_CONVERGED_DEFAULT_VALUE;
    private double maxQMismatchConverged = MAX_Q_MISMATCH_CONVERGED_DEFAULT_VALUE;
    private boolean isExportSvInjectionsForSlacks = EXPORT_SV_INJECTIONS_FOR_SLACKS_DEFAULT_VALUE;
    private boolean updateDependencies = UPDATE_DEPENDENCIES_DEFAULT_VALUE;
    private boolean exportEquipment = false;
    private boolean encodeIds = ENCODE_IDS_DEFAULT_VALUE;

    private final Map<Double, BaseVoltageMapping.BaseVoltageSource> baseVoltageByNominalVoltageMapping = new HashMap<>();

    private final BiMap<String, String> regionsIdsByRegionName = HashBiMap.create();
    private final BiMap<String, String> subRegionsIdsBySubRegionName = HashBiMap.create();
    private final Map<String, String> fictitiousContainers = new HashMap<>();
    private final Map<String, Bus> topologicalNodes = new HashMap<>();
    private final ReferenceDataProvider referenceDataProvider;

    public String getFictitiousContainerFor(Identifiable<?> id) {
        return fictitiousContainers.get(id.getId());
    }

    public void setFictitiousContainerFor(Identifiable<?> id, String containerId) {
        fictitiousContainers.put(id.getId(), containerId);
    }

    public CgmesExportContext() {
        referenceDataProvider = null;
    }

    public CgmesExportContext(Network network) {
        this(network, null, NamingStrategyFactory.create(NamingStrategyFactory.IDENTITY, DEFAULT_UUID_NAMESPACE));
    }

    public CgmesExportContext(Network network, ReferenceDataProvider referenceDataProvider) {
        this(network, referenceDataProvider, NamingStrategyFactory.create(NamingStrategyFactory.IDENTITY, DEFAULT_UUID_NAMESPACE));
    }

    public CgmesExportContext(Network network, UUID uuidNamespace) {
        this(network, null, NamingStrategyFactory.create(NamingStrategyFactory.IDENTITY, uuidNamespace));
    }

    public CgmesExportContext(Network network, ReferenceDataProvider referenceDataProvider, UUID uuidNamespace) {
        this(network, referenceDataProvider, NamingStrategyFactory.create(NamingStrategyFactory.IDENTITY, uuidNamespace));
    }

    public CgmesExportContext(Network network, ReferenceDataProvider referenceDataProvider, NamingStrategy namingStrategy) {
        this(network, referenceDataProvider, namingStrategy, ReportNode.NO_OP, null);
    }

    public CgmesExportContext(Network network, ReferenceDataProvider referenceDataProvider,
                              NamingStrategy namingStrategy, ReportNode reportNode, ExportParameters exportParameters) {
        setReportNode(reportNode);
        this.referenceDataProvider = referenceDataProvider;
        this.namingStrategy = namingStrategy;
        scenarioTime = network.getCaseDate();
        addParameters(exportParameters);
        computeCimVersion(exportParameters, network);
        computeTopologyKind(exportParameters, network);
        computeModelingAuthoritySet(exportParameters, referenceDataProvider);
        computeBoundaryIds(exportParameters, network);
        addIidmMappings(network);
    }

    private void addParameters(ExportParameters exportParameters) {
        if (exportParameters != null) {
            setExportBoundaryPowerFlows(exportParameters.exportBoundaryPowerFlows());
            setExportFlowsForSwitches(exportParameters.exportFlowsForSwitches());
            setExportTransformersWithHighestVoltageAtEnd1(exportParameters.exportTransformersWithHighestVoltageAtEnd1());
            setExportLoadFlowStatus(exportParameters.exportLoadFlowStatus());
            setExportAllLimitsGroup(exportParameters.exportAllLimitsGroup());
            setExportGeneratorsInLocalRegulationMode(exportParameters.exportGeneratorsInLocalRegulationMode());
            setMaxPMismatchConverged(exportParameters.maxPMismatchConverged());
            setMaxQMismatchConverged(exportParameters.maxQMismatchConverged());
            setExportSvInjectionsForSlacks(exportParameters.exportSvInjectionsForSlacks());
            setEncodeIds(exportParameters.encodeIds());
            setBusinessProcess(exportParameters.businessProcess());
            setModelDescription(exportParameters.modelDescription());
            setModelVersion(exportParameters.modelVersion());
            setProfiles(exportParameters.profiles());
            setBaseName(exportParameters.baseName());
            setUpdateDependencies(exportParameters.updateDependencies());
        }
    }

    private void computeCimVersion(ExportParameters exportParameters, Network network) {
        if (exportParameters != null && exportParameters.cimVersion() != null) {
            setCimVersion(Integer.parseInt(exportParameters.cimVersion()));
        } else if (network.getExtension(CimCharacteristics.class) != null) {
            setCimVersion(network.getExtension(CimCharacteristics.class).getCimVersion());
        }
    }

    private void computeTopologyKind(ExportParameters exportParameters, Network network) {
        if (exportParameters != null && exportParameters.topologyKind() != null) {
            setTopologyKind(Enum.valueOf(CgmesTopologyKind.class, exportParameters.topologyKind()));
        } else if (network.getExtension(CimCharacteristics.class) != null) {
            setTopologyKind(network.getExtension(CimCharacteristics.class).getTopologyKind());
        } else {
            CgmesTopologyKind topologyKindForExport = detectNetworkTopologyKind(network);
            if (topologyKindForExport == CgmesTopologyKind.MIXED_TOPOLOGY) {
                if (getCimVersion() < 100) {
                    topologyKindForExport = CgmesTopologyKind.BUS_BRANCH;
                } else {
                    topologyKindForExport = CgmesTopologyKind.NODE_BREAKER;
                }
            }
            setTopologyKind(topologyKindForExport);
        }
    }

    private CgmesTopologyKind detectNetworkTopologyKind(Network network) {
        long nodeBreakerVoltageLevelsCount = network.getVoltageLevelStream()
                .filter(vl -> vl.getTopologyKind() == TopologyKind.NODE_BREAKER)
                .count();
        long busBreakerVoltageLevelsCount = network.getVoltageLevelStream()
                .filter(vl -> vl.getTopologyKind() == TopologyKind.BUS_BREAKER)
                .count();

        if (nodeBreakerVoltageLevelsCount > 0 && busBreakerVoltageLevelsCount == 0) {
            return CgmesTopologyKind.NODE_BREAKER;
        } else if (nodeBreakerVoltageLevelsCount == 0 && busBreakerVoltageLevelsCount > 0) {
            return CgmesTopologyKind.BUS_BRANCH;
        } else {
            return CgmesTopologyKind.MIXED_TOPOLOGY;
        }
    }

    private void computeModelingAuthoritySet(ExportParameters exportParameters, ReferenceDataProvider referenceDataProvider) {
        if (exportParameters != null && exportParameters.modelingAuthoritySet() != null) {
            setModelingAuthoritySet(exportParameters.modelingAuthoritySet());
        } else if (referenceDataProvider != null) {
            PropertyBag sourcingActor = referenceDataProvider.getSourcingActor();
            if (sourcingActor.containsKey("masUri")) {
                setModelingAuthoritySet(sourcingActor.get("masUri"));
            }
        }
    }

    private void computeBoundaryIds(ExportParameters exportParameters, Network network) {
        // Boundary EQ id
        if (exportParameters != null && exportParameters.boundaryEqId() != null) {
            setBoundaryEqId(exportParameters.boundaryEqId());
        } else if (referenceDataProvider != null && referenceDataProvider.getEquipmentBoundaryId() != null) {
            setBoundaryEqId(referenceDataProvider.getEquipmentBoundaryId());
        } else if (network.hasProperty(BOUNDARY_EQ_ID_PROPERTY)) {
            setBoundaryEqId(network.getProperty(BOUNDARY_EQ_ID_PROPERTY));
        }

        // Boundary TP id
        if (exportParameters != null && exportParameters.boundaryTpId() != null) {
            setBoundaryTpId(exportParameters.boundaryTpId());
        } else if (referenceDataProvider != null && referenceDataProvider.getTopologyBoundaryId() != null) {
            setBoundaryTpId(referenceDataProvider.getTopologyBoundaryId());
        } else if (network.hasProperty(BOUNDARY_TP_ID_PROPERTY)) {
            setBoundaryTpId(network.getProperty(BOUNDARY_TP_ID_PROPERTY));
        }
    }

    public ReferenceDataProvider getReferenceDataProvider() {
        return referenceDataProvider;
    }

    public void addIidmMappings(Network network) {
        // For a merging view we plan to call CgmesExportContext() and then addIidmMappings(network) for every network
        // TODO add option to skip this part (if from CGMES)
        addIidmMappingsSubstations(network);
        BaseVoltageMapping bvMapping = network.getExtension(BaseVoltageMapping.class);
        if (bvMapping == null) {
            network.newExtension(BaseVoltageMappingAdder.class).add();
            bvMapping = network.getExtension(BaseVoltageMapping.class);
        }
        addIidmMappingsBaseVoltages(bvMapping, network);
        addIidmMappingsTerminals(network);
        addIidmMappingsGenerators(network);
        addIidmMappingsBatteries(network);
        addIidmMappingsShuntCompensators(network);
        addIidmMappingsStaticVarCompensators(network);
        addIidmMappingsEndsAndTapChangers(network);
        addIidmMappingsEquivalentInjection(network);
    }

    private void addIidmMappingsSubstations(Network network) {
        for (Substation substation : network.getSubstations()) {
            String regionName;
            if (!substation.hasProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + REGION_ID)) {
                Pair<String, String> region = getCreateRegion(substation);
                String regionId = region.getLeft();
                regionName = region.getRight();
                substation.setProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + REGION_ID, regionId);
                substation.setProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + REGION_NAME, regionName);
            } else {
                // Only add with this name if the id is not already mapped
                // We can not have the same id mapped to two different names
                String regionId = namingStrategy.getCgmesIdFromProperty(substation, Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + REGION_ID);
                regionName = substation.getProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + REGION_NAME);
                if (!regionsIdsByRegionName.containsValue(regionId)) {
                    regionsIdsByRegionName.computeIfAbsent(regionName, k -> regionId);
                }
            }
            String geoTag;
            if (substation.getGeographicalTags().size() == 1) {
                geoTag = substation.getGeographicalTags().iterator().next();
            } else {
                geoTag = regionName;
            }
            if (!substation.hasProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + SUB_REGION_ID)) {
                String id = subRegionsIdsBySubRegionName.computeIfAbsent(geoTag, k -> namingStrategy.getCgmesId(ref(k), SUB_GEOGRAPHICAL_REGION));
                substation.setProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + SUB_REGION_ID, id);
            } else {
                subRegionsIdsBySubRegionName.computeIfAbsent(geoTag, k -> namingStrategy.getCgmesIdFromProperty(substation, Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + SUB_REGION_ID));
            }
        }
    }

    private Pair<String, String> getCreateRegion(Substation substation) {
        // The current substation does not have explicit information for geographical region,
        // Try to obtain it from the reference data based on the current sourcing actor we are using for export
        Pair<String, String> region = null;
        if (referenceDataProvider != null) {
            region = referenceDataProvider.getSourcingActorRegion();
        }
        if (region == null) {
            // If no information is available from reference data,
            // Just create a new geographical region using country name as name
            String regionName = substation.getCountry().map(Country::name).orElse(DEFAULT_REGION);
            String regionId = regionsIdsByRegionName.computeIfAbsent(regionName, k -> namingStrategy.getCgmesId(ref(k), GEOGRAPHICAL_REGION));
            region = Pair.of(regionId, regionName);
        }
        return region;
    }

    private void addIidmMappingsBaseVoltages(BaseVoltageMapping mapping, Network network) {
        DecimalFormat noTrailingZerosFormat = new DecimalFormat("0.##");
        if (mapping.isBaseVoltageEmpty()) {
            // Here we do not have previous information about base voltages
            // (The mapping is filled when the Network has been imported from CGMES)
            // Now that we want to export, we may find some base voltages are defined in the reference data
            for (VoltageLevel vl : network.getVoltageLevels()) {
                double nominalV = vl.getNominalV();
                // Only create a new unique id if no reference data exists
                String baseVoltageId = null;
                if (referenceDataProvider != null) {
                    baseVoltageId = referenceDataProvider.getBaseVoltage(nominalV);
                    if (baseVoltageId != null) {
                        mapping.addBaseVoltage(nominalV, baseVoltageId, Source.BOUNDARY);
                    }
                }
                if (baseVoltageId == null && mapping.getBaseVoltage(nominalV) == null) {
                    CgmesObjectReference vref = ref(noTrailingZerosFormat.format(nominalV));
                    baseVoltageId = namingStrategy.getCgmesId(vref, BASE_VOLTAGE);
                    mapping.addBaseVoltage(nominalV, baseVoltageId, Source.IGM);
                }
            }
        }
        Map<Double, BaseVoltageMapping.BaseVoltageSource> bvByNominalVoltage = mapping.baseVoltagesByNominalVoltageMap();
        baseVoltageByNominalVoltageMapping.putAll(bvByNominalVoltage);
    }

    private void addIidmMappingsTerminals(Network network) {
        for (Connectable<?> c : network.getConnectables()) {
            if (isExportedEquipment(c)) {
                for (Terminal t : c.getTerminals()) {
                    addIidmMappingsTerminal(t, c);
                }
            }
        }
        addIidmMappingsSwitchTerminals(network);
        addIidmMappingsHvdcTerminals(network);
    }

    public boolean isExportEquipment() {
        return exportEquipment;
    }

    public CgmesExportContext setExportEquipment(boolean exportEquipment) {
        this.exportEquipment = exportEquipment;
        return this;
    }

    public boolean isExportedEquipment(Identifiable<?> c) {
        boolean ignored = false;
        if (c instanceof Load load) {
            ignored = load.isFictitious()
                    || isCim16BusBranchExport() && CgmesNames.STATION_SUPPLY.equals(CgmesExportUtil.loadClassName(load));
        } else if (c instanceof Switch sw) {
            ignored = sw.isFictitious() && "true".equals(sw.getProperty(Conversion.PROPERTY_IS_CREATED_FOR_DISCONNECTED_TERMINAL))
                    || isCim16BusBranchExport() && sw.getProperty(Conversion.PROPERTY_CGMES_ORIGINAL_CLASS, "").equals("GroundDisconnector")
                    || isBusBranchExport() && !sw.isRetained()
                    || isBusBranchExport() && !hasDifferentTNsAtBothEnds(sw);
        }
        return !ignored;
    }

    private void addIidmMappingsSwitchTerminals(Network network) {
        for (Switch sw : network.getSwitches()) {
            String terminal1Id = sw.getAliasFromType(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.TERMINAL + "1").orElse(null);
            if (terminal1Id == null) {
                terminal1Id = namingStrategy.getCgmesId(refTyped(sw), TERMINAL, ref(1));
                sw.addAlias(terminal1Id, Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.TERMINAL + "1");
            }
            String terminal2Id = sw.getAliasFromType(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.TERMINAL + "2").orElse(null);
            if (terminal2Id == null) {
                terminal2Id = namingStrategy.getCgmesId(refTyped(sw), TERMINAL, ref(2));
                sw.addAlias(terminal2Id, Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.TERMINAL + "2");
            }
        }
    }

    private void addIidmMappingsHvdcTerminals(Network network) {
        for (HvdcLine line : network.getHvdcLines()) {
            String dcNode1 = line.getAliasFromType(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + DCNODE + "1").orElse(null);
            if (dcNode1 == null) {
                dcNode1 = namingStrategy.getCgmesId(refTyped(line), Part.DCNODE, ref(1));
                line.addAlias(dcNode1, Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + DCNODE + "1");
            }
            String dcNode2 = line.getAliasFromType(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + DCNODE + "2").orElse(null);
            if (dcNode2 == null) {
                dcNode2 = namingStrategy.getCgmesId(refTyped(line), Part.DCNODE, ref(2));
                line.addAlias(dcNode2, Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + DCNODE + "2");
            }
            String dcTerminal1 = line.getAliasFromType(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + DCTERMINAL + "1").orElse(null);
            if (dcTerminal1 == null) {
                dcTerminal1 = namingStrategy.getCgmesId(refTyped(line), TERMINAL, ref(1));
                line.addAlias(dcTerminal1, Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + DCTERMINAL + "1");
            }
            String dcTerminal2 = line.getAliasFromType(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + DCTERMINAL + "2").orElse(null);
            if (dcTerminal2 == null) {
                dcTerminal2 = namingStrategy.getCgmesId(refTyped(line), TERMINAL, ref(2));
                line.addAlias(dcTerminal2, Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + DCTERMINAL + "2");
            }
            String acdcConverterDcTerminal1 = line.getConverterStation1().getAliasFromType(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + ACDCCONVERTERDCTERMINAL).orElse(null);
            if (acdcConverterDcTerminal1 == null) {
                acdcConverterDcTerminal1 = namingStrategy.getCgmesId(refTyped(line), ACDC_CONVERTER_DC_TERMINAL, ref(1));
                line.getConverterStation1().addAlias(acdcConverterDcTerminal1, Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + ACDCCONVERTERDCTERMINAL);
            }
            String acdcConverterDcTerminal2 = line.getConverterStation2().getAliasFromType(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + ACDCCONVERTERDCTERMINAL).orElse(null);
            if (acdcConverterDcTerminal2 == null) {
                acdcConverterDcTerminal2 = namingStrategy.getCgmesId(refTyped(line), ACDC_CONVERTER_DC_TERMINAL, ref(2));
                line.getConverterStation2().addAlias(acdcConverterDcTerminal2, Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + ACDCCONVERTERDCTERMINAL);
            }
        }
    }

    private void addIidmMappingsTerminal(Terminal t, Connectable<?> c) {
        if (c instanceof DanglingLine) {
            String terminalId = c.getAliasFromType(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.TERMINAL1).orElse(null);
            if (terminalId == null) {
                // Legacy: in previous versions, dangling line terminals were recorded in a different alias
                // read it, remove it and store in the standard alias for equipment terminal (Terminal1)
                terminalId = c.getAliasFromType(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.TERMINAL).orElse(null);
                if (terminalId != null) {
                    c.removeAlias(terminalId);
                } else {
                    terminalId = namingStrategy.getCgmesId(refTyped(c), TERMINAL);
                }
                c.addAlias(terminalId, Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.TERMINAL1);
            }
            String boundaryId = c.getAliasFromType(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + TERMINAL_BOUNDARY).orElse(null);
            if (boundaryId == null) {
                boundaryId = namingStrategy.getCgmesId(refTyped(c), BOUNDARY_TERMINAL);
                c.addAlias(boundaryId, Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + TERMINAL_BOUNDARY);
            }
        } else {
            int sequenceNumber = CgmesExportUtil.getTerminalSequenceNumber(t);
            String terminalId = c.getAliasFromType(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.TERMINAL + sequenceNumber).orElse(null);
            if (terminalId == null) {
                terminalId = namingStrategy.getCgmesId(refTyped(c), TERMINAL, ref(sequenceNumber));
                c.addAlias(terminalId, Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.TERMINAL + sequenceNumber);
            }
        }
    }

    private static boolean isCondenser(Generator generator) {
        return obtainSynchronousMachineKind(generator, generator.getMinP(), generator.getMaxP(), CgmesExportUtil.obtainCurve(generator)).contains("condenser");
    }

    private void addIidmMappingsGenerators(Network network) {
        for (Generator generator : network.getGenerators()) {
            // Condensers should not have generating units
            if (!isCondenser(generator)) {
                String generatingUnit = generator.getProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + GENERATING_UNIT);
                if (generatingUnit == null) {
                    generatingUnit = namingStrategy.getCgmesId(ref(generator), refGeneratingUnit(generator));
                    generator.setProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + GENERATING_UNIT, generatingUnit);
                }
            }
            String regulatingControlId = generator.getProperty(Conversion.PROPERTY_REGULATING_CONTROL);
            if (regulatingControlId == null && hasRegulatingControlCapability(generator)) {
                regulatingControlId = namingStrategy.getCgmesId(ref(generator), Part.REGULATING_CONTROL);
                generator.setProperty(Conversion.PROPERTY_REGULATING_CONTROL, regulatingControlId);
            }
        }
    }

    private static boolean hasRegulatingControlCapability(Generator generator) {
        return generator.getExtension(RemoteReactivePowerControl.class) != null
                || !Double.isNaN(generator.getTargetV()) && hasReactiveCapability(generator);
    }

    private static boolean hasReactiveCapability(Generator generator) {
        ReactiveLimits reactiveLimits = generator.getReactiveLimits();
        if (reactiveLimits == null) {
            return false;
        } else if (reactiveLimits.getKind() == ReactiveLimitsKind.CURVE) {
            return hasReactiveCapability((ReactiveCapabilityCurve) reactiveLimits);
        } else if (reactiveLimits.getKind() == ReactiveLimitsKind.MIN_MAX) {
            return hasReactiveCapability((MinMaxReactiveLimits) reactiveLimits);
        }
        return false;
    }

    private static boolean hasReactiveCapability(ReactiveCapabilityCurve rcc) {
        for (ReactiveCapabilityCurve.Point point : rcc.getPoints()) {
            if (point.getMaxQ() != point.getMinQ()) {
                return true;
            }
        }
        return false;
    }

    private static boolean hasReactiveCapability(MinMaxReactiveLimits mmrl) {
        return mmrl.getMaxQ() != mmrl.getMinQ();
    }

    private void addIidmMappingsBatteries(Network network) {
        for (Battery battery : network.getBatteries()) {
            String generatingUnit = battery.getProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + GENERATING_UNIT);
            if (generatingUnit == null) {
                generatingUnit = namingStrategy.getCgmesId(refTyped(battery), Part.GENERATING_UNIT);
                battery.setProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + GENERATING_UNIT, generatingUnit);
            }
            // TODO regulation
        }
    }

    private void addIidmMappingsShuntCompensators(Network network) {
        for (ShuntCompensator shuntCompensator : network.getShuntCompensators()) {
            if ("true".equals(shuntCompensator.getProperty(Conversion.PROPERTY_IS_EQUIVALENT_SHUNT))) {
                continue;
            }
            String regulatingControlId = shuntCompensator.getProperty(Conversion.PROPERTY_REGULATING_CONTROL);
            if (regulatingControlId == null && (CgmesExportUtil.isValidVoltageSetpoint(shuntCompensator.getTargetV())
                                            || !Objects.equals(shuntCompensator, shuntCompensator.getRegulatingTerminal().getConnectable()))) {
                regulatingControlId = namingStrategy.getCgmesId(ref(shuntCompensator), Part.REGULATING_CONTROL);
                shuntCompensator.setProperty(Conversion.PROPERTY_REGULATING_CONTROL, regulatingControlId);
            }
        }
    }

    private void addIidmMappingsStaticVarCompensators(Network network) {
        for (StaticVarCompensator svc : network.getStaticVarCompensators()) {
            String regulatingControlId = svc.getProperty(Conversion.PROPERTY_REGULATING_CONTROL);
            boolean validVoltageSetpoint = CgmesExportUtil.isValidVoltageSetpoint(svc.getVoltageSetpoint());
            boolean validReactiveSetpoint = CgmesExportUtil.isValidReactivePowerSetpoint(svc.getReactivePowerSetpoint());
            if (regulatingControlId == null && (validReactiveSetpoint
                                                || validVoltageSetpoint
                                                || !Objects.equals(svc, svc.getRegulatingTerminal().getConnectable()))) {
                regulatingControlId = namingStrategy.getCgmesId(ref(svc), Part.REGULATING_CONTROL);
                svc.setProperty(Conversion.PROPERTY_REGULATING_CONTROL, regulatingControlId);
            }
        }
    }

    private void addIidmMappingsEndsAndTapChangers(Network network) {
        for (TwoWindingsTransformer twt : network.getTwoWindingsTransformers()) {
            addIidmTransformerEnd(twt, 1);
            addIidmTransformerEnd(twt, 2);
            //  For two winding transformers we can not check-and-add based on endNumber
            //  The resulting IIDM tap changer is always at end1
            //  But the original position of tap changer could be 1 or 2
            addIidmTapChanger2wt(twt, twt.getPhaseTapChanger(), CgmesNames.PHASE_TAP_CHANGER);
            addIidmTapChanger2wt(twt, twt.getRatioTapChanger(), CgmesNames.RATIO_TAP_CHANGER);
        }
        for (ThreeWindingsTransformer twt : network.getThreeWindingsTransformers()) {
            addIidmTransformerEnd(twt, 1);
            addIidmTransformerEnd(twt, 2);
            addIidmTransformerEnd(twt, 3);
            addIidmTapChanger(twt, twt.getLeg1().getPhaseTapChanger(), CgmesNames.PHASE_TAP_CHANGER, 1);
            addIidmTapChanger(twt, twt.getLeg1().getRatioTapChanger(), CgmesNames.RATIO_TAP_CHANGER, 1);
            addIidmTapChanger(twt, twt.getLeg2().getPhaseTapChanger(), CgmesNames.PHASE_TAP_CHANGER, 2);
            addIidmTapChanger(twt, twt.getLeg2().getRatioTapChanger(), CgmesNames.RATIO_TAP_CHANGER, 2);
            addIidmTapChanger(twt, twt.getLeg3().getPhaseTapChanger(), CgmesNames.PHASE_TAP_CHANGER, 3);
            addIidmTapChanger(twt, twt.getLeg3().getRatioTapChanger(), CgmesNames.RATIO_TAP_CHANGER, 3);
        }
    }

    private void addIidmTransformerEnd(Identifiable<?> eq, int end) {
        String endId = eq.getAliasFromType(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.TRANSFORMER_END + end).orElse(null);
        if (endId == null) {
            endId = namingStrategy.getCgmesId(ref(eq), combo(TRANSFORMER_END, ref(end)));
            eq.addAlias(endId, Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.TRANSFORMER_END + end);
        }
    }

    private void addIidmTapChanger(Identifiable<?> eq, TapChanger<?, ?, ?, ?> tc, String typeChangerTypeName, int endNumber) {
        if (tc != null) {
            String aliasType = Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + typeChangerTypeName + endNumber;
            if (eq.getAliasFromType(aliasType).isEmpty()) {
                Part ratioPhasePart = Objects.equals(typeChangerTypeName, CgmesNames.PHASE_TAP_CHANGER) ? PHASE_TAP_CHANGER : RATIO_TAP_CHANGER;
                String newTapChangerId = namingStrategy.getCgmesId(refTyped(eq), ratioPhasePart, ref(endNumber));
                eq.addAlias(newTapChangerId, aliasType);
            }
        }
    }

    private void addIidmTapChanger2wt(Identifiable<?> eq, TapChanger<?, ?, ?, ?> tc, String typeChangerTypeName) {
        if (tc != null) {
            String aliasType1 = Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + typeChangerTypeName + 1;
            String aliasType2 = Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + typeChangerTypeName + 2;
            // Only create a new identifier, always at end 1,
            // If no previous identifiers were found
            // Neither at end 1 nor at end 2
            if (eq.getAliasFromType(aliasType1).isEmpty() && eq.getAliasFromType(aliasType2).isEmpty()) {
                Part ratioPhasePart = Objects.equals(typeChangerTypeName, CgmesNames.PHASE_TAP_CHANGER) ? PHASE_TAP_CHANGER : RATIO_TAP_CHANGER;
                String newTapChangerId = namingStrategy.getCgmesId(refTyped(eq), ratioPhasePart, ref(1));
                eq.addAlias(newTapChangerId, aliasType1);
            }
        }
    }

    private void addIidmMappingsEquivalentInjection(Network network) {
        for (DanglingLine danglingLine : network.getDanglingLines(DanglingLineFilter.ALL)) {
            String alias;
            alias = danglingLine.getProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.EQUIVALENT_INJECTION);
            if (alias == null) {
                String equivalentInjectionId = namingStrategy.getCgmesId(refTyped(danglingLine), EQUIVALENT_INJECTION);
                danglingLine.setProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + CgmesNames.EQUIVALENT_INJECTION, equivalentInjectionId);
            }
            alias = danglingLine.getProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + "EquivalentInjectionTerminal");
            if (alias == null) {
                String equivalentInjectionTerminalId = namingStrategy.getCgmesId(refTyped(danglingLine), EQUIVALENT_INJECTION, TERMINAL);
                danglingLine.setProperty(Conversion.CGMES_PREFIX_ALIAS_PROPERTIES + "EquivalentInjectionTerminal", equivalentInjectionTerminalId);
            }
        }
    }

    public int getCimVersion() {
        return cim.getVersion();
    }

    public CgmesExportContext setCimVersion(int cimVersion) {
        cim = CgmesNamespace.getCim(cimVersion);
        return this;
    }

    public CgmesTopologyKind getTopologyKind() {
        return topologyKind;
    }

    public CgmesExportContext setTopologyKind(CgmesTopologyKind topologyKind) {
        this.topologyKind = Objects.requireNonNull(topologyKind);
        return this;
    }

    public ZonedDateTime getScenarioTime() {
        return scenarioTime;
    }

    public CgmesExportContext setScenarioTime(ZonedDateTime scenarioTime) {
        this.scenarioTime = Objects.requireNonNull(scenarioTime);
        return this;
    }

    public boolean exportBoundaryPowerFlows() {
        return exportBoundaryPowerFlows;
    }

    public CgmesExportContext setExportBoundaryPowerFlows(boolean exportBoundaryPowerFlows) {
        this.exportBoundaryPowerFlows = exportBoundaryPowerFlows;
        return this;
    }

    public boolean exportFlowsForSwitches() {
        return exportFlowsForSwitches;
    }

    public CgmesExportContext setExportFlowsForSwitches(boolean exportFlowsForSwitches) {
        this.exportFlowsForSwitches = exportFlowsForSwitches;
        return this;
    }

    public boolean exportTransformersWithHighestVoltageAtEnd1() {
        return exportTransformersWithHighestVoltageAtEnd1;
    }

    public CgmesExportContext setExportTransformersWithHighestVoltageAtEnd1(boolean exportTransformersWithHighestVoltageAtEnd1) {
        this.exportTransformersWithHighestVoltageAtEnd1 = exportTransformersWithHighestVoltageAtEnd1;
        return this;
    }

    public boolean isExportLoadFlowStatus() {
        return exportLoadFlowStatus;
    }

    public CgmesExportContext setExportLoadFlowStatus(boolean exportLoadFlowStatus) {
        this.exportLoadFlowStatus = exportLoadFlowStatus;
        return this;
    }

    public boolean isExportAllLimitsGroup() {
        return exportAllLimitsGroup;
    }

    public CgmesExportContext setExportAllLimitsGroup(boolean exportAllLimitsGroup) {
        this.exportAllLimitsGroup = exportAllLimitsGroup;
        return this;
    }

    public boolean isExportGeneratorsInLocalRegulationMode() {
        return exportGeneratorsInLocalRegulationMode;
    }

    public CgmesExportContext setExportGeneratorsInLocalRegulationMode(boolean exportGeneratorsInLocalRegulationMode) {
        this.exportGeneratorsInLocalRegulationMode = exportGeneratorsInLocalRegulationMode;
        return this;
    }

    public double getMaxPMismatchConverged() {
        return maxPMismatchConverged;
    }

    public CgmesExportContext setMaxPMismatchConverged(double maxPMismatchConverged) {
        this.maxPMismatchConverged = maxPMismatchConverged;
        return this;
    }

    public double getMaxQMismatchConverged() {
        return maxQMismatchConverged;
    }

    public CgmesExportContext setMaxQMismatchConverged(double maxQMismatchConverged) {
        this.maxQMismatchConverged = maxQMismatchConverged;
        return this;
    }

    public boolean isExportSvInjectionsForSlacks() {
        return isExportSvInjectionsForSlacks;
    }

    public CgmesExportContext setExportSvInjectionsForSlacks(boolean exportSvInjectionsForSlacks) {
        this.isExportSvInjectionsForSlacks = exportSvInjectionsForSlacks;
        return this;
    }

    public String encode(String id) {
        if (encodeIds) {
            return URLEncoder.encode(id, StandardCharsets.UTF_8);
        }
        return id;
    }

    public CgmesExportContext setEncodeIds(boolean encodeIds) {
        this.encodeIds = encodeIds;
        return this;
    }

    public CgmesNamespace.Cim getCim() {
        return cim;
    }

    public NamingStrategy getNamingStrategy() {
        return namingStrategy;
    }

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

    public BaseVoltageMapping.BaseVoltageSource getBaseVoltageByNominalVoltage(double nominalV) {
        return baseVoltageByNominalVoltageMapping.get(nominalV);
    }

    public Collection<String> getRegionsIds() {
        return Collections.unmodifiableSet(regionsIdsByRegionName.values());
    }

    public String getRegionName(String regionId) {
        return regionsIdsByRegionName.inverse().get(regionId);
    }

    public String getSubRegionName(String subRegionId) {
        return subRegionsIdsBySubRegionName.inverse().get(subRegionId);
    }

    public CgmesExportContext setReportNode(ReportNode reportNode) {
        this.reportNode = reportNode;
        return this;
    }

    public ReportNode getReportNode() {
        return this.reportNode;
    }

    public void putTopologicalNode(String tn, Bus bus) {
        topologicalNodes.put(tn, bus);
    }

    public boolean containsTopologicalNode(String tn) {
        return topologicalNodes.containsKey(tn);
    }

    public Map<String, Bus> getTopologicalNodes(Network network) {
        if (topologicalNodes.isEmpty()) {
            return network.getBusBreakerView().getBusStream().collect(Collectors.toMap(b -> namingStrategy.getCgmesId(b), b -> b));
        }
        return Collections.unmodifiableMap(topologicalNodes);
    }

    /**
     * The business process related to the export, used to get a unique ID for EQ, TP, SSH and SV FullModel.
     */
    public String getBusinessProcess() {
        return businessProcess;
    }

    public CgmesExportContext setBusinessProcess(String businessProcess) {
        this.businessProcess = businessProcess;
        return this;
    }

    public String getModelingAuthoritySet() {
        return modelingAuthoritySet;
    }

    public CgmesExportContext setModelingAuthoritySet(String modelingAuthoritySet) {
        this.modelingAuthoritySet = modelingAuthoritySet;
        return this;
    }

    public String getModelDescription() {
        return modelDescription;
    }

    public CgmesExportContext setModelDescription(String modelDescription) {
        this.modelDescription = modelDescription;
        return this;
    }

    public String getModelVersion() {
        return modelVersion;
    }

    public CgmesExportContext setModelVersion(String modelVersion) {
        this.modelVersion = modelVersion;
        return this;
    }

    public String getBoundaryEqId() {
        return boundaryEqId;
    }

    public CgmesExportContext setBoundaryEqId(String boundaryEqId) {
        this.boundaryEqId = boundaryEqId;
        return this;
    }

    public String getBoundaryTpId() {
        return boundaryTpId;
    }

    public CgmesExportContext setBoundaryTpId(String boundaryTpId) {
        this.boundaryTpId = boundaryTpId;
        return this;
    }

    public List<String> getProfiles() {
        return profiles;
    }

    public CgmesExportContext setProfiles(List<String> profiles) {
        this.profiles = profiles;
        return this;
    }

    public boolean isCim16BusBranchExport() {
        return getCimVersion() == 16 && isBusBranchExport();
    }

    public boolean isBusBranchExport() {
        return getTopologyKind() == CgmesTopologyKind.BUS_BRANCH;
    }

    public String getBaseName() {
        return baseName;
    }

    public CgmesExportContext setBaseName(String baseName) {
        this.baseName = baseName;
        return this;
    }

    public CgmesExportContext setUpdateDependencies(boolean updateDependencies) {
        this.updateDependencies = updateDependencies;
        return this;
    }

    public boolean updateDependencies() {
        return updateDependencies;
    }
}