ConditionDslLoaderTest.java

/**
 * Copyright (c) 2017, 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.action.ial.dsl;

import com.powsybl.action.ial.dsl.ast.*;
import com.powsybl.commons.PowsyblException;
import com.powsybl.contingency.Contingency;
import com.powsybl.dsl.ast.ExpressionNode;
import com.powsybl.iidm.network.Line;
import com.powsybl.iidm.network.Network;
import com.powsybl.iidm.network.Terminal;
import com.powsybl.iidm.network.test.EurostagTutorialExample1Factory;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.util.Arrays;
import java.util.List;

import static org.junit.jupiter.api.Assertions.*;

/**
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 */
class ConditionDslLoaderTest {

    private Network network;
    private Line line1;
    private Line line2;

    @BeforeEach
    void setUp() throws Exception {
        network = EurostagTutorialExample1Factory.create();
        network.getVoltageLevel("VLHV1").getBusBreakerView().getBus("NHV1").setV(380).setAngle(0);
        network.getVoltageLevel("VLHV2").getBusBreakerView().getBus("NHV2").setV(380).setAngle(0);
        line1 = network.getLine("NHV1_NHV2_1");
        line2 = network.getLine("NHV1_NHV2_2");

    }

    private void loadAndAssert(String expected, String script) throws IOException {
        ExpressionNode node = (ExpressionNode) new ConditionDslLoader(script).load(network);
        assertNotNull(node);
        assertEquals(expected, ActionExpressionPrinter.toString(node));
    }

    private void evalAndAssert(Object expected, String script) throws IOException {
        ExpressionNode node = (ExpressionNode) new ConditionDslLoader(script).load(network);
        assertNotNull(node);
        assertEquals(expected, ActionExpressionEvaluator.evaluate(node, new EvaluationContext() {
            @Override
            public Network getNetwork() {
                return network;
            }

            @Override
            public Contingency getContingency() {
                return null;
            }

            @Override
            public boolean isActionTaken(String actionId) {
                return actionId.equals("action");
            }
        }));
    }

    @Test
    void testCondition() throws IOException {
        loadAndAssert("line('NHV1_NHV2_1')", "line('NHV1_NHV2_1')");
        loadAndAssert("line('NHV1_NHV2_1').terminal1.p", "line('NHV1_NHV2_1').terminal1.p");
        loadAndAssert("transformer('NGEN_NHV1')", "transformer('NGEN_NHV1')");
        loadAndAssert("branch('NGEN_NHV1')", "branch('NGEN_NHV1')");
        loadAndAssert("load('LOAD')", "load('LOAD')");

        loadAndAssert("1", "1"); // integer
        loadAndAssert("1.0", "1f"); // float
        loadAndAssert("1.0", "1d"); // double
        loadAndAssert("1.0", "1.0"); // big decimal
        for (String op : Arrays.asList("+", "-", "*", "/", "==", "<", ">", ">=", "<=", "!=")) {
            // integer
            loadAndAssert("(line('NHV1_NHV2_1').terminal1.p " + op + " 1)", "line('NHV1_NHV2_1').terminal1.p " + op + " 1");
            loadAndAssert("(1 " + op + " line('NHV1_NHV2_1').terminal1.p)", "1 " + op + " line('NHV1_NHV2_1').terminal1.p");

            // float
            loadAndAssert("(line('NHV1_NHV2_1').terminal1.p " + op + " 1.0)", "line('NHV1_NHV2_1').terminal1.p " + op + " 1f");
            loadAndAssert("(1.0 " + op + " line('NHV1_NHV2_1').terminal1.p)", "1f " + op + " line('NHV1_NHV2_1').terminal1.p");

            // double
            loadAndAssert("(line('NHV1_NHV2_1').terminal1.p " + op + " 1.0)", "line('NHV1_NHV2_1').terminal1.p " + op + " 1d");
            loadAndAssert("(1.0 " + op + " line('NHV1_NHV2_1').terminal1.p)", "1d " + op + " line('NHV1_NHV2_1').terminal1.p");

            // big decimal
            loadAndAssert("(line('NHV1_NHV2_1').terminal1.p " + op + " 1.0)", "line('NHV1_NHV2_1').terminal1.p " + op + " 1.0");
            loadAndAssert("(1.0 " + op + " line('NHV1_NHV2_1').terminal1.p)", "1.0 " + op + " line('NHV1_NHV2_1').terminal1.p");
        }

        loadAndAssert("true", "true");
        loadAndAssert("false", "true && false");
        for (String op : Arrays.asList("&&", "||")) {
            loadAndAssert("(line('NHV1_NHV2_1').overloaded " + op + " true)", "line('NHV1_NHV2_1').overloaded " + op + " true");
            loadAndAssert("(true " + op + " line('NHV1_NHV2_1').overloaded)", "true " + op + " line('NHV1_NHV2_1').overloaded");
        }
        loadAndAssert("false", "!true");
        loadAndAssert("!(line('NHV1_NHV2_1').overloaded)", "!line('NHV1_NHV2_1').overloaded");

        loadAndAssert("actionTaken('action1')", "actionTaken('action1')");
        evalAndAssert(true, "actionTaken('action')");
        loadAndAssert("contingencyOccurred('contingency1')", "contingencyOccurred('contingency1')");
        loadAndAssert("contingencyOccurred()", "contingencyOccurred()");
        evalAndAssert(false, "contingencyOccurred()");
        loadAndAssert("mostLoaded(['NHV1_NHV2_1', 'NHV1_NHV2_2'])", "mostLoaded(['NHV1_NHV2_1','NHV1_NHV2_2'])");
        loadAndAssert("isOverloaded(['NHV1_NHV2_1', 'NHV1_NHV2_2'])", "isOverloaded(['NHV1_NHV2_1','NHV1_NHV2_2'])");
        loadAndAssert("allOverloaded(['NHV1_NHV2_1', 'NHV1_NHV2_2'])", "allOverloaded(['NHV1_NHV2_1','NHV1_NHV2_2'])");
    }

    @Test
    void testExpressionEvaluator() throws IOException {

        Terminal terminal = network.getLoad("LOAD").getTerminal();
        double old = terminal.getP();
        terminal.setP(400);
        evalAndAssert(100.0, "(load('LOAD').p0 - load('LOAD').terminal.p) / 2");
        evalAndAssert(true, "(load('LOAD').p0 - load('LOAD').terminal.p) > 2");
        terminal.setP(old);

        // visitComparisonOperator
        evalAndAssert(true, "load('LOAD').p0 == 600.0");
        evalAndAssert(true, "load('LOAD').p0 != 300.0");
        evalAndAssert(true, "load('LOAD').p0 > 100.0");
        evalAndAssert(true, "load('LOAD').p0 < 1000.0");
        evalAndAssert(true, "load('LOAD').p0 >= 100.0");
        evalAndAssert(true, "load('LOAD').p0 <= 1000.0");
        evalAndAssert(599.0, "load('LOAD').p0 - 1");
        evalAndAssert(1200.0, "load('LOAD').p0 * 2");
        evalAndAssert(300.0, "load('LOAD').p0 / 2");

        // visitNotOperator
        evalAndAssert(false, "! load('LOAD').terminal.connected");

        // visitLogicalOperator
        evalAndAssert(true, "load('LOAD').terminal.connected || false");
        evalAndAssert(false, "load('LOAD').terminal.connected && false");

        // visitArithmeticOperator
        evalAndAssert(601.0, "load('LOAD').p0 + 1");
        evalAndAssert(599.0, "load('LOAD').p0 - 1");
        evalAndAssert(1200.0, "load('LOAD').p0 * 2");
        evalAndAssert(300.0, "load('LOAD').p0 / 2");
    }

    @Test
    void testIsOverloadedNode() throws IOException {
        line1.getTerminal1().setP(100.0).setQ(50.0);
        evalAndAssert(false, "isOverloaded(['NHV1_NHV2_1','NHV1_NHV2_2'])");

        line1.newCurrentLimits1().setPermanentLimit(0.00001).add();
        assertTrue(line1.getCurrentLimits1().isPresent());
        evalAndAssert(true, "isOverloaded(['NHV1_NHV2_1','NHV1_NHV2_2'])");

        line1.getTerminal1().setP(600.0).setQ(300.0); // i = 1019.2061
        double current = line1.getTerminal1().getI();
        line1.newCurrentLimits1().setPermanentLimit(current - 100).add();
        evalAndAssert(true, "isOverloaded(['NHV1_NHV2_1','NHV1_NHV2_2'])");
        evalAndAssert(true, "isOverloaded(['NHV1_NHV2_1','NHV1_NHV2_2'], 0.05)");
        line1.newCurrentLimits1().setPermanentLimit(current).add();
        evalAndAssert(true, "isOverloaded(['NHV1_NHV2_1','NHV1_NHV2_2'])"); // permanent = real current
        line1.newCurrentLimits1().setPermanentLimit(current * 2).add();
        evalAndAssert(false, "isOverloaded(['NHV1_NHV2_1','NHV1_NHV2_2'], 0.9)");
        evalAndAssert(true, "isOverloaded(['NHV1_NHV2_1','NHV1_NHV2_2'], 0.1)");

        addCurrentLimitsOnLine1();
        line1.getTerminal1().setP(400.0).setQ(150.0); // i = 649.06
        evalAndAssert(true, "isOverloaded(['NHV1_NHV2_1','NHV1_NHV2_2'], 1)");

        try {
            evalAndAssert(false, "isOverloaded(['NHV1_NHV2_1','NHV1_NHV2_2', 'UNKNOWN'])");
            fail();
        } catch (PowsyblException e) {
            assertEquals("Branch 'UNKNOWN' not found", e.getMessage());
        }
    }

    @Test
    void testAllOverloadedNode() throws IOException {
        // Both lines are not overloaded
        line1.getTerminal1().setP(100.0f).setQ(50.0f);
        line2.getTerminal1().setP(100.0f).setQ(50.0f);
        evalAndAssert(false, "allOverloaded(['NHV1_NHV2_1','NHV1_NHV2_2'])");

        // Only line1 is overloaded
        line1.getTerminal1().setP(600.0f).setQ(300.0f); // i = 1019.2061
        double current1 = line1.getTerminal1().getI();
        line1.newCurrentLimits1().setPermanentLimit(current1 - 100).add();
        evalAndAssert(false, "allOverloaded(['NHV1_NHV2_1','NHV1_NHV2_2'])");

        // Both lines are overloaded
        line2.getTerminal1().setP(600.0f).setQ(300.0f); // i = 1019.2061
        double current2 = line2.getTerminal1().getI();
        line2.newCurrentLimits1().setPermanentLimit(current2 - 100).add();
        evalAndAssert(true, "allOverloaded(['NHV1_NHV2_1','NHV1_NHV2_2'])");

        // Only line2 is overloaded
        line1.getTerminal1().setP(400.0f).setQ(150.0f); // i = 649.06
        evalAndAssert(false, "allOverloaded(['NHV1_NHV2_1','NHV1_NHV2_2'])");

        // Unknown branch
        try {
            evalAndAssert(false, "allOverloaded(['NHV1_NHV2_1','NHV1_NHV2_2', 'UNKNOWN'])");
            fail();
        } catch (PowsyblException e) {
            assertEquals("Branch 'UNKNOWN' not found", e.getMessage());
        }
    }

    private void addCurrentLimitsOnLine1() {
        line1.newCurrentLimits1()
                .setPermanentLimit(400)
                .beginTemporaryLimit()
                    .setName("20")
                    .setAcceptableDuration(20 * 60)
                    .setValue(600)
                .endTemporaryLimit()
                .beginTemporaryLimit()
                    .setName("10")
                    .setAcceptableDuration(10 * 60)
                    .setValue(700)
                    .endTemporaryLimit()
                .beginTemporaryLimit()
                    .setName("5")
                    .setAcceptableDuration(5 * 60)
                    .setValue(800)
                .endTemporaryLimit()
                .add();
    }

    @Test
    void testNetworkAccess() throws IOException {
        // add temporary limits
        line1.newCurrentLimits1()
                .setPermanentLimit(400)
                .beginTemporaryLimit()
                .setName("20")
                .setAcceptableDuration(20 * 60)
                .setValue(800)
                .endTemporaryLimit()
                .add();

        // IIDM method call
        evalAndAssert(false, "line('NHV1_NHV2_1').overloaded");
        evalAndAssert(800.0, "line('NHV1_NHV2_1').currentLimits1.getTemporaryLimitValue(1200)");

        evalAndAssert(800.0, "branch('NHV1_NHV2_1').currentLimits1.getTemporaryLimitValue(1200)");
        evalAndAssert(0.0, "branch('NHV2_NLOAD').g");
    }

    @Test
    void testLoadingRank() throws IOException {
        // add temporary limits
        line1.newCurrentLimits1()
                .setPermanentLimit(400)
                .beginTemporaryLimit()
                    .setName("20")
                    .setAcceptableDuration(20 * 60)
                    .setValue(800)
                .endTemporaryLimit()
                .add();
        line2.newCurrentLimits1()
                .setPermanentLimit(400)
                .beginTemporaryLimit()
                    .setName("20")
                    .setAcceptableDuration(20 * 60)
                    .setValue(800)
                .endTemporaryLimit()
                .add();

        // both are 20' overloaded, but line2 is more overloaded than line1
        line1.getTerminal1().setP(300).setQ(100); // line1.i1 = 480
        line2.getTerminal1().setP(400).setQ(100); // line2.i1 = 626
        evalAndAssert(2, "loadingRank('NHV1_NHV2_1', ['NHV1_NHV2_1', 'NHV1_NHV2_2'])");
        evalAndAssert(1, "loadingRank('NHV1_NHV2_2', ['NHV1_NHV2_1', 'NHV1_NHV2_2'])");

        // both are 20' overloaded, but line1 is more overloaded than line2
        line1.getTerminal1().setP(400); // line1.i1 = 626
        line2.getTerminal1().setP(300); // line2.i1 = 480
        evalAndAssert(1, "loadingRank('NHV1_NHV2_1', ['NHV1_NHV2_1', 'NHV1_NHV2_2'])");
        evalAndAssert(2, "loadingRank('NHV1_NHV2_2', ['NHV1_NHV2_1', 'NHV1_NHV2_2'])");

        // none are overloaded, but line2 % of permanent limit is greater than line1 one
        line1.getTerminal1().setP(100); // line1.i1 = 214
        line2.getTerminal1().setP(150); // line2.i1 = 273
        evalAndAssert(2, "loadingRank('NHV1_NHV2_1', ['NHV1_NHV2_1', 'NHV1_NHV2_2'])");
        evalAndAssert(1, "loadingRank('NHV1_NHV2_2', ['NHV1_NHV2_1', 'NHV1_NHV2_2'])");

        // none are overloaded, but line1 % of permanent limit is greater than line2 one
        line1.getTerminal1().setP(150); // line1.i1 = 273
        line2.getTerminal1().setP(100); // line2.i1 = 214
        evalAndAssert(1, "loadingRank('NHV1_NHV2_1', ['NHV1_NHV2_1', 'NHV1_NHV2_2'])");
        evalAndAssert(2, "loadingRank('NHV1_NHV2_2', ['NHV1_NHV2_1', 'NHV1_NHV2_2'])");
    }

    @Test
    void testLoadingRankWithDifferentAcceptableDuration() throws IOException {
        // add temporary limits
        line1.newCurrentLimits1()
                .setPermanentLimit(400)
                .beginTemporaryLimit()
                    .setName("20")
                    .setAcceptableDuration(20 * 60)
                    .setValue(800)
                .endTemporaryLimit()
                .add();
        line2.newCurrentLimits1()
                .setPermanentLimit(400)
                .beginTemporaryLimit()
                    .setName("20")
                    .setAcceptableDuration(20 * 60)
                    .setValue(600)
                .endTemporaryLimit()
                .beginTemporaryLimit()
                    .setName("5")
                    .setAcceptableDuration(5 * 60)
                    .setValue(800)
                .endTemporaryLimit()
                .add();

        // line1 current is greater than line2 one but acceptable duration of line2 is less than line1
        line1.getTerminal1().setP(410).setQ(100); // line1.i1 = 641
        line2.getTerminal1().setP(400).setQ(100); // line2.i1 = 626
        evalAndAssert(2, "loadingRank('NHV1_NHV2_1', ['NHV1_NHV2_1', 'NHV1_NHV2_2'])");
        evalAndAssert(1, "loadingRank('NHV1_NHV2_2', ['NHV1_NHV2_1', 'NHV1_NHV2_2'])");
    }

    @Test
    void testLoadingRankWithUndefinedCurrentLimitsForLine2() throws IOException {
        // add temporary limits
        line1.newCurrentLimits1()
                .setPermanentLimit(400)
                .beginTemporaryLimit()
                    .setName("20")
                    .setAcceptableDuration(20 * 60)
                    .setValue(800)
                .endTemporaryLimit()
                .add();

        // line2 current is greater than line1 one but line2 has not temporary limits
        line1.getTerminal1().setP(400).setQ(100); // line1.i1 = 626
        line2.getTerminal1().setP(500).setQ(100); // line2.i1 = 774
        evalAndAssert(1, "loadingRank('NHV1_NHV2_1', ['NHV1_NHV2_1', 'NHV1_NHV2_2'])");
        evalAndAssert(2, "loadingRank('NHV1_NHV2_2', ['NHV1_NHV2_1', 'NHV1_NHV2_2'])");
    }

    @Test
    void testLoadingRankWithCurrentLimitsAtBothSides() throws IOException {
        // add temporary limits
        line1.newCurrentLimits1()
                .setPermanentLimit(400)
                .beginTemporaryLimit()
                    .setName("20")
                    .setAcceptableDuration(20 * 60)
                    .setValue(800)
                .endTemporaryLimit()
                .add();
        line1.newCurrentLimits2()
                .setPermanentLimit(400)
                .beginTemporaryLimit()
                    .setName("20")
                    .setAcceptableDuration(20 * 60)
                    .setValue(600)
                .endTemporaryLimit()
                .beginTemporaryLimit()
                    .setName("1")
                    .setAcceptableDuration(1 * 60)
                    .setValue(800)
                .endTemporaryLimit()
                .add();
        line2.newCurrentLimits1()
                .setPermanentLimit(400)
                .beginTemporaryLimit()
                    .setName("20")
                    .setAcceptableDuration(20 * 60)
                    .setValue(800)
                .endTemporaryLimit()
                .add();

        // line2 current is greater than line2 one but acceptable duration of side 2 of line1 is less than line2 one
        line1.getTerminal1().setP(400).setQ(100); // line1.i1 = 626
        line1.getTerminal2().setP(400).setQ(100); // line1.i2 = 626
        line2.getTerminal1().setP(410).setQ(100); // line2.i1 = 641
        line2.getTerminal2().setP(410).setQ(100); // line2.i2 = 641
        evalAndAssert(1, "loadingRank('NHV1_NHV2_1', ['NHV1_NHV2_1', 'NHV1_NHV2_2'])");
        evalAndAssert(2, "loadingRank('NHV1_NHV2_2', ['NHV1_NHV2_1', 'NHV1_NHV2_2'])");

        // handle unknown branch
        try {
            evalAndAssert(null, "loadingRank('NHV1_NHV2_2', ['NHV1_NHV2_1', 'NHV1_NHV2_2', 'UNKNOWN'])");
            fail();
        } catch (PowsyblException e) {
            assertEquals("Branch 'UNKNOWN' not found", e.getMessage());
        }
    }

    @Test
    void testMostLoaded() throws IOException {
        // add temporary limits
        line1.newCurrentLimits1()
                .setPermanentLimit(400)
                .beginTemporaryLimit()
                .setName("20")
                .setAcceptableDuration(20 * 60)
                .setValue(800)
                .endTemporaryLimit()
                .add();
        line2.newCurrentLimits1()
                .setPermanentLimit(400)
                .beginTemporaryLimit()
                .setName("20")
                .setAcceptableDuration(20 * 60)
                .setValue(800)
                .endTemporaryLimit()
                .add();

        // line2 is more overloaded than line1
        line1.getTerminal1().setP(300).setQ(100); // line1.i1 = 480
        line2.getTerminal1().setP(400).setQ(100); // line2.i1 = 626
        evalAndAssert("NHV1_NHV2_2", "mostLoaded(['NHV1_NHV2_1', 'NHV1_NHV2_2'])");

        // line1 is more overloaded than line2
        line1.getTerminal1().setP(400).setQ(100); // line1.i1 = 626
        line2.getTerminal1().setP(300).setQ(100); // line2.i1 = 480
        evalAndAssert("NHV1_NHV2_1", "mostLoaded(['NHV1_NHV2_1', 'NHV1_NHV2_2'])");

        // combine with loadingRank
        evalAndAssert(1, "loadingRank(mostLoaded(['NHV1_NHV2_1', 'NHV1_NHV2_2']), ['NHV1_NHV2_1', 'NHV1_NHV2_2'])");
        evalAndAssert(1, "loadingRank('NHV1_NHV2_1', [mostLoaded(['NHV1_NHV2_1', 'NHV1_NHV2_2']), 'NHV1_NHV2_2'])");

        // handle unknown branch
        try {
            evalAndAssert(null, "mostLoaded(['NHV1_NHV2_1', 'UNKNOWN'])");
            fail();
        } catch (PowsyblException e) {
            assertEquals("Branch 'UNKNOWN' not found", e.getMessage());
        }
    }

    @Test
    void testExpressionVariableLister() {
        String script = "line('NHV1_NHV2_1').terminal1.p > 0 || line('NHV1_NHV2_1').getTerminal2().getP() > 0 && actionTaken('action1')";
        ExpressionNode node = (ExpressionNode) new ConditionDslLoader(script).load(network);
        assertNotNull(node);
        List<NetworkNode> nodes = ExpressionVariableLister.list(node);
        assertEquals(2, nodes.size());
    }

    @Test
    void testActionTakenLister() {
        String script = "actionTaken('action1') && line('NHV1_NHV2_1').terminal1.p > 0 && actionTaken('action2')";
        ExpressionNode node = (ExpressionNode) new ConditionDslLoader(script).load(network);
        assertNotNull(node);
        List<String> actions = ExpressionActionTakenLister.list(node);
        assertEquals(2, actions.size());
        assertTrue(actions.contains("action1"));
        assertTrue(actions.contains("action2"));
    }
}