diff --git a/applications/utilities/mesh/manipulation/createEngineZones/Make/files b/applications/utilities/mesh/manipulation/createEngineZones/Make/files
new file mode 100644
index 0000000000..adfb4cabd8
--- /dev/null
+++ b/applications/utilities/mesh/manipulation/createEngineZones/Make/files
@@ -0,0 +1,3 @@
+createEngineZones.C
+
+EXE = $(FOAM_APPBIN)/createEngineZones
diff --git a/applications/utilities/mesh/manipulation/createEngineZones/Make/options b/applications/utilities/mesh/manipulation/createEngineZones/Make/options
new file mode 100644
index 0000000000..78d2b36e0b
--- /dev/null
+++ b/applications/utilities/mesh/manipulation/createEngineZones/Make/options
@@ -0,0 +1,12 @@
+EXE_INC = \
+ -I$(LIB_SRC)/fvMeshMovers/multiValveEngine/lnInclude \
+ -I$(LIB_SRC)/meshTools/lnInclude \
+ -I$(LIB_SRC)/polyTopoChange/lnInclude \
+ -I$(LIB_SRC)/finiteVolume/lnInclude
+
+EXE_LIBS = \
+ -lmeshTools \
+ -lpolyTopoChange \
+ -lfiniteVolume \
+ -lgenericFvFields \
+ -lfvMeshMoversMultiValveEngine
diff --git a/applications/utilities/mesh/manipulation/createEngineZones/createEngineZones.C b/applications/utilities/mesh/manipulation/createEngineZones/createEngineZones.C
new file mode 100644
index 0000000000..731c56cfed
--- /dev/null
+++ b/applications/utilities/mesh/manipulation/createEngineZones/createEngineZones.C
@@ -0,0 +1,267 @@
+/*---------------------------------------------------------------------------*\
+ ========= |
+ \\ / F ield | OpenFOAM: The Open Source CFD Toolbox
+ \\ / O peration | Website: https://openfoam.org
+ \\ / A nd | Copyright (C) 2024 OpenFOAM Foundation
+ \\/ M anipulation |
+-------------------------------------------------------------------------------
+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 .
+
+Application
+ createEngineZones
+
+Description
+ Utility to create the cylinder head and piston bowl pointZones
+
+\*---------------------------------------------------------------------------*/
+
+#include "argList.H"
+#include "Time.H"
+#include "multiValveEngine.H"
+#include "pistonPointEdgeData.H"
+#include "PointEdgeWave.H"
+
+using namespace Foam;
+
+void calcCylinderHeadPoints(const fvMeshMovers::multiValveEngine& mve)
+{
+ const fvMesh& mesh = mve.mesh();
+
+ const pointField& points = mesh.points();
+ const polyBoundaryMesh& pbm = mesh.boundaryMesh();
+
+ // Set the cylinderHead point flags
+ boolList cylinderHeadPoints(points.size(), false);
+ forAllConstIter(labelHashSet, mve.staticPatchSet, iter)
+ {
+ UIndirectList
+ (
+ cylinderHeadPoints,
+ pbm[iter.key()].meshPoints()
+ ) = true;
+ }
+
+ // Set the liner point flags
+ boolList linerPoints(points.size(), false);
+ forAllConstIter(labelHashSet, mve.linerPatchSet, iter)
+ {
+ UIndirectList
+ (
+ linerPoints,
+ pbm[iter.key()].meshPoints()
+ ) = true;
+ }
+
+ // Find the minimum axis_ coordinate of the cylinder head region
+ // from the bottom of the intersections with the liner
+ // for which both the cylinderHead and liner point flags are true
+ // Assumes the piston moves in the positive axis direction
+
+ scalar minZ = great;
+ forAll(cylinderHeadPoints, pi)
+ {
+ if (cylinderHeadPoints[pi] && linerPoints[pi])
+ {
+ minZ = min(mve.piston.axis & points[pi], minZ);
+ }
+ }
+ reduce(minZ, maxOp());
+
+ // Create the cylinderHead point set
+ labelHashSet cylinderHeadPointSet;
+ forAll(points, pi)
+ {
+ if ((mve.piston.axis & points[pi]) > minZ)
+ {
+ cylinderHeadPointSet.insert(pi);
+ }
+ }
+
+ // Add the cylinderHead pointZone
+ mesh.pointZones().append
+ (
+ new pointZone
+ (
+ mve.cylinderHeadName,
+ cylinderHeadPointSet.toc(),
+ mesh.pointZones()
+ )
+ );
+}
+
+
+void calcPistonBowlPoints(const fvMeshMovers::multiValveEngine& mve)
+{
+ const fvMesh& mesh = mve.mesh();
+ const fvMeshMovers::multiValveEngine::pistonObject& piston = mve.piston;
+
+ const polyBoundaryMesh& pbm = mesh.boundaryMesh();
+
+ // Find the maximum axis coordinate of the piston patch-set
+ // Assumes the piston moves in the positive axis direction
+ scalar maxZ = -great;
+ forAllConstIter(labelHashSet, piston.patchSet, iter)
+ {
+ const label patchi = iter.key();
+ if (pbm[patchi].localPoints().size())
+ {
+ maxZ = max(maxZ, max(piston.axis & pbm[patchi].localPoints()));
+ }
+ }
+ reduce(maxZ, maxOp());
+
+
+ // Search for points starting at the piston surface and stopping at maxZ
+ // using a PointEdgeWave
+
+ label nPistonPatchPoints = 0;
+ forAllConstIter(labelHashSet, piston.patchSet, iter)
+ {
+ const label patchi = iter.key();
+ nPistonPatchPoints += pbm[patchi].meshPoints().size();
+ }
+
+
+ const pointField& points = mesh.points();
+ pistonPointEdgeData::trackingData td(points, piston.axis, maxZ);
+
+ // Set initial changed points to all the patch points(if patch present)
+ List pistonPatchPointData(nPistonPatchPoints);
+ labelList pistonPatchPoints(nPistonPatchPoints);
+
+ // Add the patch points to the pistonPatchPointData
+ nPistonPatchPoints = 0;
+ forAllConstIter(labelHashSet, piston.patchSet, iter)
+ {
+ const label patchi = iter.key();
+ const labelList& mp = pbm[patchi].meshPoints();
+
+ forAll(mp, ppi)
+ {
+ pistonPatchPoints[nPistonPatchPoints] = mp[ppi];
+ pistonPatchPointData[nPistonPatchPoints] = pistonPointEdgeData
+ (
+ true
+ );
+ nPistonPatchPoints++;
+ }
+ }
+
+ // Point data for wave
+ List allPointData(mesh.nPoints());
+
+ // Edge data for wave
+ List allEdgeData(mesh.nEdges());
+
+ PointEdgeWave
+ <
+ pistonPointEdgeData,
+ pistonPointEdgeData::trackingData
+ > patchCalc
+ (
+ mesh,
+ pistonPatchPoints,
+ pistonPatchPointData,
+
+ allPointData,
+ allEdgeData,
+ mesh.globalData().nTotalPoints(), // max iterations
+ td
+ );
+
+ // Create a labelHashSet of the point labels in the piston bowl
+ labelHashSet pistonBowlPointSet;
+ forAll(allPointData, pointi)
+ {
+ if (allPointData[pointi].inBowl())
+ {
+ pistonBowlPointSet.insert(pointi);
+ }
+ }
+
+ // Convert the labelHashSet to a pointZone and add to the pointZones
+ mesh.pointZones().append
+ (
+ new pointZone
+ (
+ piston.pistonBowlName,
+ pistonBowlPointSet.toc(),
+ mesh.pointZones()
+ )
+ );
+}
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+int main(int argc, char *argv[])
+{
+ #include "addRegionOption.H"
+
+ argList::addBoolOption
+ (
+ "cylinderHead",
+ "Create the cylinderHead pointZone"
+ );
+
+ argList::addBoolOption
+ (
+ "pistonBowl",
+ "Create the piston pointZone"
+ );
+
+ #include "setRootCase.H"
+ #include "createTimeNoFunctionObjects.H"
+
+ const bool cylinderHeadZone = args.optionFound("cylinderHead");
+ const bool pistonBowlZone = args.optionFound("pistonBowl");
+
+ if (!cylinderHeadZone && !pistonBowlZone)
+ {
+ FatalErrorInFunction
+ << "Neither cylinderHeadZone nor pistonBowl pointZones requested"
+ << exit(FatalError);
+ }
+
+ #include "createRegionMesh.H"
+
+ const fvMeshMovers::multiValveEngine& mve
+ (
+ refCast(mesh.mover())
+ );
+
+ if (cylinderHeadZone)
+ {
+ calcCylinderHeadPoints(mve);
+ }
+
+ if (pistonBowlZone)
+ {
+ calcPistonBowlPoints(mve);
+ }
+
+ // Take over refinement levels and write to new time directory.
+ Info<< "Writing pointZones" << endl;
+ mesh.pointZones().write();
+
+ Pout<< "End\n" << endl;
+
+ return 0;
+}
+
+
+// ************************************************************************* //