MetrixInputAnalysis.java

/*
 * Copyright (c) 2021, 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.metrix.integration.metrix;

import com.powsybl.contingency.ContingenciesProvider;
import com.powsybl.contingency.Contingency;
import com.powsybl.contingency.ContingencyElement;
import com.powsybl.contingency.ContingencyElementType;
import com.powsybl.iidm.network.Branch;
import com.powsybl.iidm.network.Identifiable;
import com.powsybl.iidm.network.IdentifiableType;
import com.powsybl.iidm.network.Network;
import com.powsybl.iidm.network.Switch;
import com.powsybl.metrix.integration.MetrixDslData;
import com.powsybl.metrix.integration.exceptions.ContingenciesScriptLoadingException;
import com.powsybl.metrix.integration.remedials.Remedial;
import com.powsybl.metrix.integration.remedials.RemedialReader;
import com.powsybl.metrix.mapping.DataTableStore;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.ResourceBundle;
import java.util.Set;
import java.util.stream.Collectors;

import static com.powsybl.metrix.integration.remedials.RemedialReader.rTrim;

/**
 * @author Marianne Funfrock {@literal <marianne.funfrock at rte-france.com>}
 */
public class MetrixInputAnalysis {

    enum LogType {
        ERROR,
        WARNING,
        INFO
    }

    private static final char SEPARATOR = ';';
    private static final String SECTION_SEPARATOR = " - ";
    private static final String COMMENT_SYMBOL = "/*";
    private static final String COMMENT_LINE_SYMBOL = "//";

    private static final ResourceBundle RESOURCE_BUNDLE = ResourceBundle.getBundle("lang.MetrixAnalysis");
    private static final String CONTINGENCIES_SECTION = RESOURCE_BUNDLE.getString("contingenciesSection");
    private static final String REMEDIALS_SECTION = RESOURCE_BUNDLE.getString("remedialsSection");

    private static final String HVDC_LINE_TYPE = "hvdcLine";
    private static final String GENERATOR_TYPE = "generator";
    private static final String LOAD_TYPE = "load";
    private static final String PHASE_TAP_CHANGER_TYPE = "phaseTapChanger";

    private final Reader remedialActionsReader;
    private final ContingenciesProvider contingenciesProvider;
    private final Network network;
    private final MetrixDslData metrixDslData;
    private final DataTableStore dataTableStore;
    private final BufferedWriter writer;
    private final BufferedWriter scriptLogBufferedWriter;

    public MetrixInputAnalysis(Reader remedialActionsReader, ContingenciesProvider contingenciesProvider, Network network,
                               MetrixDslData metrixDslData, DataTableStore dataTableStore, BufferedWriter writer) {
        this(remedialActionsReader, contingenciesProvider, network, metrixDslData, dataTableStore, writer, null);
    }

    public MetrixInputAnalysis(Reader remedialActionsReader, ContingenciesProvider contingenciesProvider, Network network,
                               MetrixDslData metrixDslData, DataTableStore dataTableStore, BufferedWriter writer, BufferedWriter scriptLogBufferedWriter) {
        Objects.requireNonNull(contingenciesProvider);
        Objects.requireNonNull(network);
        this.remedialActionsReader = remedialActionsReader;
        this.contingenciesProvider = contingenciesProvider;
        this.network = network;
        this.metrixDslData = metrixDslData;
        this.dataTableStore = dataTableStore;
        this.writer = writer;
        this.scriptLogBufferedWriter = scriptLogBufferedWriter;
    }

    public MetrixInputAnalysisResult runAnalysis() throws IOException {
        List<Contingency> contingencies = loadContingencies();
        List<Remedial> remedials = loadRemedials();
        Set<String> contingencyIds = contingencies.stream().map(Contingency::getId).collect(Collectors.toSet());
        runMetrixDslDataAnalysis(contingencyIds);
        runRemedialAnalysis(remedials, contingencyIds);
        return new MetrixInputAnalysisResult(remedials, contingencies);
    }

    private void runMetrixDslDataAnalysis(Set<String> contingencyIds) {
        if (metrixDslData == null) {
            return;
        }
        checkMetrixDslContingencies(contingencyIds, HVDC_LINE_TYPE, metrixDslData.getHvdcContingenciesMap());
        checkMetrixDslContingencies(contingencyIds, GENERATOR_TYPE, metrixDslData.getGeneratorContingenciesMap());
        checkMetrixDslContingencies(contingencyIds, LOAD_TYPE, metrixDslData.getLoadContingenciesMap());
        checkMetrixDslContingencies(contingencyIds, PHASE_TAP_CHANGER_TYPE, metrixDslData.getPtcContingenciesMap());
    }

    private void runRemedialAnalysis(List<Remedial> remedials, Set<String> contingencyIds) {
        remedials.forEach(remedial -> checkRemedial(remedial, contingencyIds));
    }

    private void writeLog(String type, String section, String message, BufferedWriter writer) {
        if (writer == null) {
            return;
        }
        try {
            writer.write(type + SEPARATOR + section + SEPARATOR + message);
            writer.newLine();
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private void writeLog(String type, String section, String message) {
        writeLog(type, section, message, writer);
    }

    private void writeContingencyLog(String contingencyId, String elementId, String messageReason) {
        String message = String.format(RESOURCE_BUNDLE.getString("invalidContingency"), contingencyId, elementId);
        writeLog(String.valueOf(LogType.WARNING), CONTINGENCIES_SECTION, message + " " + messageReason);
    }

    private void writeDslDataContingencyLog(String equipmentType, String equipmentId, String contingencyId) {
        String message = String.format(RESOURCE_BUNDLE.getString("invalidMetrixDslDataContingency"), equipmentType, contingencyId);
        writeLog(String.valueOf(LogType.WARNING), CONTINGENCIES_SECTION + SECTION_SEPARATOR + equipmentId, message);
    }

    private void writeRemedialLog(int line, String messageReason) {
        String message = String.format(RESOURCE_BUNDLE.getString("invalidRemedial"), line);
        writeLog(String.valueOf(LogType.WARNING), REMEDIALS_SECTION, message + " " + messageReason);
    }

    private void writeRemedialFileLog(int line, String messageReason) {
        String message = String.format(RESOURCE_BUNDLE.getString("invalidRemedialFile"), line);
        writeLog(String.valueOf(LogType.ERROR), REMEDIALS_SECTION, message + " " + messageReason);
    }

    /**
     * load contingencies
     * @return list of contingencies
     */
    private List<Contingency> loadContingencies() {
        List<Contingency> allContingencies;
        try {
            allContingencies = contingenciesProvider.getContingencies(network, getContextObjects());
        } catch (RuntimeException e) {
            throw new ContingenciesScriptLoadingException(e);
        }
        List<Contingency> contingencies = new ArrayList<>();
        for (Contingency cty : allContingencies) {
            if (isValidContingency(cty)) {
                contingencies.add(cty);
            }
        }
        return contingencies;
    }

    private Map<Class<?>, Object> getContextObjects() {
        Map<Class<?>, Object> contextObjects = new HashMap<>();
        if (dataTableStore != null) {
            contextObjects.put(DataTableStore.class, dataTableStore);
        }
        if (scriptLogBufferedWriter != null) {
            contextObjects.put(Writer.class, scriptLogBufferedWriter);
        }
        return contextObjects;
    }

    private static boolean isValidContingencyType(ContingencyElementType elementType) {
        return elementType == ContingencyElementType.LINE ||
            elementType == ContingencyElementType.TWO_WINDINGS_TRANSFORMER ||
            elementType == ContingencyElementType.BRANCH ||
            elementType == ContingencyElementType.GENERATOR ||
            elementType == ContingencyElementType.HVDC_LINE;
    }

    private static boolean isValidContingencyTypeForIdentifiable(IdentifiableType identifiableType, ContingencyElementType elementType) {
        if (elementType == ContingencyElementType.LINE && identifiableType == IdentifiableType.LINE) {
            return true;
        }
        if (elementType == ContingencyElementType.TWO_WINDINGS_TRANSFORMER && identifiableType == IdentifiableType.TWO_WINDINGS_TRANSFORMER) {
            return true;
        }
        if (elementType == ContingencyElementType.BRANCH && (identifiableType == IdentifiableType.LINE || identifiableType == IdentifiableType.TWO_WINDINGS_TRANSFORMER)) {
            return true;
        }
        if (elementType == ContingencyElementType.GENERATOR && identifiableType == IdentifiableType.GENERATOR) {
            return true;
        }
        return elementType == ContingencyElementType.HVDC_LINE && identifiableType == IdentifiableType.HVDC_LINE;
    }

    public static boolean isValidContingencyElement(IdentifiableType identifiableType, ContingencyElementType elementType) {
        if (!isValidContingencyType(elementType)) {
            return false;
        }
        return isValidContingencyTypeForIdentifiable(identifiableType, elementType);
    }

    private boolean isValidContingencyElement(Identifiable<?> identifiable, String contingencyId, ContingencyElement element) {
        boolean isValid = true;
        boolean isExistingIdentifiable = identifiable != null;
        boolean isValidContingencyType = isValidContingencyType(element.getType());
        if (!isExistingIdentifiable) {
            String message = RESOURCE_BUNDLE.getString("invalidContingencyNetwork");
            writeContingencyLog(contingencyId, element.getId(), message);
            isValid = false;
        }
        if (!isValidContingencyType) {
            String message = String.format(RESOURCE_BUNDLE.getString("invalidContingencyType"), element.getType());
            writeContingencyLog(contingencyId, element.getId(), message);
            isValid = false;
        }
        if (isValid && !isValidContingencyTypeForIdentifiable(identifiable.getType(), element.getType())) {
            String message = String.format(RESOURCE_BUNDLE.getString("invalidContingencyNetworkType"), element.getType(), identifiable.getType());
            writeContingencyLog(contingencyId, element.getId(), message);
            isValid = false;
        }
        return isValid;
    }

    private boolean isValidContingency(Contingency contingency) {
        boolean isValid = true;
        for (ContingencyElement element : contingency.getElements()) {
            Identifiable<?> identifiable = network.getIdentifiable(element.getId());
            isValid = isValid && isValidContingencyElement(identifiable, contingency.getId(), element);
        }
        return isValid;
    }

    /**
     * load remedials
     * @return list of remedials
     */
    private List<Remedial> loadRemedials() {
        if (remedialActionsReader == null) {
            return Collections.emptyList();
        }
        List<Remedial> remedials;
        try (BufferedReader bufferedReader = new BufferedReader(remedialActionsReader)) {
            String fileContent = bufferedReader.lines().collect(Collectors.joining(System.lineSeparator()));
            checkFile(fileContent);
            remedials = RemedialReader.parseFile(fileContent);
            remedials.forEach(this::isValidRemedial);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
        return remedials;
    }

    private boolean checkNumber(String number, int lineId, String message) {
        try {
            if (Integer.parseInt(number) < 0) {
                writeRemedialFileLog(lineId, message);
                return false;
            }
        } catch (NumberFormatException e) {
            writeRemedialFileLog(lineId, message);
            return false;
        }
        return true;
    }

    private void checkFile(String fileContent) {
        checkFile(new StringReader(fileContent));
    }

    private void checkFile(Reader reader) {
        try (BufferedReader bufferedReader = new BufferedReader(reader)) {
            int lineId = 1;
            String headerLine = bufferedReader.readLine();
            if (!checkHeader(headerLine, lineId)) {
                return;
            }
            int nbRemedial = Integer.parseInt(headerLine.split(RemedialReader.COLUMN_SEPARATOR)[1]);

            String line;
            while ((line = bufferedReader.readLine()) != null) {
                lineId++;
                checkLine(line, lineId);
                if (lineId - 1 > nbRemedial) {
                    String message = String.format(RESOURCE_BUNDLE.getString("invalidRemedialNbRemedialLessLines"), nbRemedial);
                    writeRemedialLog(lineId, message);
                }
            }

            if (lineId - 1 < nbRemedial) {
                String message = String.format(RESOURCE_BUNDLE.getString("invalidRemedialFileNbRemedialMoreLines"), nbRemedial, lineId - 1);
                writeRemedialFileLog(lineId, message);
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    /**
     * check remedial header NB;<nb remedials>;
     *
     * @param line the first line in remedial file
     * @return false if header is malformed
     */
    private boolean checkHeader(String line, int lineId) {
        if (line == null) {
            return false;
        }

        if (!checkBeginAndEnd(line, lineId)) {
            return false;
        }

        String message = String.format(RESOURCE_BUNDLE.getString("invalidRemedialFileHeader"), lineId);
        String[] columns = line.split(RemedialReader.COLUMN_SEPARATOR);
        if (columns.length != 2 || !RemedialReader.NUMBER_OF_LINES_SYMBOL.equals(columns[0])) {
            writeRemedialFileLog(lineId, message);
            return false;
        }

        return checkNumber(columns[1], lineId, message);
    }

    /**
     * check remedial line <contingency|<constraints>;<nb actions>;<actions>;
     * constraints are optional
     *
     * @param line a line describing a remedial in remedial file
     * @param lineId line number in remedial file
     */
    private void checkLine(String line, int lineId) throws IOException {
        if (!checkBeginAndEnd(line, lineId)) {
            return;
        }

        String[] actions = line.split(RemedialReader.COLUMN_SEPARATOR);
        if (actions.length >= RemedialReader.FIRST_ACTION_INDEX) {
            String message = String.format(RESOURCE_BUNDLE.getString("invalidRemedialFileAction"), lineId);
            if (!checkNumber(actions[1], lineId, message)) {
                return;
            }
        }

        boolean isNbActionsEqualToZero = actions.length >= RemedialReader.FIRST_ACTION_INDEX && Integer.parseInt(actions[1]) == 0;
        if (actions.length <= RemedialReader.FIRST_ACTION_INDEX && !isNbActionsEqualToZero) {
            String message = String.format(RESOURCE_BUNDLE.getString("invalidRemedialFileLine"), lineId);
            writeRemedialFileLog(lineId, message);
            return;
        }

        for (String action : actions) {
            rTrim(action);
            if (action == null || action.isEmpty()) {
                String message = String.format(RESOURCE_BUNDLE.getString("invalidRemedialFileEmptyElement"), lineId);
                writeRemedialFileLog(lineId, message);
            }
        }

        List<String> constraints = RemedialReader.extractConstraintFromContingencyAndConstraint(actions[0]);
        for (String constraint : constraints) {
            rTrim(constraint);
            if (constraint == null || constraint.isEmpty()) {
                String message = String.format(RESOURCE_BUNDLE.getString("invalidRemedialFileEmptyElement"), lineId);
                writeRemedialFileLog(lineId, message);
            }
        }
    }

    /**
     * check begin and end of a remedial file line
     *
     * @param line a line of remedial file
     * @param lineId line number in remedial file
     * @return true if line is not a comment and ends with the correct separator
     */
    private boolean checkBeginAndEnd(String line, int lineId) {
        rTrim(line);
        if (line.startsWith(COMMENT_SYMBOL) || line.startsWith(COMMENT_LINE_SYMBOL)) {
            String message = String.format(RESOURCE_BUNDLE.getString("invalidRemedialFileComment"), lineId);
            writeRemedialFileLog(lineId, message);
            return false;
        }
        if (!line.endsWith(RemedialReader.COLUMN_SEPARATOR)) {
            String message = String.format(RESOURCE_BUNDLE.getString("invalidRemedialFileEndLine"), lineId);
            writeRemedialFileLog(lineId, message);
            return false;
        }
        return true;
    }

    private void checkRemedial(Remedial remedial, Set<String> contingencyIds) {
        if (!remedial.getContingency().isEmpty() && !contingencyIds.contains(remedial.getContingency())) {
            String message = String.format(RESOURCE_BUNDLE.getString("invalidMetrixRemedialContingency"), remedial.getContingency());
            writeRemedialLog(remedial.getLineFile(), message);
        }
    }

    /**
     * check an action of a remedial line : action is equipment of the network and is of Branch or Switch type
     *
     * @param line line number
     * @param network network
     * @param action to check
     */
    private void isValidAction(int line, Network network, String action) {
        if (action.isEmpty()) {
            return;
        }
        Identifiable<?> identifiable = network.getIdentifiable(action);
        if (identifiable == null) {
            String message = String.format(RESOURCE_BUNDLE.getString("invalidRemedialNetwork"), action);
            writeRemedialLog(line, message);
            return;
        }
        if (!(identifiable instanceof Branch) && !(identifiable instanceof Switch)) {
            String message = String.format(RESOURCE_BUNDLE.getString("invalidRemedialActionType"), identifiable.getId());
            writeRemedialLog(line, message);
        }
    }

    /**
     * check a constraint of a remedial line : constraint is equipment of the network, is of Branch type
     * and monitored on contingencies (branchRatingOnContingencies)
     *
     * @param line number
     * @param network network
     * @param constraint to check
     */
    private void isValidConstraint(int line, Network network, String constraint) {
        if (constraint.isEmpty()) {
            return;
        }
        Identifiable<?> identifiable = network.getIdentifiable(constraint);
        if (identifiable == null) {
            String message = String.format(RESOURCE_BUNDLE.getString("invalidRemedialNetwork"), constraint);
            writeRemedialLog(line, message);
            return;
        }
        if (!(identifiable instanceof Branch)) {
            String message = String.format(RESOURCE_BUNDLE.getString("invalidRemedialConstraintType"), identifiable.getId());
            writeRemedialLog(line, message);
            return;
        }
        if (metrixDslData == null) {
            return;
        }
        if (!metrixDslData.getBranchMonitoringListNk().containsKey(constraint)) {
            String message = String.format(RESOURCE_BUNDLE.getString("invalidMetrixRemedialConstraint"), constraint);
            writeRemedialLog(line, message);
        }
    }

    private void isValidRemedial(Remedial remedial) {
        for (String action : remedial.getBranchToOpen()) {
            isValidAction(remedial.getLineFile(), network, action);
        }
        for (String action : remedial.getBranchToClose()) {
            isValidAction(remedial.getLineFile(), network, action);
        }
        for (String constraint : remedial.getConstraint()) {
            isValidConstraint(remedial.getLineFile(), network, constraint);
        }
    }

    /**
     * check contingencies used in Metrix configuration
     */
    private void checkMetrixDslContingencies(Set<String> contingencyIds, String equipmentType, Map<String, List<String>> equipmentMap) {
        for (Map.Entry<String, List<String>> entry : equipmentMap.entrySet()) {
            for (String ctyId : entry.getValue()) {
                if (!contingencyIds.contains(ctyId)) {
                    writeDslDataContingencyLog(RESOURCE_BUNDLE.getString(equipmentType), entry.getKey(), ctyId);
                }
            }
        }
    }
}