MarginCalculationTool.java

/**
 * Copyright (c) 2025, 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.dynawo.margincalculation.tool;

import com.google.auto.service.AutoService;
import com.powsybl.commons.PowsyblException;
import com.powsybl.commons.io.table.*;
import com.powsybl.commons.report.ReportNode;
import com.powsybl.contingency.ContingenciesProvider;
import com.powsybl.contingency.dsl.GroovyDslContingenciesProviderFactory;
import com.powsybl.dynawo.contingency.results.FailedCriterion;
import com.powsybl.dynawo.contingency.results.ScenarioResult;
import com.powsybl.dynamicsimulation.DynamicModelsSupplier;
import com.powsybl.dynamicsimulation.groovy.DynamicSimulationSupplierFactory;
import com.powsybl.dynawo.margincalculation.MarginCalculation;
import com.powsybl.dynawo.margincalculation.MarginCalculationParameters;
import com.powsybl.dynawo.margincalculation.MarginCalculationRunParameters;
import com.powsybl.dynawo.margincalculation.json.JsonMarginCalculationParameters;
import com.powsybl.dynawo.margincalculation.json.MarginCalculationResultSerializer;
import com.powsybl.dynawo.margincalculation.loadsvariation.supplier.LoadsVariationSupplier;
import com.powsybl.dynawo.margincalculation.results.LoadIncreaseResult;
import com.powsybl.dynawo.margincalculation.results.MarginCalculationResult;
import com.powsybl.iidm.network.ImportConfig;
import com.powsybl.iidm.network.Network;
import com.powsybl.iidm.network.tools.ConversionToolUtils;
import com.powsybl.tools.Command;
import com.powsybl.tools.Tool;
import com.powsybl.tools.ToolRunningContext;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;

import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.nio.file.Path;
import java.util.List;
import java.util.Properties;

import static com.powsybl.dynawo.margincalculation.MarginCalculationReports.createMarginCalculationToolReportNode;

/**
 * @author Laurent Issertial {@literal <laurent.issertial at rte-france.com>}
 */
@AutoService(Tool.class)
public class MarginCalculationTool implements Tool {

    private static final String CASE_FILE = "case-file";
    private static final String DYNAMIC_MODELS_FILE = "dynamic-models-file";
    private static final String CONTINGENCIES_FILE = "contingencies-file";
    private static final String LOAD_VARIATIONS_FILE = "load-variations-file";
    private static final String PARAMETERS_FILE = "parameters-file";
    private static final String OUTPUT_FILE = "output-file";
    private static final String OUTPUT_LOG_FILE = "output-log-file";

    @Override
    public Command getCommand() {
        return new Command() {
            @Override
            public String getName() {
                return "margin-calculation";
            }

            @Override
            public String getTheme() {
                return "Computation";
            }

            @Override
            public String getDescription() {
                return "Run margin calculation";
            }

            @Override
            public Options getOptions() {
                return new Options()
                        .addOption(Option.builder().longOpt(CASE_FILE)
                                .desc("the case path")
                                .hasArg()
                                .argName("FILE")
                                .required()
                                .build())
                        .addOption(Option.builder().longOpt(DYNAMIC_MODELS_FILE)
                                .desc("dynamic models description as a Groovy file: defines the dynamic models to be associated to chosen equipments of the network")
                                .hasArg()
                                .argName("FILE")
                                .required()
                                .build())
                        .addOption(Option.builder().longOpt(CONTINGENCIES_FILE)
                                .desc("contingencies description as a Groovy file")
                                .hasArg()
                                .argName("FILE")
                                .required()
                                .build())
                        .addOption(Option.builder().longOpt(LOAD_VARIATIONS_FILE)
                                .desc("load variations description as a JSON file")
                                .hasArg()
                                .argName("FILE")
                                .required()
                                .build())
                        .addOption(Option.builder().longOpt(PARAMETERS_FILE)
                                .desc("margin calculation parameters as a JSON file")
                                .hasArg()
                                .argName("FILE")
                                .build())
                        .addOption(Option.builder().longOpt(OUTPUT_FILE)
                                .desc("margin calculation results output path")
                                .hasArg()
                                .argName("FILE")
                                .build())
                        .addOption(Option.builder().longOpt(OUTPUT_LOG_FILE)
                                .desc("margin calculation logs output path")
                                .hasArg()
                                .argName("FILE")
                                .build())
                        .addOption(ConversionToolUtils.createImportParametersFileOption())
                        .addOption(ConversionToolUtils.createImportParameterOption());
            }

            @Override
            public String getUsageFooter() {
                return null;
            }
        };
    }

    @Override
    public void run(CommandLine line, ToolRunningContext context) throws Exception {

        ReportNode reportNode = createMarginCalculationToolReportNode();
        Path caseFile = context.getFileSystem().getPath(line.getOptionValue(CASE_FILE));

        context.getOutputStream().println("Loading network '" + caseFile + "'");
        Properties inputParams = ConversionToolUtils.readProperties(line, ConversionToolUtils.OptionType.IMPORT, context);
        Network network = Network.read(caseFile, context.getShortTimeExecutionComputationManager(), ImportConfig.load(), inputParams);
        if (network == null) {
            throw new PowsyblException("Case '" + caseFile + "' not found");
        }

        MarginCalculation.Runner runner = MarginCalculation.getRunner();
        Path dydFile = context.getFileSystem().getPath(line.getOptionValue(DYNAMIC_MODELS_FILE));
        DynamicModelsSupplier dynamicModelsSupplier = DynamicSimulationSupplierFactory.createDynamicModelsSupplier(dydFile, runner.getName());
        Path contingenciesFile = context.getFileSystem().getPath(line.getOptionValue(CONTINGENCIES_FILE));
        ContingenciesProvider contingenciesProvider = new GroovyDslContingenciesProviderFactory().create(contingenciesFile);
        Path loadVariationsFile = context.getFileSystem().getPath(line.getOptionValue(LOAD_VARIATIONS_FILE));
        LoadsVariationSupplier loadsVariationSupplier = LoadsVariationSupplier.getLoadsVariationSupplierForJson(loadVariationsFile);
        MarginCalculationParameters parameters = line.hasOption(PARAMETERS_FILE) ?
                JsonMarginCalculationParameters.read(context.getFileSystem().getPath(line.getOptionValue(PARAMETERS_FILE)))
                : MarginCalculationParameters.builder().build();
        MarginCalculationRunParameters runParameters = new MarginCalculationRunParameters()
                .setMarginCalculationParameters(parameters)
                .setComputationManager(context.getShortTimeExecutionComputationManager())
                .setReportNode(reportNode);

        MarginCalculationResult result = runner.run(network, dynamicModelsSupplier, contingenciesProvider, loadsVariationSupplier, runParameters);
        //Results
        Path outputLogFile = line.hasOption(OUTPUT_LOG_FILE) ? context.getFileSystem().getPath(line.getOptionValue(OUTPUT_LOG_FILE)) : null;
        if (outputLogFile != null) {
            exportLog(reportNode, context, outputLogFile);
        } else {
            printLog(reportNode, context);
        }
        Path outputFile = line.hasOption(OUTPUT_FILE) ? context.getFileSystem().getPath(line.getOptionValue(OUTPUT_FILE)) : null;
        if (outputFile != null) {
            exportResult(result, context, outputFile);
        } else {
            printResult(result, context);
        }
    }

    private void printLog(ReportNode reportNode, ToolRunningContext context) throws IOException {
        Writer writer = new OutputStreamWriter(context.getOutputStream());
        reportNode.print(writer);
        writer.flush();
    }

    private void exportLog(ReportNode reportNode, ToolRunningContext context, Path outputLogFile) throws IOException {
        context.getOutputStream().println("Writing logs to '" + outputLogFile + "'");
        reportNode.print(outputLogFile);
    }

    private void printResult(MarginCalculationResult result, ToolRunningContext context) {
        Writer writer = new OutputStreamWriter(context.getOutputStream());
        AsciiTableFormatterFactory asciiTableFormatterFactory = new AsciiTableFormatterFactory();
        printMarginCalculationResult(result, writer, asciiTableFormatterFactory, TableFormatterConfig.load());
    }

    private void exportResult(MarginCalculationResult result, ToolRunningContext context, Path outputFile) {
        context.getOutputStream().println("Writing results to '" + outputFile + "'");
        MarginCalculationResultSerializer.write(result, outputFile);
    }

    private void printMarginCalculationResult(MarginCalculationResult results, Writer writer,
                                              TableFormatterFactory formatterFactory,
                                              TableFormatterConfig formatterConfig) {
        try (TableFormatter formatter = formatterFactory.create(writer,
                "Margin calculation results",
                formatterConfig,
                getColumns())) {
            for (LoadIncreaseResult result : results.getLoadIncreaseResults()) {
                formatter.writeCell(result.loadLevel());
                formatter.writeCell(result.status().toString());

                List<FailedCriterion> failedCriteria = result.failedCriteria();
                if (failedCriteria.isEmpty()) {
                    formatter.writeEmptyCells(2);
                } else {
                    formatter.writeCell("Failed criteria (%s)".formatted(failedCriteria.size()));
                    formatter.writeEmptyCell();
                }

                List<ScenarioResult> scenarioResults = result.scenarioResults();
                if (scenarioResults.isEmpty()) {
                    formatter.writeEmptyCells(4);
                } else {
                    formatter.writeCell("Scenarios (%s)".formatted(scenarioResults.size()));
                    formatter.writeEmptyCells(3);
                }

                for (FailedCriterion criterion : failedCriteria) {
                    formatter.writeEmptyCells(2);
                    formatter.writeCell(criterion.description());
                    formatter.writeCell(criterion.time());
                    formatter.writeEmptyCells(4);
                }

                printScenarioResult(scenarioResults, formatter);
            }
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private void printScenarioResult(List<ScenarioResult> scenarioResults, TableFormatter formatter) throws IOException {
        for (ScenarioResult scenarioResult : scenarioResults) {
            formatter.writeEmptyCells(4);
            formatter.writeCell(scenarioResult.id());
            formatter.writeCell(scenarioResult.status().toString());
            List<FailedCriterion> scenarioCriteria = scenarioResult.failedCriteria();
            if (scenarioCriteria.isEmpty()) {
                formatter.writeEmptyCells(2);
            } else {
                formatter.writeCell("Scenario failed criteria (%s)".formatted(scenarioCriteria.size()));
                formatter.writeEmptyCell();
            }
            for (FailedCriterion criterion : scenarioResult.failedCriteria()) {
                formatter.writeEmptyCells(6);
                formatter.writeCell(criterion.description());
                formatter.writeCell(criterion.time());
            }
        }
    }

    private static Column[] getColumns() {
        return new Column[]{
            new Column("Load level"),
            new Column("Status"),
            new Column("Failed criteria"),
            new Column("Failed criteria time"),
            new Column("Scenarios"),
            new Column("Scenarios Status"),
            new Column("Scenarios failed criteria"),
            new Column("Scenarios failed criteria time")
        };
    }
}