From 1a55829ef9841b4491a1bfeddb55a9d69b36c01d Mon Sep 17 00:00:00 2001
From: Mark Olesen <Mark.Olesen@esi-group.com>
Date: Mon, 28 Feb 2022 13:56:42 +0100
Subject: [PATCH] ENH: add fieldLevel handling for surface writers (#2382)

- this can be used to apply a uniform field level to remove from
  a sampled field. For example,

      fieldLevel
      {
          "p.*"   1e5;        // Absolute -> gauge [Pa]
          T       273.15;     // [K] -> [C]
          U       #eval{ 10/sqrt(3) };  // Uniform mag(U)=10
      }

  After the fieldLevel has been removed, any fieldScale is applied.
  For example

      fieldScale
      {
          "p.*"   0.01;       // [Pa] -> [mbar]
      }

  The fieldLevel for vector and tensor fields may still need some
  further refinement.
---
 .../writers/abaqus/abaqusSurfaceWriter.C      |  2 -
 .../writers/abaqus/abaqusSurfaceWriter.H      | 14 +--
 .../writers/abaqus/abaqusSurfaceWriterImpl.C  | 21 +----
 .../boundary/boundaryDataSurfaceWriter.C      | 27 ++----
 .../boundary/boundaryDataSurfaceWriter.H      | 14 +--
 src/surfMesh/writers/common/surfaceWriter.C   | 92 +++++++++++++++++--
 src/surfMesh/writers/common/surfaceWriter.H   | 56 ++++++++---
 .../writers/ensight/ensightSurfaceWriter.H    |  4 +-
 .../ensight/ensightSurfaceWriterCollated.C    | 11 ++-
 .../ensight/ensightSurfaceWriterUncollated.C  | 12 ++-
 src/surfMesh/writers/foam/foamSurfaceWriter.C | 27 ++----
 src/surfMesh/writers/foam/foamSurfaceWriter.H | 15 +--
 .../writers/nastran/nastranSurfaceWriter.C    |  4 +-
 .../writers/nastran/nastranSurfaceWriter.H    |  8 +-
 .../nastran/nastranSurfaceWriterImpl.C        | 22 +----
 src/surfMesh/writers/raw/rawSurfaceWriter.C   |  6 +-
 src/surfMesh/writers/raw/rawSurfaceWriter.H   | 10 +-
 .../writers/raw/rawSurfaceWriterImpl.C        | 20 +---
 .../writers/starcd/starcdSurfaceWriter.C      | 28 ++----
 .../writers/starcd/starcdSurfaceWriter.H      |  8 +-
 src/surfMesh/writers/vtk/vtkSurfaceWriter.C   | 23 +----
 src/surfMesh/writers/vtk/vtkSurfaceWriter.H   | 12 ++-
 src/surfMesh/writers/x3d/x3dSurfaceWriter.C   | 13 ++-
 src/surfMesh/writers/x3d/x3dSurfaceWriter.H   |  7 +-
 .../squareBend/system/samplingDebug           |  7 +-
 25 files changed, 244 insertions(+), 219 deletions(-)

diff --git a/src/surfMesh/writers/abaqus/abaqusSurfaceWriter.C b/src/surfMesh/writers/abaqus/abaqusSurfaceWriter.C
index 8932d906849..9d3fd373e09 100644
--- a/src/surfMesh/writers/abaqus/abaqusSurfaceWriter.C
+++ b/src/surfMesh/writers/abaqus/abaqusSurfaceWriter.C
@@ -238,7 +238,6 @@ Foam::surfaceWriters::abaqusWriter::abaqusWriter()
 :
     surfaceWriter(),
     geometryScale_(1),
-    fieldScale_(),
     noGeometry_(false),
     outputLayout_(outputLayoutType::BY_FIELD)
 {}
@@ -251,7 +250,6 @@ Foam::surfaceWriters::abaqusWriter::abaqusWriter
 :
     surfaceWriter(options),
     geometryScale_(options.getOrDefault<scalar>("scale", 1)),
-    fieldScale_(options.subOrEmptyDict("fieldScale")),
     noGeometry_(options.getOrDefault("noGeometry", false)),
     outputLayout_(outputLayoutType::BY_FIELD)
 {}
diff --git a/src/surfMesh/writers/abaqus/abaqusSurfaceWriter.H b/src/surfMesh/writers/abaqus/abaqusSurfaceWriter.H
index 891b6885a1c..24bdf22eda7 100644
--- a/src/surfMesh/writers/abaqus/abaqusSurfaceWriter.H
+++ b/src/surfMesh/writers/abaqus/abaqusSurfaceWriter.H
@@ -5,7 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2020 OpenCFD Ltd.
+    Copyright (C) 2020-2022 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -33,7 +33,8 @@ Description
     \table
         Property   | Description                            | Required | Default
         scale      | output geometry scaling                | no  | 1
-        fieldScale | output field scaling (dictionary)      | no  | empty
+        fieldLevel  | Subtract field level before scaling   | no  | empty dict
+        fieldScale  | Output field scaling                  | no  | empty dict
         noGeometry | Suppress geometry output (beta feature) | no  | false
     \endtable
 
@@ -44,9 +45,13 @@ Description
         abaqus
         {
             scale   1000;     // [m] -> [mm]
+            fieldLevel
+            {
+                p       1e5;    // Absolute -> gauge [Pa]
+            }
             fieldScale
             {
-               "p.*"   0.01;  // [Pa] -> [mbar]
+                "p.*"   0.01;   // [Pa] -> [mbar]
             }
         }
     }
@@ -110,9 +115,6 @@ class abaqusWriter
         //- Output geometry scaling
         const scalar geometryScale_;
 
-        //- Output field scaling
-        const dictionary fieldScale_;
-
         //- BETA feature
         bool noGeometry_;
 
diff --git a/src/surfMesh/writers/abaqus/abaqusSurfaceWriterImpl.C b/src/surfMesh/writers/abaqus/abaqusSurfaceWriterImpl.C
index 5d1a8635d45..5de351d28ee 100644
--- a/src/surfMesh/writers/abaqus/abaqusSurfaceWriterImpl.C
+++ b/src/surfMesh/writers/abaqus/abaqusSurfaceWriterImpl.C
@@ -5,7 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2020 OpenCFD Ltd.
+    Copyright (C) 2020-2022 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -117,30 +117,17 @@ Foam::fileName Foam::surfaceWriters::abaqusWriter::writeTemplate
     outputFile.ext("inp");
 
 
-    // Output scaling for the variable, but not for integer types.
-    // could also solve with clever templating
+    // Implicit geometry merge()
+    tmp<Field<Type>> tfield = mergeField(localValues);
 
-    const scalar varScale =
-    (
-        std::is_integral<Type>::value
-      ? scalar(1)
-      : fieldScale_.getOrDefault<scalar>(fieldName, 1)
-    );
+    adjustOutputField(fieldName, tfield.ref());
 
     if (verbose_)
     {
-        Info<< "Writing field " << fieldName;
-        if (!equal(varScale, 1))
-        {
-            Info<< " (scaling " << varScale << ')';
-        }
         Info<< " to " << outputFile << endl;
     }
 
 
-    // Implicit geometry merge()
-    tmp<Field<Type>> tfield = mergeField(localValues) * varScale;
-
     const meshedSurf& surf = surface();
 
     if (Pstream::master() || !parallel_)
diff --git a/src/surfMesh/writers/boundary/boundaryDataSurfaceWriter.C b/src/surfMesh/writers/boundary/boundaryDataSurfaceWriter.C
index 844debe8daf..85c095f7664 100644
--- a/src/surfMesh/writers/boundary/boundaryDataSurfaceWriter.C
+++ b/src/surfMesh/writers/boundary/boundaryDataSurfaceWriter.C
@@ -6,7 +6,7 @@
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
     Copyright (C) 2015 OpenFOAM Foundation
-    Copyright (C) 2015-2021 OpenCFD Ltd.
+    Copyright (C) 2015-2022 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -56,8 +56,7 @@ Foam::surfaceWriters::boundaryDataWriter::boundaryDataWriter()
 :
     surfaceWriter(),
     header_(true),
-    streamOpt_(),
-    fieldScale_()
+    streamOpt_()
 {}
 
 
@@ -72,8 +71,7 @@ Foam::surfaceWriters::boundaryDataWriter::boundaryDataWriter
     (
         IOstreamOption::formatEnum("format", options, IOstreamOption::ASCII),
         IOstreamOption::compressionEnum("compression", options)
-    ),
-    fieldScale_(options.subOrEmptyDict("fieldScale"))
+    )
 {}
 
 
@@ -220,25 +218,20 @@ Foam::fileName Foam::surfaceWriters::boundaryDataWriter::writeTemplate
 
     const fileName outputFile(surfaceDir/timeName()/fieldName);
 
+    // Implicit geometry merge()
+    tmp<Field<Type>> tfield = mergeField(localValues);
 
-    // Output scaling for the variable, but not for integer types.
-    // could also solve with clever templating
+    adjustOutputField(fieldName, tfield.ref());
 
-    const scalar varScale =
-    (
-        std::is_integral<Type>::value
-      ? scalar(1)
-      : fieldScale_.getOrDefault<scalar>(fieldName, 1)
-    );
+    if (verbose_)
+    {
+        Info<< " to " << outputFile << endl;
+    }
 
 
     // Dummy Time to use as objectRegistry
     autoPtr<Time> dummyTimePtr(Time::New(argList::envGlobalPath()));
 
-
-    // Implicit geometry merge()
-    tmp<Field<Type>> tfield = mergeField(localValues) * varScale;
-
     const meshedSurf& surf = surface();
 
     if (Pstream::master() || !parallel_)
diff --git a/src/surfMesh/writers/boundary/boundaryDataSurfaceWriter.H b/src/surfMesh/writers/boundary/boundaryDataSurfaceWriter.H
index b77b89ea458..e20392402b9 100644
--- a/src/surfMesh/writers/boundary/boundaryDataSurfaceWriter.H
+++ b/src/surfMesh/writers/boundary/boundaryDataSurfaceWriter.H
@@ -5,7 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2015-2021 OpenCFD Ltd.
+    Copyright (C) 2015-2022 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -49,7 +49,8 @@ Description
         header      | Generate files with FoamFile header   | no  | true
         format      | ascii/binary                          | no  | ascii
         compression | Use file compression                  | no  | false
-        fieldScale  | output field scaling (dictionary)     | no  | empty
+        fieldLevel  | Subtract field level before scaling   | no  | empty dict
+        fieldScale  | Output field scaling                  | no  | empty dict
     \endtable
 
     Typical way of working:
@@ -65,9 +66,13 @@ Description
             boundaryData
             {
                 format  binary;
+                fieldLevel
+                {
+                    p       1e5;    // Absolute -> gauge [Pa]
+                }
                 fieldScale
                 {
-                   "p.*"   0.01;  // [Pa] -> [mbar]
+                    "p.*"   0.01;   // [Pa] -> [mbar]
                 }
             }
         }
@@ -157,9 +162,6 @@ class boundaryDataWriter
         //- Output stream option
         IOstreamOption streamOpt_;
 
-        //- Output field scaling
-        const dictionary fieldScale_;
-
 
     // Private Member Functions
 
diff --git a/src/surfMesh/writers/common/surfaceWriter.C b/src/surfMesh/writers/common/surfaceWriter.C
index fd3b7603e0b..2b73f957ef9 100644
--- a/src/surfMesh/writers/common/surfaceWriter.C
+++ b/src/surfMesh/writers/common/surfaceWriter.C
@@ -5,7 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2019-2021 OpenCFD Ltd.
+    Copyright (C) 2019-2022 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -152,7 +152,9 @@ Foam::surfaceWriter::surfaceWriter()
     mergeDim_(defaultMergeDim),
     merged_(),
     currTime_(),
-    outputPath_()
+    outputPath_(),
+    fieldLevel_(),
+    fieldScale_()
 {
     surfaceWriter::close();
 }
@@ -163,6 +165,8 @@ Foam::surfaceWriter::surfaceWriter(const dictionary& options)
     surfaceWriter()
 {
     options.readIfPresent("verbose", verbose_);
+    fieldLevel_ = options.subOrEmptyDict("fieldLevel");
+    fieldScale_ = options.subOrEmptyDict("fieldScale");
 }
 
 
@@ -501,21 +505,89 @@ Foam::tmp<Foam::Field<Type>> Foam::surfaceWriter::mergeFieldTemplate
 }
 
 
-#define defineSurfaceWriterMergeMethod(ThisClass, Type)                        \
+template<class Type>
+void Foam::surfaceWriter::adjustOutputFieldTemplate
+(
+    const word& fieldName,
+    Field<Type>& fld
+) const
+{
+    if (verbose_)
+    {
+        Info<< "Writing field " << fieldName;
+    }
+
+    // Output scaling for the variable, but not for integer types
+    // which are typically ids etc.
+    if (!std::is_integral<Type>::value)
+    {
+        scalar value;
+
+        // Remove *uniform* reference level
+        if
+        (
+            fieldLevel_.readIfPresent(fieldName, value, keyType::REGEX)
+         && !equal(value, 0)
+        )
+        {
+            // Could also detect brackets (...) and read accordingly
+            // or automatically scale by 1/sqrt(nComponents) instead ...
+
+            Type refLevel;
+            for (direction cmpt = 0; cmpt < pTraits<Type>::nComponents; ++cmpt)
+            {
+                setComponent(refLevel, cmpt) = value;
+            }
+
+            if (verbose_)
+            {
+                Info<< " [level " << refLevel << ']';
+            }
+
+            fld -= refLevel;
+        }
+
+        // Apply scaling
+        if
+        (
+            fieldScale_.readIfPresent(fieldName, value, keyType::REGEX)
+         && !equal(value, 1)
+        )
+        {
+            if (verbose_)
+            {
+                Info<< " [scaling " << value << ']';
+            }
+            fld *= value;
+        }
+    }
+}
+
+
+#define defineSurfaceFieldMethods(ThisClass, Type)                             \
     Foam::tmp<Foam::Field<Type>>                                               \
     ThisClass::mergeField(const Field<Type>& fld) const                        \
     {                                                                          \
         return mergeFieldTemplate(fld);                                        \
+    }                                                                          \
+                                                                               \
+    void ThisClass::adjustOutputField                                          \
+    (                                                                          \
+        const word& fieldName,                                                 \
+        Field<Type>& fld                                                       \
+    ) const                                                                    \
+    {                                                                          \
+        adjustOutputFieldTemplate(fieldName, fld);                             \
     }
 
-defineSurfaceWriterMergeMethod(Foam::surfaceWriter, Foam::label);
-defineSurfaceWriterMergeMethod(Foam::surfaceWriter, Foam::scalar);
-defineSurfaceWriterMergeMethod(Foam::surfaceWriter, Foam::vector);
-defineSurfaceWriterMergeMethod(Foam::surfaceWriter, Foam::sphericalTensor);
-defineSurfaceWriterMergeMethod(Foam::surfaceWriter, Foam::symmTensor);
-defineSurfaceWriterMergeMethod(Foam::surfaceWriter, Foam::tensor)
+defineSurfaceFieldMethods(Foam::surfaceWriter, Foam::label);
+defineSurfaceFieldMethods(Foam::surfaceWriter, Foam::scalar);
+defineSurfaceFieldMethods(Foam::surfaceWriter, Foam::vector);
+defineSurfaceFieldMethods(Foam::surfaceWriter, Foam::sphericalTensor);
+defineSurfaceFieldMethods(Foam::surfaceWriter, Foam::symmTensor);
+defineSurfaceFieldMethods(Foam::surfaceWriter, Foam::tensor)
 
-#undef defineSurfaceWriterMergeMethod
+#undef defineSurfaceFieldMethod
 
 
 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
diff --git a/src/surfMesh/writers/common/surfaceWriter.H b/src/surfMesh/writers/common/surfaceWriter.H
index db6d2341c53..4d22644f1fc 100644
--- a/src/surfMesh/writers/common/surfaceWriter.H
+++ b/src/surfMesh/writers/common/surfaceWriter.H
@@ -47,14 +47,26 @@ Description
         someFormat // Eg, ensight, vtk, etc
         {
             verbose         true;
+            fieldLevel
+            {
+                "p.*"   1e5;    // Absolute -> gauge [Pa]
+                T       273.15; // [K] -> [C]
+                U       #eval{ 10/sqrt(3) };  // Uniform magU=10
+            }
+            fieldScale
+            {
+                "p.*"   0.01;   // [Pa] -> [mbar]
+            }
         }
     }
     \endverbatim
 
   Format options:
     \table
-        Property | Description                             | Required | Default
-        verbose  | Additional output verbosity             | no  | no
+        Property | Description                              | Required | Default
+        verbose  | Additional output verbosity              | no  | no
+        fieldLevel | Subtract field level before scaling    | no  | empty dict
+        fieldScale | Output field scaling                   | no  | empty dict
     \endtable
 
 Note
@@ -156,6 +168,12 @@ protected:
         //- The full output directory and file (surface) name
         fileName outputPath_;
 
+        //- Field level to remove (on output)
+        dictionary fieldLevel_;
+
+        //- Field scaling (on output)
+        dictionary fieldScale_;
+
 
     // Protected Member Functions
 
@@ -175,18 +193,28 @@ protected:
         template<class Type>
         tmp<Field<Type>> mergeFieldTemplate(const Field<Type>& fld) const;
 
-#undef  declareSurfaceWriterMergeMethod
-#define declareSurfaceWriterMergeMethod(Type)                                  \
-        tmp<Field<Type>> mergeField(const Field<Type>& fld) const;
-
-        declareSurfaceWriterMergeMethod(label);
-        declareSurfaceWriterMergeMethod(scalar);
-        declareSurfaceWriterMergeMethod(vector);
-        declareSurfaceWriterMergeMethod(sphericalTensor);
-        declareSurfaceWriterMergeMethod(symmTensor);
-        declareSurfaceWriterMergeMethod(tensor);
-
-        #undef declareSurfaceWriterMergeMethod
+        //- Apply refLevel and fieldScaling
+        template<class Type>
+        void adjustOutputFieldTemplate
+        (
+            const word& fieldName,
+            Field<Type>& fld
+        ) const;
+
+#undef  declareSurfaceFieldMethod
+#define declareSurfaceFieldMethods(Type)                                      \
+                                                                              \
+        tmp<Field<Type>> mergeField(const Field<Type>& fld) const;            \
+        void adjustOutputField(const word& fieldName, Field<Type>& fld) const;
+
+        declareSurfaceFieldMethods(label);
+        declareSurfaceFieldMethods(scalar);
+        declareSurfaceFieldMethods(vector);
+        declareSurfaceFieldMethods(sphericalTensor);
+        declareSurfaceFieldMethods(symmTensor);
+        declareSurfaceFieldMethods(tensor);
+
+        #undef declareSurfaceFieldMethods
 
         //- Dummy templated write operation
         template<class Type>
diff --git a/src/surfMesh/writers/ensight/ensightSurfaceWriter.H b/src/surfMesh/writers/ensight/ensightSurfaceWriter.H
index a4e0ea9120c..c4cb698133f 100644
--- a/src/surfMesh/writers/ensight/ensightSurfaceWriter.H
+++ b/src/surfMesh/writers/ensight/ensightSurfaceWriter.H
@@ -41,11 +41,13 @@ Description
     }
     \endverbatim
 
-    Format options:
+    Format options for ensight:
     \table
         Property | Description                              | Required | Default
         format   | ascii/binary                             | no  | ascii
         collateTimes | use common geometry for times        | no  | true
+        fieldLevel  | Subtract field level before scaling   | no  | empty dict
+        fieldScale  | Output field scaling                  | no  | empty dict
     \endtable
 
     The collated format maintains an internal list of the known times
diff --git a/src/surfMesh/writers/ensight/ensightSurfaceWriterCollated.C b/src/surfMesh/writers/ensight/ensightSurfaceWriterCollated.C
index 564df9a18f6..473676df975 100644
--- a/src/surfMesh/writers/ensight/ensightSurfaceWriterCollated.C
+++ b/src/surfMesh/writers/ensight/ensightSurfaceWriterCollated.C
@@ -83,14 +83,19 @@ Foam::fileName Foam::surfaceWriters::ensightWriter::writeCollated
 
     if (verbose_)
     {
-        Info<< "Writing case file to " << outputFile << endl;
+        Info<< "Writing case file to " << outputFile << nl;
     }
 
-
-
     // Implicit geometry merge()
     tmp<Field<Type>> tfield = mergeField(localValues);
 
+    adjustOutputField(fieldName, tfield.ref());
+
+    if (verbose_)
+    {
+        Info<< endl;
+    }
+
     const meshedSurf& surf = surface();
 
     if (Pstream::master() || !parallel_)
diff --git a/src/surfMesh/writers/ensight/ensightSurfaceWriterUncollated.C b/src/surfMesh/writers/ensight/ensightSurfaceWriterUncollated.C
index f1b8fe1c793..8d334dea37a 100644
--- a/src/surfMesh/writers/ensight/ensightSurfaceWriterUncollated.C
+++ b/src/surfMesh/writers/ensight/ensightSurfaceWriterUncollated.C
@@ -148,13 +148,19 @@ Foam::fileName Foam::surfaceWriters::ensightWriter::writeUncollated
 
     if (verbose_)
     {
-        Info<< "Writing case file to " << outputFile << endl;
+        Info<< "Writing case file to " << outputFile << nl;
     }
 
-
-    // geometry merge() implicit
+    // Implicit geometry merge()
     tmp<Field<Type>> tfield = mergeField(localValues);
 
+    adjustOutputField(fieldName, tfield.ref());
+
+    if (verbose_)
+    {
+        Info<< endl;
+    }
+
     const meshedSurf& surf = surface();
 
     if (Pstream::master() || !parallel_)
diff --git a/src/surfMesh/writers/foam/foamSurfaceWriter.C b/src/surfMesh/writers/foam/foamSurfaceWriter.C
index ed2446932d2..693df2f839f 100644
--- a/src/surfMesh/writers/foam/foamSurfaceWriter.C
+++ b/src/surfMesh/writers/foam/foamSurfaceWriter.C
@@ -6,7 +6,7 @@
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
     Copyright (C) 2011-2016 OpenFOAM Foundation
-    Copyright (C) 2015-2020 OpenCFD Ltd.
+    Copyright (C) 2015-2022 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -50,8 +50,7 @@ namespace surfaceWriters
 Foam::surfaceWriters::foamWriter::foamWriter()
 :
     surfaceWriter(),
-    streamOpt_(),
-    fieldScale_()
+    streamOpt_()
 {}
 
 
@@ -65,8 +64,7 @@ Foam::surfaceWriters::foamWriter::foamWriter
     (
         IOstreamOption::formatEnum("format", options, IOstreamOption::ASCII),
         IOstreamOption::compressionEnum("compression", options)
-    ),
-    fieldScale_(options.subOrEmptyDict("fieldScale"))
+    )
 {}
 
 
@@ -195,30 +193,17 @@ Foam::fileName Foam::surfaceWriters::foamWriter::writeTemplate
     );
 
 
-    // Output scaling for the variable, but not for integer types.
-    // could also solve with clever templating
+    // Implicit geometry merge()
+    tmp<Field<Type>> tfield = mergeField(localValues);
 
-    const scalar varScale =
-    (
-        std::is_integral<Type>::value
-      ? scalar(1)
-      : fieldScale_.getOrDefault<scalar>(fieldName, 1)
-    );
+    adjustOutputField(fieldName, tfield.ref());
 
     if (verbose_)
     {
-        Info<< "Writing field " << fieldName;
-        if (!equal(varScale, 1))
-        {
-            Info<< " (scaling " << varScale << ')';
-        }
         Info<< " to " << surfaceDir << endl;
     }
 
 
-    // Implicit geometry merge()
-    tmp<Field<Type>> tfield = mergeField(localValues) * varScale;
-
     if (Pstream::master())
     {
         if (!isDir(outputFile.path()))
diff --git a/src/surfMesh/writers/foam/foamSurfaceWriter.H b/src/surfMesh/writers/foam/foamSurfaceWriter.H
index 3c63cd48a5c..8b18757eff9 100644
--- a/src/surfMesh/writers/foam/foamSurfaceWriter.H
+++ b/src/surfMesh/writers/foam/foamSurfaceWriter.H
@@ -6,7 +6,7 @@
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
     Copyright (C) 2011-2016 OpenFOAM Foundation
-    Copyright (C) 2015-2020 OpenCFD Ltd.
+    Copyright (C) 2015-2022 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -37,10 +37,13 @@ Description
         {
             format      ascii;
             compression true;
-
+            fieldLevel
+            {
+                p       1e5;    // Absolute -> gauge [Pa]
+            }
             fieldScale
             {
-               "p.*"   0.01;  // [Pa] -> [mbar]
+                "p.*"   0.01;   // [Pa] -> [mbar]
             }
         }
     }
@@ -51,7 +54,8 @@ Description
         Property | Description                              | Required | Default
         format   | ascii/binary                             | no  | ascii
         compression | output file compression               | no  | false
-        fieldScale | output field scaling (dictionary)      | no  | empty
+        fieldLevel  | Subtract field level before scaling   | no  | empty dict
+        fieldScale  | Output field scaling                  | no  | empty dict
     \endtable
 
     \section Output file locations
@@ -112,9 +116,6 @@ class foamWriter
         //- Output stream option (default: ASCII, uncompressed)
         IOstreamOption streamOpt_;
 
-        //- Output field scaling
-        const dictionary fieldScale_;
-
 
     // Private Member Functions
 
diff --git a/src/surfMesh/writers/nastran/nastranSurfaceWriter.C b/src/surfMesh/writers/nastran/nastranSurfaceWriter.C
index 99be7bd2003..dac8cfb2a7f 100644
--- a/src/surfMesh/writers/nastran/nastranSurfaceWriter.C
+++ b/src/surfMesh/writers/nastran/nastranSurfaceWriter.C
@@ -6,7 +6,7 @@
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
     Copyright (C) 2012-2016 OpenFOAM Foundation
-    Copyright (C) 2015-2020 OpenCFD Ltd.
+    Copyright (C) 2015-2022 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -311,7 +311,6 @@ Foam::surfaceWriters::nastranWriter::nastranWriter()
     fieldMap_(),
     commonGeometry_(false),
     geometryScale_(1),
-    fieldScale_(),
     separator_()
 {
     // if (writeFormat_ == fieldFormat::FREE)
@@ -339,7 +338,6 @@ Foam::surfaceWriters::nastranWriter::nastranWriter
     fieldMap_(),
     commonGeometry_(options.getOrDefault("commonGeometry", false)),
     geometryScale_(options.getOrDefault<scalar>("scale", 1)),
-    fieldScale_(options.subOrEmptyDict("fieldScale")),
     separator_()
 {
     if (writeFormat_ == fieldFormat::FREE)
diff --git a/src/surfMesh/writers/nastran/nastranSurfaceWriter.H b/src/surfMesh/writers/nastran/nastranSurfaceWriter.H
index 8e126e2bcc6..d6661ad5e06 100644
--- a/src/surfMesh/writers/nastran/nastranSurfaceWriter.H
+++ b/src/surfMesh/writers/nastran/nastranSurfaceWriter.H
@@ -6,7 +6,7 @@
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
     Copyright (C) 2012-2016 OpenFOAM Foundation
-    Copyright (C) 2015-2020 OpenCFD Ltd.
+    Copyright (C) 2015-2022 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -36,7 +36,8 @@ Description
         fields   | field pairs for PLOAD2/PLOAD4            | yes   |
         format   | short / long / free                      | no    | long
         scale    | output geometry scaling                  | no    | 1
-        fieldScale | output field scaling (dictionary)      | no    | empty
+        fieldLevel  | Subtract field level before scaling   | no  | empty dict
+        fieldScale  | Output field scaling                  | no  | empty dict
         commonGeometry | use separate geometry files        | no    | false
     \endtable
 
@@ -146,9 +147,6 @@ private:
         //- Output geometry scaling
         const scalar geometryScale_;
 
-        //- Output field scaling
-        const dictionary fieldScale_;
-
         //- Separator (used for free format)
         word separator_;
 
diff --git a/src/surfMesh/writers/nastran/nastranSurfaceWriterImpl.C b/src/surfMesh/writers/nastran/nastranSurfaceWriterImpl.C
index 212b40aa327..9dcb111d5b5 100644
--- a/src/surfMesh/writers/nastran/nastranSurfaceWriterImpl.C
+++ b/src/surfMesh/writers/nastran/nastranSurfaceWriterImpl.C
@@ -6,7 +6,7 @@
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
     Copyright (C) 2012-2016 OpenFOAM Foundation
-    Copyright (C) 2015-2020 OpenCFD Ltd.
+    Copyright (C) 2015-2022 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -223,31 +223,17 @@ Foam::fileName Foam::surfaceWriters::nastranWriter::writeTemplate
     }
     outputFile.ext("bdf");
 
+    // Implicit geometry merge()
+    tmp<Field<Type>> tfield = mergeField(localValues);
 
-    // Output scaling for the variable, but not for integer types.
-    // could also solve with clever templating
-
-    const scalar varScale =
-    (
-        std::is_integral<Type>::value
-      ? scalar(1)
-      : fieldScale_.getOrDefault<scalar>(fieldName, 1)
-    );
+    adjustOutputField(fieldName, tfield.ref());
 
     if (verbose_)
     {
-        Info<< "Writing field " << fieldName;
-        if (!equal(varScale, 1))
-        {
-            Info<< " (scaling " << varScale << ')';
-        }
         Info<< " to " << outputFile << endl;
     }
 
 
-    // Implicit geometry merge()
-    tmp<Field<Type>> tfield = mergeField(localValues) * varScale;
-
     const meshedSurf& surf = surface();
 
     if (Pstream::master() || !parallel_)
diff --git a/src/surfMesh/writers/raw/rawSurfaceWriter.C b/src/surfMesh/writers/raw/rawSurfaceWriter.C
index 19a46ab7232..1d223300169 100644
--- a/src/surfMesh/writers/raw/rawSurfaceWriter.C
+++ b/src/surfMesh/writers/raw/rawSurfaceWriter.C
@@ -62,8 +62,7 @@ Foam::surfaceWriters::rawWriter::rawWriter()
     streamOpt_(),
     precision_(IOstream::defaultPrecision()),
     writeNormal_(false),
-    geometryScale_(1),
-    fieldScale_()
+    geometryScale_(1)
 {}
 
 
@@ -83,8 +82,7 @@ Foam::surfaceWriters::rawWriter::rawWriter
         options.getOrDefault("precision", IOstream::defaultPrecision())
     ),
     writeNormal_(options.getOrDefault("normal", false)),
-    geometryScale_(options.getOrDefault<scalar>("scale", 1)),
-    fieldScale_(options.subOrEmptyDict("fieldScale"))
+    geometryScale_(options.getOrDefault<scalar>("scale", 1))
 {}
 
 
diff --git a/src/surfMesh/writers/raw/rawSurfaceWriter.H b/src/surfMesh/writers/raw/rawSurfaceWriter.H
index c5203b769cb..60855dba70b 100644
--- a/src/surfMesh/writers/raw/rawSurfaceWriter.H
+++ b/src/surfMesh/writers/raw/rawSurfaceWriter.H
@@ -6,7 +6,7 @@
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
     Copyright (C) 2011-2016 OpenFOAM Foundation
-    Copyright (C) 2015-2021 OpenCFD Ltd.
+    Copyright (C) 2015-2022 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -35,8 +35,9 @@ Description
         Property    | Description                           | Required | Default
         compression | Use file compression                  | no  | false
         precision   | Write precision in ascii              | no  | same as IOstream
-        scale       | output geometry scaling               | no  | 1
-        fieldScale  | output field scaling (dictionary)     | no  | empty
+        scale       | Output geometry scaling               | no  | 1
+        fieldLevel  | Subtract field level before scaling   | no  | empty dict
+        fieldScale  | Output field scaling                  | no  | empty dict
         normal      | Write face area normal in output      | no  | false
     \endtable
 
@@ -118,9 +119,6 @@ class rawWriter
         //- Output geometry scaling
         const scalar geometryScale_;
 
-        //- Output field scaling
-        const dictionary fieldScale_;
-
 
     // Private Member Functions
 
diff --git a/src/surfMesh/writers/raw/rawSurfaceWriterImpl.C b/src/surfMesh/writers/raw/rawSurfaceWriterImpl.C
index 7de4bbd6daf..d4a089bd649 100644
--- a/src/surfMesh/writers/raw/rawSurfaceWriterImpl.C
+++ b/src/surfMesh/writers/raw/rawSurfaceWriterImpl.C
@@ -106,30 +106,16 @@ Foam::fileName Foam::surfaceWriters::rawWriter::writeTemplate
     outputFile.ext("raw");
 
 
-    // Output scaling for the variable, but not for integer types.
-    // could also solve with clever templating
+    // Implicit geometry merge()
+    tmp<Field<Type>> tfield = mergeField(localValues);
 
-    const scalar varScale =
-    (
-        std::is_integral<Type>::value
-      ? scalar(1)
-      : fieldScale_.getOrDefault<scalar>(fieldName, 1)
-    );
+    adjustOutputField(fieldName, tfield.ref());
 
     if (verbose_)
     {
-        Info<< "Writing field " << fieldName;
-        if (!equal(varScale, 1))
-        {
-            Info<< " (scaling " << varScale << ')';
-        }
         Info<< " to " << outputFile << endl;
     }
 
-
-    // Implicit geometry merge()
-    tmp<Field<Type>> tfield = mergeField(localValues) * varScale;
-
     const meshedSurf& surf = surface();
 
     if (Pstream::master() || !parallel_)
diff --git a/src/surfMesh/writers/starcd/starcdSurfaceWriter.C b/src/surfMesh/writers/starcd/starcdSurfaceWriter.C
index 898d8d9539e..cc1797fc38d 100644
--- a/src/surfMesh/writers/starcd/starcdSurfaceWriter.C
+++ b/src/surfMesh/writers/starcd/starcdSurfaceWriter.C
@@ -6,7 +6,7 @@
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
     Copyright (C) 2011 OpenFOAM Foundation
-    Copyright (C) 2015-2020 OpenCFD Ltd.
+    Copyright (C) 2015-2022 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -69,8 +69,7 @@ namespace Foam
 Foam::surfaceWriters::starcdWriter::starcdWriter()
 :
     surfaceWriter(),
-    streamOpt_(),
-    fieldScale_()
+    streamOpt_()
 {}
 
 
@@ -84,8 +83,7 @@ Foam::surfaceWriters::starcdWriter::starcdWriter
     (
         IOstreamOption::ASCII,
         IOstreamOption::compressionEnum("compression", options)
-    ),
-    fieldScale_(options.subOrEmptyDict("fieldScale"))
+    )
 {}
 
 
@@ -208,31 +206,17 @@ Foam::fileName Foam::surfaceWriters::starcdWriter::writeTemplate
     outputFile /= fieldName + '_' + outputPath_.name();
     outputFile.ext("usr");
 
+    // Implicit geometry merge()
+    tmp<Field<Type>> tfield = mergeField(localValues);
 
-    // Output scaling for the variable, but not for integer types.
-    // could also solve with clever templating
-
-    const scalar varScale =
-    (
-        std::is_integral<Type>::value
-      ? scalar(1)
-      : fieldScale_.getOrDefault<scalar>(fieldName, 1)
-    );
+    adjustOutputField(fieldName, tfield.ref());
 
     if (verbose_)
     {
-        Info<< "Writing field " << fieldName;
-        if (!equal(varScale, 1))
-        {
-            Info<< " (scaling " << varScale << ')';
-        }
         Info<< " to " << outputFile << endl;
     }
 
 
-    // Implicit geometry merge()
-    tmp<Field<Type>> tfield = mergeField(localValues) * varScale;
-
     const meshedSurf& surf = surface();
 
     if (Pstream::master() || !parallel_)
diff --git a/src/surfMesh/writers/starcd/starcdSurfaceWriter.H b/src/surfMesh/writers/starcd/starcdSurfaceWriter.H
index 1d5d1ec9e8e..9ef62cfb8d7 100644
--- a/src/surfMesh/writers/starcd/starcdSurfaceWriter.H
+++ b/src/surfMesh/writers/starcd/starcdSurfaceWriter.H
@@ -6,7 +6,7 @@
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
     Copyright (C) 2011 OpenFOAM Foundation
-    Copyright (C) 2015-2020 OpenCFD Ltd.
+    Copyright (C) 2015-2022 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -34,7 +34,8 @@ Description
     \table
         Property    | Description                           | Required | Default
         compression | Use file compression                  | no  | false
-        fieldScale  | output field scaling (dictionary)     | no  | empty
+        fieldLevel  | Subtract field level before scaling   | no  | empty dict
+        fieldScale  | Output field scaling                  | no  | empty dict
     \endtable
 
     The geometry is written via the MeshedSurfaceProxy, the fields
@@ -101,9 +102,6 @@ class starcdWriter
         //- Output stream option
         IOstreamOption streamOpt_;
 
-        //- Output field scaling
-        const dictionary fieldScale_;
-
 
     // Private Member Functions
 
diff --git a/src/surfMesh/writers/vtk/vtkSurfaceWriter.C b/src/surfMesh/writers/vtk/vtkSurfaceWriter.C
index 795c929b69b..8fc7b438651 100644
--- a/src/surfMesh/writers/vtk/vtkSurfaceWriter.C
+++ b/src/surfMesh/writers/vtk/vtkSurfaceWriter.C
@@ -67,7 +67,6 @@ Foam::surfaceWriters::vtkWriter::vtkWriter()
     fmtType_(static_cast<unsigned>(vtk::formatType::INLINE_BASE64)),
     precision_(IOstream::defaultPrecision()),
     writeNormal_(false),
-    fieldScale_(),
     writer_(nullptr)
 {}
 
@@ -81,7 +80,6 @@ Foam::surfaceWriters::vtkWriter::vtkWriter
     fmtType_(static_cast<unsigned>(opts.fmt())),
     precision_(opts.precision()),
     writeNormal_(false),
-    fieldScale_(),
     writer_(nullptr)
 {}
 
@@ -98,7 +96,6 @@ Foam::surfaceWriters::vtkWriter::vtkWriter
         options.getOrDefault("precision", IOstream::defaultPrecision())
     ),
     writeNormal_(options.getOrDefault("normal", false)),
-    fieldScale_(options.subOrEmptyDict("fieldScale")),
     writer_(nullptr)
 {
     // format: ascii | binary
@@ -288,31 +285,17 @@ Foam::fileName Foam::surfaceWriters::vtkWriter::writeTemplate
     // Open file, writing geometry (if required)
     fileName outputFile = this->write();
 
+    // Implicit geometry merge()
+    tmp<Field<Type>> tfield = mergeField(localValues);
 
-    // Output scaling for the variable, but not for integer types.
-    // could also solve with clever templating
-
-    const scalar varScale =
-    (
-        std::is_integral<Type>::value
-      ? scalar(1)
-      : fieldScale_.getOrDefault<scalar>(fieldName, 1)
-    );
+    adjustOutputField(fieldName, tfield.ref());
 
     if (verbose_)
     {
-        Info<< "Writing field " << fieldName;
-        if (!equal(varScale, 1))
-        {
-            Info<< " (scaling " << varScale << ')';
-        }
         Info<< " to " << outputFile << endl;
     }
 
 
-    // Implicit geometry merge()
-    tmp<Field<Type>> tfield = mergeField(localValues) * varScale;
-
     if (Pstream::master() || !parallel_)
     {
         if (!nFields_ && writer_->legacy())
diff --git a/src/surfMesh/writers/vtk/vtkSurfaceWriter.H b/src/surfMesh/writers/vtk/vtkSurfaceWriter.H
index 7ded61776f1..6635b945c33 100644
--- a/src/surfMesh/writers/vtk/vtkSurfaceWriter.H
+++ b/src/surfMesh/writers/vtk/vtkSurfaceWriter.H
@@ -36,7 +36,8 @@ Description
         format      | ascii or binary format                | no  | binary
         legacy      | Legacy VTK output                     | no  | false
         precision   | Write precision in ascii         | no | same as IOstream
-        fieldScale  | Output field scaling (dictionary)     | no  | empty
+        fieldLevel  | Subtract field level before scaling   | no  | empty dict
+        fieldScale  | Output field scaling                  | no  | empty dict
         normal      | Write face area-normal in output      | no  | false
     \endtable
 
@@ -49,9 +50,13 @@ Description
             format      binary;
             legacy      false;
             precision   10;
+            fieldLevel
+            {
+                p       1e5;    // Absolute -> gauge [Pa]
+            }
             fieldScale
             {
-               "p.*"   0.01;  // [Pa] -> [mbar]
+                "p.*"   0.01;   // [Pa] -> [mbar]
             }
         }
     }
@@ -114,9 +119,6 @@ class vtkWriter
         //- Output face area normal
         const bool writeNormal_;
 
-        //- Output field scaling
-        const dictionary fieldScale_;
-
         //- Backend writer - master only
         autoPtr<Foam::vtk::surfaceWriter> writer_;
 
diff --git a/src/surfMesh/writers/x3d/x3dSurfaceWriter.C b/src/surfMesh/writers/x3d/x3dSurfaceWriter.C
index 725e8191470..a97f94e9240 100644
--- a/src/surfMesh/writers/x3d/x3dSurfaceWriter.C
+++ b/src/surfMesh/writers/x3d/x3dSurfaceWriter.C
@@ -5,7 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2019-2020 OpenCFD Ltd.
+    Copyright (C) 2019-2022 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -269,15 +269,18 @@ Foam::fileName Foam::surfaceWriters::x3dWriter::writeTemplate
     outputFile /= fieldName + '_' + outputPath_.name();
     outputFile.ext("x3d");
 
+    // Implicit geometry merge()
+    tmp<Field<Type>> tfield = mergeField(localValues);
+
+    adjustOutputField(fieldName, tfield.ref());
+
     if (verbose_)
     {
-        Info<< "Writing field " << fieldName << " to " << outputFile << endl;
+        Info<< " to " << outputFile << endl;
     }
 
-    const meshedSurf& surf = surface();
 
-    // Implicit geometry merge()
-    tmp<Field<Type>> tfield = mergeField(localValues);
+    const meshedSurf& surf = surface();
 
     if (Pstream::master() || !parallel_)
     {
diff --git a/src/surfMesh/writers/x3d/x3dSurfaceWriter.H b/src/surfMesh/writers/x3d/x3dSurfaceWriter.H
index 139c5655ba8..619471e3353 100644
--- a/src/surfMesh/writers/x3d/x3dSurfaceWriter.H
+++ b/src/surfMesh/writers/x3d/x3dSurfaceWriter.H
@@ -5,7 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2019-2020 OpenCFD Ltd.
+    Copyright (C) 2019-2022 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -35,6 +35,8 @@ Description
         compression | Use file compression                  | no  | false
         range       | The min/max range for colour table    | no  | automatic
         colourMap   | The colour map for rendering          | no  | coolToWarm
+        fieldLevel  | Subtract field level before scaling   | no  | empty dict
+        fieldScale  | Output field scaling                  | no  | empty dict
     \endtable
 
     \section Output file locations
@@ -57,6 +59,9 @@ Description
         `-- <field1>_surfaceName.x3d
     \endverbatim
 
+Note
+    The range is applied after any field scaling.
+
 SourceFiles
     x3dSurfaceWriter.C
 
diff --git a/tutorials/compressible/rhoSimpleFoam/squareBend/system/samplingDebug b/tutorials/compressible/rhoSimpleFoam/squareBend/system/samplingDebug
index f3ade8679c6..1859cafeced 100644
--- a/tutorials/compressible/rhoSimpleFoam/squareBend/system/samplingDebug
+++ b/tutorials/compressible/rhoSimpleFoam/squareBend/system/samplingDebug
@@ -10,7 +10,7 @@ debug
     writeControl  timeStep;
     writeInterval 1;
 
-    fields  (rho U);
+    fields  (p rho U);
 
     sampleScheme  cellPoint;
     interpolationScheme cellPoint;
@@ -24,6 +24,11 @@ debug
         {
             collateTimes  true;
             // collateTimes  false;
+            fieldLevel
+            {
+                "p.*"   1e5;
+                U       0;
+            }
         }
         raw
         {
-- 
GitLab