PsseImporter.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.psse.converter;

import com.google.auto.service.AutoService;
import com.google.common.io.ByteStreams;
import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.datasource.DataSource;
import com.powsybl.commons.datasource.ReadOnlyDataSource;
import com.powsybl.commons.parameters.ConfiguredParameter;
import com.powsybl.commons.parameters.Parameter;
import com.powsybl.commons.parameters.ParameterDefaultValueConfig;
import com.powsybl.commons.parameters.ParameterType;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.util.ContainersMapping;
import com.powsybl.psse.converter.extensions.PsseConversionContextExtensionAdder;
import com.powsybl.psse.converter.extensions.PsseModelExtensionAdder;
import com.powsybl.psse.model.PsseException;
import com.powsybl.psse.model.PsseVersion;
import com.powsybl.psse.model.io.Context;
import com.powsybl.psse.model.pf.*;
import com.powsybl.psse.model.pf.io.PowerFlowDataFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;

import static com.powsybl.psse.converter.AbstractConverter.getSubstationIdFromBuses;
import static com.powsybl.psse.converter.AbstractConverter.getSubstationIdFromPsseSubstationIds;

/**
 * @author JB Heyberger {@literal <jean-baptiste.heyberger at rte-france.com>}
 */
@AutoService(Importer.class)
public class PsseImporter implements Importer {

    private static final Logger LOGGER = LoggerFactory.getLogger(PsseImporter.class);

    private static final String FORMAT = "PSS/E";

    private static final String[] EXTENSIONS = {"raw", "RAW", "rawx", "RAWX"};

    private static final Parameter IGNORE_BASE_VOLTAGE_PARAMETER = new Parameter("psse.import.ignore-base-voltage",
            ParameterType.BOOLEAN,
            "Ignore base voltage specified in the file",
            Boolean.FALSE);
    private static final Parameter IGNORE_NODE_BREAKER_TOPOLOGY_PARAMETER = new Parameter("psse.import.ignore-node-breaker-topology",
            ParameterType.BOOLEAN,
            "Ignore the node breaker topology specified in the substation data of the file",
            Boolean.FALSE);

    @Override
    public String getFormat() {
        return FORMAT;
    }

    @Override
    public List<String> getSupportedExtensions() {
        return Arrays.asList(EXTENSIONS);
    }

    @Override
    public List<Parameter> getParameters() {
        List<Parameter> parameterList = List.of(
                IGNORE_BASE_VOLTAGE_PARAMETER,
                IGNORE_NODE_BREAKER_TOPOLOGY_PARAMETER);
        return ConfiguredParameter.load(parameterList, getFormat(), ParameterDefaultValueConfig.INSTANCE);
    }

    @Override
    public String getComment() {
        return "PSS/E Format to IIDM converter";
    }

    @Override
    public boolean exists(ReadOnlyDataSource dataSource) {
        try {
            String ext = findExtension(dataSource);
            return exists(dataSource, ext);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private String findExtension(ReadOnlyDataSource dataSource) throws IOException {
        for (String ext : EXTENSIONS) {
            if (dataSource.isDataExtension(ext) && dataSource.exists(null, ext)) {
                return ext;
            }
        }
        return null;
    }

    private boolean exists(ReadOnlyDataSource dataSource, String ext) throws IOException {
        if (ext != null) {
            try {
                return PowerFlowDataFactory.create(ext).isValidFile(dataSource, ext);
            } catch (PsseException | IOException e) {
                LOGGER.error(String.format("Invalid content in filename %s.%s: %s",
                        dataSource.getBaseName(),
                        ext,
                        e.getMessage()));
            }
        }
        return false;
    }

    @Override
    public void copy(ReadOnlyDataSource fromDataSource, DataSource toDataSource) {
        Objects.requireNonNull(fromDataSource);
        Objects.requireNonNull(toDataSource);
        try {
            String ext = findExtension(fromDataSource);
            if (!exists(fromDataSource, ext)) {
                throw new PowsyblException("From data source is not importable");
            }
            try (InputStream is = fromDataSource.newInputStream(null, ext);
                 OutputStream os = toDataSource.newOutputStream(null, ext, false)) {
                ByteStreams.copy(is, os);
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    @Override
    public Network importData(ReadOnlyDataSource dataSource, NetworkFactory networkFactory, Properties parameters) {
        Objects.requireNonNull(dataSource);
        Objects.requireNonNull(networkFactory);
        try {
            String ext = findExtension(dataSource);
            if (ext == null) {
                throw new PsseException(String.format("No Power Flow Data file found. Basename: %s, supported extensions: %s",
                        dataSource.getBaseName(),
                        String.join("|", EXTENSIONS)));
            }
            PsseVersion version = PowerFlowDataFactory.create(ext).readVersion(dataSource, ext);
            Context context = new Context();
            PssePowerFlowModel pssePowerFlowModel = PowerFlowDataFactory.create(ext, version).read(dataSource, ext, context);
            pssePowerFlowModel.getCaseIdentification().validate();

            new PsseFixes(pssePowerFlowModel, context.getVersion()).fix();
            PsseValidation psseValidation = new PsseValidation(pssePowerFlowModel, context.getVersion());
            if (!psseValidation.isValidCase()) {
                throw new PsseException("The PSS/E file is not a valid case");
            }

            Network network = networkFactory.createNetwork(dataSource.getBaseName(), FORMAT);
            // TODO store the PsseContext with the Network to be able to export back using its information
            convert(pssePowerFlowModel, network, parameters, version);

            // Add the model and context as extensions
            network.newExtension(PsseModelExtensionAdder.class).withModel(pssePowerFlowModel).add();
            network.newExtension(PsseConversionContextExtensionAdder.class).withContext(context).add();

            return network;
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private Network convert(PssePowerFlowModel psseModel, Network network, Properties parameters, PsseVersion version) {
        // set date and time
        // TODO

        boolean ignoreBaseVoltage = Parameter.readBoolean(FORMAT, parameters, IGNORE_BASE_VOLTAGE_PARAMETER,
                ParameterDefaultValueConfig.INSTANCE);
        boolean ignoreNodeBreakerTopology = Parameter.readBoolean(FORMAT, parameters, IGNORE_NODE_BREAKER_TOPOLOGY_PARAMETER,
                ParameterDefaultValueConfig.INSTANCE);
        PerUnitContext perUnitContext = new PerUnitContext(psseModel.getCaseIdentification().getSbase(), ignoreBaseVoltage);

        // The map gives access to PsseBus object with the int bus Number
        Map<Integer, PsseBus> busNumToPsseBus = psseModel.getBuses().stream().collect(Collectors.toMap(PsseBus::getI, Function.identity()));

        // Necessary data for validating nodeBreaker topology
        NodeBreakerValidation nodeBreakerValidation = new NodeBreakerValidation(ignoreNodeBreakerTopology);
        nodeBreakerValidation.fillAndValidate(psseModel, version);

        // build container to fit IIDM requirements
        ContainersMapping containersMapping = defineContainersMapping(psseModel, busNumToPsseBus, perUnitContext, nodeBreakerValidation);

        // create buses
        NodeBreakerImport nodeBreakerImport = createBuses(psseModel, containersMapping, perUnitContext, network, nodeBreakerValidation);

        // Create loads
        for (PsseLoad psseLoad : psseModel.getLoads()) {
            new LoadConverter(psseLoad, containersMapping, network, nodeBreakerImport).create();
        }

        // Create fixed shunts
        for (PsseFixedShunt psseShunt : psseModel.getFixedShunts()) {
            new FixedShuntCompensatorConverter(psseShunt, containersMapping, network, nodeBreakerImport).create();
        }

        for (PsseGenerator psseGen : psseModel.getGenerators()) {
            new GeneratorConverter(psseGen, containersMapping, network, nodeBreakerImport).create();
        }

        for (PsseNonTransformerBranch psseLine : psseModel.getNonTransformerBranches()) {
            new LineConverter(psseLine, containersMapping, perUnitContext, network, version, nodeBreakerImport).create();
        }

        for (PsseTransformer psseTfo : psseModel.getTransformers()) {
            new TransformerConverter(psseTfo, containersMapping, perUnitContext, network, busNumToPsseBus, psseModel.getCaseIdentification().getSbase(), version, nodeBreakerImport).create();
        }

        for (PsseTwoTerminalDcTransmissionLine psseTwoTerminaDc : psseModel.getTwoTerminalDcTransmissionLines()) {
            new TwoTerminalDcConverter(psseTwoTerminaDc, containersMapping, network, nodeBreakerImport).create();
        }

        // Create switched shunts
        for (PsseSwitchedShunt psseSwShunt : psseModel.getSwitchedShunts()) {
            new SwitchedShuntCompensatorConverter(psseSwShunt, containersMapping, network, version, nodeBreakerImport).create();
        }

        // Create areas
        for (PsseArea psseArea : psseModel.getAreas()) {
            new AreaConverter(psseArea, psseModel.getBuses(), containersMapping, network).create();
        }

        // Attach a slack bus
        new SlackConverter(psseModel.getBuses(), containersMapping, network, nodeBreakerImport).create();

        // Add controls
        for (PsseGenerator psseGen : psseModel.getGenerators()) {
            new GeneratorConverter(psseGen, containersMapping, network, nodeBreakerImport).addControl(busNumToPsseBus.get(psseGen.getI()));
        }
        for (PsseTransformer psseTransformer : psseModel.getTransformers()) {
            new TransformerConverter(psseTransformer, containersMapping, perUnitContext, network, busNumToPsseBus, psseModel.getCaseIdentification().getSbase(), version, nodeBreakerImport).addControl();
        }
        for (PsseSwitchedShunt psseSwShunt : psseModel.getSwitchedShunts()) {
            new SwitchedShuntCompensatorConverter(psseSwShunt, containersMapping, network, version, nodeBreakerImport).addControl();
        }
        return network;
    }

    private ContainersMapping defineContainersMapping(PssePowerFlowModel psseModel, Map<Integer, PsseBus> busNumToPsseBus, PerUnitContext perUnitContext, NodeBreakerValidation nodeBreakerValidation) {
        List<Edge> edges = new ArrayList<>();
        // only zeroImpedance Lines are necessary and as they are not allowed, nothing to do

        defineEdgesForTransformers(psseModel, busNumToPsseBus, edges);
        // buses inside a psse substation are connected in the same way as transformers
        defineEdgesForBusesInsideSubstation(nodeBreakerValidation, edges);

        return ContainersMapping.create(psseModel.getBuses(), edges,
                PsseBus::getI,
                Edge::bus1,
                Edge::bus2,
                Edge::zeroImpedance,
                Edge::transformer,
                busNumber -> getNominalVFromBusNumber(busNumToPsseBus, busNumber, perUnitContext),
                AbstractConverter::getVoltageLevelId,
                substationNums -> getSubstationId(nodeBreakerValidation, substationNums));
    }

    private void defineEdgesForTransformers(PssePowerFlowModel psseModel, Map<Integer, PsseBus> busNumToPsseBus, List<Edge> edges) {
        psseModel.getTransformers().forEach(t -> {
            if (t.getK() == 0) { // twoWindingsTransformers with zero impedance are not allowed
                if (busNumToPsseBus.containsKey(t.getI()) && busNumToPsseBus.containsKey(t.getJ())) {
                    edges.add(new Edge(t.getI(), t.getJ(), true, false));
                }
            } else { // threeWindingsTransformers with zero impedance are not allowed
                if (busNumToPsseBus.containsKey(t.getI()) && busNumToPsseBus.containsKey(t.getJ()) && busNumToPsseBus.containsKey(t.getK())) {
                    edges.add(new Edge(t.getI(), t.getJ(), true, false));
                    edges.add(new Edge(t.getI(), t.getK(), true, false));
                }
            }
        });
    }

    private void defineEdgesForBusesInsideSubstation(NodeBreakerValidation nodeBreakerValidation, List<Edge> edges) {
        nodeBreakerValidation.getValidSubstations().forEach(psseSubstation -> {
            List<Integer> busesInside = nodeBreakerValidation.getBuses(psseSubstation);
            if (busesInside.size() >= 2) {
                int bus = busesInside.get(0);
                for (int index = 1; index < busesInside.size(); index++) {
                    edges.add(new Edge(bus, busesInside.get(index), true, false));
                }
            }
        });
    }

    private static String getSubstationId(NodeBreakerValidation nodeBreakerValidation, Set<Integer> substationBusNumbers) {
        Set<Integer> validSubstationIds = nodeBreakerValidation.getValidSubstationsIds(substationBusNumbers);
        return validSubstationIds.isEmpty() ? getSubstationIdFromBuses(substationBusNumbers) : getSubstationIdFromPsseSubstationIds(validSubstationIds);
    }

    private double getNominalVFromBusNumber(Map<Integer, PsseBus> busNumToPsseBus, int busNumber, PerUnitContext perUnitContext) {
        if (!busNumToPsseBus.containsKey(busNumber)) { // never should happen
            throw new PsseException("busId without PsseBus" + busNumber);
        }
        return VoltageLevelConverter.getNominalV(busNumToPsseBus.get(busNumber), perUnitContext.ignoreBaseVoltage());
    }

    private static NodeBreakerImport createBuses(PssePowerFlowModel psseModel, ContainersMapping containersMapping,
                                                 PerUnitContext perUnitContext, Network network,
                                                 NodeBreakerValidation nodeBreakerValidation) {

        NodeBreakerImport nodeBreakerImport = new NodeBreakerImport();

        for (PsseBus psseBus : psseModel.getBuses()) {
            Substation substation = new SubstationConverter(psseBus, containersMapping, network).create();
            VoltageLevel voltageLevel = new VoltageLevelConverter(psseBus, containersMapping, perUnitContext, network, nodeBreakerValidation, nodeBreakerImport).create(substation);
            new BusConverter(psseBus, containersMapping, network, nodeBreakerImport).create(voltageLevel);
        }

        return nodeBreakerImport;
    }

    private record Edge(int bus1, int bus2, boolean transformer, boolean zeroImpedance) {
    }

    record PerUnitContext(double sb, boolean ignoreBaseVoltage) {
    }
}