/*---------------------------------------------------------------------------*\
========= |
\\ / F ield | OpenFOAM: The Open Source CFD Toolbox
\\ / O peration |
\\ / A nd | www.openfoam.com
\\/ M anipulation |
-------------------------------------------------------------------------------
Copyright (C) 2011-2016 OpenFOAM Foundation
Copyright (C) 2016-2023 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 .
\*---------------------------------------------------------------------------*/
#include "cyclicAMIPolyPatch.H"
#include "SubField.H"
#include "Time.H"
#include "unitConversion.H"
#include "OFstream.H"
#include "meshTools.H"
#include "addToRunTimeSelectionTable.H"
#include "cyclicPolyPatch.H"
// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
namespace Foam
{
defineTypeNameAndDebug(cyclicAMIPolyPatch, 0);
addToRunTimeSelectionTable(polyPatch, cyclicAMIPolyPatch, word);
addToRunTimeSelectionTable(polyPatch, cyclicAMIPolyPatch, dictionary);
}
const Foam::scalar Foam::cyclicAMIPolyPatch::tolerance_ = 1e-10;
// * * * * * * * * * * * * Private Member Functions * * * * * * * * * * * * //
Foam::vector Foam::cyclicAMIPolyPatch::findFaceNormalMaxRadius
(
const pointField& faceCentres
) const
{
// Determine a face furthest away from the axis
const vectorField n((faceCentres - rotationCentre_) ^ rotationAxis_);
const scalarField magRadSqr(magSqr(n));
label facei = findMax(magRadSqr);
DebugInFunction
<< "Patch: " << name() << nl
<< " rotFace = " << facei << nl
<< " point = " << faceCentres[facei] << nl
<< " distance = " << Foam::sqrt(magRadSqr[facei])
<< endl;
return n[facei];
}
void Foam::cyclicAMIPolyPatch::calcTransforms
(
const primitivePatch& half0,
const pointField& half0Ctrs,
const vectorField& half0Areas,
const pointField& half1Ctrs,
const vectorField& half1Areas
)
{
if (transform() != neighbPatch().transform())
{
FatalErrorInFunction
<< "Patch " << name()
<< " has transform type " << transformTypeNames[transform()]
<< ", neighbour patch " << neighbPatchName()
<< " has transform type "
<< neighbPatch().transformTypeNames[neighbPatch().transform()]
<< exit(FatalError);
}
// Calculate transformation tensors
switch (transform())
{
case ROTATIONAL:
{
tensor revT = Zero;
if (rotationAngleDefined_)
{
const tensor T(rotationAxis_*rotationAxis_);
const tensor S
(
0, -rotationAxis_.z(), rotationAxis_.y(),
rotationAxis_.z(), 0, -rotationAxis_.x(),
-rotationAxis_.y(), rotationAxis_.x(), 0
);
const tensor revTPos
(
T
+ cos(rotationAngle_)*(tensor::I - T)
+ sin(rotationAngle_)*S
);
const tensor revTNeg
(
T
+ cos(-rotationAngle_)*(tensor::I - T)
+ sin(-rotationAngle_)*S
);
// Check - assume correct angle when difference in face areas
// is the smallest
const vector transformedAreaPos = gSum(half1Areas & revTPos);
const vector transformedAreaNeg = gSum(half1Areas & revTNeg);
const vector area0 = gSum(half0Areas);
const scalar magArea0 = mag(area0) + ROOTVSMALL;
// Areas have opposite sign, so sum should be zero when correct
// rotation applied
const scalar errorPos = mag(transformedAreaPos + area0);
const scalar errorNeg = mag(transformedAreaNeg + area0);
const scalar normErrorPos = errorPos/magArea0;
const scalar normErrorNeg = errorNeg/magArea0;
if (errorPos > errorNeg && normErrorNeg < matchTolerance())
{
revT = revTNeg;
rotationAngle_ *= -1;
}
else
{
revT = revTPos;
}
const scalar areaError = min(normErrorPos, normErrorNeg);
if (areaError > matchTolerance())
{
WarningInFunction
<< "Patch areas are not consistent within "
<< 100*matchTolerance()
<< " % indicating a possible error in the specified "
<< "angle of rotation" << nl
<< " owner patch : " << name() << nl
<< " neighbour patch : " << neighbPatch().name()
<< nl
<< " angle : "
<< radToDeg(rotationAngle_) << " deg" << nl
<< " area error : " << 100*areaError << " %"
<< " match tolerance : " << matchTolerance()
<< endl;
}
if (debug)
{
scalar theta = radToDeg(rotationAngle_);
Pout<< "cyclicAMIPolyPatch::calcTransforms: patch:"
<< name()
<< " Specified rotation:"
<< " swept angle: " << theta << " [deg]"
<< " reverse transform: " << revT
<< endl;
}
}
else
{
point n0 = Zero;
point n1 = Zero;
if (half0Ctrs.size())
{
n0 = findFaceNormalMaxRadius(half0Ctrs);
}
if (half1Ctrs.size())
{
n1 = -findFaceNormalMaxRadius(half1Ctrs);
}
reduce(n0, maxMagSqrOp());
reduce(n1, maxMagSqrOp());
n0.normalise();
n1.normalise();
// Extended tensor from two local coordinate systems calculated
// using normal and rotation axis
const tensor E0
(
rotationAxis_,
(n0 ^ rotationAxis_),
n0
);
const tensor E1
(
rotationAxis_,
(-n1 ^ rotationAxis_),
-n1
);
revT = E1.T() & E0;
if (debug)
{
scalar theta = radToDeg(acos(-(n0 & n1)));
Pout<< "cyclicAMIPolyPatch::calcTransforms: patch:"
<< name()
<< " Specified rotation:"
<< " n0:" << n0 << " n1:" << n1
<< " swept angle: " << theta << " [deg]"
<< " reverse transform: " << revT
<< endl;
}
}
const_cast(forwardT()) = tensorField(1, revT.T());
const_cast(reverseT()) = tensorField(1, revT);
const_cast(separation()).clear();
const_cast(collocated()) = boolList(1, false);
break;
}
case TRANSLATIONAL:
{
if (debug)
{
Pout<< "cyclicAMIPolyPatch::calcTransforms : patch:" << name()
<< " Specified translation : " << separationVector_
<< endl;
}
const_cast(forwardT()).clear();
const_cast(reverseT()).clear();
const_cast(separation()) = vectorField
(
1,
separationVector_
);
const_cast(collocated()) = boolList(1, false);
break;
}
default:
{
if (debug)
{
Pout<< "patch:" << name()
<< " Assuming cyclic AMI pairs are colocated" << endl;
}
const_cast(forwardT()).clear();
const_cast(reverseT()).clear();
const_cast(separation()).clear();
const_cast(collocated()) = boolList(1, true);
break;
}
}
if (debug)
{
Pout<< "patch: " << name() << nl
<< " forwardT = " << forwardT() << nl
<< " reverseT = " << reverseT() << nl
<< " separation = " << separation() << nl
<< " collocated = " << collocated() << nl << endl;
}
}
Foam::autoPtr
Foam::cyclicAMIPolyPatch::cylindricalCS() const
{
const label periodicID = periodicPatchID();
if (periodicID != -1)
{
// Get the periodic patch
const coupledPolyPatch& perPp =
refCast(boundaryMesh()[periodicID]);
if (!perPp.parallel())
{
vector axis(Zero);
point axisPoint(Zero);
if
(
const cyclicPolyPatch* cpp = isA(perPp)
)
{
axis = cpp->rotationAxis();
axisPoint = cpp->rotationCentre();
}
else if
(
const cyclicAMIPolyPatch* cpp = isA(perPp)
)
{
axis = cpp->rotationAxis();
axisPoint = cpp->rotationCentre();
}
else
{
FatalErrorInFunction << "On patch " << name()
<< " have unsupported periodicPatch " << perPp.name()
<< exit(FatalError);
}
return autoPtr::New(axisPoint, axis);
}
}
return nullptr;
}
// * * * * * * * * * * * Protected Member Functions * * * * * * * * * * * * //
const Foam::autoPtr&
Foam::cyclicAMIPolyPatch::surfPtr() const
{
const word surfType(surfDict_.getOrDefault("type", "none"));
if (!surfPtr_ && owner() && surfType != "none")
{
word surfName(surfDict_.getOrDefault("name", name()));
const polyMesh& mesh = boundaryMesh().mesh();
surfPtr_ =
searchableSurface::New
(
surfType,
IOobject
(
surfName,
mesh.time().constant(),
"triSurface",
mesh,
IOobject::MUST_READ,
IOobject::NO_WRITE
),
surfDict_
);
}
return surfPtr_;
}
void Foam::cyclicAMIPolyPatch::resetAMI() const
{
resetAMI(boundaryMesh().mesh().points());
}
void Foam::cyclicAMIPolyPatch::resetAMI(const UList& points) const
{
DebugInFunction << endl;
if (!owner())
{
return;
}
const cyclicAMIPolyPatch& nbr = neighbPatch();
const pointField srcPoints(points, meshPoints());
pointField nbrPoints(points, nbr.meshPoints());
Info<< "AMI: Creating AMI for source:" << name()
<< " and target:" << nbr.name() << endl;
if (debug)
{
const Time& t = boundaryMesh().mesh().time();
OFstream os(t.path()/name() + "_neighbourPatch-org.obj");
meshTools::writeOBJ(os, neighbPatch().localFaces(), nbrPoints);
}
label patchSize0 = size();
label nbrPatchSize0 = nbr.size();
if (createAMIFaces_)
{
// AMI is created based on the original patch faces (non-extended patch)
if (srcFaceIDs_.size())
{
patchSize0 = srcFaceIDs_.size();
}
if (tgtFaceIDs_.size())
{
nbrPatchSize0 = tgtFaceIDs_.size();
}
}
// Transform neighbour patch to local system
transformPosition(nbrPoints);
primitivePatch nbrPatch0
(
SubList(nbr.localFaces(), nbrPatchSize0),
nbrPoints
);
primitivePatch patch0
(
SubList(localFaces(), patchSize0),
srcPoints
);
if (debug)
{
const Time& t = boundaryMesh().mesh().time();
OFstream osN(t.path()/name() + "_neighbourPatch-trans.obj");
meshTools::writeOBJ(osN, nbrPatch0.localFaces(), nbrPoints);
OFstream osO(t.path()/name() + "_ownerPatch.obj");
meshTools::writeOBJ(osO, this->localFaces(), localPoints());
}
// Construct/apply AMI interpolation to determine addressing and weights
AMIPtr_->upToDate(false);
// Note: e.g. redistributePar might construct the AMI with a different
// worldComm so reset it to the mesh.comm.
AMIPtr_->comm(boundaryMesh().mesh().comm());
AMIPtr_->calculate(patch0, nbrPatch0, surfPtr());
if (debug)
{
AMIPtr_->checkSymmetricWeights(true);
}
}
void Foam::cyclicAMIPolyPatch::calcTransforms()
{
DebugInFunction << endl;
const cyclicAMIPolyPatch& half0 = *this;
vectorField half0Areas(half0.size());
forAll(half0, facei)
{
half0Areas[facei] = half0[facei].areaNormal(half0.points());
}
const cyclicAMIPolyPatch& half1 = neighbPatch();
vectorField half1Areas(half1.size());
forAll(half1, facei)
{
half1Areas[facei] = half1[facei].areaNormal(half1.points());
}
calcTransforms
(
half0,
half0.faceCentres(),
half0Areas,
half1.faceCentres(),
half1Areas
);
DebugPout
<< "calcTransforms() : patch: " << name() << nl
<< " forwardT = " << forwardT() << nl
<< " reverseT = " << reverseT() << nl
<< " separation = " << separation() << nl
<< " collocated = " << collocated() << nl << endl;
}
void Foam::cyclicAMIPolyPatch::initGeometry(PstreamBuffers& pBufs)
{
DebugInFunction << endl;
// Flag AMI as needing update
AMIPtr_->upToDate(false);
polyPatch::initGeometry(pBufs);
// Early calculation of transforms so e.g. cyclicACMI can use them.
// Note: also triggers primitiveMesh face centre. Note that cell
// centres should -not- be calculated
// since e.g. cyclicACMI overrides face areas
calcTransforms();
}
void Foam::cyclicAMIPolyPatch::calcGeometry(PstreamBuffers& pBufs)
{
DebugInFunction << endl;
}
void Foam::cyclicAMIPolyPatch::initMovePoints
(
PstreamBuffers& pBufs,
const pointField& p
)
{
DebugInFunction << endl;
// See below. Clear out any local geometry
primitivePatch::movePoints(p);
// Note: processorPolyPatch::initMovePoints calls
// processorPolyPatch::initGeometry which will trigger calculation of
// patch faceCentres() and cell volumes...
if (createAMIFaces_)
{
// Note: AMI should have been updated in setTopology
// faceAreas() and faceCentres() have been reset and will be
// recalculated on-demand using the mesh points and no longer
// correspond to the scaled areas!
restoreScaledGeometry();
// deltas need to be recalculated to use new face centres!
}
else
{
AMIPtr_->upToDate(false);
}
// Early calculation of transforms. See above.
calcTransforms();
}
void Foam::cyclicAMIPolyPatch::movePoints
(
PstreamBuffers& pBufs,
const pointField& p
)
{
DebugInFunction << endl;
// Note: not calling movePoints since this will undo our manipulations!
// polyPatch::movePoints(pBufs, p);
/*
polyPatch::movePoints
-> primitivePatch::movePoints
-> primitivePatch::clearGeom:
localPointsPtr_.reset(nullptr);
faceCentresPtr_.reset(nullptr);
faceAreasPtr_.reset(nullptr);
magFaceAreasPtr_.reset(nullptr);
faceNormalsPtr_.reset(nullptr);
pointNormalsPtr_.reset(nullptr);
*/
}
void Foam::cyclicAMIPolyPatch::initUpdateMesh(PstreamBuffers& pBufs)
{
DebugInFunction << endl;
polyPatch::initUpdateMesh(pBufs);
if (createAMIFaces_ && boundaryMesh().mesh().topoChanging() && owner())
{
setAMIFaces();
}
}
void Foam::cyclicAMIPolyPatch::updateMesh(PstreamBuffers& pBufs)
{
DebugInFunction << endl;
// Note: this clears out cellCentres(), faceCentres() and faceAreas()
polyPatch::updateMesh(pBufs);
}
void Foam::cyclicAMIPolyPatch::clearGeom()
{
DebugInFunction << endl;
if (!updatingAMI_)
{
AMIPtr_->upToDate(false);
}
polyPatch::clearGeom();
}
// * * * * * * * * * * * * * * Constructors * * * * * * * * * * * * * * * * //
Foam::cyclicAMIPolyPatch::cyclicAMIPolyPatch
(
const word& name,
const label size,
const label start,
const label index,
const polyBoundaryMesh& bm,
const word& patchType,
const transformType transform,
const word& defaultAMIMethod
)
:
coupledPolyPatch(name, size, start, index, bm, patchType, transform),
nbrPatchName_(),
nbrPatchID_(-1),
fraction_(Zero),
rotationAxis_(Zero),
rotationCentre_(Zero),
rotationAngleDefined_(false),
rotationAngle_(0.0),
separationVector_(Zero),
periodicPatchName_(),
periodicPatchID_(-1),
AMIPtr_(AMIInterpolation::New(defaultAMIMethod)),
surfDict_(fileName("surface")),
surfPtr_(nullptr),
createAMIFaces_(false),
moveFaceCentres_(false),
updatingAMI_(true),
srcFaceIDs_(),
tgtFaceIDs_(),
faceAreas0_(),
faceCentres0_()
{
// Neighbour patch might not be valid yet so no transformation
// calculation possible
}
Foam::cyclicAMIPolyPatch::cyclicAMIPolyPatch
(
const word& name,
const dictionary& dict,
const label index,
const polyBoundaryMesh& bm,
const word& patchType,
const word& defaultAMIMethod
)
:
coupledPolyPatch(name, dict, index, bm, patchType),
nbrPatchName_(dict.getOrDefault("neighbourPatch", word::null)),
coupleGroup_(dict),
nbrPatchID_(-1),
fraction_(dict.getOrDefault("fraction", Zero)),
rotationAxis_(Zero),
rotationCentre_(Zero),
rotationAngleDefined_(false),
rotationAngle_(0.0),
separationVector_(Zero),
periodicPatchName_(dict.getOrDefault("periodicPatch", word::null)),
periodicPatchID_(-1),
AMIPtr_
(
AMIInterpolation::New
(
dict.getOrDefault("AMIMethod", defaultAMIMethod),
dict,
dict.getOrDefault("flipNormals", false)
)
),
surfDict_(dict.subOrEmptyDict("surface")),
surfPtr_(nullptr),
createAMIFaces_(dict.getOrDefault("createAMIFaces", false)),
moveFaceCentres_(false),
updatingAMI_(true),
srcFaceIDs_(),
tgtFaceIDs_(),
faceAreas0_(),
faceCentres0_()
{
if (nbrPatchName_.empty() && !coupleGroup_.good())
{
FatalIOErrorInFunction(dict)
<< "No \"neighbourPatch\" or \"coupleGroup\" provided."
<< exit(FatalIOError);
}
if (nbrPatchName_ == name)
{
FatalIOErrorInFunction(dict)
<< "Neighbour patch name " << nbrPatchName_
<< " cannot be the same as this patch " << name
<< exit(FatalIOError);
}
switch (transform())
{
case ROTATIONAL:
{
dict.readEntry("rotationAxis", rotationAxis_);
dict.readEntry("rotationCentre", rotationCentre_);
if (dict.readIfPresent("rotationAngle", rotationAngle_))
{
rotationAngleDefined_ = true;
rotationAngle_ = degToRad(rotationAngle_);
if (debug)
{
Info<< "rotationAngle: " << rotationAngle_ << " [rad]"
<< endl;
}
}
scalar magRot = mag(rotationAxis_);
if (magRot < SMALL)
{
FatalIOErrorInFunction(dict)
<< "Illegal rotationAxis " << rotationAxis_ << endl
<< "Please supply a non-zero vector."
<< exit(FatalIOError);
}
rotationAxis_ /= magRot;
break;
}
case TRANSLATIONAL:
{
dict.readEntry("separationVector", separationVector_);
break;
}
default:
{
// No additional info required
}
}
// Neighbour patch might not be valid yet so no transformation
// calculation possible
// If topology change, recover the sizes of the original patches and
// read additional controls
if (createAMIFaces_)
{
srcFaceIDs_.setSize(dict.get