AbstractSecurityAnalysisTool.java

/**
 * Copyright (c) 2024, 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.security.tools;

import com.powsybl.action.ActionList;
import com.powsybl.commons.io.FileUtil;
import com.powsybl.commons.io.table.AsciiTableFormatterFactory;
import com.powsybl.commons.io.table.TableFormatterConfig;
import com.powsybl.computation.ComputationException;
import com.powsybl.computation.Partition;
import com.powsybl.iidm.network.ImportConfig;
import com.powsybl.iidm.network.ImportersLoader;
import com.powsybl.iidm.network.Network;
import com.powsybl.iidm.network.tools.ConversionToolUtils;
import com.powsybl.loadflow.LoadFlowResult;
import com.powsybl.security.*;
import com.powsybl.security.converter.SecurityAnalysisResultExporters;
import com.powsybl.security.execution.AbstractSecurityAnalysisExecutionBuilder;
import com.powsybl.security.execution.AbstractSecurityAnalysisExecutionInput;
import com.powsybl.security.json.limitreduction.LimitReductionListSerDeUtil;
import com.powsybl.security.monitor.StateMonitor;
import com.powsybl.security.strategy.OperatorStrategyList;
import com.powsybl.tools.ToolOptions;
import com.powsybl.tools.ToolRunningContext;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.ParseException;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;
import java.util.concurrent.CompletionException;
import java.util.function.Supplier;
import java.util.stream.Collectors;

import static com.powsybl.iidm.network.tools.ConversionToolUtils.readProperties;
import static com.powsybl.security.tools.SecurityAnalysisToolConstants.*;
import static com.powsybl.tools.ToolConstants.TASK;
import static com.powsybl.tools.ToolConstants.TASK_COUNT;

/**
 * @author Geoffroy Jamgotchian {@literal <geoffroy.jamgotchian at rte-france.com>}
 * @author Teofil Calin BANC {@literal <teofil-calin.banc at rte-france.com>}
 * @author Laurent Issertial {@literal <laurent.issertial at rte-france.com>}
 */
public abstract class AbstractSecurityAnalysisTool<T extends AbstractSecurityAnalysisExecutionInput<T>,
        R extends AbstractSecurityAnalysisExecutionBuilder<R>> {

    public static Network readNetwork(CommandLine line, ToolRunningContext context, ImportersLoader importersLoader) throws IOException {
        ToolOptions options = new ToolOptions(line, context);
        Path caseFile = options.getPath(CASE_FILE_OPTION)
            .orElseThrow(IllegalStateException::new);
        Properties inputParams = readProperties(line, ConversionToolUtils.OptionType.IMPORT, context);
        context.getOutputStream().println("Loading network '" + caseFile + "'");
        Network network = Network.read(caseFile, context.getShortTimeExecutionComputationManager(), ImportConfig.load(), inputParams, importersLoader);
        network.getVariantManager().allowVariantMultiThreadAccess(true);
        return network;
    }

    protected static void uncheckedWriteBytes(byte[] bytes, Path path) {
        try {
            Files.write(path, bytes);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public void run(CommandLine line, ToolRunningContext context,
             R executionBuilder,
             ImportersLoader importersLoader,
             Supplier<TableFormatterConfig> tableFormatterConfigLoader) throws ParseException, IOException {

        ToolOptions options = new ToolOptions(line, context);

        // Output file and output format
        Path outputFile = options.getPath(OUTPUT_FILE_OPTION).orElse(null);
        String format = getFormat(options, outputFile);
        Network network = readNetwork(line, context, importersLoader);

        T executionInput = getExecutionInput(network);
        updateInput(options, executionInput);

        Supplier<SecurityAnalysisReport> supplier = getReportSupplier(context, options, executionBuilder, executionInput);

        SecurityAnalysisResult result = options.getPath(OUTPUT_LOG_OPTION)
                .map(logPath -> runSecurityAnalysisWithLog(supplier, logPath))
                .orElseGet(supplier).getResult();

        if (result.getPreContingencyResult().getStatus() != LoadFlowResult.ComponentResult.Status.CONVERGED) {
            context.getErrorStream().println("Pre-contingency state divergence");
        }

        if (outputFile != null) {
            context.getOutputStream().println("Writing results to '" + outputFile + "'");
            SecurityAnalysisResultExporters.export(result, outputFile, format);
        } else {
            // To avoid the closing of System.out
            Writer writer = new OutputStreamWriter(context.getOutputStream());
            Security.print(result, network, writer, new AsciiTableFormatterFactory(), tableFormatterConfigLoader.get());
        }
    }

    protected String getFormat(ToolOptions options, Path outputFile) throws ParseException {
        return outputFile != null ? options.getValue(OUTPUT_FORMAT_OPTION)
                    .orElseThrow(() -> new ParseException("Missing required option: " + OUTPUT_FORMAT_OPTION))
                : null;
    }

    protected abstract T getExecutionInput(Network network);

    public void updateInput(ToolOptions options, T inputs) {
        options.getPath(CONTINGENCIES_FILE_OPTION)
                .map(FileUtil::asByteSource)
                .ifPresent(inputs::setContingenciesSource);
        options.getValues(LIMIT_TYPES_OPTION)
                .map(types -> types.stream().map(LimitViolationType::valueOf).collect(Collectors.toList()))
                .ifPresent(inputs::addViolationTypes);
        options.getValues(WITH_EXTENSIONS_OPTION)
                .ifPresent(inputs::addResultExtensions);
        options.getValues(OUTPUT_LOG_OPTION)
                .ifPresent(f -> inputs.setWithLogs(true));
        options.getPath(MONITORING_FILE)
                .ifPresent(monitorFilePath -> inputs.setMonitors(StateMonitor.read(monitorFilePath)));
        options.getPath(STRATEGIES_FILE)
                .ifPresent(operatorStrategyFilePath -> inputs.setOperatorStrategies(OperatorStrategyList.read(operatorStrategyFilePath).getOperatorStrategies()));
        options.getPath(ACTIONS_FILE)
                .ifPresent(actionFilePath -> inputs.setActions(ActionList.readJsonFile(actionFilePath).getActions()));
        options.getPath(LIMIT_REDUCTIONS_FILE)
                .ifPresent(limitReductionsFilePath -> inputs.setLimitReductions(LimitReductionListSerDeUtil.read(limitReductionsFilePath).getLimitReductions()));
    }

    protected SecurityAnalysisReport runSecurityAnalysisWithLog(Supplier<SecurityAnalysisReport> supplier, Path logPath) {
        try {
            SecurityAnalysisReport report = supplier.get();
            // copy log bytes to file
            report.getLogBytes()
                    .ifPresent(logBytes -> uncheckedWriteBytes(logBytes, logPath));
            return report;
        } catch (CompletionException e) {
            if (e.getCause() instanceof ComputationException computationException) {
                byte[] bytes = computationException.toZipBytes();
                uncheckedWriteBytes(bytes, logPath);
            }
            throw e;
        }
    }

    protected void setupExecutionBuilder(ToolOptions options, R builder) {
        builder.forward(options.hasOption(EXTERNAL));
        options.getInt(TASK_COUNT).ifPresent(builder::distributed);
        options.getValue(TASK, Partition::parse).ifPresent(builder::subTask);
    }

    protected abstract Supplier<SecurityAnalysisReport> getReportSupplier(ToolRunningContext context, ToolOptions options, R executionBuilder, T executionInput);
}