ENH: cellDecomposer: functionObject to map fields.

Left out original files
This commit is contained in:
mattijs 2024-06-10 11:22:50 +01:00
parent 5059e1ddec
commit baa8dccb0a
3 changed files with 1005 additions and 0 deletions

View File

@ -0,0 +1,415 @@
/*---------------------------------------------------------------------------*\
========= |
\\ / F ield | OpenFOAM: The Open Source CFD Toolbox
\\ / O peration |
\\ / A nd | www.openfoam.com
\\/ M anipulation |
-------------------------------------------------------------------------------
Copyright (C) 2024 OpenCFD Ltd.
-------------------------------------------------------------------------------
License
This file is part of OpenFOAM.
OpenFOAM is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
along with OpenFOAM. If not, see <http://www.gnu.org/licenses/>.
\*---------------------------------------------------------------------------*/
#include "cellDecomposer.H"
#include "addToRunTimeSelectionTable.H"
#include "polyTopoChange.H"
#include "mapPolyMesh.H"
#include "tetDecomposer.H"
#include "syncTools.H"
#include "dummyTransform.H"
#include "ReadFields.H"
#include "surfaceFields.H"
#include "PackedBoolList.H"
#include "fvMeshTools.H"
#include "cellSetOption.H"
#include "cellBitSet.H"
#include "cellSet.H"
#include "hexMatcher.H"
#include "prismMatcher.H"
#include "pyrMatcher.H"
#include "tetMatcher.H"
#include "OBJstream.H"
// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
namespace Foam
{
namespace functionObjects
{
defineTypeNameAndDebug(cellDecomposer, 0);
addToRunTimeSelectionTable(functionObject, cellDecomposer, dictionary);
}
}
// * * * * * * * * * * * * * Private Member Functions * * * * * * * * * * * //
void Foam::functionObjects::cellDecomposer::makeMesh
(
const dictionary& dict,
const word& regionName
)
{
Info<< name() << ':' << nl
<< " Decomposing cells to region " << regionName << endl;
Info<< incrIndent;
const fv::cellSetOption::selectionModeType selectionMode
(
fv::cellSetOption::selectionModeTypeNames_.get
(
"selectionMode",
dict
)
);
// Start off from existing mesh
polyTopoChange meshMod(mesh_);
tetDecompPtr_.reset(new Foam::tetDecomposer(mesh_));
// Default values
bitSet decomposeCell(mesh_.nCells());
autoPtr<bitSet> decomposeFacePtr;
tetDecomposer::decompositionType decompType
(
tetDecomposer::FACE_CENTRE_TRIS
);
switch (selectionMode)
{
case fv::cellSetOption::smAll:
{
Info<< indent << "- selecting all cells" << endl;
decomposeCell = true;
break;
}
case fv::cellSetOption::smGeometric:
{
Info<< indent << "- selecting cells geometrically" << endl;
decomposeCell =
cellBitSet::select(mesh_, dict.subDict("selection"), true);
break;
}
case fv::cellSetOption::smCellSet:
{
const word selectionName(dict.get<word>("cellSet"));
Info<< indent
<< "- selecting cells using cellSet "
<< selectionName << endl;
decomposeCell.set
(
cellSet(mesh_, selectionName, IOobject::MUST_READ).toc()
);
break;
}
case fv::cellSetOption::smCellZone:
{
wordRes selectionNames;
if
(
!dict.readIfPresent("cellZones", selectionNames)
|| selectionNames.empty()
)
{
selectionNames.resize(1);
dict.readEntry("cellZone", selectionNames.first());
}
Info<< indent
<< "- selecting cells using cellZones "
<< flatOutput(selectionNames) << nl;
const auto& zones = mesh_.cellZones();
// Also handles groups, multiple zones etc ...
labelList zoneIDs = zones.indices(selectionNames);
if (zoneIDs.empty())
{
FatalErrorInFunction
<< "No matching cellZones: "
<< flatOutput(selectionNames) << nl
<< "Valid zones : "
<< flatOutput(zones.names()) << nl
<< "Valid groups: "
<< flatOutput(zones.groupNames())
<< nl
<< exit(FatalError);
}
if (zoneIDs.size() == 1)
{
decomposeCell.set(zones[zoneIDs.first()]);
// TBD: Foam::sort(cells_);
}
else
{
decomposeCell.set(zones.selection(zoneIDs).sortedToc());
}
break;
}
default:
{
FatalErrorInFunction
<< "Unsupported selectionMode "
<< fv::cellSetOption::selectionModeTypeNames_[selectionMode]
<< ". Valid selectionMode types are "
<< fv::cellSetOption::selectionModeTypeNames_
<< exit(FatalError);
}
}
word decompTypeName;
if (dict.readIfPresent("decomposeType", decompTypeName))
{
if (decompTypeName == "polyhedral")
{
// Automatic selection to generate hex/prism/tet only
// - subset decomposeCell to exclude hex/prism/tet
// - set faces to FACE_DIAG_QUADS
// Usually start with cellSetOption::smAll
decomposeFacePtr.reset(new bitSet(mesh_.nFaces()));
auto& decomposeFace = decomposeFacePtr();
decompType = tetDecomposer::FACE_DIAG_QUADS;
bitSet oldDecomposeCell(decomposeCell);
decomposeCell = false;
// Construct shape recognizers
prismMatcher prism;
for (const label celli : oldDecomposeCell)
{
if
(
!hexMatcher::test(mesh_, celli)
&& !tetMatcher::test(mesh_, celli)
&& !pyrMatcher::test(mesh_, celli)
&& !prism.isA(mesh_, celli)
)
{
decomposeCell.set(celli);
decomposeFace.set(mesh_.cells()[celli]);
}
}
// Sync boundary info
syncTools::syncFaceList
(
mesh_,
decomposeFace,
orEqOp<unsigned int>()
);
}
else
{
decompType = tetDecomposer::decompositionTypeNames[decompTypeName];
}
}
// Insert all changes to create tets
if (decomposeFacePtr)
{
if (debug)
{
OBJstream os(mesh_.time().path()/"orig_faces.obj");
os.write
(
UIndirectList<face>
(
mesh_.faces(),
decomposeFacePtr().sortedToc()
)(),
mesh_.points(),
false
);
Pout<< "Written " << meshMod.faces().size()
<< " faces to " << os.name() << endl;
}
tetDecompPtr_().setRefinement
(
decompType, //Foam::tetDecomposer::FACE_CENTRE_TRIS,
decomposeCell,
decomposeFacePtr(),
meshMod
);
}
else
{
tetDecompPtr_().setRefinement
(
decompType, //Foam::tetDecomposer::FACE_CENTRE_TRIS,
decomposeCell,
meshMod
);
}
if (debug)
{
OBJstream os(mesh_.time().path()/"faces.obj");
os.write(meshMod.faces(), pointField(meshMod.points()), false);
Pout<< "Written " << meshMod.faces().size()
<< " faces to " << os.name() << endl;
}
autoPtr<fvMesh> tetMeshPtr;
mapPtr_ = meshMod.makeMesh
(
tetMeshPtr,
IOobject
(
regionName,
mesh_.facesInstance(), // same instance as original mesh
mesh_.time(), //? why same database? TBD.
IOobject::READ_IF_PRESENT, // Read fv* if present
IOobject::AUTO_WRITE,
IOobject::REGISTER
),
mesh_
);
//- Update numbering on tet-decomposition engine
tetDecompPtr_().updateMesh(mapPtr_());
Info<< indent << "Writing decomposed mesh to "
<< tetMeshPtr().objectRegistry::objectRelPath()
<< endl;
tetMeshPtr().write();
fvMeshTools::createDummyFvMeshFiles(mesh_.time(), regionName, true);
// Store new mesh on object registry
tetMeshPtr.ptr()->polyMesh::store();
Info<< decrIndent;
}
// * * * * * * * * * * * * * * * * Constructors * * * * * * * * * * * * * * //
Foam::functionObjects::cellDecomposer::cellDecomposer
(
const word& name,
const Time& runTime,
const dictionary& dict
)
:
fvMeshFunctionObject(name, runTime, dict)
{
read(dict);
}
// * * * * * * * * * * * * * * * Member Functions * * * * * * * * * * * * * //
bool Foam::functionObjects::cellDecomposer::read(const dictionary& dict)
{
if (fvMeshFunctionObject::read(dict))
{
// Generate new tet equivalent mesh. TBD: why meshObject?
//meshObjects::tetDecomposition::New(mesh, dict);
dict_ = dict.optionalSubDict(typeName + "Coeffs");
dict_.readEntry("mapRegion", mapRegion_);
dict_.readEntry("fields", fieldNames_);
makeMesh(dict_, mapRegion_);
}
return true;
}
bool Foam::functionObjects::cellDecomposer::execute()
{
Log << type() << " " << name() << " execute:" << nl;
bool ok = false;
if (mesh_.changing())
{
// Re-do mesh. Currently rebuilds whole mesh. Could move points only
// for mesh motion.
tetDecompPtr_.clear();
mapPtr_.clear();
const_cast<Time&>(this->mesh_.time()).erase(mapRegion_);
makeMesh(dict_, mapRegion_);
}
// Look up
ok = mapFieldType<scalar>() || ok;
ok = mapFieldType<vector>() || ok;
ok = mapFieldType<sphericalTensor>() || ok;
ok = mapFieldType<symmTensor>() || ok;
ok = mapFieldType<tensor>() || ok;
if (log)
{
if (!ok)
{
Info<< " none" << nl;
}
Info<< endl;
}
return true;
}
bool Foam::functionObjects::cellDecomposer::write()
{
Log << type() << " " << name() << " write:" << nl;
bool ok = false;
ok = writeFieldType<scalar>() || ok;
ok = writeFieldType<vector>() || ok;
ok = writeFieldType<sphericalTensor>() || ok;
ok = writeFieldType<symmTensor>() || ok;
ok = writeFieldType<tensor>() || ok;
if (log)
{
if (!ok)
{
Info<< " none" << nl;
}
Info<< endl;
}
return true;
}
// ************************************************************************* //

View File

@ -0,0 +1,226 @@
/*---------------------------------------------------------------------------*\
========= |
\\ / F ield | OpenFOAM: The Open Source CFD Toolbox
\\ / O peration |
\\ / A nd | www.openfoam.com
\\/ M anipulation |
-------------------------------------------------------------------------------
Copyright (C) 2024 OpenCFD Ltd.
-------------------------------------------------------------------------------
License
This file is part of OpenFOAM.
OpenFOAM is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
along with OpenFOAM. If not, see <http://www.gnu.org/licenses/>.
Class
Foam::functionObjects::cellDecomposer
Group
grpFieldFunctionObjects
Description
Maps input fields from local mesh to a secondary mesh at runtime.
The secondary mesh gets created on-the-fly by decomposing the current mesh.
Operands:
\table
Operand | Type | Location
input | {vol,surface}\<Type\>Field <!--
--> | $FOAM_CASE/\<time\>/\<inpField\>
output file | - | -
output field | {vol,surface}\<Type\>Field <!--
--> | $FOAM_CASE/\<time\>/\<outField\>
\endtable
where \c \<Type\>=Scalar/Vector/SphericalTensor/SymmTensor/Tensor.
Usage
Minimal example by using \c system/controlDict.functions:
\verbatim
cellDecomposer1
{
// Mandatory entries (unmodifiable)
type cellDecomposer;
libs (fieldFunctionObjects);
// Mandatory (inherited) entries (runtime modifiable)
fields (<field1> <field2> ... <fieldN>);
mapRegion myTetMesh;
...
// Mandatory entries
// Decompose type:
decomposeType polyhedral;
// Cell set to decompose
selectionMode all;
}
\endverbatim
where the entries mean:
\table
Property | Description | Type | Req'd | Dflt
type | Type name: cellDecomposer | word | yes | -
libs | Library name: fieldFunctionObjects | word | yes | -
fields | Names of operand fields | wordList | yes | -
mapRegion | Name of region to map to | word | yes | -
decomposeType | How to decompose cells | word | yes | -
selectionMode | How to select cells (see fvOption)| word | yes | -
\endtable
decomposeType:
- faceCentre : decompose cells into tets using face centre and cell centre.
(hex becomes 6*4 tets)
- faceDiagonal : decompose cells into tets using face diagonal, similar
to implicit decomposition inside lagrangian tracking.
(hex becomes 6*2 tets)
- pyramid : keep faces intact but create (polygonal-base) pyramids using
cell centre (hex becomes 6 pyramids)
- faceDiagonalQuads : like faceDiagonal but split faces into quads and
triangles instead of just triangles
- polyhedral : like faceDiagonalQuads but only decompose non-hex/prism/tet
cells in selected set. Used to convert polyhedral mesh into
'simple' mesh.
The inherited entries are elaborated in:
- \link functionObject.H \endlink
See also
- Foam::functionObjects::mapFields
SourceFiles
cellDecomposer.C
\*---------------------------------------------------------------------------*/
#ifndef functionObjects_cellDecomposer_H
#define functionObjects_cellDecomposer_H
#include "fvMeshFunctionObject.H"
#include "volFieldsFwd.H"
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
namespace Foam
{
class tetDecomposer;
class mapPolyMesh;
namespace functionObjects
{
/*---------------------------------------------------------------------------*\
Class cellDecomposer Declaration
\*---------------------------------------------------------------------------*/
class cellDecomposer
:
public fvMeshFunctionObject
{
// Private Data
//- Parameter dictionary
dictionary dict_;
//- Name of new mesh
word mapRegion_;
//- List of field names to interpolate
wordRes fieldNames_;
//- Tet decomposer
autoPtr<tetDecomposer> tetDecompPtr_;
//- Map from polyMesh to tet-mesh
autoPtr<mapPolyMesh> mapPtr_;
// Private Member Functions
//- Generate mesh
void makeMesh(const dictionary& dict, const word& name);
//- Helper function to map the \<Type\> fields
template<class Type>
bool mapFieldType() const;
//- Helper function to write the \<Type\> fields
template<class Type>
bool writeFieldType() const;
template<class Type>
tmp<GeometricField<Type, fvPatchField, volMesh>>
interpolate
(
const GeometricField<Type, fvPatchField, volMesh>& vf,
const fvMesh& sMesh,
const labelUList& patchMap,
const labelUList& cellMap,
const labelUList& faceMap,
const bool allowUnmapped
) const;
public:
//- Runtime type information
TypeName("cellDecomposer");
// Constructors
//- Construct from Time and dictionary
cellDecomposer
(
const word& name,
const Time& runTime,
const dictionary& dict
);
//- Destructor
virtual ~cellDecomposer() = default;
// Member Functions
//- Read the cellDecomposer data
virtual bool read(const dictionary& dict);
//- Execute
virtual bool execute();
//- Write
virtual bool write();
};
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
} // End namespace functionObjects
} // End namespace Foam
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
#ifdef NoRepository
#include "cellDecomposerTemplates.C"
#endif
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
#endif
// ************************************************************************* //

View File

@ -0,0 +1,364 @@
/*---------------------------------------------------------------------------*\
========= |
\\ / F ield | OpenFOAM: The Open Source CFD Toolbox
\\ / O peration |
\\ / A nd | www.openfoam.com
\\/ M anipulation |
-------------------------------------------------------------------------------
Copyright (C) 2024 OpenCFD Ltd.
-------------------------------------------------------------------------------
License
This file is part of OpenFOAM.
OpenFOAM is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License
along with OpenFOAM. If not, see <http://www.gnu.org/licenses/>.
\*---------------------------------------------------------------------------*/
#include "fvMesh.H"
#include "emptyFvPatchFields.H"
#include "directFvPatchFieldMapper.H"
#include "mapPolyMesh.H"
//#include "polyPatch.H"
//#include "lduSchedule.H"
//#include "meshToMesh.H"
//template<class Type>
//void Foam::functionObjects::cellDecomposer::evaluateConstraintTypes
//(
// GeometricField<Type, fvPatchField, volMesh>& fld
//) const
//{
// auto& fldBf = fld.boundaryFieldRef();
//
// const UPstream::commsTypes commsType = UPstream::defaultCommsType;
// const label startOfRequests = UPstream::nRequests();
//
// if
// (
// commsType == UPstream::commsTypes::blocking
// || commsType == UPstream::commsTypes::nonBlocking
// )
// {
// forAll(fldBf, patchi)
// {
// fvPatchField<Type>& tgtField = fldBf[patchi];
//
// if
// (
// tgtField.type() == tgtField.patch().patch().type()
// && polyPatch::constraintType(tgtField.patch().patch().type())
// )
// {
// tgtField.initEvaluate(commsType);
// }
// }
//
// // Wait for outstanding requests
// if (commsType == UPstream::commsTypes::nonBlocking)
// {
// UPstream::waitRequests(startOfRequests);
// }
//
// forAll(fldBf, patchi)
// {
// fvPatchField<Type>& tgtField = fldBf[patchi];
//
// if
// (
// tgtField.type() == tgtField.patch().patch().type()
// && polyPatch::constraintType(tgtField.patch().patch().type())
// )
// {
// tgtField.evaluate(commsType);
// }
// }
// }
// else if (commsType == UPstream::commsTypes::scheduled)
// {
// const lduSchedule& patchSchedule =
// fld.mesh().globalData().patchSchedule();
//
// for (const auto& schedEval : patchSchedule)
// {
// const label patchi = schedEval.patch;
//
// fvPatchField<Type>& tgtField = fldBf[patchi];
//
// if
// (
// tgtField.type() == tgtField.patch().patch().type()
// && polyPatch::constraintType(tgtField.patch().patch().type())
// )
// {
// if (schedEval.init)
// {
// tgtField.initEvaluate(commsType);
// }
// else
// {
// tgtField.evaluate(commsType);
// }
// }
// }
// }
//}
template<class Type>
Foam::tmp
<
Foam::GeometricField<Type, Foam::fvPatchField, Foam::volMesh>
>
Foam::functionObjects::cellDecomposer::interpolate
(
const GeometricField<Type, fvPatchField, volMesh>& vf,
const fvMesh& sMesh,
const labelUList& patchMap,
const labelUList& cellMap,
const labelUList& faceMap,
const bool allowUnmapped
) const
{
// 1. Create the complete field with dummy patch fields
PtrList<fvPatchField<Type>> patchFields(patchMap.size());
forAll(patchFields, patchi)
{
// Set the first one by hand as it corresponds to the
// exposed internal faces. Additional interpolation can be put here
// as necessary.
if (patchMap[patchi] == -1)
{
patchFields.set
(
patchi,
new emptyFvPatchField<Type>
(
sMesh.boundary()[patchi],
fvPatchField<Type>::Internal::null()
)
);
}
else
{
patchFields.set
(
patchi,
fvPatchField<Type>::New
(
fvPatchFieldBase::calculatedType(),
sMesh.boundary()[patchi],
fvPatchField<Type>::Internal::null()
)
);
}
}
auto tresult = tmp<GeometricField<Type, fvPatchField, volMesh>>::New
(
IOobject
(
"subset"+vf.name(),
sMesh.time().timeName(),
sMesh,
IOobject::NO_READ,
IOobject::NO_WRITE
),
sMesh,
vf.dimensions(),
Field<Type>(vf.primitiveField(), cellMap),
patchFields
);
auto& result = tresult.ref();
result.oriented() = vf.oriented();
// 2. Change the fvPatchFields to the correct type using a mapper
// constructor (with reference to the now correct internal field)
auto& bf = result.boundaryFieldRef();
forAll(bf, patchi)
{
const label basePatchId = patchMap[patchi];
if (basePatchId != -1)
{
// Construct addressing
const fvPatch& subPatch = sMesh.boundary()[patchi];
const fvPatch& basePatch = vf.mesh().boundary()[basePatchId];
const label baseStart = basePatch.start();
const label baseSize = basePatch.size();
labelList directAddressing(subPatch.size());
forAll(directAddressing, i)
{
const label baseFacei = faceMap[subPatch.start()+i];
if (baseFacei >= baseStart && baseFacei < baseStart+baseSize)
{
directAddressing[i] = baseFacei-baseStart;
}
else
{
// Mapped from internal face. Do what? Leave up to
// fvPatchField
directAddressing[i] = -1;
}
}
directFvPatchFieldMapper mapper(directAddressing);
// allowUnmapped : special mode for if we do not want to be
// warned for unmapped faces (e.g. from fvMeshDistribute).
const bool hasUnmapped = mapper.hasUnmapped();
if (allowUnmapped)
{
mapper.hasUnmapped() = false;
}
bf.set
(
patchi,
fvPatchField<Type>::New
(
vf.boundaryField()[basePatchId],
subPatch,
result.internalField(),
mapper
)
);
if (allowUnmapped && hasUnmapped)
{
// Set unmapped values to zeroGradient. This is the default
// action for unmapped fvPatchFields. Note that this bypasses
// any special logic for handling unmapped fvPatchFields but
// since this is only used inside fvMeshDistribute ...
tmp<Field<Type>> tfld(bf[patchi].patchInternalField());
const Field<Type>& fld = tfld();
Field<Type> value(bf[patchi]);
forAll(directAddressing, i)
{
if (directAddressing[i] == -1)
{
value[i] = fld[i];
}
}
bf[patchi].fvPatchField<Type>::operator=(value);
}
}
}
return tresult;
}
template<class Type>
bool Foam::functionObjects::cellDecomposer::mapFieldType() const
{
typedef GeometricField<Type, fvPatchField, volMesh> VolFieldType;
const fvMesh& mapRegion =
this->mesh_.time().lookupObject<fvMesh>(mapRegion_);
const labelList patchMap(identity(mapRegion.boundaryMesh().size()));
const wordList fieldNames
(
this->mesh_.sortedNames<VolFieldType>(fieldNames_)
);
const bool processed = !fieldNames.empty();
for (const word& fieldName : fieldNames)
{
const VolFieldType& field = lookupObject<VolFieldType>(fieldName);
auto* mapFieldPtr = mapRegion.getObjectPtr<VolFieldType>(fieldName);
if (!mapFieldPtr)
{
mapFieldPtr = new VolFieldType
(
IOobject
(
fieldName,
time_.timeName(),
mapRegion,
IOobject::NO_READ,
IOobject::NO_WRITE,
IOobject::REGISTER
),
mapRegion,
dimensioned<Type>(field.dimensions(), Zero)
);
mapFieldPtr->store();
}
auto& mappedField = *mapFieldPtr;
mappedField = interpolate
(
field,
mapRegion,
patchMap,
mapPtr_().cellMap(),
mapPtr_().faceMap(),
false //allowUnmapped
);
Log << " " << fieldName << ": interpolated";
//evaluateConstraintTypes(mappedField);
}
return processed;
}
template<class Type>
bool Foam::functionObjects::cellDecomposer::writeFieldType() const
{
typedef GeometricField<Type, fvPatchField, volMesh> VolFieldType;
const fvMesh& mapRegion =
this->mesh_.time().lookupObject<fvMesh>(mapRegion_);
const wordList fieldNames
(
this->mesh_.sortedNames<VolFieldType>(fieldNames_)
);
const bool processed = !fieldNames.empty();
for (const word& fieldName : fieldNames)
{
const VolFieldType& mappedField =
mapRegion.template lookupObject<VolFieldType>(fieldName);
mappedField.write();
Log << " " << fieldName << ": written";
}
return processed;
}
// ************************************************************************* //