diff --git a/src/fvOptions/Make/files b/src/fvOptions/Make/files
index 7ede3766061a3b546f3f0d6c0bcfbbb92b59c5e3..d481c16016bf51730d76147a469140b52aae6389 100644
--- a/src/fvOptions/Make/files
+++ b/src/fvOptions/Make/files
@@ -13,7 +13,6 @@ $(derivedSources)/actuationDiskSource/actuationDiskSource.C
 $(derivedSources)/buoyancyEnergy/buoyancyEnergy.C
 $(derivedSources)/buoyancyForce/buoyancyForce.C
 $(derivedSources)/directionalPressureGradientExplicitSource/directionalPressureGradientExplicitSource.C
-$(derivedSources)/effectivenessHeatExchangerSource/effectivenessHeatExchangerSource.C
 $(derivedSources)/explicitPorositySource/explicitPorositySource.C
 $(derivedSources)/jouleHeatingSource/jouleHeatingSource.C
 $(derivedSources)/meanVelocityForce/meanVelocityForce.C
@@ -38,6 +37,12 @@ $(derivedSources)/viscousDissipation/viscousDissipation.C
 $(derivedSources)/buoyancyTurbSource/buoyancyTurbSource.C
 $(derivedSources)/patchCellsSource/patchCellsSource.C
 
+$(derivedSources)/heatExchangerSource/heatExchangerSource.C
+$(derivedSources)/heatExchangerSource/heatExchangerModels/heatExchangerModel/heatExchangerModel.C
+$(derivedSources)/heatExchangerSource/heatExchangerModels/heatExchangerModel/heatExchangerModelNew.C
+$(derivedSources)/heatExchangerSource/heatExchangerModels/effectivenessTable/effectivenessTable.C
+$(derivedSources)/heatExchangerSource/heatExchangerModels/referenceTemperature/referenceTemperature.C
+
 interRegion = sources/interRegion
 $(interRegion)/interRegionHeatTransfer/interRegionHeatTransferModel/interRegionHeatTransferModel.C
 $(interRegion)/interRegionHeatTransfer/constantHeatTransfer/constantHeatTransfer.C
diff --git a/src/fvOptions/sources/derived/effectivenessHeatExchangerSource/effectivenessHeatExchangerSource.C b/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/effectivenessTable/effectivenessTable.C
similarity index 51%
rename from src/fvOptions/sources/derived/effectivenessHeatExchangerSource/effectivenessHeatExchangerSource.C
rename to src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/effectivenessTable/effectivenessTable.C
index ae487be30a1bcebbc082e661025d15a9313edc77..3d4ed5576074a954cf493848c26ff3637a218a7a 100644
--- a/src/fvOptions/sources/derived/effectivenessHeatExchangerSource/effectivenessHeatExchangerSource.C
+++ b/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/effectivenessTable/effectivenessTable.C
@@ -5,8 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2013-2015 OpenFOAM Foundation
-    Copyright (C) 2016-2022 OpenCFD Ltd.
+    Copyright (C) 2022 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -26,114 +25,42 @@ License
 
 \*---------------------------------------------------------------------------*/
 
-#include "effectivenessHeatExchangerSource.H"
-#include "fvMesh.H"
-#include "fvMatrix.H"
-#include "addToRunTimeSelectionTable.H"
+#include "effectivenessTable.H"
 #include "basicThermo.H"
-#include "coupledPolyPatch.H"
 #include "surfaceInterpolate.H"
+#include "addToRunTimeSelectionTable.H"
 
 // * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
 
 namespace Foam
 {
-namespace fv
+namespace heatExchangerModels
 {
-    defineTypeNameAndDebug(effectivenessHeatExchangerSource, 0);
+    defineTypeNameAndDebug(effectivenessTable, 0);
     addToRunTimeSelectionTable
     (
-        option,
-        effectivenessHeatExchangerSource,
+        heatExchangerModel,
+        effectivenessTable,
         dictionary
     );
 }
 }
 
 
-// * * * * * * * * * * * * Private Member Functions  * * * * * * * * * * * //
-
-void Foam::fv::effectivenessHeatExchangerSource::initialise()
-{
-    const label zoneID = mesh_.faceZones().findZoneID(faceZoneName_);
-
-    if (zoneID < 0)
-    {
-        FatalErrorInFunction
-            << type() << " " << this->name() << ": "
-            << "    Unknown face zone name: " << faceZoneName_
-            << ". Valid face zones are: " << mesh_.faceZones().names()
-            << exit(FatalError);
-    }
-
-    const faceZone& fZone = mesh_.faceZones()[zoneID];
-
-    faceId_.setSize(fZone.size());
-    facePatchId_.setSize(fZone.size());
-    faceSign_.setSize(fZone.size());
-
-    label count = 0;
-    forAll(fZone, i)
-    {
-        const label facei = fZone[i];
-        label faceId = -1;
-        label facePatchId = -1;
-        if (mesh_.isInternalFace(facei))
-        {
-            faceId = facei;
-            facePatchId = -1;
-        }
-        else
-        {
-            facePatchId = mesh_.boundaryMesh().whichPatch(facei);
-            const polyPatch& pp = mesh_.boundaryMesh()[facePatchId];
-            const auto* cpp = isA<coupledPolyPatch>(pp);
-
-            if (cpp)
-            {
-                faceId = (cpp->owner() ? pp.whichFace(facei) : -1);
-            }
-            else if (!isA<emptyPolyPatch>(pp))
-            {
-                faceId = pp.whichFace(facei);
-            }
-            else
-            {
-                faceId = -1;
-                facePatchId = -1;
-            }
-        }
-
-        if (faceId >= 0)
-        {
-            if (fZone.flipMap()[i])
-            {
-                faceSign_[count] = -1;
-            }
-            else
-            {
-                faceSign_[count] = 1;
-            }
-            faceId_[count] = faceId;
-            facePatchId_[count] = facePatchId;
-            count++;
-        }
-    }
-    faceId_.setSize(count);
-    facePatchId_.setSize(count);
-    faceSign_.setSize(count);
-}
-
+// * * * * * * * * * * * * * Private Member Functions  * * * * * * * * * * * //
 
-void Foam::fv::effectivenessHeatExchangerSource::writeFileHeader(Ostream& os)
+void Foam::heatExchangerModels::effectivenessTable::writeFileHeader
+(
+    Ostream& os
+) const
 {
-    writeFile::writeHeader(os, "Effectiveness heat exchanger source");
+    writeFile::writeHeader(os, "Heat exchanger source");
     writeFile::writeCommented(os, "Time");
     writeFile::writeTabbed(os, "Net mass flux [kg/s]");
     writeFile::writeTabbed(os, "Total heat exchange [W]");
     writeFile::writeTabbed(os, "Secondary inlet T [K]");
-    writeFile::writeTabbed(os, "Tref [K]");
-    writeFile::writeTabbed(os, "Effectiveness");
+    writeFile::writeTabbed(os, "Reference T [K]");
+    writeFile::writeTabbed(os, "Effectiveness [-]");
 
     if (secondaryCpPtr_)
     {
@@ -146,16 +73,14 @@ void Foam::fv::effectivenessHeatExchangerSource::writeFileHeader(Ostream& os)
 
 // * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
 
-Foam::fv::effectivenessHeatExchangerSource::effectivenessHeatExchangerSource
+Foam::heatExchangerModels::effectivenessTable::effectivenessTable
 (
+    const fvMesh& mesh,
     const word& name,
-    const word& modelType,
-    const dictionary& dict,
-    const fvMesh& mesh
+    const dictionary& coeffs
 )
 :
-    fv::cellSetOption(name, modelType, dict, mesh),
-    writeFile(mesh, name, modelType, coeffs_),
+    heatExchangerModel(mesh, name, coeffs),
     userPrimaryInletT_(false),
     targetQdotActive_(false),
     secondaryCpPtr_
@@ -163,7 +88,7 @@ Foam::fv::effectivenessHeatExchangerSource::effectivenessHeatExchangerSource
         Function1<scalar>::NewIfPresent
         (
             "secondaryCp",
-            coeffs_,
+            coeffs,
             word::null,
             &mesh
         )
@@ -175,40 +100,29 @@ Foam::fv::effectivenessHeatExchangerSource::effectivenessHeatExchangerSource
     primaryInletT_(0),
     targetQdot_(0),
     targetQdotRelax_(0.5),
-    UName_("U"),
-    TName_("T"),
-    phiName_("phi"),
-    faceZoneName_("unknown-faceZone"),
-    faceId_(),
-    facePatchId_(),
-    faceSign_()
+    sumPhi_(0),
+    Qt_(0),
+    Tref_(0),
+    effectiveness_(0)
 {
-    read(dict);
-
-    // Set the field name to that of the energy
-    // field from which the temperature is obtained
-
-    const auto& thermo = mesh_.lookupObject<basicThermo>(basicThermo::dictName);
+    writeFileHeader(file());
+}
 
-    fieldNames_.resize(1, thermo.he().name());
 
-    fv::option::resetApplied();
+// * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * * //
 
+void Foam::heatExchangerModels::effectivenessTable::initialise()
+{
     eTable_.reset(new interpolation2DTable<scalar>(coeffs_));
 
-    initialise();
-
-    writeFileHeader(file());
+    heatExchangerModel::initialise();
 }
 
 
-// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
-
-void Foam::fv::effectivenessHeatExchangerSource::addSup
+Foam::tmp<Foam::scalarField>
+Foam::heatExchangerModels::effectivenessTable::energyDensity
 (
-    const volScalarField& rho,
-    fvMatrix<scalar>& eqn,
-    const label
+    const labelList& cells
 )
 {
     const auto& thermo = mesh_.lookupObject<basicThermo>(basicThermo::dictName);
@@ -218,7 +132,7 @@ void Foam::fv::effectivenessHeatExchangerSource::addSup
     const surfaceScalarField Cpf(fvc::interpolate(thermo.Cp()));
     const surfaceScalarField Tf(fvc::interpolate(T));
 
-    scalar sumPhi = 0;
+    sumPhi_ = 0;
     scalar sumMagPhi = 0;
     scalar CpfMean = 0;
     scalar primaryInletTfMean = 0;
@@ -231,7 +145,7 @@ void Foam::fv::effectivenessHeatExchangerSource::addSup
             const scalar phii = phi.boundaryField()[patchi][facei]*faceSign_[i];
             const scalar magPhii = mag(phii);
 
-            sumPhi += phii;
+            sumPhi_ += phii;
             sumMagPhi += magPhii;
 
             const scalar Cpfi = Cpf.boundaryField()[patchi][facei];
@@ -245,7 +159,7 @@ void Foam::fv::effectivenessHeatExchangerSource::addSup
             const scalar phii = phi[facei]*faceSign_[i];
             const scalar magPhii = mag(phii);
 
-            sumPhi += phii;
+            sumPhi_ += phii;
             sumMagPhi += magPhii;
 
             CpfMean += Cpf[facei]*magPhii;
@@ -253,8 +167,9 @@ void Foam::fv::effectivenessHeatExchangerSource::addSup
         }
     }
     reduce(CpfMean, sumOp<scalar>());
-    reduce(sumPhi, sumOp<scalar>());
+    reduce(sumPhi_, sumOp<scalar>());
     reduce(sumMagPhi, sumOp<scalar>());
+
     CpfMean /= sumMagPhi + ROOTVSMALL;
 
     scalar primaryInletT = primaryInletT_;
@@ -264,11 +179,11 @@ void Foam::fv::effectivenessHeatExchangerSource::addSup
         primaryInletT = primaryInletTfMean/(sumMagPhi + ROOTVSMALL);
     }
 
-    const scalar effectiveness = eTable_()(mag(sumPhi), secondaryMassFlowRate_);
+    effectiveness_ = eTable_()(mag(sumPhi_), secondaryMassFlowRate_);
 
-    const scalar alpha = effectiveness*CpfMean*mag(sumPhi);
+    const scalar alpha = effectiveness_*CpfMean*mag(sumPhi_);
 
-    const scalar Qt = alpha*(secondaryInletT_ - primaryInletT);
+    Qt_ = alpha*(secondaryInletT_ - primaryInletT);
 
     if
     (
@@ -276,123 +191,76 @@ void Foam::fv::effectivenessHeatExchangerSource::addSup
      && (mesh_.time().timeIndex() % targetQdotCalcInterval_ == 0)
     )
     {
-        const scalar dT = (targetQdot_ - Qt)/(alpha + ROOTVSMALL);
+        const scalar dT = (targetQdot_ - Qt_)/(alpha + ROOTVSMALL);
         secondaryInletT_ += targetQdotRelax_*dT;
     }
 
-    const scalarField TCells(T, cells_);
-    scalar Tref = 0;
-    scalarField deltaTCells(cells_.size(), Zero);
-    if (Qt > 0)
+    const scalarField TCells(T, cells);
+    scalarField deltaTCells(cells.size(), Zero);
+    Tref_ = 0;
+    if (Qt_ > 0)
     {
-        Tref = gMax(TCells);
+        Tref_ = gMax(TCells);
         forAll(deltaTCells, i)
         {
-            deltaTCells[i] = max(Tref - TCells[i], scalar(0));
+            deltaTCells[i] = max(Tref_ - TCells[i], scalar(0));
         }
     }
     else
     {
-        Tref = gMin(TCells);
+        Tref_ = gMin(TCells);
         forAll(deltaTCells, i)
         {
-            deltaTCells[i] = max(TCells[i] - Tref, scalar(0));
+            deltaTCells[i] = max(TCells[i] - Tref_, scalar(0));
         }
     }
 
-
     const auto& U = mesh_.lookupObject<volVectorField>(UName_);
     const scalarField& V = mesh_.V();
     scalar sumWeight = 0;
-    forAll(cells_, i)
+    forAll(cells, i)
     {
-        const label celli = cells_[i];
+        const label celli = cells[i];
         sumWeight += V[celli]*mag(U[celli])*deltaTCells[i];
     }
     reduce(sumWeight, sumOp<scalar>());
 
-    if (this->V() > VSMALL && mag(Qt) > VSMALL)
-    {
-        scalarField& heSource = eqn.source();
-
-        forAll(cells_, i)
-        {
-            const label celli = cells_[i];
-            heSource[celli] -=
-                Qt*V[celli]*mag(U[celli])*deltaTCells[i]
-               /(sumWeight + ROOTVSMALL);
-        }
-    }
-
-    Log << nl
-        << type() << ": " << name() << nl << incrIndent
-        << indent << "Net mass flux [kg/s]      : " << sumPhi << nl
-        << indent << "Total heat exchange [W]   : " << Qt << nl
-        << indent << "Secondary inlet T [K]     : " << secondaryInletT_ << nl
-        << indent << "Tref [K]                  : " << Tref << nl
-        << indent << "Effectiveness             : " << effectiveness
-        << decrIndent;
-
-    if (Pstream::master())
-    {
-        Ostream& os = file();
-        writeCurrentTime(os);
-
-        os  << tab << sumPhi
-            << tab << Qt
-            << tab << secondaryInletT_
-            << tab << Tref
-            << tab << effectiveness;
-
-        if (secondaryCpPtr_)
-        {
-            // Secondary Cp as a function of the starting secondary temperature
-            const scalar secondaryCp = secondaryCpPtr_->value(secondaryInletT_);
-            const scalar secondaryOutletT =
-                Qt/(secondaryMassFlowRate_*secondaryCp) + secondaryInletT_;
-
-            Log << nl << incrIndent << indent
-                << "Secondary outlet T [K]    : " << secondaryOutletT
-                << decrIndent;
-
-            os  << tab << secondaryOutletT;
-        }
-        os  << endl;
-    }
-
-    Info<< nl << endl;
+    return Qt_*deltaTCells/(sumWeight + ROOTVSMALL);
 }
 
 
-bool Foam::fv::effectivenessHeatExchangerSource::read(const dictionary& dict)
+bool Foam::heatExchangerModels::effectivenessTable::read(const dictionary& dict)
 {
-    if (!(fv::cellSetOption::read(dict) && writeFile::read(dict)))
+    if (!writeFile::read(dict))
     {
         return false;
     }
 
+    Info<< incrIndent << indent << "- using model: " << type() << endl;
+
     coeffs_.readEntry("secondaryMassFlowRate", secondaryMassFlowRate_);
     coeffs_.readEntry("secondaryInletT", secondaryInletT_);
 
     if (coeffs_.readIfPresent("primaryInletT", primaryInletT_))
     {
         userPrimaryInletT_ = true;
-        Info<< type() << " " << this->name() << ": " << indent << nl
-            << "employing user-specified primary flow inlet temperature: "
+
+        Info<< indent
+            << "- using user-specified primary flow inlet temperature: "
             << primaryInletT_ << endl;
     }
     else
     {
-        Info<< type() << " " << this->name() << ": " << indent << nl
-            << "employing flux-weighted primary flow inlet temperature"
+        Info<< indent
+            << "- using flux-weighted primary flow inlet temperature"
             << endl;
     }
 
     if (coeffs_.readIfPresent("targetQdot", targetQdot_))
     {
         targetQdotActive_ = true;
-        Info<< indent << "employing target heat rejection of "
-            << targetQdot_ << nl;
+
+        Info<< indent << "- using target heat rejection: " << targetQdot_ << nl;
 
         coeffs_.readIfPresent
         (
@@ -400,12 +268,12 @@ bool Foam::fv::effectivenessHeatExchangerSource::read(const dictionary& dict)
             targetQdotCalcInterval_
         );
 
-        Info<< indent << "updating secondary inlet temperature every "
+        Info<< indent << "- updating secondary inlet temperature every "
             << targetQdotCalcInterval_ << " iterations" << nl;
 
         coeffs_.readIfPresent("targetQdotRelax", targetQdotRelax_);
 
-        Info<< indent << "temperature relaxation:  "
+        Info<< indent << "- temperature relaxation: "
             << targetQdotRelax_ << endl;
     }
 
@@ -414,8 +282,61 @@ bool Foam::fv::effectivenessHeatExchangerSource::read(const dictionary& dict)
     phiName_ = coeffs_.getOrDefault<word>("phi", "phi");
     coeffs_.readEntry("faceZone", faceZoneName_);
 
+    Info<< decrIndent;
+
     return true;
 }
 
 
+void Foam::heatExchangerModels::effectivenessTable::write
+(
+    const bool log
+)
+{
+    if (log)
+    {
+        Info<< nl
+            << type() << ": " << name_ << nl << incrIndent
+            << indent << "Net mass flux [kg/s]      : " << sumPhi_ << nl
+            << indent << "Total heat exchange [W]   : " << Qt_ << nl
+            << indent << "Secondary inlet T [K]     : " << secondaryInletT_<< nl
+            << indent << "Reference T [K]           : " << Tref_ << nl
+            << indent << "Effectiveness [-]         : " << effectiveness_
+            << decrIndent;
+    }
+
+    if (Pstream::master())
+    {
+        Ostream& os = file();
+        writeCurrentTime(os);
+
+        os  << tab << sumPhi_
+            << tab << Qt_
+            << tab << secondaryInletT_
+            << tab << Tref_
+            << tab << effectiveness_;
+
+        if (secondaryCpPtr_)
+        {
+            // Secondary Cp as a function of the starting secondary temperature
+            const scalar secondaryCp = secondaryCpPtr_->value(secondaryInletT_);
+            const scalar secondaryOutletT =
+                Qt_/(secondaryMassFlowRate_*secondaryCp) + secondaryInletT_;
+
+            if (log)
+            {
+                Info << nl << incrIndent << indent
+                    << "Secondary outlet T [K]    : " << secondaryOutletT
+                    << decrIndent;
+            }
+
+            os  << tab << secondaryOutletT;
+        }
+        os  << endl;
+    }
+
+    Info<< nl << endl;
+}
+
+
 // ************************************************************************* //
diff --git a/src/fvOptions/sources/derived/effectivenessHeatExchangerSource/effectivenessHeatExchangerSource.H b/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/effectivenessTable/effectivenessTable.H
similarity index 63%
rename from src/fvOptions/sources/derived/effectivenessHeatExchangerSource/effectivenessHeatExchangerSource.H
rename to src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/effectivenessTable/effectivenessTable.H
index b443e751e657431005258ed5d617e7e3f0f8f8d1..476713f8e2f8b399a236cda89203051492506d09 100644
--- a/src/fvOptions/sources/derived/effectivenessHeatExchangerSource/effectivenessHeatExchangerSource.H
+++ b/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/effectivenessTable/effectivenessTable.H
@@ -5,8 +5,7 @@
     \\  /    A nd           | www.openfoam.com
      \\/     M anipulation  |
 -------------------------------------------------------------------------------
-    Copyright (C) 2013-2017 OpenFOAM Foundation
-    Copyright (C) 2016-2022 OpenCFD Ltd.
+    Copyright (C) 2022 OpenCFD Ltd.
 -------------------------------------------------------------------------------
 License
     This file is part of OpenFOAM.
@@ -25,18 +24,15 @@ License
     along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.
 
 Class
-    Foam::fv::effectivenessHeatExchangerSource
-
-Group
-    grpFvOptionsSources
+    Foam::heatExchangerModels::effectivenessTable
 
 Description
-    Heat exchanger source model for compressible flows, where the heat
-    exchanger is modelled as an energy source using a selection of cells.
+    A heat exchanger model where heat exchange
+    is calculated via an effectiveness table.
 
     The total heat exchange source is given by:
     \f[
-        Q_t = e(\phi, \dot{m}_2) (T_2 - T_1) \phi c_p
+        Q_t = e(\phi, \dot{m}_2) (T_2 - T_1) \phi C_p
     \f]
 
     where:
@@ -47,10 +43,9 @@ Description
         \dot{m}_2 | Secondary flow mass flow rate [kg/s]
         T_1       | Primary flow inlet temperature [K]
         T_2       | Secondary flow inlet temperature [K]
-        c_p       | Primary flow specific heat capacity [J/kg/K]
+        C_p       | Primary flow specific heat capacity [J/kg/K]
     \endvartable
 
-
     The distribution inside the heat exchanger is given by:
     \f[
         Q_c = \frac{V_c |U_c| (T_c - T_{ref})}{\sum(V_c |U_c| (T_c - T_{ref}))}
@@ -59,42 +54,26 @@ Description
     where:
     \vartable
         Q_c     | Source for cell
-        V_c     | Volume of the cell [m3]
+        V_c     | Volume of the cell [m^3]
         U_c     | Local cell velocity [m/s]
         T_c     | Local cell temperature [K]
         T_{ref} | Min or max(T) in cell zone depending on the sign of Qt [K]
     \endvartable
 
-    Sources applied to either of the below, if exist:
-    \verbatim
-      e         | Internal energy                            [m2/s2]
-      h         | Enthalphy                                  [m2/s2]
-    \endverbatim
-
-    Required fields:
-    \verbatim
-      T         | Temperature                                [K]
-      U         | Velocity                                   [m/s]
-      phi       | Mass flux                                  [kg/s]
-    \endverbatim
-
 Usage
     Minimal example by using \c constant/fvOptions:
     \verbatim
-    effectivenessHeatExchangerSource1
+    <name>
     {
+        // Inherited entries
+        ...
+
         // Mandatory entries
-        type                     effectivenessHeatExchangerSource;
-        faceZone                 <faceZoneName>;
+        model                    effectivenessTable;
         secondaryMassFlowRate    <scalar>;
         secondaryInletT          <scalar>;
-        file                     "effectivenessTable";
-        outOfBounds              clamp;
-
-        // Optional entries
-        U                        <word>;
-        T                        <word>;
-        phi                      <word>;
+        file                     "<word>";
+        outOfBounds              <word>;
 
         // Conditional optional entries
 
@@ -108,23 +87,17 @@ Usage
 
             // when secondary outlet temperature is requested
             secondaryCp             <Function1<scalar>>;
-
-        // Inherited entries
-        ...
     }
     \endverbatim
 
     where the entries mean:
     \table
       Property  | Description                       | Type  | Reqd | Deflt
-      type      | Type name: effectivenessHeatExchangerSource <!--
-                -->                                 | word  | yes  | -
-      secondaryMassFlowRate | Secondary flow mass rate [kg/s] <!--
+      model     | Model name:effectivenessTable     | word  | yes  | -
+      secondaryMassFlowRate | Secondary flow mass rate [kg/s]     <!--
                 -->                                 | scalar | yes | -
-      secondaryInletT | Secondary flow inlet temperature [K]  <!--
+      secondaryInletT | Secondary flow inlet temperature [K]      <!--
                 -->                                 | scalar | yes | -
-      faceZone  | Name of the faceZone at the heat exchanger inlet <!--
-                -->                                 | word   | yes | -
       file      | 2D effectiveness table = function of primary    <!--
                 --> and secondary mass flow rates [kg/s] | file | yes | -
       primaryInletT | Primary flow temperature at the heat exchanger inlet <!--
@@ -136,16 +109,11 @@ Usage
                 --> under-relaxation coefficient    | scalar | no  | -
       secondaryCp | Secondary flow specific heat capacity <!--
                 -->                    | Function1\<scalar\> | no  | -
-      U         | Name of operand velocity field    | word   | no  | U
-      T         | Name of operand temperature field | word   | no  | T
-      phi       | Name of operand flux field        | word   | no  | phi
     \endtable
 
     The inherited entries are elaborated in:
-      - \link fvOption.H \endlink
-      - \link cellSetOption.H \endlink
-      - \link writeFile.H \endlink
-      - \link Function1.H \endlink
+      - \link heatExchangerSource.H \endlink
+      - \link interpolation2DTable.H \endlink
 
     The effectiveness table is described in terms of the primary and secondary
     mass flow rates.  For example, the table:
@@ -202,34 +170,31 @@ Note
     flux-averaged temperature is used.
 
 SourceFiles
-    effectivenessHeatExchangerSource.C
+    effectivenessTable.C
 
 \*---------------------------------------------------------------------------*/
 
-#ifndef fv_effectivenessHeatExchangerSource_H
-#define fv_effectivenessHeatExchangerSource_H
+#ifndef Foam_heatExchangerModels_effectivenessTable_H
+#define Foam_heatExchangerModels_effectivenessTable_H
 
-#include "cellSetOption.H"
-#include "autoPtr.H"
+#include "heatExchangerModel.H"
 #include "interpolation2DTable.H"
-#include "writeFile.H"
 #include "Function1.H"
 
 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
 
 namespace Foam
 {
-namespace fv
+namespace heatExchangerModels
 {
 
 /*---------------------------------------------------------------------------*\
-              Class effectivenessHeatExchangerSource Declaration
+                              Class effectivenessTable Declaration
 \*---------------------------------------------------------------------------*/
 
-class effectivenessHeatExchangerSource
+class effectivenessTable
 :
-    public fv::cellSetOption,
-    public functionObjects::writeFile
+    public heatExchangerModel
 {
     // Private Data
 
@@ -264,100 +229,78 @@ class effectivenessHeatExchangerSource
         //- Target heat rejection temperature under-relaxation coefficient
         scalar targetQdotRelax_;
 
-        //- Name of operand velocity field
-        word UName_;
-
-        //- Name of operand temperature field
-        word TName_;
+        //- Net mass flux [kg/s]
+        scalar sumPhi_;
 
-        //- Name of operand flux field
-        word phiName_;
+        //- Total heat exchange [W]
+        scalar Qt_;
 
-        //- Name of the faceZone at the heat exchanger inlet
-        word faceZoneName_;
+        //- Reference temperature [K]
+        scalar Tref_;
 
-        //- Local list of face IDs
-        labelList faceId_;
-
-        //- Local list of patch IDs per face
-        labelList facePatchId_;
-
-        //- List of +1/-1 representing face flip map (1 use as is, -1 negate)
-        labelList faceSign_;
+        //- Effectiveness of the heat exchanger [-]
+        scalar effectiveness_;
 
 
     // Private Member Functions
 
-        //- Initialise heat exchanger source model
-        void initialise();
+    // I-O
 
-        //- Output file header information
-        virtual void writeFileHeader(Ostream& os);
+        //- Write file header information
+        void writeFileHeader(Ostream& os) const;
 
 
 public:
 
     //- Runtime type information
-    TypeName("effectivenessHeatExchangerSource");
+    TypeName("effectivenessTable");
 
 
     // Constructors
 
         //- Construct from components
-        effectivenessHeatExchangerSource
+        effectivenessTable
         (
+            const fvMesh& mesh,
             const word& name,
-            const word& modelType,
-            const dictionary& dict,
-            const fvMesh& mesh
+            const dictionary& coeffs
         );
 
         //- No copy construct
-        effectivenessHeatExchangerSource
-        (
-            const effectivenessHeatExchangerSource&
-        ) = delete;
+        effectivenessTable(const effectivenessTable&) = delete;
 
         //- No copy assignment
-        void operator=(const effectivenessHeatExchangerSource&) = delete;
+        void operator=(const effectivenessTable&) = delete;
 
 
     //- Destructor
-    virtual ~effectivenessHeatExchangerSource() = default;
+    virtual ~effectivenessTable() = default;
 
 
     // Member Functions
 
-        //- Add explicit/implicit contribution to momentum equation
-        virtual void addSup
-        (
-            fvMatrix<scalar>& eqn,
-            const label fieldi
-        )
-        {
-            NotImplemented;
-        }
-
-        //- Add explicit/implicit contribution
-        //- to compressible momentum equation
-        virtual void addSup
-        (
-            const volScalarField& rho,
-            fvMatrix<scalar>& eqn,
-            const label fieldi
-        );
+    // Evaluation
+
+        //- Initialise data members of the model
+        virtual void initialise();
+
+        //- Return energy density per unit length [J/m3/m]
+        virtual tmp<scalarField> energyDensity(const labelList& cells);
+
 
+    // I-O
 
-        // I-O
+        //- Read top-level dictionary
+        virtual bool read(const dictionary& dict);
 
-            //- Read dictionary
-            virtual bool read(const dictionary& dict);
+        //- Write data to stream and files
+        virtual void write(const bool log);
 };
 
 
 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
 
-} // End namespace fv
+} // End namespace heatExchangerModels
 } // End namespace Foam
 
 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
diff --git a/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/heatExchangerModel/heatExchangerModel.C b/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/heatExchangerModel/heatExchangerModel.C
new file mode 100644
index 0000000000000000000000000000000000000000..9034a02b9182961c883d6f00fd0ad78c4ba1070c
--- /dev/null
+++ b/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/heatExchangerModel/heatExchangerModel.C
@@ -0,0 +1,138 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2022 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 "heatExchangerModel.H"
+#include "coupledPolyPatch.H"
+#include "surfaceInterpolate.H"
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+namespace Foam
+{
+    defineTypeNameAndDebug(heatExchangerModel, 0);
+    defineRunTimeSelectionTable(heatExchangerModel, dictionary);
+}
+
+
+// * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
+
+Foam::heatExchangerModel::heatExchangerModel
+(
+    const fvMesh& mesh,
+    const word& name,
+    const dictionary& coeffs
+)
+:
+    writeFile(mesh, name, typeName, coeffs),
+    mesh_(mesh),
+    coeffs_(coeffs),
+    name_(name),
+    UName_("U"),
+    TName_("T"),
+    phiName_("phi"),
+    faceZoneName_("unknown-faceZone"),
+    faceId_(),
+    facePatchId_(),
+    faceSign_()
+{}
+
+
+// * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * * //
+
+void Foam::heatExchangerModel::initialise()
+{
+    const label zoneID = mesh_.faceZones().findZoneID(faceZoneName_);
+
+    if (zoneID < 0)
+    {
+        FatalErrorInFunction
+            << type() << " " << name_ << ": "
+            << "    Unknown face zone name: " << faceZoneName_
+            << ". Valid face zones are: " << mesh_.faceZones().names()
+            << exit(FatalError);
+    }
+
+    const faceZone& fZone = mesh_.faceZones()[zoneID];
+
+    faceId_.setSize(fZone.size());
+    facePatchId_.setSize(fZone.size());
+    faceSign_.setSize(fZone.size());
+
+    label count = 0;
+    forAll(fZone, i)
+    {
+        const label facei = fZone[i];
+        label faceId = -1;
+        label facePatchId = -1;
+        if (mesh_.isInternalFace(facei))
+        {
+            faceId = facei;
+            facePatchId = -1;
+        }
+        else
+        {
+            facePatchId = mesh_.boundaryMesh().whichPatch(facei);
+            const polyPatch& pp = mesh_.boundaryMesh()[facePatchId];
+            const auto* cpp = isA<coupledPolyPatch>(pp);
+
+            if (cpp)
+            {
+                faceId = (cpp->owner() ? pp.whichFace(facei) : -1);
+            }
+            else if (!isA<emptyPolyPatch>(pp))
+            {
+                faceId = pp.whichFace(facei);
+            }
+            else
+            {
+                faceId = -1;
+                facePatchId = -1;
+            }
+        }
+
+        if (faceId >= 0)
+        {
+            if (fZone.flipMap()[i])
+            {
+                faceSign_[count] = -1;
+            }
+            else
+            {
+                faceSign_[count] = 1;
+            }
+            faceId_[count] = faceId;
+            facePatchId_[count] = facePatchId;
+            count++;
+        }
+    }
+    faceId_.setSize(count);
+    facePatchId_.setSize(count);
+    faceSign_.setSize(count);
+}
+
+
+// ************************************************************************* //
diff --git a/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/heatExchangerModel/heatExchangerModel.H b/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/heatExchangerModel/heatExchangerModel.H
new file mode 100644
index 0000000000000000000000000000000000000000..39e6ebdb590ec746704f0959e6c31d3375ad5de4
--- /dev/null
+++ b/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/heatExchangerModel/heatExchangerModel.H
@@ -0,0 +1,191 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2022 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::heatExchangerModels
+
+Description
+    A namespace for various heat exchanger model implementations.
+
+Class
+    Foam::heatExchangerModel
+
+Description
+    Base class for heat exchanger models to handle various
+    characteristics for the \c heatExchangerSource fvOption.
+
+SourceFiles
+    heatExchangerModel.C
+    heatExchangerModelNew.C
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef Foam_heatExchangerModel_H
+#define Foam_heatExchangerModel_H
+
+#include "fvMesh.H"
+#include "writeFile.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+/*---------------------------------------------------------------------------*\
+                    Class heatExchangerModel Declaration
+\*---------------------------------------------------------------------------*/
+
+class heatExchangerModel
+:
+    public functionObjects::writeFile
+{
+protected:
+
+    // Protected Data
+
+        //- Reference to the mesh
+        const fvMesh& mesh_;
+
+        //- Dictionary containing coefficients specific to the chosen model
+        const dictionary& coeffs_;
+
+        //- Reference to the name of the fvOption source
+        const word& name_;
+
+        //- Name of operand velocity field
+        word UName_;
+
+        //- Name of operand temperature field
+        word TName_;
+
+        //- Name of operand flux field
+        word phiName_;
+
+        //- Name of the faceZone at the heat exchanger inlet
+        word faceZoneName_;
+
+        //- Local list of face IDs
+        labelList faceId_;
+
+        //- Local list of patch IDs per face
+        labelList facePatchId_;
+
+        //- List of +1/-1 representing face flip map (1 use as is, -1 negate)
+        labelList faceSign_;
+
+
+public:
+
+    //- Runtime type information
+    TypeName("heatExchangerModel");
+
+
+    // Declare runtime constructor selection table
+
+        declareRunTimeSelectionTable
+        (
+            autoPtr,
+            heatExchangerModel,
+            dictionary,
+            (
+                const fvMesh& mesh,
+                const word& name,
+                const dictionary& coeffs
+            ),
+            (mesh, name, coeffs)
+        );
+
+
+    // Selectors
+
+        //- Return a reference to the selected heat exchanger model
+        static autoPtr<heatExchangerModel> New
+        (
+            const fvMesh& mesh,
+            const word& name,
+            const dictionary& coeffs
+        );
+
+
+    // Constructors
+
+        //- Construct from components
+        heatExchangerModel
+        (
+            const fvMesh& mesh,
+            const word& name,
+            const dictionary& coeffs
+        );
+
+        //- No copy construct
+        heatExchangerModel(const heatExchangerModel&) = delete;
+
+        //- No copy assignment
+        void operator=(const heatExchangerModel&) = delete;
+
+
+    //- Destructor
+    virtual ~heatExchangerModel() = default;
+
+
+    // Member Functions
+
+    // Access
+
+        //- Return const reference to the name of velocity field
+        virtual const word& U() const
+        {
+            return UName_;
+        }
+
+
+    // Evaluation
+
+        //- Initialise data members of the model
+        virtual void initialise();
+
+        //- Return energy density per unit length [J/m3/m]
+        virtual tmp<scalarField> energyDensity(const labelList& cells) = 0;
+
+
+    // I-O
+
+        //- Read top-level dictionary
+        virtual bool read(const dictionary& dict) = 0;
+
+        //- Write data to stream and files
+        virtual void write(const bool log) = 0;
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
diff --git a/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/heatExchangerModel/heatExchangerModelNew.C b/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/heatExchangerModel/heatExchangerModelNew.C
new file mode 100644
index 0000000000000000000000000000000000000000..90acf6074e32e88570c048d0c7e3d37f70e8889a
--- /dev/null
+++ b/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/heatExchangerModel/heatExchangerModelNew.C
@@ -0,0 +1,58 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2022 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 "heatExchangerModel.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+Foam::autoPtr<Foam::heatExchangerModel> Foam::heatExchangerModel::New
+(
+    const fvMesh& mesh,
+    const word& name,
+    const dictionary& coeffs
+)
+{
+    const word modelType(coeffs.get<word>("model"));
+
+    auto* ctorPtr = dictionaryConstructorTable(modelType);
+
+    if (!ctorPtr)
+    {
+        FatalIOErrorInLookup
+        (
+            coeffs,
+            "heatExchangerModel",
+            modelType,
+            *dictionaryConstructorTablePtr_
+        ) << exit(FatalIOError);
+    }
+
+    return autoPtr<heatExchangerModel>(ctorPtr(mesh, name, coeffs));
+}
+
+
+// ************************************************************************* //
diff --git a/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/referenceTemperature/referenceTemperature.C b/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/referenceTemperature/referenceTemperature.C
new file mode 100644
index 0000000000000000000000000000000000000000..c022de3f21213646277002f3165c7a76d4497b58
--- /dev/null
+++ b/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/referenceTemperature/referenceTemperature.C
@@ -0,0 +1,311 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2022 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 "referenceTemperature.H"
+#include "fvMesh.H"
+#include "basicThermo.H"
+#include "surfaceInterpolate.H"
+#include "addToRunTimeSelectionTable.H"
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace heatExchangerModels
+{
+    defineTypeNameAndDebug(referenceTemperature, 0);
+    addToRunTimeSelectionTable
+    (
+        heatExchangerModel,
+        referenceTemperature,
+        dictionary
+    );
+}
+}
+
+
+// * * * * * * * * * * * * * Private Member Functions  * * * * * * * * * * * //
+
+Foam::scalar
+Foam::heatExchangerModels::referenceTemperature::primaryNetMassFlux() const
+{
+    const auto& phi = mesh_.lookupObject<surfaceScalarField>(phiName_);
+
+    scalar sumPhi = 0;
+
+    forAll(faceId_, i)
+    {
+        const label facei = faceId_[i];
+        if (facePatchId_[i] != -1)
+        {
+            const label patchi = facePatchId_[i];
+            sumPhi += phi.boundaryField()[patchi][facei]*faceSign_[i];
+        }
+        else
+        {
+            sumPhi += phi[facei]*faceSign_[i];
+        }
+    }
+    reduce(sumPhi, sumOp<scalar>());
+
+    return sumPhi;
+}
+
+
+Foam::scalar
+Foam::heatExchangerModels::referenceTemperature::primaryInletTemperature() const
+{
+    const auto& phi = mesh_.lookupObject<surfaceScalarField>(phiName_);
+    const auto& T = mesh_.lookupObject<volScalarField>(TName_);
+
+    const surfaceScalarField Tf(fvc::interpolate(T));
+
+    scalar sumMagPhi = 0;
+    scalar primaryInletTfMean = 0;
+
+    forAll(faceId_, i)
+    {
+        const label facei = faceId_[i];
+        if (facePatchId_[i] != -1)
+        {
+            const label patchi = facePatchId_[i];
+            const scalar phii = phi.boundaryField()[patchi][facei]*faceSign_[i];
+            const scalar magPhii = mag(phii);
+
+            sumMagPhi += magPhii;
+
+            const scalar Tfi = Tf.boundaryField()[patchi][facei];
+
+            primaryInletTfMean += Tfi*magPhii;
+        }
+        else
+        {
+            const scalar phii = phi[facei]*faceSign_[i];
+            const scalar magPhii = mag(phii);
+
+            sumMagPhi += magPhii;
+
+            primaryInletTfMean += Tf[facei]*magPhii;
+        }
+    }
+    reduce(sumMagPhi, sumOp<scalar>());
+    reduce(primaryInletTfMean, sumOp<scalar>());
+
+    return primaryInletTfMean/(sumMagPhi + ROOTVSMALL);
+}
+
+
+Foam::scalarField
+Foam::heatExchangerModels::referenceTemperature::temperatureDifferences
+(
+    const labelList& cells
+) const
+{
+    const auto& T = mesh_.lookupObject<volScalarField>(TName_);
+    const scalarField TCells(T, cells);
+    scalarField deltaTCells(cells.size(), Zero);
+
+    if (Qt_ > 0)
+    {
+        forAll(deltaTCells, i)
+        {
+            deltaTCells[i] = max(Tref_ - TCells[i], scalar(0));
+        }
+    }
+    else
+    {
+        forAll(deltaTCells, i)
+        {
+            deltaTCells[i] = max(TCells[i] - Tref_, scalar(0));
+        }
+    }
+
+    return deltaTCells;
+}
+
+
+Foam::scalar
+Foam::heatExchangerModels::referenceTemperature::weight
+(
+    const labelList& cells,
+    const scalarField& deltaTCells
+) const
+{
+    scalar sumWeight = 0;
+
+    const auto& U = mesh_.lookupObject<volVectorField>(UName_);
+    const scalarField& V = mesh_.V();
+
+    forAll(cells, i)
+    {
+        const label celli = cells[i];
+        sumWeight += V[celli]*mag(U[celli])*deltaTCells[i];
+    }
+    reduce(sumWeight, sumOp<scalar>());
+
+    return sumWeight;
+}
+
+
+void Foam::heatExchangerModels::referenceTemperature::writeFileHeader
+(
+    Ostream& os
+) const
+{
+    writeFile::writeHeader(os, "Effectiveness heat exchanger source");
+    writeFile::writeCommented(os, "Time");
+    writeFile::writeTabbed(os, "Net mass flux [kg/s]");
+    writeFile::writeTabbed(os, "Total heat exchange [W]");
+    writeFile::writeTabbed(os, "Reference T [K]");
+    os  << endl;
+}
+
+
+// * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
+
+Foam::heatExchangerModels::referenceTemperature::referenceTemperature
+(
+    const fvMesh& mesh,
+    const word& name,
+    const dictionary& coeffs
+)
+:
+    heatExchangerModel(mesh, name, coeffs),
+    targetQdotPtr_
+    (
+        Function1<scalar>::New
+        (
+            "targetQdot",
+            coeffs,
+            &mesh_
+        )
+    ),
+    TrefTablePtr_(nullptr),
+    sumPhi_(0),
+    Qt_(0),
+    Tref_(0)
+{
+    writeFileHeader(file());
+}
+
+
+// * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * * //
+
+void Foam::heatExchangerModels::referenceTemperature::initialise()
+{
+    heatExchangerModel::initialise();
+}
+
+
+Foam::tmp<Foam::scalarField>
+Foam::heatExchangerModels::referenceTemperature::energyDensity
+(
+    const labelList& cells
+)
+{
+    sumPhi_ = primaryNetMassFlux();
+
+    Qt_ = targetQdotPtr_->value(mesh_.time().value());
+
+    if (TrefTablePtr_)
+    {
+        const scalar primaryInletT = primaryInletTemperature();
+        Tref_ = TrefTablePtr_()(mag(sumPhi_), primaryInletT);
+    }
+
+    const scalarField deltaTCells(temperatureDifferences(cells));
+
+    const scalar sumWeight = weight(cells, deltaTCells);
+
+    return Qt_*deltaTCells/(sumWeight + ROOTVSMALL);
+}
+
+
+bool Foam::heatExchangerModels::referenceTemperature::read
+(
+    const dictionary& dict
+)
+{
+    if (!writeFile::read(dict))
+    {
+        return false;
+    }
+
+    Info<< incrIndent << indent << "- using model: " << type() << endl;
+
+    if (coeffs_.readIfPresent("Tref", Tref_))
+    {
+        Info<< indent << "- using constant reference temperature: " << Tref_
+            << endl;
+    }
+    else
+    {
+        TrefTablePtr_.reset(new interpolation2DTable<scalar>(coeffs_));
+
+        Info<< indent << "- using reference temperature table"
+            << endl;
+    }
+
+    UName_ = coeffs_.getOrDefault<word>("U", "U");
+    TName_ = coeffs_.getOrDefault<word>("T", "T");
+    phiName_ = coeffs_.getOrDefault<word>("phi", "phi");
+    coeffs_.readEntry("faceZone", faceZoneName_);
+
+    Info<< decrIndent;
+
+    return true;
+}
+
+
+void Foam::heatExchangerModels::referenceTemperature::write(const bool log)
+{
+    if (log)
+    {
+        Info<< nl
+            << type() << ": " << name_ << nl << incrIndent
+            << indent << "Net mass flux [kg/s]      : " << sumPhi_ << nl
+            << indent << "Total heat exchange [W]   : " << Qt_ << nl
+            << indent << "Reference T [K]           : " << Tref_
+            << decrIndent;
+    }
+
+    if (Pstream::master())
+    {
+        Ostream& os = file();
+        writeCurrentTime(os);
+
+        os  << tab << sumPhi_
+            << tab << Qt_
+            << tab << Tref_
+            << endl;
+    }
+
+    if (log) Info<< nl << endl;
+}
+
+
+// ************************************************************************* //
diff --git a/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/referenceTemperature/referenceTemperature.H b/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/referenceTemperature/referenceTemperature.H
new file mode 100644
index 0000000000000000000000000000000000000000..123f088140f967afb8f367e666a4cac5dc57f63b
--- /dev/null
+++ b/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerModels/referenceTemperature/referenceTemperature.H
@@ -0,0 +1,202 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2022 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::heatExchangerModels::referenceTemperature
+
+Description
+    A heat exchanger model where heat exchange
+    is calculated via a temperature table.
+
+Usage
+    Minimal example by using \c constant/fvOptions:
+    \verbatim
+    <name>
+    {
+        // Inherited entries
+        ...
+
+        // Mandatory entries
+        model                    referenceTemperature;
+        targetQdot               <Function1<scalar>>;
+
+        // Conditional mandatory entries
+
+            // Option-1
+            Tref                     <scalar>;
+
+            // Option-2
+            file                     "<word>";
+    }
+    \endverbatim
+
+    where the entries mean:
+    \table
+      Property  | Description                       | Type  | Reqd | Deflt
+      model     | Model name:effectivenessTable     | word  | yes  | -
+      targetQdot | Target heat rejection [J/s]                    <!--
+                -->                    | Function1\<scalar\> | yes | -
+      Tref      | Reference temperature [K]         | scalar | choice | -
+      file      | 2D reference temperature table = function of primary    <!--
+                --> mass flow rate [kg/s] and primary flow temperature    <!--
+                -->                                 | file   | choice | -
+    \endtable
+
+    The inherited entries are elaborated in:
+      - \link heatExchangerSource.H \endlink
+      - \link interpolation2DTable.H \endlink
+
+SourceFiles
+    referenceTemperature.C
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef Foam_heatExchangerModels_referenceTemperature_H
+#define Foam_heatExchangerModels_referenceTemperature_H
+
+#include "heatExchangerModel.H"
+#include "interpolation2DTable.H"
+#include "Function1.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace heatExchangerModels
+{
+
+/*---------------------------------------------------------------------------*\
+                    Class referenceTemperature Declaration
+\*---------------------------------------------------------------------------*/
+
+class referenceTemperature
+:
+    public heatExchangerModel
+{
+    // Private Data
+
+        //- Target heat rejection [J/s]
+        autoPtr<Function1<scalar>> targetQdotPtr_;
+
+        //- 2D reference temperature table
+        //- Function of primary mass flow rate [kg/s]
+        //- and primary flow temperature [K]
+        autoPtr<interpolation2DTable<scalar>> TrefTablePtr_;
+
+        //- Net mass flux [kg/s]
+        scalar sumPhi_;
+
+        //- Total heat exchange [J/s]
+        scalar Qt_;
+
+        //- Reference temperature [K]
+        scalar Tref_;
+
+
+    // Private Member Functions
+
+    // Evaluation
+
+        //- Return primary-flow net mass flux
+        scalar primaryNetMassFlux() const;
+
+        //- Return primary-flow inlet temperature
+        scalar primaryInletTemperature() const;
+
+        //- Return temperature differences
+        scalarField temperatureDifferences(const labelList& cells) const;
+
+        //- Return interim weights
+        scalar weight
+        (
+            const labelList& cells,
+            const scalarField& deltaTCells
+        ) const;
+
+
+    // I-O
+
+        //- Write file header information
+        void writeFileHeader(Ostream& os) const;
+
+
+public:
+
+    //- Runtime type information
+    TypeName("referenceTemperature");
+
+
+    // Constructors
+
+        //- Construct from components
+        referenceTemperature
+        (
+            const fvMesh& mesh,
+            const word& name,
+            const dictionary& coeffs
+        );
+
+        //- No copy construct
+        referenceTemperature(const referenceTemperature&) = delete;
+
+        //- No copy assignment
+        void operator=(const referenceTemperature&) = delete;
+
+
+    //- Destructor
+    virtual ~referenceTemperature() = default;
+
+
+    // Member Functions
+
+    // Evaluation
+
+        //- Initialise data members of the model
+        virtual void initialise();
+
+        //- Return energy density per unit length [J/m3/m]
+        virtual tmp<scalarField> energyDensity(const labelList& cells);
+
+
+    // I-O
+
+        //- Read top-level dictionary
+        virtual bool read(const dictionary& dict);
+
+        //- Write data to stream and files
+        virtual void write(const bool log);
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace heatExchangerModels
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //
diff --git a/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerSource.C b/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerSource.C
new file mode 100644
index 0000000000000000000000000000000000000000..2a3cdb3dca526d63972e9dd4b5eca3e7097d3d32
--- /dev/null
+++ b/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerSource.C
@@ -0,0 +1,116 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2013-2015 OpenFOAM Foundation
+    Copyright (C) 2016-2022 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 "heatExchangerSource.H"
+#include "heatExchangerModel.H"
+#include "fvMatrix.H"
+#include "basicThermo.H"
+#include "addToRunTimeSelectionTable.H"
+
+// * * * * * * * * * * * * * * Static Data Members * * * * * * * * * * * * * //
+
+namespace Foam
+{
+namespace fv
+{
+    defineTypeNameAndDebug(heatExchangerSource, 0);
+    addToRunTimeSelectionTable(option, heatExchangerSource, dictionary);
+}
+}
+
+
+// * * * * * * * * * * * * * * * * Constructors  * * * * * * * * * * * * * * //
+
+Foam::fv::heatExchangerSource::heatExchangerSource
+(
+    const word& name,
+    const word& modelType,
+    const dictionary& dict,
+    const fvMesh& mesh
+)
+:
+    fv::cellSetOption(name, modelType, dict, mesh),
+    heatExchangerModelPtr_(heatExchangerModel::New(mesh, name, coeffs_))
+{
+    read(dict);
+
+    // Set the field name to that of the energy
+    // field from which the temperature is obtained
+
+    const auto& thermo = mesh_.lookupObject<basicThermo>(basicThermo::dictName);
+
+    fieldNames_.resize(1, thermo.he().name());
+
+    fv::option::resetApplied();
+
+    heatExchangerModelPtr_->initialise();
+}
+
+
+// * * * * * * * * * * * * * * * Member Functions  * * * * * * * * * * * * * //
+
+void Foam::fv::heatExchangerSource::addSup
+(
+    const volScalarField& rho,
+    fvMatrix<scalar>& eqn,
+    const label
+)
+{
+    const scalarField Q(heatExchangerModelPtr_->energyDensity(cells_));
+
+    if (this->V() > VSMALL)
+    {
+        const word& UName = heatExchangerModelPtr_->U();
+        const auto& U = mesh_.lookupObject<volVectorField>(UName);
+
+        const scalarField& V = mesh_.V();
+        scalarField& heSource = eqn.source();
+
+        forAll(cells_, i)
+        {
+            const label celli = cells_[i];
+            heSource[celli] -= Q[i]*V[celli]*mag(U[celli]);
+        }
+    }
+
+    heatExchangerModelPtr_->write(log);
+}
+
+
+bool Foam::fv::heatExchangerSource::read(const dictionary& dict)
+{
+    if (!fv::cellSetOption::read(dict) || !heatExchangerModelPtr_->read(dict))
+    {
+        return false;
+    }
+
+    return true;
+}
+
+
+// ************************************************************************* //
diff --git a/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerSource.H b/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerSource.H
new file mode 100644
index 0000000000000000000000000000000000000000..cf3d7d0c54e54cff8c9f8c792d808ea1b4f6b495
--- /dev/null
+++ b/src/fvOptions/sources/derived/heatExchangerSource/heatExchangerSource.H
@@ -0,0 +1,201 @@
+/*---------------------------------------------------------------------------*\
+  =========                 |
+  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
+   \\    /   O peration     |
+    \\  /    A nd           | www.openfoam.com
+     \\/     M anipulation  |
+-------------------------------------------------------------------------------
+    Copyright (C) 2013-2017 OpenFOAM Foundation
+    Copyright (C) 2016-2022 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::fv::heatExchangerSource
+
+Group
+    grpFvOptionsSources
+
+Description
+    Heat exchanger source model for compressible flows, where the heat
+    exchanger is modelled as an energy source using a selection of cells.
+
+    Sources applied to either of the below, if exist:
+    \verbatim
+      e         | Internal energy                            [m2/s2]
+      h         | Enthalphy                                  [m2/s2]
+    \endverbatim
+
+    Required fields:
+    \verbatim
+      T         | Temperature                                [K]
+      U         | Velocity                                   [m/s]
+      phi       | Mass flux                                  [kg/s]
+    \endverbatim
+
+Usage
+    Minimal example by using \c constant/fvOptions:
+    \verbatim
+    <name>
+    {
+        // Mandatory entries
+        type                     heatExchangerSource;
+        model                    <word>;
+        faceZone                 <word>;
+
+        // Conditional entries
+
+            // Option-1: when model == effectivenessTable
+
+            // Option-2: when model == referenceTemperature
+
+        // Optional entries
+        U                        <word>;
+        T                        <word>;
+        phi                      <word>;
+
+        // Inherited entries
+        ...
+    }
+    \endverbatim
+
+    where the entries mean:
+    \table
+      Property  | Description                       | Type  | Reqd | Deflt
+      type      | Type name: heatExchangerSource    | word  | yes  | -
+      model     | Name of the heat exchanger model  | word  | yes  | -
+      faceZone  | Name of the faceZone at the heat exchanger inlet <!--
+                -->                                 | word   | yes | -
+      U         | Name of operand velocity field    | word   | no  | U
+      T         | Name of operand temperature field | word   | no  | T
+      phi       | Name of operand flux field        | word   | no  | phi
+    \endtable
+
+    Options for the \c model entry:
+    \verbatim
+      effectivenessTable    | Calculate heat exchange via an effectiveness table
+      referenceTemperature  | Calculate heat exchange via a temperature table
+    \endverbatim
+
+    The inherited entries are elaborated in:
+      - \link fvOption.H \endlink
+      - \link cellSetOption.H \endlink
+      - \link writeFile.H \endlink
+      - \link Function1.H \endlink
+
+SourceFiles
+    heatExchangerSource.C
+
+\*---------------------------------------------------------------------------*/
+
+#ifndef Foam_fv_heatExchangerSource_H
+#define Foam_fv_heatExchangerSource_H
+
+#include "cellSetOption.H"
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+namespace Foam
+{
+
+// Forward Declarations
+class heatExchangerModel;
+
+namespace fv
+{
+
+/*---------------------------------------------------------------------------*\
+                    Class heatExchangerSource Declaration
+\*---------------------------------------------------------------------------*/
+
+class heatExchangerSource
+:
+    public fv::cellSetOption
+{
+    // Private Data
+
+        //- Heat exchanger model
+        autoPtr<heatExchangerModel> heatExchangerModelPtr_;
+
+
+public:
+
+    //- Runtime type information
+    TypeName("heatExchangerSource");
+
+
+    // Constructors
+
+        //- Construct from components
+        heatExchangerSource
+        (
+            const word& name,
+            const word& modelType,
+            const dictionary& dict,
+            const fvMesh& mesh
+        );
+
+        //- No copy construct
+        heatExchangerSource(const heatExchangerSource&) = delete;
+
+        //- No copy assignment
+        void operator=(const heatExchangerSource&) = delete;
+
+
+    //- Destructor
+    virtual ~heatExchangerSource() = default;
+
+
+    // Member Functions
+
+        //- Add explicit/implicit contribution to momentum equation
+        virtual void addSup
+        (
+            fvMatrix<scalar>& eqn,
+            const label fieldi
+        )
+        {
+            NotImplemented;
+        }
+
+        //- Add explicit/implicit contribution
+        //- to compressible momentum equation
+        virtual void addSup
+        (
+            const volScalarField& rho,
+            fvMatrix<scalar>& eqn,
+            const label fieldi
+        );
+
+
+    // I-O
+
+        //- Read top-level dictionary
+        virtual bool read(const dictionary& dict);
+};
+
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+} // End namespace fv
+} // End namespace Foam
+
+// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
+
+#endif
+
+// ************************************************************************* //