From a60fe9c7b029877f0f23267b623ddf4f97ebe68a Mon Sep 17 00:00:00 2001
From: Mark Olesen <Mark.Olesen@esi-group.com>
Date: Mon, 16 Dec 2019 21:50:47 +0100
Subject: [PATCH] ENH: PDRsetFields utility (#1216)

- the PDRsetFields utility processes a set of geometrical obstructions
  to determine the equivalent blockage effects.

  These fields are necessary inputs for PDRFoam calculations.

  After setting up the geometries, the -dry-run option can be used to
  generate a VTK file for diagnosis and post-processing purposes.

- this is an initial release, with improvements slated for the future.

NOTE
  - the field results may be less than fully reliable when run in
    single-precision. This howver does not represent a realistic
    restriction since the prepared fields target a combustion
    application which will invariably be double-precision.
---
 .../preProcessing/PDRsetFields/Make/files     |   19 +
 .../preProcessing/PDRsetFields/Make/options   |   14 +
 .../preProcessing/PDRsetFields/PDRarrays.C    |  141 ++
 .../preProcessing/PDRsetFields/PDRarrays.H    |  215 ++
 .../PDRsetFields/PDRarraysAnalyse.C           | 1099 +++++++++
 .../PDRsetFields/PDRarraysCalc.C              | 1984 +++++++++++++++++
 .../preProcessing/PDRsetFields/PDRlegacy.H    |   75 +
 .../PDRsetFields/PDRlegacyMeshSpec.C          |  340 +++
 .../PDRsetFields/PDRmeshArrays.C              |  262 +++
 .../PDRsetFields/PDRmeshArrays.H              |  131 ++
 .../preProcessing/PDRsetFields/PDRparams.C    |  111 +
 .../preProcessing/PDRsetFields/PDRparams.H    |  165 ++
 .../preProcessing/PDRsetFields/PDRpatchDef.C  |   52 +
 .../preProcessing/PDRsetFields/PDRpatchDef.H  |  123 +
 .../preProcessing/PDRsetFields/PDRsetFields.C |  351 +++
 .../preProcessing/PDRsetFields/PDRsetFields.H |  142 ++
 .../preProcessing/PDRsetFields/PDRutils.H     |   62 +
 .../PDRsetFields/PDRutilsInternal.H           |  221 ++
 .../PDRsetFields/PDRutilsIntersect.C          |  712 ++++++
 .../PDRsetFields/PDRutilsOverlap.C            |  767 +++++++
 .../PDRsetFields/obstacles/ObstaclesDict      |  179 ++
 .../PDRsetFields/obstacles/PDRobstacle.C      |  737 ++++++
 .../PDRsetFields/obstacles/PDRobstacle.H      |  477 ++++
 .../PDRsetFields/obstacles/PDRobstacleI.H     |   46 +
 .../PDRsetFields/obstacles/PDRobstacleIO.C    |  353 +++
 .../obstacles/PDRobstacleLegacyIO.C           |  475 ++++
 .../obstacles/PDRobstacleLegacyRead.C         |  567 +++++
 .../PDRsetFields/obstacles/PDRobstacleTypes.C |  512 +++++
 .../PDRsetFields/obstacles/PDRobstacleTypes.H |  300 +++
 29 files changed, 10632 insertions(+)
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/Make/files
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/Make/options
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/PDRarrays.C
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/PDRarrays.H
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/PDRarraysAnalyse.C
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/PDRarraysCalc.C
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/PDRlegacy.H
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/PDRlegacyMeshSpec.C
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/PDRmeshArrays.C
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/PDRmeshArrays.H
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/PDRparams.C
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/PDRparams.H
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/PDRpatchDef.C
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/PDRpatchDef.H
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/PDRsetFields.C
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/PDRsetFields.H
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/PDRutils.H
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/PDRutilsInternal.H
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/PDRutilsIntersect.C
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/PDRutilsOverlap.C
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/obstacles/ObstaclesDict
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacle.C
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacle.H
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleI.H
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleIO.C
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleLegacyIO.C
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleLegacyRead.C
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleTypes.C
 create mode 100644 applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleTypes.H

diff --git a/applications/utilities/preProcessing/PDRsetFields/Make/files b/applications/utilities/preProcessing/PDRsetFields/Make/files
new file mode 100644
index 00000000000..af1c28d4cac
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/Make/files
@@ -0,0 +1,19 @@
+PDRsetFields.C
+
+PDRarrays.C
+PDRarraysAnalyse.C
+PDRarraysCalc.C
+PDRmeshArrays.C
+PDRparams.C
+PDRpatchDef.C
+PDRlegacyMeshSpec.C
+PDRutilsIntersect.C
+PDRutilsOverlap.C
+
+obstacles/PDRobstacle.C
+obstacles/PDRobstacleIO.C
+obstacles/PDRobstacleTypes.C
+obstacles/PDRobstacleLegacyIO.C
+obstacles/PDRobstacleLegacyRead.C
+
+EXE = $(FOAM_APPBIN)/PDRsetFields
diff --git a/applications/utilities/preProcessing/PDRsetFields/Make/options b/applications/utilities/preProcessing/PDRsetFields/Make/options
new file mode 100644
index 00000000000..eb9d10ceb00
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/Make/options
@@ -0,0 +1,14 @@
+EXE_INC = \
+    -Iobstacles \
+    -I$(LIB_SRC)/finiteVolume/lnInclude \
+    -I$(LIB_SRC)/fileFormats/lnInclude \
+    -I$(LIB_SRC)/surfMesh/lnInclude \
+    -I$(LIB_SRC)/meshTools/lnInclude \
+    -I$(LIB_SRC)/mesh/blockMesh/lnInclude
+
+EXE_LIBS = \
+    -lfiniteVolume \
+    -lfileFormats \
+    -lsurfMesh \
+    -lmeshTools \
+    -lblockMesh
diff --git a/applications/utilities/preProcessing/PDRsetFields/PDRarrays.C b/applications/utilities/preProcessing/PDRsetFields/PDRarrays.C
new file mode 100644
index 00000000000..f15cd52fd63
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/PDRarrays.C
@@ -0,0 +1,141 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2016 Shell Research Ltd.
+    Copyright (C) 2019 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 "PDRarrays.H"
+#include "PDRblock.H"
+
+// * * * * * * * * * * * * * * * Local Functions * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+// Smaller helper to resize matrix and assign to Zero
+template<class T>
+inline void resizeMatrix(SquareMatrix<T>& mat, const label n)
+{
+    mat.setSize(n);
+    mat = Zero;
+}
+
+
+// Smaller helper to resize i-j-k field and assign to uniform value,
+// normally Zero
+template<class T>
+inline void resizeField
+(
+    IjkField<T>& fld,
+    const labelVector& ijk,
+    const T& val = T(Zero)
+)
+{
+    fld.resize(ijk);
+    fld = val;
+}
+
+} // End namespace Foam
+
+
+// * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
+
+Foam::PDRarrays::PDRarrays()
+:
+    pdrBlock_(std::cref<PDRblock>(PDRblock::null()))
+{}
+
+
+Foam::PDRarrays::PDRarrays(const PDRblock& pdrBlock)
+:
+    PDRarrays()
+{
+    reset(pdrBlock);
+}
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+void Foam::PDRarrays::reset(const PDRblock& pdrBlock)
+{
+    pdrBlock_ = std::cref<PDRblock>(pdrBlock);
+
+    // Resize all the major arrays, which are grouped in the structure arrp
+    // All the relevant dimensions are in PDRblock
+
+    // Cell-based addressing
+    const labelVector cellDims = pdrBlock.sizes();
+
+    // Face or point-based addressing
+    const labelVector faceDims(cellDims + labelVector::one);
+
+    // Max addressing dimensions for 2D arrays, with some extra space
+    // These will be used for any combination of x,y,z,
+    // so need to be dimensioned to the maximum size in both directions
+    const label maxDim = cmptMax(pdrBlock.sizes()) + 2;
+
+    resizeField(v_block, cellDims);
+    resizeField(surf, cellDims);
+
+    resizeField(area_block_s, cellDims);
+    resizeField(area_block_r, cellDims);
+    resizeField(dirn_block, cellDims);
+
+    resizeField(face_block, faceDims);
+
+    resizeField(along_block, cellDims);
+
+    resizeField(betai_inv1, cellDims);
+
+    resizeField(obs_count, cellDims);
+    resizeField(sub_count, cellDims);
+    resizeField(grating_count, cellDims);
+
+    resizeField(drag_s, cellDims);
+    resizeField(drag_r, cellDims);
+
+    resizeField(obs_size, cellDims);
+
+    for (auto& list : overlap_1d)
+    {
+        list.resize(maxDim);
+        list = Zero;
+    }
+
+    resizeMatrix(aboverlap, maxDim);
+    resizeMatrix(abperim, maxDim);
+    resizeMatrix(a_lblock, maxDim);
+    resizeMatrix(b_lblock, maxDim);
+    resizeMatrix(ac_lblock, maxDim);
+    resizeMatrix(bc_lblock, maxDim);
+    resizeMatrix(c_count, maxDim);
+    resizeMatrix(c_drag, maxDim);
+
+    resizeField(face_patch, faceDims, labelVector::uniform(-1));
+    resizeField(hole_in_face, faceDims);
+}
+
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/PDRarrays.H b/applications/utilities/preProcessing/PDRsetFields/PDRarrays.H
new file mode 100644
index 00000000000..908b45a37f9
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/PDRarrays.H
@@ -0,0 +1,215 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2016 Shell Research Ltd.
+    Copyright (C) 2019 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::PDRarrays
+
+Description
+    Work array definitions for PDR fields
+
+SourceFiles
+    PDRarrays.C
+    PDRarraysCalc.C
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef PDRarrays_H
+#define PDRarrays_H
+
+#include "symmTensor.H"
+#include "symmTensor2D.H"
+#include "SquareMatrix.H"
+#include "IjkField.H"
+#include <functional>
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+// Forward Declarations
+class PDRblock;
+class PDRmeshArrays;
+class PDRobstacle;
+class PDRpatchDef;
+
+
+/*---------------------------------------------------------------------------*\
+                          Class PDRarrays Declaration
+\*---------------------------------------------------------------------------*/
+
+class PDRarrays
+{
+    //- Reference to PDRblock
+    std::reference_wrapper<const PDRblock> pdrBlock_;
+
+public:
+
+    // Data Members
+    // Entries used for analysis and when writing fields
+
+    //- Volume blockage
+    IjkField<scalar> v_block;
+
+    //- Surface area in cell
+    IjkField<scalar> surf;
+
+    //- Obstacle size in cell
+    IjkField<scalar> obs_size;
+
+    //- Summed area blockage (directional) from sharp obstacles
+    IjkField<vector> area_block_s;
+
+    //- Summed area blockage (directional) from round obstacles
+    IjkField<vector> area_block_r;
+
+    //- A total directional blockage in the cell
+    IjkField<Vector<bool>> dirn_block;
+
+    //- Face area blockage for face,
+    //- summed from cell centre-plane to cell centre-plane
+    IjkField<vector> face_block;
+
+    //- Longitudinal area blockage from obstacles that extend all the way
+    //- through the cell in a given direction.
+    IjkField<vector> along_block;
+
+    IjkField<vector> betai_inv1;
+
+    //- Number of obstacles in cell.
+    //  Can be non-integer if an obstacle does not pass all way through cell
+    IjkField<scalar> obs_count;
+
+    //- Number of obstacles parallel to specified direction
+    IjkField<vector> sub_count;
+
+    //- Addition to count to account for grating comprises many bars
+    //- (to get Lobs right)
+    IjkField<vector> grating_count;
+
+    //- Tensorial drag from sharp obstacles
+    IjkField<symmTensor> drag_s;
+
+    //- Directional drag from round obstacles
+    IjkField<vector> drag_r;
+
+
+    // Next arrays are for 2D calculations of intersection
+
+    // One-dimensional scratch areas for cell overlaps
+    Vector<List<scalar>> overlap_1d;
+
+    // In two dimensions, area of cell covered by circle
+    SquareMatrix<scalar> aboverlap;
+
+    // In two dimensions, length of perimeter of circle witthin cell
+    SquareMatrix<scalar> abperim;
+
+    // For offset cells, i.e. face blockage
+    SquareMatrix<scalar> a_lblock, b_lblock;
+
+    // For centred cells
+    SquareMatrix<scalar> ac_lblock, bc_lblock;
+
+    // The count in the cells
+    SquareMatrix<scalar> c_count;
+
+    //- Cell-centred drag
+    SquareMatrix<symmTensor2D> c_drag;
+
+    //- Face field for (directional) for patch Id
+    IjkField<labelVector> face_patch;
+
+    //- Face field for (directional) hole in face
+    IjkField<Vector<bool>> hole_in_face;
+
+
+    // Constructors
+
+        //- Construct null
+        PDRarrays();
+
+        //- Construct and reset
+        explicit PDRarrays(const PDRblock& pdrBlock);
+
+
+    //- Destructor
+    ~PDRarrays() = default;
+
+
+    // Member Functions
+
+        //- Reset PDRblock reference, resize and zero arrays
+        void reset(const PDRblock& pdrBlock);
+
+        //- Reference to PDRblock
+        const PDRblock& block() const
+        {
+            return pdrBlock_.get();
+        }
+
+        //- Summary of the blockages
+        //  For diagnostics and general overview
+        void blockageSummary() const;
+
+        //- Add cylinder blockage
+        void addCylinder(const PDRobstacle& obs);
+
+        //- Add general (non-cylinder) blockage
+        void addBlockage
+        (
+            const PDRobstacle& obs,
+            DynamicList<PDRpatchDef>& patches,
+            const int volumeSign
+        );
+
+
+        static void calculateAndWrite
+        (
+            PDRarrays& arr,
+            const PDRmeshArrays& meshIndexing,
+            const fileName& casepath,
+            const UList<PDRpatchDef>& patches
+        );
+
+        void calculateAndWrite
+        (
+            const fileName& casepath,
+            const PDRmeshArrays& meshIndexing,
+            const UList<PDRpatchDef>& patches
+        );
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/PDRarraysAnalyse.C b/applications/utilities/preProcessing/PDRsetFields/PDRarraysAnalyse.C
new file mode 100644
index 00000000000..a72c769ab1f
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/PDRarraysAnalyse.C
@@ -0,0 +1,1099 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2016 Shell Research Ltd.
+    Copyright (C) 2019 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 "PDRsetFields.H"
+#include "PDRobstacle.H"
+#include "PDRpatchDef.H"
+#include "PDRutils.H"
+#include "PDRutilsInternal.H"
+#include "ListOps.H"
+
+#include <cstdlib>
+#include <cstdio>
+#include <cstring>
+
+#ifndef FULLDEBUG
+#define NDEBUG
+#endif
+#include <cassert>
+
+using namespace Foam;
+using namespace Foam::PDRutils;
+
+// * * * * * * * * * * * * * * * Local Functions * * * * * * * * * * * * * * //
+
+// Cell blockage.
+//
+// Use simple sum, because this will eventually be divided by a notional
+// number of rows to give a per-row blockage ratio
+//
+//      b is the (fractional) area blockage. f is 1 or 0.5 to split between ends
+//      Thus if b isclose to 1, the obstacle is totally blocking the cell in this direction,
+//      and we could modify the behaviour if we wish.
+inline static void add_blockage_c
+(
+    scalar& a,
+    bool& blocked,
+    const scalar b,
+    const scalar f = 1.0
+)
+{
+    a += b * f;
+    if (b > pars.blockageNoCT)
+    {
+        blocked = true;
+    }
+}
+
+
+// Face blockage
+//
+// Adds more area blockage to existing amount by assuming partial overlap,
+// i.e. multiplying porosities.
+//
+// Simple addition if the existing amount is negative, because negative
+// blocks (summed first) should just cancel out part of positive blocks.
+inline static void add_blockage_f
+(
+    scalar& a,
+    const scalar b,
+    bool isHole
+)
+{
+    if (a > 0.0)
+    {
+        // Both positive
+        a = 1.0 - (1.0 - a) * (1.0 - b);
+    }
+    else if (b < pars.blockedFacePar || isHole)
+    {
+        // Add until it eventually becomes positive
+        a += b;
+    }
+    else
+    {
+        // If one obstacle blocks face, face is blocked, regardless of
+        // overlap calculations, unless an input negative obstacle makes a
+        // hole in it
+        a = b;
+    }
+}
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+void Foam::PDRarrays::addCylinder(const PDRobstacle& obs)
+{
+    if (equal(obs.vbkge, 0))
+    {
+        return;
+    }
+
+    if (isNull(block()))
+    {
+        FatalErrorInFunction
+            << "No PDRblock set" << nl
+            << exit(FatalError);
+    }
+
+    const PDRblock& pdrBlock = block();
+    const PDRblock::location& xgrid = pdrBlock.grid().x();
+    const PDRblock::location& ygrid = pdrBlock.grid().y();
+    const PDRblock::location& zgrid = pdrBlock.grid().z();
+
+    scalarList& xoverlap = overlap_1d.x();
+    scalarList& yoverlap = overlap_1d.y();
+    scalarList& zoverlap = overlap_1d.z();
+
+    int     cxmin, cxmax, cymin, cymax, czmin, czmax;
+    int     cfxmin, cfxmax, cfymin, cfymax, cfzmin, cfzmax;
+
+    scalar rad_a, rad_b;
+    vector area_block(Zero);
+
+    if (obs.typeId == PDRobstacle::CYLINDER)
+    {
+        rad_a = rad_b = 0.5*obs.dia();
+    }
+    else
+    {
+        rad_a = 0.5*(obs.wa * ::cos(obs.theta()) + obs.wb * ::sin(obs.theta()));
+        rad_b = 0.5*(obs.wa * ::sin(obs.theta()) + obs.wb * ::cos(obs.theta()));
+    }
+
+    switch (obs.orient)
+    {
+        case vector::Z:
+        {
+            // Determine the part of the grid (potentially) covered by this obstacle.
+            one_d_overlap
+            (
+                obs.x() - rad_a, obs.x() + rad_a,
+                pdrBlock.grid().x(),
+                xoverlap, &cxmin, &cxmax, &cfxmin, &cfxmax
+            ); assert(cxmax >=0);
+
+            one_d_overlap
+            (
+                obs.y() - rad_b, obs.y() + rad_b,
+                pdrBlock.grid().y(),
+                yoverlap, &cymin, &cymax, &cfymin, &cfymax
+            ); assert(cymax >=0);
+
+            one_d_overlap
+            (
+                obs.z(), obs.z() + obs.len(),
+                pdrBlock.grid().z(),
+                zoverlap, &czmin, &czmax, &cfzmin, &cfzmax
+            ); assert(czmax >=0);
+
+            // The area of intersection with each 2D cell in an x-y plane.
+            //  a corresponds to x, and b to y.
+            circle_overlap
+            (
+                obs.x(), obs.y(), obs.dia(),
+                obs.theta(), obs.wa, obs.wb,
+                pdrBlock.grid().x(), cxmin, cfxmax,
+                pdrBlock.grid().y(), cymin, cfymax,
+                aboverlap, abperim, a_lblock, ac_lblock, c_count, c_drag,
+                b_lblock, bc_lblock
+            );
+
+
+            for (label ix = cxmin; ix <= cfxmax; ix++)
+            {
+                for (label iy = cymin; iy <= cfymax; iy++)
+                {
+                    for (label iz = czmin; iz <= cfzmax; iz++)
+                    {
+                        const scalar vol_block = aboverlap(ix,iy) * zoverlap[iz];
+                        v_block(ix,iy,iz) += vol_block;
+                        surf(ix,iy,iz) += abperim(ix,iy) * zoverlap[iz] * zgrid.width(iz);
+
+                        // In the 2D case, the ends of the cylinder appear to
+                        // be in the cell even when not, making surf and
+                        // obs_size wrong. So leave out ends.
+
+                        if (!pars.two_d && (iz == czmin || iz == czmax))
+                        {
+                            // End cells
+                            const scalar both_ends_fac = (czmin == czmax ? 2.0 : 1.0);
+
+                            surf(ix,iy,iz) += aboverlap(ix,iy)
+                                * xgrid.width(ix) * ygrid.width(iy) * both_ends_fac;
+                        }
+
+                        const scalar temp = c_count(ix,iy) * zoverlap[iz];
+
+                        obs_count(ix,iy,iz) += temp;
+                        sub_count(ix,iy,iz).z() += temp;
+
+                        if (!pars.two_d && (iz == czmin || iz == czmax))
+                        {
+                            // End faces
+                            const scalar both_ends_fac = (czmin == czmax ? 2.0 : 1.0);
+
+                            sub_count(ix,iy,iz).z() -= temp * both_ends_fac / 2.0;
+                        }
+
+                        // Keep the blockage and drag of round obst separate
+                        // from the sharp for the moment because different
+                        // blockage ratio corrections will be needed later.
+                        //
+                        // Only the relevant diagonal elements of drag
+                        // are stored; other are zero.
+
+                        area_block.x() = ac_lblock(ix,iy) * zoverlap[iz];
+                        area_block.y() = bc_lblock(ix,iy) * zoverlap[iz];
+
+                        // Do not want blockage and drag across the end
+                        // except at perimeter.
+                        if (aboverlap(ix,iy) < pars.blockedFacePar)
+                        {
+                            if (obs.typeId == PDRobstacle::CYLINDER)
+                            {
+                                drag_r(ix,iy,iz).x() += c_drag(ix,iy).xx() * zoverlap[iz] / xgrid.width(ix) / ygrid.width(iy);
+                                drag_r(ix,iy,iz).y() += c_drag(ix,iy).yy() * zoverlap[iz] / xgrid.width(ix) / ygrid.width(iy);
+
+                                add_blockage_c(area_block_r(ix,iy,iz).x(), dirn_block(ix,iy,iz).x(), area_block.x(), 1.0);
+                                add_blockage_c(area_block_r(ix,iy,iz).y(), dirn_block(ix,iy,iz).y(), area_block.y(), 1.0);
+                            }
+                            else
+                            {
+                                drag_s(ix,iy,iz).xx() += c_drag(ix,iy).xx() * zoverlap[iz] / xgrid.width(ix) / ygrid.width(iy);
+                                drag_s(ix,iy,iz).yy() += c_drag(ix,iy).yy() * zoverlap[iz] / xgrid.width(ix) / ygrid.width(iy);
+
+                                add_blockage_c(area_block_s(ix,iy,iz).x(), dirn_block(ix,iy,iz).x(), area_block.x(), 1.0);
+                                add_blockage_c(area_block_s(ix,iy,iz).y(), dirn_block(ix,iy,iz).y(), area_block.y(), 1.0);
+                            }
+                        }
+                        // Here we accumulate 1/betai - 1.
+                        betai_inv1(ix,iy,iz).x() += vol_block / (1.0 - area_block.x() + floatSMALL);
+                        betai_inv1(ix,iy,iz).y() += vol_block / (1.0 - area_block.y() + floatSMALL);
+                        betai_inv1(ix,iy,iz).z() += vol_block / (1.0 - aboverlap(ix,iy) + floatSMALL);
+
+                        // The off-diagonal elements of drag are stored in "drag" for BOTH round and sharp ostacles
+                        drag_s(ix,iy,iz).xy() += c_drag(ix,iy).xy() * zoverlap[iz] / xgrid.width(ix) / ygrid.width(iy);
+
+                        add_blockage_f(face_block(ix,iy,iz).x(), a_lblock(ix,iy) * zoverlap[iz], hole_in_face(ix,iy,iz).x());
+                        add_blockage_f(face_block(ix,iy,iz).y(), b_lblock(ix,iy) * zoverlap[iz], hole_in_face(ix,iy,iz).y());
+                    }
+
+                    // Face blockage in the z direction
+                    if (czmin == czmax)
+                    {
+                        // Does not span cell. Block nearest face.
+                        add_blockage_f(face_block(ix,iy,cfzmax).z(), aboverlap(ix,iy), hole_in_face(ix,iy,cfzmax).z());
+                    }
+                    else
+                    {
+                        // In at least two cells.
+                        // Block first and last faces overlapped
+                        add_blockage_f(face_block(ix,iy,czmin+1).z(), aboverlap(ix,iy), hole_in_face(ix,iy,czmin+1).z());
+                        if (czmax > czmin+1)
+                        {
+                            add_blockage_f(face_block(ix,iy,czmax).z(), aboverlap(ix,iy), hole_in_face(ix,iy,czmax).z());
+                        }
+                    }
+
+                    // z_block is used to work out the blockage ratio for each
+                    // "row" of sub-grid obstacles so this cylinder should
+                    // not have any eeffct in cells that it completely spans.
+                    // Hence statement commented out below and replaced by
+                    // two lines after this loop. That longitudinal clockage
+                    // goes into new array z_aling_block
+
+                    for (label iz = czmin+1; iz < czmax; ++iz)
+                    {
+                        // Internal only
+                        along_block(ix,iy,iz).z() += aboverlap(ix,iy);
+                    }
+
+                    // Longitudinal drag only applies at ends of cylinder.
+                    // If cylinder spans more than 1 cell, apply half at each
+                    // end.
+
+                    drag_s(ix,iy,czmin).zz() += aboverlap(ix,iy) * xgrid.width(ix) * ygrid.width(iy) / 2.0;
+                    drag_s(ix,iy,czmax).zz() += aboverlap(ix,iy) * xgrid.width(ix) * ygrid.width(iy) / 2.0;
+
+                    add_blockage_c(area_block_s(ix,iy,czmin).z(), dirn_block(ix,iy,czmin).z(), aboverlap(ix,iy), 0.5);
+                    add_blockage_c(area_block_s(ix,iy,czmax).z(), dirn_block(ix,iy,czmax).z(), aboverlap(ix,iy), 0.5);
+                }
+            }
+
+            break;
+        }
+
+        case vector::X:  // orientation
+        {
+            // x-direction cylinder. a,b are y,z.
+            one_d_overlap
+            (
+                obs.x(), obs.x() + obs.len(),
+                pdrBlock.grid().x(),
+                xoverlap, &cxmin, &cxmax, &cfxmin, &cfxmax
+            ); assert(cxmax >=0);
+
+            one_d_overlap
+            (
+                obs.y() - rad_a, obs.y() + rad_a,
+                pdrBlock.grid().y(),
+                yoverlap, &cymin, &cymax, &cfymin, &cfymax
+            ); assert(cymax >=0);
+
+            one_d_overlap
+            (
+                obs.z() - rad_b, obs.z() + rad_b,
+                pdrBlock.grid().z(),
+                zoverlap, &czmin, &czmax, &cfzmin, &cfzmax
+            ); assert(czmax >=0);
+
+            circle_overlap
+            (
+                obs.y(), obs.z(), obs.dia(),
+                obs.theta(), obs.wa, obs.wb,
+                pdrBlock.grid().y(), cymin, cfymax,
+                pdrBlock.grid().z(), czmin, cfzmax,
+                aboverlap, abperim, a_lblock, ac_lblock, c_count, c_drag,
+                b_lblock, bc_lblock
+            );
+
+
+            for (label iy = cymin; iy <= cfymax; iy++)
+            {
+                for (label iz = czmin; iz <= cfzmax; iz++)
+                {
+                    for (label ix = cxmin; ix <= cxmax; ix++)
+                    {
+                        const scalar vol_block = aboverlap(iy,iz) * xoverlap[ix];
+                        v_block(ix,iy,iz) += vol_block;
+                        surf(ix,iy,iz) += abperim(iy,iz) * xoverlap[ix] * xgrid.width(ix);
+
+                        if (ix == cxmin || ix == cxmax)
+                        {
+                            // End cells
+                            const scalar both_ends_fac = (cxmin == cxmax ? 2.0 : 1.0);
+
+                            surf(ix,iy,iz) += aboverlap(iy,iz)
+                                * ygrid.width(iy) * zgrid.width(iz) * both_ends_fac;
+                        }
+
+                        const scalar temp = c_count(iy,iz) * xoverlap[ix];
+                        obs_count(ix,iy,iz) += temp;
+                        sub_count(ix,iy,iz).x() += temp;
+
+                        if (ix == cfxmin || ix == cfxmax)
+                        {
+                            // End faces
+                            const scalar both_ends_fac = (cfxmin == cfxmax ? 2.0 : 1.0);
+
+                            sub_count(ix,iy,iz).x() -= temp * both_ends_fac / 2.0;
+                        }
+
+                        area_block.y() = ac_lblock(iy,iz) * xoverlap[ix];
+                        area_block.z() = bc_lblock(iy,iz) * xoverlap[ix];
+
+                        // Do not want blockage and drag across the end
+                        // except at perimeter.
+                        if (aboverlap(iy,iz) < pars.blockedFacePar)
+                        {
+                            if (obs.typeId == PDRobstacle::CYLINDER)
+                            {
+                                drag_r(ix,iy,iz).y() += c_drag(iy,iz).xx() * xoverlap[ix] / ygrid.width(iy) / zgrid.width(iz);
+                                drag_r(ix,iy,iz).z() += c_drag(iy,iz).yy() * xoverlap[ix] / ygrid.width(iy) / zgrid.width(iz);
+
+                                add_blockage_c(area_block_r(ix,iy,iz).y(), dirn_block(ix,iy,iz).y(), area_block.y(), 1.0);
+                                add_blockage_c(area_block_r(ix,iy,iz).z(), dirn_block(ix,iy,iz).z(), area_block.z(), 1.0);
+                            }
+                            else
+                            {
+                                drag_s(ix,iy,iz).yy() += c_drag(iy,iz).xx() * xoverlap[ix] / ygrid.width(iy) / zgrid.width(iz);
+                                drag_s(ix,iy,iz).zz() += c_drag(iy,iz).yy() * xoverlap[ix] / ygrid.width(iy) / zgrid.width(iz);
+
+                                add_blockage_c(area_block_s(ix,iy,iz).y(), dirn_block(ix,iy,iz).y(), area_block.y(), 1.0);
+                                add_blockage_c(area_block_s(ix,iy,iz).z(), dirn_block(ix,iy,iz).z(), area_block.z(), 1.0);
+                            }
+                        }
+                        betai_inv1(ix,iy,iz).y() += vol_block / (1.0 - area_block.y() + floatSMALL);
+                        betai_inv1(ix,iy,iz).z() += vol_block / (1.0 - area_block.z() + floatSMALL);
+                        betai_inv1(ix,iy,iz).x() += vol_block / (1.0 - aboverlap(iy,iz) + floatSMALL);
+
+                        // The off-diagonal elements of drag are stored in "drag" for BOTH round and sharp ostacles
+                        drag_s(ix,iy,iz).yz() += c_drag(iy,iz).xy() * xoverlap[ix] / ygrid.width(iy) / zgrid.width(iz);
+
+                        add_blockage_f(face_block(ix,iy,iz).y(), a_lblock(iy,iz) * xoverlap[ix], hole_in_face(ix,iy,iz).y());
+                        add_blockage_f(face_block(ix,iy,iz).z(), b_lblock(iy,iz) * xoverlap[ix], hole_in_face(ix,iy,iz).z());
+                    }
+                    if (cxmin == cxmax)
+                    {
+                        // Does not span cell. Block nearest face.
+                        add_blockage_f(face_block(cfxmax,iy,iz).x(), aboverlap(iy,iz), hole_in_face(cfxmax,iy,iz).x());
+                    }
+                    else
+                    {
+                        // In at least two cells.
+                        // Block first and last faces overlapped
+                        add_blockage_f(face_block(cxmin+1,iy,iz).x(), aboverlap(iy,iz), hole_in_face(cxmin+1,iy,iz).x());
+                        if (cxmax > cxmin+1)
+                        {
+                            add_blockage_f(face_block(cxmax,iy,iz).x(), aboverlap(iy,iz), hole_in_face(cxmax,iy,iz).x());
+                        }
+                    }
+
+                    for (label ix = cxmin+1; ix < cxmax; ++ix)
+                    {
+                        // Internal only
+                        along_block(ix,iy,iz).x() += aboverlap(iy,iz);
+                    }
+                    drag_s(cxmin,iy,iz).xx() += aboverlap(iy,iz) * ygrid.width(iy) * zgrid.width(iz) / 2.0;
+                    drag_s(cxmax,iy,iz).xx() += aboverlap(iy,iz) * ygrid.width(iy) * zgrid.width(iz) / 2.0;
+
+                    add_blockage_c(area_block_s(cxmin,iy,iz).x(), dirn_block(cxmin,iy,iz).x(), aboverlap(iy,iz), 0.5);
+                    add_blockage_c(area_block_s(cxmax,iy,iz).x(), dirn_block(cxmax,iy,iz).x(), aboverlap(iy,iz), 0.5);
+                }
+            }
+
+            break;
+        }
+
+        case vector::Y:  // orientation
+        {
+            // y-direction cylinder. a,b are z,x.
+            one_d_overlap
+            (
+                obs.x() - rad_b, obs.x() + rad_b,
+                pdrBlock.grid().x(),
+                xoverlap, &cxmin, &cxmax, &cfxmin, &cfxmax
+            ); assert(cxmax >=0);
+
+            one_d_overlap
+            (
+                obs.y(), obs.y() + obs.len(),
+                pdrBlock.grid().y(),
+                yoverlap, &cymin, &cymax, &cfymin, &cfymax
+            ); assert(cymax >=0);
+
+            one_d_overlap
+            (
+                obs.z() - rad_a, obs.z() + rad_a,
+                pdrBlock.grid().z(),
+                zoverlap, &czmin, &czmax, &cfzmin, &cfzmax
+            ); assert(czmax >=0);
+
+            circle_overlap
+            (
+                obs.z(), obs.x(), obs.dia(),
+                obs.theta(), obs.wa, obs.wb,
+                pdrBlock.grid().z(), czmin, cfzmax,
+                pdrBlock.grid().x(), cxmin, cfxmax,
+                aboverlap, abperim, a_lblock, ac_lblock, c_count, c_drag,
+                b_lblock, bc_lblock
+            );
+
+
+            for (label iz = czmin; iz <= cfzmax; iz++)
+            {
+                for (label ix = cxmin; ix <= cfxmax; ix++)
+                {
+                    for (label iy = cymin; iy <= cymax; iy++)
+                    {
+                        const scalar vol_block = aboverlap(iz,ix) * yoverlap[iy];
+                        v_block(ix,iy,iz) += vol_block;
+                        surf(ix,iy,iz) += abperim(iz,ix) * yoverlap[iy] * ygrid.width(iy);
+
+                        if (iy == cymin || iy == cymax)
+                        {
+                            // End cells
+                            const scalar both_ends_fac = (cymin == cymax ? 2.0 : 1.0);
+
+                            surf(ix,iy,iz) += aboverlap(iz,ix)
+                                * zgrid.width(iz) * xgrid.width(ix) * both_ends_fac;
+                        }
+
+                        const scalar temp = c_count(iz,ix) * yoverlap[iy];
+
+                        obs_count(ix,iy,iz) += temp;
+                        sub_count(ix,iy,iz).y() += temp;
+
+                        if (iy == cfymin || iy == cfymax)
+                        {
+                            // End faces
+                            const scalar both_ends_fac = (cfymin == cfymax ? 2.0 : 1.0);
+
+                            sub_count(ix,iy,iz).y() -= temp * both_ends_fac / 2.0;
+                        }
+
+                        area_block.z() = ac_lblock(iz,ix) * yoverlap[iy];
+                        area_block.x() = bc_lblock(iz,ix) * yoverlap[iy];
+
+                        // Do not want blockage and drag across the end
+                        // except at perimeter.
+                        if (aboverlap(iz,ix) < pars.blockedFacePar)
+                        {
+                            if (obs.typeId == PDRobstacle::CYLINDER)
+                            {
+                                drag_r(ix,iy,iz).z() += c_drag(iz,ix).xx() * yoverlap[iy] / zgrid.width(iz) / xgrid.width(ix);
+                                drag_r(ix,iy,iz).x() += c_drag(iz,ix).yy() * yoverlap[iy] / zgrid.width(iz) / xgrid.width(ix);
+
+                                add_blockage_c(area_block_r(ix,iy,iz).z(), dirn_block(ix,iy,iz).z(), area_block.z(), 1.0);
+                                add_blockage_c(area_block_r(ix,iy,iz).x(), dirn_block(ix,iy,iz).x(), area_block.x(), 1.0);
+                            }
+                            else
+                            {
+                                drag_s(ix,iy,iz).zz() += c_drag(iz,ix).xx() * yoverlap[iy] / zgrid.width(iz) / xgrid.width(ix);
+                                drag_s(ix,iy,iz).xx() += c_drag(iz,ix).yy() * yoverlap[iy] / zgrid.width(iz) / xgrid.width(ix);
+
+                                add_blockage_c(area_block_s(ix,iy,iz).z(), dirn_block(ix,iy,iz).z(), area_block.z(), 1.0);
+                                add_blockage_c(area_block_s(ix,iy,iz).x(), dirn_block(ix,iy,iz).x(), area_block.x(), 1.0);
+                            }
+                        }
+                        betai_inv1(ix,iy,iz).z() += vol_block / (1.0 - area_block.z() + floatSMALL);
+                        betai_inv1(ix,iy,iz).x() += vol_block / (1.0 - area_block.x() + floatSMALL);
+                        betai_inv1(ix,iy,iz).y() += vol_block / (1.0 - aboverlap(iz,ix) + floatSMALL);
+
+                        // The off-diagonal elements of drag are stored in "drag" for BOTH round and sharp obstacles
+                        drag_s(ix,iy,iz).xz() += c_drag(iz,ix).xy() * yoverlap[iy] / zgrid.width(iz) / xgrid.width(ix);
+
+                        add_blockage_f(face_block(ix,iy,iz).z(), a_lblock(iz,ix) * yoverlap[iy], hole_in_face(ix,iy,iz).z());
+                        add_blockage_f(face_block(ix,iy,iz).x(), b_lblock(iz,ix) * yoverlap[iy], hole_in_face(ix,iy,iz).x());
+                    }
+                    if (cymin == cymax)
+                    {
+                        // Does not span cell. Block nearest face.
+                        add_blockage_f(face_block(ix,cfymax,iz).y(), aboverlap(iz,ix), hole_in_face(ix,cfymax,iz).y());
+                    }
+                    else
+                    {
+                        // In at least two cells.
+                        // Block first and last faces overlapped
+                        add_blockage_f(face_block(ix,cymin+1,iz).y(), aboverlap(iz,ix), hole_in_face(ix,cymin+1,iz).y());
+                        if (cymax > cymin+1)
+                        {
+                            add_blockage_f(face_block(ix,cymax,iz).y(), aboverlap(iz,ix), hole_in_face(ix,cymax,iz).y());
+                        }
+                    }
+
+                    for (label iy = cymin+1; iy < cymax; ++iy)
+                    {
+                        // Internal only
+                        along_block(ix,iy,iz).y() += aboverlap(iz,ix);
+                    }
+
+                    drag_s(ix,cymin,iz).yy() += aboverlap(iz,ix) * zgrid.width(iz) * xgrid.width(ix) / 2.0;
+                    drag_s(ix,cymax,iz).yy() += aboverlap(iz,ix) * zgrid.width(iz) * xgrid.width(ix) / 2.0;
+
+                    add_blockage_c(area_block_s(ix,cymin,iz).y(), dirn_block(ix,cymin,iz).y(), aboverlap(iz,ix), 0.5);
+                    add_blockage_c(area_block_s(ix,cymax,iz).y(), dirn_block(ix,cymax,iz).y(), aboverlap(iz,ix), 0.5);
+                }
+            }
+
+            break;
+        }
+
+        default:  // orientation
+        {
+            Info<< "Unexpected orientation " << int(obs.orient) << nl;
+            break;
+        }
+    }
+}
+
+
+void Foam::PDRarrays::addBlockage
+(
+    const PDRobstacle& obs,
+    DynamicList<PDRpatchDef>& patches,
+    const int volumeSign
+)
+{
+    // The volumeSign indicates whether this pass is for negative or positive
+    // obstacles. Both if 0.
+    if
+    (
+        equal(obs.vbkge, 0)
+     || (volumeSign < 0 && obs.vbkge >= 0)
+     || (volumeSign > 0 && obs.vbkge < 0)
+    )
+    {
+        return;
+    }
+
+    if (isNull(block()))
+    {
+        FatalErrorInFunction
+            << "No PDRblock set" << nl
+            << exit(FatalError);
+    }
+
+    const PDRblock& pdrBlock = block();
+    const PDRblock::location& xgrid = pdrBlock.grid().x();
+    const PDRblock::location& ygrid = pdrBlock.grid().y();
+    const PDRblock::location& zgrid = pdrBlock.grid().z();
+
+    scalarList& xoverlap = overlap_1d.x();
+    scalarList& yoverlap = overlap_1d.y();
+    scalarList& zoverlap = overlap_1d.z();
+
+
+    // 0 will be used later for faces found to be blocked.
+    // 2 is used for wall-function faces.
+    label patchNum = PDRpatchDef::LAST_PREDEFINED;
+
+    // Only the part of the panel that covers full cell faces will be used
+    // so later should keep the panel in the list plus a hole (-ve obstacle) for
+    // part that will become blowoff b.c.
+
+    int indir = 0;
+
+    // Panel or patch
+    const bool isPatch =
+    (
+        (obs.typeId == PDRobstacle::LOUVRE_BLOWOFF && obs.blowoff_type > 0)
+     || (obs.typeId == PDRobstacle::RECT_PATCH)
+    );
+
+    if (isPatch)
+    {
+        const auto& identifier = obs.identifier;
+
+        const auto spc = identifier.find_first_of(" \t\n\v\f\r");
+
+        const word patchName = word::validate(identifier.substr(0, spc));
+
+        patchNum = ListOps::find
+        (
+            patches,
+            [=](const PDRpatchDef& p){ return patchName == p.patchName; },
+            1  // skip 0 (blocked face)
+        );
+
+        if (patchNum < 1)
+        {
+            // The patch name was not already in the list
+            patchNum = patches.size();
+
+            patches.append(PDRpatchDef(patchName));
+        }
+
+
+        PDRpatchDef& p = patches[patchNum];
+
+        if (obs.typeId == PDRobstacle::RECT_PATCH)
+        {
+            indir = obs.inlet_dirn;
+            p.patchType = 0;
+        }
+        else
+        {
+            p.patchType = obs.blowoff_type;
+            p.blowoffPress = obs.blowoff_press;
+            p.blowoffTime = obs.blowoff_time;
+            if (obs.span.x() < 1e-5)
+            {
+                indir = 1;
+            }
+            else if (obs.span.y() < 1e-5)
+            {
+                indir = 2;
+            }
+            else if (obs.span.z() < 1e-5)
+            {
+                indir = 3;
+            }
+            else
+            {
+                FatalErrorInFunction
+                    << "Blowoff panel should have zero thickness" << nl
+                    << exit(FatalError);
+            }
+        }
+    }
+
+    int cxmin, cxmax, cfxmin, cfxmax;
+    one_d_overlap
+    (
+        obs.x(), obs.x() + obs.span.x(),
+        pdrBlock.grid().x(),
+        xoverlap, &cxmin, &cxmax, &cfxmin, &cfxmax
+    ); assert(cxmax >=0);
+
+    int cymin, cymax, cfymin, cfymax;
+    one_d_overlap
+    (
+        obs.y(), obs.y() + obs.span.y(),
+        pdrBlock.grid().y(),
+        yoverlap, &cymin, &cymax, &cfymin, &cfymax
+    ); assert(cymax >=0);
+
+    int czmin, czmax, cfzmin, cfzmax;
+    one_d_overlap
+    (
+        obs.z(), obs.z() + obs.span.z(),
+        pdrBlock.grid().z(),
+        zoverlap, &czmin, &czmax, &cfzmin, &cfzmax
+    ); assert(czmax >=0);
+
+    two_d_overlap(xoverlap, cxmin, cxmax, yoverlap, cymin, cymax, aboverlap);
+
+
+    const scalar vbkge = obs.vbkge;
+    const scalar xbkge = obs.xbkge;
+    const scalar ybkge = obs.ybkge;
+    const scalar zbkge = obs.zbkge;
+
+    // Compensate for double-counting of drag if two edges in same cell
+    const vector double_f
+    (
+        ((cxmin == cxmax) ? 0.5 : 1.0),
+        ((cymin == cymax) ? 0.5 : 1.0),
+        ((czmin == czmax) ? 0.5 : 1.0)
+    );
+
+    for (label ix = cxmin; ix <= cfxmax; ix++)
+    {
+        const scalar xov = xoverlap[ix];
+
+        scalar area, cell_area, temp;
+
+        for (label iy = cymin; iy <= cfymax; iy++)
+        {
+            const scalar yov = yoverlap[iy];
+
+            for (label iz = czmin; iz <= cfzmax; iz++)
+            {
+                const scalar zov = zoverlap[iz];
+
+                if
+                (
+                    isPatch
+                 &&
+                    (
+                        (indir == -1 && ix == cfxmin)
+                     || (indir == 1  && ix == cfxmax)
+                     || (indir == -2 && iy == cfymin)
+                     || (indir == 2  && iy == cfymax)
+                     || (indir == -3 && iz == cfzmin)
+                     || (indir == 3  && iz == cfzmax)
+                    )
+                )
+                {
+                    /* Type RECT_PATCH (16) exists to set all faces it covers to be in a particular patch
+                     usually an inlet or outlet.
+                     ?? If the face not on a cell boundary, this will move it to the lower-cordinate
+                     face of the relevant cell. It should be at the face of teh volume blocked by
+                     the obstacle. But, if that is not at a cell boundary, the obstacle will be
+                     putting blockage in front of teh vent, so we should be checking that it is
+                     at a cell boundary. */
+
+                    switch (indir) // Face orientation
+                    {
+                        // X
+                        case -1:
+                        case 1:
+                            if (yov * zov > pars.blockedFacePar)
+                            {
+                                face_patch(ix,iy,iz).x() = patchNum;
+                            }
+                            break;
+
+                        // Y
+                        case -2:
+                        case 2:
+                            if (zov * xov > pars.blockedFacePar)
+                            {
+                                face_patch(ix,iy,iz).y() = patchNum;
+                            }
+                            break;
+
+                        // Z
+                        case -3:
+                        case 3:
+                            if (xov * yov > pars.blockedFacePar)
+                            {
+                                face_patch(ix,iy,iz).z() = patchNum;
+                            }
+                            break;
+                    }
+                }  // End of code for patch
+
+
+                const scalar vol_block = aboverlap(ix,iy) * zov * vbkge;
+                v_block(ix,iy,iz) += vol_block;
+
+                // These are the face blockages
+                if ((ix > cxmin && ix <= cxmax) || (cxmin == cxmax && ix == cfxmax))
+                {
+                    temp = yov * zov * xbkge;
+
+                    // Has -ve volumeSign only when processing user-defined
+                    // -ve blocks
+                    if (volumeSign < 0)
+                    {
+                        hole_in_face(ix,iy,iz).x() = true;
+                    }
+                    add_blockage_f(face_block(ix,iy,iz).x(), temp, hole_in_face(ix,iy,iz).x());
+                    if (temp > pars.blockedFacePar && ! hole_in_face(ix,iy,iz).x() && !isPatch)
+                    {
+                        // Put faces of block in wall patch
+                        face_patch(ix,iy,iz).x() = PDRpatchDef::WALL_PATCH;
+                    }
+                }
+                if ((iy > cymin && iy <= cymax) || (cymin == cymax && iy == cfymax))
+                {
+                    temp = zov * xov * ybkge;
+                    if (volumeSign < 0)
+                    {
+                        hole_in_face(ix,iy,iz).y() = true;
+                    }
+                    add_blockage_f(face_block(ix,iy,iz).y(), temp, hole_in_face(ix,iy,iz).y());
+                    if (temp > pars.blockedFacePar && ! hole_in_face(ix,iy,iz).y() && !isPatch)
+                    {
+                        face_patch(ix,iy,iz).y() = PDRpatchDef::WALL_PATCH;
+                    }
+                }
+                if ((iz > czmin && iz <= czmax) || (czmin == czmax && iz == cfzmax))
+                {
+                    temp = xov * yov * zbkge;
+                    if (volumeSign < 0)
+                    {
+                        hole_in_face(ix,iy,iz).z() = true;
+                    }
+                    add_blockage_f(face_block(ix,iy,iz).z(), temp, hole_in_face(ix,iy,iz).z());
+                    if (temp > pars.blockedFacePar && ! hole_in_face(ix,iy,iz).z() && !isPatch)
+                    {
+                        face_patch(ix,iy,iz).z() = PDRpatchDef::WALL_PATCH;
+                    }
+                }
+
+                // These are the interior blockages
+                /* As for cylinders, blockage that extends longitudinally all the way through the cell
+                 should not be in x_block etc., but it does go into new arrays along_block etc. */
+                area = yov * zov * xbkge; // Note this is fraction of cell c-s area
+                if (ix < cxmin || ix > cxmax)
+                {}
+                else if (ix > cxmin && ix < cxmax && xbkge >= 1.0)
+                {
+                    along_block(ix,iy,iz).x() += area;
+                    betai_inv1(ix,iy,iz).x() += vol_block / (1.0 - area + floatSMALL);
+                }
+                else if (ix == cxmin || ix == cxmax)
+                {
+                    // If front and back of the obstacle are not in the
+                    // same cell, put half in each
+                    const scalar double_f = (cxmin == cxmax ? 1.0 : 0.5);
+
+                    add_blockage_c(area_block_s(ix,iy,iz).x(), dirn_block(ix,iy,iz).x(), area, double_f);
+                    cell_area = (ygrid.width(iy) * zgrid.width(iz));
+                    surf(ix,iy,iz) += double_f * area * cell_area;
+                    betai_inv1(ix,iy,iz).x() += vol_block / (1.0 - area + floatSMALL);
+
+                    // To get Lobs right for a grating, allow for the fact that it is series of bars by having additional count
+                    if (obs.typeId == PDRobstacle::GRATING && obs.orient == vector::X)
+                    {
+                        //  * cell_area to make dimensional, then / std::sqrt(cell_area) to get width
+                        temp = area * std::sqrt(cell_area) / obs.slat_width - 1.0;
+                        if (temp > 0.0) { grating_count(ix,iy,iz).x() += temp; }
+                    }
+                }
+
+                /* As for cylinders, blockage that extends longitudinally all the way through the cell
+                 should not be in x_block etc., but it does go into new arrays along_block etc. */
+                area = zov * xov * ybkge;
+                if (iy < cymin || iy > cymax)
+                {}
+                else if (iy > cymin && iy < cymax && ybkge >= 1.0)
+                {
+                    along_block(ix,iy,iz).y() += area;
+                    betai_inv1(ix,iy,iz).y() += vol_block / (1.0 - area + floatSMALL);
+                }
+                else if (iy == cymin || iy == cymax)
+                {
+                    // If front and back of the obstacle are not in the
+                    // same cell, put half in each
+                    const scalar double_f = (cymin == cymax ? 1.0 : 0.5);
+
+                    add_blockage_c(area_block_s(ix,iy,iz).y(), dirn_block(ix,iy,iz).y(), area, double_f);
+                    cell_area = (zgrid.width(iz) * xgrid.width(ix));
+                    surf(ix,iy,iz) += double_f * area * cell_area;
+                    betai_inv1(ix,iy,iz).y() += vol_block / (1.0 - area + floatSMALL);
+
+                    if (obs.typeId == PDRobstacle::GRATING && obs.orient == vector::Y)
+                    {
+                        //  * cell_area to make dimensional, then / std::sqrt(cell_area) to get width
+                        temp = area * std::sqrt(cell_area) / obs.slat_width - 1.0;
+                        if (temp > 0.0) { grating_count(ix,iy,iz).y() += temp; }
+                    }
+                }
+
+                area = xov * yov * zbkge;
+                if (iz < czmin || iz > czmax)
+                {}
+                else if (iz > czmin && iz < czmax && zbkge >= 1.0)
+                {
+                    along_block(ix,iy,iz).z() += area;
+                    betai_inv1(ix,iy,iz).z() += vol_block / (1.0 - area + floatSMALL);
+                }
+                else if (iz == czmin || iz == czmax)
+                {
+                    // If front and back of the obstacle are not in the
+                    // same cell, put half in each
+                    const scalar double_f = (czmin == czmax ? 1.0 : 0.5);
+
+                    add_blockage_c(area_block_s(ix,iy,iz).z(), dirn_block(ix,iy,iz).z(), area, double_f);
+                    cell_area = (xgrid.width(ix) * ygrid.width(iy));
+                    surf(ix,iy,iz) += double_f * area * cell_area;
+                    betai_inv1(ix,iy,iz).z() += vol_block / (1.0 - area + floatSMALL);
+
+                    if (obs.typeId == PDRobstacle::GRATING && obs.orient == vector::Z)
+                    {
+                        //  * cell_area to make dimensional, then / std::sqrt(cell_area) to get width
+                        temp = area * std::sqrt(cell_area) / obs.slat_width - 1.0;
+                        if (temp > 0.0) { grating_count(ix,iy,iz).z() += temp; }
+                    }
+                }
+            }
+        }
+    }
+
+    if (obs.typeId == PDRobstacle::RECT_PATCH)
+    {
+        // Was only needed to set face_patch values
+        return;
+    }
+
+
+    /*  A narrow obstacle completely crossing the cell adds 1 to the count for the transverse directions
+    If all four edges are not in the relevant cell, we take 1/4 for each edge that is in.
+    If it does not totally cross the cell, then the count is reduced proportionately
+    If it is porous, then the count is reduced proportionately.
+    ?? Should it be? At least this is consistent with effect going smoothly to zero as porosity approaches 1 ??
+
+    Note that more than 1 can be added for one obstacle, if sides and ends are in the cell.
+
+    We do all the x-aligned edges first, both for y-blockage and z-blockage.                                */
+
+    /* Intersection obstacles can have more edges than the intersecting obstacles that
+     generated them, so not good to incorporate these in N and drag.  */
+
+    for (label ix = cxmin; ix <= cxmax; ix++)
+    {
+        // Factor of 0.25 because 4 edges to be done
+        scalar olap25 = 0.25 * xoverlap[ix];
+
+        const scalar temp =
+            ((pars.noIntersectN && vbkge < 0.334) ? 0 : (olap25 * vbkge));
+
+        obs_count(ix,cymin,czmin) += temp;
+        obs_count(ix,cymax,czmin) += temp;
+        obs_count(ix,cymin,czmax) += temp;
+        obs_count(ix,cymax,czmax) += temp;
+
+        sub_count(ix,cymin,czmin).x() += temp;
+        sub_count(ix,cymax,czmin).x() += temp;
+        sub_count(ix,cymin,czmax).x() += temp;
+        sub_count(ix,cymax,czmax).x() += temp;
+
+        // The 0.25 becomes 0.5 to allow for front/back faces in drag direction
+        olap25 *= 2.0;
+
+        drag_s(ix,cymin,czmin).yy() += zoverlap[czmin] * double_f.z() * olap25 * ybkge / ygrid.width(cymin);
+        drag_s(ix,cymax,czmin).yy() += zoverlap[czmin] * double_f.z() * olap25 * ybkge / ygrid.width(cymax);
+        drag_s(ix,cymin,czmax).yy() += zoverlap[czmax] * double_f.z() * olap25 * ybkge / ygrid.width(cymin);
+        drag_s(ix,cymax,czmax).yy() += zoverlap[czmax] * double_f.z() * olap25 * ybkge / ygrid.width(cymax);
+
+        drag_s(ix,cymin,czmin).zz() += yoverlap[cymin] * double_f.y() * olap25 * zbkge / zgrid.width(czmin);
+        drag_s(ix,cymax,czmin).zz() += yoverlap[cymax] * double_f.y() * olap25 * zbkge / zgrid.width(czmin);
+        drag_s(ix,cymin,czmax).zz() += yoverlap[cymin] * double_f.y() * olap25 * zbkge / zgrid.width(czmax);
+        drag_s(ix,cymax,czmax).zz() += yoverlap[cymax] * double_f.y() * olap25 * zbkge / zgrid.width(czmax);
+
+        // Porous obstacles do not only have drag at edges
+        if (xbkge < 1.0)
+        {
+            for (label iy = cymin+1; iy < cymax; iy++)
+            {
+                for (label iz = czmin+1; iz < czmax; iz++)
+                {
+                    // Internal only
+                    drag_s(ix,iy,iz).xx() = xbkge / xgrid.width(ix);
+                }
+            }
+        }
+    }
+
+    for (label iy = cymin; iy <= cymax; iy++)
+    {
+        scalar olap25 = 0.25 * yoverlap[iy];
+        const scalar temp =
+            ((pars.noIntersectN && vbkge < 0.334) ? 0 : (olap25 * vbkge));
+
+        obs_count(cxmin,iy,czmin) += temp;
+        obs_count(cxmax,iy,czmin) += temp;
+        obs_count(cxmin,iy,czmax) += temp;
+        obs_count(cxmax,iy,czmax) += temp;
+
+        sub_count(cxmin,iy,czmin).y() += temp;
+        sub_count(cxmax,iy,czmin).y() += temp;
+        sub_count(cxmin,iy,czmax).y() += temp;
+        sub_count(cxmax,iy,czmax).y() += temp;
+
+        olap25 *= 2.0;
+
+        if (iy > cymin && iy < cymax) // Avoid re-doing corners already done above
+        {
+            drag_s(cxmin,iy,czmin).zz() += xoverlap[cxmin] * double_f.x() * olap25 * zbkge / zgrid.width(czmin);
+            drag_s(cxmax,iy,czmin).zz() += xoverlap[cxmin] * double_f.x() * olap25 * zbkge / zgrid.width(czmin);
+            drag_s(cxmin,iy,czmax).zz() += xoverlap[cxmax] * double_f.x() * olap25 * zbkge / zgrid.width(czmax);
+            drag_s(cxmax,iy,czmax).zz() += xoverlap[cxmax] * double_f.x() * olap25 * zbkge / zgrid.width(czmax);
+        }
+        drag_s(cxmin,iy,czmin).xx() += zoverlap[czmin] * double_f.z() * olap25 * xbkge / xgrid.width(cxmin);
+        drag_s(cxmax,iy,czmin).xx() += zoverlap[czmax] * double_f.z() * olap25 * xbkge / xgrid.width(cxmax);
+        drag_s(cxmin,iy,czmax).xx() += zoverlap[czmin] * double_f.z() * olap25 * xbkge / xgrid.width(cxmin);
+        drag_s(cxmax,iy,czmax).xx() += zoverlap[czmax] * double_f.z() * olap25 * xbkge / xgrid.width(cxmax);
+
+        // Porous obstacles do not only have drag at edges
+        if (ybkge < 1.0)
+        {
+            for (label iz = czmin+1; iz < czmax; iz++)
+            {
+                for (label ix = cxmin+1; ix < cxmax; ix++)
+                {
+                    // Internal only
+                    drag_s(ix,iy,iz).yy() = ybkge / ygrid.width(iy);
+                }
+            }
+        }
+    }
+
+    for (label iz = czmin; iz <= czmax; iz++)
+    {
+        scalar olap25 = 0.25 * zoverlap[iz];
+        const scalar temp =
+            ((pars.noIntersectN && vbkge < 0.334) ? 0 : (olap25 * vbkge));
+
+        obs_count(cxmin,cymin,iz) += temp;
+        obs_count(cxmin,cymax,iz) += temp;
+        obs_count(cxmax,cymin,iz) += temp;
+        obs_count(cxmax,cymax,iz) += temp;
+
+        sub_count(cxmin,cymin,iz).z() += temp;
+        sub_count(cxmin,cymax,iz).z() += temp;
+        sub_count(cxmax,cymin,iz).z() += temp;
+        sub_count(cxmax,cymax,iz).z() += temp;
+
+        olap25 *= 2.0;
+
+        if (iz > czmin && iz < czmax) // Avoid re-doing corners already done above
+        {
+            drag_s(cxmin,cymin,iz).xx() += yoverlap[cymin] * double_f.y() * olap25 * xbkge / xgrid.width(cxmin);
+            drag_s(cxmax,cymin,iz).xx() += yoverlap[cymin] * double_f.y() * olap25 * xbkge / xgrid.width(cxmax);
+            drag_s(cxmin,cymax,iz).xx() += yoverlap[cymax] * double_f.y() * olap25 * xbkge / xgrid.width(cxmin);
+            drag_s(cxmax,cymax,iz).xx() += yoverlap[cymax] * double_f.y() * olap25 * xbkge / xgrid.width(cxmax);
+
+            drag_s(cxmin,cymin,iz).yy() += xoverlap[cxmin] * double_f.x() * olap25 * ybkge / ygrid.width(cymin);
+            drag_s(cxmax,cymin,iz).yy() += xoverlap[cxmax] * double_f.x() * olap25 * ybkge / ygrid.width(cymin);
+            drag_s(cxmin,cymax,iz).yy() += xoverlap[cxmin] * double_f.x() * olap25 * ybkge / ygrid.width(cymax);
+            drag_s(cxmax,cymax,iz).yy() += xoverlap[cxmax] * double_f.x() * olap25 * ybkge / ygrid.width(cymax);
+        }
+
+        // Porous obstacles do not only have drag at edges
+        if (zbkge < 1.0)
+        {
+            for (label ix = cxmin+1; ix < cxmax; ix++)
+            {
+                for (label iy = cymin+1; iy < cymax; iy++)
+                {
+                    // Internal only
+                    drag_s(ix,iy,iz).zz() = zbkge / zgrid.width(iz);
+                }
+            }
+        }
+    }
+}
+
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/PDRarraysCalc.C b/applications/utilities/preProcessing/PDRsetFields/PDRarraysCalc.C
new file mode 100644
index 00000000000..e74ff71d7d6
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/PDRarraysCalc.C
@@ -0,0 +1,1984 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2016 Shell Research Ltd.
+    Copyright (C) 2019 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 "PDRarrays.H"
+#include "PDRblock.H"
+#include "PDRpatchDef.H"
+#include "PDRmeshArrays.H"
+#include "PDRparams.H"
+
+#include "PDRsetFields.H"
+
+#include "bitSet.H"
+#include "DynamicList.H"
+#include "dimensionSet.H"
+#include "symmTensor.H"
+#include "SquareMatrix.H"
+#include "IjkField.H"
+#include "MinMax.H"
+#include "volFields.H"
+#include "OFstream.H"
+#include "OSspecific.H"
+
+#ifndef FULLDEBUG
+#define NDEBUG
+#endif
+#include <cassert>
+
+using namespace Foam;
+
+HashTable<string> fieldNotes
+({
+    { "Lobs", "" },
+    { "Aw", "surface area per unit volume" },
+    { "CR", "" },
+    { "CT", "" },
+    { "N", "" },
+    { "ns", "" },
+    { "Nv", "" },
+    { "nsv", "" },
+    { "Bv", "area blockage" },
+    { "B", "area blockage" },
+    { "betai", "" },
+    { "Blong", "longitudinal blockage" },
+    { "Ep", "1/Lobs" },
+});
+
+
+// calc_fields
+
+
+// Local Functions
+/*
+// calc_drag_etc
+make_header
+tail_field
+write_scalarField
+write_uniformField
+write_symmTensorField
+write_pU_fields
+write_blocked_face_list
+write_blockedCellsSet
+*/
+
+// Somewhat similar to what the C-fprintf would have had
+static constexpr unsigned outputPrecision = 8;
+
+void calc_drag_etc
+(
+    double  brs, double  brr, bool blocked,
+    double  surr_br, double  surr_dr,
+    scalar* drags_p, scalar* dragr_p,
+    double  count,
+    scalar* cbdi_p,
+    double cell_vol
+);
+
+
+void write_scalarField
+(
+    const word& fieldName, const IjkField<scalar>& fld,
+    const scalar& deflt, const scalarMinMax& limits, const char *wall_bc,
+    const PDRmeshArrays& meshIndexing,
+    const UList<PDRpatchDef>& patches,
+    const dimensionSet& dims, const fileName& casepath
+);
+
+void write_uniformField
+(
+    const word& fieldName, const scalar& deflt, const char *wall_bc,
+    const PDRmeshArrays& meshIndexing,
+    const UList<PDRpatchDef>& patches,
+    const dimensionSet& dims, const fileName& casepath
+);
+
+void write_pU_fields
+(
+    const PDRmeshArrays& meshIndexing,
+    const UList<PDRpatchDef>& patches,
+    const fileName& casepath
+);
+
+void write_symmTensorField
+(
+    const word& fieldName, const IjkField<symmTensor>& fld,
+    const symmTensor& deflt, const char *wall_bc,
+    const PDRmeshArrays& meshIndexing,
+    const UList<PDRpatchDef>& patches,
+    const dimensionSet& dims, const fileName& casepath
+);
+
+void write_symmTensorFieldV
+(
+    const word& fieldName, const IjkField<vector>& fld,
+    const symmTensor& deflt, const char *wall_bc,
+    const PDRmeshArrays& meshIndexing,
+    const UList<PDRpatchDef>& patches,
+    const dimensionSet& dims, const fileName& casepath
+);
+
+void write_blocked_face_list
+(
+    const IjkField<vector>& face_block,
+    const IjkField<labelVector>& face_patch,
+    const IjkField<scalar>& obs_count,
+    IjkField<vector>& sub_count,
+    IjkField<Vector<direction>>& n_blocked_faces,
+    const PDRmeshArrays& meshIndexing,
+    const UList<PDRpatchDef>& patches,
+    double limit_par, const fileName& casepath
+);
+
+void write_blockedCellsSet
+(
+    const IjkField<scalar>& fld,
+    const PDRmeshArrays& meshIndexing, double limit_par,
+    const IjkField<Vector<direction>>& n_blocked_faces,
+    const fileName& casepath,
+    const word& listName
+);
+
+
+// The average values of surrounding an array position
+static inline scalar averageSurrounding
+(
+    const SquareMatrix<scalar>& mat,
+    const label i,
+    const label j
+)
+{
+    return
+    (
+        mat(i,j)   + mat(i,j+1) + mat(i,j+2)
+      + mat(i+1,j) /* centre */ + mat(i+1,j+2)
+      + mat(i+2,j) + mat(i+2,j+1) + mat(i+2,j+2)
+    ) / 8.0;  // Average
+}
+
+
+// Helper
+template<class Type>
+static inline Ostream& putUniform(Ostream& os, const word& key, const Type& val)
+{
+    os.writeKeyword(key) << "uniform " << val << token::END_STATEMENT << nl;
+    return os;
+}
+
+
+static void make_header
+(
+    Ostream& os,
+    const fileName& location,
+    const word& clsName,
+    const word& object
+)
+{
+    string note = fieldNotes(object);
+
+    IOobject::writeBanner(os);
+
+    os  << "FoamFile\n{\n"
+        << "    version     2.0;\n"
+        << "    format      ascii;\n"
+        << "    class       " << clsName << ";\n";
+
+    if (!note.empty())
+    {
+        os << "    note        " << note << ";\n";
+    }
+
+    if (!location.empty())
+    {
+        os << "    location    " << location << ";\n";
+    }
+
+    os  << "    object      " << object << ";\n"
+        << "}\n";
+
+    IOobject::writeDivider(os) << nl;
+}
+
+
+void Foam::PDRarrays::calculateAndWrite
+(
+    PDRarrays& arr,
+    const PDRmeshArrays& meshIndexing,
+    const fileName& casepath,
+    const UList<PDRpatchDef>& patches
+)
+{
+    if (isNull(arr.block()))
+    {
+        FatalErrorInFunction
+            << "No PDRblock set" << nl
+            << exit(FatalError);
+    }
+
+    const PDRblock& pdrBlock = arr.block();
+
+    const labelVector& cellDims = meshIndexing.cellDims;
+    const labelVector& faceDims = meshIndexing.faceDims;
+
+    const int xdim = faceDims.x();
+    const int ydim = faceDims.y();
+    const int zdim = faceDims.z();
+    const scalar maxCT = pars.maxCR * pars.cb_r;
+
+
+    // Later used to store the total effective blockage ratio per cell/direction
+    IjkField<symmTensor>& drag_s = arr.drag_s;
+
+    IjkField<vector>& drag_r = arr.drag_r;
+
+    const IjkField<vector>& area_block_s = arr.area_block_s;
+    const IjkField<vector>& area_block_r = arr.area_block_r;
+    const IjkField<Vector<bool>>& dirn_block = arr.dirn_block;
+
+    const IjkField<vector>& betai_inv1 = arr.betai_inv1;
+
+    IjkField<scalar>& obs_count = arr.obs_count;
+    IjkField<vector>& sub_count = arr.sub_count; // ns. Later used to hold longitudinal blockage
+    const IjkField<vector>& grating_count = arr.grating_count;
+
+    IjkField<scalar>& v_block = arr.v_block;
+    IjkField<scalar>& surf = arr.surf;
+
+    // Lobs. Later used for initial Ep
+    IjkField<scalar>& obs_size = arr.obs_size;
+
+    Info<< "Calculating fields" << nl;
+
+    // Local scratch arrays
+
+    // The turbulance generation field CT.
+    // Later used to to hold the beta_i in tensor form
+    IjkField<vector> cbdi(cellDims, Zero);
+
+
+    // For 2D addressing it is convenient to just use the max dimension
+    // and avoid resizing when handling each direction.
+
+    // Dimension of the cells and a layer of surrounding halo cells
+    const labelVector surrDims = (faceDims + labelVector::uniform(2));
+
+    // Max addressing dimensions
+    const label maxDim = cmptMax(surrDims);
+
+    // Blockage-ratio correction to the drag
+    //
+    // neiBlock:
+    //   2-D for averaging the blockage ratio of neighbouring cells.
+    //   It extends one cell  outside the domain in each direction,
+    //   so the indices are offset by 1.
+    // neiDrag:
+    //   2-D array for averaging the drag ratio of neighbouring cells
+
+    SquareMatrix<scalar> neiBlock(maxDim, Zero);
+    SquareMatrix<scalar> neiDrag(maxDim, Zero);
+
+    // X blockage, drag
+
+    for (label ix = 0; ix < pdrBlock.size(vector::X); ++ix)
+    {
+        for (label iy = 0; iy < pdrBlock.size(vector::Y); ++iy)
+        {
+            for (label iz = 0; iz <= zdim; ++iz)
+            {
+                const label izz =
+                    (iz == 0 ? 0 : iz == zdim ? zdim - 2 : iz - 1);
+
+                neiBlock(iy+1, iz) =
+                (
+                    area_block_s(ix,iy,izz).x()
+                  + area_block_r(ix,iy,izz).x()
+                );
+
+                neiDrag(iy+1, iz) =
+                (
+                    drag_s(ix,iy,izz).xx() * pars.cd_s
+                  + drag_r(ix,iy,izz).x() * pars.cd_r
+                );
+            }
+        }
+        for (label iz = 0; iz < surrDims.z(); ++iz)
+        {
+            if (pars.yCyclic)
+            {
+                // Cyclic in y
+                neiBlock(0, iz) = neiBlock(cellDims.y(), iz);
+                neiDrag(0, iz)  = neiDrag(cellDims.y(), iz);
+                neiBlock(ydim, iz) = neiBlock(1, iz);
+                neiDrag(ydim, iz)  = neiDrag(1, iz);
+            }
+            else
+            {
+                neiBlock(0, iz) = neiBlock(1, iz);
+                neiDrag(0, iz)  = neiDrag(1, iz);
+                neiBlock(ydim, iz) = neiBlock(cellDims.y(), iz);
+                neiDrag(ydim, iz)  = neiDrag(cellDims.y(), iz);
+            }
+        }
+
+        for (label iy = 0; iy < pdrBlock.size(vector::Y); ++iy)
+        {
+            for (label iz = 0; iz < pdrBlock.size(vector::Z); ++iz)
+            {
+                const scalar cell_vol = pdrBlock.V(ix,iy,iz);
+
+                const scalar surr_br = averageSurrounding(neiBlock, iy, iz);
+                const scalar surr_dr = averageSurrounding(neiDrag, iy, iz);
+
+                calc_drag_etc
+                (
+                    area_block_s(ix,iy,iz).x(),
+                    area_block_r(ix,iy,iz).x(),
+                    dirn_block(ix,iy,iz).x(),
+                    surr_br, surr_dr,
+                    &(drag_s(ix,iy,iz).xx()),
+                    &(drag_r(ix,iy,iz).x()),
+                    obs_count(ix,iy,iz),
+                    &(cbdi(ix,iy,iz).x()),
+                    cell_vol
+                );
+            }
+        }
+    }
+
+
+    // Y blockage, drag
+
+    neiBlock = Zero;
+    neiDrag = Zero;
+
+    for (label iy = 0; iy < pdrBlock.size(vector::Y); ++iy)
+    {
+        for (label iz = 0; iz < pdrBlock.size(vector::Z); ++iz)
+        {
+            for (label ix = 0; ix <= xdim; ++ix)
+            {
+                const label ixx =
+                    (ix == 0 ? 0 : ix == xdim ? xdim - 2 : ix - 1);
+
+                neiBlock(iz+1, ix) =
+                (
+                    area_block_s(ixx,iy,iz).y()
+                  + area_block_r(ixx,iy,iz).y()
+                );
+                neiDrag(iz+1, ix) =
+                (
+                    drag_s(ixx,iy,iz).yy() * pars.cd_s
+                  + drag_r(ixx,iy,iz).y() * pars.cd_r
+                );
+            }
+        }
+        for (label ix = 0; ix < surrDims.x(); ++ix)
+        {
+            neiBlock(0, ix) = neiBlock(1, ix);
+            neiDrag(0, ix)  = neiDrag(1, ix);
+            neiBlock(zdim, ix) = neiBlock(cellDims.z(), ix);
+            neiDrag(zdim, ix)  = neiDrag(cellDims.z(), ix);
+        }
+
+        for (label iz = 0; iz < pdrBlock.size(vector::Z); ++iz)
+        {
+            for (label ix = 0; ix < pdrBlock.size(vector::X); ++ix)
+            {
+                const scalar cell_vol = pdrBlock.V(ix,iy,iz);
+
+                const scalar surr_br = averageSurrounding(neiBlock, iz, ix);
+                const scalar surr_dr = averageSurrounding(neiDrag, iz, ix);
+
+                calc_drag_etc
+                (
+                    area_block_s(ix,iy,iz).y(),
+                    area_block_r(ix,iy,iz).y(),
+                    dirn_block(ix,iy,iz).y(),
+                    surr_br, surr_dr,
+                    &(drag_s(ix,iy,iz).yy()),
+                    &(drag_r(ix,iy,iz).y()),
+                    obs_count(ix,iy,iz),
+                    &(cbdi(ix,iy,iz).y()),
+                    cell_vol
+                );
+            }
+        }
+    }
+
+
+    // Z blockage, drag
+
+    neiBlock = Zero;
+    neiDrag = Zero;
+
+    for (label iz = 0; iz < pdrBlock.size(vector::Z); ++iz)
+    {
+        for (label ix = 0; ix < pdrBlock.size(vector::X); ++ix)
+        {
+            for (label iy = 0; iy <= ydim; ++iy)
+            {
+                label iyy;
+
+                if (pars.yCyclic)
+                {
+                    iyy = (iy == 0 ? ydim - 2 : iy == ydim ? 0 : iy - 1);
+                }
+                else
+                {
+                    iyy = (iy == 0 ? 0 : iy == ydim ? ydim - 2 : iy - 1);
+                }
+
+                neiBlock(ix+1, iy) =
+                (
+                    area_block_s(ix,iyy,iz).z()
+                  + area_block_r(ix,iyy,iz).z()
+                );
+                neiDrag(ix+1, iy) =
+                (
+                    drag_s(ix,iyy,iz).zz() * pars.cd_s
+                  + drag_r(ix,iyy,iz).z() * pars.cd_r
+                );
+            }
+        }
+        for (label iy = 0; iy < surrDims.y(); ++iy)
+        {
+            neiBlock(0, iy) = neiBlock(1, iy);
+            neiDrag(0, iy)  = neiDrag(1, iy);
+            neiBlock(xdim, iy) = neiBlock(cellDims.x(), iy);
+            neiDrag(xdim, iy)  = neiDrag(cellDims.x(), iy);
+        }
+
+        for (label ix = 0; ix < pdrBlock.size(vector::X); ++ix)
+        {
+            for (label iy = 0; iy < pdrBlock.size(vector::Y); ++iy)
+            {
+                const scalar cell_vol = pdrBlock.V(ix,iy,iz);
+
+                const scalar surr_br = averageSurrounding(neiBlock, ix, iy);
+                const scalar surr_dr = averageSurrounding(neiDrag, ix, iy);
+
+                calc_drag_etc
+                (
+                    area_block_s(ix,iy,iz).z(),
+                    area_block_r(ix,iy,iz).z(),
+                    dirn_block(ix,iy,iz).z(),
+                    surr_br, surr_dr,
+                    &(drag_s(ix,iy,iz).zz()),
+                    &(drag_r(ix,iy,iz).z()),
+                    obs_count(ix,iy,iz),
+                    &(cbdi(ix,iy,iz).z()),
+                    cell_vol
+                );
+            }
+        }
+    }
+
+    neiBlock.clear();
+    neiDrag.clear();
+
+
+    // Calculate other parameters
+
+    for (label iz = 0; iz < pdrBlock.size(vector::Z); ++iz)
+    {
+        for (label ix = 0; ix < pdrBlock.size(vector::X); ++ix)
+        {
+            for (label iy = 0; iy < pdrBlock.size(vector::Y); ++iy)
+            {
+                const scalar dx = pdrBlock.dx(ix);
+                const scalar dy = pdrBlock.dy(iy);
+                const scalar dz = pdrBlock.dz(iz);
+                const scalar cell_vol = pdrBlock.V(ix, iy, iz);
+                const scalar cell_size = pdrBlock.width(ix, iy, iz);
+
+                drag_s(ix,iy,iz).xy() *= pars.cd_s;
+                drag_s(ix,iy,iz).xz() *= pars.cd_s;
+                drag_s(ix,iy,iz).yz() *= pars.cd_s;
+
+                if (drag_s(ix,iy,iz).xx() > pars.maxCR) { drag_s(ix,iy,iz).xx() = pars.maxCR; } ;
+                if (drag_s(ix,iy,iz).yy() > pars.maxCR) { drag_s(ix,iy,iz).yy() = pars.maxCR; } ;
+                if (drag_s(ix,iy,iz).zz() > pars.maxCR) { drag_s(ix,iy,iz).zz() = pars.maxCR; } ;
+
+                if (cbdi(ix,iy,iz).x() > maxCT ) { cbdi(ix,iy,iz).x() = maxCT; } ;
+                if (cbdi(ix,iy,iz).y() > maxCT ) { cbdi(ix,iy,iz).y() = maxCT; } ;
+                if (cbdi(ix,iy,iz).z() > maxCT ) { cbdi(ix,iy,iz).z() = maxCT; } ;
+
+                surf(ix,iy,iz) /= cell_vol;
+
+                /* Calculate length scale of obstacles in each cell
+                 Result is stored in surf. */
+
+                {
+                    const scalar vb = v_block(ix,iy,iz);
+
+                    if
+                    (
+                        (
+                            ((area_block_s(ix,iy,iz).x() + area_block_r(ix,iy,iz).x()) < MIN_AB_FOR_SIZE)
+                         && ((area_block_s(ix,iy,iz).y() + area_block_r(ix,iy,iz).y()) < MIN_AB_FOR_SIZE)
+                         && ((area_block_s(ix,iy,iz).z() + area_block_r(ix,iy,iz).z()) < MIN_AB_FOR_SIZE)
+                        )
+                     || ( vb > MAX_VB_FOR_SIZE )
+                     || ((obs_count(ix,iy,iz) + cmptSum(grating_count(ix,iy,iz))) < MIN_COUNT_FOR_SIZE)
+                     || ( surf(ix,iy,iz) <= 0.0 )
+                    )
+                    {
+                        obs_size(ix,iy,iz) = cell_size * pars.empty_lobs_fac;
+                    }
+                    else
+                    {
+                        /* A small sliver of a large cylinder ina cell can give large surface area
+                         but low volume, hence snall "size". Therefore the vol/area formulation
+                         is only fully implemented when count is at least COUNT_FOR_SIZE.*/
+                        double nn, lobs, lobsMax;
+                        nn = obs_count(ix,iy,iz) - sub_count(ix,iy,iz).x() + grating_count(ix,iy,iz).x();
+                        if ( nn < 1.0 ) { nn = 1.0; }
+                        lobsMax = (area_block_s(ix,iy,iz).x() + area_block_r(ix,iy,iz).x()) / nn * std::sqrt( dy * dz );
+                        nn = obs_count(ix,iy,iz) - sub_count(ix,iy,iz).y() + grating_count(ix,iy,iz).y();
+                        if ( nn < 1.0 ) { nn = 1.0; }
+                        lobs = (area_block_s(ix,iy,iz).y() + area_block_r(ix,iy,iz).y()) / nn * std::sqrt( dz * dx );
+                        if ( lobs > lobsMax )
+                        {
+                            lobsMax = lobs;
+                        }
+
+                        nn = obs_count(ix,iy,iz) - sub_count(ix,iy,iz).z() + grating_count(ix,iy,iz).z();
+                        if ( nn < 1.0 ) { nn = 1.0; }
+                        lobs = (area_block_s(ix,iy,iz).z() + area_block_r(ix,iy,iz).z()) / nn * std::sqrt( dx * dy );
+                        if ( lobs > lobsMax )
+                        {
+                            lobsMax = lobs;
+                        }
+
+                        obs_size(ix,iy,iz) = lobsMax;
+                    }
+                }
+
+                /* The formulation correctly deals with triple intersections. For quadruple intersections
+                 and worse, there are very many second level overlaps and the resulting volume can be large
+                 positive. However, many or all of these may be eliminated because of the minimum volume of
+                 overlap blocks. Then the result can be negative volume - constrain accordingly
+                 */
+
+                if (v_block(ix,iy,iz) < 0)
+                {
+                    v_block(ix,iy,iz) = 0;
+                }
+                else if (v_block(ix,iy,iz) > 1)
+                {
+                    v_block(ix,iy,iz) = 1;
+                }
+
+                /* We can get -ve sub_count (ns) if two pipes/bars intersect and the dominat direction
+                 of the (-ve) intersection block is not the same as either of the intersecting obstacles.
+                 Also, if we have two hirizontal abrs intersecting, the overlap block can have vertical
+                 edges in a cell where the original bars do not. This can give -ve N and ns.
+                 Negative N is removed by  write_scalar.  */
+
+                for (direction cmpt=0; cmpt < vector::nComponents; ++cmpt)
+                {
+                    if (sub_count(ix,iy,iz)[cmpt] < 0)
+                    {
+                        sub_count(ix,iy,iz)[cmpt] = 0;
+                    }
+                }
+
+                v_block(ix,iy,iz) = 1.0 - v_block(ix,iy,iz); // Now porosity
+            }
+        }
+    }
+
+
+//*** Now we start writing the fields *********//
+
+        /* v_block is now porosity
+           The maximum value does not override the default value placed in the external cells,
+           so pars.cong_max_betav can be set just below 1 to mark the congested-region cells
+           for use by the adaptive mesh refinement.   */
+
+    IjkField<Vector<direction>> n_blocked_faces
+    (
+        faceDims,
+        Vector<direction>::uniform(0)
+    );
+
+    write_blocked_face_list
+    (
+        arr.face_block, arr.face_patch,
+        obs_count, sub_count, n_blocked_faces,
+        meshIndexing, patches,
+        pars.blockedFacePar, casepath
+    );
+    write_blockedCellsSet
+    (
+        arr.v_block,
+        meshIndexing, pars.blockedCellPoros, n_blocked_faces,
+        casepath, "blockedCellsSet"
+    );
+
+    write_scalarField
+    (
+        "betav", arr.v_block, 1, {0, pars.cong_max_betav}, "zeroGradient",
+        meshIndexing, patches,
+        dimless, casepath
+    );
+
+    for (label iz = 0; iz < pdrBlock.size(vector::Z); ++iz)
+    {
+        for (label ix = 0; ix < pdrBlock.size(vector::X); ++ix)
+        {
+            for (label iy = 0; iy < pdrBlock.size(vector::Y); ++iy)
+            {
+                const scalar cell_vol = pdrBlock.V(ix, iy, iz);
+
+                /* After the correction to set the number of obstacles normal to a blocked face
+                 to be zero, we can have N and all the components of ns the same. Then there
+                 are no obstacles in the cell as the number in each direction is n minus ns component),
+                 but N is not zero. This can cause problems. We reduce all four numbers by the same amount,
+                 which is OK as only the difference is used except when N is checked to se if there are
+                 any obstacles in then cell. */
+
+                scalar nmin = cmptMin(sub_count(ix,iy,iz));
+
+                sub_count(ix,iy,iz).x() -= nmin;
+                sub_count(ix,iy,iz).y() -= nmin;
+                sub_count(ix,iy,iz).z() -= nmin;
+
+                obs_count(ix,iy,iz) -= nmin;
+
+                assert(obs_count(ix,iy,iz) > -1);
+                if ( pars.new_fields )
+                {
+                    /* New fields Nv and nsv are intensive quantities that stay unchanged as a cell is subdivided
+                     We do not divide by cell volume because we assume that typical obstacle
+                     is a cylinder passing through the cell */
+                    const scalar cell_23 = ::pow(cell_vol, 2.0/3.0);
+                    obs_count(ix,iy,iz) /= cell_23;
+                    sub_count(ix,iy,iz) /= cell_23;
+                }
+            }
+        }
+    }
+
+
+    {
+        Info<< "Writing field files" << nl;
+
+        // obs_size is now the integral scale of the generated turbulence
+        write_scalarField
+        (
+            "Lobs", arr.obs_size, DEFAULT_LOBS, {0, 10}, "zeroGradient",
+            meshIndexing, patches,
+            dimLength, casepath
+        );
+        // surf is now surface area per unit volume
+        write_scalarField
+        (
+            "Aw", arr.surf, 0, {0, 1000}, "zeroGradient",
+            meshIndexing, patches,
+            inv(dimLength), casepath
+        );
+        write_symmTensorField
+        (
+            "CR", arr.drag_s, Zero, "zeroGradient",
+            meshIndexing, patches, inv(dimLength), casepath
+        );
+        write_symmTensorFieldV
+        (
+            "CT", cbdi, Zero, "zeroGradient",
+            meshIndexing, patches,
+            inv(dimLength), casepath
+        );
+        if ( pars.new_fields )
+        {
+            // These have been divided by cell volume ^ (2/3)
+            write_scalarField
+            (
+                "Nv", arr.obs_count, 0, {0, 1000}, "zeroGradient",
+                meshIndexing, patches,
+                dimless, casepath
+            );
+            write_symmTensorFieldV
+            (
+                "nsv", arr.sub_count, Zero, "zeroGradient",
+                meshIndexing, patches,
+                dimless, casepath
+            );
+        }
+        else
+        {
+            write_scalarField
+            (
+                "N", arr.obs_count, 0, {0, 1000}, "zeroGradient",
+                meshIndexing, patches,
+                dimless, casepath
+            );
+            write_symmTensorFieldV
+            (
+                "ns", arr.sub_count, Zero, "zeroGradient",
+                meshIndexing, patches, dimless, casepath
+            );
+        }
+
+        // Compute some further variables; store in already used arrays
+        // Re-use the drag array
+        drag_s = Zero;
+
+        for (label ix = 0; ix < pdrBlock.size(vector::X); ++ix)
+        {
+            for (label iy = 0; iy < pdrBlock.size(vector::Y); ++iy)
+            {
+                for (label iz = 0; iz < pdrBlock.size(vector::Z); ++iz)
+                {
+                    // Effective blockage ratio per cell/direction
+                    vector eff_block =
+                    (
+                        area_block_s(ix,iy,iz) * pars.cd_s/pars.cd_r
+                      + area_block_r(ix,iy,iz)
+                    );
+
+                    // Convert from B to Bv
+                    if (pars.new_fields)
+                    {
+                        eff_block /= pdrBlock.width(ix, iy, iz);
+                    }
+
+                    // Effective blockage is zero when faces are blocked
+                    for (direction cmpt=0; cmpt < vector::nComponents; ++cmpt)
+                    {
+                        if (dirn_block(ix,iy,iz)[cmpt] || eff_block[cmpt] < 0)
+                        {
+                            eff_block[cmpt] = 0;
+                        }
+                    }
+
+                    // Use the drag array to store the total effective blockage ratio per cell/direction
+                    // - off-diagonal already zeroed
+                    drag_s(ix,iy,iz).xx() = eff_block.x();
+                    drag_s(ix,iy,iz).yy() = eff_block.y();
+                    drag_s(ix,iy,iz).zz() = eff_block.z();
+
+                    cbdi(ix,iy,iz).x() = 1.0 / (betai_inv1(ix,iy,iz).x() + 1.0);
+                    cbdi(ix,iy,iz).y() = 1.0 / (betai_inv1(ix,iy,iz).y() + 1.0);
+                    cbdi(ix,iy,iz).z() = 1.0 / (betai_inv1(ix,iy,iz).z() + 1.0);
+
+                    if (cbdi(ix,iy,iz).z() < 0 || cbdi(ix,iy,iz).z() > 1.0)
+                    {
+                        WarningInFunction
+                            << "beta_i problem. z-betai_inv1=" << betai_inv1(ix,iy,iz).z()
+                            << " beta_i=" << cbdi(ix,iy,iz).z()
+                            << nl;
+                    }
+
+                    //Use the obs_size array to store Ep
+                    //We use Ep/(Xp-0.999) as length scale to avoid divide by zero,
+                    // so this is OK for initial Xp=1.
+                    obs_size(ix,iy,iz) = 0.001 / obs_size(ix,iy,iz);
+
+                    // Use the count array to store the combustion flag ( --1 everywhere in rectangular cells).
+                    obs_count(ix,iy,iz) = 1.0;
+                }
+            }
+        }
+
+        // drag array holds area blockage
+        if ( pars.new_fields )
+        {
+            write_symmTensorField
+            (
+                "Bv", arr.drag_s, Zero, "zeroGradient",
+                meshIndexing, patches,
+                dimless, casepath
+            );
+        }
+        else
+        {
+            write_symmTensorField
+            (
+                "B", arr.drag_s, Zero, "zeroGradient",
+                meshIndexing, patches,
+                dimless, casepath
+            );
+        }
+
+        // cbdi array holds beta_i
+        write_symmTensorFieldV
+        (
+            "betai", cbdi, symmTensor::I, "zeroGradient",
+            meshIndexing, patches,
+            dimless, casepath
+        );
+
+        // The longitudinal blockage
+        write_symmTensorFieldV
+        (
+            "Blong", arr.along_block, Zero, "zeroGradient",
+            meshIndexing, patches,
+            dimless, casepath
+        );
+
+        // obs_size array now contains 1/Lobs
+        write_scalarField
+        (
+            "Ep", arr.obs_size, DEFAULT_EP, {0, 10}, "zeroGradient",
+            meshIndexing, patches,
+            inv(dimLength), casepath
+        );
+        write_uniformField
+        (
+            "b", 1.0, "zeroGradient",
+            meshIndexing, patches,
+            dimless, casepath
+        );
+        write_uniformField
+        (
+            "k", DEFAULT_K, K_WALL_FN,
+            meshIndexing, patches,
+            sqr(dimVelocity),
+            casepath
+        );
+
+        write_uniformField
+        (
+            "epsilon", DEFAULT_EPS, EPS_WALL_FN,
+            meshIndexing, patches,
+            sqr(dimVelocity)/dimTime, casepath
+        );
+        write_uniformField
+        (
+            "ft", 0, "zeroGradient",
+            meshIndexing, patches,
+            dimless, casepath
+        );
+        write_uniformField
+        (
+            "Su", DEFAULT_SU, "zeroGradient",
+            meshIndexing, patches,
+            dimVelocity, casepath
+        );
+        write_uniformField
+        (
+            "T", DEFAULT_T, "zeroGradient",
+            meshIndexing, patches,
+            dimTemperature, casepath
+        );
+        write_uniformField
+        (
+            "Tu", DEFAULT_T, "zeroGradient",
+            meshIndexing,  patches,
+            dimTemperature, casepath
+        );
+        write_uniformField
+        (
+            "Xi", 1, "zeroGradient",
+            meshIndexing, patches,
+            dimless, casepath
+        );
+        write_uniformField
+        (
+            "Xp", 1, "zeroGradient",
+            meshIndexing, patches,
+            dimless, casepath
+        );
+        write_uniformField
+        (
+            "GRxp", 0, "zeroGradient",
+            meshIndexing, patches,
+            inv(dimTime), casepath
+        );
+        write_uniformField
+        (
+            "GRep", 0, "zeroGradient",
+            meshIndexing, patches,
+            inv(dimLength*dimTime), casepath
+        );
+        write_uniformField
+        (
+            "RPers", 0, "zeroGradient",
+            meshIndexing, patches,
+            inv(dimTime), casepath
+        );
+        write_pU_fields(meshIndexing, patches, casepath);
+
+        write_uniformField
+        (
+            "alphat", 0, ALPHAT_WALL,
+            meshIndexing, patches,
+            dimMass/(dimLength*dimTime),
+            casepath
+        );
+        write_uniformField
+        (
+            "mut", 0, MUT_WALL_FN,
+            meshIndexing, patches,
+            dimDynamicViscosity, casepath
+        );
+        // combustFlag is 1 in rectangular region, 0 or 1 elsewhere
+        // (although user could set it to another value)
+        if (equal(pars.outerCombFac, 1))
+        {
+            write_uniformField
+            (
+                "combustFlag", 1, "zeroGradient",
+                meshIndexing, patches,
+                dimless, casepath
+            );
+        }
+        else
+        {
+            write_scalarField
+            (
+                "combustFlag", arr.obs_count, pars.outerCombFac, {0, 1}, "zeroGradient",
+                meshIndexing, patches,
+                dimless, casepath
+            );
+        }
+        if ( pars.deluge )
+        {
+            write_uniformField
+            (
+                "H2OPS", 0, "zeroGradient",
+                meshIndexing, patches,
+                dimless, casepath
+            );
+            write_uniformField
+            (
+                "AIR", 0, "zeroGradient",
+                meshIndexing, patches,
+                dimless, casepath
+            );
+            write_uniformField
+            (
+                "Ydefault", 0, "zeroGradient",
+                meshIndexing, patches,
+                dimless, casepath
+            );
+            write_uniformField
+            (
+                "eRatio", 1, "zeroGradient",
+                meshIndexing, patches,
+                dimless, casepath
+            );
+            write_uniformField
+            (
+                "sprayFlag", 1, "zeroGradient",
+                meshIndexing, patches,
+                dimless, casepath
+            );
+        }
+    }
+}
+
+
+void Foam::PDRarrays::calculateAndWrite
+(
+    const fileName& casepath,
+    const PDRmeshArrays& meshIndexing,
+    const UList<PDRpatchDef>& patches
+)
+{
+    calculateAndWrite(*this, meshIndexing, casepath, patches);
+}
+
+
+void calc_drag_etc
+(
+    double brs, double  brr, bool  blocked,
+    double surr_br, double surr_dr,
+    scalar* drags_p, scalar* dragr_p,
+    double count,
+    scalar* cbdi_p,
+    double cell_vol
+)
+{
+    // Total blockage ratio
+    scalar br = brr + brs;
+
+    // Idealise obstacle arrangement as sqrt(count) rows.
+    // Make br the blockage ratio for each row.
+    if (count > 1.0) { br /= std::sqrt(count); }
+
+    const scalar alpha =
+    (
+        br < 0.99
+      ? (1.0 - 0.5 * br) / (1.0 - br) / (1.0 - br)
+      : GREAT
+    );
+
+    // For the moment keep separate the two contributions to the blockage-corrected drag
+    /* An isolated long obstcale will have two of the surronding eight cells with the same blockage,
+     so surr_br would be br/4. In this case no correction. Rising to full correction when
+     all surrounding cells have the same blockage. */
+    const scalar expon =
+    (
+        br > 0.0
+      ? min(max((surr_br / br - 0.25) * 4.0 / 3.0, scalar(0)), scalar(1))
+      : 0.0
+    );
+
+    const scalar alpha_r = ::pow(alpha, 0.5 + 0.5 * expon);
+    const scalar alpha_s = ::pow(alpha, expon);
+
+    *dragr_p *=  alpha_r;
+    *drags_p *=  ::pow(alpha_s, 1.09);
+    *cbdi_p  = ( pars.cb_r * pars.cd_r * *dragr_p + pars.cb_s * pars.cd_s * *drags_p );
+    if ( *cbdi_p < 0.0 ) { *cbdi_p = 0.0; }
+
+    // Finally sum the drag.
+    *drags_p = ( *drags_p * pars.cd_s + *dragr_p * pars.cd_r );
+    if ( *drags_p < 0.0 ) { *drags_p = 0.0; }
+    /* If well-blocked cells are surrounded by empty cells, the flow just goes round
+     and the drag parameters have little effect. So, for any cells much more empty
+     than the surrounding cells, we put some CR in there as well.                  */
+    if ( (surr_dr * 0.25) > *drags_p )
+    {
+        *drags_p = surr_dr * 0.25;
+        *cbdi_p = *drags_p * (pars.cb_r + pars.cb_s ) * 0.5;
+        // Don't know whether surr. stuff was round or sharp; use average of cb factors
+    }
+    if ( blocked ) { *cbdi_p = 0.0; *drags_p = 0.0; *dragr_p = 0.0; }
+}
+
+
+void Foam::PDRarrays::blockageSummary() const
+{
+    if (isNull(block()))
+    {
+        WarningInFunction
+            << nl
+            << "No blockage information - PDRblock is not set" << nl;
+        return;
+    }
+
+    const PDRblock& pdrBlock = block();
+
+    scalar totArea = 0;
+    scalar totCount = 0;
+    scalar totVolBlock = 0;
+
+    vector totBlock(Zero);
+    vector totDrag(Zero);
+
+    for (label iz = 0; iz < pdrBlock.size(vector::Z); ++iz)
+    {
+        for (label iy = 0; iy < pdrBlock.size(vector::Y); ++iy)
+        {
+            for (label ix = 0; ix < pdrBlock.size(vector::X); ++ix)
+            {
+                const labelVector ijk(ix,iy,iz);
+
+                totVolBlock += v_block(ijk) * pdrBlock.V(ijk);
+                totArea += surf(ijk);
+
+                totCount += max(0, obs_count(ijk));
+
+                totDrag.x() += max(0, drag_s(ijk).xx());
+                totDrag.y() += max(0, drag_s(ijk).yy());
+                totDrag.z() += max(0, drag_s(ijk).zz());
+
+                for (direction cmpt=0; cmpt < vector::nComponents; ++cmpt)
+                {
+                    totBlock[cmpt] += max(0, area_block_s(ijk)[cmpt]);
+                    totBlock[cmpt] += max(0, area_block_r(ijk)[cmpt]);
+                }
+            }
+        }
+    }
+
+    Info<< nl
+        << "Volume blockage: " << totVolBlock << nl
+        << "Total drag:  " << totDrag << nl
+        << "Total count: " << totCount << nl
+        << "Total area blockage: " << totBlock << nl
+        << "Total surface area: " << totArea << nl;
+}
+
+
+// ------------------------------------------------------------------------- //
+
+// Another temporary measure
+template<class Type>
+static void tail_field
+(
+    Ostream& os,
+    const Type& deflt,
+    const char* wall_bc,
+    const UList<PDRpatchDef>& patches
+)
+{
+    // seaGround
+    {
+        os.beginBlock("seaGround");
+        os.writeKeyword("type") << wall_bc << token::END_STATEMENT << nl;
+        putUniform(os, "value", deflt);
+        os.endBlock();
+    }
+
+    forAll(patches, patchi)
+    {
+        const word& patchName = patches[patchi].patchName;
+
+        if (PDRpatchDef::BLOCKED_FACE == patchi)
+        {
+            // blockedFaces
+            os.beginBlock(patchName);
+
+            // No wall functions for blockedFaces patch unless selected
+            if (pars.blockedFacesWallFn)
+            {
+                os.writeKeyword("type") << wall_bc << token::END_STATEMENT << nl;
+                putUniform(os, "value", deflt);
+            }
+            else
+            {
+                os.writeEntry("type", "zeroGradient");
+            }
+
+            os.endBlock();
+        }
+        else if (patches[patchi].patchType == 0)
+        {
+            os.beginBlock(patchName);
+
+            os.writeKeyword("type") << wall_bc << token::END_STATEMENT << nl;
+            putUniform(os, "value", deflt);
+
+            os.endBlock();
+        }
+        else
+        {
+            os.beginBlock(word(patchName + "Wall"));
+            os.writeKeyword("type") << wall_bc << token::END_STATEMENT << nl;
+            putUniform(os, "value", deflt);
+            os.endBlock();
+
+            os.beginBlock(word(patchName + "Cyclic_half0"));
+            os.writeEntry("type", "cyclic");
+            os.endBlock();
+
+            os.beginBlock(word(patchName + "Cyclic_half1"));
+            os.writeEntry("type", "cyclic");
+            os.endBlock();
+        }
+    }
+
+    if (pars.yCyclic)
+    {
+        os.beginBlock("Cyclic_half0");
+        os.writeEntry("type", "cyclic");
+        os.endBlock();
+
+        os.beginBlock("Cyclic_half1");
+        os.writeEntry("type", "cyclic");
+        os.endBlock();
+    }
+    else
+    {
+        os.beginBlock("ySymmetry");
+        os.writeEntry("type", "symmetryPlane");
+        os.endBlock();
+    }
+
+    if ( pars.two_d )
+    {
+        os.beginBlock("z_boundaries");
+        os.writeEntry("type", "empty");
+        os.endBlock();
+    }
+    if ( pars.outer_orthog )
+    {
+        os.beginBlock("outer_inner");
+        os.writeEntry("type", "cyclicAMI");
+        os.writeEntry("neighbourPatch", "inner_outer");
+        os.endBlock();
+
+        os.beginBlock("inner_outer");
+        os.writeEntry("type", "cyclicAMI");
+        os.writeEntry("neighbourPatch", "outer_inner");
+        os.endBlock();
+    }
+}
+
+
+// ------------------------------------------------------------------------- //
+
+void write_scalarField
+(
+    const word& fieldName, const IjkField<scalar>& fld,
+    const scalar& deflt, const scalarMinMax& limits, const char *wall_bc,
+    const PDRmeshArrays& meshIndexing,
+    const UList<PDRpatchDef>& patches,
+    const dimensionSet& dims, const fileName& casepath
+)
+{
+    fileName path = (casepath / pars.timeName / fieldName);
+    OFstream os(path);
+    os.precision(outputPrecision);
+
+    make_header(os, "", volScalarField::typeName, fieldName);
+
+    os.writeEntry("dimensions", dims);
+
+    os << nl;
+    os.writeKeyword("internalField")
+        << "nonuniform List<scalar>" << nl
+        << meshIndexing.nCells() << nl << token::BEGIN_LIST << nl;
+
+    for (label celli=0; celli < meshIndexing.nCells(); ++celli)
+    {
+        const labelVector& cellIdx = meshIndexing.cellIndex[celli];
+
+        if (cmptMin(cellIdx) < 0)
+        {
+            os  << deflt << nl;
+            continue;
+        }
+
+        os << limits.clip(fld(cellIdx)) << nl;
+    }
+
+    os << token::END_LIST << token::END_STATEMENT << nl;
+
+    os << nl;
+    os.beginBlock("boundaryField");
+
+    // outer
+    {
+        os.beginBlock("outer");
+
+        os.writeEntry("type", "inletOutlet");
+        putUniform(os, "inletValue", deflt);
+        putUniform(os, "value", deflt);
+
+        os.endBlock();
+    }
+
+    tail_field(os, deflt, wall_bc, patches);
+
+    os.endBlock(); // boundaryField
+
+    IOobject::writeEndDivider(os);
+}
+
+
+// ------------------------------------------------------------------------- //
+
+void write_uniformField
+(
+    const word& fieldName, const scalar& deflt, const char *wall_bc,
+    const PDRmeshArrays& meshIndexing,
+    const UList<PDRpatchDef>& patches,
+    const dimensionSet& dims, const fileName& casepath
+)
+{
+    OFstream os(casepath / pars.timeName / fieldName);
+    os.precision(outputPrecision);
+
+    make_header(os, "", volScalarField::typeName, fieldName);
+
+    os.writeEntry("dimensions", dims);
+
+    os << nl;
+    putUniform(os, "internalField", deflt);
+
+    os << nl;
+    os.beginBlock("boundaryField");
+
+    // outer
+    {
+        os.beginBlock("outer");
+        if (fieldName == "alphat" || fieldName == "mut")
+        {
+            // Different b.c. for alphat & mut
+            os.writeEntry("type", "calculated");
+        }
+        else
+        {
+            os.writeEntry("type", "inletOutlet");
+            putUniform(os, "inletValue", deflt);
+        }
+
+        putUniform(os, "value", deflt);
+        os.endBlock();
+    }
+
+    tail_field(os, deflt, wall_bc, patches);
+
+    os.endBlock(); // boundaryField
+
+    IOobject::writeEndDivider(os);
+}
+
+
+// ------------------------------------------------------------------------- //
+
+void write_pU_fields
+(
+    const PDRmeshArrays& meshIndexing,
+    const UList<PDRpatchDef>& patches,
+    const fileName& casepath
+)
+{
+    // Velocity field
+    {
+        OFstream os(casepath / pars.timeName / "Ubet");
+        os.precision(outputPrecision);
+
+        make_header(os, "", volVectorField::typeName, "Ubet");
+
+        os.writeEntry("dimensions", dimVelocity);
+
+        os << nl;
+        putUniform(os, "internalField", vector::zero);
+
+        os << nl;
+        os.beginBlock("boundaryField");
+
+        // "outer"
+        {
+            os.beginBlock("outer");
+            os.writeEntry("type", "inletOutlet");
+            putUniform(os, "inletValue", vector::zero);
+            os.endBlock();
+        }
+
+        // seaGround
+        {
+            os.beginBlock("seaGround");
+            os.writeEntry("type", "zeroGradient");
+            os.endBlock();
+        }
+
+        // Patch 0 is the blocked faces' and 1 is mergingFaces for ignition cell
+        for (label patchi = 0; patchi < 3; ++patchi)
+        {
+            os.beginBlock(patches[patchi].patchName);
+            os.writeKeyword("type") << pars.UPatchBc.c_str()
+                << token::END_STATEMENT << nl;
+            os.endBlock();
+        }
+
+        for (label patchi = 3; patchi < patches.size(); ++patchi)
+        {
+            const PDRpatchDef& p = patches[patchi];
+            const word& patchName = p.patchName;
+
+            if (p.patchType == 0)
+            {
+                os.beginBlock(patchName);
+
+                os.writeEntry("type", "timeVaryingMappedFixedValue");
+                os.writeEntry("fileName", "<case>" / (patchName + ".dat"));
+                os.writeEntry("outOfBounds", "clamp");
+                putUniform(os, "value", vector::zero);
+                os.endBlock();
+            }
+            else
+            {
+                os.beginBlock(word(patchName + "Wall"));
+                os.writeEntry("type", "activePressureForceBaffleVelocity");
+
+                os.writeEntry("cyclicPatch", word(patchName + "Cyclic_half0"));
+                os.writeEntry("openFraction", 0); // closed
+                os.writeEntry("openingTime", p.blowoffTime);
+                os.writeEntry("maxOpenFractionDelta", 0.1);
+                os.writeEntry("forceBased", "false");
+                os.writeEntry("opening", "true");
+
+                putUniform(os, "value", vector::zero);
+                os.endBlock();
+
+                os.beginBlock(word(patchName + "Cyclic_half0"));
+                os.writeEntry("type", "cyclic");
+                putUniform(os, "value", vector::zero);
+                os.endBlock();
+
+                os.beginBlock(word(patchName + "Cyclic_half1"));
+                os.writeEntry("type", "cyclic");
+                putUniform(os, "value", vector::zero);
+                os.endBlock();
+            }
+        }
+
+        if (pars.yCyclic)
+        {
+            os.beginBlock("yCyclic_half0");
+            os.writeEntry("type", "cyclic");
+            os.endBlock();
+
+            os.beginBlock("yCyclic_half1");
+            os.writeEntry("type", "cyclic");
+            os.endBlock();
+        }
+        else
+        {
+            os.beginBlock("ySymmetry");
+            os.writeEntry("type", "symmetryPlane");
+            os.endBlock();
+        }
+
+        if ( pars.outer_orthog )
+        {
+            os.beginBlock("outer_inner");
+            os.writeEntry("type", "cyclicAMI");
+            os.writeEntry("neighbourPatch", "inner_outer");
+            os.endBlock();
+
+            os.beginBlock("inner_outer");
+            os.writeEntry("type", "cyclicAMI");
+            os.writeEntry("neighbourPatch", "outer_inner");
+        }
+
+        os.endBlock();  // boundaryField
+
+        IOobject::writeEndDivider(os);
+    }
+
+
+    // Pressure field
+    {
+        const scalar deflt = DEFAULT_P;
+        const char *wall_bc = "zeroGradient;\n\trho\trho";
+
+        OFstream os(casepath / pars.timeName / "p");
+        os.precision(outputPrecision);
+
+        make_header(os, "", volScalarField::typeName, "p");
+
+        os.writeEntry("dimensions", dimPressure);
+
+        os << nl;
+        putUniform(os, "internalField", deflt);
+
+        os << nl;
+        os.beginBlock("boundaryField");
+
+        // "outer"
+        {
+            os.beginBlock("outer");
+            os.writeEntry("type", "waveTransmissive");
+            os.writeEntry("gamma", 1.3);
+            os.writeEntry("fieldInf", deflt);
+            os.writeEntry("lInf", 5);
+            putUniform(os, "value", deflt);
+            os.endBlock();
+        }
+
+        tail_field(os, deflt, wall_bc, patches);
+
+        os.endBlock();  // boundaryField
+
+        IOobject::writeEndDivider(os);
+    }
+}
+
+
+// ------------------------------------------------------------------------- //
+
+void write_symmTensorField
+(
+    const word& fieldName,
+    const IjkField<symmTensor>& fld,
+    const symmTensor& deflt, const char *wall_bc,
+    const PDRmeshArrays& meshIndexing,
+    const UList<PDRpatchDef>& patches,
+    const dimensionSet& dims, const fileName& casepath
+)
+{
+    OFstream os(casepath / pars.timeName / fieldName);
+    os.precision(outputPrecision);
+
+    make_header(os, "", volSymmTensorField::typeName, fieldName);
+
+    os.writeEntry("dimensions", dims);
+
+    os << nl;
+    os.writeKeyword("internalField")
+        << "nonuniform List<symmTensor>" << nl
+        << meshIndexing.nCells() << nl << token::BEGIN_LIST << nl;
+
+    for (label celli=0; celli < meshIndexing.nCells(); ++celli)
+    {
+        const labelVector& cellIdx = meshIndexing.cellIndex[celli];
+
+        if (cmptMin(cellIdx) < 0)
+        {
+            os  << deflt << nl;
+            continue;
+        }
+
+        os << fld(cellIdx) << nl;
+    }
+    os << token::END_LIST << token::END_STATEMENT << nl;
+
+    os << nl;
+    os.beginBlock("boundaryField");
+
+    // outer
+    {
+        os.beginBlock("outer");
+
+        os.writeEntry("type", "inletOutlet");
+        putUniform(os, "inletValue", deflt);
+        putUniform(os, "value", deflt);
+
+        os.endBlock();
+    }
+
+    tail_field(os, deflt, wall_bc, patches);
+
+    os.endBlock(); // boundaryField
+
+    IOobject::writeEndDivider(os);
+}
+
+
+// Write a volSymmTensorField but with vectors as input.
+// The off-diagonals are zero.
+void write_symmTensorFieldV
+(
+    const word& fieldName,
+    const IjkField<vector>& fld,
+    const symmTensor& deflt, const char *wall_bc,
+    const PDRmeshArrays& meshIndexing,
+    const UList<PDRpatchDef>& patches,
+    const dimensionSet& dims, const fileName& casepath
+)
+{
+    OFstream os(casepath / pars.timeName / fieldName);
+    os.precision(outputPrecision);
+
+    make_header(os, "", volSymmTensorField::typeName, fieldName);
+
+    os.writeEntry("dimensions", dims);
+
+    os << nl;
+    os.writeKeyword("internalField")
+        << "nonuniform List<symmTensor>" << nl
+        << meshIndexing.nCells() << nl << token::BEGIN_LIST << nl;
+
+    symmTensor val(symmTensor::zero);
+
+    for (label celli=0; celli < meshIndexing.nCells(); ++celli)
+    {
+        const labelVector& cellIdx = meshIndexing.cellIndex[celli];
+
+        if (cmptMin(cellIdx) < 0)
+        {
+            os  << deflt << nl;
+            continue;
+        }
+
+        const vector& vec = fld(cellIdx);
+
+        val.xx() = vec.x();
+        val.yy() = vec.y();
+        val.zz() = vec.z();
+
+        os << val << nl;
+    }
+    os << token::END_LIST << token::END_STATEMENT << nl;
+
+    os << nl;
+    os.beginBlock("boundaryField");
+
+    // outer
+    {
+        os.beginBlock("outer");
+
+        os.writeEntry("type", "inletOutlet");
+        putUniform(os, "inletValue", deflt);
+        putUniform(os, "value", deflt);
+
+        os.endBlock();
+    }
+
+    tail_field(os, deflt, wall_bc, patches);
+
+    os.endBlock(); // boundaryField
+
+    IOobject::writeEndDivider(os);
+}
+
+
+// ------------------------------------------------------------------------- //
+
+void write_blocked_face_list
+(
+    const IjkField<vector>& face_block,
+    const IjkField<labelVector>& face_patch,
+    const IjkField<scalar>& obs_count, IjkField<vector>& sub_count,
+    IjkField<Vector<direction>>& n_blocked_faces,
+    const PDRmeshArrays& meshIndexing,
+    const UList<PDRpatchDef>& patches,
+    double limit_par, const fileName& casepath
+)
+{
+    /* Create the lists of face numbers for faces that have already been defined as
+     belonging to (inlet) patches), and others that are found to be blocked.
+     Then write these out to set files,    */
+
+    const labelVector& cellDims = meshIndexing.cellDims;
+
+    Map<bitSet> usedFaces;
+
+    Info<< "Number of patches: " << patches.size() << nl;
+
+    for (label facei=0; facei < meshIndexing.nFaces(); ++facei)
+    {
+        // The related i-j-k face index for the mesh face
+        const labelVector& faceIdx = meshIndexing.faceIndex[facei];
+
+        if (cmptMin(faceIdx) < 0)
+        {
+            continue;
+        }
+
+        const label ix = faceIdx.x();
+        const label iy = faceIdx.y();
+        const label iz = faceIdx.z();
+        const direction orient = meshIndexing.faceOrient[facei];
+
+        label patchId = -1;
+        scalar val(Zero);
+
+        /* A bit messy to be changing sub_count here. but there is a problem of generation
+         of subgrid flame area Xp when the flame approaches a blocked wall. the fix is to make
+         the normal component of "n" zero in the cells adjacent to the blocked face. That component
+         of n is zero when that component of sub_count i.e. ns) equals count (i.e. N). */
+        {
+            switch (orient)
+            {
+                case vector::X:
+                {
+                    // face_block is the face blockage;
+                    // face_patch is the patch number on the face (if any)
+                    val = face_block(faceIdx).x();
+                    patchId = face_patch(faceIdx).x();
+
+                    if
+                    (
+                        val > limit_par
+                     && iy < cellDims[vector::Y]
+                     && iz < cellDims[vector::Z]
+                    )
+                    {
+                        // n_blocked_faces:
+                        // count of x-faces blocked for this cell
+
+                        if (ix < cellDims[vector::X])
+                        {
+                            ++n_blocked_faces(ix,iy,iz).x();
+                            sub_count(ix,iy,iz).x() = obs_count(ix,iy,iz);
+                        }
+
+                        if (ix > 0)
+                        {
+                            // And the neighbouring cell
+                            ++n_blocked_faces(ix-1,iy,iz).x();
+                            sub_count(ix-1,iy,iz).x() = obs_count(ix-1,iy,iz);
+                        }
+                    }
+                }
+                break;
+
+                case vector::Y:
+                {
+                    val = face_block(faceIdx).y();
+                    patchId = face_patch(faceIdx).y();
+
+                    if
+                    (
+                        val > limit_par
+                     && iz < cellDims[vector::Z]
+                     && ix < cellDims[vector::X]
+                    )
+                    {
+                        // n_blocked_faces:
+                        // count of y-faces blocked for this cell
+
+                        if (iy < cellDims[vector::Y])
+                        {
+                            ++n_blocked_faces(ix,iy,iz).y();
+                            sub_count(ix,iy,iz).y() = obs_count(ix,iy,iz);
+                        }
+
+                        if (iy > 0)
+                        {
+                            // And the neighbouring cell
+                            ++n_blocked_faces(ix,iy-1,iz).y();
+                            sub_count(ix,iy-1,iz).y() = obs_count(ix,iy-1,iz);
+                        }
+                    }
+                }
+                break;
+
+                case vector::Z:
+                {
+                    val = face_block(faceIdx).z();
+                    patchId = face_patch(faceIdx).z();
+
+                    if
+                    (
+                        val > limit_par
+                     && ix < cellDims[vector::X]
+                     && iy < cellDims[vector::Y]
+                    )
+                    {
+                        // n_blocked_faces:
+                        // count of z-faces blocked for this cell
+
+                        if (iz < cellDims[vector::Z])
+                        {
+                            ++n_blocked_faces(ix,iy,iz).z();
+                            sub_count(ix,iy,iz).z() = obs_count(ix,iy,iz);
+                        }
+
+                        if (iz > 0)
+                        {
+                            // And the neighbouring cell
+                            ++n_blocked_faces(ix,iy,iz-1).z();
+                            sub_count(ix,iy,iz-1).z() = obs_count(ix,iy,iz-1);
+                        }
+                    }
+                }
+                break;
+            }
+
+            if (patchId > 0)
+            {
+                // If this face is on a defined patch add to list
+                usedFaces(patchId).set(facei);
+            }
+            else if (val > limit_par)
+            {
+                // Add to blocked faces list
+                usedFaces(PDRpatchDef::BLOCKED_FACE).set(facei);
+            }
+        }
+    }
+
+    // Write in time or constant dir
+    const bool hasPolyMeshTimeDir = isDir(casepath/pars.timeName/"polyMesh");
+
+    const fileName setsDir =
+    (
+        casepath
+      / (hasPolyMeshTimeDir ? pars.timeName : word("constant"))
+      / fileName("polyMesh/sets")
+    );
+
+    if (!isDir(setsDir))
+    {
+        mkDir(setsDir);
+    }
+
+
+    // Create as blockedFaces Set file for each patch, including
+    // basic blocked faces
+    forAll(patches, patchi)
+    {
+        const word& patchName = patches[patchi].patchName;
+
+        OFstream os(setsDir / (patchName + "Set"));
+
+        make_header(os, "polyMesh/sets", "faceSet", patchName);
+
+        // Check for blocked faces
+        const auto& fnd = usedFaces.cfind(patchi);
+
+        if (fnd.good() && (*fnd).any())
+        {
+            os << nl << (*fnd).toc() << nl;
+        }
+        else
+        {
+            os << nl << labelList() << nl;
+        }
+
+        IOobject::writeEndDivider(os);
+    }
+
+    // Create the PDRMeshDict, listing the blocked faces sets and their patch names
+
+    {
+        DynamicList<word> panelNames;
+
+        OFstream os(casepath / "system/PDRMeshDict");
+
+        make_header(os, "system", "dictionary", "PDRMeshDict");
+
+        os.writeEntry("blockedCells", "blockedCellsSet");
+        os << nl << "blockedFaces" << nl << token::BEGIN_LIST << nl;
+
+        for (const PDRpatchDef& p : patches)
+        {
+            const word& patchName = p.patchName;
+            const word setName = patchName + "Set";
+
+            if (p.patchType == 0)  // Patch
+            {
+                os  << "    " << token::BEGIN_LIST
+                    << setName << token::SPACE
+                    << patchName << token::END_LIST
+                    << nl;
+            }
+            else if (p.patchType > 0)  // Panel
+            {
+                panelNames.append(setName);
+            }
+        }
+
+        os  << token::END_LIST << token::END_STATEMENT << nl << nl;
+        os.beginBlock("coupledFaces");
+
+        for (const PDRpatchDef& p : patches)
+        {
+            const word& patchName = p.patchName;
+            const word setName = patchName + "Set";
+
+            if (p.patchType > 0)  // Panel
+            {
+                os.beginBlock(setName);
+                os.writeEntry("wallPatchName", word(patchName + "Wall"));
+                os.writeEntry("cyclicMasterPatchName", patchName);
+                os.endBlock();
+            }
+        }
+        os.endBlock() << nl;
+
+        os.writeEntry("defaultPatch", "blockedFaces");
+
+        IOobject::writeEndDivider(os);
+
+        // Write panelList
+        OFstream(casepath / "panelList")()
+            << panelNames << token::END_STATEMENT << nl;
+    }
+}
+
+
+void write_blockedCellsSet
+(
+    const IjkField<scalar>& fld,
+    const PDRmeshArrays& meshIndexing,
+    double limit_par,
+    const IjkField<Vector<direction>>& n_blocked_faces,
+    const fileName& casepath,
+    const word& listName
+)
+{
+    if (listName.empty())
+    {
+        return;
+    }
+
+    // Write in time or constant dir
+    const bool hasPolyMeshTimeDir = isDir(casepath/pars.timeName/"polyMesh");
+
+    const fileName path =
+    (
+        casepath
+      / (hasPolyMeshTimeDir ? pars.timeName : word("constant"))
+      / fileName("polyMesh/sets")
+      / listName
+    );
+
+    if (!isDir(path.path()))
+    {
+        mkDir(path.path());
+    }
+
+    bitSet blockedCell;
+
+    for (label celli=0; celli < meshIndexing.nCells(); ++celli)
+    {
+        const labelVector& cellIdx = meshIndexing.cellIndex[celli];
+
+        if (cmptMin(cellIdx) < 0)
+        {
+            continue;
+        }
+
+        if (fld(cellIdx) < limit_par)
+        {
+            blockedCell.set(celli);
+            continue;
+        }
+
+        const Vector<direction>& blocked = n_blocked_faces(cellIdx);
+
+        const label n_bfaces = cmptSum(blocked);
+
+        label n_bpairs = 0;
+
+        if (n_bfaces > 1)
+        {
+            for (direction cmpt=0; cmpt < vector::nComponents; ++cmpt)
+            {
+                if (blocked[cmpt] > 1) ++n_bpairs;
+            }
+
+            #if 0
+            // Extra debugging
+            Info<<"block " << celli << " from "
+                << blocked << " -> ("
+                << n_bfaces << ' ' << n_bpairs
+                << ')' << nl;
+            #endif
+        }
+
+        if
+        (
+            n_bfaces >= pars.nFacesToBlockC
+         || n_bpairs >= pars.nPairsToBlockC
+        )
+        {
+            blockedCell.set(celli);
+        }
+    }
+
+
+    OFstream os(path);
+    make_header(os, "constant/polyMesh/sets", "cellSet", listName);
+
+    if (blockedCell.any())
+    {
+        os << blockedCell.toc();
+    }
+    else
+    {
+        os << labelList();
+    }
+
+    os << token::END_STATEMENT << nl;
+
+    IOobject::writeEndDivider(os);
+}
+
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/PDRlegacy.H b/applications/utilities/preProcessing/PDRsetFields/PDRlegacy.H
new file mode 100644
index 00000000000..bbb660f04ab
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/PDRlegacy.H
@@ -0,0 +1,75 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2016 Shell Research Ltd.
+    Copyright (C) 2019 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/>.
+
+Namespace
+    Foam::PDRlegacy
+
+Description
+    Legacy and transitional routines
+
+SourceFiles
+    PDRlegacyMeshSpec.C
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef PDRlegacy_H
+#define PDRlegacy_H
+
+#include "PDRblock.H"
+#include "fileName.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+// Forward Declarations
+class ISstream;
+class PDRblock;
+
+/*---------------------------------------------------------------------------*\
+                              Namespace PDRlegacy
+\*---------------------------------------------------------------------------*/
+
+namespace PDRlegacy
+{
+    void print_info(const PDRblock& block);
+
+    void read_mesh_spec(const fileName& casepath, PDRblock& pdrBlock);
+
+    void read_mesh_spec(ISstream& is, PDRblock& pdrBlock);
+}
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/PDRlegacyMeshSpec.C b/applications/utilities/preProcessing/PDRsetFields/PDRlegacyMeshSpec.C
new file mode 100644
index 00000000000..720ec86359f
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/PDRlegacyMeshSpec.C
@@ -0,0 +1,340 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2016 Shell Research Ltd.
+    Copyright (C) 2019 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/>.
+
+\*---------------------------------------------------------------------------*/
+
+// Read spec that resemble this:
+//
+//   xmesh
+//   (
+//       ( -8.18  14  0.83 )
+//       ( -0.69  11  1.00 )
+//       (  0.69  14  1.20 )
+//       (  8.18  0 )
+//   )
+//
+//   ymesh
+//   (
+//       ...
+//   )
+
+#include "PDRlegacy.H"
+
+// OpenFOAM includes
+#include "error.H"
+#include "IFstream.H"
+#include "stringOps.H"
+#include "OSspecific.H"
+
+#define XMESH_TAG "xmesh"
+#define YMESH_TAG "ymesh"
+#define ZMESH_TAG "zmesh"
+
+
+// * * * * * * * * * * * * * * * Local Functions * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace PDRlegacy
+{
+namespace Detail
+{
+
+//- Simple structure for processing a mesh spec entry
+//  Handles an entry with (float), (float int), or (float int float)
+//
+//  This is for handling a sub-spec that resembles this:
+//
+//  (
+//      ( -8.18  14  0.83 )
+//      ( -0.69  11  1.00 )
+//      (  0.69  14  1.20 )
+//      (  8.18  0 )
+//  )
+struct pdrMeshSpecLine
+{
+    scalar knot;
+    label  ndiv;
+    scalar factor;
+
+    pdrMeshSpecLine() : knot(0), ndiv(0), factor(0) {}
+
+    //- Cheap means to avoid uniform list output
+    bool operator!=(const pdrMeshSpecLine&) const
+    {
+        return false;
+    }
+};
+
+
+// Read mesh-spec entry
+Istream& operator>>(Istream& is, pdrMeshSpecLine& spec)
+{
+    spec.knot = 0;
+    spec.ndiv = 0;
+    spec.factor = 0;
+
+    is.readBegin("pdrMeshSpecLine");
+
+    // Must have a point
+    is >> spec.knot;
+
+    token tok(is);
+    if (tok.isLabel())
+    {
+        spec.ndiv = tok.labelToken();
+
+        if (spec.ndiv)
+        {
+            is >> spec.factor;
+        }
+    }
+    else
+    {
+        is.putBack(tok);
+    }
+
+    is.readEnd("pdrMeshSpecLine");
+
+    is.check(FUNCTION_NAME);
+    return is;
+}
+
+#ifdef FULLDEBUG
+// Write mesh-spec entry
+Ostream& operator<<(Ostream& os, const pdrMeshSpecLine& spec)
+{
+    os << token::BEGIN_LIST << spec.knot;
+
+    if (spec.ndiv)
+    {
+        os  << token::SPACE << spec.ndiv
+            << token::SPACE << spec.factor;
+    }
+
+    os << token::END_LIST;
+
+    return os;
+}
+#endif
+
+
+void read_spec(ISstream& is, const direction cmpt, List<scalar>& gridPoint)
+{
+    if (!gridPoint.empty())
+    {
+        FatalErrorInFunction
+            << "Duplicate specification of "
+            << vector::componentNames[cmpt]
+            << " grid"
+            << exit(FatalError);
+    }
+
+    List<pdrMeshSpecLine> specs(is);
+
+    if (specs.size() < 2)
+    {
+        FatalErrorInFunction
+            << "Grid specification for " << vector::componentNames[cmpt]
+            << " is too small. Need at least two points!" << nl
+            << exit(FatalError);
+    }
+
+    specs.last().ndiv = 0; // safety
+
+
+    DynamicList<scalar> knots;
+    DynamicList<label>  divisions;
+    DynamicList<scalar> factors;
+
+    for (const auto& spec : specs)
+    {
+        knots.append(spec.knot);
+
+        if (spec.ndiv < 1)
+        {
+            break;
+        }
+        divisions.append(spec.ndiv);
+        factors.append(spec.factor);
+    }
+
+    label nPoints = 1;
+    for (const label nDiv : divisions)
+    {
+        nPoints += nDiv;
+    }
+
+    if (nPoints < 2)
+    {
+        FatalErrorInFunction
+            << "No cells defined for direction "
+            << vector::componentNames[cmpt] << nl
+            << exit(FatalError);
+    }
+
+
+    // Define the grid points
+    gridPoint.resize(nPoints);
+
+    const label nSegments = divisions.size();
+
+    label start = 0;
+
+    for (label segmenti=0; segmenti < nSegments; ++segmenti)
+    {
+        const label nDiv = divisions[segmenti];
+        const scalar factor = factors[segmenti];
+
+        SubList<scalar> subPoint(gridPoint, nDiv+1, start);
+        start += nDiv;
+
+        subPoint[0] = knots[segmenti];
+        subPoint[nDiv] = knots[segmenti+1];
+
+        const scalar dist = (subPoint.last() - subPoint.first());
+
+        if (equal(factor, scalar(1)))
+        {
+            for (label i=1; i < nDiv; ++i)
+            {
+                subPoint[i] = (subPoint[0] + (dist * i)/nDiv);
+            }
+        }
+        else
+        {
+            scalar delta = dist * (1.0 - factor) / (1.0 - ::pow(factor, nDiv));
+
+            scalar xyz = subPoint[0];
+
+            for (label i=0; i < nDiv; ++i)
+            {
+                subPoint[i] = xyz;
+                xyz += delta;
+                delta *= factor;
+            }
+        }
+    }
+}
+
+
+} // End namespace Detail
+} // End namespace PDRlegacy
+} // End namespace Foam
+
+
+// * * * * * * * * * * * * * * * Global Functions  * * * * * * * * * * * * * //
+
+void Foam::PDRlegacy::print_info(const PDRblock& block)
+{
+    Info<< "PDRblock" << nl
+        << "    nCells: " << block.sizes() << nl
+        << "    Box: " << block.bounds() << nl
+        << "x " << flatOutput(block.grid().x()) << nl
+        << "y " << flatOutput(block.grid().y()) << nl
+        << "z " << flatOutput(block.grid().z()) << nl
+        << endl;
+}
+
+
+void Foam::PDRlegacy::read_mesh_spec(const fileName& casepath, PDRblock& block)
+{
+    Info<< "Reading pdrMeshSpec (legacy format)" << nl;
+
+    bool processed = false;
+
+    for (const fileName dirName : { "system", "constant/polyMesh" })
+    {
+        fileName path
+        (
+            casepath / dirName / "pdrMeshSpec"
+        );
+
+        if (Foam::isFile(path))
+        {
+            IFstream is(path);
+
+            read_mesh_spec(is, block);
+            processed = true;
+            break;
+        }
+    }
+
+    if (!processed)
+    {
+        FatalErrorInFunction
+            << "Did not process pdrMeshSpec" << nl
+            << exit(FatalError);
+    }
+}
+
+
+void Foam::PDRlegacy::read_mesh_spec(ISstream& is, PDRblock& block)
+{
+    Vector<scalarList> grid;
+
+    string line;
+
+    while (is.good())
+    {
+        is.getLine(line);
+        stringOps::inplaceTrim(line);
+
+        if (line == XMESH_TAG)
+        {
+            Detail::read_spec(is, vector::X, grid.x());
+        }
+        else if (line == YMESH_TAG)
+        {
+            Detail::read_spec(is, vector::Y, grid.y());
+        }
+        else if (line == ZMESH_TAG)
+        {
+            Detail::read_spec(is, vector::Z, grid.z());
+        }
+    }
+
+    for (direction cmpt=0; cmpt < vector::nComponents; ++cmpt)
+    {
+        if (grid[cmpt].empty())
+        {
+            FatalErrorInFunction
+                << "No specification for "
+                << vector::componentNames[cmpt]
+                << " grid" << nl
+                << exit(FatalError);
+        }
+    }
+
+    block.reset(grid.x(), grid.y(), grid.z());
+
+    #ifdef FULLDEBUG
+    print_info(block);
+    #endif
+}
+
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/PDRmeshArrays.C b/applications/utilities/preProcessing/PDRsetFields/PDRmeshArrays.C
new file mode 100644
index 00000000000..8e6a6c1fd95
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/PDRmeshArrays.C
@@ -0,0 +1,262 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2016 Shell Research Ltd.
+    Copyright (C) 2019 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 "PDRmeshArrays.H"
+#include "PDRblock.H"
+#include "polyMesh.H"
+#include "Time.H"
+#include "IjkField.H"
+
+// Notes
+//
+// Determines the face and cell numbers of all faces and cells in the
+// central rectangular region where CAD_PDR operates. First,
+// "points" is read and the coordinates (by which I mean here the
+// indices in the x, y and z coordinate arrays) are determined. Then
+// "faces" is read and for each the coordinates of the lower- x,y,z
+// corner are determioned, also the orientation (X, Y or Z).
+// (Orientation in the sense of e.g. + or -x is not noted.) The files
+// "owner" and "neighbour" specify the six faces around each cell, so
+// from these the coordinates of the cells are determined.
+//
+// Full checks are made that the mesh in the central region is consistent
+// with CAD_PDR's mesh specified by the PDRmeshSpec file.
+//
+// Eventually, when writing out results, we shall work through the
+// full list of cells, writing default values for any cells that are
+// not in the central regtion.
+
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+Foam::scalar Foam::PDRmeshArrays::gridPointRelTol = 0.02;
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+void Foam::PDRmeshArrays::classify
+(
+    const polyMesh& mesh,
+    const PDRblock& pdrBlock
+)
+{
+    // Additional copy of i-j-k addressing
+    cellDims = pdrBlock.sizes();
+    faceDims = (cellDims + labelVector::one);
+
+    const label maxPointId = cmptMax(pdrBlock.sizes())+1;
+
+    Info<< "Mesh" << nl
+        << "  nPoints:" << mesh.nPoints()
+        << "  nCells:" << mesh.nCells()
+        << "  nFaces:" << mesh.nFaces() << nl;
+
+    Info<< "PDRblock" << nl
+        << "  minEdgeLen:" << pdrBlock.minEdgeLen() << nl;
+
+
+    // Bin points into i-j-k locations
+    List<labelVector> pointIndex(mesh.nPoints());
+
+    for (label pointi=0; pointi < mesh.nPoints(); ++pointi)
+    {
+        const point& pt = mesh.points()[pointi];
+        pointIndex[pointi] = pdrBlock.gridIndex(pt, gridPointRelTol);
+    }
+
+    // Min x,y,z index
+    const labelMinMax invertedLimits(maxPointId, -maxPointId);
+    Vector<labelMinMax> faceLimits;
+
+    const Vector<direction> faceBits
+    (
+        boundBox::XDIR,
+        boundBox::YDIR,
+        boundBox::ZDIR
+    );
+
+    faceIndex.resize(mesh.nFaces());
+    faceOrient.resize(mesh.nFaces());
+
+    for (label facei=0; facei < mesh.nFaces(); ++facei)
+    {
+        faceLimits.x() = faceLimits.y() = faceLimits.z() = invertedLimits;
+
+        for (const label pointi : mesh.faces()[facei])
+        {
+            for (direction cmpt=0; cmpt < labelVector::nComponents; ++cmpt)
+            {
+                faceLimits[cmpt].add(pointIndex[pointi][cmpt]);
+            }
+        }
+
+        direction inPlane(0u);
+
+        for (direction cmpt=0; cmpt < labelVector::nComponents; ++cmpt)
+        {
+            const auto& limits = faceLimits[cmpt];
+
+            if (!limits.valid())
+            {
+                // This should be impossible
+                FatalErrorInFunction
+                    << "Unexpected search failure for " << facei << " in "
+                    << vector::componentNames[cmpt] << "-direction" << nl
+                    << exit(FatalError);
+            }
+
+            if (limits.min() < 0)
+            {
+                FatalErrorInFunction
+                    << "Face " << facei << " contains non-grid point in "
+                    << vector::componentNames[cmpt] << "-direction" << nl
+                    << exit(FatalError);
+            }
+            else if (limits.min() == limits.max())
+            {
+                // In plane
+                inPlane |= faceBits[cmpt];
+            }
+            else if (limits.min() + 1 != limits.max())
+            {
+                FatalErrorInFunction
+                    << "Face " << facei
+                    << " not in " << vector::componentNames[cmpt] << "-plane" << nl
+                    << exit(FatalError);
+            }
+        }
+
+        switch (inPlane)
+        {
+            case boundBox::XDIR:
+                faceOrient[facei] = vector::X;
+                break;
+
+            case boundBox::YDIR:
+                faceOrient[facei] = vector::Y;
+                break;
+
+            case boundBox::ZDIR:
+                faceOrient[facei] = vector::Z;
+                break;
+
+            default:
+                FatalErrorInFunction
+                    << "Face " << facei << " not in an x/y/z plane?" << nl
+                    << exit(FatalError);
+                break;
+        }
+
+        faceIndex[facei] =
+            labelVector
+            (
+                faceLimits.x().min(),
+                faceLimits.y().min(),
+                faceLimits.z().min()
+            );
+    }
+
+
+    // Bin cells into i-j-k locations
+    cellIndex = std::move(pointIndex);
+    cellIndex = labelVector::uniform(maxPointId);
+    cellIndex.resize(mesh.nCells(), labelVector::uniform(maxPointId));
+
+    // Option 1: use PDRblock.findCell() method
+    if (true)
+    {
+        const pointField& cc = mesh.cellCentres();
+
+        for (label celli=0; celli < mesh.nCells(); ++celli)
+        {
+            cellIndex[celli] = pdrBlock.findCell(cc[celli]);
+        }
+    }
+
+    // Option 2: walk cell faces and use faceIndex information
+    if (false)
+    {
+        for (label celli=0; celli < mesh.nCells(); ++celli)
+        {
+            labelVector& cellIdx = cellIndex[celli];
+
+            for (const label facei : mesh.cells()[celli])
+            {
+                cellIdx.x() = min(cellIdx.x(), faceIndex[facei].x());
+                cellIdx.y() = min(cellIdx.y(), faceIndex[facei].y());
+                cellIdx.z() = min(cellIdx.z(), faceIndex[facei].z());
+            }
+
+            if (cmptMin(cellIdx) < 0)
+            {
+                cellIdx = labelVector(-1,-1,-1);
+            }
+        }
+    }
+
+
+    // Verify that all i-j-k cells were found
+    {
+        // This could be more efficient - but we want to be picky
+        IjkField<bool> cellFound(pdrBlock.sizes(), false);
+
+        for (label celli=0; celli < cellIndex.size(); ++celli)
+        {
+            const labelVector& cellIdx = cellIndex[celli];
+
+            if (cmptMin(cellIdx) >= 0)
+            {
+                cellFound(cellIdx) = true;
+            }
+        }
+
+        label firstMissing = cellFound.find(false);
+
+        if (firstMissing >= 0)
+        {
+            FatalErrorInFunction
+                << "No cell found for " << pdrBlock.index(firstMissing)
+                << " indexing"
+                << exit(FatalError);
+        }
+    }
+}
+
+
+void Foam::PDRmeshArrays::read
+(
+    const Time& runTime,
+    const PDRblock& pdrBlock
+)
+{
+    #include "createPolyMesh.H"
+    classify(mesh, pdrBlock);
+}
+
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/PDRmeshArrays.H b/applications/utilities/preProcessing/PDRsetFields/PDRmeshArrays.H
new file mode 100644
index 00000000000..26142e54fb8
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/PDRmeshArrays.H
@@ -0,0 +1,131 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2019 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::PDRmeshArrays
+
+Description
+    OpenFOAM/PDRblock addressing information
+
+    Provides mapping for a rectilinear OpenFOAM mesh in terms of
+    i-j-k indices for faces and cells.
+
+    The mesh points are first binned according to their i-j-k locations.
+    Next the faces are classified according to their lowest x/y/z
+    coordinates and the face orientation as x/y/z.
+    Orientation in the sense +x or -x is not noted.
+    The cell faces are then examined to determine the appropriate i-j-k
+    location.
+
+SourceFiles
+    PDRmeshmeshArraysIO.C
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef PDRmeshArrays_H
+#define PDRmeshArrays_H
+
+#include "labelVector.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+// Forward Declarations
+class PDRblock;
+class polyMesh;
+class Time;
+
+/*---------------------------------------------------------------------------*\
+                          Class PDRmeshArrays Declaration
+\*---------------------------------------------------------------------------*/
+
+class PDRmeshArrays
+{
+public:
+
+    //- Relative tolerance when matching grid points. Default = 0.02
+    static scalar gridPointRelTol;
+
+    //- The cell i-j-k addressing range
+    labelVector cellDims;
+
+    //- The face i-j-k addressing range
+    labelVector faceDims;
+
+    //- For each cell, the corresponding i-j-k address.
+    List<labelVector> cellIndex;
+
+    //- For each face, the corresponding i-j-k address.
+    List<labelVector> faceIndex;
+
+    //- For each face, the x/y/z orientation
+    List<direction> faceOrient;
+
+
+    // Constructors
+
+        //- Construct null
+        PDRmeshArrays() = default;
+
+
+    //- Destructor
+    ~PDRmeshArrays() = default;
+
+
+    // Member Functions
+
+        //- The number of cells
+        label nCells() const
+        {
+            return cellIndex.size();
+        }
+
+        //- The number of faces
+        label nFaces() const
+        {
+            return faceIndex.size();
+        }
+
+
+        //- Determine i-j-k indices for faces/cells
+        void classify(const polyMesh& mesh, const PDRblock& pdrBlock);
+
+        //- Read OpenFOAM mesh and determine i-j-k indices for faces/cells
+        void read(const Time& runTime, const PDRblock& pdrBlock);
+
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/PDRparams.C b/applications/utilities/preProcessing/PDRsetFields/PDRparams.C
new file mode 100644
index 00000000000..9554f8618fb
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/PDRparams.C
@@ -0,0 +1,111 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2019 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 "PDRparams.H"
+#include "stringOps.H"
+
+// * * * * * * * * * * * * * * * * Global Data * * * * * * * * * * * * * * * //
+
+// Global parameter settings
+Foam::PDRparams Foam::pars;
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+void Foam::PDRparams::readDefaults(const dictionary& dict)
+{
+    dict.readIfPresent("legacyMeshSpec", legacyMeshSpec);
+    dict.readIfPresent("legacyObsSpec",  legacyObsSpec);
+
+    dict.readIfPresent("two_d", two_d);
+    dict.readIfPresent("yCyclic", yCyclic);
+    dict.readIfPresent("ySymmetry", ySymmetry);
+    dict.readIfPresent("deluge", deluge);
+
+    dict.readIfPresent("newFields", new_fields);
+    dict.readIfPresent("noIntersectN", noIntersectN);
+
+    dict.readIfPresent("blockedFacesWallFn", blockedFacesWallFn);
+    dict.readIfPresent("ignoreGratings", ignoreGratings);
+
+    outer_orthog = dict.found("outer_orthog");
+
+    dict.readIfPresent("debugLevel", debugLevel);
+    dict.readIfPresent("nFacesToBlockC", nFacesToBlockC);
+    dict.readIfPresent("nPairsToBlockC", nPairsToBlockC);
+    dict.readIfPresent("overlaps", overlaps);
+
+    dict.readIfPresent("gridPointTol", gridPointTol);
+
+    dict.readIfPresent("Cb_r", cb_r);
+    dict.readIfPresent("Cb_s", cb_s);
+
+    dict.readIfPresent("Cd_r", cd_r);
+    dict.readIfPresent("Cd_s", cd_s);
+
+    dict.readIfPresent("congRegionMaxBetav", cong_max_betav);
+
+    dict.readIfPresent("min_overlap_vol", min_overlap_vol);
+    dict.readIfPresent("min_overlap_area", min_overlap_area);
+    dict.readIfPresent("min_width", min_width);
+    dict.readIfPresent("empty_lobs_fac", empty_lobs_fac);
+    dict.readIfPresent("outerCombFac", outerCombFac);
+    dict.readIfPresent("obs_expand", obs_expand);
+
+    dict.readIfPresent("def_grating_slat_w", def_grating_slat_w);
+    dict.readIfPresent("blockedCellPoros", blockedCellPoros);
+    dict.readIfPresent("blockedFacePar", blockedFacePar);
+    dict.readIfPresent("maxCR", maxCR);
+
+    dict.readIfPresent("blockageNoCT", blockageNoCT);
+    dict.readIfPresent("scale", scale);
+
+    UPatchBc = "fixedValue;value uniform (0 0 0)";
+    if (dict.readIfPresent("UPatchBc", UPatchBc))
+    {
+        stringOps::inplaceTrim(UPatchBc);
+    }
+}
+
+
+void Foam::PDRparams::read(const dictionary& dict)
+{
+    readDefaults(dict);
+
+    dict.readEntry("obsFileDir", obsfile_dir);
+    dict.readEntry("obsFileNames", obsfile_names);
+
+    stringOps::inplaceExpand(obsfile_dir);
+
+    for (auto& f : obsfile_names)
+    {
+        stringOps::inplaceExpand(f);
+    }
+}
+
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/PDRparams.H b/applications/utilities/preProcessing/PDRsetFields/PDRparams.H
new file mode 100644
index 00000000000..0d383021f69
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/PDRparams.H
@@ -0,0 +1,165 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2016 Shell Research Ltd.
+    Copyright (C) 2018-2019 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::PDRparams
+
+Description
+    Parameters for PDRsetFields
+
+SourceFiles
+    PDRparams.C
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef PDRparams_H
+#define PDRparams_H
+
+#include "labelList.H"
+#include "scalarList.H"
+#include "wordList.H"
+#include "fileNameList.H"
+#include "dictionary.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+/*---------------------------------------------------------------------------*\
+                          Class PDRparams Declaration
+\*---------------------------------------------------------------------------*/
+
+class PDRparams
+{
+public:
+
+    // Data Members
+
+        fileName obsfile_dir;
+        wordList obsfile_names;
+        word    timeName;
+        string  UPatchBc;   //!< "fixedValue;value uniform (0 0 0)"
+
+        bool legacyMeshSpec{false};
+        bool legacyObsSpec{false};
+
+        bool two_d{false};
+        bool yCyclic{false};
+        bool ySymmetry{false};
+        bool deluge{false};
+        bool new_fields{true};
+        bool noIntersectN{true};
+        bool blockedFacesWallFn{false};
+        bool ignoreGratings{false};
+        bool outer_orthog{false};
+
+        int debugLevel{0};
+
+        //- Min number of blocked cell faces
+        //- for a cell to be marked as blocked
+        int nFacesToBlockC{6};
+
+        //- Min number of blocked cell face pairs (on opposite faces of a cell)
+        //- for a cell to be marked as blocked
+        int nPairsToBlockC{3};
+
+        //- Flag to control which overlap calculations are performed
+        int overlaps{0x7};
+
+        scalar gridPointTol{0.02};
+
+        scalar cb_r{0.035};
+        scalar cb_s{0.08};
+
+        scalar cd_r{1.2};
+        scalar cd_s{2.0};
+
+        scalar cong_max_betav{1.0};
+
+        scalar min_overlap_vol{0};
+        scalar min_overlap_area{0};
+
+        //- Ignore obstacles with second dimension (or diameter) less than this
+        scalar min_width{0.001};
+
+        //- Lobs in empty cell is this * cube root of cell volume
+        scalar empty_lobs_fac{1.0};
+
+        //- Value for outer region
+        scalar outerCombFac{1.0};
+
+        scalar obs_expand{0};
+
+        //- Default slat thickness grating
+        scalar def_grating_slat_w{0.005};
+
+        //- Cells with porosity less than this are blocked
+        scalar blockedCellPoros{0.05};
+
+        //- Faces with area blockage greater than this are blocked
+        scalar blockedFacePar{0.95};
+
+        //- Upper limit on CR (CT also gets limited)
+        scalar maxCR{1e30};
+
+        //- If a single obstacle blocks a cell by more than this,
+        //- then no CT in that direction
+        scalar blockageNoCT{0.95};
+
+        //- Overall scale factor
+        scalar scale{1.0};
+
+
+    // Constructors
+
+        //- Construct null
+        PDRparams() = default;
+
+
+    // Member Functions
+
+        //- Set or read defaults from dictionary.
+        //  Can also be used with an empty dictionary
+        void readDefaults(const dictionary& dict);
+
+        //- Read program parameters from dictionary
+        void read(const dictionary& dict);
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+extern Foam::PDRparams pars;  //!< Globals for program parameters (ugly hack)
+
+} // End namespace Foam
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/PDRpatchDef.C b/applications/utilities/preProcessing/PDRsetFields/PDRpatchDef.C
new file mode 100644
index 00000000000..2fcf12d1ac1
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/PDRpatchDef.C
@@ -0,0 +1,52 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2019 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 "PDRpatchDef.H"
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+const Foam::Enum
+<
+    Foam::PDRpatchDef::predefined
+>
+Foam::PDRpatchDef::names
+({
+    { predefined::BLOCKED_FACE, "blockedFaces" },
+    { predefined::MERGING_PATCH, "mergingFaces" },
+    { predefined::WALL_PATCH, "wallFaces" },
+});
+
+
+// * * * * * * * * * * * * * * * Member Operators  * * * * * * * * * * * * * //
+
+void Foam::PDRpatchDef::operator=(const std::string& newName)
+{
+    patchName = word::validate(newName);
+}
+
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/PDRpatchDef.H b/applications/utilities/preProcessing/PDRsetFields/PDRpatchDef.H
new file mode 100644
index 00000000000..076786cd186
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/PDRpatchDef.H
@@ -0,0 +1,123 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2016 Shell Research Ltd.
+    Copyright (C) 2019 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::PDRpatchDef
+
+Description
+    Bookkeeping for patch definitions
+
+SourceFiles
+    PDRpatchDef.H
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef PDRpatchDef_H
+#define PDRpatchDef_H
+
+#include "string.H"
+#include "scalar.H"
+#include "Enum.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+/*---------------------------------------------------------------------------*\
+                         Class PDRpatchDef Declaration
+\*---------------------------------------------------------------------------*/
+
+class PDRpatchDef
+{
+public:
+
+    //- Patch predefines
+    enum predefined
+    {
+        BLOCKED_FACE = 0,
+        MERGING_PATCH = 1,
+        WALL_PATCH = 2,
+        LAST_PREDEFINED = 2, // First user patch number will be 1 more
+        NUM_PREDEFINED = 3
+    };
+
+    //- Names for predefined types
+    static const Enum<predefined> names;
+
+
+    // Data Members
+
+    word patchName;
+
+    label patchType;
+
+    scalar blowoffPress;
+
+    scalar blowoffTime;
+
+
+    // Constructors
+
+        //- Construct null
+        PDRpatchDef()
+        :
+             patchName(),
+             patchType(0),
+             blowoffPress(0),
+             blowoffTime(0)
+        {}
+
+        //- Construct with given patch name
+        explicit PDRpatchDef(const word& name)
+        :
+             patchName(name),
+             patchType(0),
+             blowoffPress(0),
+             blowoffTime(0)
+        {}
+
+
+        //- Construct with given patch name
+        PDRpatchDef& operator=(const PDRpatchDef&) = default;
+        PDRpatchDef& operator=(PDRpatchDef&&) = default;
+
+        //- Assign new patch name
+        void operator=(const std::string& newName);
+};
+
+
+typedef PDRpatchDef PATCH;
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/PDRsetFields.C b/applications/utilities/preProcessing/PDRsetFields/PDRsetFields.C
new file mode 100644
index 00000000000..ca1c724aad3
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/PDRsetFields.C
@@ -0,0 +1,351 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2016 Shell Research Ltd.
+    Copyright (C) 2019 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/>.
+
+Applications
+    PDRsetFields
+
+Description
+    Preparation of fields for PDRFoam
+
+SourceFiles
+    PDRsetFields.C
+
+\*---------------------------------------------------------------------------*/
+
+#include "argList.H"
+#include "Time.H"
+#include "IOdictionary.H"
+
+#include "PDRsetFields.H"
+#include "PDRlegacy.H"
+#include "PDRutils.H"
+#include "IOmanip.H"
+
+using namespace Foam;
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+//  Main program:
+
+int main(int argc, char* argv[])
+{
+    argList::addNote
+    (
+        "Processes a set of geometrical obstructions to determine the"
+        " equivalent blockage effects when setting cases for PDRFoam"
+    );
+    argList::noParallel();
+    argList::noFunctionObjects();
+
+    argList::addOption
+    (
+        "time",
+        "time",
+        "Specify a time"
+    );
+
+    argList::addOption("dict", "file", "Use alternative PDRsetFieldsDict");
+
+    argList::addBoolOption
+    (
+        "legacy",
+        "Force use of legacy obstacles table"
+    );
+
+    argList::addBoolOption
+    (
+        "dry-run",
+        "Read obstacles and write VTK only"
+    );
+
+    #include "setRootCase.H"
+    #include "createTime.H"
+
+    const word dictName("PDRsetFieldsDict");
+    #include "setSystemRunTimeDictionaryIO.H"
+
+    Info<< "Reading " << dictName << "\n" << endl;
+
+    IOdictionary setFieldsDict(dictIO);
+
+    const bool dryrun = args.found("dry-run");
+
+    const fileName& casepath = runTime.globalPath();
+
+    pars.timeName = "0";
+    args.readIfPresent("time", pars.timeName);
+
+    // Program parameters (globals)
+    pars.read(setFieldsDict);
+
+    if (args.found("legacy"))
+    {
+        pars.legacyObsSpec = true;
+    }
+
+    // Always have the following:
+    // 0 = blockedFaces patch (no wall functions)
+    // 1 = mergingFaces patch
+    // 2 = wallFaces patch
+
+    DynamicList<PDRpatchDef> patches;
+    patches.resize(PDRpatchDef::NUM_PREDEFINED);
+
+    for
+    (
+        PDRpatchDef::predefined predef :
+        {
+            PDRpatchDef::BLOCKED_FACE,
+            PDRpatchDef::MERGING_PATCH,
+            PDRpatchDef::WALL_PATCH,
+        }
+    )
+    {
+        patches[predef] = PDRpatchDef::names[predef];
+    }
+
+
+    // Dimensions and grid points for i-j-k domain
+    PDRblock pdrBlock;
+
+    if (pars.legacyMeshSpec)
+    {
+        PDRlegacy::read_mesh_spec(casepath, pdrBlock);
+    }
+    else
+    {
+        IOdictionary iodict
+        (
+            IOobject
+            (
+                "PDRblockMeshDict",
+                runTime.system(),
+                runTime,
+                IOobject::MUST_READ_IF_MODIFIED,
+                IOobject::NO_WRITE
+            )
+        );
+
+        pdrBlock.read(iodict);
+
+        #ifdef FULLDEBUG
+        PDRlegacy::print_info(pdrBlock);
+        #endif
+    }
+
+    // Storage for obstacles and cylinder-like obstacles
+    DynamicList<PDRobstacle> obstacles, cylinders;
+
+    // Read in obstacles
+    const scalar volObstacles =
+    (
+        pars.legacyObsSpec
+      ? PDRobstacle::legacyReadFiles
+        (
+            pars.obsfile_dir, pars.obsfile_names,
+            pdrBlock.bounds(),
+            obstacles,
+            cylinders
+        )
+      : PDRobstacle::readFiles
+        (
+            pars.obsfile_dir, pars.obsfile_names,
+            pdrBlock.bounds(),
+            obstacles,
+            cylinders
+        )
+    );
+
+
+    PDRobstacle::generateVtk(casepath/"VTK", obstacles, cylinders);
+
+    if (dryrun)
+    {
+        Info<< nl
+            << "dry-run: stopping after reading/writing obstacles" << nl
+            << "\nEnd\n" << nl;
+        return 0;
+    }
+
+
+    // Bookkeeping of the ranges within the obstacle list
+
+    // Original blockage at the start
+    const labelRange origBlocks(0, obstacles.size());
+
+    // Intersection blockage
+    labelRange interBlocks(origBlocks.after(), 0);
+
+    scalar volSubtract = 0;
+
+    // Do binary intersections between blocks and cylinders (or diag-beam)
+    // by creating -ve blocks at the overlap
+
+    labelRange int1Blocks(origBlocks.after(), 0);
+
+    if (pars.overlaps % 2 > 0)
+    {
+        Info<< "    block/cylinder intersections" << endl;
+
+        label nblocked = obstacles.size();
+
+        volSubtract += block_cylinder_overlap(obstacles, origBlocks, cylinders);
+
+        nblocked = (obstacles.size() - nblocked);
+
+        interBlocks += nblocked;
+        int1Blocks += nblocked;
+    }
+
+    // Do binary intersections between blocks
+    // by creating -ve blocks at the overlap
+
+    labelRange int2Blocks(int1Blocks.after(), 0);
+    if (pars.overlaps % 4 > 1)
+    {
+        Info<< "    block/block intersections" << endl;
+
+        label nblocked = obstacles.size();
+
+        volSubtract += block_overlap(obstacles, origBlocks, 1.0);
+
+        nblocked = (obstacles.size() - nblocked);
+
+        interBlocks += nblocked;
+        int2Blocks += nblocked;
+    }
+
+    // Correct for triple intersections
+    // by looking for overlaps between the -ve blocks just created
+
+    labelRange int3Blocks(int2Blocks.after(), 0);
+    if (pars.overlaps % 8 > 3)
+    {
+        Info<< "    triple intersections" << endl;
+
+        label nblocked = obstacles.size();
+
+        volSubtract += block_overlap(obstacles, interBlocks, 1.0/3.0);
+
+        nblocked = (obstacles.size() - nblocked);
+
+        interBlocks += nblocked;
+        int3Blocks += nblocked;
+    }
+
+
+    // The field arrays, in one structure pass around easily
+    PDRarrays arr(pdrBlock);
+
+    Info<< "Apply blockage" << endl;
+
+    // Create blockage and count arrays by working through
+    // real and extra blocks and cylinders
+
+    // User-defined negative blocks. Use "sign" to distinguish
+    if (origBlocks.size())
+    {
+        Info<< "    negative blocks: " << origBlocks.size() << nl;
+
+        for (const PDRobstacle& obs : obstacles[origBlocks])
+        {
+            arr.addBlockage(obs, patches, -1);
+        }
+    }
+
+    // Do the intersection blocks positive and negative
+    // These are done first so that negative area blockage cancels positive
+
+    if (interBlocks.size())
+    {
+        Info<< "    blocks " << interBlocks.size() << nl;
+
+        for (const PDRobstacle& obs : obstacles[interBlocks])
+        {
+            arr.addBlockage(obs, patches, 0);
+        }
+    }
+
+    // The positive real bocks
+    if (origBlocks.size())
+    {
+        Info<< "    positive blocks: " << origBlocks.size() << nl;
+
+        for (const PDRobstacle& obs : obstacles[origBlocks])
+        {
+            arr.addBlockage(obs, patches, 1);
+        }
+    }
+
+    // The cylinders
+    if (cylinders.size())
+    {
+        Info<< "    cylinders: " << cylinders.size() << nl;
+
+        for (const PDRobstacle& obs : cylinders)
+        {
+            arr.addCylinder(obs);
+        }
+    }
+
+    // Calculation of the fields of drag, turbulence
+    // generation and combustion enhancement
+
+    arr.blockageSummary();
+
+    // Mapping of OpenFOAM cells/faces to i-j-k indices
+    PDRmeshArrays meshIdx;
+    meshIdx.gridPointRelTol = pars.gridPointTol;
+
+    meshIdx.read(runTime, pdrBlock);
+
+    PDRarrays::calculateAndWrite(arr, meshIdx, casepath, patches);
+
+    Info<< nl
+        << setw(6) << origBlocks.size() << " blocks and "
+        << cylinders.size() << " cylinders/diagonal blocks" << nl;
+
+    Info<< setw(6) << int2Blocks.size()
+        << " intersections amongst blocks" << nl;
+
+    Info<< setw(6) << int1Blocks.size()
+        << " intersections between blocks and cyl/beams" << nl;
+
+    Info<< setw(6) << int1Blocks.size()
+        << "/3 triple intersections" << nl;
+
+    Info<< "Volume of obstacles read in: " << volObstacles
+        << ", volume of intersections: " << volSubtract << nl;
+
+    Info<< nl << "After corrections:" << nl;
+    arr.blockageSummary();
+
+    Info<< nl << "\nEnd\n" << endl;
+
+    return 0;
+}
+
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/PDRsetFields.H b/applications/utilities/preProcessing/PDRsetFields/PDRsetFields.H
new file mode 100644
index 00000000000..85bcda7aed8
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/PDRsetFields.H
@@ -0,0 +1,142 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2016 Shell Research Ltd.
+    Copyright (C) 2019 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/>.
+
+Description
+    Preparation of fields for PDRFoam
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef PDRsetFields_H
+#define PDRsetFields_H
+
+#include "PDRarrays.H"
+#include "PDRblock.H"
+#include "PDRmeshArrays.H"
+#include "PDRobstacle.H"
+#include "PDRpatchDef.H"
+#include "PDRparams.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+using namespace Foam;
+
+
+//YCYCLIC is set to 1 for running the test cases with a cyclic boundry condition in the y direction
+//TWO_D is set to 1 for running the 2D test cases (no z direction) - usually same case as YCYCLIC
+//Now specified in CAD_PDRDict and read in as globals.
+
+// The program also labels intersection obstacles as types 96 and 86, but not then use these values
+// Obstacles in group definitions have a multiple of 100 added to the type number
+
+#define floatSMALL  1.e-10
+
+// Default initial values of field variables, used outside congested area,
+//and everywhere for uniform fields. They atre strings because same routines
+//are used to create b.c.s for scalars and tensors.
+#define DEFAULT_K       0.00015
+#define DEFAULT_EPS     1e-5
+#define DEFAULT_T       300
+#define DEFAULT_P       100000
+#define DEFAULT_SU      0.5
+#define DEFAULT_LOBS    0.1   // Does not matter what it is outside congestion
+                              // but zero would cause problems with Ep
+#define DEFAULT_EP      0.01  // Gives length scale 0.1, calc. as (Xp-0.999)/Ep with Xp=1
+
+// Boundary conditions on walls for all variables where it is not "zero_gradient"
+#define K_WALL_FN       "kqRWallFunction"
+#define EPS_WALL_FN     "epsilonWallFunction"
+#define ALPHAT_WALL     "nutkWallFunction"
+#define MUT_WALL_FN     "mutkWallFunction"
+#define NUT_WALL_FN     "nutkWallFunction"
+
+#define K_WALL_FN_LEGACY        "compressible::kqRWallFunction"
+#define EPS_WALL_FN_LEGACY      "compressible::epsilonWallFunction"
+#define ALPHAT_WALL_FN_LEGACY   "alphatWallFunction;\n\t\tPrt\t0.85"
+
+
+// The following parameters are used to decide when there arMAX_Ne sufficient (parts of)
+// obstacles ina cell for them to define the length scale of the generated turbulence.
+#define MIN_AB_FOR_SIZE 0.002
+#define MAX_VB_FOR_SIZE 0.9
+#define COUNT_FOR_SIZE  0.1
+#define MIN_COUNT_FOR_SIZE 0.05
+
+// These define how blocked a face or cell has to be for removal from the mesh
+//#define BLOCKED_CELL_PAR  0.05  //<-  Now pars.blockedCellPoros
+//#define BLOCKED_FACE_PAR  0.95  //<-  Now pars.blockedFacePar
+
+
+//- Calculate block/block overlaps
+//
+// Binary self-intersections are to be checked for blocks.
+// Resulting negative blocks are appended to blocks.
+// These new blocks have the opposite sign from input blocks, and
+// blockage multiplied by multiplier.
+//
+// If the number of newly generated blocks is required, check the size
+// of blocks on output vs input to see how many have been added.
+//
+// \param[in,out] blocks
+// \param[in] range - the range within blocks to be examined
+//
+// \return overlap volume
+scalar block_overlap
+(
+    DynamicList<PDRobstacle>& blocks,
+    const labelRange& range,
+    const scalar multiplier = 1.0
+);
+
+
+//- Calculate block/cylinder overlaps
+//
+// Binary intersections are to be checked for blocks and cylinders.
+// Resulting negative blocks are appended to blocks.
+// These new blocks have the opposite sign from input blocks, and
+// blockage multiplied by multiplier.
+//
+// If the number of newly generated blocks is required, check the size
+// of blocks on output vs input to see how many have been added.
+//
+// \param[in,out] arrp
+// \param[in,out] blocks
+// \param[in] range - the range within blocks to be examined
+// \param[in] cylinders - the cylinders to be examined
+//
+//  \return overlap volume
+scalar block_cylinder_overlap
+(
+    DynamicList<PDRobstacle>& blocks,
+    const labelRange& range,
+    const UList<PDRobstacle>& cylinders
+);
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/PDRutils.H b/applications/utilities/preProcessing/PDRsetFields/PDRutils.H
new file mode 100644
index 00000000000..9d28f5b2c6d
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/PDRutils.H
@@ -0,0 +1,62 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2019 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/>.
+
+Namespace
+    Foam::PDRutils
+
+Description
+    Utilities for PDR (eg, for setFields)
+
+SourceFiles
+    PDRUtils.C
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef PDRutils_H
+#define PDRutils_H
+
+#include "PDRarrays.H"
+#include "PDRblock.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+// Forward Declarations
+class PDRobstacle;
+
+namespace PDRutils
+{
+
+
+} // End namespace PDRutils
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/PDRutilsInternal.H b/applications/utilities/preProcessing/PDRsetFields/PDRutilsInternal.H
new file mode 100644
index 00000000000..f6e11a97899
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/PDRutilsInternal.H
@@ -0,0 +1,221 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2019 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/>.
+
+Namespace
+    Foam::PDRutils
+
+Description
+    Utilities for PDR (eg, for setFields). Internal usage only.
+
+    The C lineage of the original code is still evident in the use of
+    pointers instead of references.
+    This will be addressed in later versions of the code (2019-12).
+
+SourceFiles
+    PDRUtils.C
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef PDRutilsInternal_H
+#define PDRutilsInternal_H
+
+#include "PDRutils.H"
+#include "PDRarrays.H"
+#include "PDRblock.H"
+#include "symmTensor2D.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace PDRutils
+{
+
+//- Determine 1-D overlap locations for a geometric entity
+//
+// \param[in] xmin - min position of the geometric entity
+// \param[in] xmax - max position of the geometric entity
+// \param[in] grid - grid point information
+// \param[out] olap - Fraction of cell-width with overlap
+//     0 for no overlap, 1 for full overlap.
+// \param[out] cmin - first cell index (inclusive) with overlap,
+//     values in the range \c [0,nCells]
+// \param[out] cmax - last cell index (inclusive) with overlap,
+//     values in the range \c [0,nCells]
+// \param[out] cfmin - first cell index (inclusive) with staggered face,
+//     values in the range \c [0,nCells]
+// \param[out] cfmax - last cell index (inclusive) with staggered face,
+//     values in the range \c [0,nCells]
+void one_d_overlap
+(
+    scalar xmin,
+    scalar xmax,
+    const PDRblock::location& grid,
+    List<scalar>& olap,
+    int *cmin, int *cmax,
+    int *cfmin, int *cfmax
+);
+
+
+//- Combine two 1D overlaps.
+//  Multiplying the two 1-d overlaps yields the proportion of each (2D) cell
+//  that is covered.
+//
+//  \note We go one over the relevant min/max limits since these
+//  values might be used.
+//  The 1D arrays will have bee initially zeroed throughout.
+void two_d_overlap
+(
+    const UList<scalar>& a_olap, label amin, label amax,
+    const UList<scalar>& b_olap, label bmin, label bmax,
+    SquareMatrix<scalar>& ab_olap
+);
+
+
+//- Calculate the proportion of each (two-dimensional) grid cell
+//- overlapped by the circle or angled rectangle.
+//
+//  Coordinates are labelled a and b.
+//
+// \param[in] ac, bc  coordinates of centre of circle or rectangle
+// \param[in] dia      diameter of circle (zero for rectangle)
+// \param[in] theta, wa, wb     parameters for rectangle
+// \param[in] amin, amax first and last cells in a-grid overlapped by object
+// \param[in] agrid               locations of grid lines of a-grid
+// \param[in] amin, amax first and last cells in b-grid overlapped by object
+// \param[in] bgrid               locations of grid lines of b-grid
+//
+// \param[out] abolap
+//     2-D array of (proportionate) area blockage by grid cell
+// \param[out] a_lblock
+//     2-D array of (proportionate) blockage to a-direction flow
+//     (This will be area blockage when extruded in the third
+//     coordinate).
+//
+// \param[out] a_count
+//   2-D array The contribution of this object to the count of
+// obstacles blocking a-direction flow. This is only non-zero if the
+// object is inside the lateral boundaries of the cell. It is large
+// negative if the cell is totally blocked in this direction.
+//
+//
+// \param[out] c_drag
+//
+// 2-D array of tensor that will give tensor drag in each cell (when
+// multiplied Cd, cylinder length, and 0.5 rho*U^2) Dimension: L.
+//
+// \note this routine does not zero array elements outside the amin
+//  to amax, bmin to bmax area.
+void circle_overlap
+(
+    scalar ac, scalar bc, scalar dia,
+    scalar theta, scalar wa, scalar wb,
+    const PDRblock::location& agrid, label amin, label amax,
+    const PDRblock::location& bgrid, label bmin, label bmax,
+    SquareMatrix<scalar>& ab_olap,
+    SquareMatrix<scalar>& ab_perim,
+    SquareMatrix<scalar>& a_lblock,
+    SquareMatrix<scalar>& ac_lblock,
+    SquareMatrix<scalar>& c_count,
+    SquareMatrix<symmTensor2D>& c_drag,
+    SquareMatrix<scalar>& b_lblock,
+    SquareMatrix<scalar>& bc_lblock
+);
+
+
+//- Area of intersection between circle and rectangle.
+//
+// Calculates the area of intersection between the circle, centre (xc, yc), radius rad,
+// and the rectangle with sides at x = x1 & x2, and y = y1 and y2.
+//
+// The return value is the fraction of the rectangle's area covered by the circle.
+double inters_cy
+(
+    double xc,   //!< circle centre (x)
+    double yc,   //!< circle centre (y)
+    double rad,  //!< circle radius
+    double x1, double x2,
+    double y1, double y2,
+    scalar* perim_p,
+    scalar* x_proj_edge_p,
+    scalar* y_proj_edge_p,
+    scalar* x_overlap_p,
+    scalar* y_overlap_p
+);
+
+
+//- The area overlap in the plane of a diagonal block and a cell.
+//
+// Calculates the overlap, in the plane of a diagonal block and a cell,
+// plus blockage and drag parameters.
+// Note that x and y herein may be any two of the three coordinates - would have been better not to label them x and y.
+//
+// On entry:
+//   xc, yc                       Coordinates of axis of d.b.
+//   theta, wa, wb        Angle and widths
+//
+//   The returned parameters will be multipled by the length of the obstacle's intersection with
+//   the third dimension of the 3-D cell to give this obstacle's contribution to the count, drag
+//   and area blockages.
+//   The return value is the area of intersection, which will multiply to volume blockage.
+//
+double inters_db
+(
+    double xc, double yc, double theta,
+    double wa, double wb,
+    double x1, double x2,
+    double y1, double y2,
+    scalar* count_p,
+    symmTensor2D& vdrag, scalar* perim_p,
+    scalar* x_lblk, scalar* y_lblk,
+    scalar* x_centre_p, scalar* y_centre_p
+);
+
+
+/* Calculates the blockage to x-direction flow presented by the specified circle on
+ the specified rectangle.
+ Becomes the area blockage when extruded to in the third dimension.
+ In other words, it is the projection on the y axis of the intersection between the
+ circle and the rectangle.
+ Returns fraction blocked
+ Note that x and y in this routine may in fact be any two of the three dimensions.
+ */
+double l_blockage
+(
+    double xc, double yc, double rad,
+    double x1, double x2,
+    double y1, double y2,
+    scalar* count_p, scalar* drag_p, scalar* centre_p
+);
+
+
+} // End namespace PDRutils
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/PDRutilsIntersect.C b/applications/utilities/preProcessing/PDRsetFields/PDRutilsIntersect.C
new file mode 100644
index 00000000000..6cadff43e5e
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/PDRutilsIntersect.C
@@ -0,0 +1,712 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2016 Shell Research Ltd.
+    Copyright (C) 2019 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 "PDRsetFields.H"
+#include "PDRutilsInternal.H"
+#include "mathematicalConstants.H"
+
+#ifndef FULLDEBUG
+#define NDEBUG
+#endif
+#include <cassert>
+
+using namespace Foam::constant;
+
+// * * * * * * * * * * * * * * * Local Functions * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+// Calculate the area of the sector of a circle whose ends are at
+// (dxa, dya) and (dxb, dyb) relative to the centre. radsqu is radius
+// squared.
+//
+// (We trust that this is consistent with the other parameters..)
+inline static scalar sector
+(
+    scalar dxa, scalar dya,
+    scalar dxb, scalar dyb
+)
+{
+    scalar angle = (::atan2(dyb, dxb) - ::atan2(dya, dxa));
+
+    if (angle < -1.0E-10)
+    {
+        angle += mathematical::twoPi;
+    }
+
+    return angle;
+}
+
+} // End namespace Foam
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+double Foam::PDRutils::inters_cy
+(
+    double xc, double yc, double rad,
+    double x1, double x2,
+    double y1, double y2,
+    scalar* perim_p,
+    scalar* x_proj_edge_p, scalar* y_proj_edge_p,
+    scalar* x_overlap_p, scalar* y_overlap_p
+)
+{
+    double angle, area, del;
+    double x_int[6][2], y_int[6][2]; // Coordinates of intersections between the circle and sides of the rectangle.
+    double x_arc[6][2], y_arc[6][2]; // Coordinates of end orc (within cell) for each quadrant
+    double dx[6], dy[6];
+
+    double x_olap_min = GREAT;
+    double x_olap_max = -GREAT;
+    double y_olap_min = GREAT;
+    double y_olap_max = -GREAT;
+    int n_vert, n_oppv;
+    int no_intersection;
+
+    const double dx1 = (x1 - xc);
+    const double dx2 = (x2 - xc);
+    const double dy1 = (y1 - yc);
+    const double dy2 = (y2 - yc);
+
+    const double dx1squ = dx1 * dx1;
+    const double dx2squ = dx2 * dx2;
+    const double dy1squ = dy1 * dy1;
+    const double dy2squ = dy2 * dy2;
+    const double radsqu = rad * rad;
+
+
+    /* Going clockwise from (x1, y1), vertices are labelled 1,2,3,4, with 0 the same as 4
+     and 5 the same as 1 (so that we can find the vertices on either side of any of 1 to 4).*/
+
+    dx[1] = dx1; dy[1] = dy1;
+    dx[2] = dx1; dy[2] = dy2;
+    dx[3] = dx2; dy[3] = dy2;
+    dx[4] = dx2; dy[4] = dy1;
+    dx[0] = dx2; dy[0] = dy1;
+    dx[5] = dx1; dy[5] = dy1;
+
+    // The positions of the ends of the arcs, if these points are
+    // inside the cell, they will be changed, if necessary, below.
+
+    x_arc[2][0] = x_arc[1][1] = -rad; y_arc[2][0] = y_arc[1][1] =  0.0;
+    x_arc[3][0] = x_arc[2][1] =  0.0; y_arc[3][0] = y_arc[2][1] =  rad;
+    x_arc[4][0] = x_arc[3][1] =  rad; y_arc[4][0] = y_arc[3][1] =  0.0;
+    x_arc[1][0] = x_arc[4][1] =  0.0; y_arc[1][0] = y_arc[4][1] = -rad;
+
+    // We catch arcs that are entirely inside the rectangle
+    // Note: this is wrong for a circle completely outside, but that
+    // will be dealt with separately
+
+    int arc_in[6] = { /* zero-initialied */ };
+    arc_in[1] = (dx1 < -rad && dy1 < -rad) ? 1 : 0;
+    arc_in[2] = (dx1 < -rad && dy2 >  rad) ? 1 : 0;
+    arc_in[3] = (dx2 >  rad && dy2 >  rad) ? 1 : 0;
+    arc_in[4] = (dx2 >  rad && dy1 < -rad) ? 1 : 0;
+
+    // Work out which vertices are in the circle
+
+    int vert_in[6];
+    vert_in[1] = (dx1squ + dy1squ <= radsqu);
+    vert_in[2] = (dx1squ + dy2squ <= radsqu);
+    vert_in[3] = (dx2squ + dy2squ <= radsqu);
+    vert_in[4] = (dx2squ + dy1squ <= radsqu);
+    vert_in[0] = vert_in[4];
+    vert_in[5] = vert_in[1];
+
+    int n_in = 0;
+    if (vert_in[1]) ++n_in;
+    if (vert_in[2]) ++n_in;
+    if (vert_in[3]) ++n_in;
+    if (vert_in[4]) ++n_in;
+
+
+    /* We now calculate the points of intersection of the circle with, successively,
+     x=x1, y=y2, x=x2. y=y1.
+
+     Where there are two intersections with one side, need to be careful about
+     the order of the two points (i.e. clockwise round the rectangle) so that
+     later on we get the right sector (short or long way round the circumference) */
+
+    int n_int[6] = { /* zero-initialied */ };
+    n_int[1] = 0;
+    if ( dx1squ <= radsqu)
+    {
+        del = std::sqrt( radsqu - dx1squ);
+        if ( ( ( -del ) <= dy2 ) && ( del >= dy1 ) )
+        {
+            x_int[1][0] = x_int[1][1] = dx1;
+            if ( (-del ) > dy1 )
+            {
+                y_int[1][0] = -del;
+                n_int[1]++;
+                // This intersection will be an end of the 3rd- or 4th-quadrant arc
+                if ( dx1 > 0.0 ) { x_arc[4][1] = dx1; y_arc[4][1] = -del; arc_in[4] = 1; }
+                else             { x_arc[1][1] = dx1; y_arc[1][1] = -del; arc_in[1] = 1; }
+            }
+            if ( ( del ) < dy2 )
+            {
+                y_int[1][n_int[1]] =  del;
+                n_int[1]++;
+                if ( dx1 > 0.0 ) { x_arc[3][0] = dx1; y_arc[3][0] = del; arc_in[3] = 1; }
+                else             { x_arc[2][0] = dx1; y_arc[2][0] = del; arc_in[2] = 1; }
+            }
+        }
+    }
+
+    n_int[2] = 0;
+    if ( dy2squ <= radsqu)
+    {
+        del = std::sqrt( radsqu - dy2squ);
+        if ( ( ( -del ) <= dx2 ) && ( del >= dx1 ) )
+        {
+            y_int[2][0] = y_int[2][1] = dy2;
+            if ( (-del ) > dx1 )
+            {
+                x_int[2][0] = -del;
+                n_int[2]++;
+                if ( dy2 > 0.0 ) { x_arc[2][1] = -del; y_arc[2][1] = dy2; arc_in[2] = 1; }
+                else             { x_arc[1][1] = -del; y_arc[1][1] = dy2; arc_in[1] = 1; }
+            }
+            if ( ( del ) < dx2 )
+            {
+                x_int[2][n_int[2]] =  del;
+                n_int[2]++;
+                if ( dy2 > 0.0 ) { x_arc[3][0] = del; y_arc[3][0] = dy2; arc_in[3] = 1; }
+                else             { x_arc[4][0] = del; y_arc[4][0] = dy2; arc_in[4] = 1; }
+            }
+        }
+    }
+
+    n_int[3] = 0;
+    if ( dx2squ <= radsqu)
+    {
+        del = std::sqrt( radsqu - dx2squ);
+        if ( ( ( -del ) <= dy2 ) && ( del >= dy1 ) )
+        {
+            x_int[3][0] = x_int[3][1] = dx2;
+            if ( ( del ) < dy2 )
+            {
+                y_int[3][0] = del;
+                n_int[3]++;
+                if ( dx2 > 0.0 ) { x_arc[3][1] = dx2; y_arc[3][1] = del; arc_in[3] = 1; }
+                else             { x_arc[2][1] = dx2; y_arc[2][1] = del; arc_in[2] = 1; }
+            }
+            if ( (-del ) > dy1 )
+            {
+                y_int[3][n_int[3]] = -del;
+                n_int[3]++;
+                if ( dx2 > 0.0 ) { x_arc[4][0] = dx2; y_arc[4][0] = -del; arc_in[4] = 1; }
+                else             { x_arc[1][0] = dx2; y_arc[1][0] = -del; arc_in[1] = 1; }
+            }
+        }
+    }
+
+    n_int[4] = 0;
+    if ( dy1squ <= radsqu)
+    {
+        del = std::sqrt( radsqu - dy1squ);
+        if ( ( ( -del ) <= dx2 ) && ( del >= dx1 ) )
+        {
+            y_int[4][0] = y_int[4][1] = dy1;
+            if ( ( del ) < dx2 )
+            {
+                x_int[4][0] = del;
+                n_int[4]++;
+                if ( dy1 > 0.0 ) { x_arc[3][1] = del; y_arc[3][1] = dy1; arc_in[3] = 1; }
+                else             { x_arc[4][1] = del; y_arc[4][1] = dy1; arc_in[4] = 1; }
+            }
+            if ( (-del ) > dx1 )
+            {
+                x_int[4][n_int[4]] = -del;
+                n_int[4]++;
+                if ( dy1 > 0.0 ) { x_arc[2][0] = -del; y_arc[2][0] = dy1; arc_in[2] = 1; }
+                else             { x_arc[1][0] = -del; y_arc[1][0] = dy1; arc_in[1] = 1; }
+            }
+        }
+    }
+
+    n_int[0] = n_int[4];
+    n_int[5] = n_int[1];
+
+    y_int[0][0] = y_int[0][1] = dy1;
+    x_int[0][0] = x_int[4][0];
+    x_int[0][1] = x_int[4][1];
+    x_int[5][0] = x_int[5][1] = dx1;
+    y_int[5][0] = y_int[1][0];
+    y_int[5][1] = y_int[1][1];
+
+    /* There are five separate cases, depending of the number of vertices inside the circle */
+    switch ( n_in )
+    {
+        case 0:
+        {
+            /* We start with the whole area of the circle, and then subtract any bits that stick out. */
+            area =  radsqu * mathematical::pi;
+            *perim_p = mathematical::twoPi * rad;
+            no_intersection = true;
+            for (n_vert = 1; n_vert < 5; n_vert++)
+            {
+                assert(n_int[n_vert] != 1);
+                if (n_int[n_vert] == 2)
+                {
+                    /* The area of the bit to be subtracted is a sector minus a triangle. */
+                    no_intersection = false;
+                    angle = sector( x_int[n_vert][1], y_int[n_vert][1], x_int[n_vert][0], y_int[n_vert][0]);
+                    area -= angle * radsqu * 0.5;
+                    *perim_p -= angle * rad;
+                    /* Two trinagles specified here, but one has zero area. */
+                    area += ( - ( y_int[n_vert][1] - y_int[n_vert][0] ) * x_int[n_vert][0]
+                              + ( x_int[n_vert][1] - x_int[n_vert][0] ) * y_int[n_vert][0] ) / 2.0;
+                }
+            }
+            /* Need to allow for when the circle is completely out side the rectanglle
+             by checking if the centre is outside the rectangle                                           */
+            if ( no_intersection )
+            {
+                if ( (dx1>0) ||(dx2<0) || (dy1>0) || (dy2<0) )
+                {
+                    *perim_p = *x_proj_edge_p = *y_proj_edge_p = 0.0;
+                    area = *x_overlap_p = *y_overlap_p = 0.0;
+                    return area;
+                }
+            }
+
+            break;
+        }
+
+        case 1:
+        {
+            /* Find which vertex is inside */
+            n_vert = 1;
+            while ( !vert_in[n_vert] ) { n_vert++; assert( n_vert < 5 ); }
+            assert( n_int[n_vert-1] == 1 );
+            if ( n_int[n_vert] != 1 )
+            {
+                assert( n_int[n_vert] == 1 );
+            }
+            angle = sector( x_int[n_vert-1][0], y_int[n_vert-1][0], x_int[n_vert][0], y_int[n_vert][0]);
+            area = angle * radsqu * 0.5;
+            *perim_p = angle * rad;
+            /* We subtract (or add) two triangles; the other two evaluate to zero */
+            area -= ( - ( x_int[n_vert][0]   - dx[n_vert] ) * dy[n_vert]
+                      + ( x_int[n_vert-1][0] - dx[n_vert] ) * dy[n_vert]
+                      + ( y_int[n_vert][0]   - dy[n_vert] ) * dx[n_vert]
+                      - ( y_int[n_vert-1][0] - dy[n_vert] ) * dx[n_vert] ) / 2.0;
+
+            break;
+        }
+
+        case 2:
+        {
+            /* This time n_vert is the number of the side which is completely inside the circle */
+            n_vert = 1;
+            while ( !(vert_in[n_vert] && vert_in[n_vert+1]) ) { n_vert++; assert( n_vert < 5 ); }
+            assert( n_int[n_vert-1] == 1 );
+            assert( n_int[n_vert+1] == 1 );
+            angle = sector( x_int[n_vert-1][0], y_int[n_vert-1][0], x_int[n_vert+1][0], y_int[n_vert+1][0]);
+            area = angle * radsqu * 0.5;
+            *perim_p = angle * rad;
+            /* We subtract (or add) three triangles; the other three evaluate to zero */
+            area += ( ( x_int[n_vert+1][0] - dx[n_vert+1] ) * dy[n_vert+1]
+                      - ( x_int[n_vert-1][0] - dx[n_vert] ) * dy[n_vert]
+                      - ( y_int[n_vert+1][0] - dy[n_vert+1] ) * dx[n_vert+1]
+                      + ( y_int[n_vert-1][0] - dy[n_vert] ) * dx[n_vert]
+                      + ( dx[n_vert+1] -dx[n_vert] ) * dy[n_vert]
+                      - ( dy[n_vert+1] -dy[n_vert] ) * dx[n_vert] ) / 2.0;
+
+            switch ( n_vert )
+            {
+                case 1: x_olap_min = dx1; break;
+                case 2: y_olap_max = dy2; break;
+                case 3: x_olap_max = dx2; break;
+                case 4: y_olap_min = dy1; break;
+            }
+
+            break;
+        }
+
+        case 3:
+        {
+            /* Find which vertex is NOT inside */
+            n_vert = 1;
+            while ( vert_in[n_vert] ) { n_vert++; assert( n_vert < 5 ); }
+            assert( n_int[n_vert-1] == 1 );
+            assert( n_int[n_vert] == 1 );
+            n_oppv = (n_vert + 2) % 4;
+            angle = sector( x_int[n_vert][0], y_int[n_vert][0], x_int[n_vert-1][0], y_int[n_vert-1][0]);
+            area = angle * radsqu * 0.5;
+            *perim_p = angle * rad;
+            /* We subtract (or add) four triangles; the other four evaluate to zero */
+            area += ( - ( x_int[n_vert][0]   - dx[n_vert+1] ) * dy[n_vert+1]
+                      + ( x_int[n_vert-1][0] - dx[n_vert-1] ) * dy[n_vert-1]
+                      + ( y_int[n_vert][0]   - dy[n_vert+1] ) * dx[n_vert+1]
+                      - ( y_int[n_vert-1][0] - dy[n_vert-1] ) * dx[n_vert-1]
+                      + ( dx[n_oppv] -dx[n_vert+1] ) * dy[n_oppv]
+                      - ( dx[n_oppv] -dx[n_vert-1] ) * dy[n_oppv]
+                      - ( dy[n_oppv] -dy[n_vert+1] ) * dx[n_oppv]
+                      + ( dy[n_oppv] -dy[n_vert-1] ) * dx[n_oppv] ) / 2.0;
+
+            x_olap_min = dx1;
+            y_olap_max = dy2;
+            x_olap_max = dx2;
+            y_olap_min = dy1;
+
+            break;
+        }
+
+        case 4:
+        {
+            /* Easy! We have the whole rectangle.  */
+            area = *x_overlap_p = *y_overlap_p = 1.0; // Normalised
+            *perim_p = *x_proj_edge_p = *y_proj_edge_p = 0.0;
+            return area;
+
+            break;
+        }
+    }
+
+    // The area may be very small negative by rounding errors
+    assert(area >=-1.0E-4);
+    if (area < 0.0) area = 0.0;
+    /* Return the overlap as a fraction of the rectangle's area. */
+    area /= ( (x2 - x1 ) * ( y2 - y1 ) );
+
+    // Sum the parts of the circumference that are inside the circle, projected onto axes
+    *x_proj_edge_p =
+    (
+        (y_arc[1][1] - y_arc[1][0]) * arc_in[1]
+      + (y_arc[2][1] - y_arc[2][0]) * arc_in[2]
+      + (y_arc[3][0] - y_arc[3][1]) * arc_in[3]
+      + (y_arc[4][0] - y_arc[4][1]) * arc_in[4]
+    );
+
+    *y_proj_edge_p =
+    (
+        (x_arc[1][0] - x_arc[1][1]) * arc_in[1]
+      + (x_arc[2][1] - x_arc[2][0]) * arc_in[2]
+      + (x_arc[3][1] - x_arc[3][0]) * arc_in[3]
+      + (x_arc[4][0] - x_arc[4][1]) * arc_in[4]
+    );
+
+    if (arc_in[1])
+    {
+        x_olap_min = min(x_olap_min, x_arc[1][1]);
+        x_olap_max = max(x_olap_max, x_arc[1][0]);
+        y_olap_min = min(y_olap_min, y_arc[1][0]);
+        y_olap_max = max(y_olap_max, y_arc[1][1]);
+    }
+    if (arc_in[2])
+    {
+        x_olap_min = min(x_olap_min, x_arc[2][0]);
+        x_olap_max = max(x_olap_max, x_arc[2][1]);
+        y_olap_min = min(y_olap_min, y_arc[2][0]);
+        y_olap_max = max(y_olap_max, y_arc[2][1]);
+    }
+    if (arc_in[3])
+    {
+        x_olap_min = min(x_olap_min, x_arc[3][0]);
+        x_olap_max = max(x_olap_max, x_arc[3][1]);
+        y_olap_min = min(y_olap_min, y_arc[3][1]);
+        y_olap_max = max(y_olap_max, y_arc[3][0]);
+    }
+    if (arc_in[4])
+    {
+        x_olap_min = min(x_olap_min, x_arc[4][1]);
+        x_olap_max = max(x_olap_max, x_arc[4][0]);
+        y_olap_min = min(y_olap_min, y_arc[4][1]);
+        y_olap_max = max(y_olap_max, y_arc[4][0]);
+    }
+
+    *x_overlap_p = ( x_olap_max - x_olap_min ) / ( x2 - x1 );
+    *y_overlap_p = ( y_olap_max - y_olap_min ) / ( y2 - y1 );
+    assert ( *x_overlap_p >= -floatSMALL );
+    assert ( *y_overlap_p >= -floatSMALL );
+
+    return area;
+} // End intersect
+
+
+// ************************************************************************* //
+
+double Foam::PDRutils::l_blockage
+(
+    double xc, double yc, double rad,
+    double x1, double x2,
+    double y1, double y2,
+    scalar* count_p, scalar* drag_p, scalar* centre_p
+)
+{
+    double xi = 0.0, lb, lb1, lb2, del;
+    bool within = true; // Indicates that the the intersection does not overlap the ends of the line
+
+    /* xi is the side we need to calc. intersections with */
+    if      ( xc < x1 ) { xi = x1; }
+    else if ( xc > x2 ) { xi = x2; }
+
+    if ( xi == 0.0 )
+    {
+        del = rad;     // The relevant lowest ( or highest) point is at end of vertical radius
+    }
+    else                // The relevant lowest ( or highest) point at intersection with x = xi
+    {
+        del = rad*rad - ( xi - xc ) * ( xi - xc );
+        if ( del < 0.0 ) { del = 0.0; }                  // No intersection
+        else             { del = std::sqrt(del); }
+    }
+
+    if ( ( yc + del ) > y2 ) { lb2  = y2; within = false; } else { lb2  = yc + del; }
+    if ( ( yc - del ) < y1 ) { lb1 = y1; within = false; } else { lb1 = yc - del; }
+
+    lb = (lb2 - lb1) / (y2 - y1);
+    *centre_p = (lb2 + lb1) * 0.5;
+
+    if ( lb < 0.0 )  lb = 0.0;
+
+    /* *count_p is 0 if the circle overlaps either y-side of the rectangle,
+     1 if the circle is entirely inside the rectangle
+     reduced if it overlaps x-sides.
+     A negative value indicates total blockage*/
+    if ( within && (lb > 0.0) )
+    {
+        *count_p = 1.0;
+        if ( ( xc - rad ) < x1 ) *count_p -= 0.5;
+        if ( ( xc + rad ) > x2 ) *count_p -= 0.5;
+    }
+    else
+    {
+        *count_p = 0.0;
+    }
+    *drag_p = lb * 1.2; //*drag_p = lb * CD_ROUND;
+    if ( lb > 0.99 ) { *count_p = -1000.0; *drag_p = 1000.0; }
+    assert(lb >-100.0);
+    return lb;
+}// End l_blockage
+
+
+// ************************************************************************* //
+
+double Foam::PDRutils::inters_db
+(
+    double xc, double yc, double theta,
+    double wa, double wb,
+    double x1, double x2,
+    double y1, double y2,
+    scalar* count_p,
+    symmTensor2D& vdrag, scalar* perim_p,
+    scalar* x_lblk_p, scalar* y_lblk_p,
+    scalar* x_centre_p, scalar* y_centre_p
+)
+{
+    double  x_int[6][2], y_int[6][2]; // Coordinates of intersections between the circle and sides of the rectangle.
+    double  area, lpa, lpb, len;
+
+    double  m = ::tan( theta );
+    double  cth = ::cos( theta );
+    double  sth = ::sin( theta );
+
+    double  was  = wa * sth * 0.5;
+    double  wac  = wa * cth * 0.5;
+    double  wbs  = wb * sth * 0.5;
+    double  wbc  = wb * cth * 0.5;
+    double  waos = wa / sth * 0.5;
+    double  waoc = wa / cth * 0.5;
+    double  wbos = wb / sth * 0.5;
+    double  wboc = wb / cth * 0.5;
+
+    double xb[6], yb[6], xp1, xp2, yp1, yp2;
+
+    double  dx1 = (x1 - xc);
+    double  dx2 = (x2 - xc);
+    double  dy1 = (y1 - yc);
+    double  dy2 = (y2 - yc);
+
+    *count_p = 0;
+
+// The vertices of the rectangle (all coordinates relative to centre of rectangle)
+    xb[1] = -wac - wbs;
+    yb[1] = -was + wbc;
+    xb[3] =  wac + wbs;
+    yb[3] =  was - wbc;
+    xb[2] =  wac - wbs;
+    yb[2] =  was + wbc;
+    xb[4] = -wac + wbs;
+    yb[4] = -was - wbc;
+
+    // First parameter of x_int or y_int determines which side of the cell we intersecting with
+    //  Second parameter 0 is first intersection, 1 is second, going clockwise
+
+    if ( xb[1] < dx1 )  // i.e. if left corner of block is to the left of x1
+    {
+        // Where one of lower sides of block intersects with x=x1
+        // Innermost max determines which intersection is the genuine one
+        // (not if whole block is to left of x1)
+        y_int[1][0] = min(max(max(dx1 * m - wboc, -dx1 / m - waos), dy1), dy2);
+        // Upper intersection
+        y_int[1][1] = min(max(min(dx1 * m + wboc, -dx1 / m + waos), dy1), dy2);
+    }
+    else
+    {
+        y_int[1][1] = dy1;
+        y_int[1][0] = dy2;
+        // We add a quarter to count for each vertex inside the cell
+        if ( (yb[1] > dy1) && (yb[1] < dy2) ) // ?? Seems inefficient ??
+        { *count_p += 0.25; }
+    }
+    if ( xb[3] > dx2 )
+    {
+        y_int[3][1] = min(max(max(dx2 * m - wboc, -dx2 / m - waos), dy1), dy2);
+        y_int[3][0] = min(max(min(dx2 * m + wboc, -dx2 / m + waos), dy1), dy2);
+    }
+    else
+    {
+        y_int[3][0] = dy1;
+        y_int[3][1] = dy2;
+        if (yb[3] > dy1 && yb[3] < dy2)
+        {
+            *count_p += 0.25;
+        }
+    }
+    if (yb[2] > dy2)
+    {
+        x_int[2][0] = min(max(max(dy2 / m - wbos, -dy2 * m - waoc), dx1), dx2);
+        x_int[2][1] = min(max(min(dy2 / m + wbos, -dy2 * m + waoc), dx1), dx2);
+    }
+    else
+    {
+        x_int[2][0] = dx2;
+        x_int[2][1] = dx1;
+        if ( (xb[2] > dx1) && (xb[2] < dx2) )
+        { *count_p += 0.25; }
+    }
+    if ( yb[4] < dy1 )
+    {
+        x_int[4][1] = min(max(max(dy1 / m - wbos, -dy1 * m - waoc ), dx1), dx2);
+        x_int[4][0] = min(max(min(dy1 / m + wbos, -dy1 * m + waoc ), dx1), dx2);
+    }
+    else
+    {
+        x_int[4][1] = dx2;
+        x_int[4][0] = dx1;
+        if ( (xb[4] > dx1) && (xb[4] < dx2) )
+        { *count_p += 0.25; }
+    }
+
+    y_int[0][0] = y_int[0][1] = dy1;
+    x_int[0][0] = x_int[4][0];
+    x_int[0][1] = x_int[4][1];
+    x_int[5][0] = x_int[5][1] = dx1;
+    y_int[5][0] = y_int[1][0];
+    y_int[5][1] = y_int[1][1];
+
+
+// We can now define a smaller enclosing rectangle
+
+    xp1 = min(x_int[2][0], x_int[4][1]);  // Leftmost of the intersections with top and bottom of cell
+    if ( yb[1] > dy1 && yb[1] < dy2 )       xp1 = min(xp1, xb[1] ); // left corner of block
+    xp1 = max(xp1, dx1);  // Make sure it is not to the left of x1
+
+    yp2 = max(y_int[1][1], y_int[3][0] );
+    if ( xb[2] > dx1 && xb[2] < dx2 )  yp2 = max(yp2, yb[2] );
+    yp2 = min(yp2, dy2);
+
+    xp2 = max(x_int[2][1], x_int[4][0] );
+    if ( yb[3] > dy1 && yb[3] < dy2 )  xp2 = max(xp2, xb[3] );
+    xp2 = min(xp2, dx2);
+
+    yp1 = min(y_int[1][0], y_int[3][1]);
+    if ( xb[4] > dx1 && xb[4] < dx2 )  yp1 = min(yp1, yb[4] );
+    yp1 = max(yp1, dy1 );
+
+    // Conveniently, the dimensions of the enclosing rectangle give us the line blockages
+    *x_lblk_p = (xp2 - xp1 ) / (x2 - x1 );
+    if ( *x_lblk_p < 0.0 ) { *x_lblk_p = 0.0; *count_p = 0.0; };  // ?? Better to trap no intersection earlier??
+    *y_lblk_p = (yp2 - yp1 ) / (y2 - y1 );
+    if ( *y_lblk_p < 0.0 ) { *y_lblk_p = 0.0; *count_p = 0.0; };
+
+    *x_centre_p = xc + (xp2 + xp1 ) * 0.5;
+    *y_centre_p = yc + (yp2 + yp1 ) * 0.5;
+
+    *perim_p = lpa = lpb = 0.0;;
+    area = (xp2 - xp1 ) * ( yp2 - yp1 );
+    {
+        double  dxx, dyy;
+        // Lower left
+        dyy = max(0.0, min(yb[1], y_int[1][0]) - yp1);
+        dxx = min(xb[4], x_int[0][1] ) - xp1;
+
+        if ( ( dxx * dyy) > 0.0 )
+        {
+            area -= dxx * dyy * 0.5;
+            len = std::hypot(dxx, dyy);
+            lpa += len * 0.5;
+            *perim_p += len;
+        }
+        // Upper left
+        dxx = max(0.0, min(xb[2], x_int[2][0]) - xp1);
+        dyy = yp2 - max(yb[1], y_int[1][1] );
+        if ( ( dxx * dyy) > 0.0 )
+        {
+            area -= dxx * dyy * 0.5;
+            len = std::hypot(dxx, dyy);
+            lpb += len * 0.5;
+            *perim_p += len;
+        }
+        // Upper right
+        dyy = max(0.0, yp2 - max(yb[3], y_int[3][0]));
+        dxx = xp2 - max(xb[2], x_int[2][1] );
+        if ( ( dxx * dyy) > 0.0 )
+        {
+            area -= dxx * dyy * 0.5;
+            len = std::hypot(dxx, dyy);
+            lpa += len * 0.5;
+            *perim_p += len;
+        }
+        // Lower right
+        dxx = max(0.0, xp2 - max(xb[4], x_int[4][0]));
+        dyy = min(yb[3], y_int[3][1] ) - yp1;
+        if ( ( dxx * dyy) > 0.0 )
+        {
+            area -= dxx * dyy * 0.5;
+            len = std::hypot(dxx, dyy);
+            lpb += len * 0.5;
+            *perim_p += len;
+        }
+
+    }
+
+    vdrag.xx() = lpa * cth * cth + lpb * sth * sth;
+    vdrag.xy() = lpa * cth * sth - lpb * sth * cth;
+    vdrag.yy() = lpa * sth * sth + lpb * cth * cth;
+
+    return area / ( (x2 - x1 ) * ( y2 - y1 ) );
+} // End inters_db
+
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/PDRutilsOverlap.C b/applications/utilities/preProcessing/PDRsetFields/PDRutilsOverlap.C
new file mode 100644
index 00000000000..85f5fa9efa3
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/PDRutilsOverlap.C
@@ -0,0 +1,767 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2016 Shell Research Ltd.
+    Copyright (C) 2019 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 "PDRsetFields.H"
+#include "PDRutilsInternal.H"
+#include "mathematicalConstants.H"
+
+#ifndef FULLDEBUG
+#define NDEBUG
+#endif
+#include <cassert>
+
+using namespace Foam::constant;
+
+// * * * * * * * * * * * * * * * Local Functions * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+    // A sign-corrected multiply
+    // This is used for porosity of obstacle intersections
+    inline static scalar COMBLK(const scalar a, const scalar b)
+    {
+        if (a < 0)
+        {
+            return -a * b;
+        }
+
+        return a * b;
+    }
+
+
+    // Obstacle satisfies some minimum size checks.
+    // A volume check misses thin plates, so use area.
+    // Thin sheet overlaps can be produced by touching objects
+    // if the obs_extend parameter is > 0.
+    inline static bool obsHasMinSize(const vector& span, const PDRparams& tol)
+    {
+        return
+        (
+            (cmptProduct(span) > tol.min_overlap_vol)
+         &&
+            (
+                (span.x() * span.y() > tol.min_overlap_area)
+             || (span.y() * span.z() > tol.min_overlap_area)
+             || (span.z() * span.x() > tol.min_overlap_area)
+            )
+        );
+    }
+
+} // End namespace Foam
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+
+void Foam::PDRutils::one_d_overlap
+(
+    scalar xmin,
+    scalar xmax,
+    const PDRblock::location& grid,
+    List<scalar>& olap,
+    int *cmin, int *cmax,
+    int *cfmin, int *cfmax
+)
+{
+    // Looking at one coordinate direction, called x here, for something
+    // that extends from xmin to xmax, calculate how much it overlaps
+    // each cell in this direction. Result returned in 'olap' List is
+    // the proportion of the grid step overlapped, i.e dimensionless.
+    // First and last steps overlapped given by *cmin, *cmax
+    // Ditto for shifted grid given by *cfmin, *cfmax.
+
+    // Initially zero everwhere
+    olap = Zero;
+
+    if (olap.size() < grid.nPoints())
+    {
+        FatalErrorInFunction
+            << "The overlap scratch array is too small, has "
+            << olap.size() << " but needs " << grid.nPoints() << nl
+            << exit(FatalError);
+    }
+
+
+    // No intersection with the box
+    if (xmax <= grid.first() || grid.last() <= xmin)
+    {
+        // Mark as bad range, cannot iterate
+        *cmin = 0;
+        *cmax = -1;
+
+        // Another bad range (cannot iterate) but for extra safety ensure
+        // that (cfmin -> cmin) and (cmax -> cfmax) cannot iterate either
+        *cfmin = 1;
+        *cfmax = -2;
+        return;
+    }
+
+    // Ensure search is within the (point) bounds
+    xmin = grid.clip(xmin);
+    xmax = grid.clip(xmax);
+
+    // The begin/end of the obstacle
+    *cmin = grid.findCell(xmin);
+    *cmax = grid.findCell(xmax);
+
+    for (label ix = *cmin; ix <= *cmax; ++ix)
+    {
+        olap[ix] = 1.0;
+    }
+
+    // Fixup ends
+    if (*cmax == *cmin)
+    {
+        olap[*cmax] = (xmax - xmin) / grid.width(*cmax);
+    }
+    else
+    {
+        if (grid[*cmin] < xmin)
+        {
+            olap[*cmin] = (grid[*cmin+1] - xmin) / grid.width(*cmin);
+        }
+
+        if (xmax < grid[*cmax+1])
+        {
+            olap[*cmax] = (xmax - grid[*cmax]) / grid.width(*cmax);
+        }
+    }
+    assert(olap[*cmax] >= 0.0);
+
+
+    // Is xmin below/above the cell-centre (for virtual staggered-grid) ?
+    *cfmin =
+    (
+        xmin < grid.C(*cmin)
+      ? *cmin
+      : Foam::min(*cmin+1, grid.nCells()-1)
+    );
+
+    // Is xmax below/above the cell-centre (for virtual staggered-grid) ?
+    *cfmax =
+    (
+        xmax < grid.C(*cmax)
+      ? *cmax
+      : Foam::min(*cmax+1, grid.nCells()-1)
+    );
+}
+
+
+/**************************************************************************************************/
+
+void Foam::PDRutils::two_d_overlap
+(
+    const UList<scalar>& a_olap, label amin, label amax,
+    const UList<scalar>& b_olap, label bmin, label bmax,
+    SquareMatrix<scalar>& ab_olap
+)
+{
+    // We go one over the relevant min/max limits since these values might be
+    // used. If not, they would have been zeroed in one_d_overlap
+
+    amin = Foam::max(0, amin-1);
+    bmin = Foam::max(0, bmin-1);
+    amax = Foam::min(a_olap.size()-1, amax+1);
+    bmax = Foam::min(b_olap.size()-1, bmax+1);
+
+    for (label ia = amin; ia <= amax; ++ia)
+    {
+        for (label ib = bmin; ib <= bmax; ++ib)
+        {
+            ab_olap(ia,ib) = a_olap[ia] * b_olap[ib];
+        }
+    }
+}
+
+
+/**************************************************************************************************/
+
+void Foam::PDRutils::circle_overlap
+(
+    scalar ac, scalar bc, scalar dia,
+    scalar theta, scalar wa, scalar wb,
+    const PDRblock::location& agrid, label amin, label amax,
+    const PDRblock::location& bgrid, label bmin, label bmax,
+    SquareMatrix<scalar>& ab_olap,
+    SquareMatrix<scalar>& ab_perim,
+    SquareMatrix<scalar>& a_lblock,
+    SquareMatrix<scalar>& ac_lblock,
+    SquareMatrix<scalar>& c_count,
+    SquareMatrix<symmTensor2D>& c_drag,
+    SquareMatrix<scalar>& b_lblock,
+    SquareMatrix<scalar>& bc_lblock
+)
+{
+    /* This routine calculates the proportion of each (two-dimensional) grid cell
+       overlapped by the circle or angled rectangle. Coordinates are labelled a and b.
+       On entry:
+              ac, bc                coordinates of centre of circle or rectangle
+              dia                   diameter of circle (zero for rectangle)
+              theta, wa, wb parameters for rectangle
+              agrid[]               locations of grid lines of a-grid
+              amin, amax    first and last cells in a-grid overlapped by object
+              (similarly for b)
+            On exit:
+              abolap                2-D array of (proportionate) area blockage by grid cell
+              a_lblock              2-D array of (proportionate) blockage to a-direction flow
+                                            (This will be area blockage when extruded in the third coordinate).
+              a_count               (2-D array)The contribution of this object to the count of obstacles blocking
+                                            a-direction flow. This is only non-zero if the object is inside the
+                                            lateral boundaries of the cell. It is large negative if the cell is
+                                            totally blocked in this direction.
+              (similarly for b)
+              c_drag                2-D array of tensor that will give tensor drag in each cell (when multiplied
+                                            Cd, cylinder length, and 0.5 rho*U^2) Dimension: L.
+
+       Note that this routine does not zero array elements outside the amin to amax, bmin to bmax area.
+    */
+
+    scalar count, a_lblk, b_lblk, perim, dummy;
+
+    symmTensor2D vdrag(Zero);
+
+    // Prevent stepping outside of the array when the obstacle is on the
+    // upper boundary
+
+    // Upper limit of inclusive range is nCells-1
+    amin = Foam::max(0, amin);
+    bmin = Foam::max(0, bmin);
+    amax = Foam::min(amax, agrid.nCells()-1);
+    bmax = Foam::min(bmax, bgrid.nCells()-1);
+
+    for (label ia = amin; ia <= amax; ++ia)
+    {
+        // Cell-centred grid
+        const scalar a1 = agrid[ia];
+        const scalar a2 = agrid[ia+1];
+
+        // Left-shifted staggered face grid (-1 addressing is OK)
+        const scalar af1 = agrid.C(ia-1);
+        const scalar af2 = agrid.C(ia);
+
+        for (label ib = bmin; ib <= bmax; ++ib)
+        {
+            // Cell-centred grid
+            const scalar b1 = bgrid[ib];
+            const scalar b2 = bgrid[ib+1];
+
+            // Left-shifted staggered face grid (-1 addressing is OK)
+            const scalar bf1 = bgrid.C(ib-1);
+            const scalar bf2 = bgrid.C(ib);
+
+            // Do the centred cell
+            if ( dia > 0.0 )
+            {
+                ab_olap(ia,ib) = inters_cy
+                (
+                    ac, bc, 0.5*dia, a1, a2, b1, b2, &perim,
+                    &dummy, &dummy, &b_lblk, &a_lblk
+                );
+/* The last two arguments of the above call appear to be reversed, but the inters_cy routine returns
+   the amount of overlap in the a and b direcvtions, which are the blockage to the b and a directions. */
+
+/* abolap * cell area is area of cylinder in this cell. Divide by PI%D^2/4 to get proportion of cylinder in cell
+   For whole cylinger c_drag should be = D, so multiply by D.                                                   */
+
+                c_drag(ia,ib).xx() = c_drag(ia,ib).yy() = 4.0 * ab_olap(ia,ib) * (a2 - a1) * (b2 - b1) / dia / mathematical::pi;
+                c_drag(ia,ib).xy() = Zero;
+                c_count(ia,ib) = perim / (mathematical::pi * dia);
+
+//******?????
+                scalar area = (a2 - a1) * (b2 - b1);
+                scalar rat = dia * dia / area - 1.5;
+                if (rat > 0.0)
+                {
+                    scalar da = ac - 0.5 * (a1 + a2);
+                    scalar db = bc - 0.5 * (b1 + b2);
+                    scalar dc = std::hypot(da, db);
+                    scalar rat1 = min(max((dc / sqrt(area) - 0.3) * 1.4, 0), 1);
+                    scalar drg0 = c_drag(ia,ib).xx();
+                    scalar drg1 = c_drag(ia,ib).yy();
+                    scalar drg = std::hypot(drg0, drg1);
+                    c_drag(ia,ib).xx() = drg * ( 1.0 - rat1 ) + drg * da*da/dc/dc * rat1;
+                    c_drag(ia,ib).yy() = drg * ( 1.0 - rat1 ) + drg * db*db/dc/dc * rat1;
+                    c_drag(ia,ib).xy() = drg * da*db/dc/dc *rat1;
+                }
+            }
+            else
+            {
+                ab_olap(ia,ib) = inters_db( ac, bc, theta, wa, wb, a1, a2, b1, b2, &count, c_drag(ia,ib), &perim, &a_lblk, &b_lblk, &dummy, &dummy );
+                c_count(ia,ib) = perim / ( wa + wb ) * 0.5;
+            }
+            ac_lblock(ia,ib) = a_lblk;
+            bc_lblock(ia,ib) = b_lblk;
+            ab_perim(ia,ib) = perim;
+
+            // Do the a-shifted cell
+            if ( dia > 0.0 ) // I.e. a cylinder, not a d.b.
+            {
+                if (ac >= af1 && ac < af2)
+                {
+                    // Only want to block one layer of faces
+                    a_lblock(ia,ib) = l_blockage
+                    (
+                        ac, bc, 0.5*dia,
+                        af1, af2, b1, b2, &count, &dummy, &dummy
+                    );
+                }
+                inters_cy
+                (
+                    ac, bc, 0.5*dia,
+                    af1, af2, b1, b2,
+                    &perim, &count, &dummy, &dummy, &dummy
+                );
+            }
+            else
+            {
+                inters_db
+                (
+                    ac, bc, theta, wa, wb, af1, af2, b1, b2,
+                    &count, vdrag, &dummy, &a_lblk, &b_lblk, &dummy, &dummy
+                );
+                a_lblock(ia,ib) = a_lblk;
+            }
+
+            // Do the b-shifted cell
+            if ( dia > 0.0 )
+            {
+                if (bc >= bf1 && bc < bf2)
+                {
+                    // Only want to block one layer of faces
+                    b_lblock(ia,ib) = l_blockage
+                    (
+                        bc, ac, 0.5*dia, bf1, bf2, a1, a2,
+                        &count, &(vdrag.yy()), &dummy
+                    );
+                }
+
+                inters_cy
+                (
+                    ac, bc, 0.5*dia,
+                    a1, a2, bf1, bf2,
+                    &perim, &dummy, &count, &dummy, &dummy
+                );
+            }
+            else
+            {
+                inters_db
+                (
+                    ac, bc, theta, wa, wb,
+                    a1, a2, bf1, bf2,
+                    &count, vdrag, &dummy, &a_lblk, &b_lblk, &dummy, &dummy
+                );
+                b_lblock(ia,ib) =  b_lblk;
+            }
+        }
+    }
+
+}  // End circle_overlap
+
+
+/**************************************************************************************************/
+
+scalar block_overlap
+(
+    DynamicList<PDRobstacle>& blocks,
+    const labelRange& range,
+    const scalar multiplier
+)
+{
+    // Size information
+    const label nBlock = range.size();
+
+    // The return value
+    scalar totVolume = 0;
+
+    if (nBlock < 2) return 0;
+
+
+    // Sort blocks by their x-position (with sortBias)
+    labelList blkOrder;
+    sortedOrder(blocks[range], blkOrder);
+
+    DynamicList<PDRobstacle> newBlocks;
+
+    // Work through the sorted blocks
+    for (label i1 = 0; i1 < nBlock-1; ++i1)
+    {
+        const PDRobstacle& blk1 = blocks[range[blkOrder[i1]]];
+
+        // Upper coordinates
+        const vector max1 = blk1.pt + blk1.span;
+
+        // For second block start with the next one on the list, and
+        // stop when we find the first one whose biased x-position
+        // is beyond the end of the block1
+
+        for (label i2 = i1 + 1; i2 < nBlock; ++i2)
+        {
+            const PDRobstacle& blk2 = blocks[range[blkOrder[i2]]];
+
+            // Upper coordinates
+            const vector max2 = blk2.pt + blk2.span;
+
+            if (max1.x() <= blk2.x())
+            {
+                break;
+            }
+
+            if
+            (
+                max1.y() <= blk2.y()
+             || max1.z() <= blk2.z()
+             || max2.y() <= blk1.y()
+             || max2.z() <= blk1.z()
+             || (blk1.vbkge * blk2.vbkge <= 0)
+            )
+            {
+                continue;
+            }
+
+
+            {
+                PDRobstacle over;
+
+                over.pt = max(blk1.pt, blk2.pt);
+                over.span = min(max1, max2) - over.pt;
+
+                assert(cmptProduct(over.span) > 0.0);
+
+                // This routine should only have been called for all +ve o r all -ve obstacles
+                assert(blk1.vbkge * blk2.vbkge > 0);
+                /* At the first level of intersection, we create an obstacle of blockage -1 (if both objects solid)
+                 to cancel out the double counting. (multiplier is 1).
+                 ?? COMBLK does a (sign corrected) multiply; is this corrrect for porous obstacles?
+                 Depends on how blockages were summed in the first place. In fact this -ve obstacle
+                 concept only works if the blockages are summed??*/
+                over.vbkge = - COMBLK( blk1.vbkge, blk2.vbkge ) * multiplier;
+                over.xbkge = - COMBLK( blk1.xbkge, blk2.xbkge ) * multiplier;
+                over.ybkge = - COMBLK( blk1.ybkge, blk2.ybkge ) * multiplier;
+                over.zbkge = - COMBLK( blk1.zbkge, blk2.zbkge ) * multiplier;
+                over.typeId = 81 + int(15 * multiplier);  // Not subsequently used
+
+                if (obsHasMinSize(over.span, pars))
+                {
+                    // Obstacle satisfies some minimum size checks
+                    totVolume -= over.volume();
+
+                    newBlocks.append(over);
+                }
+            }
+        }
+    }
+
+    blocks.append(std::move(newBlocks));
+
+    return totVolume;
+}
+
+
+/**************************************************************************************************/
+
+using namespace Foam::PDRutils;
+
+scalar block_cylinder_overlap
+(
+    DynamicList<PDRobstacle>& blocks,
+    const labelRange& range,
+    const UList<PDRobstacle>& cylinders
+)
+{
+    // Size information
+    const label nBlock = range.size();
+    const label nCyl = cylinders.size();
+
+    // The return value
+    scalar totVolume = 0;
+
+    if (!nBlock || !nCyl) return 0;
+
+    scalar area, a_lblk, b_lblk, dummy, a_centre, b_centre;
+    symmTensor2D dum2;
+
+
+    // Sort blocks and cylinders by their x-position (with sortBias)
+    labelList blkOrder;
+    sortedOrder(blocks[range], blkOrder);
+
+    labelList cylOrder;
+    sortedOrder(cylinders, cylOrder);
+
+    DynamicList<PDRobstacle> newBlocks;
+
+    // Work through the sorted blocks
+    for (label i1 = 0; i1 < nBlock; i1++)
+    {
+        const PDRobstacle& blk1 = blocks[range[blkOrder[i1]]];
+
+        // Upper coordinates
+        const vector max1 = blk1.pt + blk1.span;
+
+        // Cyls whose end is before start of this block no longer
+        // need to be considered
+
+        label i2 = 0;
+        while (i2 < nCyl-1 && cylinders[cylOrder[i2]] < blk1)
+        {
+            ++i2;
+        }
+
+        for (/*nil*/; i2 < nCyl; ++i2)
+        {
+            const PDRobstacle& cyl2 = cylinders[cylOrder[i2]];
+
+            // Calculate overlap in axis direction; if zero continue.
+            // Calculate 2-d overlap and c 0f g; if area zero continue.
+
+            PDRobstacle over;
+
+
+            switch (cyl2.orient)
+            {
+                case vector::Z:
+                {
+                    const scalar zm2 = cyl2.z() + cyl2.len();
+                    if (blk1.z() > zm2 || cyl2.z() > max1.z()) continue;
+
+                    if ( cyl2.dia() == 0.0 )
+                    {
+                        area = inters_db
+                        (
+                            cyl2.x(), cyl2.y(), cyl2.theta(), cyl2.wa, cyl2.wb,
+                            blk1.x(), max1.x(),
+                            blk1.y(), max1.y(),
+                            &dummy, dum2, &dummy, &a_lblk, &b_lblk,
+                            &a_centre, &b_centre
+                        );
+                    }
+                    else
+                    {
+                        area = inters_cy
+                        (
+                            cyl2.x(), cyl2.y(), 0.5*cyl2.dia(),
+                            blk1.x(), max1.x(),
+                            blk1.y(), max1.y(),
+                            &dummy, &dummy, &dummy, &dummy, &dummy
+                        );
+                        b_lblk = l_blockage
+                        (
+                            cyl2.x(), cyl2.y(), 0.5*cyl2.dia(),
+                            blk1.x(), max1.x(),
+                            blk1.y(), max1.y(),
+                            &dummy, &dummy, &b_centre
+                        );
+                        a_lblk = l_blockage
+                        (
+                            cyl2.y(), cyl2.x(), 0.5*cyl2.dia(),
+                            blk1.y(), max1.y(),
+                            blk1.x(), max1.x(),
+                            &dummy, &dummy, &a_centre
+                        );
+                    }
+                    if (equal(area, 0)) continue;
+                    assert(a_lblk >0.0);
+                    assert(b_lblk >0.0);
+
+                    // The intersection between a circle and a rectangle  can be an odd shape.
+                    // We have its area. a_lblk and b_lblk are dimensions of enclosing rectangle
+                    // and a_centre and b_centre its centre. We scale this rectangle down to
+                    // the corect areacorrect area, as a rectangular approximation to the intersection.
+                    const scalar ratio = std::sqrt( area / a_lblk / b_lblk );
+
+                    a_lblk *= blk1.span.x() * ratio;
+                    b_lblk *= blk1.span.y() * ratio;
+                    assert(b_lblk >0.0);
+                    assert(a_lblk >0.0);
+
+                    over.x() = a_centre - 0.5 * a_lblk;
+                    over.y() = b_centre - 0.5 * b_lblk;
+                    over.z() = max(blk1.z(), cyl2.z());
+
+                    over.span.x() = a_lblk;
+                    over.span.y() = b_lblk;
+                    over.span.z() = min(max1.z(), cyl2.z() + cyl2.len()) - over.z();
+                    assert(over.x() > -200.0);
+                    assert(over.x() < 2000.0);
+                }
+                break;
+
+                case vector::Y:
+                {
+                    const scalar ym2 = cyl2.y() + cyl2.len();
+                    if (blk1.y() > ym2 || cyl2.y() > max1.y()) continue;
+
+                    if ( cyl2.dia() == 0.0 )
+                    {
+                        area = inters_db
+                        (
+                            cyl2.z(), cyl2.x(), cyl2.theta(), cyl2.wa, cyl2.wb,
+                            blk1.z(), max1.z(),
+                            blk1.x(), max1.x(),
+                            &dummy, dum2, &dummy, &a_lblk, &b_lblk,
+                            &a_centre, &b_centre
+                        );
+                    }
+                    else
+                    {
+                        area = inters_cy
+                        (
+                            cyl2.z(), cyl2.x(), 0.5*cyl2.dia(),
+                            blk1.z(), max1.z(),
+                            blk1.x(), max1.x(),
+                            &dummy, &dummy, &dummy, &dummy, &dummy
+                        );
+
+                        b_lblk = l_blockage
+                        (
+                            cyl2.z(), cyl2.x(), 0.5*cyl2.dia(),
+                            blk1.z(), max1.z(),
+                            blk1.x(), max1.x(),
+                            &dummy, &dummy, &b_centre
+                        );
+
+                        a_lblk = l_blockage
+                        (
+                            cyl2.x(), cyl2.z(), 0.5*cyl2.dia(),
+                            blk1.x(), max1.x(),
+                            blk1.z(), max1.z(),
+                            &dummy, &dummy, &a_centre
+                        );
+                    }
+
+                    if (equal(area, 0)) continue;
+                    assert(a_lblk >0.0);
+                    assert(b_lblk >0.0);
+
+                    // a_lblk and b_lblk are dimensions of enclosing rectangle.
+                    // Need to scale to correct area
+                    const scalar ratio = std::sqrt( area / a_lblk / b_lblk );
+                    a_lblk *= blk1.span.z() * ratio;
+                    b_lblk *= blk1.span.x() * ratio;
+
+                    over.z() = a_centre - a_lblk * 0.5;
+                    over.x() = b_centre - b_lblk * 0.5;
+                    over.y() = max(blk1.y(), cyl2.y());
+
+                    over.span.z() = a_lblk;
+                    over.span.x() = b_lblk;
+                    over.span.y() = min(max1.y(), cyl2.y() + cyl2.len()) - over.y();
+                }
+                break;
+
+                case vector::X:
+                {
+                    const scalar xm2 = cyl2.x() + cyl2.len();
+                    if (blk1.x() > xm2 || cyl2.x() > max1.x()) continue;
+
+                    if ( cyl2.dia() == 0.0 )
+                    {
+                        area = inters_db
+                        (
+                            cyl2.y(), cyl2.z(), cyl2.theta(), cyl2.wa, cyl2.wb,
+                            blk1.y(), max1.y(),
+                            blk1.z(), max1.z(),
+                            &dummy, dum2, &dummy, &a_lblk, &b_lblk,
+                            &a_centre, &b_centre
+                        );
+                    }
+                    else
+                    {
+                        area = inters_cy
+                        (
+                            cyl2.y(), cyl2.z(), 0.5*cyl2.dia(),
+                            blk1.y(), max1.y(),
+                            blk1.z(), max1.z(),
+                            &dummy, &dummy, &dummy, &dummy, &dummy
+                        );
+
+                        b_lblk = l_blockage
+                        (
+                            cyl2.y(), cyl2.z(), 0.5*cyl2.dia(),
+                            blk1.y(), max1.y(),
+                            blk1.z(), max1.z(),
+                            &dummy, &dummy, &b_centre
+                        );
+
+                        a_lblk = l_blockage
+                        (
+                            cyl2.z(), cyl2.y(), 0.5*cyl2.dia(),
+                            blk1.z(), max1.z(),
+                            blk1.y(), max1.y(),
+                            &dummy, &dummy, &a_centre
+                        );
+
+                    }
+
+                    if (equal(area, 0)) continue;
+                    assert(a_lblk >0.0);
+                    assert(b_lblk >0.0);
+
+                    // a_lblk and b_lblk are dimensions of enclosing rectangle.
+                    // Need to scale to correct area
+                    const scalar ratio = std::sqrt( area / a_lblk / b_lblk );
+                    assert(ratio >-10000.0);
+                    assert(ratio <10000.0);
+                    a_lblk *= blk1.span.y() * ratio;
+                    b_lblk *= blk1.span.z() * ratio;
+
+                    over.y() = a_centre - a_lblk * 0.5;
+                    over.z() = b_centre - b_lblk * 0.5;
+                    over.x() = max(blk1.x(), cyl2.x());
+
+                    over.span.y() = a_lblk;
+                    over.span.z() = b_lblk;
+                    over.span.x() = min(max1.x(), cyl2.x() + cyl2.len()) - over.x();
+                }
+                break;
+            }
+            over.vbkge = over.xbkge = over.ybkge = over.zbkge = -1.0;
+            over.typeId = PDRobstacle::IGNORE;
+
+            assert(cmptProduct(over.span) > 0.0);
+            assert(b_lblk >0.0);
+            assert(a_lblk >0.0);
+            assert(over.x() > -10000.0);
+
+            if (obsHasMinSize(over.span, pars))
+            {
+                // Obstacle satisfies some minimum size checks
+                totVolume -= over.volume();
+
+                newBlocks.append(over);
+            }
+        }
+    }
+
+    blocks.append(std::move(newBlocks));
+
+    return totVolume;
+}
+
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/obstacles/ObstaclesDict b/applications/utilities/preProcessing/PDRsetFields/obstacles/ObstaclesDict
new file mode 100644
index 00000000000..84b5a97cf9a
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/obstacles/ObstaclesDict
@@ -0,0 +1,179 @@
+/*--------------------------------*- C++ -*----------------------------------*\
+| =========                 |                                                 |
+| \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox           |
+|  \\    /   O peration     | Version:  v1812                                 |
+|   \\  /    A nd           | Web:      www.OpenFOAM.com                      |
+|    \\/     M anipulation  |                                                 |
+\*---------------------------------------------------------------------------*/
+FoamFile
+{
+    version     2.0;
+    format      ascii;
+    class       dictionary;
+    object      obstaclesDict;
+}
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+group1
+{
+    positions
+    (
+        (0 0 0)
+    );
+
+    obstacles
+    (
+        box { point (0 0 0); span (0.05 0.05 2); porosity 0; }
+        box { point (1 0 0); span (0.05 0.05 2); porosity 0; }
+        box { point (1 0 0); span (0.05 0.05 2); porosity 0; }
+        box { point (2 0 0); span (0.05 0.05 2); porosity 0; }
+        box { point (3 0 0); span (0.05 0.05 2); porosity 0; }
+        box { point (0 1 0); span (0.05 0.05 2); porosity 0; }
+        box { point (1 1 0); span (0.05 0.05 2); porosity 0; }
+        box { point (2 1 0); span (0.05 0.05 2); porosity 0; }
+        box { point (3 1 0); span (0.05 0.05 2); porosity 0; }
+        box { point (0 2 0); span (0.05 0.05 2); porosity 0; }
+        box { point (1 2 0); span (0.05 0.05 2); porosity 0; }
+        box { point (2 2 0); span (0.05 0.05 2); porosity 0; }
+        box { point (3 2 0); span (0.05 0.05 2); porosity 0; }
+        box { point (0 3 0); span (0.05 0.05 2); porosity 0; }
+        box { point (1 3 0); span (0.05 0.05 2); porosity 0; }
+        box { point (2 3 0); span (0.05 0.05 2); porosity 0; }
+        box { point (3 3 0); span (0.05 0.05 2); porosity 0; }
+        box { point (0 0 0); span (0.05 3.05 0.05); }
+        box { point (1 0 0); span (0.05 3.05 0.05); }
+        box { point (2 0 0); span (0.05 3.05 0.05); }
+        box { point (3 0 0); span (0.05 3.05 0.05); }
+        box { point (0 0 1); span (0.05 3.05 0.05); }
+        box { point (1 0 1); span (0.05 3.05 0.05); }
+        box { point (2 0 1); span (0.05 3.05 0.05); }
+        box { point (3 0 1); span (0.05 3.05 0.05); }
+        box { point (0 0 2); span (0.05 3.05 0.05); }
+        box { point (1 0 2); span (0.05 3.05 0.05); }
+        box { point (2 0 2); span (0.05 3.05 0.05); }
+        box { point (3 0 2); span (0.05 3.05 0.05); }
+        box { point (0 0 0); span (3.05 0.05 0.05); }
+        box { point (0 1 0); span (3.05 0.05 0.05); }
+        box { point (0 2 0); span (3.05 0.05 0.05); }
+        box { point (0 3 0); span (3.05 0.05 0.05); }
+        box { point (0 0 1); span (3.05 0.05 0.05); }
+        box { point (0 1 1); span (3.05 0.05 0.05); }
+        box { point (0 2 1); span (3.05 0.05 0.05); }
+        box { point (0 3 1); span (3.05 0.05 0.05); }
+        box { point (0 0 2); span (3.05 0.05 0.05); }
+        box { point (0 1 2); span (3.05 0.05 0.05); }
+        box { point (0 2 2); span (3.05 0.05 0.05); }
+        box { point (0 3 2); span (3.05 0.05 0.05); }
+
+        patch { point (1 1 1); span (2 2 2); inlet x; outlet y; name xpatch; }
+    );
+}
+
+group11
+{
+    positions
+    (
+        (0 0 0)
+    );
+
+}
+
+group14
+{
+    positions
+    (
+        (0 0 0.15)
+        (0 0 0.45)
+    );
+
+    obstacles
+    (
+        box { point (0.05   0  1.05); span(0.006 3.05 0.05); }
+        box { point (0.997  0  1.05); span(0.006 3.05 0.05); }
+        box { point (1.05   0  1.05); span(0.006 3.05 0.05); }
+        box { point (1.997  0  1.05); span(0.006 3.05 0.05); }
+        box { point (2.05   0  1.05); span(0.006 3.05 0.05); }
+        box { point (2.997  0  1.05); span(0.006 3.05 0.05); }
+
+        cyl { point (0.05  0.025  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (0.05  0.275  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (0.05  0.525  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (0.05  0.775  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (0.05  1.025  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (0.05  1.275  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (0.05  1.525  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (0.05  1.775  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (0.05  2.025  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (0.05  2.275  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (0.05  2.525  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (0.05  2.775  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (0.05  3.025  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (1.05  0.025  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (1.05  0.275  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (1.05  0.525  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (1.05  0.775  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (1.05  1.025  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (1.05  1.275  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (1.05  1.525  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (1.05  1.775  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (1.05  2.025  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (1.05  2.275  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (1.05  2.525  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (1.05  2.775  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (1.05  3.025  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (2.05  0.025  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (2.05  0.275  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (2.05  0.525  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (2.05  0.775  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (2.05  1.025  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (2.05  1.275  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (2.05  1.525  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (2.05  1.775  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (2.05  2.025  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (2.05  2.275  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (2.05  2.525  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (2.05  2.775  1.075); length 0.947; diameter 0.026; direction x; }
+        cyl { point (2.05  3.025  1.075); length 0.947; diameter 0.026; direction x; }
+    );
+}
+
+
+group17
+{
+    positions
+    (
+        (0  0  0)
+        (0  0  0.30)
+    );
+
+}
+
+group21
+{
+    positions
+    (
+        (0  0  0)
+    );
+
+}
+
+group31
+{
+    positions
+    (
+        (0  0  0)
+    );
+
+}
+
+group41
+{
+    positions
+    (
+        (0  0  0)
+    );
+
+}
+
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacle.C b/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacle.C
new file mode 100644
index 00000000000..91ccc58d14f
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacle.C
@@ -0,0 +1,737 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2019 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 "PDRobstacle.H"
+#include "boundBox.H"
+#include "meshedSurf.H"
+#include "axisAngleRotation.H"
+#include "coordinateSystem.H"
+#include "foamVtkSurfaceWriter.H"
+#include "unitConversion.H"
+#include "addToMemberFunctionSelectionTable.H"
+
+using namespace Foam::constant;
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+namespace Foam
+{
+    defineMemberFunctionSelectionTable(PDRobstacle, read, dictRead);
+}
+
+
+// * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
+
+Foam::PDRobstacle::PDRobstacle()
+:
+    groupId(0),
+    typeId(0),
+    orient(vector::X),
+    sortBias(0),
+    pt(Zero),
+    span(Zero),
+    wa(0),
+    wb(0),
+    vbkge(0),
+    xbkge(0),
+    ybkge(0),
+    zbkge(0),
+    blowoff_type(0),
+    identifier()
+{}
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+void Foam::PDRobstacle::clear()
+{
+    groupId = 0;
+    typeId = 0;
+    orient = vector::X;
+    sortBias = 0;
+    pt = Zero;
+    span = Zero;
+    wa = 0;
+    wb = 0;
+    vbkge = 0;
+    xbkge = 0;
+    ybkge = 0;
+    zbkge = 0;
+    blowoff_type = 0;
+    identifier.clear();
+}
+
+
+void Foam::PDRobstacle::readProperties(const dictionary& dict)
+{
+    PDRobstacle::clear();
+
+    // Read as word, which handles quoted or unquoted entries
+    word obsName;
+
+    if (dict.readIfPresent("name", obsName))
+    {
+        identifier = std::move(obsName);
+    }
+}
+
+
+void Foam::PDRobstacle::scale(const scalar factor)
+{
+    if (factor <= 0)
+    {
+        return;
+    }
+
+    sortBias *= factor;
+
+    switch (typeId)
+    {
+        case PDRobstacle::CYLINDER:
+        {
+            pt *= factor;
+
+            dia() *= factor;
+            len() *= factor;
+            break;
+        }
+
+        case PDRobstacle::DIAG_BEAM:
+        {
+            pt *= factor;
+
+            len() *= factor;
+            wa *= factor;
+            wb *= factor;
+            break;
+        }
+
+        case PDRobstacle::CUBOID_1:
+        case PDRobstacle::LOUVRE_BLOWOFF:
+        case PDRobstacle::CUBOID:
+        case PDRobstacle::WALL_BEAM:
+        case PDRobstacle::GRATING:
+        case PDRobstacle::RECT_PATCH:
+        {
+            pt *= factor;
+            span *= factor;
+
+            if (typeId == PDRobstacle::GRATING)
+            {
+                slat_width *= factor;
+            }
+            break;
+        }
+    }
+}
+
+
+Foam::scalar Foam::PDRobstacle::volume() const
+{
+    scalar vol = 0;
+
+    switch (typeId)
+    {
+        case PDRobstacle::CYLINDER:
+            vol = 0.25 * mathematical::pi * sqr(dia()) * len();
+            break;
+
+        case PDRobstacle::DIAG_BEAM:
+            vol = wa * wb * len();
+            break;
+
+        case PDRobstacle::CUBOID_1:
+        case PDRobstacle::LOUVRE_BLOWOFF:
+        case PDRobstacle::CUBOID:
+        case PDRobstacle::WALL_BEAM:
+        case PDRobstacle::GRATING:
+            vol = cmptProduct(span) * vbkge;
+            break;
+    }
+
+    return vol;
+}
+
+
+bool Foam::PDRobstacle::tooSmall(const scalar minWidth) const
+{
+    if (minWidth <= 0)
+    {
+        return false;
+    }
+
+    switch (typeId)
+    {
+        case PDRobstacle::CYLINDER:
+        {
+            // The effective half-width
+            if ((0.25 * dia() * sqrt(mathematical::pi)) <= minWidth)
+            {
+                return true;
+            }
+            break;
+        }
+
+        case PDRobstacle::DIAG_BEAM:
+        {
+            if
+            (
+                (len() <= minWidth && wa <= minWidth)
+             || (len() <= minWidth && wb <= minWidth)
+             || (wa  <= minWidth && wb <= minWidth)
+            )
+            {
+                return true;
+            }
+            break;
+        }
+
+        case PDRobstacle::CUBOID_1:
+        case PDRobstacle::LOUVRE_BLOWOFF:
+        case PDRobstacle::CUBOID:
+        case PDRobstacle::WALL_BEAM:
+        case PDRobstacle::GRATING:
+        case PDRobstacle::RECT_PATCH:
+        {
+            if
+            (
+                (span.x() <= minWidth && span.y() <= minWidth)
+             || (span.y() <= minWidth && span.z() <= minWidth)
+             || (span.z() <= minWidth && span.x() <= minWidth)
+            )
+            {
+                return true;
+            }
+
+            break;
+        }
+    }
+
+    return false;
+}
+
+
+Foam::volumeType Foam::PDRobstacle::trim(const boundBox& bb)
+{
+    volumeType::type vt = volumeType::UNKNOWN;
+
+    if (!bb.valid() || !typeId)
+    {
+        return vt;
+    }
+
+    switch (typeId)
+    {
+        case PDRobstacle::CYLINDER:
+        {
+            const scalar rad = 0.5*dia();
+
+            direction e1 = vector::X;
+            direction e2 = vector::Y;
+            direction e3 = vector::Z;
+
+            if (orient == vector::X)
+            {
+                e1 = vector::Y;
+                e2 = vector::Z;
+                e3 = vector::X;
+            }
+            else if (orient == vector::Y)
+            {
+                e1 = vector::Z;
+                e2 = vector::X;
+                e3 = vector::Y;
+            }
+            else
+            {
+                orient = vector::Z;  // extra safety?
+            }
+
+            if
+            (
+                (pt[e1] + rad <= bb.min()[e1])
+             || (pt[e2] + rad <= bb.min()[e2])
+             || (pt[e3] + len() <= bb.min()[e3])
+             || (pt[e1] - rad >= bb.max()[e1])
+             || (pt[e2] - rad >= bb.max()[e2])
+             || (pt[e3] >= bb.max()[e3])
+            )
+            {
+                // No overlap
+                return volumeType::OUTSIDE;
+            }
+
+            vt = volumeType::INSIDE;
+
+            // Trim obstacle length as required
+            if (pt[e3] < bb.min()[e3])
+            {
+                vt = volumeType::MIXED;
+                len() -= bb.min()[e3] - pt[e3];
+                pt[e3] = bb.min()[e3];
+            }
+
+            if (pt[e3] + len() > bb.max()[e3])
+            {
+                vt = volumeType::MIXED;
+                len() = bb.max()[e3] - pt[e3];
+            }
+
+            // Cannot trim diameter very well, so just mark as protruding
+            if
+            (
+                (pt[e1] - rad < bb.min()[e1]) || (pt[e1] + rad > bb.max()[e1])
+             || (pt[e2] - rad < bb.min()[e2]) || (pt[e2] + rad > bb.max()[e2])
+            )
+            {
+                vt = volumeType::MIXED;
+            }
+
+            break;
+        }
+
+
+        case PDRobstacle::DIAG_BEAM:
+        {
+            // Not implemented
+            break;
+        }
+
+
+        case PDRobstacle::CUBOID_1:
+        case PDRobstacle::LOUVRE_BLOWOFF:
+        case PDRobstacle::CUBOID:
+        case PDRobstacle::WALL_BEAM:
+        case PDRobstacle::GRATING:
+        case PDRobstacle::RECT_PATCH:
+        {
+            for (direction cmpt=0; cmpt < vector::nComponents; ++cmpt)
+            {
+                if
+                (
+                    ((pt[cmpt] + span[cmpt]) < bb.min()[cmpt])
+                 || (pt[cmpt] > bb.max()[cmpt])
+                )
+                {
+                    // No overlap
+                    return volumeType::OUTSIDE;
+                }
+            }
+
+
+            vt = volumeType::INSIDE;
+
+            // Trim obstacle as required
+
+            for (direction cmpt=0; cmpt < vector::nComponents; ++cmpt)
+            {
+                if (pt[cmpt] < bb.min()[cmpt])
+                {
+                    vt = volumeType::MIXED;
+                    if (span[cmpt] > 0)
+                    {
+                        span[cmpt] -= bb.min()[cmpt] - pt[cmpt];
+                    }
+                    pt[cmpt] = bb.min()[cmpt];
+                }
+
+
+                if (pt[cmpt] + span[cmpt] > bb.max()[cmpt])
+                {
+                    vt = volumeType::MIXED;
+                    span[cmpt] -= bb.max()[cmpt] - pt[cmpt];
+                }
+            }
+
+            break;
+        }
+    }
+
+    return vt;
+}
+
+
+Foam::meshedSurface Foam::PDRobstacle::surface() const
+{
+    meshedSurface surf;
+
+    const PDRobstacle& obs = *this;
+
+    switch (obs.typeId)
+    {
+        case PDRobstacle::CUBOID_1 :
+        case PDRobstacle::CUBOID :
+        {
+            boundBox box(obs.pt, obs.pt + obs.span);
+
+            pointField pts(box.points());
+            faceList fcs(boundBox::faces);
+
+            surf.transfer(pts, fcs);
+
+            break;
+        }
+
+        case PDRobstacle::DIAG_BEAM :
+        {
+            boundBox box(Zero);
+
+            switch (orient)
+            {
+                case vector::X:
+                {
+                    box.min() = vector(0, -0.5*obs.wa, -0.5*obs.wb);
+                    box.max() = vector(obs.len(), 0.5*obs.wa,  0.5*obs.wb);
+                    break;
+                }
+
+                case vector::Y:
+                {
+                    box.min() = vector(-0.5*obs.wb, 0, -0.5*obs.wa);
+                    box.max() = vector(0.5*obs.wb, obs.len(), 0.5*obs.wa);
+                    break;
+                }
+
+                case vector::Z:
+                {
+                    box.min() = vector(-0.5*obs.wa, -0.5*obs.wb, 0);
+                    box.max() = vector(0.5*obs.wa,  0.5*obs.wb, obs.len());
+                    break;
+                }
+            }
+
+            coordinateSystem cs
+            (
+                obs.pt,
+                coordinateRotations::axisAngle
+                (
+                    vector::components(obs.orient),
+                    obs.theta(),
+                    false
+                )
+            );
+
+            pointField pts0(box.points());
+            faceList fcs(boundBox::faces);
+
+            pointField pts(cs.globalPosition(pts0));
+
+            surf.transfer(pts, fcs);
+
+            break;
+        }
+
+        case PDRobstacle::CYLINDER :
+        {
+            // Tessellation 12 looks fairly reasonable
+
+            constexpr int nDiv = 12;
+
+            point org(obs.pt);
+
+            direction e1 = vector::X;
+            direction e2 = vector::Y;
+            direction e3 = vector::Z;
+
+            if (obs.orient == vector::X)
+            {
+                e1 = vector::Y;
+                e2 = vector::Z;
+                e3 = vector::X;
+            }
+            else if (obs.orient == vector::Y)
+            {
+                e1 = vector::Z;
+                e2 = vector::X;
+                e3 = vector::Y;
+            }
+
+            pointField pts(2*nDiv, org);
+            faceList   fcs(2 + nDiv);
+
+            // Origin for back
+            org[e3] += obs.len();
+            SubList<point>(pts, nDiv, nDiv) = org;
+
+            const scalar radius = 0.5*obs.dia();
+
+            for (label i=0; i < nDiv; ++i)
+            {
+                const scalar angle = (i * mathematical::twoPi) / nDiv;
+                const scalar s = radius * sin(angle);
+                const scalar c = radius * cos(angle);
+
+                pts[i][e1] += s;
+                pts[i][e2] += c;
+
+                pts[nDiv+i][e1] += s;
+                pts[nDiv+i][e2] += c;
+            }
+
+            // Side-faces
+            for (label facei=0; facei < nDiv; ++facei)
+            {
+                face& f = fcs[facei];
+                f.resize(4);
+
+                f[0] = facei;
+                f[3] = (facei + 1) % nDiv;
+                f[1] = f[0] + nDiv;
+                f[2] = f[3] + nDiv;
+            }
+
+            {
+                // Front face
+                face& f1 = fcs[nDiv];
+                f1.resize(nDiv);
+
+                f1[0] = 0;
+                for (label pti=1; pti < nDiv; ++pti)
+                {
+                    f1[pti] = nDiv-pti;
+                }
+
+                // Back face
+                labelList& f2 = fcs[nDiv+1];
+                f2 = identity(nDiv, nDiv);
+            }
+
+            surf.transfer(pts, fcs);
+
+            break;
+        }
+
+        case PDRobstacle::RECT_PATCH :
+        {
+            pointField pts(4, obs.span);
+            pts[0] = Zero;
+
+            switch (obs.inlet_dirn)
+            {
+                case -1:
+                case 1:
+                {
+                    for (auto& p : pts)
+                    {
+                        p.x() = 0;
+                    }
+
+                    pts[1].z() = 0;
+                    pts[3].y() = 0;
+                    break;
+                }
+                case -2:
+                case 2:
+                {
+                    for (auto& p : pts)
+                    {
+                        p.y() = 0;
+                    }
+
+                    pts[1].x() = 0;
+                    pts[3].z() = 0;
+                    break;
+                }
+                default:
+                {
+                    for (auto& p : pts)
+                    {
+                        p.z() = 0;
+                    }
+
+                    pts[1].y() = 0;
+                    pts[3].x() = 0;
+                    break;
+                }
+            }
+
+            // pts += obs.pt;
+
+            faceList fcs(one(), face(identity(4)));
+
+            surf.transfer(pts, fcs);
+
+            break;
+        }
+
+        default:
+            break;
+
+//         LOUVRE_BLOWOFF = 5,
+//         WALL_BEAM =  7,
+//         GRATING   =  8,
+//         CIRC_PATCH  = 12,
+//         MESH_PLANE  = 46,
+    }
+
+    return surf;
+}
+
+
+Foam::label Foam::PDRobstacle::addPieces
+(
+    vtk::surfaceWriter& surfWriter,
+    const UList<PDRobstacle>& list,
+    label pieceId
+)
+{
+    for (const PDRobstacle& obs : list)
+    {
+        meshedSurface surf(obs.surface());
+
+        if (!surf.empty())
+        {
+            surfWriter.piece(surf.points(), surf.surfFaces());
+
+            surfWriter.writeGeometry();
+            surfWriter.beginCellData(2);
+            surfWriter.writeUniform("group", label(obs.groupId));
+            surfWriter.writeUniform("type", label(obs.typeId));
+            surfWriter.writeUniform("obstacle", pieceId);
+            ++pieceId;
+        }
+    }
+
+    return pieceId;
+}
+
+
+void Foam::PDRobstacle::generateVtk
+(
+    const fileName& outputDir,
+    const UList<PDRobstacle>& obslist,
+    const UList<PDRobstacle>& cyllist
+)
+{
+    label pieceId = 0;
+
+    meshedSurf::emptySurface dummy;
+
+    vtk::surfaceWriter surfWriter
+    (
+        dummy.points(),
+        dummy.faces(),
+        // vtk::formatType::INLINE_ASCII,
+        (outputDir / "Obstacles"),
+        false  // serial only
+    );
+
+    pieceId = addPieces(surfWriter, obslist, pieceId);
+    pieceId = addPieces(surfWriter, cyllist, pieceId);
+
+    Info<< "Wrote " << pieceId << " obstacles (VTK) to "
+        << outputDir/"Obstacles" << nl;
+}
+
+
+// * * * * * * * * * * * * * * * IOstream Operators  * * * * * * * * * * * * //
+
+Foam::Ostream& Foam::operator<<
+(
+    Ostream& os,
+    const InfoProxy<PDRobstacle>& iproxy
+)
+{
+    const PDRobstacle& obs = iproxy.t_;
+
+    switch (obs.typeId)
+    {
+        case PDRobstacle::CUBOID_1 :
+        case PDRobstacle::CUBOID :
+            os  << "box  { point " << obs.pt
+                << "; size " << obs.span
+                << "; }";
+            break;
+
+        case PDRobstacle::CYLINDER :
+            os  << "cyl { point " << obs.pt
+                << "; length " << obs.len() << "; diameter " << obs.dia()
+                << "; direction " << vector::componentNames[obs.orient]
+                << "; }";
+            break;
+
+        case PDRobstacle::DIAG_BEAM :
+            os  << "diag { point " << obs.pt
+                << "; length " << obs.len()
+                << "; width (" << obs.wa << ' ' << obs.wb << ')'
+                << "; angle " << radToDeg(obs.theta())
+                << "; direction " << vector::componentNames[obs.orient]
+                << "; }";
+            break;
+
+        case PDRobstacle::WALL_BEAM :
+            os  << "wallbeam { point " << obs.pt
+                << " size " << obs.span
+                << "; }";
+            break;
+
+        case PDRobstacle::GRATING :
+            os  << "grate { point " << obs.pt
+                << "; size " << obs.span
+                << "; slats " << obs.slat_width
+                << "; }";
+            break;
+
+        case PDRobstacle::LOUVER_BLOWOFF :
+            os  << "louver { point " << obs.pt
+                << "; size " << obs.span
+                << "; pressure " << paToBar(obs.blowoff_press)
+                << "; }";
+            break;
+
+        case PDRobstacle::RECT_PATCH :
+            os  << "patch { " << obs.pt
+                << "; size " << obs.span
+                << "; name " << obs.identifier
+                << "; }";
+            break;
+
+        case PDRobstacle::OLD_INLET :
+        case PDRobstacle::OLD_BLOWOFF :
+        case PDRobstacle::IGNITION :
+            os  << "/* ignored: " << obs.typeId << " */";
+            break;
+
+        default:
+            os  << "/* unknown: " << obs.typeId << " */";
+            break;
+
+    }
+
+    return os;
+}
+
+
+// * * * * * * * * * * * * * * * Global Operators  * * * * * * * * * * * * * //
+
+bool Foam::operator<(const PDRobstacle& a, const PDRobstacle& b)
+{
+    return (a.pt.x() + a.sortBias) < (b.pt.x() + b.sortBias);
+}
+
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacle.H b/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacle.H
new file mode 100644
index 00000000000..0580a8015f1
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacle.H
@@ -0,0 +1,477 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2016 Shell Research Ltd.
+    Copyright (C) 2019 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::PDRobstacle
+
+Description
+    Obstacle definitions for PDR
+
+SourceFiles
+    PDRobstacle.C
+    PDRobstacleIO.C
+    PDRobstacleRead.C
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef PDRobstacle_H
+#define PDRobstacle_H
+
+#include "InfoProxy.H"
+#include "labelPair.H"
+#include "MeshedSurface.H"
+#include "MeshedSurfacesFwd.H"
+#include "boundBox.H"
+#include "DynamicList.H"
+#include "pointField.H"
+#include "volumeType.H"
+#include "memberFunctionSelectionTables.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+// Forward Declarations
+class boundBox;
+class PDRobstacle;
+
+Istream& operator>>(Istream& is, PDRobstacle& obs);
+Ostream& operator<<(Ostream& os, const InfoProxy<PDRobstacle>& info);
+
+namespace vtk
+{
+    class surfaceWriter;
+}
+
+
+/*---------------------------------------------------------------------------*\
+                         Class PDRobstacle Declaration
+\*---------------------------------------------------------------------------*/
+
+class PDRobstacle
+{
+public:
+
+    //- Obstacle types (legacy numbering)
+    enum legacyTypes
+    {
+        NONE =  0,          //!< Placeholder
+        CUBOID_1  =  1,
+        CYLINDER  =  2,
+        LOUVER_BLOWOFF = 5,
+        LOUVRE_BLOWOFF = 5,
+        CUBOID    =  6,
+        WALL_BEAM =  7,
+        GRATING   =  8,
+        OLD_INLET =  9,     //!< ignored (old)
+        OLD_BLOWOFF = 10,   //!< ignored (old)
+        CIRC_PATCH  = 12,
+        RECT_PATCH  = 16,
+        DIAG_BEAM   = 22,
+        IGNITION    = 41,   //!< ignored (old)
+        MESH_PLANE  = 46,
+        IGNORE = 200
+    };
+
+
+    // Static Data Members
+
+        //- The max blowoff pressure [bar]
+        //  Primarily to catch accidental input in Pa or mbar
+        static constexpr int maxBlowoffPressure = 10;
+
+
+    // Data Members
+
+        //- The group-id
+        label groupId;
+
+        //- The obstacle type-id
+        int typeId;
+
+        //- The x/y/z orientation (0,1,2)
+        direction orient;
+
+        //- Bias for position sorting
+        scalar sortBias;
+
+        //- The obstacle location.
+        //  Lower corner for boxes, end-centre for cylinders
+        point pt;
+
+        //- The obstacle dimensions (for boxes)
+        vector span;
+
+        // Accessors for cylinders and diagonal blocks
+
+        inline scalar dia()   const { return span[vector::X]; }
+        inline scalar theta() const { return span[vector::Y]; }
+        inline scalar len()   const { return span[vector::Z]; }
+
+        inline scalar& dia()   { return span[vector::X]; }
+        inline scalar& theta() { return span[vector::Y]; }
+        inline scalar& len()   { return span[vector::Z]; }
+
+        union
+        {
+            scalar wa;
+            scalar slat_width;
+            scalar blowoff_press;
+        };
+        union
+        {
+            scalar wb;
+            scalar blowoff_time;
+        };
+        scalar vbkge;
+        scalar xbkge;
+        scalar ybkge;
+        scalar zbkge;
+
+        union
+        {
+            int blowoff_type;
+            int inlet_dirn;
+        };
+
+        string identifier;
+
+public:
+
+    // Constructors
+
+        //- Construct zero-initialized
+        PDRobstacle();
+
+        //- Read construct as named dictionary
+        explicit PDRobstacle(Istream& is);
+
+
+    // Member Function Selectors
+
+        declareMemberFunctionSelectionTable
+        (
+            void,
+            PDRobstacle,
+            read,
+            dictRead,
+            (
+                PDRobstacle& obs,
+                const dictionary& dict
+            ),
+            (obs, dict)
+        );
+
+
+    // Static Member Functions
+
+        //- Read obstacle files and add to the lists
+        //  \return the total volume
+        static scalar legacyReadFiles
+        (
+            const fileName& obsFileDir,
+            const wordList& obsFileNames,
+            const boundBox& meshBb,
+            // output
+            DynamicList<PDRobstacle>& blocks,
+            DynamicList<PDRobstacle>& cylinders
+        );
+
+        //- Read obstacle files and set the lists
+        //  \return the total volume
+        static scalar readFiles
+        (
+            const fileName& obsFileDir,
+            const wordList& obsFileNames,
+            const boundBox& meshBb,
+            // output
+            DynamicList<PDRobstacle>& blocks,
+            DynamicList<PDRobstacle>& cylinders
+        );
+
+
+    // Member Functions
+
+        //- Read name / dictionary
+        bool read(Istream& is);
+
+        //- Read the 'name' identifier if present
+        void readProperties(const dictionary& dict);
+
+        //- Obstacle position accessors
+        inline scalar x() const { return pt.x(); }
+        inline scalar y() const { return pt.y(); }
+        inline scalar z() const { return pt.z(); }
+        inline scalar& x() { return pt.x(); }
+        inline scalar& y() { return pt.y(); }
+        inline scalar& z() { return pt.z(); }
+
+
+        //- Is obstacle type id cylinder-like?
+        inline static bool isCylinder(const label id);
+
+        //- Is obstacle cylinder-like?
+        inline bool isCylinder() const;
+
+        //- Reset to a zero obstacle
+        void clear();
+
+        //- Scale obstacle dimensions by specified scaling factor
+        //  Zero and negative factors are ignored
+        void scale(const scalar factor);
+
+        //- Volume of the obstacle
+        scalar volume() const;
+
+        //- True if the obstacle is considered to be too small
+        bool tooSmall(const scalar minWidth) const;
+
+        //- Set values from single-line, multi-column format.
+        //  The only input format, but termed 'legacy' since it may
+        //  be replaced in the near future.
+        //  \return false if the scanning failed or if the obstacle type
+        //      is not supported (or no longer supported)
+        bool setFromLegacy
+        (
+            const int groupTypeId,
+            const string& buffer,
+            const label lineNo = -1,
+            const word& inputFile = word::null
+        );
+
+        //- Trim obstacle to ensure it is within the specified bounding box
+        volumeType trim(const boundBox& bb);
+
+        //- Surface (points, faces) representation
+        meshedSurface surface() const;
+
+        //- Add pieces to vtp output
+        static label addPieces
+        (
+            vtk::surfaceWriter& surfWriter,
+            const UList<PDRobstacle>& list,
+            label pieceId = 0
+        );
+
+        //- Generate multi-piece VTK (vtp) file of obstacles
+        static void generateVtk
+        (
+            const fileName& outputDir,
+            const UList<PDRobstacle>& obslist,
+            const UList<PDRobstacle>& cyllist
+        );
+
+
+    // IOstream Operators
+
+        //- Return info proxy.
+        InfoProxy<PDRobstacle> info() const
+        {
+            return *this;
+        }
+
+        friend Istream& operator>>(Istream& is, PDRobstacle& obs);
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+// Global Operators
+
+//- Compare according to x0 position
+bool operator<(const PDRobstacle& a, const PDRobstacle& b);
+
+//- For list output, assert that no obstacles are identical
+inline bool operator!=(const PDRobstacle& a, const PDRobstacle& b)
+{
+    return true;
+}
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace PDRlegacy
+{
+
+/*---------------------------------------------------------------------------*\
+                    Class obstacleGrouping Declaration
+\*---------------------------------------------------------------------------*/
+
+//- Locations for each instance of an obstacle group.
+class obstacleGrouping
+:
+    public DynamicList<point>
+{
+    //- Number of obstacles counted
+    label nObstacle_;
+
+    //- Number of cylinder-like obstacles counted
+    label nCylinder_;
+
+
+public:
+
+    //- Construct null
+    obstacleGrouping()
+    :
+        nObstacle_(0),
+        nCylinder_(0)
+    {}
+
+    //- Construct with one location (instance)
+    explicit obstacleGrouping(const vector& origin)
+    :
+        obstacleGrouping()
+    {
+        append(origin);
+    }
+
+    //- Clear obstacle count and locations
+    void clear()
+    {
+        nObstacle_ = 0;
+        nCylinder_ = 0;
+        DynamicList<point>::clear();
+    }
+
+    //- Increment the number of obstacles
+    void addObstacle()
+    {
+        ++nObstacle_;
+    }
+
+    //- Increment the number of cylinder-like obstacles
+    void addCylinder()
+    {
+        ++nCylinder_;
+    }
+
+    //- The number of obstacles
+    label nObstacle() const
+    {
+        return nObstacle_;
+    }
+
+    //- The number of cylinder-like obstacles
+    label nCylinder() const
+    {
+        return nCylinder_;
+    }
+
+    //- The number of locations x number of obstacles
+    label nTotalObstacle() const
+    {
+        return size() * nObstacle_;
+    }
+
+    //- The number of locations x number of cylinder-like obstacles
+    label nTotalCylinder() const
+    {
+        return size() * nCylinder_;
+    }
+
+    //- The number of locations x number of obstacles
+    label nTotal() const
+    {
+        return size() * (nObstacle_ + nCylinder_);
+    }
+
+    //- Add a location
+    using DynamicList<point>::append;
+
+    //- Add a location
+    void append(const scalar x, const scalar y, const scalar z)
+    {
+        append(point(x, y, z));
+    }
+};
+
+
+// Service Functions
+
+//- Read obstacle files, do counting only.
+//  \return nObstacle, nCylinder read
+labelPair readObstacleFiles
+(
+    const fileName& obsFileDir,
+    const wordList& obsFileNames,
+    Map<obstacleGrouping>& groups
+);
+
+
+//- Read obstacle files and add to the lists
+//  \return the total volume
+scalar readObstacleFiles
+(
+    const fileName& obsFileDir,
+    const wordList& obsFileNames,
+    const Map<obstacleGrouping>& groups,
+    const boundBox& meshBb,
+    // output
+    DynamicList<PDRobstacle>& blocks,
+    DynamicList<PDRobstacle>& cylinders
+);
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace PDRlegacy
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+// Global Operators
+
+//- Locations for each instance of an obstacle group.
+inline Ostream& operator<<
+(
+    Ostream& os,
+    const PDRlegacy::obstacleGrouping& group
+)
+{
+    os  << token::BEGIN_LIST
+        << group.size() << token::SPACE
+        << group.nObstacle() << token::SPACE
+        << group.nCylinder() << token::END_LIST;
+
+    return os;
+}
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#include "PDRobstacleI.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleI.H b/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleI.H
new file mode 100644
index 00000000000..954591664cc
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleI.H
@@ -0,0 +1,46 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2019 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/>.
+
+\*---------------------------------------------------------------------------*/
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+inline bool Foam::PDRobstacle::isCylinder(const label id)
+{
+    return
+    (
+        id == PDRobstacle::CYLINDER
+     || id == PDRobstacle::DIAG_BEAM
+    );
+}
+
+
+inline bool Foam::PDRobstacle::isCylinder() const
+{
+    return isCylinder(typeId);
+}
+
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleIO.C b/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleIO.C
new file mode 100644
index 00000000000..1d4c9182f86
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleIO.C
@@ -0,0 +1,353 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2019 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 "PDRsetFields.H"
+#include "PDRobstacle.H"
+#include "volumeType.H"
+
+using namespace Foam;
+using namespace Foam::constant;
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+bool Foam::PDRobstacle::read(Istream& is)
+{
+    this->clear();
+
+    const word obsType(is);
+    const dictionary dict(is);
+
+    const auto mfIter = readdictReadMemberFunctionTablePtr_->cfind(obsType);
+
+    if (!mfIter.good())
+    {
+        FatalIOErrorInFunction(is)
+            << "Unknown obstacle type: " << obsType << nl
+            << "Valid types:" << nl
+            << readdictReadMemberFunctionTablePtr_->sortedToc() << nl
+            << exit(FatalIOError);
+    }
+
+    mfIter()(*this, dict);
+
+    return true;
+}
+
+
+Foam::scalar Foam::PDRobstacle::readFiles
+(
+    const fileName& obsFileDir,
+    const wordList& obsFileNames,
+    const boundBox& meshBb,
+
+    DynamicList<PDRobstacle>& blocks,
+    DynamicList<PDRobstacle>& cylinders
+)
+{
+    blocks.clear();
+    cylinders.clear();
+
+    scalar totVolume = 0;
+    label nOutside = 0;
+    label nProtruding = 0;
+
+    scalar shift = pars.obs_expand;
+
+    if (!obsFileNames.empty())
+    {
+        Info<< "Reading obstacle files" << nl;
+    }
+
+    label maxGroup = -1;
+
+    for (const word& inputFile : obsFileNames)
+    {
+        Info<< "    file: " << inputFile << nl;
+
+        fileName path = (obsFileDir / inputFile);
+
+        IFstream is(path);
+        dictionary inputDict(is);
+
+        const scalar scaleFactor = inputDict.getOrDefault<scalar>("scale", 0);
+
+        const label verbose = inputDict.getOrDefault<label>("verbose", 0);
+
+        for (const entry& dEntry : inputDict)
+        {
+            if (!dEntry.isDict())
+            {
+                // ignore non-dictionary entry
+                continue;
+            }
+
+            const dictionary& dict = dEntry.dict();
+
+            if (!dict.getOrDefault("enabled", true))
+            {
+                continue;
+            }
+
+            label obsGroupId = 0;
+            if (dict.readIfPresent("groupId", obsGroupId))
+            {
+                maxGroup = max(maxGroup, obsGroupId);
+            }
+            else
+            {
+                obsGroupId = ++maxGroup;
+            }
+
+
+            pointField pts;
+            dict.readIfPresent("locations", pts);
+            if (pts.empty())
+            {
+                pts.resize(1, Zero);
+            }
+
+            List<PDRobstacle> obsInput;
+            dict.readEntry("obstacles", obsInput);
+
+            label nCyl = 0; // The number of cylinders vs blocks
+
+            for (PDRobstacle& obs : obsInput)
+            {
+                obs.groupId = obsGroupId;
+                obs.scale(scaleFactor);
+
+                if (obs.isCylinder())
+                {
+                    ++nCyl;
+                }
+            }
+
+            const label nBlock = (obsInput.size() - nCyl);
+
+            blocks.reserve(blocks.size() + nBlock*pts.size());
+            cylinders.reserve(cylinders.size() + nCyl*pts.size());
+
+            if (verbose)
+            {
+                Info<< "Read " << obsInput.size() << " obstacles ("
+                    << nCyl << " cylinders) with "
+                    << pts.size() << " locations" << nl;
+
+                if (verbose > 1)
+                {
+                    Info<< "locations " << pts << nl
+                        << "obstacles " << obsInput << nl;
+                }
+            }
+
+            for (const PDRobstacle& scanObs : obsInput)
+            {
+                // Reject anything below minimum width
+                if (scanObs.tooSmall(pars.min_width))
+                {
+                    continue;
+                }
+
+                for (const point& origin : pts)
+                {
+                    // A different (very small) shift for each obstacle
+                    // so that faces cannot be coincident
+
+                    shift += floatSMALL;
+                    const scalar shift2 = shift * 2.0;
+
+
+                switch (scanObs.typeId)
+                {
+                    case PDRobstacle::CYLINDER:
+                    {
+                        // Make a copy
+                        PDRobstacle obs(scanObs);
+
+                        // Offset for the group position
+                        obs.pt += origin;
+
+                        // Shift the end outwards so, if exactly on
+                        // cell boundary, now overlap cell.
+                        // So included in Aw.
+                        obs.pt -= point::uniform(shift);
+                        obs.len() += shift2;
+                        obs.dia() -= floatSMALL;
+
+
+                        // Trim against the mesh bounds
+                        // - ignore if it doesn't overlap
+                        const volumeType vt = obs.trim(meshBb);
+
+                        switch (vt)
+                        {
+                            case volumeType::OUTSIDE:
+                                ++nOutside;
+                                continue; // Can ignore the rest
+                                break;
+
+                            case volumeType::MIXED:
+                                ++nProtruding;
+                                break;
+
+                            default:
+                                break;
+                        }
+
+                        // Later for position sorting
+                        switch (obs.orient)
+                        {
+                            case vector::X:
+                                obs.sortBias = obs.len();
+                                break;
+                            case vector::Y:
+                                obs.sortBias = 0.5*obs.dia();
+                                break;
+                            case vector::Z:
+                                obs.sortBias = 0.5*obs.dia();
+                                break;
+                        }
+
+                        totVolume += obs.volume();
+                        cylinders.append(obs);
+
+                        break;
+                    }
+
+                    case PDRobstacle::DIAG_BEAM:
+                    {
+                        // Make a copy
+                        PDRobstacle obs(scanObs);
+
+                        // Offset for the group position
+                        obs.pt += origin;
+
+                        // Shift the end outwards so, if exactly on
+                        // cell boundary, now overlap cell.
+                        // So included in Aw.
+                        obs.pt -= point::uniform(shift);
+                        obs.len() += shift2;
+                        obs.wa += shift2;
+                        obs.wb += shift2;
+
+                        totVolume += obs.volume();
+                        cylinders.append(obs);
+
+                        break;
+                    }
+
+                    case PDRobstacle::CUBOID_1:
+                    case PDRobstacle::LOUVRE_BLOWOFF:
+                    case PDRobstacle::CUBOID:
+                    case PDRobstacle::WALL_BEAM:
+                    case PDRobstacle::GRATING:
+                    case PDRobstacle::RECT_PATCH:
+                    {
+                        // Make a copy
+                        PDRobstacle obs(scanObs);
+
+                        // Offset for the position of the group
+                        obs.pt += origin;
+
+                        if (obs.typeId == PDRobstacle::GRATING)
+                        {
+                            if (obs.slat_width <= 0)
+                            {
+                                obs.slat_width = pars.def_grating_slat_w;
+                            }
+                        }
+
+                        // Shift the end outwards so, if exactly on
+                        // cell boundary, now overlap cell.
+                        // So included in Aw.
+                        obs.pt -= point::uniform(shift);
+                        obs.span += point::uniform(shift2);
+
+
+                        // Trim against the mesh bounds
+                        // - ignore if it doesn't overlap
+                        const volumeType vt = obs.trim(meshBb);
+
+                        switch (vt)
+                        {
+                            case volumeType::OUTSIDE:
+                                ++nOutside;
+                                continue; // Can ignore the rest
+                                break;
+
+                            case volumeType::MIXED:
+                                ++nProtruding;
+                                break;
+
+                            default:
+                                break;
+                        }
+
+                        totVolume += obs.volume();
+
+                        blocks.append(obs);
+
+                        break;
+                    }
+                }
+            }
+            }
+
+            // Info<< "Cylinders: " << cylinders << nl;
+        }
+
+        if (nOutside || nProtruding)
+        {
+            Info<< "Warning: " << nOutside << " obstacles outside domain, "
+                << nProtruding << " obstacles partly outside domain" << nl;
+        }
+    }
+
+    // #ifdef FULLDEBUG
+    // Info<< blocks << nl << cylinders << nl;
+    // #endif
+
+
+    Info<< "Number of obstacles: "
+        << (blocks.size() + cylinders.size()) << " ("
+        << cylinders.size() << " cylinders)" << nl;
+
+    return totVolume;
+}
+
+
+// * * * * * * * * * * * * * * * IOstream Operators  * * * * * * * * * * * * //
+
+Foam::Istream& Foam::operator>>(Istream& is, PDRobstacle& obs)
+{
+    obs.read(is);
+
+    return is;
+}
+
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleLegacyIO.C b/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleLegacyIO.C
new file mode 100644
index 00000000000..c2d305c08cf
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleLegacyIO.C
@@ -0,0 +1,475 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2019 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 "PDRobstacle.H"
+#include "vector.H"
+#include "doubleVector.H"
+#include "stringOps.H"
+#include "unitConversion.H"
+#include <cmath>
+
+#define ReportLineInfo(line, file)                                            \
+    if (line >= 0 && !file.empty())                                           \
+    {                                                                         \
+        Info<< " Line " << line << " of file '" << file << '\'';              \
+    }                                                                         \
+    Info<< nl;
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+bool Foam::PDRobstacle::setFromLegacy
+(
+    const int groupTypeId,
+    const string& buffer,
+    const label lineNo,
+    const word& inputFile
+)
+{
+    // Handling the optional identifier string can be a pain.
+    // Generally can only say that it exists if there are 15 or
+    // more columns.
+    //
+    // Cylinder has 8 normal entries
+    // Cuboid, diagonal beam etc have 14 normal entries
+    // However, reject anything that looks like a slipped numeric
+
+    double dummy1;
+
+    string in_ident;
+
+    const auto columns = stringOps::splitSpace(buffer);
+
+    for (std::size_t coli = 14; coli < columns.size(); ++coli)
+    {
+        // See if it can parse into a numerical value
+        if (!readDouble(columns[coli].str(), dummy1))
+        {
+            // Not a numeric value. This must be our identifier
+            in_ident = buffer.substr(columns[coli].first - buffer.begin());
+
+            #ifdef FULLDEBUG
+            Info<< "Identifier: " << in_ident << nl;
+            #endif
+            break;
+        }
+    }
+
+    // Strip off group number
+    groupId = groupTypeId / 100;
+    typeId  = groupTypeId % 100;
+
+    // This is a safe value
+    orient = vector::X;
+
+    switch (typeId)
+    {
+        case PDRobstacle::CYLINDER:
+        {
+            // 8 Tokens
+            // "%d %lf %lf %lf %lf %lf %d %lf"
+            // USP 13/8/14  Read vbkge in case a negative cyl to punch a circular hole
+
+            int in_typeId;
+            double in_x, in_y, in_z;
+            double in_len, in_dia;
+            int in_orient;
+            double in_poro;
+
+            int nread =
+                sscanf
+                (
+                    buffer.c_str(),
+                    "%d %lf %lf %lf %lf %lf %d %lf",
+                    &in_typeId, &in_x, &in_y, &in_z,
+                    &in_len, &in_dia, &in_orient,
+                    &in_poro
+                );
+
+            if (nread < 8)
+            {
+                Info<< "Expected 8 items, but read in " << nread;
+                ReportLineInfo(lineNo, inputFile);
+            }
+
+            identifier = in_ident;
+            pt = point(in_x, in_y, in_z);
+
+            len() = in_len;
+            dia() = in_dia;
+
+            orient = vector::X;  // Check again later
+
+            // Read porosity. Convert to blockage.
+            vbkge = 1.0 - in_poro;
+
+            // Orientation (1,2,3) on input -> (0,1,2)
+            // - sortBias for later position sorting
+            switch (in_orient)
+            {
+                case 1:
+                    orient = vector::X;
+                    sortBias = len();
+                    break;
+                case 2:
+                    orient = vector::Y;
+                    sortBias = 0.5*dia();
+                    break;
+                case 3:
+                    orient = vector::Z;
+                    sortBias = 0.5*dia();
+                    break;
+                default:
+                    sortBias = len();
+                    Info<< "Unexpected orientation " << in_orient;
+                    ReportLineInfo(lineNo, inputFile);
+                    break;
+            }
+        }
+        break;
+
+        case PDRobstacle::DIAG_BEAM:
+        {
+            // A diagonal block
+
+            // 14 columns + identifier
+            // "%d %lf %lf %lf %lf %lf %d %lf %lf %lf %lf %lf %d %lf %s"
+            // vbkge (porosity at this stage) should be 0. Not used (yet)
+
+            int in_typeId;
+            double in_x, in_y, in_z;
+            double in_len, in_theta;
+            int in_orient;
+            double in_wa, in_wb, in_poro;
+            double col_11, col_12, col_14;
+            int col_13;
+
+            int nread =
+                sscanf
+                (
+                    buffer.c_str(),
+                    "%d %lf %lf %lf %lf %lf %d %lf %lf %lf %lf %lf %d %lf",
+                    &in_typeId, &in_x, &in_y, &in_z,
+                    &in_len, &in_theta, &in_orient,
+                    &in_wa, &in_wb, &in_poro,
+                    &col_11, &col_12, &col_13, &col_14
+                );
+
+            if (nread < 14)
+            {
+                Info<< "Expected min 10 items, but read in " << nread;
+                ReportLineInfo(lineNo, inputFile);
+            }
+
+            identifier = in_ident;
+            pt = point(in_x, in_y, in_z);
+
+            len() = in_len;
+            dia() = 0;
+            theta() = 0; // Fix later on
+
+            orient = vector::X;  // Check again later
+
+            wa  = in_wa;
+            wb  = in_wb;
+
+            // Degrees on input, limit to range [0, PI]
+            while (in_theta > 180) in_theta -= 180;
+            while (in_theta < 0) in_theta += 180;
+
+            // Swap axes when theta > PI/2
+            // For 89-90 degrees it becomes -ve, which is picked up
+            // in next section
+            if (in_theta > 89)
+            {
+                in_theta -= 90;
+                // Swap wa <-> wb
+                wa = in_wb;
+                wb = in_wa;
+            }
+
+            theta() = degToRad(in_theta);
+
+            // Orientation (1,2,3) on input -> (0,1,2)
+            // - sortBias for later position sorting
+            switch (in_orient)
+            {
+                case 1:
+                    orient = vector::X;
+                    sortBias = len();
+                    break;
+
+                case 2:
+                    orient = vector::Y;
+                    sortBias = 0.5*(wa * sin(theta()) + wb * cos(theta()));
+                    break;
+
+                case 3:
+                    orient = vector::Z;
+                    sortBias = 0.5*(wa * cos(theta()) + wb * sin(theta()));
+                    break;
+
+                default:
+                    sortBias = len();
+                    Info<< "Unexpected orientation " << in_orient;
+                    ReportLineInfo(lineNo, inputFile);
+                    break;
+            }
+
+
+            // If very nearly aligned with axis, turn it into normal block,
+            // to avoid 1/tan(theta) blowing up
+            if (in_theta < 1)
+            {
+                switch (orient)
+                {
+                    case vector::X:
+                        span = vector(len(), wa, wb);
+                        // Was end center, now lower corner
+                        pt.y() = pt.y() - 0.5 * span.y();
+                        pt.z() = pt.z() - 0.5 * span.z();
+                        break;
+
+                    case vector::Y:
+                        span = vector(wb, len(), wa);
+                        // Was end center, now lower corner
+                        pt.z() = pt.z() - 0.5 * span.z();
+                        pt.x() = pt.x() - 0.5 * span.x();
+                        break;
+
+                    case vector::Z:
+                        span = vector(wa, wb, len());
+                        // Was end center, now lower corner
+                        pt.x() = pt.x() - 0.5 * span.x();
+                        pt.y() = pt.y() - 0.5 * span.y();
+                        break;
+                }
+
+                typeId = PDRobstacle::CUBOID;
+                sortBias = 0;
+                xbkge = ybkge = zbkge = vbkge = 1.0;
+                blowoff_type = 0;
+
+                Info<< "... changed to type cuboid" << nl;
+                break;
+            }
+        }
+        break;
+
+        case PDRobstacle::CUBOID_1:       // Cuboid "Type 1"
+        case PDRobstacle::LOUVRE_BLOWOFF: // Louvred wall or blow-off panel
+        case PDRobstacle::CUBOID:         // Cuboid
+        case PDRobstacle::WALL_BEAM:      // Beam against wall (treated here as normal cuboid)
+        case PDRobstacle::GRATING:        // Grating
+        case PDRobstacle::RECT_PATCH:     // Inlet, outlet, other b.c. (rectangular)
+        {
+            // 14 columns + identifier
+            // "%d %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %d %lf"
+
+            int in_typeId;
+            double in_x, in_y, in_z;
+            double in_delx, in_dely, in_delz;
+            double in_poro, in_porox, in_poroy, in_poroz;
+            double col_12;
+            int col_13;
+            double in_blowoff_time = 0;
+
+            int nread =
+                sscanf
+                (
+                    buffer.c_str(),
+                    "%d %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %lf %d %lf",
+                    &in_typeId, &in_x, &in_y, &in_z,
+                    &in_delx, &in_dely, &in_delz,
+                    &in_poro, &in_porox, &in_poroy, &in_poroz,
+                    &col_12, &col_13, &in_blowoff_time
+                );
+
+            blowoff_time = scalar(in_blowoff_time);
+
+            if (nread < 14)
+            {
+                Info<< "Expected 14 items, but read in " << nread;
+                ReportLineInfo(lineNo, inputFile);
+            }
+
+            identifier = in_ident;
+            pt = point(in_x, in_y, in_z);
+
+            span = vector(in_delx, in_dely, in_delz);
+
+            // Read porosity. Convert to blockage.
+            vbkge = 1.0 - in_poro;
+            xbkge = 1.0 - in_porox;
+            ybkge = 1.0 - in_poroy;
+            zbkge = 1.0 - in_poroz;
+
+            if
+            (
+                typeId == PDRobstacle::CUBOID_1
+             || typeId == PDRobstacle::WALL_BEAM
+             || typeId == PDRobstacle::RECT_PATCH
+            )
+            {
+                // Check for invalid input
+
+                if (vbkge != 1.0 || xbkge != 1.0 || ybkge != 1.0 || zbkge != 1.0)
+                {
+                    Info<< "Type " << typeId << " is porous (setting to blockage).";
+                    ReportLineInfo(lineNo, inputFile);
+
+                    vbkge = 1;
+                    xbkge = 1;
+                    ybkge = 1;
+                    zbkge = 1;
+                }
+                if (typeId == PDRobstacle::RECT_PATCH)
+                {
+                    // Correct interpretation of column 13
+                    inlet_dirn = col_13;
+
+                    if (identifier.empty())
+                    {
+                        FatalErrorInFunction
+                            << "RECT_PATCH without a patch name"
+                            << exit(FatalError);
+                    }
+                }
+            }
+            else if (typeId == PDRobstacle::CUBOID)
+            {
+            }
+            else
+            {
+                if (!equal(cmptProduct(span), 0))
+                {
+                    Info<< "Type " << typeId << " has non-zero thickness.";
+                    ReportLineInfo(lineNo, inputFile);
+                }
+            }
+
+            if (typeId == PDRobstacle::LOUVRE_BLOWOFF)
+            {
+                // Blowoff panel
+                blowoff_press = barToPa(col_12);
+                blowoff_type = col_13;
+
+                if (blowoff_type == 1)
+                {
+                    Info<< "Type " << typeId
+                        << ": blowoff-type 1 not yet implemented.";
+                    ReportLineInfo(lineNo, inputFile);
+
+                    if (blowoff_time != 0)
+                    {
+                        Info<< "Type " << typeId << " has blowoff time set,"
+                            << " not set to blow off cell-by-cell";
+                        ReportLineInfo(lineNo, inputFile);
+                    }
+                }
+                else
+                {
+                    if
+                    (
+                        (blowoff_type == 1 || blowoff_type == 2)
+                     && (col_12 > 0)
+                    )
+                    {
+                        if (col_12 > maxBlowoffPressure)
+                        {
+                            Info<< "Blowoff pressure (" << col_12
+                                << ") too high for blowoff type "
+                                << blowoff_type;
+                            ReportLineInfo(lineNo, inputFile);
+                        }
+                    }
+                    else
+                    {
+                        Info<< "Problem with blowoff parameters";
+                        ReportLineInfo(lineNo, inputFile);
+                        Info<< "Col12 " << col_12
+                            << " Blowoff type " << blowoff_type
+                            << ", blowoff pressure " << blowoff_press << nl;
+                    }
+                }
+            }
+            else if (typeId == PDRobstacle::WALL_BEAM)
+            {
+                // WALL_BEAM against walls only contribute half to drag
+                // if ((col_12 == 1) || (col_12 == -1)) { against_wall_fac = 0.5; }
+            }
+            else if (typeId == PDRobstacle::GRATING)
+            {
+                if (col_12 > 0)
+                {
+                    slat_width = col_12;
+                }
+                else
+                {
+                    slat_width = 0;
+                }
+
+                // Set orientation
+                if (equal(span.x(), 0))
+                {
+                    orient = vector::X;
+                }
+                else if (equal(span.y(), 0))
+                {
+                    orient = vector::Y;
+                }
+                else
+                {
+                    orient = vector::Z;
+                }
+            }
+        }
+        break;
+
+        case 0:  // Group location
+        case PDRobstacle::OLD_INLET: // Ventilation source only
+            return false;
+            break;
+
+        case PDRobstacle::IGNITION:  // Ignition (now ignored. 2019-04)
+            Info<< "Ignition cell type ignored";
+            ReportLineInfo(lineNo, inputFile);
+            return false;
+            break;
+
+        default:
+            Info<< "Unexpected type " << typeId;
+            ReportLineInfo(lineNo, inputFile);
+            return false;
+            break;
+    }
+
+    return true;
+}
+
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleLegacyRead.C b/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleLegacyRead.C
new file mode 100644
index 00000000000..27b1dd4fe64
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleLegacyRead.C
@@ -0,0 +1,567 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2016 Shell Research Ltd.
+    Copyright (C) 2019 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 "PDRsetFields.H"
+#include "PDRobstacle.H"
+#include "volumeType.H"
+
+using namespace Foam;
+using namespace Foam::constant;
+
+#undef USE_ZERO_INSTANCE_GROUPS
+// #define USE_ZERO_INSTANCE_GROUPS
+
+
+// Counting
+Foam::labelPair Foam::PDRlegacy::readObstacleFiles
+(
+    const fileName& obsFileDir,
+    const wordList& obsFileNames,
+    Map<obstacleGrouping>& groups
+)
+{
+    // Default group with single instance and position (0,0,0)
+    groups(0).clear();
+    groups(0).append(point::zero);
+
+    string buffer;
+
+    if (!obsFileNames.empty())
+    {
+        Info<< "Counting groups in obstacle files" << nl;
+    }
+    for (const word& inputFile : obsFileNames)
+    {
+        Info<< "    file: " << inputFile << nl;
+
+        fileName path = (obsFileDir / inputFile);
+
+        IFstream is(path);
+
+        while (is.good())
+        {
+            // Process each line of obstacle files
+            is.getLine(buffer);
+
+            const auto firstCh = buffer.find_first_not_of(" \t\n\v\f\r");
+
+            if
+            (
+                firstCh == std::string::npos
+             || buffer[firstCh] == '#'
+            )
+            {
+                // Empty line or comment line
+                continue;
+            }
+
+            int typeId;
+            double x, y, z; // Double (not scalar) to match sscanf spec
+
+            if
+            (
+                sscanf(buffer.c_str(), "%d %lf %lf %lf", &typeId, &x, &y, &z)<4
+             || typeId == 0
+             || typeId == PDRobstacle::MESH_PLANE
+            )
+            {
+                continue;
+            }
+
+            x *= pars.scale;
+            y *= pars.scale;
+            z *= pars.scale;
+
+            const label groupId = typeId / 100;
+            typeId %= 100;
+
+            if (typeId == PDRobstacle::OLD_INLET)
+            {
+                Info<< "Ignored old-inlet type" << nl;
+                continue;
+            }
+            else if (typeId == PDRobstacle::GRATING && pars.ignoreGratings)
+            {
+                Info<< "Ignored grating" << nl;
+                continue;
+            }
+
+            if (typeId == 0)
+            {
+                // Defining a group location
+                groups(groupId).append(x, y, z);
+            }
+            else if (PDRobstacle::isCylinder(typeId))
+            {
+                // Increment cylinder count for the group
+                groups(groupId).addCylinder();
+            }
+            else
+            {
+                // Increment obstacle count for the group
+                groups(groupId).addObstacle();
+            }
+        }
+    }
+
+
+    label nTotalObs = 0;
+    label nTotalCyl = 0;
+
+    label nMissedObs = 0;
+    label nMissedCyl = 0;
+
+    forAllConstIters(groups, iter)
+    {
+        const auto& group = iter.val();
+
+        nTotalObs += group.nTotalObstacle();
+        nTotalCyl += group.nTotalCylinder();
+
+        if (group.empty())
+        {
+            nMissedObs += group.nObstacle();
+            nMissedCyl += group.nCylinder();
+        }
+    }
+
+    for (const label groupId : groups.sortedToc())
+    {
+        const auto& group = groups[groupId];
+
+        if (groupId)
+        {
+            if (group.size())
+            {
+                Info<< "Found " << group.size()
+                    << " instances of group " << groupId << " ("
+                    << group.nObstacle() << " obstacles "
+                    << group.nCylinder() << " cylinders)"
+                    << nl;
+            }
+        }
+        else
+        {
+            // The group 0 is for ungrouped obstacles
+            Info<< "Found "
+                << group.nObstacle() << " obstacles "
+                << group.nCylinder() << " cylinders not in groups" << nl;
+        }
+    }
+
+    Info<< "Number of obstacles: "
+        << (nTotalObs + nTotalCyl) << " ("
+        << nTotalCyl << " cylinders)" << nl;
+
+    if (nMissedObs + nMissedCyl)
+    {
+        #ifdef USE_ZERO_INSTANCE_GROUPS
+
+        nTotalObs += nMissedObs;
+        nTotalCyl += nMissedCyl;
+        Info<< "Adding " << (nMissedObs + nMissedCyl)
+            << " obstacles in groups without instances to default group" << nl;
+
+        #else
+
+        Warning
+            << nl << "Found " << (nMissedObs + nMissedCyl)
+            << " obstacles in groups without instances" << nl << nl;
+
+        if (pars.debugLevel > 1)
+        {
+            for (const label groupId : groups.sortedToc())
+            {
+                const auto& group = groups[groupId];
+
+                if
+                (
+                    groupId && group.empty()
+                 && (group.nObstacle() || group.nCylinder())
+                )
+                {
+                    Info<< "    Group " << groupId << " ("
+                        << group.nObstacle() << " obstacles "
+                        << group.nCylinder() << " cylinders)"
+                        << nl;
+                }
+            }
+        }
+        #endif
+    }
+
+    return labelPair(nTotalObs, nTotalCyl);
+}
+
+
+Foam::scalar Foam::PDRlegacy::readObstacleFiles
+(
+    const fileName& obsFileDir,
+    const wordList& obsFileNames,
+    const Map<obstacleGrouping>& groups,
+    const boundBox& meshBb,
+
+    DynamicList<PDRobstacle>& blocks,
+    DynamicList<PDRobstacle>& cylinders
+)
+{
+    // Catch programming errors
+    if (!groups.found(0))
+    {
+        FatalErrorInFunction
+            << "No default group 0 defined!" << nl
+            << exit(FatalError);
+    }
+
+    scalar totVolume = 0;
+    label nOutside = 0;
+    label nProtruding = 0;
+
+    scalar shift = pars.obs_expand;
+
+    string buffer;
+
+    if (!obsFileNames.empty())
+    {
+        Info<< "Reading obstacle files" << nl;
+    }
+
+    for (const word& inputFile : obsFileNames)
+    {
+        Info<< "    file: " << inputFile << nl;
+
+        fileName path = (obsFileDir / inputFile);
+
+        IFstream is(path);
+
+        label lineNo = 0;
+        while (is.good())
+        {
+            // Process each line of obstacle files
+            ++lineNo;
+            is.getLine(buffer);
+
+            const auto firstCh = buffer.find_first_not_of(" \t\n\v\f\r");
+
+            if
+            (
+                firstCh == std::string::npos
+             || buffer[firstCh] == '#'
+            )
+            {
+                // Empty line or comment line
+                continue;
+            }
+
+            // Quick reject
+
+            int typeId;     // Int (not label) to match sscanf spec
+            double x, y, z; // Double (not scalar) to match sscanf spec
+
+            if
+            (
+                sscanf(buffer.c_str(), "%d %lf %lf %lf", &typeId, &x, &y, &z) < 4
+             || typeId == 0
+             || typeId == PDRobstacle::MESH_PLANE
+            )
+            {
+                continue;
+            }
+
+            int groupId = typeId / 100;
+            typeId %= 100;
+
+            if
+            (
+                typeId == PDRobstacle::OLD_INLET
+             || (typeId == PDRobstacle::GRATING && pars.ignoreGratings)
+            )
+            {
+                // Silent - already warned during counting
+                continue;
+            }
+
+            if (typeId == 0)
+            {
+                // Group location - not an obstacle
+                continue;
+            }
+
+            if (!groups.found(groupId))
+            {
+                // Catch programming errors.
+                // - group should be there after the previous read
+                Warning
+                    << "Encountered undefined group: " << groupId << nl;
+                continue;
+            }
+
+            #ifdef USE_ZERO_INSTANCE_GROUPS
+            const obstacleGrouping& group =
+            (
+                groups[groups[groupId].size() ? groupId : 0]
+            );
+            #else
+            const obstacleGrouping& group = groups[groupId];
+            #endif
+
+            // Add the obstacle to the list with different position
+            // offsets according to its group. Non-group obstacles
+            // are treated as group 0, which has a single instance
+            // with position (0,0,0) and are added only once.
+
+            PDRobstacle scanObs;
+
+            if
+            (
+                !scanObs.setFromLegacy
+                (
+                    (groupId * 100) + typeId,
+                    buffer,
+                    lineNo,
+                    inputFile
+                )
+            )
+            {
+                continue;
+            }
+
+            scanObs.scale(pars.scale);
+
+            // Ignore anything below minimum width
+            if (scanObs.tooSmall(pars.min_width))
+            {
+                continue;
+            }
+
+
+            for (const point& origin : group)
+            {
+                // A different (very small) shift for each obstacle
+                // so that faces cannot be coincident
+                shift += floatSMALL;
+                const scalar shift2 = shift * 2.0;
+
+                switch (typeId)
+                {
+                    case PDRobstacle::CYLINDER:
+                    {
+                        // Make a copy
+                        PDRobstacle obs(scanObs);
+
+                        // Offset for the position of the group
+                        obs.pt += origin;
+
+                        // Shift the end outwards so, if exactly on
+                        // cell boundary, now overlap cell.
+                        // So included in Aw.
+                        obs.pt -= point::uniform(shift);
+                        obs.len() += shift2;
+                        obs.dia() -= floatSMALL;
+
+
+                        // Trim against the mesh bounds
+                        // - ignore if it doesn't overlap
+                        const volumeType vt = obs.trim(meshBb);
+
+                        switch (vt)
+                        {
+                            case volumeType::OUTSIDE:
+                                ++nOutside;
+                                continue; // Can ignore the rest
+                                break;
+
+                            case volumeType::MIXED:
+                                ++nProtruding;
+                                break;
+
+                            default:
+                                break;
+                        }
+
+                        // Later for position sorting
+                        switch (obs.orient)
+                        {
+                            case vector::X:
+                                obs.sortBias = obs.len();
+                                break;
+                            case vector::Y:
+                                obs.sortBias = 0.5*obs.dia();
+                                break;
+                            case vector::Z:
+                                obs.sortBias = 0.5*obs.dia();
+                                break;
+                        }
+
+                        totVolume += obs.volume();
+                        cylinders.append(obs);
+
+                        break;
+                    }
+
+                    case PDRobstacle::DIAG_BEAM:
+                    {
+                        // Make a copy
+                        PDRobstacle obs(scanObs);
+
+                        // Offset for the position of the group
+                        obs.pt += origin;
+
+                        // Shift the end outwards so, if exactly on
+                        // cell boundary, now overlap cell.
+                        // So included in Aw.
+                        obs.pt -= point::uniform(shift);
+                        obs.len() += shift2;
+                        obs.wa += shift2;
+                        obs.wb += shift2;
+
+                        totVolume += obs.volume();
+                        cylinders.append(obs);
+
+                        break;
+                    }
+
+                    case PDRobstacle::CUBOID_1:       // Cuboid "Type 1"
+                    case PDRobstacle::LOUVRE_BLOWOFF: // Louvred wall or blow-off panel
+                    case PDRobstacle::CUBOID:         // Cuboid
+                    case PDRobstacle::WALL_BEAM:      // Beam against wall (treated here as normal cuboid)
+                    case PDRobstacle::GRATING:        // Grating
+                    case PDRobstacle::RECT_PATCH:     // Inlet, outlet or ather b.c. (rectangular)
+                    {
+                        // Make a copy
+                        PDRobstacle obs(scanObs);
+
+                        // Offset for the position of the group
+                        obs.pt += origin;
+
+                        if (typeId == PDRobstacle::GRATING)
+                        {
+                            if (obs.slat_width <= 0)
+                            {
+                                obs.slat_width = pars.def_grating_slat_w;
+                            }
+                        }
+
+                        // Shift the end outwards so, if exactly on
+                        // cell boundary, now overlap cell.
+                        // So included in Aw.
+                        obs.pt -= point::uniform(shift);
+                        obs.span += point::uniform(shift2);
+
+
+                        // Trim against the mesh bounds
+                        // - ignore if it doesn't overlap
+                        const volumeType vt = obs.trim(meshBb);
+
+                        switch (vt)
+                        {
+                            case volumeType::OUTSIDE:
+                                ++nOutside;
+                                continue; // Can ignore the rest
+                                break;
+
+                            case volumeType::MIXED:
+                                ++nProtruding;
+                                break;
+
+                            default:
+                                break;
+                        }
+
+                        totVolume += obs.volume();
+
+                        blocks.append(obs);
+
+                        break;
+                    }
+                }
+            }
+        }
+
+        if (nOutside || nProtruding)
+        {
+            Info<< "Warning: " << nOutside << " obstacles outside domain, "
+                << nProtruding << " obstacles partly outside domain" << nl;
+        }
+    }
+
+
+    // #ifdef FULLDEBUG
+    // Info<< blocks << nl << cylinders << nl;
+    // #endif
+
+    return totVolume;
+}
+
+
+Foam::scalar Foam::PDRobstacle::legacyReadFiles
+(
+    const fileName& obsFileDir,
+    const wordList& obsFileNames,
+    const boundBox& meshBb,
+    DynamicList<PDRobstacle>& blocks,
+    DynamicList<PDRobstacle>& cylinders
+)
+{
+    // Still just with legacy reading
+
+    // Count the obstacles and get the group locations
+    Map<PDRlegacy::obstacleGrouping> groups;
+
+    const labelPair obsCounts =
+        PDRlegacy::readObstacleFiles(obsFileDir, obsFileNames, groups);
+
+    const label nObstacle = obsCounts.first();
+    const label nCylinder = obsCounts.second();
+
+    // Info<< "grouping: " << groups << endl;
+
+    if (!nObstacle && !nCylinder)
+    {
+        FatalErrorInFunction
+            << "No obstacles in domain" << nl
+            << exit(FatalError);
+    }
+
+    blocks.clear();
+    blocks.reserve(4 * max(4, nObstacle));
+
+    cylinders.clear();
+    cylinders.reserve(4 * max(4, nCylinder));
+
+    return PDRlegacy::readObstacleFiles
+    (
+        obsFileDir, obsFileNames, groups,
+        meshBb,
+        blocks,
+        cylinders
+    );
+}
+
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleTypes.C b/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleTypes.C
new file mode 100644
index 00000000000..3fc8275f46f
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleTypes.C
@@ -0,0 +1,512 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2019 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 "PDRobstacleTypes.H"
+#include "PDRobstacleTypes.H"
+#include "Enum.H"
+#include "unitConversion.H"
+#include "addToMemberFunctionSelectionTable.H"
+
+using namespace Foam::constant;
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#define addObstacleReader(obsType, obsName)                                   \
+    namespace Foam                                                            \
+    {                                                                         \
+    namespace PDRobstacles                                                    \
+    {                                                                         \
+        addNamedToMemberFunctionSelectionTable                                \
+        (                                                                     \
+            PDRobstacle,                                                      \
+            obsType,                                                          \
+            read,                                                             \
+            dictRead,                                                         \
+            obsName                                                           \
+        );                                                                    \
+    }                                                                         \
+    }
+
+
+// * * * * * * * * * * * * * * * Local Functions * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+// Read porosity, change to blockage. Clamp values [0-1] silently
+static const scalarMinMax limits01(scalarMinMax::zero_one());
+
+// Volume porosity -> blockage
+inline scalar getPorosity(const dictionary& dict)
+{
+    return 1 - limits01.clip(dict.getOrDefault<scalar>("porosity", 0));
+}
+
+// Direction porosities -> blockage
+inline vector getPorosities(const dictionary& dict)
+{
+    vector blockage(vector::one);
+
+    if (dict.readIfPresent("porosities", blockage))
+    {
+        for (scalar& val : blockage)
+        {
+            val = 1 - limits01.clip(val);
+        }
+    }
+
+    return blockage;
+}
+
+
+// Check for "porosity", or "porosities"
+// inline static bool hasPorosity(const dictionary& dict)
+// {
+//     return dict.found("porosity") || dict.found("porosities");
+// }
+
+
+static const Foam::Enum<Foam::vector::components>
+vectorComponentsNames
+({
+    { vector::components::X, "x" },
+    { vector::components::Y, "y" },
+    { vector::components::Z, "z" },
+});
+
+
+enum inletDirnType
+{
+    _X = -1,    // -ve x
+    _Y = -2,    // -ve y
+    _Z = -3,    // -ve z
+    X = 1,      // +ve x
+    Y = 2,      // +ve y
+    Z = 3,      // +ve z
+};
+
+static const Foam::Enum<inletDirnType>
+inletDirnNames
+({
+    { inletDirnType::_X, "-x" },
+    { inletDirnType::_Y, "-y" },
+    { inletDirnType::_Z, "-z" },
+    { inletDirnType::_X, "_x" },
+    { inletDirnType::_Y, "_y" },
+    { inletDirnType::_Z, "_z" },
+    { inletDirnType::X, "+x" },
+    { inletDirnType::Y, "+y" },
+    { inletDirnType::Z, "+z" },
+    { inletDirnType::X, "x" },
+    { inletDirnType::Y, "y" },
+    { inletDirnType::Z, "z" },
+});
+
+} // End namespace Foam
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+addObstacleReader(cylinder, cyl);
+addObstacleReader(cylinder, cylinder);
+
+void Foam::PDRobstacles::cylinder::read
+(
+    PDRobstacle& obs,
+    const dictionary& dict
+)
+{
+    obs.PDRobstacle::readProperties(dict);
+    obs.typeId = enumTypeId;
+
+    // Enforce complete blockage
+    obs.xbkge = obs.ybkge = obs.zbkge = obs.vbkge = 1;
+    // if (hasPorosity(dict)) ... warn?
+
+
+    dict.readEntry("point", obs.pt);
+    dict.readEntry("length", obs.len());
+    dict.readEntry("diameter", obs.dia());
+
+    obs.orient = vectorComponentsNames.get("direction", dict);
+
+    // The sortBias for later position sorting
+    switch (obs.orient)
+    {
+        case vector::X:
+            obs.sortBias = obs.len();
+            break;
+
+        default:
+            obs.sortBias = 0.5*obs.dia();
+            break;
+    }
+}
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+addObstacleReader(diagbeam, diag);
+addObstacleReader(diagbeam, diagbeam);
+
+void Foam::PDRobstacles::diagbeam::read
+(
+    PDRobstacle& obs,
+    const dictionary& dict
+)
+{
+    obs.PDRobstacle::readProperties(dict);
+    obs.typeId = enumTypeId;
+
+    // Enforce complete blockage
+    obs.xbkge = obs.ybkge = obs.zbkge = obs.vbkge = 1;
+    // if (hasPorosity(dict)) ... warn?
+
+
+    dict.readEntry("point", obs.pt);
+    dict.readEntry("length", obs.len());
+    obs.dia() = Zero;
+    obs.theta() = Zero;   // Fix later on
+
+    obs.orient = vectorComponentsNames.get("direction", dict);
+
+    // Angle (degrees) on input, limit to range [0, PI]
+    scalar angle = dict.get<scalar>("angle");
+
+    while (angle > 180) angle -= 180;
+    while (angle < 0) angle += 180;
+
+    labelPair dims;
+    dict.readEntry("width", dims);
+
+    // Swap axes when theta > PI/2
+    // For 89-90 degrees it becomes -ve, which is picked up in following section
+    if (angle > 89)
+    {
+        angle -= 90;
+        dims.flip();  // Swap dimensions
+    }
+
+    obs.theta() = degToRad(angle);
+
+    obs.wa = dims.first();
+    obs.wb = dims.second();
+
+    const scalar ctheta = cos(obs.theta());
+    const scalar stheta = sin(obs.theta());
+
+    // The sortBias for later position sorting
+    switch (obs.orient)
+    {
+        case vector::X:
+            obs.sortBias = obs.len();
+            break;
+
+        case vector::Y:
+            obs.sortBias = 0.5*(obs.wa * stheta + obs.wb * ctheta);
+            break;
+
+        case vector::Z:
+            obs.sortBias = 0.5*(obs.wa * ctheta + obs.wb * stheta);
+            break;
+    }
+
+    // If very nearly aligned with axis, turn it into normal block,
+    // to avoid 1/tan(theta) blowing up
+    if (angle < 1)
+    {
+        Info<< "... changed diag-beam to box" << nl;
+
+        switch (obs.orient)
+        {
+            case vector::X:
+                obs.span = vector(obs.len(), obs.wa, obs.wb);
+                break;
+
+            case vector::Y:
+                obs.span = vector(obs.wb, obs.len(), obs.wa);
+                break;
+
+            case vector::Z:
+                obs.span = vector(obs.wa, obs.wb, obs.len());
+                break;
+        }
+
+        // The pt was end centre (cylinder), now lower corner
+        vector adjustPt = -0.5*obs.span;
+        adjustPt[obs.orient] = 0;
+
+        obs.pt -= adjustPt;
+
+        obs.typeId = PDRobstacles::cuboid::enumTypeId;
+        obs.sortBias = 0;
+        obs.xbkge = obs.ybkge = obs.zbkge = obs.vbkge = 1.0;
+        obs.blowoff_type = 0;
+    }
+}
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+addObstacleReader(cuboid, box);
+
+void Foam::PDRobstacles::cuboid::read
+(
+    PDRobstacle& obs,
+    const dictionary& dict
+)
+{
+    obs.PDRobstacle::readProperties(dict);
+    obs.typeId = enumTypeId;
+
+    // Default - full blockage
+    obs.xbkge = obs.ybkge = obs.zbkge = obs.vbkge = 1;
+
+
+    dict.readEntry("point", obs.pt);
+    dict.readEntry("size", obs.span);
+
+    // Optional
+    obs.vbkge = getPorosity(dict);
+
+    // Optional
+    const vector blockages = getPorosities(dict);
+    obs.xbkge = blockages.x();
+    obs.ybkge = blockages.y();
+    obs.zbkge = blockages.z();
+}
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+addObstacleReader(wallbeam, wallbeam);
+
+void Foam::PDRobstacles::wallbeam::read
+(
+    PDRobstacle& obs,
+    const dictionary& dict
+)
+{
+    PDRobstacles::cuboid::read(obs, dict);
+    obs.typeId = enumTypeId;
+
+    // Enforce complete blockage
+    obs.xbkge = obs.ybkge = obs.zbkge = obs.vbkge = 1;
+
+    // if (hasPorosity(dict)) ... warn?
+
+    // Additional adjustment for drag etc.
+}
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+addObstacleReader(grating, grating);
+addObstacleReader(grating, grate);
+
+void Foam::PDRobstacles::grating::read
+(
+    PDRobstacle& obs,
+    const dictionary& dict
+)
+{
+    obs.PDRobstacle::readProperties(dict);
+    obs.typeId = enumTypeId;
+
+    // Initialize to full blockage
+    obs.xbkge = obs.ybkge = obs.zbkge = obs.vbkge = 1;
+
+    dict.readEntry("point", obs.pt);
+    dict.readEntry("size", obs.span);
+
+    // TODO: better error handling
+    // if (!equal(cmptProduct(obs.span), 0))
+    // {
+    //     Info<< "Type " << typeId << " has non-zero thickness.";
+    //     ReportLineInfo(lineNo, inputFile);
+    // }
+
+    obs.vbkge = getPorosity(dict);
+
+    const vector blockages = getPorosities(dict);
+    obs.xbkge = blockages.x();
+    obs.ybkge = blockages.y();
+    obs.zbkge = blockages.z();
+
+    // TODO: Warning if porosity was not specified?
+
+
+    // TBD: Default slat width from PDR.params
+    obs.slat_width = dict.getOrDefault<scalar>("slats", Zero);
+
+    // Determine the orientation
+    if (equal(obs.span.x(), 0))
+    {
+        obs.orient = vector::X;
+    }
+    else if (equal(obs.span.y(), 0))
+    {
+        obs.orient = vector::Y;
+    }
+    else
+    {
+        obs.orient = vector::Z;
+    }
+}
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+addObstacleReader(louver, louver);
+addObstacleReader(louver, louvre);
+
+void Foam::PDRobstacles::louver::read
+(
+    PDRobstacle& obs,
+    const dictionary& dict
+)
+{
+    obs.PDRobstacle::readProperties(dict);
+    obs.typeId = enumTypeId;
+
+    // Initialize to full blockage
+    obs.xbkge = obs.ybkge = obs.zbkge = obs.vbkge = 1;
+
+    dict.readEntry("point", obs.pt);
+    dict.readEntry("size", obs.span);
+
+    // TODO: better error handling
+    // if (!equal(cmptProduct(obs.span), 0))
+    // {
+    //     Info<< "Type " << typeId << " has non-zero thickness.";
+    //     ReportLineInfo(lineNo, inputFile);
+    // }
+
+    obs.vbkge = getPorosity(dict);
+
+    const vector blockages = getPorosities(dict);
+    obs.xbkge = blockages.x();
+    obs.ybkge = blockages.y();
+    obs.zbkge = blockages.z();
+
+    // TODO: Warning if porosity was not specified?
+
+
+    // Blowoff pressure [bar]
+    const scalar blowoffPress = dict.get<scalar>("pressure");
+
+    obs.blowoff_press = barToPa(blowoffPress);
+    obs.blowoff_time = dict.getOrDefault<scalar>("time", 0);
+    obs.blowoff_type = dict.getOrDefault<label>("type", 2);
+
+    if (obs.blowoff_type == 1)
+    {
+        Info<< "Louver : blowoff-type 1 not yet implemented." << nl;
+        // ReportLineInfo(lineNo, inputFile);
+
+        if (obs.blowoff_time != 0)
+        {
+            Info<< "Louver : has blowoff time set,"
+                << " not set to blow off cell-by-cell" << nl;
+            // ReportLineInfo(lineNo, inputFile);
+        }
+    }
+    else
+    {
+        if
+        (
+            (obs.blowoff_type == 1 || obs.blowoff_type == 2)
+         && (blowoffPress > 0)
+        )
+        {
+            if (blowoffPress > maxBlowoffPressure)
+            {
+                Info<< "Blowoff pressure (" << blowoffPress
+                    << ") too high for blowoff type "
+                    << obs.blowoff_type << nl;
+                // ReportLineInfo(lineNo, inputFile);
+            }
+        }
+        else
+        {
+            Info<< "Problem with blowoff parameters" << nl;
+            // ReportLineInfo(lineNo, inputFile);
+            Info<< "Pressure[bar] " << blowoffPress
+                << " Blowoff type " << obs.blowoff_type
+                << ", blowoff pressure " << obs.blowoff_press << nl;
+        }
+    }
+}
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+addObstacleReader(patch, patch);
+
+void Foam::PDRobstacles::patch::read
+(
+    PDRobstacle& obs,
+    const dictionary& dict
+)
+{
+    obs.PDRobstacle::readProperties(dict);
+    obs.typeId = enumTypeId;
+
+    const auto nameLen = obs.identifier.length();
+
+    word patchName = word::validate(obs.identifier);
+
+    if (patchName.empty())
+    {
+        FatalErrorInFunction
+            << "RECT_PATCH without a patch name"
+            << exit(FatalError);
+    }
+    else if (patchName.length() != nameLen)
+    {
+        WarningInFunction
+            << "RECT_PATCH stripped invalid characters from patch name: "
+            << obs.identifier
+            << exit(FatalError);
+
+        obs.identifier = std::move(patchName);
+    }
+
+    // Enforce complete blockage
+    obs.xbkge = obs.ybkge = obs.zbkge = obs.vbkge = 1;
+
+    dict.readEntry("point", obs.pt);
+    dict.readEntry("size", obs.span);
+    obs.inlet_dirn = inletDirnNames.get("direction", dict);
+}
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#undef addObstacleReader
+
+// ************************************************************************* //
diff --git a/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleTypes.H b/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleTypes.H
new file mode 100644
index 00000000000..9fa7d93a25d
--- /dev/null
+++ b/applications/utilities/preProcessing/PDRsetFields/obstacles/PDRobstacleTypes.H
@@ -0,0 +1,300 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2019 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/>.
+
+Namespace
+    Foam::PDRobstacles
+
+Description
+    Reader classes for concrete PDRsetFields obstacle types.
+
+SourceFiles
+    PDRobstacleTypes.C
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef PDRobstacleTypes_H
+#define PDRobstacleTypes_H
+
+#include "PDRobstacle.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace PDRobstacles
+{
+
+/*---------------------------------------------------------------------------*\
+                          Class cylinder Declaration
+\*---------------------------------------------------------------------------*/
+
+/*! \brief A cylinder, selectable as \c cyl or \c cylinder
+
+Dictionary controls
+\table
+    Property    | Description                               | Required | Default
+    point       | The end centre-point                      | yes |
+    length      | The cylinder length                       | yes |
+    diameter    | The cylinder diameter                     | yes |
+    direction   | The x/y/z direction                       | yes |
+\endtable
+
+Example,
+\verbatim
+cyl
+{
+     point       (0 0 0);
+     length      0.95;
+     diameter    0.05;
+     direction   x;
+}
+\endverbatim
+
+\note Does not have porosity.
+*/
+struct cylinder : public PDRobstacle
+{
+    static constexpr int enumTypeId = legacyTypes::CYLINDER;
+    static void read(PDRobstacle& obs, const dictionary& dict);
+};
+
+
+
+/*---------------------------------------------------------------------------*\
+                          Class diagbeam Declaration
+\*---------------------------------------------------------------------------*/
+
+/*! \brief A diagonal beam, which is cylinder-like,
+ selectable as \c diag or \c diagbeam
+
+ If the beam angle is close to zero, it will be changed to a box
+ (PDRobstacles::cuboid)
+
+Dictionary controls
+\table
+    Property    | Description                               | Required | Default
+    point       | The end centre-point                      | yes |
+    length      | The beam length                           | yes |
+    width       | The beam cross-dimensions                 | yes |
+    angle       | The beam angle (degrees)                  | yes |
+    direction   | The x/y/z direction                       | yes |
+\endtable
+
+Example,
+\verbatim
+diag
+{
+     point       (0 0 0);
+     length      0.95;
+     width       (0.05 0.01);
+     angle       25;
+     direction   x;
+}
+\endverbatim
+
+\note Does not have porosity.
+*/
+struct diagbeam : public PDRobstacle
+{
+    static constexpr int enumTypeId = legacyTypes::DIAG_BEAM;
+    static void read(PDRobstacle& obs, const dictionary& dict);
+};
+
+
+/*---------------------------------------------------------------------------*\
+                           Class cuboid Declaration
+\*---------------------------------------------------------------------------*/
+
+/*! \brief A cuboid, selectable as \c box
+
+Dictionary controls
+\table
+    Property    | Description                               | Required | Default
+    point       | The lower left corner                     | yes |
+    size        | The (x y z) dimensions                    | yes |
+    porosity    | The volumetric porosity                   | no  | 0
+    porosities  | The directional porosities                | no  | (0 0 0)
+\endtable
+
+Example,
+\verbatim
+box
+{
+     point       (0 0 0);
+     size        (0.05 0.05 2);
+}
+\endverbatim
+*/
+struct cuboid : public PDRobstacle
+{
+    static constexpr int enumTypeId = legacyTypes::CUBOID;
+    static void read(PDRobstacle& obs, const dictionary& dict);
+};
+
+
+
+/*---------------------------------------------------------------------------*\
+                           Class wallbeam Declaration
+\*---------------------------------------------------------------------------*/
+
+/*! \brief A wallbeam, selectable as \c wallbeam which is currently identical
+ to a box (PDRobstacles::cuboid)
+
+Dictionary controls
+\table
+    Property    | Description                               | Required | Default
+    point       | The lower left corner                     | yes |
+    size        | The (x y z) dimensions                    | yes |
+\endtable
+
+Example,
+\verbatim
+wallbeam
+{
+     point       (0 0 0);
+     size        (0.05 0.05 2);
+}
+\endverbatim
+
+\note Does not have porosity.
+*/
+struct wallbeam : public PDRobstacle
+{
+    static constexpr int enumTypeId = legacyTypes::WALL_BEAM;
+    static void read(PDRobstacle& obs, const dictionary& dict);
+};
+
+
+/*---------------------------------------------------------------------------*\
+                           Class grating Declaration
+\*---------------------------------------------------------------------------*/
+
+/*! \brief A grating, selectable as \c grate or \c grating
+
+The dimensions must include one component that is zero,
+which establishes the orientation.
+
+Dictionary controls
+\table
+    Property    | Description                               | Required | Default
+    point       | The lower left corner                     | yes |
+    size        | The (x y z) dimensions                    | yes |
+    slats       | The slat width                            | no  | 0
+\endtable
+
+Example,
+\verbatim
+grating
+{
+     point       (0 0 0);
+     size        (0.1 0.1 0);
+     slats       0.005;
+}
+\endverbatim
+*/
+struct grating : public PDRobstacle
+{
+    static constexpr int enumTypeId = legacyTypes::GRATING;
+    static void read(PDRobstacle& obs, const dictionary& dict);
+};
+
+
+/*---------------------------------------------------------------------------*\
+                           Class louver Declaration
+\*---------------------------------------------------------------------------*/
+
+/*! \brief Louver blowoff, selectable as \c louver or \c louvre
+
+Dictionary controls
+\table
+    Property    | Description                               | Required | Default
+    point       | The lower left corner                     | yes |
+    size        | The (x y z) dimensions                    | yes |
+    pressure    | The blowoff pressure (bar)                | yes |
+    time        | The blowoff time                          | no  | 0
+    type        | The blowoff type (1, 2)                   | no  | 1
+\endtable
+
+Example,
+\verbatim
+louver
+{
+     point       (0 0 0);
+     size        (0.1 0.1);
+     pressure    3;
+     type        1;
+}
+\endverbatim
+*/
+struct louver : public PDRobstacle
+{
+    static constexpr int enumTypeId = legacyTypes::LOUVER_BLOWOFF;
+    static void read(PDRobstacle& obs, const dictionary& dict);
+};
+
+
+/*---------------------------------------------------------------------------*\
+                           Class patch Declaration
+\*---------------------------------------------------------------------------*/
+
+/*! \brief Rectangular patch, selectable as \c patch
+
+Dictionary controls
+\table
+    Property    | Description                               | Required | Default
+    name        | The patch name corner                     | yes |
+    point       | The lower left corner                     | yes |
+    size        | The (x y z) dimensions                    | yes |
+    direction   | The direction (x/y/z, _x/_y/_z or "-x"/"-y"/"-z" | yes |
+\endtable
+
+Example,
+\verbatim
+patch
+{
+     name        inlet;
+     direction   _x;
+     point       (0 0 0);
+     size        (0 0.05 1.0);
+}
+\endverbatim
+*/
+struct patch : public PDRobstacle
+{
+    static constexpr int enumTypeId = legacyTypes::RECT_PATCH;
+    static void read(PDRobstacle& obs, const dictionary& dict);
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace PDRobstacles
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
-- 
GitLab