NetworkStateComparator.java

/**
 * Copyright (c) 2019, 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.iidm.comparator;

import com.google.common.collect.Lists;
import com.powsybl.iidm.network.*;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.ss.util.CellRangeAddress;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.io.OutputStream;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.BinaryOperator;

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

    private static final Logger LOGGER = LoggerFactory.getLogger(NetworkStateComparator.class);

    private interface ColumnMapper<T extends Identifiable> {

        String getTitle();

        void setValue(T obj, Cell cell);

    }

    private static final ColumnMapper<Bus> BUS_V = new ColumnMapper<>() {

        @Override
        public String getTitle() {
            return "v (KV)";
        }

        @Override
        public void setValue(Bus bus, Cell cell) {
            if (!Double.isNaN(bus.getV())) {
                cell.setCellValue(bus.getV());
            }
        }
    };

    private static final ColumnMapper<Bus> BUS_ANGLE = new ColumnMapper<>() {

        @Override
        public String getTitle() {
            return "\u03B8 (��)";
        }

        @Override
        public void setValue(Bus bus, Cell cell) {
            if (!Double.isNaN(bus.getAngle())) {
                cell.setCellValue(bus.getAngle());
            }
        }
    };

    private static class BranchP1ColumnMapper<T extends Branch> implements ColumnMapper<T> {

        @Override
        public String getTitle() {
            return "p1 (MW)";
        }

        @Override
        public void setValue(Branch branch, Cell cell) {
            if (!Double.isNaN(branch.getTerminal1().getP())) {
                cell.setCellValue(branch.getTerminal1().getP());
            }
        }
    }

    private static class BranchQ1ColumnMapper<T extends Branch> implements ColumnMapper<T> {

        @Override
        public String getTitle() {
            return "q1 (MVAr)";
        }

        @Override
        public void setValue(Branch branch, Cell cell) {
            if (!Double.isNaN(branch.getTerminal1().getQ())) {
                cell.setCellValue(branch.getTerminal1().getQ());
            }
        }
    }

    private static class BranchP2ColumnMapper<T extends Branch> implements ColumnMapper<T> {

        @Override
        public String getTitle() {
            return "p2 (MW)";
        }

        @Override
        public void setValue(Branch branch, Cell cell) {
            if (!Double.isNaN(branch.getTerminal2().getP())) {
                cell.setCellValue(branch.getTerminal2().getP());
            }
        }
    }

    private static class BranchQ2ColumnMapper<T extends Branch> implements ColumnMapper<T> {

        @Override
        public String getTitle() {
            return "q2 (MVAr)";
        }

        @Override
        public void setValue(Branch branch, Cell cell) {
            if (!Double.isNaN(branch.getTerminal2().getQ())) {
                cell.setCellValue(branch.getTerminal2().getQ());
            }
        }
    }

    private static class BranchRatioColumnMapper<T extends Branch> implements ColumnMapper<T> {

        @Override
        public String getTitle() {
            return "v1/v2 (pu)";
        }

        @Override
        public void setValue(Branch branch, Cell cell) {
            if (branch instanceof TwoWindingsTransformer twt
                    && (twt.hasRatioTapChanger() ||
                        twt.hasPhaseTapChanger())) {
                Bus b1 = branch.getTerminal1().getBusView().getBus();
                Bus b2 = branch.getTerminal2().getBusView().getBus();
                if (b1 != null && !Double.isNaN(b1.getV()) && b2 != null && !Double.isNaN(b2.getV()) && b2.getV() != 0) {
                    cell.setCellValue(b1.getV() / b2.getV());
                }
            }
        }
    }

    private static class BranchDephaColumnMapper<T extends Branch> implements ColumnMapper<T> {

        @Override
        public String getTitle() {
            return "depha (��)";
        }

        @Override
        public void setValue(Branch branch, Cell cell) {
            if (branch instanceof TwoWindingsTransformer twt
                    && (twt.hasRatioTapChanger() ||
                    twt.hasPhaseTapChanger())) {
                Bus b1 = branch.getTerminal1().getBusView().getBus();
                Bus b2 = branch.getTerminal2().getBusView().getBus();
                if (b1 != null && !Double.isNaN(b1.getAngle()) && b2 != null && !Double.isNaN(b2.getAngle())) {
                    cell.setCellValue(b1.getAngle() - b2.getAngle());
                }
            }
        }
    }

    private abstract static class AbstractT3wtPColumnMapper<T extends ThreeWindingsTransformer> implements ColumnMapper<T> {
        protected final ThreeSides side;
        private final String title;

        public AbstractT3wtPColumnMapper(ThreeSides side, String title) {
            this.side = side;
            this.title = title;
        }

        @Override
        public String getTitle() {
            return title;
        }
    }

    private static class T3wtPColumnMapper extends AbstractT3wtPColumnMapper<ThreeWindingsTransformer> {

        public T3wtPColumnMapper(ThreeSides side, String title) {
            super(side, title);
        }

        @Override
        public void setValue(ThreeWindingsTransformer t3wt, Cell cell) {
            if (!Double.isNaN(t3wt.getTerminal(side).getP())) {
                cell.setCellValue(t3wt.getTerminal(side).getP());
            }
        }
    }

    private static class T3wtQColumnMapper extends AbstractT3wtPColumnMapper<ThreeWindingsTransformer> {

        public T3wtQColumnMapper(ThreeSides side, String title) {
            super(side, title);
        }

        @Override
        public void setValue(ThreeWindingsTransformer t3wt, Cell cell) {
            if (!Double.isNaN(t3wt.getTerminal(side).getQ())) {
                cell.setCellValue(t3wt.getTerminal(side).getQ());
            }
        }
    }

    private static class T3wtRatioTapColumnMapper extends AbstractT3wtPColumnMapper<ThreeWindingsTransformer> {

        public T3wtRatioTapColumnMapper(ThreeSides side, String title) {
            super(side, title);
        }

        @Override
        public void setValue(ThreeWindingsTransformer t3wt, Cell cell) {
            final RatioTapChanger rtc = t3wt.getLegs().get(side.ordinal()).getRatioTapChanger();
            if (rtc != null) {
                cell.setCellValue(rtc.getTapPosition());
            }
        }
    }

    private static class T3wtPhaseTapColumnMapper extends AbstractT3wtPColumnMapper<ThreeWindingsTransformer> {

        public T3wtPhaseTapColumnMapper(ThreeSides side, String title) {
            super(side, title);
        }

        @Override
        public void setValue(ThreeWindingsTransformer t3wt, Cell cell) {
            final PhaseTapChanger ptc = t3wt.getLegs().get(side.ordinal()).getPhaseTapChanger();
            if (ptc != null) {
                cell.setCellValue(ptc.getTapPosition());
            }
        }
    }

    private static final BranchP1ColumnMapper<Line> LINE_P1 = new BranchP1ColumnMapper<>();

    private static final BranchQ1ColumnMapper<Line> LINE_Q1 = new BranchQ1ColumnMapper<>();

    private static final BranchP2ColumnMapper<Line> LINE_P2 = new BranchP2ColumnMapper<>();

    private static final BranchQ2ColumnMapper<Line> LINE_Q2 = new BranchQ2ColumnMapper<>();

    private static final BranchP1ColumnMapper<TwoWindingsTransformer> TWT_P1 = new BranchP1ColumnMapper<>();

    private static final BranchQ1ColumnMapper<TwoWindingsTransformer> TWT_Q1 = new BranchQ1ColumnMapper<>();

    private static final BranchP2ColumnMapper<TwoWindingsTransformer> TWT_P2 = new BranchP2ColumnMapper<>();

    private static final BranchQ2ColumnMapper<TwoWindingsTransformer> TWT_Q2 = new BranchQ2ColumnMapper<>();

    private static final BranchRatioColumnMapper<TwoWindingsTransformer> TWT_RATIO = new BranchRatioColumnMapper<>();

    private static final BranchDephaColumnMapper<TwoWindingsTransformer> TWT_DEPHA = new BranchDephaColumnMapper<>();

    private static final ColumnMapper<TwoWindingsTransformer> TWT_RATIO_TAP = new ColumnMapper<>() {

        @Override
        public String getTitle() {
            return "ratio tap";
        }

        @Override
        public void setValue(TwoWindingsTransformer twt, Cell cell) {
            RatioTapChanger rtc = twt.getRatioTapChanger();
            if (rtc != null) {
                cell.setCellValue(rtc.getTapPosition());
            }
        }
    };

    private static final ColumnMapper<TwoWindingsTransformer> TWT_PHASE_TAP = new ColumnMapper<>() {

        @Override
        public String getTitle() {
            return "phase tap";
        }

        @Override
        public void setValue(TwoWindingsTransformer twt, Cell cell) {
            PhaseTapChanger ptc = twt.getPhaseTapChanger();
            if (ptc != null) {
                cell.setCellValue(ptc.getTapPosition());
            }
        }
    };

    private static class InjectionPColumnMapper<T extends Injection> implements ColumnMapper<T> {

        @Override
        public String getTitle() {
            return "p (MW)";
        }

        @Override
        public void setValue(Injection inj, Cell cell) {
            if (!Double.isNaN(inj.getTerminal().getP())) {
                cell.setCellValue(inj.getTerminal().getP());
            }
        }
    }

    private static class InjectionQColumnMapper<T extends Injection> implements ColumnMapper<T> {

        @Override
        public String getTitle() {
            return "q (MW)";
        }

        @Override
        public void setValue(Injection inj, Cell cell) {
            if (!Double.isNaN(inj.getTerminal().getQ())) {
                cell.setCellValue(inj.getTerminal().getQ());
            }
        }
    }

    private static class InjectionVColumnMapper<T extends Injection> implements ColumnMapper<T> {

        @Override
        public String getTitle() {
            return "v (KV)";
        }

        @Override
        public void setValue(Injection inj, Cell cell) {
            Bus b = inj.getTerminal().getBusView().getBus();
            if (b != null && !Double.isNaN(b.getV())) {
                cell.setCellValue(b.getV());
            }
        }
    }

    private static final InjectionPColumnMapper<Generator> GEN_P = new InjectionPColumnMapper<>();

    private static final InjectionQColumnMapper<Generator> GEN_Q = new InjectionQColumnMapper<>();

    private static final InjectionVColumnMapper<Generator> GEN_V = new InjectionVColumnMapper<>();

    private static final InjectionPColumnMapper<HvdcConverterStation> HVDC_P = new InjectionPColumnMapper<>();

    private static final InjectionQColumnMapper<HvdcConverterStation> HVDC_Q = new InjectionQColumnMapper<>();

    private static final InjectionVColumnMapper<HvdcConverterStation> HVDC_V = new InjectionVColumnMapper<>();

    private static final InjectionPColumnMapper<Load> LOAD_P = new InjectionPColumnMapper<>();

    private static final InjectionQColumnMapper<Load> LOAD_Q = new InjectionQColumnMapper<>();

    private static final ColumnMapper<ShuntCompensator> SHUNT_SECTIONS = new ColumnMapper<>() {

        @Override
        public String getTitle() {
            return "shunt sections";
        }

        @Override
        public void setValue(ShuntCompensator sc, Cell cell) {
            cell.setCellValue(sc.getSectionCount());
        }
    };

    private static final InjectionPColumnMapper<ShuntCompensator> SHUNT_P = new InjectionPColumnMapper<>();

    private static final InjectionQColumnMapper<ShuntCompensator> SHUNT_Q = new InjectionQColumnMapper<>();

    private static final InjectionVColumnMapper<StaticVarCompensator> SVC_V = new InjectionVColumnMapper<>();

    private static final InjectionQColumnMapper<StaticVarCompensator> SVC_Q = new InjectionQColumnMapper<>();

    private static final T3wtPColumnMapper T3WT_P1 = new T3wtPColumnMapper(ThreeSides.ONE, "p1 (MW)");
    private static final T3wtPColumnMapper T3WT_P2 = new T3wtPColumnMapper(ThreeSides.TWO, "p2 (MW)");
    private static final T3wtPColumnMapper T3WT_P3 = new T3wtPColumnMapper(ThreeSides.THREE, "p3 (MW)");
    private static final T3wtQColumnMapper T3WT_Q1 = new T3wtQColumnMapper(ThreeSides.ONE, "q1 (MVAr)");
    private static final T3wtQColumnMapper T3WT_Q2 = new T3wtQColumnMapper(ThreeSides.TWO, "q2 (MVAr)");
    private static final T3wtQColumnMapper T3WT_Q3 = new T3wtQColumnMapper(ThreeSides.THREE, "q3 (MVAr)");
    private static final T3wtRatioTapColumnMapper T3WT_RATIO1 = new T3wtRatioTapColumnMapper(ThreeSides.ONE, "ratio tap 1");
    private static final T3wtRatioTapColumnMapper T3WT_RATIO2 = new T3wtRatioTapColumnMapper(ThreeSides.TWO, "ratio tap 2");
    private static final T3wtRatioTapColumnMapper T3WT_RATIO3 = new T3wtRatioTapColumnMapper(ThreeSides.THREE, "ratio tap 3");
    private static final T3wtPhaseTapColumnMapper T3WT_PHASE1 = new T3wtPhaseTapColumnMapper(ThreeSides.ONE, "phase tap 1");
    private static final T3wtPhaseTapColumnMapper T3WT_PHASE2 = new T3wtPhaseTapColumnMapper(ThreeSides.TWO, "phase tap 2");
    private static final T3wtPhaseTapColumnMapper T3WT_PHASE3 = new T3wtPhaseTapColumnMapper(ThreeSides.THREE, "phase tap 3");

    private static final class DiffColumnMapper<T extends Identifiable> implements ColumnMapper<T> {

        private final String title;

        private final int column1;

        private final int column2;

        private DiffColumnMapper(String title, int column1, int column2) {
            this.title = title;
            this.column1 = column1;
            this.column2 = column2;
        }

        @Override
        public String getTitle() {
            return title;
        }

        @Override
        public void setValue(T obj, Cell cell) {
            int row = cell.getRowIndex() + 1;
            String ax = (char) ('A' + column1) + Integer.toString(row);
            String ay = (char) ('A' + column2) + Integer.toString(row);
            cell.setCellFormula("IF(OR(ISBLANK(" + ax + "), ISBLANK(" + ay + ")), \"\",  ABS(" + ax + "-" + ay + "))");
        }

    }

    private static class SheetContext<T extends Identifiable> {

        private final List<T> objs;

        private final Sheet sheet;

        private final Row rowHeader1;

        private final Row rowHeader2;

        private final List<Row> rows = new ArrayList<>();

        SheetContext(List<T> objs, Workbook wb, String sheetTitle) {
            this.objs = Objects.requireNonNull(objs);

            sheet = wb.createSheet(sheetTitle);

            rowHeader1 = sheet.createRow(0);
            rowHeader2 = sheet.createRow(1);

            for (int i = 0; i < objs.size(); i++) {
                rows.add(sheet.createRow(i + 2));
            }
        }

        List<T> getObjs() {
            return objs;
        }

        Sheet getSheet() {
            return sheet;
        }

        Row getRowHeader1() {
            return rowHeader1;
        }

        Row getRowHeader2() {
            return rowHeader2;
        }

        List<Row> getRows() {
            return rows;
        }
    }

    private static final List<ColumnMapper<Bus>> BUS_MAPPERS = List.of(BUS_V, BUS_ANGLE);

    private static final List<ColumnMapper<Line>> LINE_MAPPERS = List.of(LINE_P1, LINE_P2, LINE_Q1, LINE_Q2);

    private static final List<ColumnMapper<TwoWindingsTransformer>> TRANSFO_MAPPERS = List.of(TWT_P1, TWT_P2, TWT_Q1, TWT_Q2, TWT_RATIO_TAP, TWT_PHASE_TAP, TWT_RATIO, TWT_DEPHA);

    private static final List<ColumnMapper<ThreeWindingsTransformer>> T3WT_MAPPERS = List.of(T3WT_P1, T3WT_P2, T3WT_P3, T3WT_Q1, T3WT_Q2, T3WT_Q3, T3WT_RATIO1, T3WT_RATIO2, T3WT_RATIO3, T3WT_PHASE1, T3WT_PHASE2, T3WT_PHASE3);

    private static final List<ColumnMapper<Generator>> GENERATOR_MAPPERS = List.of(GEN_P, GEN_Q, GEN_V);

    private static final List<ColumnMapper<HvdcConverterStation>> HVDC_MAPPERS = List.of(HVDC_P, HVDC_Q, HVDC_V);

    private static final List<ColumnMapper<Load>> LOAD_MAPPERS = List.of(LOAD_P, LOAD_Q);

    private static final List<ColumnMapper<ShuntCompensator>> SHUNT_MAPPERS = List.of(SHUNT_SECTIONS, SHUNT_P, SHUNT_Q);

    private static final List<ColumnMapper<StaticVarCompensator>> SVC_MAPPERS = List.of(SVC_Q, SVC_V);

    private final Network network;

    private final String otherState;

    public NetworkStateComparator(Network network, String otherState) {
        this.network = Objects.requireNonNull(network);
        this.otherState = Objects.requireNonNull(otherState);
    }

    private static Cell createTitleCell(Row row, int i, CellStyle titleCellStyle) {
        Cell c = row.createCell(i);
        c.setCellStyle(titleCellStyle);
        return c;
    }

    private static <T extends Identifiable> void createColumnHeader(SheetContext<T> sheetContext, CellStyle titleCellStyle) {
        createTitleCell(sheetContext.getRowHeader1(), 0, titleCellStyle);
        createTitleCell(sheetContext.getRowHeader2(), 0, titleCellStyle).setCellValue("id");
        createTitleCell(sheetContext.getRowHeader1(), 1, titleCellStyle);
        createTitleCell(sheetContext.getRowHeader2(), 1, titleCellStyle).setCellValue("name");
        for (int i = 0; i < sheetContext.getObjs().size(); i++) {
            sheetContext.getRows().get(i).createCell(0).setCellValue(sheetContext.getObjs().get(i).getId());
            sheetContext.getRows().get(i).createCell(1).setCellValue(sheetContext.getObjs().get(i).getNameOrId());
        }
    }

    private static <T extends Identifiable> void createColumns(SheetContext<T> sheetContext, CellStyle titleCellStyle,
                                                               int columnOffset, String columnsTitle, List<ColumnMapper<T>> mappers) {
        createTitleCell(sheetContext.getRowHeader1(), columnOffset, titleCellStyle).setCellValue(columnsTitle);
        for (int i = 0; i < mappers.size(); i++) {
            createTitleCell(sheetContext.getRowHeader1(), columnOffset + 1 + i, titleCellStyle);
        }
        for (int i = 0; i < mappers.size(); i++) {
            createTitleCell(sheetContext.getRowHeader2(), columnOffset + i, titleCellStyle).setCellValue(mappers.get(i).getTitle());
        }
        sheetContext.getSheet().addMergedRegion(new CellRangeAddress(0, 0, columnOffset, columnOffset + mappers.size() - 1));
        for (int y = 0; y < sheetContext.getObjs().size(); y++) {
            T obj = sheetContext.getObjs().get(y);
            Row row = sheetContext.getRows().get(y);
            for (int x = 0; x < mappers.size(); x++) {
                Cell cell = row.createCell(columnOffset + x);
                mappers.get(x).setValue(obj, cell);
            }
        }
    }

    private <T extends Identifiable> void createSheet(List<T> objs, Workbook wb, CellStyle titleCellStyle,
                                                      String sheetTitle, List<ColumnMapper<T>> mappers) {
        if (mappers.isEmpty()) {
            throw new IllegalArgumentException("no mappers provided");
        }

        SheetContext<T> sheetContext = new SheetContext<>(objs, wb, sheetTitle);

        // create id and name columns
        createColumnHeader(sheetContext, titleCellStyle);

        // create state columns
        int stateColumnOffset = 2;
        createColumns(sheetContext, titleCellStyle, stateColumnOffset, network.getVariantManager().getWorkingVariantId(), mappers);

        String oldState = network.getVariantManager().getWorkingVariantId();
        network.getVariantManager().setWorkingVariant(otherState);

        // create other state columns
        int otherStateColumnOffset = stateColumnOffset + mappers.size();
        createColumns(sheetContext, titleCellStyle, otherStateColumnOffset, network.getVariantManager().getWorkingVariantId(), mappers);
        network.getVariantManager().setWorkingVariant(oldState);

        // create diff columns
        int diffColumnOffset = stateColumnOffset + 2 * mappers.size();
        List<ColumnMapper<T>> diffMappers = new ArrayList<>(mappers.size());
        for (int i = 0; i < mappers.size(); i++) {
            diffMappers.add(new DiffColumnMapper<>(mappers.get(i).getTitle(), stateColumnOffset + i, otherStateColumnOffset + i));
        }
        createColumns(sheetContext, titleCellStyle, diffColumnOffset, "Diff", diffMappers);

        // auto resize ID columns
        sheetContext.getSheet().autoSizeColumn(0);
        sheetContext.getSheet().autoSizeColumn(1);

        // auto filter
        sheetContext.getSheet().setAutoFilter(new CellRangeAddress(1, 1, stateColumnOffset, stateColumnOffset + 3 * mappers.size() - 1));

        // create diff stats (only if there is at least one object to compare, otherwise current code creates circular references in worksheet).
        if (!objs.isEmpty()) {
            createRowFooter(sheetContext, diffColumnOffset, mappers, 0, "Max", (fromCell, toCell) -> "MAX(" + fromCell + ":" + toCell + ")");
            createRowFooter(sheetContext, diffColumnOffset, mappers, 1, "Min", (fromCell, toCell) -> "MIN(" + fromCell + ":" + toCell + ")");
            createRowFooter(sheetContext, diffColumnOffset, mappers, 2, "Average", (fromCell, toCell) -> "AVERAGE(" + fromCell + ":" + toCell + ")");
        }
    }

    private <T extends Identifiable> void createRowFooter(SheetContext<T> sheetContext, int diffColumnOffset,
                                                          List<ColumnMapper<T>> mappers, int footerIndex, String title, BinaryOperator<String> function) {
        Row rowFooterMax = sheetContext.getSheet().createRow(sheetContext.getObjs().size() + 2 + footerIndex);
        Cell titleCell = rowFooterMax.createCell(diffColumnOffset - 1);
        titleCell.setCellValue(title);
        for (int i = 0; i < mappers.size(); i++) {
            Cell cell = rowFooterMax.createCell(diffColumnOffset + i);
            String col = CellReference.convertNumToColString(diffColumnOffset + i);
            String fromCell = col + "3";
            String toCell = col + (sheetContext.getObjs().size() + 2);
            cell.setCellFormula(function.apply(fromCell, toCell));
        }
    }

    private void createSheets(Workbook wb, CellStyle titleCellStyle) {
        createSheet(Lists.newArrayList(network.getBusView().getBuses()), wb, titleCellStyle, "Buses", BUS_MAPPERS);
        createSheet(Lists.newArrayList(network.getLines()), wb, titleCellStyle, "Lines", LINE_MAPPERS);
        createSheet(Lists.newArrayList(network.getTwoWindingsTransformers()), wb, titleCellStyle, "2WindingsTransformers", TRANSFO_MAPPERS);
        createSheet(Lists.newArrayList(network.getThreeWindingsTransformers()), wb, titleCellStyle, "3WindingsTransformers", T3WT_MAPPERS);
        createSheet(Lists.newArrayList(network.getGenerators()), wb, titleCellStyle, "Generators", GENERATOR_MAPPERS);
        createSheet(Lists.newArrayList(network.getHvdcConverterStations()), wb, titleCellStyle, "HVDC converter stations", HVDC_MAPPERS);
        createSheet(Lists.newArrayList(network.getLoads()), wb, titleCellStyle, "Loads", LOAD_MAPPERS);
        createSheet(Lists.newArrayList(network.getShuntCompensators()), wb, titleCellStyle, "Shunts", SHUNT_MAPPERS);
        createSheet(Lists.newArrayList(network.getStaticVarCompensators()), wb, titleCellStyle, "Static VAR Compensators", SVC_MAPPERS);
    }

    public void generateXls(Path xsl) {
        try (OutputStream out = Files.newOutputStream(xsl)) {
            generateXls(out);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
    }

    public void generateXls(OutputStream out) {
        long start = System.currentTimeMillis();
        Workbook wb = new XSSFWorkbook();
        CellStyle titleCellStyle = wb.createCellStyle();
        titleCellStyle.setAlignment(HorizontalAlignment.CENTER);
        createSheets(wb, titleCellStyle);
        try {
            wb.write(out);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }
        LOGGER.info("XLS comparison file {}/{} generated in {} ms", network.getVariantManager().getWorkingVariantId(),
                otherState, System.currentTimeMillis() - start);
    }
}