From 07eded03a7e3c45344d4758f7ab5a39a931a7b7a Mon Sep 17 00:00:00 2001
From: Mark Olesen <Mark.Olesen@esi-group.com>
Date: Wed, 30 Aug 2023 16:08:30 +0200
Subject: [PATCH] ENH: error handling for empty surfaces in surfaceFieldValue
 (#2966)

- for workflows with appearing/disappearing patches (for example)
  can specify that empty surfaces should be ignored or warned about
  instead of raising a FatalError.

  Note that this handling is additional to the regular top-level
  "errors" specification. So specifying 'strict' will only actually
  result in a FatalError if the "errors" does not trap errors.

- "ignore" : any empty surfaces are simply ignored and no
  file output (besides the header).

- "warn" : empty surfaces are warned about a few times (10)
  and the file output contains a NaN entry

- "strict" : corresponds to the default behaviour.
  Throws a FatalError if the surface is empty.
  This error may still be caught by the top-level "errors" handling.
---
 .../surfaceFieldValue/surfaceFieldValue.C     | 326 ++++++++++++++----
 .../surfaceFieldValue/surfaceFieldValue.H     |  10 +-
 2 files changed, 267 insertions(+), 69 deletions(-)

diff --git a/src/functionObjects/field/fieldValues/surfaceFieldValue/surfaceFieldValue.C b/src/functionObjects/field/fieldValues/surfaceFieldValue/surfaceFieldValue.C
index 0b8517c8a7e..98bf75f3ef7 100644
--- a/src/functionObjects/field/fieldValues/surfaceFieldValue/surfaceFieldValue.C
+++ b/src/functionObjects/field/fieldValues/surfaceFieldValue/surfaceFieldValue.C
@@ -37,6 +37,9 @@ License
 
 // * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
 
+// Max number of warnings
+static constexpr const unsigned maxWarnings = 10u;
+
 namespace Foam
 {
 namespace functionObjects
@@ -135,39 +138,13 @@ void Foam::functionObjects::fieldValues::surfaceFieldValue::setFaceZoneFaces()
         mesh_.faceZones().indices(selectionNames_)
     );
 
-    // Total number of faces selected
+    // Total number of faces that could be selected (before patch filtering)
     label numFaces = 0;
     for (const label zoneId : zoneIds)
     {
         numFaces += mesh_.faceZones()[zoneId].size();
     }
 
-    if (zoneIds.empty())
-    {
-        FatalErrorInFunction
-            << type() << ' ' << name() << ": "
-            << regionTypeNames_[regionType_] << '(' << regionName_ << "):" << nl
-            << "    No matching face zone(s): "
-            << flatOutput(selectionNames_)  << nl
-            << "    Known face zones: "
-            << flatOutput(mesh_.faceZones().names()) << nl
-            << exit(FatalError);
-    }
-
-    // Could also check this
-    #if 0
-    if (!returnReduceOr(numFaces))
-    {
-        WarningInFunction
-            << type() << ' ' << name() << ": "
-            << regionTypeNames_[regionType_] << '(' << regionName_ << "):" << nl
-            << "    The faceZone specification: "
-            << flatOutput(selectionNames_) << nl
-            << "    resulted in 0 faces" << nl
-            << exit(FatalError);
-    }
-    #endif
-
     faceId_.resize_nocopy(numFaces);
     facePatchId_.resize_nocopy(numFaces);
     faceFlip_.resize_nocopy(numFaces);
@@ -223,7 +200,75 @@ void Foam::functionObjects::fieldValues::surfaceFieldValue::setFaceZoneFaces()
     faceId_.resize(numFaces);
     facePatchId_.resize(numFaces);
     faceFlip_.resize(numFaces);
-    nFaces_ = returnReduce(faceId_.size(), sumOp<label>());
+    nFaces_ = returnReduce(numFaces, sumOp<label>());
+
+    if (!nFaces_)
+    {
+        // Raise warning or error
+        refPtr<OSstream> os;
+        bool fatal = false;
+
+        ++nWarnings_;  // Always increment (even if ignore etc)
+
+        switch (emptySurfaceError_)
+        {
+            case error::handlerTypes::IGNORE:
+            {
+                break;
+            }
+
+            case error::handlerTypes::WARN:
+            {
+                if (nWarnings_ <= maxWarnings)
+                {
+                    os.ref(WarningInFunction);
+                }
+                break;
+            }
+
+            // STRICT / DEFAULT
+            default:
+            {
+                os.ref(FatalErrorInFunction);
+                fatal = true;
+                break;
+            }
+        }
+
+        if (os)
+        {
+            os.ref()
+                << type() << ' ' << name() << ": "
+                << regionTypeNames_[regionType_]
+                << '(' << regionName_ << "):" << nl;
+
+            if (zoneIds.empty())
+            {
+                os.ref()
+                    << "    No matching face zones: "
+                    << flatOutput(selectionNames_)  << nl
+                    << "    Known face zones: "
+                    << flatOutput(mesh_.faceZones().names()) << nl;
+            }
+            else
+            {
+                os.ref()
+                    << "    The face zones: "
+                    << flatOutput(selectionNames_) << nl
+                    << "    resulted in 0 faces" << nl;
+            }
+
+            if (fatal)
+            {
+                FatalError<< exit(FatalError);
+            }
+            else if (nWarnings_ == maxWarnings)
+            {
+                os.ref()
+                    << "... suppressing further warnings." << nl;
+            }
+        }
+    }
 }
 
 
@@ -283,7 +328,7 @@ void Foam::functionObjects::fieldValues::surfaceFieldValue::setPatchFaces()
         nGood = 0;
         for (const label patchi : selected)
         {
-            if (!bad.found(patchi))
+            if (!bad.contains(patchi))
             {
                 patchIds[nGood] = patchi;
                 ++nGood;
@@ -295,36 +340,10 @@ void Foam::functionObjects::fieldValues::surfaceFieldValue::setPatchFaces()
         patchIds = std::move(selected);
     }
 
-    if (patchIds.empty())
-    {
-        FatalErrorInFunction
-            << type() << ' ' << name() << ": "
-            << regionTypeNames_[regionType_] << '(' << regionName_ << "):" << nl
-            << "    No matching patch name(s): "
-            << flatOutput(selectionNames_)  << nl
-            << "    Known patch names:" << nl
-            << mesh_.boundaryMesh().names() << nl
-            << exit(FatalError);
-    }
-
-    // Could also check this
-    #if 0
-    if (!returnReduceOr(numFaces))
-    {
-        WarningInFunction
-            << type() << ' ' << name() << ": "
-            << regionTypeNames_[regionType_] << '(' << regionName_ << "):" << nl
-            << "    The patch specification: "
-            << flatOutput(selectionNames_) << nl
-            << "    resulted in 0 faces" << nl
-            << exit(FatalError);
-    }
-    #endif
-
-    faceId_.resize(numFaces);
-    facePatchId_.resize(numFaces);
-    faceFlip_.resize(numFaces);
-    nFaces_ = returnReduce(faceId_.size(), sumOp<label>());
+    faceId_.resize_nocopy(numFaces);
+    facePatchId_.resize_nocopy(numFaces);
+    faceFlip_.resize_nocopy(numFaces);
+    nFaces_ = returnReduce(numFaces, sumOp<label>());
 
     numFaces = 0;
     for (const label patchi : patchIds)
@@ -338,6 +357,74 @@ void Foam::functionObjects::fieldValues::surfaceFieldValue::setPatchFaces()
 
         numFaces += len;
     }
+
+    if (!nFaces_)
+    {
+        // Raise warning or error
+        refPtr<OSstream> os;
+        bool fatal = false;
+
+        ++nWarnings_;  // Always increment (even if ignore etc)
+
+        switch (emptySurfaceError_)
+        {
+            case error::handlerTypes::IGNORE:
+            {
+                break;
+            }
+
+            case error::handlerTypes::WARN:
+            {
+                if (nWarnings_ <= maxWarnings)
+                {
+                    os.ref(WarningInFunction);
+                }
+                break;
+            }
+
+            // STRICT / DEFAULT
+            default:
+            {
+                os.ref(FatalErrorInFunction);
+                fatal = true;
+                break;
+            }
+        }
+
+        if (os)
+        {
+            os.ref()
+                << type() << ' ' << name() << ": "
+                << regionTypeNames_[regionType_]
+                << '(' << regionName_ << "):" << nl;
+
+            if (patchIds.empty())
+            {
+                os.ref()
+                    << "    No matching patches: "
+                    << flatOutput(selectionNames_)  << nl
+                    << "    Known patch names:" << nl
+                    << mesh_.boundaryMesh().names() << nl;
+            }
+            else
+            {
+                os.ref()
+                    << "    The patches: "
+                    << flatOutput(selectionNames_) << nl
+                    << "    resulted in 0 faces" << nl;
+            }
+
+            if (fatal)
+            {
+                FatalError<< exit(FatalError);
+            }
+            else if (nWarnings_ == maxWarnings)
+            {
+                os.ref()
+                    << "... suppressing further warnings." << nl;
+            }
+        }
+    }
 }
 
 
@@ -511,20 +598,30 @@ bool Foam::functionObjects::fieldValues::surfaceFieldValue::update()
         return false;
     }
 
+    // Reset some values
+    totalArea_ = 0;
+    nFaces_ = 0;
+    bool checkEmptyFaces = true;
+
     switch (regionType_)
     {
         case stFaceZone:
         {
+            // Raises warning or error internally, don't check again
             setFaceZoneFaces();
+            checkEmptyFaces = false;
             break;
         }
         case stPatch:
         {
+            // Raises warning or error internally, don't check again
             setPatchFaces();
+            checkEmptyFaces = false;
             break;
         }
         case stObject:
         {
+            // TBD: special handling of cast errors?
             const auto& s = refCast<const polySurface>(obr());
             nFaces_ = returnReduce(s.size(), sumOp<label>());
             break;
@@ -538,23 +635,76 @@ bool Foam::functionObjects::fieldValues::surfaceFieldValue::update()
         // Compiler warning if we forgot an enumeration
     }
 
-    if (nFaces_ == 0)
+    if (nFaces_)
     {
-        FatalErrorInFunction
-            << type() << ' ' << name() << ": "
-            << regionTypeNames_[regionType_] << '(' << regionName_ << "):" << nl
-            << "    Region has no faces" << exit(FatalError);
+        // Appears to be successful
+        needsUpdate_ = false;
+        totalArea_ = totalArea();   // Update the area
+        nWarnings_ = 0u;            // Reset the warnings counter
     }
+    else if (checkEmptyFaces)
+    {
+        // Raise warning or error
+        refPtr<OSstream> os;
+        bool fatal = false;
 
-    totalArea_ = totalArea();
+        ++nWarnings_;  // Always increment (even if ignore etc)
+
+        switch (emptySurfaceError_)
+        {
+            case error::handlerTypes::IGNORE:
+            {
+                break;
+            }
+
+            case error::handlerTypes::WARN:
+            {
+                if (nWarnings_ <= maxWarnings)
+                {
+                    os.ref(WarningInFunction);
+                }
+                break;
+            }
+
+            // STRICT / DEFAULT
+            default:
+            {
+                os.ref(FatalErrorInFunction);
+                fatal = true;
+                break;
+            }
+        }
+
+        if (os)
+        {
+            os.ref()
+                << type() << ' ' << name() << ": "
+                << regionTypeNames_[regionType_]
+                << '(' << regionName_ << "):" << nl
+                << "    Region has no faces" << endl;
+
+            if (fatal)
+            {
+                FatalError<< exit(FatalError);
+            }
+            else if (nWarnings_ == maxWarnings)
+            {
+                os.ref()
+                    << "... suppressing further warnings." << nl;
+            }
+        }
+    }
 
     Log << "    total faces   = " << nFaces_ << nl
         << "    total area    = " << totalArea_ << nl
         << endl;
 
-    writeFileHeader(file());
+    // Emit file header on success or change of state
+    if (nWarnings_ <= 1)
+    {
+        writeFileHeader(file());
+    }
 
-    needsUpdate_ = false;
     return true;
 }
 
@@ -931,10 +1081,12 @@ Foam::functionObjects::fieldValues::surfaceFieldValue::surfaceFieldValue
     ),
     needsUpdate_(true),
     writeArea_(false),
+    emptySurfaceError_(error::handlerTypes::DEFAULT),
     selectionNames_(),
     weightFieldNames_(),
     totalArea_(0),
     nFaces_(0),
+    nWarnings_(0),
     faceId_(),
     facePatchId_(),
     faceFlip_()
@@ -965,10 +1117,12 @@ Foam::functionObjects::fieldValues::surfaceFieldValue::surfaceFieldValue
     ),
     needsUpdate_(true),
     writeArea_(false),
+    emptySurfaceError_(error::handlerTypes::DEFAULT),
     selectionNames_(),
     weightFieldNames_(),
     totalArea_(0),
     nFaces_(0),
+    nWarnings_(0),
     faceId_(),
     facePatchId_(),
     faceFlip_()
@@ -995,12 +1149,21 @@ bool Foam::functionObjects::fieldValues::surfaceFieldValue::read
 
     needsUpdate_ = true;
     writeArea_ = dict.getOrDefault("writeArea", false);
+    emptySurfaceError_ = error::handlerNames.getOrDefault
+    (
+        "empty-surface",
+        dict,
+        error::handlerTypes::DEFAULT,
+        true  // Failsafe behaviour
+    );
+
     weightFieldNames_.clear();
     // future?
     // sampleFaceScheme_ = dict.getOrDefault<word>("sampleScheme", "cell");
 
     totalArea_ = 0;
     nFaces_ = 0;
+    nWarnings_ = 0;
     faceId_.clear();
     facePatchId_.clear();
     faceFlip_.clear();
@@ -1182,12 +1345,39 @@ bool Foam::functionObjects::fieldValues::surfaceFieldValue::write()
         writeCurrentTime(file());
     }
 
+    // Handle ignore/warn about empty-surface
+    if (!nFaces_)
+    {
+        totalArea_ = 0;  // Update the area (safety)
+
+        if (operation_ != opNone)
+        {
+            if (emptySurfaceError_ == error::handlerTypes::WARN)
+            {
+                if (writeArea_)
+                {
+                    Log << "    total area = " << totalArea_ << endl;
+                    file() << tab << totalArea_;
+                }
+
+                file() << tab << "NaN";
+                Log << endl;
+            }
+
+            file() << endl;
+        }
+
+        // Early exit on error
+        return true;
+    }
+
     if (writeArea_)
     {
+        // Update the area
         totalArea_ = totalArea();
         Log << "    total area = " << totalArea_ << endl;
 
-        if (operation_ != opNone && Pstream::master())
+        if (operation_ != opNone && UPstream::master())
         {
             file() << tab << totalArea_;
         }
diff --git a/src/functionObjects/field/fieldValues/surfaceFieldValue/surfaceFieldValue.H b/src/functionObjects/field/fieldValues/surfaceFieldValue/surfaceFieldValue.H
index 3782e878a76..efda52462a8 100644
--- a/src/functionObjects/field/fieldValues/surfaceFieldValue/surfaceFieldValue.H
+++ b/src/functionObjects/field/fieldValues/surfaceFieldValue/surfaceFieldValue.H
@@ -6,7 +6,7 @@
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
     Copyright (C) 2011-2017 OpenFOAM Foundation
-    Copyright (C) 2015-2020 OpenCFD Ltd.
+    Copyright (C) 2015-2023 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -88,6 +88,7 @@ Usage
         scaleFactor     1.0;
         writeArea       false;
         surfaceFormat   none;
+        empty-surface   warn;  // default | warn | ignore | strict
 
         // Optional (inherited) entries
         ...
@@ -111,6 +112,7 @@ Usage
       writeArea    | Write the surface area             | bool |  no   | false
       surfaceFormat | Output value format               | word <!--
                 --> | conditional on writeFields  | none
+      empty-surface | Error handling for empty surfaces | enum | no | default
     \endtable
 
     The inherited entries are elaborated in:
@@ -400,6 +402,9 @@ protected:
         //- Optionally write the area of the surfaceFieldValue
         bool writeArea_;
 
+        //- Handling of empty surfaces (nFaces = 0). Default is Fatal.
+        error::handlerTypes emptySurfaceError_;
+
         //- Extended selections
         wordRes selectionNames_;
 
@@ -412,6 +417,9 @@ protected:
         //- Global number of faces
         label nFaces_;
 
+        //- Number of warnings emitted since the last valid update
+        unsigned nWarnings_;
+
 
     // If operating on mesh faces (faceZone, patch)
 
-- 
GitLab