Comparison.java

/**
 * Copyright (c) 2017-2018, 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.cgmes.conversion.test.network.compare;

import com.powsybl.cgmes.extensions.CgmesMetadataModels;
import com.powsybl.cgmes.extensions.CimCharacteristics;
import com.powsybl.cgmes.model.CgmesMetadataModel;
import com.powsybl.cgmes.model.CgmesSubset;
import com.powsybl.iidm.network.*;
import com.powsybl.iidm.network.ReactiveCapabilityCurve.Point;
import com.powsybl.iidm.network.extensions.*;

import java.util.*;
import java.util.function.BiConsumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * @author Luma Zamarre��o {@literal <zamarrenolm at aia.es>}
 */
public class Comparison {

    public Comparison(
            Network expected,
            Network actual,
            ComparisonConfig config) {
        this.expected = expected;
        this.actual = actual;
        this.config = config;
        this.networkMapping = config.networkMappingFactory.create(expected, actual);
        this.diff = config.differences;
    }

    public void compareBuses() {
        diff.current(expected);
        compareBuses(
                expected.getBusBreakerView().getBusStream(),
                actual.getBusBreakerView().getBusStream(),
                this::compareBuses);
    }

    public void compare() {
        diff.current(expected);
        if (config.checkNetworkId) {
            compare("networkId", expected.getId(), actual.getId());
        }

        // Compare CIM characteristics
        compareCIMCharacteristics(expected.getExtension(CimCharacteristics.class), actual.getExtension(CimCharacteristics.class));

        // Compare metadata
        compareCgmesMetadata(expected.getExtension(CgmesMetadataModels.class), actual.getExtension(CgmesMetadataModels.class));

        // TODO Consider other attributes of network (name, caseData, forecastDistance, ...)
        compare(
                expected.getSubstationStream(),
                actual.getSubstationStream(),
                this::compareSubstations);
        compare(
                expected.getVoltageLevelStream(),
                actual.getVoltageLevelStream(),
                this::compareVoltageLevels);
        compareBuses(
                expected.getBusBreakerView().getBusStream(),
                actual.getBusBreakerView().getBusStream(),
                this::compareBuses);
        compare(
                expected.getLoadStream(),
                actual.getLoadStream(),
                this::compareLoads);
        compare(
                expected.getShuntCompensatorStream(),
                actual.getShuntCompensatorStream(),
                this::compareShunts);
        compare(
                expected.getStaticVarCompensatorStream(),
                actual.getStaticVarCompensatorStream(),
                this::compareStaticVarCompensators);
        compare(
                expected.getGeneratorStream(),
                actual.getGeneratorStream(),
                this::compareGenerators);
        compare(
                expected.getSwitchStream(),
                actual.getSwitchStream(),
                this::compareSwitches);
        compare(
                expected.getLineStream(),
                actual.getLineStream(),
                this::testLines);
        compare(
                expected.getTwoWindingsTransformerStream(),
                actual.getTwoWindingsTransformerStream(),
                this::compareTwoWindingTransformers);
        compare(
                expected.getThreeWindingsTransformerStream(),
                actual.getThreeWindingsTransformerStream(),
                this::compareThreeWindingsTransformers);
        compare(
                expected.getDanglingLineStream(DanglingLineFilter.ALL).filter(dl -> !dl.isPaired()),
                actual.getDanglingLineStream(DanglingLineFilter.ALL).filter(dl -> !dl.isPaired()),
                this::compareDanglingLines);
        diff.end();
    }

    // The actual network does not contain unexpected identifiables
    // All identifiables in expected stream exist in actual network and have same attributes
    private <T extends Identifiable<T>> void compare(
            Stream<T> expecteds,
            Stream<T> actuals,
            BiConsumer<T, T> testAttributes) {
        actuals.forEach(actual -> {
            Identifiable expected = networkMapping.findExpected(actual);
            if (expected == null) {
                diff.unexpected(actual);
                return;
            }
            String context = className(actual);
            compare(context, expected.getClass(), actual.getClass());
        });
        expecteds.forEach(expected -> {
            Identifiable<?> actual = networkMapping.findActual(expected);
            if (actual == null) {
                diff.missing(expected);
                return;
            }
            diff.match(expected);
            diff.current(expected);
            String context = className(actual);
            compare(context, expected.getClass(), actual.getClass());
            context = context + ".name";
            compareNames(context, expected.getOptionalName().orElse(""), actual.getOptionalName().orElse(""));
            // Obtained identifiable in actual must be of type T
            @SuppressWarnings("unchecked")
            T tactual = (T) actual;
            testAttributes.accept((T) expected, tactual);
        });
    }

    private void compareCIMCharacteristics(CimCharacteristics expected, CimCharacteristics actual) {
        if (expected == null && actual != null) {
            diff.unexpected(actual.getExtendable().getId() + "_cimCharacteristics_extension");
            return;
        }
        if (expected != null) {
            if (actual == null) {
                diff.missing(expected.getExtendable().getId() + "_cimCharacteristics_extension");
                return;
            }
            compare("topologyKind", expected.getTopologyKind().toString(), actual.getTopologyKind().toString());
            compare("cimVersion", expected.getCimVersion(), actual.getCimVersion());
        }
    }

    private void compareCgmesMetadata(CgmesMetadataModels expected, CgmesMetadataModels actual) {
        if (expected == null && actual != null) {
            diff.unexpected(actual.getExtendable().getId() + "_cgmesMetadataModels_extension");
            return;
        }
        if (expected != null) {
            if (actual == null) {
                diff.missing(expected.getExtendable().getId() + "_cgmesMetadataModels_extension");
                return;
            }
            if (config.ignoreMissingMetadata) {
                // All actual models should be in expected models
                // But some expected models may be missing
                for (CgmesMetadataModel actualModel : actual.getModels()) {
                    Optional<CgmesMetadataModel> expectedModel = expected.getModelForSubsetModelingAuthoritySet(actualModel.getSubset(), actualModel.getModelingAuthoritySet());
                    if (expectedModel.isEmpty()) {
                        diff.unexpected(actual.getExtendable().getId() + "_cgmesMetadataModels_Model " + actualModel.getId());
                    } else {
                        compareCgmesMetadataModels(expectedModel.get(), actualModel);
                    }
                }
            } else {
                // If we are not ignoring models, the sorted models should be comparable one by one
                if (expected.getModels().size() != actual.getModels().size()) {
                    diff.compare("cgmesMetadataModels_size", expected.getModels().size(), actual.getModels().size());
                }
                List<CgmesMetadataModel> expectedModels = expected.getSortedModels();
                List<CgmesMetadataModel> actualModels = actual.getSortedModels();
                for (int k = 0; k < expectedModels.size(); k++) {
                    compareCgmesMetadataModels(expectedModels.get(k), actualModels.get(k));
                }
            }
        }
    }

    private void compareCgmesMetadataModels(CgmesMetadataModel expected, CgmesMetadataModel actual) {
        String prefix = "CgmesMetadataModel " + expected.getId() + "_";
        if (config.checkNetworkId) {
            compare(prefix + "id", expected.getId(), actual.getId());
        }
        compare(prefix + "subset", expected.getSubset(), actual.getSubset());
        compare(prefix + "description", expected.getDescription(), actual.getDescription());
        // If subset has not been exported, the version has not been incremented
        CgmesSubset subset = expected.getSubset();
        int expectedVersion = expected.getVersion();
        if (config.versionIncremented && config.isExportedSubset(subset)) {
            expectedVersion++;
        }
        compare(prefix + "version", expectedVersion, actual.getVersion());
        compare(prefix + "modelingAuthoritySet", expected.getModelingAuthoritySet(), actual.getModelingAuthoritySet());
        compare(prefix + "profiles", expected.getProfiles(), actual.getProfiles());
        compare(prefix + "dependentOn", expected.getDependentOn(), actual.getDependentOn());
        compare(prefix + "supersedes", expected.getSupersedes(), actual.getSupersedes());
    }

    void compare(String context, Set<String> expecteds, Set<String> actuals) {
        for (String expected : expecteds) {
            if (!actuals.contains(expected)) {
                diff.missing(context + ": " + expected);
            }
        }
        for (
            String actual : actuals) {
            if (!expecteds.contains(actual)) {
                diff.unexpected(context + ": " + actual);
            }
        }
    }

    // Buses in bus breaker view are not inserted in the index for Network Identifiables
    // We prepare an index external to the network for comparing the two lists
    private void compareBuses(
            Stream<Bus> expecteds,
            Stream<Bus> actuals,
            BiConsumer<Bus, Bus> testAttributes) {
        Map<String, Bus> actualsById = new HashMap<>();
        actuals.forEach(b -> actualsById.put(b.getId(), b));
        Map<String, Bus> expectedsById = new HashMap<>();
        expecteds.forEach(b -> expectedsById.put(b.getId(), b));

        actualsById.values().forEach(actual -> {
            Bus expected = expectedsById.get(actual.getId());
            if (expected == null) {
                diff.unexpected(actual);
                return;
            }
            String context = className(actual);
            compare(context, expected.getClass(), actual.getClass());
        });
        expectedsById.values().forEach(expected -> {
            Bus actual = actualsById.get(expected.getId());
            if (actual == null) {
                diff.missing(expected);
                return;
            }
            diff.match(expected);
            diff.current(expected);
            String context = className(actual);
            compare(context, expected.getClass(), actual.getClass());
            context = context + ".name";
            compareNames(context, expected.getOptionalName().orElse(""), actual.getOptionalName().orElse(""));
            testAttributes.accept((Bus) expected, actual);
        });
    }

    private void compareSubstations(Substation expected, Substation actual) {
        compare("country", expected.getCountry(), actual.getCountry());
        Set<String> mappedActualGeoTags = actual.getGeographicalTags().stream()
                .map(networkMapping::applyPrefixToActual)
                .collect(Collectors.toSet());
        compare("geographicalTags",
                expected.getGeographicalTags(),
                mappedActualGeoTags);
    }

    private void compareVoltageLevels(VoltageLevel expected, VoltageLevel actual) {
        equivalent("Substation", expected.getSubstation().orElse(null), actual.getSubstation().orElse(null));
        compare("nominalV", expected.getNominalV(), actual.getNominalV());
        if (config.checkVoltageLevelLimits) {
            compare("lowVoltageLimit",
                    expected.getLowVoltageLimit(),
                    actual.getLowVoltageLimit());
            compare("highVoltageLimit",
                    expected.getHighVoltageLimit(),
                    actual.getHighVoltageLimit());
        }
        SlackTerminal expectedSlackTerminal = expected.getExtension(SlackTerminal.class);
        SlackTerminal actualSlackTerminal = actual.getExtension(SlackTerminal.class);
        if (expectedSlackTerminal == null) {
            if (actualSlackTerminal != null) {
                diff.unexpected("slackTerminal");
            }
        } else {
            if (actualSlackTerminal == null) {
                diff.missing("slackTerminal");
            } else {
                equivalent("slackTerminal", expectedSlackTerminal.getTerminal().getConnectable(),
                        actualSlackTerminal.getTerminal().getConnectable());
            }
        }
    }

    private void compareBuses(Bus expected, Bus actual) {
        equivalent("VoltageLevel", expected.getVoltageLevel(), actual.getVoltageLevel());
        compareAliases(expected, actual);
        compare("v", expected.getV(), actual.getV());
        compare("angle", expected.getAngle(), actual.getAngle());
    }

    private void compareLoads(Load expected, Load actual) {
        equivalent("VoltageLevel",
                expected.getTerminal().getVoltageLevel(),
                actual.getTerminal().getVoltageLevel());
        compareAliases(expected, actual);
        compare("p0", expected.getP0(), actual.getP0());
        compare("q0", expected.getQ0(), actual.getQ0());
        // TODO Should we check terminals ? (we are not setting terminal id)
        compare("p", expected.getTerminal().getP(), actual.getTerminal().getP());
        compare("q", expected.getTerminal().getQ(), actual.getTerminal().getQ());
        compareLoadDetails(expected.getExtension(LoadDetail.class), actual.getExtension(LoadDetail.class));
    }

    private void compareLoadDetails(LoadDetail expected, LoadDetail actual) {
        if (expected == null) {
            if (actual != null) {
                diff.unexpected("expected conform or not conform load (is energyConsumer)");
                return;
            }
            return;
        }
        if (actual == null) {
            diff.unexpected("expected energyConsumer (is conform or not conform load)");
            return;
        }
        diff.compare("fixedActivePower", expected.getFixedActivePower(), actual.getFixedActivePower());
        diff.compare("fixedReactivePower", expected.getFixedReactivePower(), actual.getFixedReactivePower());
        diff.compare("variableActivePower", expected.getVariableActivePower(), actual.getVariableActivePower());
        diff.compare("variableReactivePower", expected.getVariableReactivePower(), actual.getVariableReactivePower());
    }

    private void compareShunts(ShuntCompensator expected, ShuntCompensator actual) {
        equivalent("VoltageLevel",
                expected.getTerminal().getVoltageLevel(),
                actual.getTerminal().getVoltageLevel());
        compareAliases(expected, actual);
        compare("sectionCount",
                expected.getSectionCount(),
                actual.getSectionCount());
        compare("voltageRegulationOn",
                expected.isVoltageRegulatorOn(),
                actual.isVoltageRegulatorOn());
        compare("targetV",
                expected.getTargetV(),
                actual.getTargetV());
        compare("targetDeadband",
                expected.getTargetDeadband(),
                actual.getTargetDeadband());
        sameIdentifier("regulationTerminal",
                expected.getRegulatingTerminal().getBusBreakerView().getBus(),
                actual.getRegulatingTerminal().getBusBreakerView().getBus());
        compareShuntModels(expected, actual);
    }

    private void compareShuntModels(ShuntCompensator expected, ShuntCompensator actual) {
        switch (expected.getModelType()) {
            case LINEAR:
                compare("maximumSectionCount",
                        expected.getMaximumSectionCount(),
                        actual.getMaximumSectionCount());
                compare("bPerSection",
                        expected.getModel(ShuntCompensatorLinearModel.class).getBPerSection(),
                        actual.getModel(ShuntCompensatorLinearModel.class).getBPerSection());
                compare("gPerSection",
                        expected.getModel(ShuntCompensatorLinearModel.class).getGPerSection(),
                        actual.getModel(ShuntCompensatorLinearModel.class).getGPerSection());
                break;
            case NON_LINEAR:
                ShuntCompensatorNonLinearModel expectedModel = expected.getModel(ShuntCompensatorNonLinearModel.class);
                ShuntCompensatorNonLinearModel actualModel = actual.getModel(ShuntCompensatorNonLinearModel.class);
                if (expectedModel.getAllSections().size() > actualModel.getAllSections().size()) {
                    for (int i = actualModel.getAllSections().size(); i < expectedModel.getAllSections().size(); i++) {
                        diff.missing("section" + i);
                    }
                }
                if (expectedModel.getAllSections().size() < actualModel.getAllSections().size()) {
                    for (int i = expectedModel.getAllSections().size(); i < actualModel.getAllSections().size(); i++) {
                        diff.unexpected("section" + i);
                    }
                }
                List<ShuntCompensatorNonLinearModel.Section> expectedSections = expectedModel.getAllSections();
                List<ShuntCompensatorNonLinearModel.Section> actualSections = actualModel.getAllSections();
                for (int i = 0; i < expectedSections.size(); i++) {
                    ShuntCompensatorNonLinearModel.Section expectedSection = expectedSections.get(i);
                    ShuntCompensatorNonLinearModel.Section actualSection = actualSections.get(i);
                    compare("section" + i + 1 + ".b", expectedSection.getB(), actualSection.getB());
                    compare("section" + i + 1 + ".g", expectedSection.getG(), actualSection.getG());
                }
                break;
            default:
                throw new IllegalStateException("Unexpected shunt model type: " + expected.getModelType());
        }
    }

    private void compareStaticVarCompensators(
            StaticVarCompensator expected,
            StaticVarCompensator actual) {
        equivalent("VoltageLevel",
                expected.getTerminal().getVoltageLevel(),
                actual.getTerminal().getVoltageLevel());
        compareAliases(expected, actual);
        compare("Bmin",
                expected.getBmin(),
                actual.getBmin());
        compare("Bmax",
                expected.getBmax(),
                actual.getBmax());
        compare("voltageSetpoint",
                expected.getVoltageSetpoint(),
                actual.getVoltageSetpoint());
        compare("reactivePowerSetpoint",
                expected.getReactivePowerSetpoint(),
                actual.getReactivePowerSetpoint());
        compare("regulationMode",
                expected.getRegulationMode(),
                actual.getRegulationMode());
        sameIdentifier("regulationTerminal",
                expected.getRegulatingTerminal().getBusBreakerView().getBus(),
                actual.getRegulatingTerminal().getBusBreakerView().getBus());
    }

    private void compareGenerators(Generator expected, Generator actual) {
        equivalent("VoltageLevel",
                expected.getTerminal().getVoltageLevel(),
                actual.getTerminal().getVoltageLevel());
        compareAliases(expected, actual);
        sameIdentifier("ConnectableBus",
                expected.getTerminal().getBusBreakerView().getConnectableBus(),
                actual.getTerminal().getBusBreakerView().getConnectableBus());
        Bus be = expected.getTerminal().getBusBreakerView().getBus();
        Bus ba = actual.getTerminal().getBusBreakerView().getBus();
        if (be == null) {
            if (ba != null) {
                diff.unexpected(ba);
                return;
            }
        } else {
            if (ba == null) {
                diff.missing(be);
                return;
            }
            sameIdentifier("Bus", be, ba);
        }
        compare("minP", expected.getMinP(), actual.getMinP());
        compare("maxP", expected.getMaxP(), actual.getMaxP());
        compareGeneratorReactiveLimits(expected.getReactiveLimits(), actual.getReactiveLimits());
        compare("targetP", expected.getTargetP(), actual.getTargetP());
        compare("targetQ", expected.getTargetQ(), actual.getTargetQ());
        compare("targetV", expected.getTargetV(), actual.getTargetV());
        compare("isVoltageRegulatorOn",
                expected.isVoltageRegulatorOn(),
                actual.isVoltageRegulatorOn());
        if (config.checkGeneratorRegulatingTerminal
                && (expected.getRegulatingTerminal() != null
                || actual.getRegulatingTerminal() != null)) {
            sameIdentifier("RegulatingTerminalBus",
                    expected.getRegulatingTerminal().getBusBreakerView().getBus(),
                    actual.getRegulatingTerminal().getBusBreakerView().getBus());
        }

        compare("energySource", expected.getEnergySource(), actual.getEnergySource());
        compare("ratedS", expected.getRatedS(), actual.getRatedS());
        compare("terminalP", expected.getTerminal().getP(), actual.getTerminal().getP());
        compare("terminalQ", expected.getTerminal().getQ(), actual.getTerminal().getQ());
        compareQPercents(expected.getExtension(CoordinatedReactiveControl.class), actual.getExtension(CoordinatedReactiveControl.class));
    }

    private void compareQPercents(CoordinatedReactiveControl expected, CoordinatedReactiveControl actual) {
        if (expected == null) {
            if (actual != null) {
                diff.unexpected("qPercent");
                return;
            }
            return;
        }
        if (actual == null) {
            diff.unexpected("qPercent");
            return;
        }
        diff.compare("qPercent", expected.getQPercent(), actual.getQPercent());
    }

    private void compareGeneratorReactiveLimits(ReactiveLimits expected, ReactiveLimits actual) {
        switch (expected.getKind()) {
            case MIN_MAX:
                compareGeneratorMinMaxReactiveLimits(
                        (MinMaxReactiveLimits) expected,
                        (MinMaxReactiveLimits) actual);
                break;

            case CURVE:
                if (config.checkGeneratorReactiveCapabilityCurve) {
                    compareGeneratorReactiveCapabilityCurve(
                            (ReactiveCapabilityCurve) expected,
                            (ReactiveCapabilityCurve) actual);
                }
                break;

            default:
                throw new IllegalStateException("Unexpected ReactiveLimitsKing value: " + expected.getKind());
        }
    }

    private void compareGeneratorMinMaxReactiveLimits(
            MinMaxReactiveLimits expected,
            MinMaxReactiveLimits actual) {
        compare("minQ", expected.getMinQ(), actual.getMinQ());
        compare("maxQ", expected.getMaxQ(), actual.getMaxQ());
    }

    private void compareGeneratorReactiveCapabilityCurve(
            ReactiveCapabilityCurve expected,
            ReactiveCapabilityCurve actual) {
        // From the IIDM API we don't know if the collection of points is sorted,
        // so we sort points by active power, then compare resulting lists point by point
        Comparator<Point> comparePoints = (p0, p1) -> Double.compare(p0.getP(), p1.getP());
        List<Point> e = expected.getPoints().stream().sorted(comparePoints)
                .toList();
        List<Point> a = actual.getPoints().stream().sorted(comparePoints)
                .toList();
        compare("reactiveCapabilityCurve.size", e.size(), a.size());
        for (int k = 0; k < e.size(); k++) {
            Point pe = e.get(k);
            Point pa = a.get(k);
            compare("reactiveCapabilityCurvePoint.p", pe.getP(), pa.getP());
            compare("reactiveCapabilityCurvePoint.minQ", pe.getMinQ(), pa.getMinQ());
            compare("reactiveCapabilityCurvePoint.maxQ", pe.getMaxQ(), pa.getMaxQ());
        }
    }

    private void compareSwitches(Switch expected, Switch actual) {
        equivalent("VoltageLevel", expected.getVoltageLevel(), actual.getVoltageLevel());
        compareAliases(expected, actual);
        // No additional properties to check
    }

    private void testLines(Line expected, Line actual) {
        equivalent("VoltageLevel1",
                expected.getTerminal1().getVoltageLevel(),
                actual.getTerminal1().getVoltageLevel());
        equivalent("VoltageLevel2",
                expected.getTerminal2().getVoltageLevel(),
                actual.getTerminal2().getVoltageLevel());
        compareAliases(expected, actual);
        compare("r", expected.getR(), actual.getR());
        compare("x", expected.getX(), actual.getX());
        compare("g1", expected.getG1(), actual.getG1());
        compare("b1", expected.getB1(), actual.getB1());
        compare("g2", expected.getG2(), actual.getG2());
        compare("b2", expected.getB2(), actual.getB2());
        compareLoadingLimits(expected, actual,
                expected.getActivePowerLimits1().orElse(null),
                actual.getActivePowerLimits1().orElse(null));
        compareLoadingLimits(expected, actual,
                expected.getApparentPowerLimits1().orElse(null),
                actual.getApparentPowerLimits1().orElse(null));
        compareLoadingLimits(expected, actual,
                expected.getCurrentLimits1().orElse(null),
                actual.getCurrentLimits1().orElse(null));
        compareLoadingLimits(expected, actual,
                expected.getActivePowerLimits2().orElse(null),
                actual.getActivePowerLimits2().orElse(null));
        compareLoadingLimits(expected, actual,
                expected.getApparentPowerLimits2().orElse(null),
                actual.getApparentPowerLimits2().orElse(null));
        compareLoadingLimits(expected, actual,
                expected.getCurrentLimits2().orElse(null),
                actual.getCurrentLimits2().orElse(null));
    }

    private void compareDanglingLines(DanglingLine expected, DanglingLine actual) {
        equivalent("VoltageLevel",
                expected.getTerminal().getVoltageLevel(),
                actual.getTerminal().getVoltageLevel());
        compareAliases(expected, actual);
        compare("r", expected.getR(), actual.getR());
        compare("x", expected.getX(), actual.getX());
        compare("g", expected.getG(), actual.getG());
        compare("b", expected.getB(), actual.getB());
        compare("p0", expected.getP0(), actual.getP0());
        compare("q0", expected.getQ0(), actual.getQ0());
        compare("pairingKey", expected.getPairingKey(), actual.getPairingKey());
        compareLoadingLimits(expected, actual,
                expected.getActivePowerLimits().orElse(null),
                actual.getActivePowerLimits().orElse(null));
        compareLoadingLimits(expected, actual,
                expected.getApparentPowerLimits().orElse(null),
                actual.getApparentPowerLimits().orElse(null));
        compareLoadingLimits(expected, actual,
                expected.getCurrentLimits().orElse(null),
                actual.getCurrentLimits().orElse(null));
    }

    private void compareLoadingLimits(
            Identifiable bexpected,
            Identifiable bactual,
            LoadingLimits expected,
            LoadingLimits actual) {
        if (expected == null) {
            if (actual != null) {
                diff.unexpected(bactual);
                return;
            }
        } else {
            if (actual == null) {
                diff.missing(bexpected);
                return;
            }
            compare("permanentLimit", expected.getPermanentLimit(), actual.getPermanentLimit());
            compareTemporaryLimits(bactual, expected.getTemporaryLimits(), actual.getTemporaryLimits());
        }
    }

    private void compareTwoWindingTransformers(TwoWindingsTransformer expected,
                                               TwoWindingsTransformer actual) {
        equivalent("VoltageLevel1",
                expected.getTerminal1().getVoltageLevel(),
                actual.getTerminal1().getVoltageLevel());
        equivalent("VoltageLevel2",
                expected.getTerminal2().getVoltageLevel(),
                actual.getTerminal2().getVoltageLevel());
        compareAliases(expected, actual);
        compare("r", expected.getR(), actual.getR());
        compare("x", expected.getX(), actual.getX());
        compare("g", expected.getG(), actual.getG());
        compare("b", expected.getB(), actual.getB());
        compare("ratedU1", expected.getRatedU1(), actual.getRatedU1());
        compare("ratedU2", expected.getRatedU2(), actual.getRatedU2());
        compareLoadingLimits(expected, actual,
                expected.getActivePowerLimits1().orElse(null),
                actual.getActivePowerLimits1().orElse(null));
        compareLoadingLimits(expected, actual,
                expected.getApparentPowerLimits1().orElse(null),
                actual.getApparentPowerLimits1().orElse(null));
        compareLoadingLimits(expected, actual,
                expected.getCurrentLimits1().orElse(null),
                actual.getCurrentLimits1().orElse(null));
        compareLoadingLimits(expected, actual,
                expected.getActivePowerLimits2().orElse(null),
                actual.getActivePowerLimits2().orElse(null));
        compareLoadingLimits(expected, actual,
                expected.getApparentPowerLimits2().orElse(null),
                actual.getApparentPowerLimits2().orElse(null));
        compareLoadingLimits(expected, actual,
                expected.getCurrentLimits2().orElse(null),
                actual.getCurrentLimits2().orElse(null));

        compareRatioTapChanger(expected.getRatioTapChanger(), actual.getRatioTapChanger());
        comparePhaseTapChanger(expected.getPhaseTapChanger(), actual.getPhaseTapChanger());
        comparePhaseAngleClock2(expected.getExtension(TwoWindingsTransformerPhaseAngleClock.class), actual.getExtension(TwoWindingsTransformerPhaseAngleClock.class));
    }

    private void comparePhaseAngleClock2(TwoWindingsTransformerPhaseAngleClock expected, TwoWindingsTransformerPhaseAngleClock actual) {
        if (expected == null && actual == null) {
            return;
        } else if (expected == null && actual != null) {
            diff.unexpected("phaseAngleClock2wt");
            return;
        } else if (expected != null && actual == null) {
            diff.unexpected("phaseAngleClock2wt");
            return;
        } else {
            diff.compare("phaseAngleClock", expected.getPhaseAngleClock(), actual.getPhaseAngleClock());
        }
    }

    private void compareThreeWindingsTransformers(ThreeWindingsTransformer expected,
                                                  ThreeWindingsTransformer actual) {
        compareAliases(expected, actual);
        compareLeg(expected.getLeg1(), actual.getLeg1(), expected, actual);
        compareLeg(expected.getLeg2(), actual.getLeg2(), expected, actual);
        compareLeg(expected.getLeg3(), actual.getLeg3(), expected, actual);
        comparePhaseAngleClock3(expected.getExtension(ThreeWindingsTransformerPhaseAngleClock.class), actual.getExtension(ThreeWindingsTransformerPhaseAngleClock.class));
    }

    private void comparePhaseAngleClock3(ThreeWindingsTransformerPhaseAngleClock expected, ThreeWindingsTransformerPhaseAngleClock actual) {
        if (expected == null && actual == null) {
            return;
        } else if (expected == null && actual != null) {
            diff.unexpected("phaseAngleClock3wt");
            return;
        } else if (expected != null && actual == null) {
            diff.unexpected("phaseAngleClock3wt");
            return;
        } else {
            diff.compare("phaseAngleClockLeg2", expected.getPhaseAngleClockLeg2(), actual.getPhaseAngleClockLeg2());
            diff.compare("phaseAngleClockLeg3", expected.getPhaseAngleClockLeg3(), actual.getPhaseAngleClockLeg3());
        }
    }

    private void compareLeg(ThreeWindingsTransformer.Leg expected, ThreeWindingsTransformer.Leg actual,
                            ThreeWindingsTransformer expectedt, ThreeWindingsTransformer actualt) {
        equivalent("VoltageLevel",
                expected.getTerminal().getVoltageLevel(),
                actual.getTerminal().getVoltageLevel());
        compare("r", expected.getR(), actual.getR());
        compare("x", expected.getX(), actual.getX());
        compare("g", expected.getG(), actual.getG());
        compare("b", expected.getB(), actual.getB());

        compare("ratedU", expected.getRatedU(), actual.getRatedU());
        compareLoadingLimits(expectedt, actualt,
                expected.getActivePowerLimits().orElse(null),
                actual.getActivePowerLimits().orElse(null));
        compareLoadingLimits(expectedt, actualt,
                expected.getApparentPowerLimits().orElse(null),
                actual.getApparentPowerLimits().orElse(null));
        compareLoadingLimits(expectedt, actualt,
                expected.getCurrentLimits().orElse(null),
                actual.getCurrentLimits().orElse(null));
        compareRatioTapChanger(expected.getRatioTapChanger(), actual.getRatioTapChanger());
        comparePhaseTapChanger(expected.getPhaseTapChanger(), actual.getPhaseTapChanger());
    }

    private void compareRatioTapChanger(
            RatioTapChanger expected,
            RatioTapChanger actual) {
        compareTapChanger(expected, actual, this::compareRatioTapChangerStep);
        if (expected == null) {
            return;
        }
        compare("ratioTapChanger.hasLoadTapChangingCapabilities",
                expected.hasLoadTapChangingCapabilities(),
                actual.hasLoadTapChangingCapabilities());
        compare("ratioTapChanger.targetV", expected.getTargetV(), actual.getTargetV());
    }

    private void comparePhaseTapChanger(
            PhaseTapChanger expected,
            PhaseTapChanger actual) {
        compareTapChanger(expected, actual, this::comparePhaseTapChangerStep);
        if (expected == null) {
            return;
        }
        compare("phaseTapChanger.regulationMode",
                expected.getRegulationMode(),
                actual.getRegulationMode());
        compare("phaseTapChanger.regulationValue",
                expected.getRegulationValue(),
                actual.getRegulationValue());
    }

    private <TC extends TapChanger<TC, TCS, ?, ?>, TCS extends TapChangerStep<TCS>> void compareTapChanger(
            TapChanger<TC, TCS, ?, ?> expected,
            TapChanger<TC, TCS, ?, ?> actual,
            BiConsumer<TCS, TCS> testTapChangerStep1) {
        if (expected == null) {
            if (actual != null) {
                diff.unexpected("TapChanger");
                return;
            }
        } else {
            if (actual == null) {
                diff.missing("TapChanger");
                return;
            }
            compare("tapChanger.lowTapPosition",
                    expected.getLowTapPosition(),
                    actual.getLowTapPosition());
            compare("tapChanger.highTapPosition",
                    expected.getHighTapPosition(),
                    actual.getHighTapPosition());
            compare("tapChanger.tapPosition",
                    expected.getTapPosition(),
                    actual.getTapPosition());
            compare("tapChanger.targetDeadband",
                    expected.getTargetDeadband(),
                    actual.getTargetDeadband());
            compare("tapChanger.stepCount", expected.getStepCount(), actual.getStepCount());
            // Check steps
            for (int k = expected.getLowTapPosition(); k <= expected.getHighTapPosition(); k++) {
                TCS stepExpected = expected.getStep(k);
                TCS stepActual = actual.getStep(k);
                compareTapChangerStep(stepExpected, stepActual, testTapChangerStep1);
            }
            // Check regulation
            compare("tapChanger.isRegulating", expected.isRegulating(),
                    actual.isRegulating());
            if (expected.getRegulationTerminal() == null
                    || actual.getRegulationTerminal() == null) {
                // TODO We are not checking regulation terminals if one of them is null
            } else {
                equivalent(
                        "tapChanger.RegulationTerminalConnectable",
                        expected.getRegulationTerminal().getConnectable(),
                        actual.getRegulationTerminal().getConnectable());
            }

            if (expected.getRegulationTerminal() != null || actual.getRegulationTerminal() != null) {
                if (expected.getRegulationTerminal() == null) {
                    diff.unexpected("Tap changer regulating terminal");
                    return;
                }
                if (actual.getRegulationTerminal() == null) {
                    diff.missing("TapChanger regulating terminal");
                    return;
                }
                sameIdentifier("tapChanger.getRegulationTerminal",
                        expected.getRegulationTerminal().getBusBreakerView().getBus(),
                        actual.getRegulationTerminal().getBusBreakerView().getBus());
            }
        }
    }

    private <TC extends TapChanger<TC, TCS, ?, ?>, TCS extends TapChangerStep<TCS>> void compareTapChangerStep(
            TCS expected,
            TCS actual,
            BiConsumer<TCS, TCS> testTapChangerStep1) {
        compare("tapChangerStep.r", expected.getR(), actual.getR());
        compare("tapChangerStep.x", expected.getX(), actual.getX());
        compare("tapChangerStep.g", expected.getG(), actual.getG());
        compare("tapChangerStep.b", expected.getB(), actual.getB());
        compare("tapChangerStep.rho", expected.getRho(), actual.getRho());
        if (testTapChangerStep1 != null) {
            testTapChangerStep1.accept(expected, actual);
        }
    }

    private void compareRatioTapChangerStep(RatioTapChangerStep expected, RatioTapChangerStep actual) {
        // No additional attributes to test
    }

    private void comparePhaseTapChangerStep(PhaseTapChangerStep expected, PhaseTapChangerStep actual) {
        compare("phaseTapChangerStep.alpha", expected.getAlpha(), actual.getAlpha());
    }

    //

    private void compareNames(String context, String expected, String actual) {
        // The names could be different only in trailing whitespace
        String expected1 = expected.trim();
        String actual1 = actual.trim();
        if (config.compareNamesAllowSuffixes) {
            int endIndex = Math.min(expected.length(), actual.length());
            compare(context,
                    expected1.substring(0, endIndex),
                    actual1.substring(0, endIndex));
        } else {
            compare(context, expected1, actual1);
        }
    }

    private void compareTemporaryLimits(Identifiable bactual,
                                        Collection<LoadingLimits.TemporaryLimit> expected,
                                        Collection<LoadingLimits.TemporaryLimit> actual) {
        if (expected.size() != actual.size()) {
            diff.unexpected(bactual);
            return;
        }
        Iterator<CurrentLimits.TemporaryLimit> actualIt = actual.iterator();
        for (CurrentLimits.TemporaryLimit e : expected) {
            CurrentLimits.TemporaryLimit a = actualIt.next();
            diff.compare("temporaryLimit", e.getName(), a.getName());
            diff.compare("temporaryLimit", e.getAcceptableDuration(), a.getAcceptableDuration());
            diff.compare("temporaryLimit", e.getValue(), a.getValue());
        }
    }

    private void compare(String context, double expected, double actual) {
        diff.compare(context, expected, actual, config.tolerance);
    }

    private void compare(String context, Object expected, Object actual) {
        diff.compare(context, expected, actual);
    }

    private <I extends Identifiable<I>> void compareAliases(I expected, I actual) {
        for (String alias : expected.getAliases()) {
            if (!actual.getAliases().contains(alias)) {
                diff.missing(alias);
                break;
            }
            Optional<String> type = expected.getAliasType(alias);
            if (!type.isPresent()) {
                actual.getAliasType(alias).ifPresent(diff::unexpected);
            } else {
                if (!actual.getAliasType(alias).isPresent()) {
                    diff.missing(type.get());
                }
                compare("alias", type.get(), actual.getAliasType(alias).get());
            }
        }
        for (String alias : actual.getAliases()) {
            if (!expected.getAliases().contains(alias)) {
                diff.unexpected(alias);
            }
        }
    }

    private void equivalent(
            String context,
            Identifiable expected,
            Identifiable actual) {
        if (!networkMapping.equivalent(expected, actual)) {
            diff.notEquivalent(context, expected, actual);
        }
    }

    private void sameIdentifier(
            String context,
            Identifiable expected,
            Identifiable actual) {
        boolean sameIdentifier;
        if (expected == null) {
            sameIdentifier = actual == null;
        } else if (actual == null) {
            sameIdentifier = false;
        } else {
            sameIdentifier = expected.getId().equals(actual.getId());
        }
        if (!sameIdentifier) {
            diff.notSameIdentifier(context, expected, actual);
        }
    }

    public static String className(Identifiable o) {
        String s = o.getClass().getName();
        int dot = s.lastIndexOf('.');
        if (dot >= 0) {
            s = s.substring(dot + 1);
        }
        s = s.replace("Impl", "");
        return s;
    }

    private final Network expected;
    private final Network actual;
    private final NetworkMapping networkMapping;
    private final Differences diff;
    private final ComparisonConfig config;
}