AbstractTransformerSerDe.java

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

import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.io.TreeDataWriter;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.PhaseTapChanger.RegulationMode;
import com.powsybl.iidm.serde.util.IidmSerDeUtil;

import java.util.OptionalInt;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.DoubleConsumer;

/**
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 */
abstract class AbstractTransformerSerDe<T extends Connectable<T>, A extends IdentifiableAdder<T, A>> extends AbstractSimpleIdentifiableSerDe<T, A, Substation> {

    private static final String ATTR_LOW_TAP_POSITION = "lowTapPosition";
    private static final String ATTR_TAP_POSITION = "tapPosition";
    private static final String ATTR_SOLVED_TAP_POSITION = "solvedTapPosition";
    private static final String ATTR_REGULATING = "regulating";
    private static final String ELEM_TERMINAL_REF = "terminalRef";
    private static final String ATTR_REGULATION_MODE = "regulationMode";
    private static final String ATTR_REGULATION_VALUE = "regulationValue";
    private static final String ATTR_LOAD_TAP_CHANGING_CAPABILITIES = "loadTapChangingCapabilities";
    static final String STEP_ROOT_ELEMENT_NAME = "step";
    static final String STEP_ARRAY_ELEMENT_NAME = "steps";
    private static final String TARGET_DEADBAND = "targetDeadband";
    private static final String RATIO_TAP_CHANGER = "ratioTapChanger";
    private static final String PHASE_TAP_CHANGER = "phaseTapChanger";

    protected static void writeTapChangerStep(TapChangerStep<?> tcs, TreeDataWriter writer) {
        writer.writeDoubleAttribute("r", tcs.getR());
        writer.writeDoubleAttribute("x", tcs.getX());
        writer.writeDoubleAttribute("g", tcs.getG());
        writer.writeDoubleAttribute("b", tcs.getB());
        writer.writeDoubleAttribute("rho", tcs.getRho());
    }

    private static void writeTargetDeadband(double targetDeadband, NetworkSerializerContext context) {
        // in IIDM version 1.0, 0 as targetDeadband is ignored for backwards compatibility
        // (i.e. ensuring round trips in IIDM version 1.0)
        IidmSerDeUtil.runUntilMaximumVersion(IidmVersion.V_1_1, context, () -> context.getWriter().writeDoubleAttribute(TARGET_DEADBAND, targetDeadband, 0));
        IidmSerDeUtil.runFromMinimumVersion(IidmVersion.V_1_2, context, () -> context.getWriter().writeDoubleAttribute(TARGET_DEADBAND, targetDeadband));
    }

    private static double readTargetDeadband(NetworkDeserializerContext context, boolean regulating) {
        double[] targetDeadband = new double[1];
        IidmSerDeUtil.runUntilMaximumVersion(IidmVersion.V_1_1, context, () -> {
            targetDeadband[0] = context.getReader().readDoubleAttribute(TARGET_DEADBAND);
            // in IIDM version 1.0, NaN as targetDeadband when regulating is allowed.
            // in IIDM version 1.1 and more recent, it is forbidden and throws an exception
            // to prevent issues, targetDeadband is set to 0 in this case
            if (regulating && Double.isNaN(targetDeadband[0])) {
                targetDeadband[0] = 0;
            }
        });
        IidmSerDeUtil.runFromMinimumVersion(IidmVersion.V_1_2,
                context, () -> targetDeadband[0] = context.getReader().readDoubleAttribute(TARGET_DEADBAND));
        return targetDeadband[0];
    }

    private static void writeTapChanger(TapChanger<?, ?, ?, ?> tc, NetworkSerializerContext context) {
        context.getWriter().writeIntAttribute(ATTR_LOW_TAP_POSITION, tc.getLowTapPosition());
        var tp = tc.findTapPosition();
        var stp = tc.findSolvedTapPosition();
        IidmSerDeUtil.runUntilMaximumVersion(IidmVersion.V_1_13, context, () -> {
            var position = stp.isPresent() ? stp : tp;
            context.getWriter().writeOptionalIntAttribute(ATTR_TAP_POSITION, position.isPresent() ? position.getAsInt() : null);
        });
        IidmSerDeUtil.runFromMinimumVersion(IidmVersion.V_1_14, context, () -> {
            context.getWriter().writeOptionalIntAttribute(ATTR_TAP_POSITION, tp.isPresent() ? tp.getAsInt() : null);
            context.getWriter().writeOptionalIntAttribute(ATTR_SOLVED_TAP_POSITION, stp.isPresent() ? stp.getAsInt() : null);
        });
        writeTargetDeadband(tc.getTargetDeadband(), context);
    }

    protected static void writeRatioTapChanger(String name, RatioTapChanger rtc, NetworkSerializerContext context) {
        context.getWriter().writeStartNode(context.getVersion().getNamespaceURI(context.isValid()), name);

        Boolean optionalRegulatingValue = !rtc.hasLoadTapChangingCapabilities() ? null : rtc.isRegulating();
        context.getWriter().writeOptionalBooleanAttribute(ATTR_REGULATING, optionalRegulatingValue);

        writeTapChanger(rtc, context);
        context.getWriter().writeBooleanAttribute(ATTR_LOAD_TAP_CHANGING_CAPABILITIES, rtc.hasLoadTapChangingCapabilities());
        IidmSerDeUtil.runUntilMaximumVersion(IidmVersion.V_1_11, context, () -> context.getWriter().writeDoubleAttribute("targetV", rtc.getRegulationValue()));
        IidmSerDeUtil.runFromMinimumVersion(IidmVersion.V_1_12, context, () -> {
            context.getWriter().writeEnumAttribute(ATTR_REGULATION_MODE, rtc.getRegulationMode());
            context.getWriter().writeDoubleAttribute(ATTR_REGULATION_VALUE, rtc.getRegulationValue());
        });
        TerminalRefSerDe.writeTerminalRef(rtc.getRegulationTerminal(), context, ELEM_TERMINAL_REF);

        context.getWriter().writeStartNodes();
        for (int p = rtc.getLowTapPosition(); p <= rtc.getHighTapPosition(); p++) {
            RatioTapChangerStep rtcs = rtc.getStep(p);
            context.getWriter().writeStartNode(context.getVersion().getNamespaceURI(context.isValid()), STEP_ROOT_ELEMENT_NAME);
            writeTapChangerStep(rtcs, context.getWriter());
            context.getWriter().writeEndNode();
        }
        context.getWriter().writeEndNodes();

        context.getWriter().writeEndNode();
    }

    protected static void readRatioTapChanger(String elementName, RatioTapChangerAdder adder, Terminal terminal, NetworkDeserializerContext context) {
        boolean regulating = readTapChangerAttributes(adder, context);

        boolean loadTapChangingCapabilities = context.getReader().readBooleanAttribute(ATTR_LOAD_TAP_CHANGING_CAPABILITIES);
        adder.setLoadTapChangingCapabilities(loadTapChangingCapabilities);

        IidmSerDeUtil.runUntilMaximumVersion(IidmVersion.V_1_11, context, () -> {
            double targetV = context.getReader().readDoubleAttribute("targetV");
            if (!Double.isNaN(targetV)) {
                adder.setRegulationMode(RatioTapChanger.RegulationMode.VOLTAGE);
            }
            adder.setRegulationValue(targetV);
        });
        IidmSerDeUtil.runFromMinimumVersion(IidmVersion.V_1_12, context, () -> {
            RatioTapChanger.RegulationMode regulationMode = context.getReader().readEnumAttribute(ATTR_REGULATION_MODE, RatioTapChanger.RegulationMode.class);
            double regulationValue = context.getReader().readDoubleAttribute(ATTR_REGULATION_VALUE);
            adder.setRegulationMode(regulationMode)
                    .setRegulationValue(regulationValue);
        });
        IidmSerDeUtil.runUntilMaximumVersion(IidmVersion.V_1_13, context, () -> {
            // starting v1.14 it is forbidden to be regulating without on-load tap changing capabilities
            if (!loadTapChangingCapabilities && regulating) {
                adder.setRegulating(false);
            }
        });

        boolean[] hasTerminalRef = new boolean[1];
        context.getReader().readChildNodes(subElementName -> {
            switch (subElementName) {
                case ELEM_TERMINAL_REF -> {
                    hasTerminalRef[0] = true;
                    readTapChangerTerminalRef(adder, terminal, context);
                }
                case STEP_ROOT_ELEMENT_NAME -> {
                    RatioTapChangerAdder.StepAdder stepAdder = adder.beginStep();
                    readSteps(context, stepAdder);
                    stepAdder.endStep();
                    context.getReader().readEndNode();
                }
                default -> throw new PowsyblException("Unknown element name '" + subElementName + "' in '" + elementName + "'");
            }
        });
        if (!hasTerminalRef[0]) {
            adder.add();
        }
    }

    protected static void readRatioTapChanger(TwoWindingsTransformer twt, NetworkDeserializerContext context) {
        readRatioTapChanger(RATIO_TAP_CHANGER, twt.newRatioTapChanger(), twt.getTerminal1(), context);
    }

    protected static void readRatioTapChanger(int leg, ThreeWindingsTransformer.Leg twl, NetworkDeserializerContext context) {
        readRatioTapChanger(RATIO_TAP_CHANGER + leg, twl.newRatioTapChanger(), twl.getTerminal(), context);
    }

    /**
     * Create ENUM to read old phase tap changer regulation mode values (versions < 1.14): FIXED_TAP value is no longer present for newer versions
     * Older versions with FIXED_TAP value should be imported as CURRENT_LIMITER regulation mode with a regulating boolean set to false
     */
    private enum PhaseTapChangerRegulationModeSerDe {
        CURRENT_LIMITER,
        ACTIVE_POWER_CONTROL,
        FIXED_TAP;

        static RegulationMode convertToRegulationMode(PhaseTapChangerRegulationModeSerDe regulationModeSerDe) {
            switch (regulationModeSerDe) {
                case CURRENT_LIMITER, FIXED_TAP -> {
                    return RegulationMode.CURRENT_LIMITER;
                }
                case ACTIVE_POWER_CONTROL -> {
                    return RegulationMode.ACTIVE_POWER_CONTROL;
                }
            }
            return RegulationMode.CURRENT_LIMITER;
        }
    }

    protected static void writePhaseTapChanger(String name, PhaseTapChanger ptc, NetworkSerializerContext context) {
        context.getWriter().writeStartNode(context.getVersion().getNamespaceURI(context.isValid()), name);

        RegulationMode regMode = ptc.getRegulationMode();
        Boolean optionalRegulatingValue = !ptc.hasLoadTapChangingCapabilities() || regMode == null ? null : ptc.isRegulating();
        context.getWriter().writeOptionalBooleanAttribute(ATTR_REGULATING, optionalRegulatingValue);

        writeTapChanger(ptc, context);
        IidmSerDeUtil.runFromMinimumVersion(IidmVersion.V_1_14, context, () ->
            context.getWriter().writeBooleanAttribute(ATTR_LOAD_TAP_CHANGING_CAPABILITIES, ptc.hasLoadTapChangingCapabilities())
        );
        if (context.getVersion().compareTo(IidmVersion.V_1_5) <= 0
                && (Double.isNaN(ptc.getRegulationValue()) || ptc.getRegulationTerminal() == null)) {
            // Backward compatibility for <= IIDM 1.5 where import was failing when regulation mode != FIXED_TAP
            // and either regulating value is NaN or regulation terminal is null.
            context.getWriter().writeEnumAttribute(ATTR_REGULATION_MODE, PhaseTapChangerRegulationModeSerDe.FIXED_TAP);
        } else {
            context.getWriter().writeEnumAttribute(ATTR_REGULATION_MODE, regMode);
        }
        context.getWriter().writeDoubleAttribute(ATTR_REGULATION_VALUE, ptc.getRegulationValue());
        TerminalRefSerDe.writeTerminalRef(ptc.getRegulationTerminal(), context, ELEM_TERMINAL_REF);

        context.getWriter().writeStartNodes();
        for (int p = ptc.getLowTapPosition(); p <= ptc.getHighTapPosition(); p++) {
            PhaseTapChangerStep ptcs = ptc.getStep(p);
            context.getWriter().writeStartNode(context.getVersion().getNamespaceURI(context.isValid()), STEP_ROOT_ELEMENT_NAME);
            writeTapChangerStep(ptcs, context.getWriter());
            context.getWriter().writeDoubleAttribute("alpha", ptcs.getAlpha());
            context.getWriter().writeEndNode();
        }
        context.getWriter().writeEndNodes();

        context.getWriter().writeEndNode();
    }

    protected static void readPhaseTapChanger(String name, PhaseTapChangerAdder adder, Terminal terminal, NetworkDeserializerContext context) {
        readTapChangerAttributes(adder, context);
        AtomicReference<RegulationMode> regulationMode = new AtomicReference<>();
        // Set regulation according to IIDM version
        IidmSerDeUtil.runUntilMaximumVersion(IidmVersion.V_1_13, context, () -> {
            adder.setRegulationMode(null);
            PhaseTapChangerRegulationModeSerDe regulationModeSerDe = context.getReader().readEnumAttribute(ATTR_REGULATION_MODE, PhaseTapChangerRegulationModeSerDe.class);
            if (regulationModeSerDe != null) {
                if (PhaseTapChangerRegulationModeSerDe.FIXED_TAP.equals(regulationModeSerDe)) {
                    adder.setRegulating(false);
                }
                regulationMode.set(PhaseTapChangerRegulationModeSerDe.convertToRegulationMode(regulationModeSerDe));
                adder.setRegulationMode(regulationMode.get());
            }
        });
        IidmSerDeUtil.runFromMinimumVersion(IidmVersion.V_1_14, context, () -> {
            boolean loadTapChangingCapabilities = context.getReader().readBooleanAttribute(ATTR_LOAD_TAP_CHANGING_CAPABILITIES);
            adder.setLoadTapChangingCapabilities(loadTapChangingCapabilities);
            regulationMode.set(context.getReader().readEnumAttribute(ATTR_REGULATION_MODE, RegulationMode.class));
            adder.setRegulationMode(regulationMode.get());
        });

        adder.setRegulationValue(checkRegulationValue(regulationMode.get(), context.getReader().readDoubleAttribute(ATTR_REGULATION_VALUE)));

        boolean[] hasTerminalRef = new boolean[1];
        context.getReader().readChildNodes(elementName -> {
            switch (elementName) {
                case ELEM_TERMINAL_REF -> {
                    hasTerminalRef[0] = true;
                    readTapChangerTerminalRef(adder, terminal, context);
                }
                case STEP_ROOT_ELEMENT_NAME -> {
                    PhaseTapChangerAdder.StepAdder stepAdder = adder.beginStep();
                    readSteps(context, stepAdder);
                    double alpha = context.getReader().readDoubleAttribute("alpha");
                    stepAdder.setAlpha(alpha)
                            .endStep();
                    context.getReader().readEndNode();
                }
                default -> throw new PowsyblException("Unknown element name '" + elementName + "' in '" + name + "'");
            }
        });
        if (!hasTerminalRef[0]) {
            adder.add();
        }
    }

    private static double checkRegulationValue(RegulationMode regulationMode, double regulationValue) {
        if (regulationMode == PhaseTapChanger.RegulationMode.CURRENT_LIMITER && regulationValue < 0) {
            return Math.abs(regulationValue);
        }
        return regulationValue;
    }

    private static void readTapChangerTerminalRef(TapChangerAdder<?, ?, ?, ?, ?, ?> adder, Terminal terminal, NetworkDeserializerContext context) {
        TerminalRefSerDe.readTerminalRef(context, terminal.getVoltageLevel().getNetwork(), tRef -> {
            adder.setRegulationTerminal(tRef);
            adder.add();
        });
    }

    private static boolean readTapChangerAttributes(TapChangerAdder<?, ?, ?, ?, ?, ?> adder, NetworkDeserializerContext context) {
        boolean regulating = context.getReader().readOptionalBooleanAttribute(ATTR_REGULATING).orElse(false);
        int lowTapPosition = context.getReader().readIntAttribute(ATTR_LOW_TAP_POSITION);
        OptionalInt tapPosition = context.getReader().readOptionalIntAttribute(ATTR_TAP_POSITION);
        IidmSerDeUtil.runFromMinimumVersion(IidmVersion.V_1_14, context, () -> {
            OptionalInt solvedTapPosition = context.getReader().readOptionalIntAttribute(ATTR_SOLVED_TAP_POSITION);
            solvedTapPosition.ifPresent(adder::setSolvedTapPosition);
        });
        double targetDeadband = readTargetDeadband(context, regulating);
        adder.setLowTapPosition(lowTapPosition)
                .setTargetDeadband(targetDeadband)
                .setRegulating(regulating);
        tapPosition.ifPresent(adder::setTapPosition);
        return regulating;
    }

    protected static void readPhaseTapChanger(TwoWindingsTransformer twt, NetworkDeserializerContext context) {
        readPhaseTapChanger(PHASE_TAP_CHANGER, twt.newPhaseTapChanger(), twt.getTerminal1(), context);
    }

    protected static void readPhaseTapChanger(int leg, ThreeWindingsTransformer.Leg twl, NetworkDeserializerContext context) {
        readPhaseTapChanger(PHASE_TAP_CHANGER + leg, twl.newPhaseTapChanger(), twl.getTerminal(), context);
    }

    private static void readSteps(NetworkDeserializerContext context, TapChangerStepAdder<?, ?> adder) {
        double r = context.getReader().readDoubleAttribute("r");
        double x = context.getReader().readDoubleAttribute("x");
        double g = context.getReader().readDoubleAttribute("g");
        double b = context.getReader().readDoubleAttribute("b");
        double rho = context.getReader().readDoubleAttribute("rho");
        adder.setR(r).setX(x).setG(g).setB(b).setRho(rho);
    }

    /**
     * Read the apparent power in kVA.
     * @param name the field name to read
     * @param context the XMLStreamReader accessor
     * @param consumer the method will used apparent power value read
     */
    protected static void readRatedS(String name, NetworkDeserializerContext context, DoubleConsumer consumer) {
        IidmSerDeUtil.runFromMinimumVersion(IidmVersion.V_1_2, context, () -> {
            double ratedS = context.getReader().readDoubleAttribute(name);
            consumer.accept(ratedS);
        });
    }

    /**
     * Write the apparent power in kVA.
     * @param name the field name to write
     * @param ratedS the apparent power value to serialize
     * @param context the XMLStreamWriter accessor
     */
    protected static void writeRatedS(String name, double ratedS, NetworkSerializerContext context) {
        IidmSerDeUtil.runFromMinimumVersion(IidmVersion.V_1_2, context, () -> context.getWriter().writeDoubleAttribute(name, ratedS));
    }
}