AbstractSecurityAnalysisExecutionHandler.java

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

import com.google.common.io.ByteSource;
import com.powsybl.action.Action;
import com.powsybl.action.ActionList;
import com.powsybl.computation.*;
import com.powsybl.iidm.serde.NetworkSerDe;
import com.powsybl.security.execution.AbstractSecurityAnalysisExecutionInput;
import com.powsybl.security.execution.NetworkVariant;
import com.powsybl.security.json.limitreduction.LimitReductionListSerDeUtil;
import com.powsybl.security.limitreduction.LimitReduction;
import com.powsybl.security.limitreduction.LimitReductionList;
import com.powsybl.security.monitor.StateMonitor;
import com.powsybl.security.strategy.OperatorStrategy;
import com.powsybl.security.strategy.OperatorStrategyList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.InputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.util.Collections;
import java.util.List;

import static com.google.common.base.Preconditions.checkArgument;
import static java.util.Objects.requireNonNull;

/**
 * @author Laurent Issertial {@literal <laurent.issertial at rte-france.com>}
 */
public abstract class AbstractSecurityAnalysisExecutionHandler<R,
        T extends AbstractSecurityAnalysisExecutionInput<T>,
        S extends AbstractSecurityAnalysisCommandOptions<S>> extends AbstractExecutionHandler<R> {

    protected static final Logger LOGGER = LoggerFactory.getLogger(AbstractSecurityAnalysisExecutionHandler.class);

    private static final String NETWORK_FILE = "network.xiidm";
    private static final String CONTINGENCIES_FILE = "contingencies.groovy";
    private static final String PARAMETERS_FILE = "parameters.json";
    private static final String ACTIONS_FILE = "actions.json";
    private static final String STRATEGIES_FILE = "strategies.json";
    private static final String LIMIT_REDUCTIONS_FILE = "limit-reductions.json";

    protected final int executionCount;
    protected final T input;
    private final ResultReader<R> reader;
    private final OptionsCustomizer<S> optionsCustomizer;
    private final ExceptionHandler exceptionHandler;

    /**
     * Defines the result type, and how is should be read from the working directory after the command execution.
     * It is typically provided as a lambda.
     *
     * @param <R> the result type
     */
    @FunctionalInterface
    public interface ResultReader<R> {
        R read(Path workinDir);
    }

    /**
     * May define or override command options.
     * It is typically provided as a lambda.
     */
    @FunctionalInterface
    public interface OptionsCustomizer<S extends AbstractSecurityAnalysisCommandOptions<S>> {
        void customizeOptions(Path workinDir, S options);
    }

    /**
     * Defines the creation of computation exceptions, in particular log reading.
     */
    @FunctionalInterface
    public interface ExceptionHandler {
        ComputationException createComputationException(Path workingDir, Exception cause);
    }

    protected AbstractSecurityAnalysisExecutionHandler(ResultReader<R> reader,
                                                       OptionsCustomizer<S> optionsCustomizer,
                                                       ExceptionHandler exceptionHandler,
                                                       int executionCount,
                                                       T input) {
        this.reader = requireNonNull(reader);
        this.optionsCustomizer = optionsCustomizer;
        this.exceptionHandler = exceptionHandler;
        checkArgument(executionCount > 0, "Execution count must be positive.");
        this.executionCount = executionCount;
        this.input = requireNonNull(input);
    }


    /**
     * Copies case file, contingencies file, and parameters file to working directory,
     * and creates the {@literal itools security-analysis} command(s) to be executed,
     * based on configuration and the optional options' customizer.
     */
    @Override
    public List<CommandExecution> before(Path workingDir) throws IOException {
        CommandExecution execution = createSecurityAnalysisCommandExecution(workingDir);
        if (!input.getMonitors().isEmpty()) {
            StateMonitor.write(input.getMonitors(), workingDir.resolve("montoring_file.json"));
        }
        return Collections.singletonList(execution);
    }

    /**
     * Reads result from the working directory, as defined by the specified reader.
     */
    @Override
    public R after(Path workingDir, ExecutionReport report) throws IOException {
        try {
            super.after(workingDir, report);
            R result = reader.read(workingDir);
            LOGGER.debug("End of command execution in {}. ", workingDir);
            return result;
        } catch (Exception exception) {
            throw exceptionHandler.createComputationException(workingDir, exception);
        }
    }

    /**
     * Create the {@literal itools security-analysis} command and copies necessary files to working directory.
     * Options may be added through the specified {@link #optionsCustomizer}
     */
    protected abstract CommandExecution createSecurityAnalysisCommandExecution(Path workingDir);

    protected void mapInputToCommand(Path workingDir, S options) {
        options.resultExtensions(input.getResultExtensions())
            .violationTypes(input.getViolationTypes());

        addCaseFile(options, workingDir, input.getNetworkVariant());

        input.getContingenciesSource().ifPresent(
                source -> addContingenciesFile(options, workingDir, source)
        );

        if (executionCount > 1) {
            options.task(taskNumber -> new Partition(taskNumber + 1, executionCount));
        }

        if (optionsCustomizer != null) {
            optionsCustomizer.customizeOptions(workingDir, options);
        }
        addOperatorStrategyFile(options, workingDir, input.getOperatorStrategies());
        addActionFile(options, workingDir, input.getActions());
        addLimitReductionsFile(options, workingDir, input.getLimitReductions());
    }

    private static Path getCasePath(Path workingDir) {
        return workingDir.resolve(NETWORK_FILE);
    }

    protected static Path getParametersPath(Path workingDir) {
        return workingDir.resolve(PARAMETERS_FILE);
    }

    private static Path getActionsPath(Path workingDir) {
        return workingDir.resolve(ACTIONS_FILE);
    }

    private static Path getStrategiesPath(Path workingDir) {
        return workingDir.resolve(STRATEGIES_FILE);
    }

    private static Path getLimitReductionsPath(Path workingDir) {
        return workingDir.resolve(LIMIT_REDUCTIONS_FILE);
    }

    private static Path getContingenciesPath(Path workingDir) {
        return workingDir.resolve(CONTINGENCIES_FILE);
    }

    /**
     * Add case file option, and write network to working directory.
     */
    private void addCaseFile(S options, Path workingDir, NetworkVariant variant) {
        Path dest = getCasePath(workingDir);
        options.caseFile(dest);
        LOGGER.debug("Copying network to file {}", dest);
        NetworkSerDe.write(variant.getVariant(), dest);
    }

    /**
     * Add contingencies file option, and write it to working directory.
     */
    private void addContingenciesFile(S options, Path workingDir, ByteSource source) {
        Path dest = getContingenciesPath(workingDir);
        options.contingenciesFile(dest);
        LOGGER.debug("Writing contingencies to file {}", dest);
        copySourceToPath(source, dest);
    }

    /**
     * Add operator strategies file option, and write it as JSON to working directory.
     */
    private void addOperatorStrategyFile(S options, Path workingDir, List<OperatorStrategy> operatorStrategies) {
        if (operatorStrategies.isEmpty()) {
            return;
        }
        Path path = getStrategiesPath(workingDir);
        options.strategiesFile(path);
        LOGGER.debug("Writing operator strategies to file {}", path);
        new OperatorStrategyList(operatorStrategies)
                .write(path);
    }

    /**
     * Add action file option, and write it as JSON to working directory.
     */
    private void addActionFile(S options, Path workingDir, List<Action> actions) {
        if (actions.isEmpty()) {
            return;
        }
        Path path = getActionsPath(workingDir);
        options.actionsFile(path);
        LOGGER.debug("Writing actions to file {}", path);
        new ActionList(actions)
                .writeJsonFile(path);
    }

    /**
     * Add limit reductions file option, and write it as JSON to working directory.
     */
    private void addLimitReductionsFile(S options, Path workingDir, List<LimitReduction> limitReductions) {
        if (limitReductions.isEmpty()) {
            return;
        }
        Path path = getLimitReductionsPath(workingDir);
        options.limitReductionsFile(path);
        LOGGER.debug("Writing limit reductions to file {}", path);
        LimitReductionListSerDeUtil.write(new LimitReductionList(limitReductions), path);
    }

    /**
     * Copies bytes from the source to target path.
     */
    protected static void copySourceToPath(ByteSource source, Path dest) {
        try (InputStream is = source.openBufferedStream()) {
            Files.copy(is, dest, StandardCopyOption.REPLACE_EXISTING);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

}