MatpowerExporter.java

/**
 * Copyright (c) 2022, 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.matpower.converter;

import com.google.auto.service.AutoService;
import com.powsybl.commons.config.PlatformConfig;
import com.powsybl.commons.datasource.DataSource;
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.commons.report.ReportNode;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.extensions.SlackTerminal;
import com.powsybl.iidm.network.util.HvdcUtils;
import com.powsybl.matpower.model.*;
import org.apache.commons.math3.complex.Complex;
import org.jgrapht.Graph;
import org.jgrapht.alg.connectivity.ConnectivityInspector;
import org.jgrapht.alg.util.Pair;
import org.jgrapht.graph.Pseudograph;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.util.*;
import java.util.function.DoubleUnaryOperator;
import java.util.stream.Stream;

/**
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 */
@AutoService(Exporter.class)
public class MatpowerExporter implements Exporter {

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

    private static final double BASE_MVA = 100;
    private static final int AREA_NUMBER = 1;
    private static final int LOSS_ZONE = 1;
    private static final int CONNECTED_STATUS = 1;
    private static final int DISCONNECTED_STATUS = 0;
    private static final String V_PROP = "v";
    private static final String ANGLE_PROP = "angle";
    private static final double MIN_Z_PU = Math.pow(10, -8);

    public static final String WITH_BUS_NAMES_PARAMETER_NAME = "matpower.export.with-bus-names";
    public static final String MAX_GENERATOR_ACTIVE_POWER_LIMIT_PARAMETER_NAME = "matpower.export.max-generator-active-power-limit";
    public static final String MAX_GENERATOR_REACTIVE_POWER_LIMIT_PARAMETER_NAME = "matpower.export.max-generator-reactive-power-limit";

    private static final boolean WITH_BUS_NAMES_DEFAULT_VALUE = false;
    private static final double MAX_GENERATOR_ACTIVE_POWER_LIMIT_DEFAULT_VALUE = 10000;
    private static final double MAX_GENERATOR_REACTIVE_POWER_LIMIT_DEFAULT_VALUE = 10000;

    private static final Parameter WITH_BUS_NAMES_PARAMETER = new Parameter(WITH_BUS_NAMES_PARAMETER_NAME,
                                                                            ParameterType.BOOLEAN,
                                                                            "Export bus names",
                                                                            WITH_BUS_NAMES_DEFAULT_VALUE);
    private static final Parameter MAX_GENERATOR_ACTIVE_POWER_LIMIT_PARAMETER
            = new Parameter(MAX_GENERATOR_ACTIVE_POWER_LIMIT_PARAMETER_NAME,
                            ParameterType.DOUBLE,
                            "Max generator active power limit to export",
                            MAX_GENERATOR_ACTIVE_POWER_LIMIT_DEFAULT_VALUE);
    private static final Parameter MAX_GENERATOR_REACTIVE_POWER_LIMIT_PARAMETER
            = new Parameter(MAX_GENERATOR_REACTIVE_POWER_LIMIT_PARAMETER_NAME,
                            ParameterType.DOUBLE,
                            "Max generator reactive power limit to export",
                            MAX_GENERATOR_REACTIVE_POWER_LIMIT_DEFAULT_VALUE);

    private static final List<Parameter> PARAMETERS = List.of(WITH_BUS_NAMES_PARAMETER,
                                                              MAX_GENERATOR_ACTIVE_POWER_LIMIT_PARAMETER,
                                                              MAX_GENERATOR_REACTIVE_POWER_LIMIT_PARAMETER);

    private final ParameterDefaultValueConfig defaultValueConfig;

    public MatpowerExporter() {
        this(PlatformConfig.defaultConfig());
    }

    public MatpowerExporter(PlatformConfig platformConfig) {
        defaultValueConfig = new ParameterDefaultValueConfig(platformConfig);
    }

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

    @Override
    public String getComment() {
        return "IIDM to MATPOWER format converter";
    }

    @Override
    public List<Parameter> getParameters() {
        return ConfiguredParameter.load(PARAMETERS, getFormat(), defaultValueConfig);
    }

    private static boolean hasSlackExtension(Bus bus) {
        VoltageLevel vl = bus.getVoltageLevel();
        SlackTerminal slackTerminal = vl.getExtension(SlackTerminal.class);
        if (slackTerminal != null) {
            Terminal terminal = slackTerminal.getTerminal();
            return terminal.getBusView().getBus() == bus;
        }
        return false;
    }

    private static MBus.Type getType(Bus bus, Context context) {
        if (context.refBusId.contains(bus.getId()) || hasSlackExtension(bus)) {
            return MBus.Type.REF;
        }
        // PV buses will be defined at the end of the export process
        return MBus.Type.PQ;
    }

    static class Context {

        private final double maxGeneratorActivePowerLimit;

        private final double maxGeneratorReactivePowerLimit;

        final List<String> refBusId = new ArrayList<>();

        int num = 0;

        final Map<String, Integer> mBusesNumbersByIds = new HashMap<>();

        final List<String> generatorIdsConvertedToLoad = new ArrayList<>();
        final Set<Component> synchronousComponentsToBeExported = new HashSet<>();
        final Map<Integer, List<GenRc>> generatorsToBeExported = new HashMap<>();

        private record GenRc(String id, double targetVpu, double targetP, double minP, double maxP, double targetQ, double minQ, double maxQ,
                             boolean isValidVoltageRegulation, boolean isRemoteRegulation, double ratedS) {
        }

        public Context(double maxGeneratorActivePowerLimit, double maxGeneratorReactivePowerLimit) {
            this.maxGeneratorActivePowerLimit = maxGeneratorActivePowerLimit;
            this.maxGeneratorReactivePowerLimit = maxGeneratorReactivePowerLimit;
        }

        // Matpower power flow does not support multiple components
        // Only Vsc HvdcLines with regulation on at both converters are exported as dcLines.
        // Vsc converters with regulation on are exported as generators and as loads when regulation is off
        // All Lcc converters are exported as loads
        // then we cannot always include the complete mainConnectedComponent.
        // Only the synchronousComponents connected to the MainSynchronousComponent
        // using Vsc HvdcLines with regulation on at both converters must be considered.
        // We built a graph with all synchronousComponents connected by vsc hvdcLines.
        // Only the connectedSets including the MainSynchronousComponent, must be considered
        private void findSynchronousComponentsToBeExported(Network network) {
            // MainSynchronousComponent is always exported
            synchronousComponentsToBeExported.add(network.getBusView().getSynchronousComponents().stream()
                    .filter(Context::isMainSynchronousComponent)
                    .findAny()
                    .orElseThrow());

            List<Set<Component>> connectedSets = findConnectedSetsOfSynchronousComponents(network);

            for (Set<Component> connectedSet : connectedSets) {
                if (connectedSet.stream().anyMatch(Context::isMainSynchronousComponent)) {
                    synchronousComponentsToBeExported.addAll(connectedSet);
                }
            }
        }

        // Duplicated vertices and edges are discarded by the graph.
        private static List<Set<Component>> findConnectedSetsOfSynchronousComponents(Network network) {
            Graph<Component, Pair<Component, Component>> scGraph = new Pseudograph<>(null, null, false);

            // Only Vsc hvdcLines are considered
            network.getHvdcLines().forEach(hvdcLine -> {
                if (hvdcLine.getConverterStation1().getHvdcType().equals(HvdcConverterStation.HvdcType.VSC)
                        && hvdcLine.getConverterStation2().getHvdcType().equals(HvdcConverterStation.HvdcType.VSC)
                        && isExportedAsDcLine((VscConverterStation) hvdcLine.getConverterStation1(), (VscConverterStation) hvdcLine.getConverterStation2())) {
                    addToGraph(scGraph, hvdcLine);
                }
            });

            return new ConnectivityInspector<>(scGraph).connectedSets();
        }

        private static void addToGraph(Graph<Component, Pair<Component, Component>> scGraph, HvdcLine hvdcLine) {
            Component synchronousComponent1 = findComponent(hvdcLine.getConverterStation1());
            Component synchronousComponent2 = findComponent(hvdcLine.getConverterStation2());

            if (synchronousComponent1 != null && synchronousComponent2 != null && synchronousComponent1 != synchronousComponent2) {
                scGraph.addVertex(synchronousComponent1);
                scGraph.addVertex(synchronousComponent2);
                scGraph.addEdge(synchronousComponent1, synchronousComponent2, Pair.of(synchronousComponent1, synchronousComponent2));
            }
        }

        private static Component findComponent(HvdcConverterStation<?> hvdcConverterStation) {
            Terminal terminal = hvdcConverterStation.getTerminal();
            Bus bus = terminal.getBusView().getBus();
            return terminal.isConnected() && bus != null ? bus.getSynchronousComponent() : null;
        }

        private static boolean isMainSynchronousComponent(Component synchronousComponent) {
            return synchronousComponent.getSize() > 0 && synchronousComponent.getBuses().iterator().next().isInMainSynchronousComponent();
        }
    }

    private static boolean isExported(Bus bus, Context context) {
        return bus != null && context.synchronousComponentsToBeExported.contains(bus.getSynchronousComponent());
    }

    // In matpower cases, the bus number is the only way to identify it. During the export process, we preserve the
    // original ones if the iidm model has been created by importing a matpower model
    private static int preserveBusIds(Network network, Context context) {
        List<Bus> busBreakerViewBuses = network.getVoltageLevelStream()
                .filter(voltageLevel -> voltageLevel.getTopologyKind().equals(TopologyKind.BUS_BREAKER))
                .flatMap(voltageLevel -> voltageLevel.getBusBreakerView().getBusStream())
                .toList();

        Map<String, List<Integer>> busIdNumbers = new HashMap<>();
        for (Bus busBreakerViewBus : busBreakerViewBuses) {
            OptionalInt number = extractBusNumber(busBreakerViewBus.getId());
            if (number.isPresent()) {
                Bus bus = busBreakerViewBus.getVoltageLevel().getBusView().getMergedBus(busBreakerViewBus.getId());
                if (bus != null) {
                    busIdNumbers.computeIfAbsent(bus.getId(), n -> new ArrayList<>()).add(number.getAsInt());
                }
            }
        }
        // select the minimum as the number
        busIdNumbers.forEach((key, value) -> context.mBusesNumbersByIds.put(key, value.stream().min(Comparator.naturalOrder()).orElseThrow()));

        // last number used
        return context.mBusesNumbersByIds.values().stream().max(Comparator.naturalOrder()).orElse(0);
    }

    // according to the busId of the import process
    private static OptionalInt extractBusNumber(String configuredBusId) {
        String busNumber = configuredBusId.replace("BUS-", "");
        return busNumber.matches("[1-9]\\d*") ? OptionalInt.of(Integer.parseInt(busNumber)) : OptionalInt.empty();
    }

    private static int findBusNumber(String busId, Context context) {
        if (context.mBusesNumbersByIds.containsKey(busId)) {
            return context.mBusesNumbersByIds.get(busId);
        }
        context.num++;
        context.mBusesNumbersByIds.put(busId, context.num);
        return context.num;
    }

    private static void createTransformerStarBuses(Network network, MatpowerModel model, Context context) {
        for (ThreeWindingsTransformer twt : network.getThreeWindingsTransformers()) {
            Bus bus1 = twt.getLeg1().getTerminal().getBusView().getBus();
            Bus bus2 = twt.getLeg2().getTerminal().getBusView().getBus();
            Bus bus3 = twt.getLeg3().getTerminal().getBusView().getBus();
            if (isExported(bus1, context) && isExported(bus2, context) && isExported(bus3, context)) {
                MBus mBus = new MBus();
                mBus.setNumber(findBusNumber(twt.getId(), context));
                mBus.setName(twt.getNameOrId());
                mBus.setType(MBus.Type.PQ);
                mBus.setAreaNumber(AREA_NUMBER);
                mBus.setLossZone(LOSS_ZONE);
                mBus.setBaseVoltage(twt.getRatedU0());
                mBus.setMinimumVoltageMagnitude(0d);
                mBus.setMaximumVoltageMagnitude(0d);
                mBus.setRealPowerDemand(0d);
                mBus.setReactivePowerDemand(0d);
                mBus.setShuntConductance(0d);
                mBus.setShuntSusceptance(0d);
                mBus.setVoltageMagnitude(checkAndFixVoltageMagnitude(twt.hasProperty(V_PROP) ? Double.parseDouble(twt.getProperty(V_PROP)) / twt.getRatedU0() : 1d));
                mBus.setVoltageAngle(checkAndFixVoltageAngle(twt.hasProperty(ANGLE_PROP) ? Double.parseDouble(twt.getProperty(ANGLE_PROP)) : 0d));
                mBus.setMinimumVoltageMagnitude(0d);
                mBus.setMaximumVoltageMagnitude(0d);
                model.addBus(mBus);
            }
        }
    }

    private static void createDanglingLineBuses(Network network, MatpowerModel model, Context context) {
        for (DanglingLine dl : network.getDanglingLines(DanglingLineFilter.UNPAIRED)) {
            Terminal t = dl.getTerminal();
            Bus bus = t.getBusView().getBus();
            if (isExported(bus, context)) {
                VoltageLevel vl = t.getVoltageLevel();
                MBus mBus = new MBus();
                mBus.setNumber(findBusNumber(dl.getId(), context));
                mBus.setName(dl.getNameOrId());
                mBus.setType(MBus.Type.PQ);
                mBus.setAreaNumber(AREA_NUMBER);
                mBus.setLossZone(LOSS_ZONE);
                mBus.setBaseVoltage(dl.getTerminal().getVoltageLevel().getNominalV());
                mBus.setMinimumVoltageMagnitude(0d);
                mBus.setMaximumVoltageMagnitude(0d);
                mBus.setRealPowerDemand(dl.getP0());
                mBus.setReactivePowerDemand(dl.getQ0());
                mBus.setShuntConductance(0d);
                mBus.setShuntSusceptance(0d);
                mBus.setVoltageMagnitude(checkAndFixVoltageMagnitude(dl.getBoundary().getV() / vl.getNominalV()));
                mBus.setVoltageAngle(checkAndFixVoltageAngle(dl.getBoundary().getAngle()));
                mBus.setMinimumVoltageMagnitude(0d);
                mBus.setMaximumVoltageMagnitude(0d);
                model.addBus(mBus);
            }
        }
    }

    private static void createBuses(Network network, MatpowerModel model, Context context) {
        for (Bus bus : network.getBusView().getBuses()) {
            if (isExported(bus, context)) {
                VoltageLevel vl = bus.getVoltageLevel();
                MBus mBus = new MBus();
                mBus.setNumber(findBusNumber(bus.getId(), context));
                mBus.setName(bus.getNameOrId());
                mBus.setType(getType(bus, context));
                mBus.setAreaNumber(AREA_NUMBER);
                mBus.setLossZone(LOSS_ZONE);
                mBus.setBaseVoltage(vl.getNominalV());
                double pDemand = 0;
                double qDemand = 0;
                for (Load l : bus.getLoads()) {
                    pDemand += l.getP0();
                    qDemand += l.getQ0();
                }
                for (Battery battery : bus.getBatteries()) {
                    // generator convention for batteries
                    pDemand -= battery.getTargetP();
                    qDemand -= battery.getTargetQ();
                }
                for (LccConverterStation lcc : bus.getLccConverterStations()) {
                    pDemand += HvdcUtils.getConverterStationTargetP(lcc);
                    qDemand += HvdcUtils.getLccConverterStationLoadTargetQ(lcc);
                }
                mBus.setRealPowerDemand(pDemand);
                mBus.setReactivePowerDemand(qDemand);
                double gSum = 0;
                double bSum = 0;
                double zb = vl.getNominalV() * vl.getNominalV() / BASE_MVA;
                for (ShuntCompensator sc : bus.getShuntCompensators()) {
                    gSum += sc.getG() * zb * BASE_MVA;
                    bSum += sc.getB() * zb * BASE_MVA;
                }
                mBus.setShuntConductance(gSum);
                mBus.setShuntSusceptance(bSum);
                mBus.setVoltageMagnitude(checkAndFixVoltageMagnitude(bus.getV() / vl.getNominalV()));
                mBus.setVoltageAngle(checkAndFixVoltageAngle(bus.getAngle()));
                mBus.setMinimumVoltageMagnitude(getVoltageLimit(vl.getLowVoltageLimit(), vl.getNominalV()));
                mBus.setMaximumVoltageMagnitude(getVoltageLimit(vl.getHighVoltageLimit(), vl.getNominalV()));
                model.addBus(mBus);
            }
        }

        createDanglingLineBuses(network, model, context);
        createTransformerStarBuses(network, model, context);
    }

    private static double getVoltageLimit(double voltageLimit, double nominalV) {
        return Double.isNaN(voltageLimit) ? 0 : voltageLimit / nominalV;
    }

    private static boolean isEmergencyLimit(LoadingLimits.TemporaryLimit limit) {
        return limit.getAcceptableDuration() <= 60;
    }

    private static Optional<LoadingLimits.TemporaryLimit> findShortTermLimit(Stream<LoadingLimits.TemporaryLimit> limitStream) {
        return limitStream.filter(limit -> !isEmergencyLimit(limit))
                .max(Comparator.comparing(LoadingLimits.TemporaryLimit::getAcceptableDuration));
    }

    private static Optional<LoadingLimits.TemporaryLimit> findEmergencyLimit(Stream<LoadingLimits.TemporaryLimit> limitStream) {
        return limitStream.filter(MatpowerExporter::isEmergencyLimit)
                .min(Comparator.comparing(LoadingLimits.TemporaryLimit::getAcceptableDuration));
    }

    private static Optional<LoadingLimits.TemporaryLimit> previousLimit(Collection<LoadingLimits.TemporaryLimit> limits, LoadingLimits.TemporaryLimit limit) {
        return limits.stream().filter(l -> l.getAcceptableDuration() > limit.getAcceptableDuration())
                .min(Comparator.comparing(LoadingLimits.TemporaryLimit::getAcceptableDuration));
    }

    private static double toApparentPower(double current, VoltageLevel vl) {
        return current * vl.getNominalV() / 1000d;
    }

    private static void createLimits(MBranch mBranch, LoadingLimits limits, DoubleUnaryOperator converter) {
        // rateA is mapped to permanent limit
        if (!Double.isNaN(limits.getPermanentLimit())) {
            mBranch.setRateA(converter.applyAsDouble(limits.getPermanentLimit()));
        }
        // rateB is mapped to the shortest term limit, if not an emergency limit (tempo <= 60s)
        LoadingLimits.TemporaryLimit limitB = findShortTermLimit(limits.getTemporaryLimits().stream())
                .filter(limit -> !isEmergencyLimit(limit) && limit.getValue() != Double.MAX_VALUE)
                .orElse(null);
        if (limitB != null) {
            mBranch.setRateB(converter.applyAsDouble(limitB.getValue()));
        }
        // rateC is mapped to the emergency limit (tempo <= 60s)
        findEmergencyLimit(limits.getTemporaryLimits().stream())
                .flatMap(limit -> previousLimit(limits.getTemporaryLimits(), limit))
                .filter(limit -> limitB == null || limit.getAcceptableDuration() != limitB.getAcceptableDuration())
                .ifPresent(limitC -> mBranch.setRateC(converter.applyAsDouble(limitC.getValue())));
    }

    private static void createLimits(List<FlowsLimitsHolder> limitsHolders, VoltageLevel vl, MBranch mBranch) {
        limitsHolders.stream().flatMap(limitsHolder -> Stream.concat(limitsHolder.getApparentPowerLimits().stream(), // apparent power limits first then current limits
                                                                     limitsHolder.getCurrentLimits().stream()))
                .filter(limits -> !Double.isNaN(limits.getPermanentLimit())) // skip when there is no permanent
                .max(Comparator.comparingInt(loadingLimit -> loadingLimit.getTemporaryLimits().size())) // many temporary limits first
                .ifPresent(limits -> {
                    if (limits.getLimitType() == LimitType.CURRENT) {
                        createLimits(mBranch, limits, current -> toApparentPower(current, vl)); // convert from A to MVA
                    } else {
                        createLimits(mBranch, limits, DoubleUnaryOperator.identity());
                    }
                });
    }

    /**
     * Arbitrary adapted on side one.
     */
    private static class FlowsLimitsHolderBranchAdapter implements FlowsLimitsHolder {

        private final Branch<?> branch;

        private final TwoSides side;

        public FlowsLimitsHolderBranchAdapter(Branch<?> branch, TwoSides side) {
            this.branch = branch;
            this.side = side;
        }

        @Override
        public List<OperationalLimitsGroup> getOperationalLimitsGroups() {
            throw new UnsupportedOperationException();
        }

        @Override
        public Optional<String> getSelectedOperationalLimitsGroupId() {
            throw new UnsupportedOperationException();
        }

        @Override
        public Optional<OperationalLimitsGroup> getOperationalLimitsGroup(String id) {
            throw new UnsupportedOperationException();
        }

        @Override
        public Optional<OperationalLimitsGroup> getSelectedOperationalLimitsGroup() {
            throw new UnsupportedOperationException();
        }

        @Override
        public OperationalLimitsGroup newOperationalLimitsGroup(String id) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void setSelectedOperationalLimitsGroup(String id) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void removeOperationalLimitsGroup(String id) {
            throw new UnsupportedOperationException();
        }

        @Override
        public void cancelSelectedOperationalLimitsGroup() {
            throw new UnsupportedOperationException();
        }

        @Override
        public Optional<CurrentLimits> getCurrentLimits() {
            return branch.getCurrentLimits(side);
        }

        @Override
        public CurrentLimits getNullableCurrentLimits() {
            return branch.getNullableCurrentLimits(side);
        }

        @Override
        public Optional<ActivePowerLimits> getActivePowerLimits() {
            return branch.getActivePowerLimits(side);
        }

        @Override
        public ActivePowerLimits getNullableActivePowerLimits() {
            return branch.getNullableActivePowerLimits(side);
        }

        @Override
        public Optional<ApparentPowerLimits> getApparentPowerLimits() {
            return branch.getApparentPowerLimits(side);
        }

        @Override
        public ApparentPowerLimits getNullableApparentPowerLimits() {
            return branch.getNullableApparentPowerLimits(side);
        }

        @Override
        public CurrentLimitsAdder newCurrentLimits() {
            throw new UnsupportedOperationException();
        }

        @Override
        public ApparentPowerLimitsAdder newApparentPowerLimits() {
            throw new UnsupportedOperationException();
        }

        @Override
        public ActivePowerLimitsAdder newActivePowerLimits() {
            throw new UnsupportedOperationException();
        }
    }

    private void createLines(Network network, MatpowerModel model, Context context) {
        for (Line l : network.getLines()) {
            Terminal t1 = l.getTerminal1();
            Terminal t2 = l.getTerminal2();
            createMBranch(l.getId(), t1, t2, l.getR(), l.getX(), l.getB1(), l.getB2(), context)
                    .ifPresent(branch -> {
                        createLimits(List.of(new FlowsLimitsHolderBranchAdapter(l, TwoSides.ONE), new FlowsLimitsHolderBranchAdapter(l, TwoSides.TWO)),
                                     t1.getVoltageLevel(), branch);
                        model.addBranch(branch);
                    });
        }
    }

    private void createTransformers2(Network network, MatpowerModel model, Context context) {
        for (TwoWindingsTransformer twt : network.getTwoWindingsTransformers()) {
            createTransformer(twt, model, context);
        }
    }

    private void createTransformer(TwoWindingsTransformer twt, MatpowerModel model, Context context) {
        Terminal t1 = twt.getTerminal1();
        Terminal t2 = twt.getTerminal2();
        Bus bus1 = t1.getBusView().getBus();
        Bus bus2 = t2.getBusView().getBus();
        if (isExported(bus1, context) && isExported(bus2, context)) {
            if (!bus1.getId().equals(bus2.getId())) {
                VoltageLevel vl1 = t1.getVoltageLevel();
                VoltageLevel vl2 = t2.getVoltageLevel();
                MBranch mBranch = new MBranch();
                mBranch.setFrom(context.mBusesNumbersByIds.get(bus1.getId()));
                mBranch.setTo(context.mBusesNumbersByIds.get(bus2.getId()));
                mBranch.setStatus(CONNECTED_STATUS);
                double r = twt.getR();
                double x = twt.getX();
                double b = twt.getB();
                double rho = (twt.getRatedU2() / vl2.getNominalV()) / (twt.getRatedU1() / vl1.getNominalV());
                var rtc = twt.getRatioTapChanger();
                if (rtc != null) {
                    rho *= rtc.getCurrentStep().getRho();
                    r *= 1 + rtc.getCurrentStep().getR() / 100;
                    x *= 1 + rtc.getCurrentStep().getX() / 100;
                    b *= 1 + rtc.getCurrentStep().getB() / 100;
                }
                var ptc = twt.getPhaseTapChanger();
                if (ptc != null) {
                    mBranch.setPhaseShiftAngle(-ptc.getCurrentStep().getAlpha());
                    rho *= ptc.getCurrentStep().getRho();
                    r *= 1 + ptc.getCurrentStep().getR() / 100;
                    x *= 1 + ptc.getCurrentStep().getX() / 100;
                    b *= 1 + ptc.getCurrentStep().getB() / 100;
                }
                mBranch.setRatio(1d / rho);
                double zb = vl2.getNominalV() * vl2.getNominalV() / BASE_MVA;
                double rpu = r / zb;
                double xpu = x / zb;
                setBranchRX(twt.getId(), mBranch, rpu, xpu);
                mBranch.setB(b * zb);
                createLimits(List.of(new FlowsLimitsHolderBranchAdapter(twt, TwoSides.ONE), new FlowsLimitsHolderBranchAdapter(twt, TwoSides.TWO)),
                    t1.getVoltageLevel(), mBranch);
                model.addBranch(mBranch);
            } else {
                LOGGER.warn("Skip branch between connected to same bus '{}' at both sides", bus1.getId());
            }
        }
    }

    private void createTieLines(Network network, MatpowerModel model, Context context) {
        for (TieLine l : network.getTieLines()) {
            Terminal t1 = l.getDanglingLine1().getTerminal();
            Terminal t2 = l.getDanglingLine2().getTerminal();
            createMBranch(l.getId(), t1, t2, l.getR(), l.getX(), l.getB1(), l.getB2(), context)
                    .ifPresent(branch -> {
                        createLimits(List.of(new FlowsLimitsHolderBranchAdapter(l, TwoSides.ONE), new FlowsLimitsHolderBranchAdapter(l, TwoSides.TWO)),
                                     t1.getVoltageLevel(), branch);
                        model.addBranch(branch);
                    });
        }
    }

    private static Optional<MBranch> createMBranch(String id, Terminal t1, Terminal t2, double r, double x, double b1, double b2, Context context) {
        Bus bus1 = t1.getBusView().getBus();
        Bus bus2 = t2.getBusView().getBus();
        if (isExported(bus1, context) && isExported(bus2, context)) {
            if (!bus1.getId().equals(bus2.getId())) {
                VoltageLevel vl1 = t1.getVoltageLevel();
                VoltageLevel vl2 = t2.getVoltageLevel();
                MBranch mBranch = new MBranch();
                mBranch.setFrom(context.mBusesNumbersByIds.get(bus1.getId()));
                mBranch.setTo(context.mBusesNumbersByIds.get(bus2.getId()));
                mBranch.setStatus(CONNECTED_STATUS);

                double rpu = impedanceToPerUnitForLine(r, vl1.getNominalV(), vl2.getNominalV(), BASE_MVA);
                double xpu = impedanceToPerUnitForLine(x, vl1.getNominalV(), vl2.getNominalV(), BASE_MVA);
                Complex ytr = impedanceToAdmittance(r, x);
                double b1pu = admittanceEndToPerUnitForLine(ytr.getImaginary(), b1, vl1.getNominalV(), vl2.getNominalV(), BASE_MVA);
                double b2pu = admittanceEndToPerUnitForLine(ytr.getImaginary(), b2, vl2.getNominalV(), vl1.getNominalV(), BASE_MVA);
                setBranchRX(id, mBranch, rpu, xpu);
                mBranch.setB(b1pu + b2pu);
                return Optional.of(mBranch);
            } else {
                LOGGER.warn("Skip branch between connected to same bus '{}' at both sides", bus1.getId());
            }
        }
        return Optional.empty();
    }

    // avoid NaN when r and x, both are 0.0
    private static Complex impedanceToAdmittance(double r, double x) {
        return r == 0.0 && x == 0.0 ? new Complex(0.0, 0.0) : new Complex(r, x).reciprocal();
    }

    private static double impedanceToPerUnitForLine(double impedance, double nominalVoltageAtEnd,
        double nominalVoltageAtOtherEnd, double sBase) {
        // this method handles also line with different nominal voltage at ends
        return impedance * sBase / (nominalVoltageAtEnd * nominalVoltageAtOtherEnd);
    }

    private static double admittanceEndToPerUnitForLine(double transmissionAdmittance, double shuntAdmittanceAtEnd,
        double nominalVoltageAtEnd, double nominalVoltageAtOtherEnd, double sBase) {
        // this method handles also line with different nominal voltage at ends
        // note that ytr is in engineering units
        return (shuntAdmittanceAtEnd * nominalVoltageAtEnd * nominalVoltageAtEnd
            + (nominalVoltageAtEnd - nominalVoltageAtOtherEnd) * nominalVoltageAtEnd * transmissionAdmittance) / sBase;
    }

    private void createDanglingLineBranches(Network network, MatpowerModel model, Context context) {
        for (DanglingLine dl : network.getDanglingLines(DanglingLineFilter.UNPAIRED)) {
            Terminal t = dl.getTerminal();
            Bus bus = t.getBusView().getBus();
            if (isExported(bus, context)) {
                VoltageLevel vl = t.getVoltageLevel();
                MBranch mBranch = new MBranch();
                mBranch.setFrom(context.mBusesNumbersByIds.get(bus.getId()));
                mBranch.setTo(context.mBusesNumbersByIds.get(dl.getId()));
                mBranch.setStatus(CONNECTED_STATUS);
                double zb = vl.getNominalV() * vl.getNominalV() / BASE_MVA;
                double rpu = dl.getR() / zb;
                double xpu = dl.getX() / zb;
                setBranchRX(dl.getId(), mBranch, rpu, xpu);
                mBranch.setB(dl.getB() * zb);
                createLimits(List.of(dl), t.getVoltageLevel(), mBranch);
                model.addBranch(mBranch);
            }
        }
    }

    private void createTransformerLegs(Network network, MatpowerModel model, Context context) {
        for (ThreeWindingsTransformer twt : network.getThreeWindingsTransformers()) {
            var leg1 = twt.getLeg1();
            var leg2 = twt.getLeg2();
            var leg3 = twt.getLeg3();
            Terminal t1 = leg1.getTerminal();
            Terminal t2 = leg2.getTerminal();
            Terminal t3 = leg3.getTerminal();
            Bus bus1 = t1.getBusView().getBus();
            Bus bus2 = t2.getBusView().getBus();
            Bus bus3 = t3.getBusView().getBus();
            if (isExported(bus1, context) && isExported(bus2, context) && isExported(bus3, context)) {
                model.addBranch(createTransformerLeg(twt, leg1, bus1, context));
                model.addBranch(createTransformerLeg(twt, leg2, bus2, context));
                model.addBranch(createTransformerLeg(twt, leg3, bus3, context));
            }
        }
    }

    private static MBranch createTransformerLeg(ThreeWindingsTransformer twt, ThreeWindingsTransformer.Leg leg, Bus bus, Context context) {
        MBranch mBranch = new MBranch();
        mBranch.setFrom(context.mBusesNumbersByIds.get(bus.getId()));
        mBranch.setTo(context.mBusesNumbersByIds.get(twt.getId()));
        mBranch.setStatus(CONNECTED_STATUS);
        double rho = 1d / (leg.getRatedU() / leg.getTerminal().getVoltageLevel().getNominalV());
        double r = leg.getR();
        double x = leg.getX();
        double b = leg.getB();
        var rtc = leg.getRatioTapChanger();
        if (rtc != null) {
            rho *= rtc.getCurrentStep().getRho();
            r *= 1 + rtc.getCurrentStep().getR() / 100;
            x *= 1 + rtc.getCurrentStep().getX() / 100;
            b *= 1 + rtc.getCurrentStep().getB() / 100;
        }
        var ptc = leg.getPhaseTapChanger();
        if (ptc != null) {
            mBranch.setPhaseShiftAngle(-ptc.getCurrentStep().getAlpha());
            rho *= ptc.getCurrentStep().getRho();
            r *= 1 + ptc.getCurrentStep().getR() / 100;
            x *= 1 + ptc.getCurrentStep().getX() / 100;
            b *= 1 + ptc.getCurrentStep().getB() / 100;
        }
        double zb = Math.pow(twt.getRatedU0(), 2) / BASE_MVA;
        double rpu = r / zb;
        double xpu = x / zb;
        setBranchRX(twt.getId() + "(leg " + leg.getSide().getNum() + ")", mBranch, rpu, xpu);
        mBranch.setB(b * zb);
        mBranch.setRatio(1d / rho);
        createLimits(List.of(leg), leg.getTerminal().getVoltageLevel(), mBranch);
        return mBranch;
    }

    private static void setBranchRX(String id, MBranch mBranch, double rpu, double xpu) {
        double zpu = Math.hypot(rpu, xpu);
        double newRpu = rpu;
        double newXpu = xpu;
        if (zpu < MIN_Z_PU) {
            LOGGER.warn("Branch '{}' has a low impedance {}, cut to {}", id, zpu, MIN_Z_PU);
            newRpu = 0;
            newXpu = MIN_Z_PU;
        }
        mBranch.setR(newRpu);
        mBranch.setX(newXpu);
    }

    private void createBranches(Network network, MatpowerModel model, Context context) {
        createLines(network, model, context);
        createTieLines(network, model, context);
        createTransformers2(network, model, context);
        createDanglingLineBranches(network, model, context);
        createTransformerLegs(network, model, context);
    }

    private void findDanglingLineGenerators(Network network, Context context) {
        for (DanglingLine dl : network.getDanglingLines(DanglingLineFilter.UNPAIRED)) {
            Terminal t = dl.getTerminal();
            Bus bus = t.getBusView().getBus();
            if (isExported(bus, context)) {
                var g = dl.getGeneration();
                if (g != null) {
                    int busNumber = context.mBusesNumbersByIds.get(dl.getId());
                    VoltageLevel vl = t.getVoltageLevel();
                    addMgen(context, busNumber, dl.getId(),
                            checkAndFixTargetVpu(g.getTargetV() / vl.getNominalV()),
                            g.getTargetP(),
                            Math.max(g.getMinP(), -context.maxGeneratorActivePowerLimit),
                            Math.min(g.getMaxP(), context.maxGeneratorActivePowerLimit),
                            g.getTargetQ(),
                            Math.max(g.getReactiveLimits().getMinQ(g.getTargetP()), -context.maxGeneratorReactivePowerLimit),
                            Math.min(g.getReactiveLimits().getMaxQ(g.getTargetP()), context.maxGeneratorReactivePowerLimit),
                            g.isVoltageRegulationOn(), false, Double.NaN);
                }
            }
        }
    }

    private void findGenerators(Network network, Context context) {
        for (Generator g : network.getGenerators()) {
            Terminal t = g.getTerminal();
            Bus bus = t.getBusView().getBus();
            if (isExported(bus, context)) {
                int busNumber = context.mBusesNumbersByIds.get(bus.getId());
                String id = g.getId();
                double targetP = g.getTargetP();
                double targetQ = g.getTargetQ();
                double targetVpu = checkAndFixTargetVpu(findTargetVpu(g));
                double minP = g.getMinP();
                double maxP = g.getMaxP();
                double maxQ = g.getReactiveLimits().getMaxQ(g.getTargetP());
                double minQ = g.getReactiveLimits().getMinQ(g.getTargetP());
                Bus regulatedBus = g.getRegulatingTerminal().getBusView().getBus();
                boolean isValidVoltageRegulation = isValidVoltageRegulation(g.isVoltageRegulatorOn(), regulatedBus);
                boolean isRemoteRegulation = isRemoteRegulation(bus, regulatedBus);
                double ratedS = g.getRatedS();
                addMgen(context, busNumber, id, targetVpu, targetP, minP, maxP, targetQ, Math.min(minQ, maxQ), Math.max(minQ, maxQ), isValidVoltageRegulation, isRemoteRegulation, ratedS);
            }
        }
    }

    // matpower only supports local control, all remote control will be localized with the defined targetVpu
    private static double findTargetVpu(Generator generator) {
        return generator.getTargetV() / generator.getRegulatingTerminal().getVoltageLevel().getNominalV();
    }

    private void findStaticVarCompensatorGenerators(Network network, Context context) {
        for (StaticVarCompensator svc : network.getStaticVarCompensators()) {
            Terminal t = svc.getTerminal();
            Bus bus = t.getBusView().getBus();
            if (isExported(bus, context)) {
                int busNumber = context.mBusesNumbersByIds.get(bus.getId());
                String id = svc.getId();
                double targetQ;
                if (StaticVarCompensator.RegulationMode.REACTIVE_POWER.equals(svc.getRegulationMode())) {
                    targetQ = -svc.getReactivePowerSetpoint();
                } else { // OFF or VOLTAGE regulation
                    targetQ = 0;
                }
                double vSquared = bus.getVoltageLevel().getNominalV() * bus.getVoltageLevel().getNominalV(); // approximation
                double minQ = svc.getBmin() * vSquared;
                double maxQ = svc.getBmax() * vSquared;
                double targetVpu = checkAndFixTargetVpu(findTargetVpu(svc));
                Bus regulatedBus = svc.getRegulatingTerminal().getBusView().getBus();
                boolean isValidVoltageRegulation = isValidVoltageRegulation(StaticVarCompensator.RegulationMode.VOLTAGE.equals(svc.getRegulationMode()), regulatedBus);
                boolean isRemoteRegulation = isRemoteRegulation(bus, regulatedBus);
                addMgen(context, busNumber, id, targetVpu, 0, 0, 0, targetQ, minQ, maxQ, isValidVoltageRegulation, isRemoteRegulation, Double.NaN);
            }
        }
    }

    // matpower only supports local control, all remote control will be localized with the defined targetVpu
    private static double findTargetVpu(StaticVarCompensator staticVarCompensator) {
        return staticVarCompensator.getVoltageSetpoint() / staticVarCompensator.getRegulatingTerminal().getVoltageLevel().getNominalV();
    }

    private void createDcLines(Network network, MatpowerModel model, Context context) {

        for (HvdcLine hvdcLine : network.getHvdcLines()) {
            HvdcConverterStation<?> hvdcConverterStation1 = hvdcLine.getConverterStation1();
            HvdcConverterStation<?> hvdcConverterStation2 = hvdcLine.getConverterStation2();
            if (hvdcConverterStation1 instanceof VscConverterStation vscConverterStation1
                    && hvdcConverterStation2 instanceof VscConverterStation vscConverterStation2) {

                if (hvdcLine.getConvertersMode().equals(HvdcLine.ConvertersMode.SIDE_1_RECTIFIER_SIDE_2_INVERTER)) {
                    exportVscHvdcLine(vscConverterStation1, vscConverterStation2, hvdcLine, model, context);
                } else {
                    exportVscHvdcLine(vscConverterStation2, vscConverterStation1, hvdcLine, model, context);
                }
            }
        }
    }

    private static boolean isExportedAsDcLine(VscConverterStation vscConverterStation1, VscConverterStation vscConverterStation2) {
        return vscConverterStation1.isVoltageRegulatorOn() && vscConverterStation2.isVoltageRegulatorOn();
    }

    private static void exportVscHvdcLine(VscConverterStation rectifierVscConverterStation, VscConverterStation inverterVscConverterStation, HvdcLine hvdcLine, MatpowerModel model, Context context) {
        if (isExportedAsDcLine(rectifierVscConverterStation, inverterVscConverterStation)) {
            createDcLine(rectifierVscConverterStation, inverterVscConverterStation, hvdcLine, model, context);
        } else {
            createGeneratorOrLoadFromVscConverter(rectifierVscConverterStation, context);
            createGeneratorOrLoadFromVscConverter(inverterVscConverterStation, context);
        }
    }

    private static void createDcLine(VscConverterStation rectifierVscConverterStation, VscConverterStation inverterVscConverterStation, HvdcLine hvdcLine, MatpowerModel model, Context context) {
        Terminal rectifierTerminal = rectifierVscConverterStation.getTerminal();
        Bus rectifierBus = findBus(rectifierTerminal);
        Terminal inverterTerminal = inverterVscConverterStation.getTerminal();
        Bus inverterBus = findBus(inverterTerminal);

        if (isExported(rectifierBus, context) && isExported(inverterBus, context)) {
            MDcLine mdcLine = new MDcLine();

            mdcLine.setFrom(context.mBusesNumbersByIds.get(rectifierBus.getId()));
            mdcLine.setTo(context.mBusesNumbersByIds.get(inverterBus.getId()));
            mdcLine.setStatus(getStatus(rectifierTerminal, inverterTerminal));

            double rectifierTargetP = -HvdcUtils.getConverterStationTargetP(rectifierVscConverterStation);
            double inverterTargetP = HvdcUtils.getConverterStationTargetP(inverterVscConverterStation);

            double maxP = hvdcLine.getMaxP();
            mdcLine.setPmin(0.0);
            mdcLine.setPmax(maxP);

            // equal to the negative of the injection of corresponding dummy generator
            mdcLine.setPf(rectifierTargetP);
            mdcLine.setQf(checkAndFixTargetQ(rectifierVscConverterStation.getReactivePowerSetpoint()));
            mdcLine.setVf(checkAndFixTargetVpu(findTargetVpu(rectifierVscConverterStation)));
            double rectifierMinQ = checkAndFixMinQ(rectifierVscConverterStation.getReactiveLimits().getMinQ(rectifierTargetP));
            double rectifierMaxQ = checkAndFixMaxQ(rectifierVscConverterStation.getReactiveLimits().getMaxQ(rectifierTargetP));
            mdcLine.setQminf(rectifierMinQ);
            mdcLine.setQmaxf(rectifierMaxQ);

            // equal to the injection of the corresponding generator
            mdcLine.setPt(inverterTargetP);
            mdcLine.setQt(checkAndFixTargetQ(inverterVscConverterStation.getReactivePowerSetpoint()));
            mdcLine.setVt(checkAndFixTargetVpu(findTargetVpu(inverterVscConverterStation)));
            double inverterMinQ = checkAndFixMinQ(inverterVscConverterStation.getReactiveLimits().getMinQ(inverterTargetP));
            double inverterMaxQ = checkAndFixMaxQ(inverterVscConverterStation.getReactiveLimits().getMaxQ(inverterTargetP));
            mdcLine.setQmint(inverterMinQ);
            mdcLine.setQmaxt(inverterMaxQ);

            double losses = rectifierTargetP - inverterTargetP;
            double l0 = calculateL0(rectifierVscConverterStation.getLossFactor(), rectifierTargetP, losses);
            mdcLine.setLoss0(l0);
            mdcLine.setLoss1(calculateL1(l0, losses, rectifierTargetP));
            model.addDcLine(mdcLine);
        }
    }

    private static Bus findBus(Terminal terminal) {
        return terminal.getBusView().getBus() != null ? terminal.getBusView().getBus() : terminal.getBusView().getConnectableBus();
    }

    private static int getStatus(Terminal t1, Terminal t2) {
        return t1.isConnected() && t2.isConnected() ? CONNECTED_STATUS : DISCONNECTED_STATUS;
    }

    private static double checkAndFixTargetQ(double targetQ) {
        return Double.isNaN(targetQ) ? 0.0 : targetQ;
    }

    private static double checkAndFixTargetVpu(double targetVpu) {
        return Double.isNaN(targetVpu) || targetVpu <= 0.0 ? 1.0 : targetVpu;
    }

    // If minQ is -Double.MAX_VALUE matpower sets the reactive power in dclines to NaN
    private static double checkAndFixMinQ(double minQ) {
        return minQ < -Integer.MAX_VALUE ? -Integer.MAX_VALUE : minQ;
    }

    // If maxQ is Double.MAX_VALUE matpower sets the reactive power in dclines to NaN
    private static double checkAndFixMaxQ(double maxQ) {
        return maxQ > Integer.MAX_VALUE ? Integer.MAX_VALUE : maxQ;
    }

    // matpower only supports local control, all remote control will be localized with the defined targetVpu
    private static double findTargetVpu(VscConverterStation vscConverterStation) {
        double nominalV = vscConverterStation.getTerminal().getVoltageLevel().getNominalV();
        if (vscConverterStation.getRegulatingTerminal() != null) {
            nominalV = vscConverterStation.getRegulatingTerminal().getVoltageLevel().getNominalV();
        }
        return vscConverterStation.getVoltageSetpoint() / nominalV;
    }

    // According to the import process, to guarantee round-trip
    private static double calculateL0(double lossFactor, double rectifierTargetP, double losses) {
        return rectifierTargetP != 0.0 ? lossFactor * rectifierTargetP / 100.0 : losses;
    }

    private static double calculateL1(double l0, double losses, double rectifierTargetP) {
        return rectifierTargetP != 0.0 ? (losses - l0) / rectifierTargetP : 0.0;
    }

    private static void createGeneratorOrLoadFromVscConverter(VscConverterStation vscConverterStation, Context context) {
        Terminal terminal = vscConverterStation.getTerminal();
        Bus bus = findBus(terminal);

        if (isExported(bus, context)) {
            int busNumber = context.mBusesNumbersByIds.get(bus.getId());
            String id = vscConverterStation.getId();
            double targetQ = checkAndFixTargetQ(vscConverterStation.getReactivePowerSetpoint());
            double targetVpu = checkAndFixTargetVpu(findTargetVpu(vscConverterStation));
            Bus regulatedBus = vscConverterStation.getRegulatingTerminal().getBusView().getBus();
            double targetP = HvdcUtils.getConverterStationTargetP(vscConverterStation);
            double minQ = checkAndFixMinQ(vscConverterStation.getReactiveLimits().getMinQ(targetP)); // approximation
            double maxQ = checkAndFixMaxQ(vscConverterStation.getReactiveLimits().getMaxQ(targetP)); // approximation
            boolean isValidVoltageRegulation = isValidVoltageRegulation(vscConverterStation.isVoltageRegulatorOn(), regulatedBus);
            double maxP = vscConverterStation.getHvdcLine().getMaxP();
            boolean isRemoteRegulation = isRemoteRegulation(bus, regulatedBus);
            addMgen(context, busNumber, id, targetVpu, targetP, -maxP, maxP, targetQ, minQ, maxQ, isValidVoltageRegulation, isRemoteRegulation, Double.NaN);
        }
    }

    private static void addMgen(Context context, int busNum, String id, double targetVpu, double targetP, double minP, double maxP,
                                double targetQ, double minQ, double maxQ, boolean isValidVoltageRegulation, boolean isRemoteRegulation, double ratedS) {
        Context.GenRc genRc = new Context.GenRc(id, targetVpu, targetP, minP, maxP, targetQ, minQ, maxQ, isValidVoltageRegulation, isRemoteRegulation, ratedS);
        context.generatorsToBeExported.computeIfAbsent(busNum, k -> new ArrayList<>()).add(genRc);
    }

    // Matpower power flow does not support bus with multiple generators that do not have the same voltage regulation
    // status. if the bus has PV type, all of its generator must have a valid voltage set point.
    private static void createGeneratorsAndDefinePVBuses(MatpowerModel model, Context context) {
        context.generatorsToBeExported.keySet().stream().sorted().forEach(busNumber -> {
            List<Context.GenRc> genRcs = context.generatorsToBeExported.get(busNumber);
            MBus mBus = model.getBusByNum(busNumber);
            List<Context.GenRc> genRcsWithRegulationOn = genRcs.stream().filter(genRc -> genRc.isValidVoltageRegulation).toList();
            List<Context.GenRc> genRcsWithRegulationOff = genRcs.stream().filter(genRc -> !genRc.isValidVoltageRegulation).toList();
            if (genRcsWithRegulationOn.isEmpty()) {
                genRcsWithRegulationOff.forEach(genRc -> {
                    MGen mGen = createMGen(model, busNumber, genRc, context);
                    // we can safely set voltage setpoint to zero, because a PQ bus never go back to PV even if reactive limits
                    // are activated in Matpower power flow
                    mGen.setVoltageMagnitudeSetpoint(0);
                });
            } else {
                if (mBus.getType().equals(MBus.Type.PQ)) {
                    mBus.setType(MBus.Type.PV);
                }
                genRcsWithRegulationOn.forEach(genRc -> createMGen(model, busNumber, genRc, context));

                genRcsWithRegulationOff.forEach(genRc -> {
                    mBus.setRealPowerDemand(mBus.getRealPowerDemand() - genRc.targetP);
                    mBus.setReactivePowerDemand(mBus.getReactivePowerDemand() - genRc.targetQ);
                    context.generatorIdsConvertedToLoad.add(genRc.id);
                });
            }
        });
    }

    private static MGen createMGen(MatpowerModel model, int busNumber, Context.GenRc genRc, Context context) {
        MGen mGen = new MGen();
        mGen.setNumber(busNumber);
        mGen.setStatus(CONNECTED_STATUS);
        mGen.setRealPowerOutput(genRc.targetP);
        mGen.setReactivePowerOutput(Double.isNaN(genRc.targetQ) ? 0 : genRc.targetQ);
        mGen.setVoltageMagnitudeSetpoint(genRc.targetVpu);

        mGen.setMinimumRealPowerOutput(Math.max(genRc.minP, -context.maxGeneratorActivePowerLimit));
        mGen.setMaximumRealPowerOutput(Math.min(genRc.maxP, context.maxGeneratorActivePowerLimit));
        mGen.setMinimumReactivePowerOutput(Math.max(genRc.minQ, -context.maxGeneratorReactivePowerLimit));
        mGen.setMaximumReactivePowerOutput(Math.min(genRc.maxQ, context.maxGeneratorReactivePowerLimit));
        mGen.setTotalMbase(Double.isNaN(genRc.ratedS) ? 0 : genRc.ratedS);
        model.addGenerator(mGen);

        if (genRc.isRemoteRegulation) {
            LOGGER.warn("Generator remote voltage control not supported in Matpower model, control has been localized {}", genRc.id);
        }
        return mGen;
    }

    private static boolean isValidVoltageRegulation(boolean voltageRegulation, Bus regulatedBus) {
        return voltageRegulation && regulatedBus != null;
    }

    private static boolean isRemoteRegulation(Bus bus, Bus regulatedBus) {
        return !(bus != null && regulatedBus != null && bus.getId().equals(regulatedBus.getId()));
    }

    private static double checkAndFixVoltageMagnitude(double voltageMagnitude) {
        return Double.isNaN(voltageMagnitude) || voltageMagnitude <= 0.0 ? 1.0 : voltageMagnitude;
    }

    private static double checkAndFixVoltageAngle(double voltageAngle) {
        return Double.isNaN(voltageAngle) ? 0.0 : voltageAngle;
    }

    // Matpower needs a slack bus for each synchronous component
    // Slack must be defined in a bus with generation or with dclines
    // to serve the roles of both a voltage angle reference and
    // a real power slack
    private static void findSlackBusesForEachSynchronousComponent(Context context) {

        context.synchronousComponentsToBeExported.forEach(synchronousComponent -> {
            boolean hasSlack = synchronousComponent.getBusStream()
                    .filter(bus -> isExported(bus, context))
                    .anyMatch(MatpowerExporter::hasSlackExtension);

            if (!hasSlack) {
                String refBusId = synchronousComponent.getBusStream()
                        .filter(bus -> isExported(bus, context))
                        .map(MatpowerExporter::getActivePowerGenerationAndVscCount)
                        .max(Comparator.comparing(Rc::activePowerGeneration)
                                .thenComparing(Rc::vscConvertersWithRegulationOn)
                                .thenComparing(rc -> rc.bus.getId()))
                        .orElseThrow()
                        .bus().getId();
                context.refBusId.add(refBusId);
                LOGGER.debug("Matpower reference bus automatically selected: {} for synchronousComponent: {}", refBusId, synchronousComponent.getNum());
            }
        });
    }

    private static Rc getActivePowerGenerationAndVscCount(Bus bus) {
        double[] activePowerGeneration = new double[1];
        int[] vscConverterCount = new int[1];
        bus.visitConnectedEquipments(new DefaultTopologyVisitor() {
            @Override
            public void visitGenerator(Generator generator) {
                activePowerGeneration[0] += generator.getMaxP();
            }

            @Override
            public void visitHvdcConverterStation(HvdcConverterStation<?> hvdcConverterStation) {
                if (hvdcConverterStation instanceof VscConverterStation vscConverterStation
                        && vscConverterStation.isVoltageRegulatorOn()) {
                        vscConverterCount[0]++;
                    }
                }
        });
        return new Rc(bus, activePowerGeneration[0], vscConverterCount[0]);
    }

    private record Rc(Bus bus, double activePowerGeneration, int vscConvertersWithRegulationOn) {
    }

    @Override
    public void export(Network network, Properties parameters, DataSource dataSource, ReportNode reportNode) {
        Objects.requireNonNull(network);
        Objects.requireNonNull(dataSource);
        Objects.requireNonNull(reportNode);

        boolean withBusNames = Parameter.readBoolean(getFormat(), parameters, WITH_BUS_NAMES_PARAMETER, defaultValueConfig);
        double maxGeneratorActivePower = Parameter.readDouble(getFormat(), parameters, MAX_GENERATOR_ACTIVE_POWER_LIMIT_PARAMETER, defaultValueConfig);
        double maxGeneratorReactivePower = Parameter.readDouble(getFormat(), parameters, MAX_GENERATOR_REACTIVE_POWER_LIMIT_PARAMETER, defaultValueConfig);

        MatpowerModel model = new MatpowerModel(network.getId());
        model.setBaseMva(BASE_MVA);
        model.setVersion(MatpowerFormatVersion.V2);

        Context context = new Context(maxGeneratorActivePower, maxGeneratorReactivePower);
        context.findSynchronousComponentsToBeExported(network);

        findSlackBusesForEachSynchronousComponent(context);

        context.num = preserveBusIds(network, context);
        createBuses(network, model, context);
        createBranches(network, model, context);
        findGenerators(network, context);
        findStaticVarCompensatorGenerators(network, context);
        findDanglingLineGenerators(network, context);
        createDcLines(network, model, context);

        createGeneratorsAndDefinePVBuses(model, context);

        if (!context.generatorIdsConvertedToLoad.isEmpty()) {
            LOGGER.debug("{} generators have been converted to a load: {}", context.generatorIdsConvertedToLoad.size(), context.generatorIdsConvertedToLoad);
        }

        try (OutputStream os = dataSource.newOutputStream(null, MatpowerConstants.EXT, false)) {
            MatpowerWriter.write(model, os, withBusNames);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }

        LOGGER.info("Matpower export of '{}' done", network.getId());
    }
}