EuropeanLvTestFeederFactory.java

/**
 * Copyright (c) 2023, RTE (http://www.rte-france.com)
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 * SPDX-License-Identifier: MPL-2.0
 */
package com.powsybl.iidm.network.test;

import com.powsybl.commons.PowsyblException;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.extensions.*;
import com.univocity.parsers.annotations.Parsed;
import com.univocity.parsers.common.processor.BeanListProcessor;
import com.univocity.parsers.csv.CsvParser;
import com.univocity.parsers.csv.CsvParserSettings;
import org.apache.commons.configuration2.INIConfiguration;
import org.apache.commons.configuration2.SubnodeConfiguration;
import org.apache.commons.configuration2.ex.ConfigurationException;
import java.time.ZonedDateTime;

import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.UncheckedIOException;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;

/**
 * European Low Voltage Test Feeder.
 * <p><a href="https://cmte.ieee.org/pes-testfeeders/resources/">PES test feeders</a></p>
 *
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 */
public final class EuropeanLvTestFeederFactory {

    private EuropeanLvTestFeederFactory() {
    }

    private static void createSource(Transformer transformer, Network network) {
        INIConfiguration iniConfiguration = new INIConfiguration();
        try (Reader reader = new InputStreamReader(Objects.requireNonNull(EuropeanLvTestFeederFactory.class.getResourceAsStream("/europeanLvTestFeeder/Source.csv")), StandardCharsets.UTF_8)) {
            iniConfiguration.read(reader);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        } catch (ConfigurationException e) {
            throw new PowsyblException(e);
        }
        SubnodeConfiguration source = iniConfiguration.getSection("Source");
        double voltage = Integer.parseInt(source.getString("Voltage").replace(" kV", ""));
        double pu = Double.parseDouble(source.getString("pu"));
        double isc3 = Integer.parseInt(source.getString("ISC3").replace(" A", ""));
        double isc1 = Integer.parseInt(source.getString("ISC1").replace(" A", ""));
        Substation sourceSubstation = network.newSubstation()
                .setId(getSubstationId(transformer.bus2)) // so that transformer has both sides in same substation
                .add();
        VoltageLevel sourceVoltageLevel = sourceSubstation.newVoltageLevel()
                .setId("SourceVoltageLevel")
                .setNominalV(voltage)
                .setTopologyKind(TopologyKind.BUS_BREAKER)
                .add();
        sourceVoltageLevel.getBusBreakerView().newBus()
                .setId("SourceBus")
                .add();
        Generator sourceGenerator = sourceVoltageLevel.newGenerator()
                .setId("SourceGenerator")
                .setBus("SourceBus")
                .setMinP(0)
                .setMaxP(0)
                .setTargetP(0)
                .setVoltageRegulatorOn(true)
                .setTargetV(voltage * pu)
                .add();
        sourceVoltageLevel.newExtension(SlackTerminalAdder.class)
                .withTerminal(sourceGenerator.getTerminal())
                .add();
        double zn = pu * voltage / (Math.sqrt(3) * isc1);
        double zz = pu * voltage / (Math.sqrt(3) * isc3);
        double rn = Math.sqrt(zn * zn / 101);
        double xn = 10 * rn;
        double rz = Math.sqrt(zz * zz / 101);
        double xz = 10 * rz;
        sourceGenerator.newExtension(GeneratorFortescueAdder.class)
                .withRn(rn)
                .withXn(xn)
                .withRz(rz)
                .withXz(xz)
                .add();
    }

    public static class BusCoord {
        @Parsed(field = "Busname")
        int busName;

        @Parsed
        double x;

        @Parsed
        double y;
    }

    public static class Line {
        @Parsed(field = "Name")
        String name;

        @Parsed(field = "Bus1")
        int bus1;

        @Parsed(field = "Bus2")
        int bus2;

        @Parsed(field = "Phases")
        String phases;

        @Parsed(field = "Length")
        double length;

        @Parsed(field = "Units")
        String units;

        @Parsed(field = "LineCode")
        String code;
    }

    public static class LineCode {
        @Parsed(field = "Name")
        String name;

        @Parsed
        int nphases;

        @Parsed(field = "R1")
        double r1;

        @Parsed(field = "X1")
        double x1;

        @Parsed(field = "R0")
        double r0;

        @Parsed(field = "X0")
        double x0;

        @Parsed(field = "C1")
        double c1;

        @Parsed(field = "C0")
        double c0;

        @Parsed(field = "Units")
        String units;
    }

    public static class Load {
        @Parsed(field = "Name")
        String name;

        @Parsed
        int numPhases;

        @Parsed(field = "Bus")
        int bus;

        @Parsed
        char phases;

        @Parsed
        double kV;

        @Parsed(field = "Model")
        int model;

        @Parsed(field = "Connection")
        String connection;

        @Parsed
        double kW;

        @Parsed(field = "PF")
        double pf;

        @Parsed(field = "Yearly")
        String yearly;
    }

    public static class Transformer {
        @Parsed(field = "Name")
        String name;

        @Parsed
        int phases;

        @Parsed
        String bus1;

        @Parsed
        int bus2;

        @Parsed(field = "kV_pri")
        double kvPri;

        @Parsed(field = "kV_sec")
        double kvSec;

        @Parsed(field = "MVA")
        double mva;

        @Parsed(field = "Conn_pri")
        String connPri;

        @Parsed(field = "Conn_sec")
        String connSec;

        @Parsed(field = "%XHL")
        double xhl;

        @Parsed(field = "% resistance")
        double resistance;
    }

    private static <T> List<T> parseCsv(String resourceName, Class<T> clazz) {
        try (Reader inputReader = new InputStreamReader(Objects.requireNonNull(EuropeanLvTestFeederFactory.class.getResourceAsStream(resourceName)), StandardCharsets.UTF_8)) {
            BeanListProcessor<T> rowProcessor = new BeanListProcessor<>(clazz);
            CsvParserSettings settings = new CsvParserSettings();
            settings.setHeaderExtractionEnabled(true);
            settings.setProcessor(rowProcessor);
            CsvParser parser = new CsvParser(settings);
            parser.parse(inputReader);
            return rowProcessor.getBeans();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private static String getBusId(int busName) {
        return "Bus-" + busName;
    }

    private static String getVoltageLevelId(int busName) {
        return "VoltageLevel-" + busName;
    }

    private static String getSubstationId(int busName) {
        return "Substation-" + busName;
    }

    private static void createBuses(Network network) {
        for (BusCoord busCoord : parseCsv("/europeanLvTestFeeder/Buscoords.csv", BusCoord.class)) {
            String substationId = getSubstationId(busCoord.busName);
            Substation s = network.getSubstation(substationId);
            if (s == null) {
                s = network.newSubstation()
                        .setId(substationId)
                        .add();
            }
            VoltageLevel vl = s.newVoltageLevel()
                    .setId(getVoltageLevelId(busCoord.busName))
                    .setTopologyKind(TopologyKind.BUS_BREAKER)
                    .setNominalV(1)
                    .add();
            vl.getBusBreakerView().newBus()
                    .setId(getBusId(busCoord.busName))
                    .add();
        }
    }

    private static void createLines(Network network) {
        Map<String, LineCode> lineCodes = new HashMap<>();
        for (LineCode lineCode : parseCsv("/europeanLvTestFeeder/LineCodes.csv", LineCode.class)) {
            lineCodes.put(lineCode.name, lineCode);
        }
        for (Line line : parseCsv("/europeanLvTestFeeder/Lines.csv", Line.class)) {
            LineCode lineCode = lineCodes.get(line.code);
            var l = network.newLine()
                    .setId("Line-" + line.bus1 + "-" + line.bus2)
                    .setVoltageLevel1(getVoltageLevelId(line.bus1))
                    .setBus1(getBusId(line.bus1))
                    .setVoltageLevel2(getVoltageLevelId(line.bus2))
                    .setBus2(getBusId(line.bus2))
                    .setR(lineCode.r1 * line.length)
                    .setX(lineCode.x1 * line.length)
                    .add();
            l.newExtension(LineFortescueAdder.class)
                    .withRz(lineCode.r0 * line.length)
                    .withXz(lineCode.x0 * line.length)
                    .add();
        }
    }

    private static LoadConnectionType getConnectionType(Load load) {
        if (load.connection.equals("wye")) {
            return LoadConnectionType.Y;
        }
        throw new PowsyblException("Unknown load connection: " + load.connection);
    }

    private static void createLoads(Network network) {
        for (Load load : parseCsv("/europeanLvTestFeeder/Loads.csv", Load.class)) {
            var vl = network.getVoltageLevel(getVoltageLevelId(load.bus));
            double p0 = load.kW / 1000;
            double q0 = p0 * load.pf;
            var l = vl.newLoad()
                    .setId("Load-" + load.bus)
                    .setBus(getBusId(load.bus))
                    .setP0(p0)
                    .setQ0(q0)
                    .add();
            double deltaPa = 0;
            double deltaQa = 0;
            double deltaPb = 0;
            double deltaQb = 0;
            double deltaPc = 0;
            double deltaQc = 0;
            switch (load.phases) {
                case 'A':
                    deltaPb = -p0;
                    deltaQb = -q0;
                    deltaPc = -p0;
                    deltaQc = -q0;
                    break;
                case 'B':
                    deltaPa = -p0;
                    deltaQa = -q0;
                    deltaPc = -p0;
                    deltaQc = -q0;
                    break;
                case 'C':
                    deltaPa = -p0;
                    deltaQa = -q0;
                    deltaPb = -p0;
                    deltaQb = -q0;
                    break;
                default:
                    throw new PowsyblException("Unknown phase: " + load.phases);
            }
            l.newExtension(LoadAsymmetricalAdder.class)
                    .withConnectionType(getConnectionType(load))
                    .withDeltaPa(deltaPa)
                    .withDeltaQa(deltaQa)
                    .withDeltaPb(deltaPb)
                    .withDeltaQb(deltaQb)
                    .withDeltaPc(deltaPc)
                    .withDeltaQc(deltaQc)
                    .add();
        }
    }

    private static WindingConnectionType getConnectionType(String conn) {
        switch (conn) {
            case "Delta":
                return WindingConnectionType.DELTA;
            case "Wye":
                return WindingConnectionType.Y;
            default:
                throw new PowsyblException("Connection type not supported: " + conn);
        }
    }

    private static void createTransformer(Transformer transformer, Network network) {
        String busId1 = transformer.bus1; // source
        String busId2 = getBusId(transformer.bus2);
        Bus bus1 = network.getBusBreakerView().getBus(busId1);
        Bus bus2 = network.getBusBreakerView().getBus(busId2);
        Substation s = bus1.getVoltageLevel().getSubstation().orElseThrow();
        double sb = 1; // 1 mva
        double zb = transformer.kvPri * transformer.kvPri / sb;
        double r = transformer.resistance / 100 / transformer.mva * zb;
        double x = transformer.xhl / 100 / transformer.mva * zb;
        var twt = s.newTwoWindingsTransformer()
                .setId("Transformer-" + transformer.bus1 + "-" + transformer.bus2)
                .setBus1(busId1)
                .setVoltageLevel1(bus1.getVoltageLevel().getId())
                .setBus2(busId2)
                .setVoltageLevel2(bus2.getVoltageLevel().getId())
                .setRatedU1(transformer.kvPri)
                .setRatedU2(transformer.kvSec)
                .setRatedS(transformer.mva)
                .setR(r)
                .setX(x)
                .add();
        twt.newExtension(TwoWindingsTransformerFortescueAdder.class)
                .withConnectionType1(getConnectionType(transformer.connPri))
                .withConnectionType2(getConnectionType(transformer.connSec))
                .add();
    }

    public static Network create() {
        return create(NetworkFactory.findDefault());
    }

    public static Network create(NetworkFactory networkFactory) {
        Network network = networkFactory.createNetwork("EuropeanLvTestFeeder", "csv");
        network.setCaseDate(ZonedDateTime.parse("2023-04-11T23:59:00.000+01:00"));
        Transformer transformer = parseCsv("/europeanLvTestFeeder/Transformer.csv", Transformer.class).get(0);
        createSource(transformer, network);
        createBuses(network);
        createLines(network);
        createLoads(network);
        createTransformer(transformer, network);
        return network;
    }
}