From 1c609de6545290577a8a70cf7a8cedec76d33646 Mon Sep 17 00:00:00 2001
From: Timo Koch <timo.koch@iws.uni-stuttgart.de>
Date: Mon, 2 Jan 2023 17:51:42 +0100
Subject: [PATCH] [test] Use the fieldcompare library if available for fuzzy
 comparisons

---
 bin/testing/runtest.py | 235 ++++++++++++++++++++++++++++++++---------
 1 file changed, 187 insertions(+), 48 deletions(-)

diff --git a/bin/testing/runtest.py b/bin/testing/runtest.py
index dc18f84b3f..7d32a0e53f 100755
--- a/bin/testing/runtest.py
+++ b/bin/testing/runtest.py
@@ -11,8 +11,127 @@ import os
 import sys
 import subprocess
 import json
-from fuzzycomparevtu import compareVTK
-from fuzzycomparedata import compareData
+
+
+try:
+    import numpy as np
+    import fieldcompare.mesh as meshcompare
+    import fieldcompare.tabular as tabularcompare
+    from fieldcompare import FieldDataComparator, protocols, DefaultFieldComparisonCallback
+    from fieldcompare.mesh import MeshFieldsComparator
+    from fieldcompare.predicates import FuzzyEquality
+    from fieldcompare.io import CSVFieldReader, read
+
+    protocols.MeshFields = meshcompare.MeshFields
+    protocols.TabularFields = tabularcompare.TabularFields
+
+    def makePredicateSelector(
+        relThreshold,
+        absThreshold,
+        zeroValueThreshold,
+        sourceFieldNameTransform=lambda name: name,
+    ):
+        """Create a predicate selector for fieldcompare emulates the Dumux behaviour"""
+
+        def _selector(
+            sourceField: protocols.Field, referenceField: protocols.Field
+        ) -> protocols.Predicate:
+            sourceFieldName = sourceFieldNameTransform(sourceField.name)
+            magnitude = np.max(np.abs(referenceField.values))
+            _absThreshold = max(
+                float(zeroValueThreshold.get(sourceFieldName, 0.0)), magnitude * absThreshold
+            )
+            return FuzzyEquality(abs_tol=_absThreshold, rel_tol=relThreshold)
+
+        return _selector
+
+    def fieldcompareMeshData(
+        source, ref, absThreshold=0.0, relThreshold=1e-7, zeroValueThreshold=None
+    ):
+        """Compares mesh data with the fieldcompare library"""
+
+        print(f"-- Comparing {source} and {ref}")
+
+        zeroValueThreshold = zeroValueThreshold or {}
+        if zeroValueThreshold:
+            print(f"-- Using the following absolute thresholds: {zeroValueThreshold}")
+
+        # read the files
+        sourceFields = read(source)
+        referenceFields = read(ref)
+
+        # some type checking to be sure we are comparing meshes
+        if not isinstance(sourceFields, protocols.MeshFields):
+            raise IOError("Source file could not been identified as mesh file!")
+        if not isinstance(referenceFields, protocols.MeshFields):
+            raise IOError("Reference file could not been identified as mesh file!")
+
+        # hard-code some values for the mesh comparisons (as for Dumux legacy backend)
+        sourceFields.domain.set_tolerances(abs_tol=1e-2, rel_tol=1.5e-7)
+        referenceFields.domain.set_tolerances(abs_tol=1e-2, rel_tol=1.5e-7)
+
+        compare = MeshFieldsComparator(source=sourceFields, reference=referenceFields)
+        result = compare(
+            predicate_selector=makePredicateSelector(
+                relThreshold, absThreshold, zeroValueThreshold
+            ),
+            fieldcomp_callback=DefaultFieldComparisonCallback(verbosity=1),
+            reordering_callback=lambda msg: print(f"-- {msg}"),
+        )
+
+        print(f"-- Summary: {result.status} ({result.report})\n")
+
+        if not result:
+            return 1
+        return 0
+
+    # pylint: disable=too-many-arguments
+    def fieldcompareCSVData(
+        source, ref, delimiter, absThreshold=0.0, relThreshold=1e-7, zeroValueThreshold=None
+    ):
+        """Compares CSV data with the fieldcompare library"""
+
+        print(f"-- Comparing {source} and {ref}")
+
+        zeroValueThreshold = zeroValueThreshold or {}
+        if zeroValueThreshold:
+            print(f"-- Using the following absolute thresholds: {zeroValueThreshold}")
+
+        sourceFields = CSVFieldReader(delimiter=delimiter, use_names=False).read(source)
+        referenceFields = CSVFieldReader(delimiter=delimiter, use_names=False).read(ref)
+
+        # some type checking to be sure we are comparing CSV data
+        if not isinstance(sourceFields, protocols.TabularFields):
+            raise IOError("Source file could not been identified as CSV-like file!")
+        if not isinstance(referenceFields, protocols.TabularFields):
+            raise IOError("Reference file could not been identified as CSV-like file!")
+
+        compare = FieldDataComparator(source=sourceFields, reference=referenceFields)
+        result = compare(
+            predicate_selector=makePredicateSelector(
+                relThreshold,
+                absThreshold,
+                zeroValueThreshold,
+                lambda name: f"row {float(name.strip('field_'))}",
+            ),
+            fieldcomp_callback=DefaultFieldComparisonCallback(verbosity=1),
+        )
+
+        print(f"-- Summary: {result.status} ({result.report})\n")
+
+        if not result:
+            return 1
+        return 0
+
+    BACKEND = "fieldcompare"
+
+
+# fall back to Dumux legacy backend if we don't have fieldcompare
+except ImportError:
+    from fuzzycomparevtu import compareVTK as fieldcompareMeshData
+    from fuzzycomparedata import compareData as fieldcompareCSVData
+
+    BACKEND = "legacy"
 
 
 def readCmdParameters():
@@ -98,69 +217,89 @@ def readCmdParameters():
     return args
 
 
+def _exactComparison(args):
+    """Exact comparison driver"""
+    returnCode = 0
+    for i in range(0, len(args["files"]) // 2):
+        print("\nExact comparison...")
+        result = subprocess.call(["diff", args["files"][i * 2], args["files"][(i * 2) + 1]])
+        if result:
+            returnCode = 1
+    return returnCode
+
+
+def _fuzzyMeshComparison(args):
+    """Fuzzy mesh comparison driver"""
+    numFailed = 0
+    for i in range(0, len(args["files"]) // 2):
+        print(f"\nFuzzy data comparison with {BACKEND} backend")
+        source, ref = args["files"][i * 2], args["files"][(i * 2) + 1]
+        if "reference" in source and "reference" not in ref:
+            source, ref = ref, source
+        relThreshold = args["relative"]
+        absThreshold = args["absolute"]
+        zeroValueThreshold = args["zeroThreshold"]
+        numFailed += fieldcompareMeshData(
+            source, ref, absThreshold, relThreshold, zeroValueThreshold
+        )
+
+    return int(numFailed > 0)
+
+
+def _fuzzyDataComparison(args):
+    """Fuzzy data comparison driver"""
+    numFailed = 0
+    for i in range(0, len(args["files"]) // 2):
+        print(f"\nFuzzy data comparison with {BACKEND} backend")
+        source, ref = args["files"][i * 2], args["files"][(i * 2) + 1]
+        if "reference" in source and "reference" not in ref:
+            source, ref = ref, source
+        delimiter = args["delimiter"]
+        relThreshold = args["relative"]
+        absThreshold = args["absolute"]
+        zeroValueThreshold = args["zeroThreshold"]
+        numFailed += fieldcompareCSVData(
+            source, ref, delimiter, absThreshold, relThreshold, zeroValueThreshold
+        )
+
+    return int(numFailed > 0)
+
+
+def _scriptComparison(args):
+    """Script comparison driver"""
+    returnCode = 0
+    for i in range(0, len(args["files"]) // 2):
+        print(f"\n{args['script']} comparison")
+        result = subprocess.call(args["script"], args["files"][i * 2], args["files"][(i * 2) + 1])
+        if result:
+            returnCode = 1
+    return returnCode
+
+
 def runRegressionTest(args):
     """Run regression test scripts against reference data"""
 
     # exact comparison?
     if args["script"] == ["exact"]:
-        returnCode = 0
-        for i in range(0, len(args["files"]) // 2):
-            print("\nExact comparison...")
-            result = subprocess.call(["diff", args["files"][i * 2], args["files"][(i * 2) + 1]])
-            if result:
-                returnCode = 1
-        sys.exit(returnCode)
+        sys.exit(_exactComparison(args))
 
-    # fuzzy comparison?
+    # fuzzy mesh comparison?
     elif args["script"] == ["fuzzy"] or args["script"] == [
         os.path.dirname(os.path.abspath(__file__)) + "/fuzzycomparevtu.py"
     ]:
-        returnCode = 0
-        for i in range(0, len(args["files"]) // 2):
-            print("\nFuzzy comparison...")
-            result = compareVTK(
-                args["files"][i * 2],
-                args["files"][(i * 2) + 1],
-                relative=args["relative"],
-                absolute=args["absolute"],
-                zeroValueThreshold=args["zeroThreshold"],
-            )
-            if result:
-                returnCode = 1
-        sys.exit(returnCode)
+        sys.exit(_fuzzyMeshComparison(args))
 
-    # fuzzy comparison of data sets?
+    # fuzzy comparison of CSV-like data sets?
     elif args["script"] == ["fuzzyData"]:
-        returnCode = 0
-        for i in range(0, len(args["files"]) // 2):
-            print("\nFuzzy data comparison...")
-            result = compareData(
-                args["files"][i * 2],
-                args["files"][(i * 2) + 1],
-                args["delimiter"],
-                relative=args["relative"],
-                absolute=args["absolute"],
-                zeroValueThreshold=args["zeroThreshold"],
-            )
-            if result:
-                returnCode = 1
-        sys.exit(returnCode)
+        sys.exit(_fuzzyDataComparison(args))
 
     # other script?
     else:
-        returnCode = 0
-        for i in range(0, len(args["files"]) // 2):
-            print(f"\n{args['script']} comparison...")
-            result = subprocess.call(
-                args["script"], args["files"][i * 2], args["files"][(i * 2) + 1]
-            )
-            if result:
-                returnCode = 1
-        sys.exit(returnCode)
+        sys.exit(_scriptComparison(args))
 
 
 def runTest():
-    """Run a DuMux test"""
+    """DuMux test driver"""
 
     args = readCmdParameters()
 
-- 
GitLab