Security.java

/**
 * Copyright (c) 2016, All partners of the iTesla project (http://www.itesla-project.eu/consortium)
 * 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.security;

import com.powsybl.commons.io.table.*;
import com.powsybl.iidm.network.Country;
import com.powsybl.iidm.network.Identifiable;
import com.powsybl.iidm.network.LoadingLimits;
import com.powsybl.iidm.network.Network;
import com.powsybl.iidm.network.limitmodification.LimitsComputer;
import com.powsybl.security.detectors.LoadingLimitType;
import com.powsybl.security.limitreduction.SimpleLimitsComputer;
import com.powsybl.security.results.PostContingencyResult;

import java.io.IOException;
import java.io.StringWriter;
import java.io.UncheckedIOException;
import java.io.Writer;
import java.text.NumberFormat;
import java.util.*;
import java.util.function.Consumer;
import java.util.stream.Collectors;

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

    private static final String PERMANENT_LIMIT_NAME = "Permanent limit";

    private static final String CONTINGENCY = "Contingency";
    private static final String STATUS = "Status";
    private static final String ACTION = "Action";
    private static final String COUNTRY = "Country";
    private static final String BASE_VOLTAGE = "Base voltage";
    private static final String EQUIPMENT = "Equipment";
    private static final String END = "End";
    private static final String VIOLATION_TYPE = "Violation type";
    private static final String VIOLATION_NAME = "Violation name";
    private static final String VALUE = "Value";
    private static final String LIMIT = "Limit";
    private static final String ABS_VALUE_LIMIT = "abs(value-limit)";
    private static final String LOADING_RATE = "Loading rate %";

    private Security() {
    }

    public static List<LimitViolation> checkLimits(Network network) {
        return checkLimits(network, EnumSet.allOf(LoadingLimitType.class), LimitsComputer.NO_MODIFICATIONS);
    }

    public static List<LimitViolation> checkLimits(Network network, double limitReductionValue) {
        return checkLimits(network, EnumSet.allOf(LoadingLimitType.class), limitReductionValue);
    }

    public static List<LimitViolation> checkLimits(Network network, LoadingLimitType currentLimitType, double limitReductionValue) {
        Objects.requireNonNull(currentLimitType);
        return checkLimits(network, EnumSet.of(currentLimitType), limitReductionValue);
    }

    public static List<LimitViolation> checkLimits(Network network, Set<LoadingLimitType> currentLimitTypes, double limitReductionValue) {
        // allow to increase the limits
        if (limitReductionValue <= 0) {
            throw new IllegalArgumentException("Bad limit reduction " + limitReductionValue);
        }
        return checkLimits(network, currentLimitTypes, new SimpleLimitsComputer(limitReductionValue));
    }

    public static List<LimitViolation> checkLimits(Network network, LimitsComputer<Identifiable<?>, LoadingLimits> limitsComputer) {
        return checkLimits(network, EnumSet.allOf(LoadingLimitType.class), limitsComputer);
    }

    public static List<LimitViolation> checkLimits(Network network, Set<LoadingLimitType> currentLimitTypes, LimitsComputer<Identifiable<?>, LoadingLimits> limitsComputer) {
        Objects.requireNonNull(network);
        Objects.requireNonNull(currentLimitTypes);
        List<LimitViolation> violations = new ArrayList<>();
        LimitViolationDetection.checkAll(network, currentLimitTypes, limitsComputer, violations::add);
        return violations;
    }

    public static List<LimitViolation> checkLimitsDc(Network network, double limitReductionValue, double dcPowerFactor) {
        // allow to increase the limits
        if (limitReductionValue <= 0) {
            throw new IllegalArgumentException("Bad limit reduction " + limitReductionValue);
        }
        return checkLimitsDc(network, new SimpleLimitsComputer(limitReductionValue), dcPowerFactor);
    }

    public static List<LimitViolation> checkLimitsDc(Network network, LimitsComputer<Identifiable<?>, LoadingLimits> limitsComputer, double dcPowerFactor) {
        Objects.requireNonNull(network);
        if (dcPowerFactor <= 0 || dcPowerFactor > 1) {
            throw new IllegalArgumentException("Invalid DC power factor " + dcPowerFactor);
        }
        List<LimitViolation> violations = new ArrayList<>();
        LimitViolationDetection.checkAllDc(network, dcPowerFactor, EnumSet.allOf(LoadingLimitType.class), limitsComputer, violations::add);
        return violations;
    }

    public static String printLimitsViolations(Network network) {
        return printLimitsViolations(network, LimitViolationFilter.load());
    }

    public static String printLimitsViolations(Network network, boolean writeName) {
        return printLimitsViolations(checkLimits(network), network, new LimitViolationWriteConfig(LimitViolationFilter.load(),
                                                                                                  TableFormatterConfig.load(),
                                                                                                  writeName));
    }

    public static String printLimitsViolations(Network network, LimitViolationFilter filter) {
        return printLimitsViolations(checkLimits(network), network, filter);
    }

    public static String printLimitsViolations(List<LimitViolation> violations, Network network) {
        return printLimitsViolations(violations, network, LimitViolationFilter.load());
    }

    public static String printLimitsViolations(List<LimitViolation> violations, Network network, LimitViolationFilter filter) {
        return printLimitsViolations(violations, network, filter, TableFormatterConfig.load());
    }

    public static class LimitViolationWriteConfig {

        private final LimitViolationFilter filter;

        private final TableFormatterConfig formatterConfig;

        private final boolean writeName;

        public LimitViolationWriteConfig(LimitViolationFilter filter, TableFormatterConfig formatterConfig, boolean writeName) {
            this.filter = filter;
            this.formatterConfig = Objects.requireNonNull(formatterConfig);
            this.writeName = writeName;
        }

        public LimitViolationFilter getFilter() {
            return filter;
        }

        public TableFormatterConfig getFormatterConfig() {
            return formatterConfig;
        }

        public boolean isWriteName() {
            return writeName;
        }
    }

    public static class PostContingencyLimitViolationWriteConfig extends LimitViolationWriteConfig {

        private final boolean filterPreContingencyViolations;

        public PostContingencyLimitViolationWriteConfig(LimitViolationFilter filter, TableFormatterConfig formatterConfig,
                                                        boolean writeName, boolean filterPreContingencyViolations) {
            super(filter, formatterConfig, writeName);
            this.filterPreContingencyViolations = filterPreContingencyViolations;
        }

        public boolean isFilterPreContingencyViolations() {
            return filterPreContingencyViolations;
        }
    }

    public static String printLimitsViolations(List<LimitViolation> violations, Network network, LimitViolationFilter filter, TableFormatterConfig formatterConfig) {
        return printLimitsViolations(violations, network, new LimitViolationWriteConfig(filter, formatterConfig, false));
    }

    public static String printLimitsViolations(List<LimitViolation> violations, Network network, LimitViolationWriteConfig printConfig) {
        Objects.requireNonNull(violations);
        Objects.requireNonNull(network);
        Objects.requireNonNull(printConfig);

        TableFormatterFactory formatterFactory = new AsciiTableFormatterFactory();
        Writer writer = new StringWriter();
        List<LimitViolation> filteredViolations = printConfig.getFilter() != null ? printConfig.getFilter().apply(violations, network) : violations;

        NumberFormat numberFormat = getFormatter(printConfig.getFormatterConfig().getLocale(), 4, 4);
        NumberFormat percentageFormat = getFormatter(printConfig.getFormatterConfig().getLocale(), 2, 2);

        try (TableFormatter formatter = formatterFactory.create(writer,
                "",
                printConfig.getFormatterConfig(),
                new Column(EQUIPMENT + " (" + filteredViolations.size() + ")"),
                new Column(END),
                new Column(COUNTRY),
                new Column(BASE_VOLTAGE)
                    .setHorizontalAlignment(HorizontalAlignment.RIGHT),
                new Column(VIOLATION_TYPE),
                new Column(VIOLATION_NAME),
                new Column(VALUE)
                    .setHorizontalAlignment(HorizontalAlignment.RIGHT)
                    .setNumberFormat(numberFormat),
                new Column(LIMIT)
                    .setHorizontalAlignment(HorizontalAlignment.RIGHT)
                    .setNumberFormat(numberFormat),
                new Column(ABS_VALUE_LIMIT)
                    .setHorizontalAlignment(HorizontalAlignment.RIGHT)
                    .setNumberFormat(numberFormat),
                new Column(LOADING_RATE)
                    .setHorizontalAlignment(HorizontalAlignment.RIGHT)
                    .setNumberFormat(percentageFormat))) {
            filteredViolations.stream()
                    .sorted(Comparator.comparing(LimitViolation::getSubjectId))
                    .forEach(writeLineLimitsViolations(network, formatter, printConfig.isWriteName()));
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
        return writer.toString().trim();
    }

    private static Consumer<? super LimitViolation> writeLineLimitsViolations(Network network, TableFormatter formatter, boolean writeName) {
        return violation -> {
            try {
                formatter.writeCell(writeName ? violation.getSubjectName() : violation.getSubjectId())
                         .writeCell(LimitViolationHelper.getVoltageLevelId(violation, network, writeName))
                         .writeCell(LimitViolationHelper.getCountry(violation, network).map(Country::name).orElse(""))
                         .writeCell((int) LimitViolationHelper.getNominalVoltage(violation, network))
                         .writeCell(violation.getLimitType().name())
                         .writeCell(getViolationName(violation))
                         .writeCell(violation.getValue())
                         .writeCell(getViolationLimit(violation))
                         .writeCell(getAbsValueLimit(violation))
                         .writeCell(getViolationRate(violation));
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        };
    }

    private static double getAbsValueLimit(LimitViolation violation) {
        return Math.abs(violation.getValue() - violation.getLimit() * violation.getLimitReduction());
    }

    public static void printPreContingencyViolations(SecurityAnalysisResult result, Network network, Writer writer, TableFormatterFactory formatterFactory,
                                                     LimitViolationFilter limitViolationFilter) {
        printPreContingencyViolations(result, network, writer, formatterFactory, TableFormatterConfig.load(), limitViolationFilter);
    }

    public static void printPreContingencyViolations(SecurityAnalysisResult result, Network network, Writer writer, TableFormatterFactory formatterFactory,
                                                     TableFormatterConfig formatterConfig, LimitViolationFilter limitViolationFilter) {
        printPreContingencyViolations(result, network, writer, formatterFactory, new LimitViolationWriteConfig(limitViolationFilter, formatterConfig, false));
    }

    public static void printPreContingencyViolations(SecurityAnalysisResult result, Network network, Writer writer, TableFormatterFactory formatterFactory,
                                                     LimitViolationWriteConfig printConfig) {
        Objects.requireNonNull(result);
        Objects.requireNonNull(network);
        Objects.requireNonNull(writer);
        Objects.requireNonNull(formatterFactory);
        Objects.requireNonNull(printConfig);

        NumberFormat numberFormat = getFormatter(printConfig.getFormatterConfig().getLocale(), 4, 4);
        NumberFormat percentageFormat = getFormatter(printConfig.getFormatterConfig().getLocale(), 2, 2);

        List<LimitViolation> filteredLimitViolations = printConfig.getFilter() != null
                ? printConfig.getFilter().apply(result.getPreContingencyLimitViolationsResult().getLimitViolations(), network)
                : result.getPreContingencyLimitViolationsResult().getLimitViolations();

        try (TableFormatter formatter = formatterFactory.create(writer,
                "Pre-contingency violations",
                printConfig.getFormatterConfig(),
                new Column(ACTION),
                new Column(EQUIPMENT + " (" + filteredLimitViolations.size() + ")"),
                new Column(END),
                new Column(COUNTRY),
                new Column(BASE_VOLTAGE)
                    .setHorizontalAlignment(HorizontalAlignment.RIGHT),
                new Column(VIOLATION_TYPE),
                new Column(VIOLATION_NAME),
                new Column(VALUE)
                    .setHorizontalAlignment(HorizontalAlignment.RIGHT)
                    .setNumberFormat(numberFormat),
                new Column(LIMIT)
                    .setHorizontalAlignment(HorizontalAlignment.RIGHT)
                    .setNumberFormat(numberFormat),
                new Column(ABS_VALUE_LIMIT)
                    .setHorizontalAlignment(HorizontalAlignment.RIGHT)
                    .setNumberFormat(numberFormat),
                new Column(LOADING_RATE)
                    .setHorizontalAlignment(HorizontalAlignment.RIGHT)
                    .setNumberFormat(percentageFormat))) {
            for (String action : result.getPreContingencyLimitViolationsResult().getActionsTaken()) {
                formatter.writeCell(action)
                        .writeEmptyCell()
                        .writeEmptyCell()
                        .writeEmptyCell()
                        .writeEmptyCell()
                        .writeEmptyCell()
                        .writeEmptyCell()
                        .writeEmptyCell()
                        .writeEmptyCell()
                        .writeEmptyCell()
                        .writeEmptyCell();
            }
            filteredLimitViolations.stream()
                    .sorted(Comparator.comparing(LimitViolation::getSubjectId))
                    .forEach(writeLinePreContingencyViolations(network, formatter, printConfig.isWriteName()));
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    private static Consumer<? super LimitViolation> writeLinePreContingencyViolations(Network network, TableFormatter formatter, boolean writeName) {
        return violation -> {
            try {
                formatter.writeEmptyCell()
                        .writeCell(writeName ? violation.getSubjectName() : violation.getSubjectId())
                        .writeCell(LimitViolationHelper.getVoltageLevelId(violation, network, writeName))
                        .writeCell(LimitViolationHelper.getCountry(violation, network).map(Country::name).orElse(""))
                        .writeCell((int) LimitViolationHelper.getNominalVoltage(violation, network))
                        .writeCell(violation.getLimitType().name())
                        .writeCell(getViolationName(violation))
                        .writeCell(violation.getValue())
                        .writeCell(getViolationLimit(violation))
                        .writeCell(getAbsValueLimit(violation))
                        .writeCell(getViolationRate(violation));
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        };
    }

    private static String getViolationName(LimitViolation violation) {
        if (violation.getLimitName() != null) {
            return violation.getLimitName();
        } else if (violation.getAcceptableDuration() != Integer.MAX_VALUE) {
            // TATL
            return String.format("Overload %d'", violation.getAcceptableDuration() / 60);
        } else if (violation.getLimitType() == LimitViolationType.CURRENT) {
            // PATL
            return PERMANENT_LIMIT_NAME;
        } else {
            return "";
        }
    }

    private static double getViolationLimit(LimitViolation violation) {
        return violation.getLimit() * violation.getLimitReduction();
    }

    private static double getViolationRate(LimitViolation violation) {
        return Math.abs(violation.getValue()) / violation.getLimit() * 100.0;
    }

    private static NumberFormat getFormatter(Locale locale, int minimumFractionDigits, int maximumFractionDigits) {
        NumberFormat numberFormat = NumberFormat.getNumberInstance(locale);
        numberFormat.setMinimumFractionDigits(minimumFractionDigits);
        numberFormat.setMaximumFractionDigits(maximumFractionDigits);
        numberFormat.setGroupingUsed(false);
        return numberFormat;
    }

    /**
     * Used to identify a limit violation to avoid duplicated violation between pre and post contingency analysis
     */
    private static class LimitViolationKey {

        private final String id;
        private final LimitViolationType limitType;
        private final double limit;

        public LimitViolationKey(String id, LimitViolationType limitType, double limit) {
            this.id = Objects.requireNonNull(id);
            this.limitType = Objects.requireNonNull(limitType);
            this.limit = limit;
        }

        @Override
        public int hashCode() {
            return Objects.hash(id, limitType, limit);
        }

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof LimitViolationKey other) {
                return id.equals(other.id) && limitType == other.limitType && limit == other.limit;
            }
            return false;
        }
    }

    private static LimitViolationKey toKey(LimitViolation violation) {
        return new LimitViolationKey(violation.getSubjectId(), violation.getLimitType(), violation.getLimit());
    }

    public static void printPostContingencyViolations(SecurityAnalysisResult result, Network network, Writer writer, TableFormatterFactory formatterFactory,
            LimitViolationFilter limitViolationFilter) {
        printPostContingencyViolations(result, network, writer, formatterFactory, limitViolationFilter, true);
    }

    public static void printPostContingencyViolations(SecurityAnalysisResult result, Network network, Writer writer, TableFormatterFactory formatterFactory,
                                                      LimitViolationFilter limitViolationFilter, boolean filterPreContingencyViolations) {
        printPostContingencyViolations(result, network, writer, formatterFactory, TableFormatterConfig.load(), limitViolationFilter, filterPreContingencyViolations);
    }

    public static void printPostContingencyViolations(SecurityAnalysisResult result, Network network, Writer writer, TableFormatterFactory formatterFactory,
                                                      TableFormatterConfig formatterConfig, LimitViolationFilter limitViolationFilter, boolean filterPreContingencyViolations) {
        printPostContingencyViolations(result, network, writer, formatterFactory,
                new PostContingencyLimitViolationWriteConfig(limitViolationFilter, formatterConfig, false, filterPreContingencyViolations));
    }

    public static void printPostContingencyViolations(SecurityAnalysisResult result, Network network, Writer writer, TableFormatterFactory formatterFactory,
                                                      PostContingencyLimitViolationWriteConfig writeConfig) {
        Objects.requireNonNull(result);
        Objects.requireNonNull(network);
        Objects.requireNonNull(writer);
        Objects.requireNonNull(formatterFactory);
        Objects.requireNonNull(writeConfig);
        if (!result.getPostContingencyResults().isEmpty()) {
            Set<LimitViolationKey> preContingencyViolations = writeConfig.isFilterPreContingencyViolations()
                    ? result.getPreContingencyLimitViolationsResult().getLimitViolations()
                            .stream()
                            .map(Security::toKey)
                            .collect(Collectors.toSet())
                    : Collections.emptySet();

            NumberFormat numberFormat = getFormatter(writeConfig.getFormatterConfig().getLocale(), 4, 4);
            NumberFormat percentageFormat = getFormatter(writeConfig.getFormatterConfig().getLocale(), 2, 2);

            int sumFilter = result.getPostContingencyResults()
                .stream()
                .sorted(Comparator.comparing(o2 -> o2.getContingency().getId()))
                .mapToInt(postContingencyResult -> {
                    // configured filtering
                    List<LimitViolation> filteredLimitViolations = writeConfig.getFilter() != null
                            ? writeConfig.getFilter().apply(postContingencyResult.getLimitViolationsResult().getLimitViolations(), network)
                            : postContingencyResult.getLimitViolationsResult().getLimitViolations();

                    // pre-contingency violations filtering
                    List<LimitViolation> filteredLimitViolations2 = filteredLimitViolations.stream()
                            .filter(violation -> preContingencyViolations.isEmpty() || !preContingencyViolations.contains(toKey(violation)))
                            .toList();

                    return filteredLimitViolations2.size();
                }
               ).sum();

            try (TableFormatter formatter = formatterFactory.create(writer,
                "Post-contingency limit violations",
                writeConfig.getFormatterConfig(),
                new Column(CONTINGENCY),
                new Column(STATUS),
                new Column(ACTION),
                new Column(EQUIPMENT + " (" + sumFilter + ")"),
                new Column(END),
                new Column(COUNTRY),
                new Column(BASE_VOLTAGE)
                    .setHorizontalAlignment(HorizontalAlignment.RIGHT),
                new Column(VIOLATION_TYPE),
                new Column(VIOLATION_NAME),
                new Column(VALUE)
                    .setHorizontalAlignment(HorizontalAlignment.RIGHT)
                    .setNumberFormat(numberFormat),
                new Column(LIMIT)
                    .setHorizontalAlignment(HorizontalAlignment.RIGHT)
                    .setNumberFormat(numberFormat),
                new Column(ABS_VALUE_LIMIT)
                    .setHorizontalAlignment(HorizontalAlignment.RIGHT)
                    .setNumberFormat(numberFormat),
                new Column(LOADING_RATE)
                    .setHorizontalAlignment(HorizontalAlignment.RIGHT)
                    .setNumberFormat(percentageFormat))) {
                result.getPostContingencyResults()
                    .stream()
                    .sorted(Comparator.comparing(o2 -> o2.getContingency().getId()))
                    .forEach(writePostContingencyResult(writeConfig.getFilter(), network, preContingencyViolations, formatter, writeConfig.isWriteName()));
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        }
    }

    private static Consumer<? super PostContingencyResult> writePostContingencyResult(LimitViolationFilter limitViolationFilter, Network network,
                                                                                      Set<LimitViolationKey> preContingencyViolations, TableFormatter formatter, boolean writeName) {
        return postContingencyResult -> {
            try {
                // configured filtering
                List<LimitViolation> filteredLimitViolations = limitViolationFilter != null
                        ? limitViolationFilter.apply(postContingencyResult.getLimitViolationsResult().getLimitViolations(), network)
                        : postContingencyResult.getLimitViolationsResult().getLimitViolations();

                // pre-contingency violations filtering
                List<LimitViolation> filteredLimitViolations2 = filteredLimitViolations.stream()
                        .filter(violation -> preContingencyViolations.isEmpty() || !preContingencyViolations.contains(toKey(violation)))
                        .toList();

                if (!filteredLimitViolations2.isEmpty() || postContingencyResult.getStatus() != PostContingencyComputationStatus.CONVERGED) {
                    formatter.writeCell(postContingencyResult.getContingency().getId())
                            .writeCell(postContingencyResult.getStatus().name())
                            .writeEmptyCell()
                            .writeCell(EQUIPMENT + " (" + filteredLimitViolations2.size() + ")")
                            .writeEmptyCell()
                            .writeEmptyCell()
                            .writeEmptyCell()
                            .writeEmptyCell()
                            .writeEmptyCell()
                            .writeEmptyCell()
                            .writeEmptyCell()
                            .writeEmptyCell()
                            .writeEmptyCell();

                    for (String action : postContingencyResult.getLimitViolationsResult().getActionsTaken()) {
                        formatter.writeEmptyCell()
                                .writeEmptyCell()
                                .writeCell(action)
                                .writeEmptyCell()
                                .writeEmptyCell()
                                .writeEmptyCell()
                                .writeEmptyCell()
                                .writeEmptyCell()
                                .writeEmptyCell()
                                .writeEmptyCell()
                                .writeEmptyCell()
                                .writeEmptyCell()
                                .writeEmptyCell();
                    }

                    filteredLimitViolations2.stream()
                            .sorted(Comparator.comparing(LimitViolation::getSubjectId))
                            .forEach(writeLimitViolation(network, formatter, writeName));
                }
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        };
    }

    private static Consumer<? super LimitViolation> writeLimitViolation(Network network, TableFormatter formatter, boolean writeName) {
        return violation -> {
            try {
                formatter.writeEmptyCell()
                        .writeEmptyCell()
                        .writeEmptyCell()
                        .writeCell(writeName ? violation.getSubjectName() : violation.getSubjectId())
                        .writeCell(LimitViolationHelper.getVoltageLevelId(violation, network, writeName))
                        .writeCell(LimitViolationHelper.getCountry(violation, network).map(Country::name).orElse(""))
                        .writeCell((int) LimitViolationHelper.getNominalVoltage(violation, network))
                        .writeCell(violation.getLimitType().name())
                        .writeCell(getViolationName(violation))
                        .writeCell(violation.getValue())
                        .writeCell(getViolationLimit(violation))
                        .writeCell(getAbsValueLimit(violation))
                        .writeCell(getViolationRate(violation));
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        };
    }

    public static void print(SecurityAnalysisResult result, Network network, Writer writer, TableFormatterFactory tableFormatterFactory, TableFormatterConfig tableFormatterConfig) {
        print(result, network, writer, tableFormatterFactory, new PostContingencyLimitViolationWriteConfig(null, tableFormatterConfig, false, true));
    }

    public static void print(SecurityAnalysisResult result, Network network, Writer writer, TableFormatterFactory tableFormatterFactory,
                             PostContingencyLimitViolationWriteConfig writeConfig) {
        printPreContingencyViolations(result, network, writer, tableFormatterFactory, writeConfig);
        printPostContingencyViolations(result, network, writer, tableFormatterFactory, writeConfig);
    }
}