diff --git a/src/functionObjects/forces/Make/files b/src/functionObjects/forces/Make/files
index 310f1d60ba..4d8d3dcd63 100644
--- a/src/functionObjects/forces/Make/files
+++ b/src/functionObjects/forces/Make/files
@@ -1,4 +1,5 @@
forces/forces.C
forceCoeffs/forceCoeffs.C
+propellerInfo/propellerInfo.C
LIB = $(FOAM_LIBBIN)/libforces
diff --git a/src/functionObjects/forces/Make/options b/src/functionObjects/forces/Make/options
index 3be6e09cef..cdab453037 100644
--- a/src/functionObjects/forces/Make/options
+++ b/src/functionObjects/forces/Make/options
@@ -2,6 +2,7 @@ EXE_INC = \
-I$(LIB_SRC)/finiteVolume/lnInclude \
-I$(LIB_SRC)/fileFormats/lnInclude \
-I$(LIB_SRC)/meshTools/lnInclude \
+ -I$(LIB_SRC)/surfMesh/lnInclude \
-I$(LIB_SRC)/thermophysicalModels/basic/lnInclude \
-I$(LIB_SRC)/transportModels \
-I$(LIB_SRC)/transportModels/compressible/lnInclude \
@@ -12,6 +13,7 @@ EXE_INC = \
LIB_LIBS = \
-lfiniteVolume \
-lmeshTools \
+ -lsurfMesh \
-lfluidThermophysicalModels \
-lincompressibleTransportModels \
-lcompressibleTransportModels \
diff --git a/src/functionObjects/forces/propellerInfo/propellerInfo.C b/src/functionObjects/forces/propellerInfo/propellerInfo.C
new file mode 100644
index 0000000000..e696b9d1fb
--- /dev/null
+++ b/src/functionObjects/forces/propellerInfo/propellerInfo.C
@@ -0,0 +1,924 @@
+/*---------------------------------------------------------------------------*\
+ ========= |
+ \\ / F ield | OpenFOAM: The Open Source CFD Toolbox
+ \\ / O peration |
+ \\ / A nd | www.openfoam.com
+ \\/ M anipulation |
+-------------------------------------------------------------------------------
+ Copyright (C) 2021 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 "propellerInfo.H"
+#include "cylindricalCS.H"
+#include "fvMesh.H"
+#include "IOMRFZoneList.H"
+#include "mathematicalConstants.H"
+#include "interpolation.H"
+#include "Function1.H"
+#include "surfaceWriter.H"
+#include "treeDataCell.H"
+#include "addToRunTimeSelectionTable.H"
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace functionObjects
+{
+ defineTypeNameAndDebug(propellerInfo, 0);
+ addToRunTimeSelectionTable(functionObject, propellerInfo, dictionary);
+}
+}
+
+const Foam::Enum
+Foam::functionObjects::propellerInfo::rotationModeNames_
+({
+ { rotationMode::SPECIFIED, "specified" },
+ { rotationMode::MRF, "MRF" },
+});
+
+
+// * * * * * * * * * * * * Protected Member Functions * * * * * * * * * * * //
+
+void Foam::functionObjects::propellerInfo::setCoordinateSystem
+(
+ const dictionary& dict
+)
+{
+ vector origin(Zero);
+ vector axis(Zero);
+
+ switch (rotationMode_)
+ {
+ case rotationMode::SPECIFIED:
+ {
+ origin = dict.get("origin");
+ axis = dict.get("axis");
+ axis.normalise();
+
+ n_ = dict.get("n");
+ break;
+ }
+ case rotationMode::MRF:
+ {
+ MRFName_ = dict.get("MRF");
+ const auto* MRFZones =
+ mesh_.cfindObject("MRFProperties");
+
+ if (!MRFZones)
+ {
+ FatalIOErrorInFunction(dict)
+ << "Unable to find MRFProperties in the database. "
+ << "Is this an MRF case?"
+ << exit(FatalIOError);
+ }
+ const auto& mrf = MRFZones->MRFZoneList::getFromName(MRFName_);
+ vector offset = dict.getOrDefault("originOffset", vector::zero);
+ origin = offset + mrf.origin();
+ axis = mrf.axis();
+
+ // Convert rad/s to revolutions per second
+ n_ = (mrf.Omega() & axis)/constant::mathematical::twoPi;
+ break;
+ }
+ default:
+ {
+ FatalErrorInFunction
+ << "Unhandled enumeration " << rotationModeNames_[rotationMode_]
+ << abort(FatalError);
+ }
+ }
+
+ vector alphaAxis;
+ if (!dict.readIfPresent("alphaAxis", alphaAxis))
+ {
+ // Value has not been set - find vector orthogonal to axis
+
+ vector cand(Zero);
+ scalar minDot = GREAT;
+ for (direction d = 0; d < 3; ++d)
+ {
+ vector test(Zero);
+ test[d] = 1;
+ scalar dotp = mag(test & axis);
+ if (dotp < minDot)
+ {
+ minDot = dotp;
+ cand = test;
+ }
+ }
+
+ alphaAxis = axis ^ cand;
+ }
+
+ alphaAxis.normalise();
+
+ coordSysPtr_.reset(new coordSystem::cylindrical(origin, axis, alphaAxis));
+}
+
+
+void Foam::functionObjects::propellerInfo::setRotationalSpeed()
+{
+ switch (rotationMode_)
+ {
+ case rotationMode::SPECIFIED:
+ {
+ // Set on dictionary re-read
+ break;
+ }
+ case rotationMode::MRF:
+ {
+ const auto* MRFZones =
+ mesh_.cfindObject("MRFProperties");
+
+ if (!MRFZones)
+ {
+ FatalErrorInFunction
+ << "Unable to find MRFProperties in the database. "
+ << "Is this an MRF case?"
+ << exit(FatalError);
+ }
+
+ const auto& mrf = MRFZones->MRFZoneList::getFromName(MRFName_);
+
+ // Convert rad/s to revolutions per second
+ n_ = (mrf.Omega() & mrf.axis())/constant::mathematical::twoPi;
+ break;
+ }
+ default:
+ {
+ FatalErrorInFunction
+ << "Unhandled enumeration " << rotationModeNames_[rotationMode_]
+ << abort(FatalError);
+ }
+ }
+}
+
+
+void Foam::functionObjects::propellerInfo::createFiles()
+{
+ if (!writeToFile())
+ {
+ return;
+ }
+
+ if (writePropellerPerformance_ && !propellerPerformanceFilePtr_)
+ {
+ propellerPerformanceFilePtr_ = createFile("propellerPerformance");
+ auto& os = propellerPerformanceFilePtr_();
+
+ writeHeader(os, "Propeller performance");
+ writeHeaderValue(os, "CofR", coordSysPtr_->origin());
+ writeHeaderValue(os, "Radius", radius_);
+ writeHeaderValue(os, "Axis", coordSysPtr_->e3());
+
+ writeHeader(os, "");
+
+ writeCommented(os, "Time");
+ writeTabbed(os, "n");
+ writeTabbed(os, "URef");
+ writeTabbed(os, "J");
+ writeTabbed(os, "KT");
+ writeTabbed(os, "10*KQ");
+ writeTabbed(os, "eta0");
+ os << nl;
+ }
+
+ if (writeWakeFields_)
+ {
+ if (!wakeFilePtr_) wakeFilePtr_ = createFile("wake");
+ if (!axialWakeFilePtr_) axialWakeFilePtr_ = createFile("axialWake");
+ }
+}
+
+
+const Foam::volVectorField& Foam::functionObjects::propellerInfo::U() const
+{
+ const auto* UPtr = mesh_.cfindObject(UName_);
+
+ if (!UPtr)
+ {
+ FatalErrorInFunction
+ << "Unable to find velocity field " << UName_
+ << " . Available vector fields are: "
+ << mesh_.names()
+ << exit(FatalError);
+ }
+
+ return *UPtr;
+}
+
+
+void Foam::functionObjects::propellerInfo::setSampleDiskGeometry
+(
+ const coordinateSystem& coordSys,
+ const scalar r1,
+ const scalar r2,
+ const scalar nTheta,
+ const label nRadius,
+ faceList& faces,
+ pointField& points
+) const
+{
+ label nPoint = nRadius*nTheta;
+ if (r1 < SMALL)
+ {
+ nPoint += 1; // 1 for origin
+ }
+ else
+ {
+ nPoint += nTheta;
+ }
+ const label nFace = nRadius*nTheta;
+
+ points.setSize(nPoint);
+ faces.setSize(nFace);
+
+ const point& origin = coordSys.origin();
+ const scalar zCoord = 0;
+ label pointi = 0;
+ for (int radiusi = 0; radiusi <= nRadius; ++radiusi)
+ {
+ if (r1 < SMALL && radiusi == 0)
+ {
+ points[pointi++] = origin;
+ }
+ else
+ {
+ const scalar r = r1 + radiusi*(r2 - r1)/nRadius;
+
+ for (label i = 0; i < nTheta; ++i)
+ {
+ point p
+ (
+ r,
+ (i/scalar(nTheta))*constant::mathematical::twoPi,
+ zCoord
+ );
+
+ points[pointi++] = coordSys.globalPosition(p);
+ }
+ }
+ }
+
+
+ const List