/*---------------------------------------------------------------------------*\
  =========                 |
  \\      /  F ield         | OpenFOAM: The Open Source CFD Toolbox
   \\    /   O peration     |
    \\  /    A nd           | Copyright (C) 2019 Tomislav Maric, TU Darmstadt
     \\/     M anipulation  |
-------------------------------------------------------------------------------
License
    This file is part of OpenFOAM.

    OpenFOAM is free software: you can redistribute it and/or modify it
    under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    OpenFOAM is distributed in the hope that it will be useful, but WITHOUT
    ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
    FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
    for more details.

    You should have received a copy of the GNU General Public License
    along with OpenFOAM.  If not, see <http://www.gnu.org/licenses/>.

Application
    foamTestFvcReconstruct

Description
    Test application for the fvc::reconstruct operator. 

    Reconstruct cell-centered velocity fields from face-centered volumetric
    values for: 

    1. 2D Cases
        1.1 2D shear velocity 
        1.2 2D Hadamard–Rybczynski velocity
        
    2. 3D cases
        2.1 3D shear velocity
        2.2 3D deformation velocity

Relevant publications

    Advection velocity fields (shear and deformation):

    @article{Maric2018,
        title = {{An enhanced un-split face-vertex flux-based VoF method}},
        author = {Mari{\'{c}}, Tomislav and Marschall, Holger and Bothe, Dieter},
        doi = {10.1016/j.jcp.2018.03.048},
        journal = {J. Comput. Phys.},
        month = {apr},
        pages = {967--993},
        url = {https://doi.org/10.1016/j.jcp.2018.03.048},
        volume = {371},
        year = {2018}
    }

Author
    Tomislav Maric, maric@mma.tu-darmstadt.de
    Mathematical Modeling and Analysis 
    TU Darmstadt, Germany

\*---------------------------------------------------------------------------*/

#ifndef  fvcReconstructTestFunctions_HPP
#define  fvcReconstructTestFunctions_HPP

#include <cmath>
#include <sstream>
#include <string>
#include "emptyFvPatch.H"
#include "fvcSurfaceIntegrate.H"
#include <cassert>
#include <iomanip>
#include <experimental/filesystem>

namespace Foam {

template<typename Vector>
Vector shear2Dvelocity
(
    Vector const& point, 
    scalar t=0., 
    scalar T=1.
)
{
    return Vector{
        sin(2.*M_PI * point[1])*sqr(sin(M_PI * point[0])) * cos(M_PI * t / T),
        -sin(2.*M_PI * point[0])*sqr(sin(M_PI * point[1])) * cos(M_PI * t / T),
        0.
    };
}

template<typename Tensor, typename Vector>
Tensor shear2DvelocityGradient
(
    Vector const& point, 
    scalar t=0., 
    scalar T=1.
)
{
    double x = point[0]; 
    double y = point[1]; 

    return Tensor(
        2*M_PI*sin(M_PI*x)*sin(2*M_PI*y)*cos(M_PI*x)*cos(M_PI*t/T), 
        2*M_PI*pow(sin(M_PI*x), 2)*cos(2*M_PI*y)*cos(M_PI*t/T), 
        0, 
        -2*M_PI*pow(sin(M_PI*y), 2)*cos(2*M_PI*x)*cos(M_PI*t/T), 
        -2*M_PI*sin(2*M_PI*x)*sin(M_PI*y)*cos(M_PI*y)*cos(M_PI*t/T), 
        0, 
        0, 
        0, 
        0
    );
}


class shear2D
{
    scalar t_; 
    scalar T_; 

    public: 
        static const string name() { return "SHEAR_2D"; }

        shear2D()
            :
                t_(0.), 
                T_(1.)
        {}

        shear2D(std::ostream& os)
            :
                shear2D()
        {
            os << this->metadata(); 
        }

        shear2D(scalar t, scalar T)
            :
                t_(t), 
                T_(T)
        {}

        shear2D(scalar t, scalar T, std::ostream& os)
            :
                shear2D(t, T)
        {
            os << this->metadata();
        }

        const std::string metadata() const { 
            std::stringstream ss;
            ss << "# METADATA \n# TEST" << shear2D::name() << "\n" 
                << "# t = " << t_ << "\n# T = " << T_ << "\n";  
            return ss.str();
        }

        template<typename Vector> 
        Vector velocity(Vector const& point) const
        {
            return shear2Dvelocity(point, t_, T_);
        }

        template<typename Vector> 
        Foam::tensor velocityGradient(Vector const& point) const
        {
            return shear2DvelocityGradient<Foam::tensor>(point, t_, T_);
        }
};

class hadamardRybczynsky2D
{
    vector center_;  
    scalar R_, Uinf_, muo_, mui_, muRatio_; 

    // Velocity components taken from 
    // 
    // [1] Brenn, Günter. Analytical solutions for transport processes. New York:
    // Springer, 2016.
    // 
    // Chapter 6: Flows with interfaces, page 168. 

    public: 
        static const string name() { return "HADAMARD_RYBCZYNSKY_2D"; }

        hadamardRybczynsky2D(
            vector center, 
            scalar radius, 
            scalar velocity, 
            scalar muo, 
            scalar mui
        )
            :
                center_(center), 
                R_(radius), 
                Uinf_(velocity),
                muo_(muo), 
                mui_(mui), 
                muRatio_(muo_ / mui_)
        {}

        hadamardRybczynsky2D(
            vector center, 
            scalar radius, 
            scalar velocity, 
            scalar muo, 
            scalar mui, 
            std::ostream& os
        )
            :
                hadamardRybczynsky2D(center, radius, velocity, muo, mui)
        {
            os << this->metadata();
        }

        const std::string metadata() const { 
            std::stringstream ss;

            ss << "#METADATA\n# TEST: " << hadamardRybczynsky2D::name() << "\n"
                << "# center = " 
                    << center_[0] << "," 
                    << center_[1] << "," 
                    << center_[2] << "\n"
                << "# radius = " << R_ << "\n"
                << "# velocity = " << Uinf_ << "\n"
                << "# muo = " << muo_ << "\n"  
                << "# mui = " << mui_ << "\n";  

            return ss.str();
        }

        template<typename Vector> 
        Vector velocity(Vector const& point) const
        {
            Vector U(0, 0, 0);

            const Vector cp = point - center_; 
            const scalar radius = sqrt(dot(cp,cp)); 
            const scalar radiusRatio = radius / R_;  

            if (cp[0] == 0)
                return Vector(0,0,0); 

            const scalar theta = atan(cp[1] / cp[0]); 

            scalar Ur = 0, Utheta = 0;

            if (radius < R_) // Point is indside the droplet, or on the interface. 
            {
                // G. Brenn
                Ur = 0.5*Uinf_*muRatio_*(
                    pow(radiusRatio, 2) - 1
                )*cos(theta)/(muRatio_ + 1);

                Utheta = -0.5*Uinf_*muRatio_*(
                    2*pow(radiusRatio, 2) - 1
                )*sin(theta)/(muRatio_ + 1); 
            }
            else // Point is outside of the droplet.
            {
                // G. Brenn
                Ur = 0.5*Uinf_*(
                    pow(radiusRatio, 3)/(muRatio_ + 1) - 
                    radiusRatio*(2*muRatio_ + 3)/(muRatio_ + 1) + 2
                )*cos(theta); 

                // Error in the sign (*-1) is added to equation 6.132 from [1]
                Utheta = -0.5*Uinf_*(
                    0.5*pow(radiusRatio, 3)/(muRatio_ + 1) + 
                    radiusRatio*(muRatio_ + 1.5)/(muRatio_ + 1) - 2
                )*sin(theta);

            }

            U[0] = Ur*cos(theta) - Utheta*sin(theta); 
            U[1] = Ur*sin(theta) + Utheta*cos(theta);

            return U;
        }

        template<typename Vector> 
        Foam::tensor velocityGradient(Vector const& point) const
        {
            const scalar x = point[0];
            const scalar y = point[1];
            const scalar cx = center_[0];
            const scalar cy = center_[1];

            const Vector cp = point - center_; 
            const scalar radius = sqrt(dot(cp,cp)); 
            const scalar radiusRatio = radius / R_;  

            if (radius < R_)
                return Foam::tensor(

                    Uinf_*muRatio_*(2.0*cx - 2.0*x)*pow(pow(cx - x, 2) + pow(cy - y, 2), -2.0)*(0.5*pow(radiusRatio, 2)*pow(x, 2) + 1.0*pow(radiusRatio, 2)*pow(y, 2) - 0.5*pow(x, 2) - 0.5*pow(y, 2))/(muRatio_ + 1) + Uinf_*muRatio_*(1.0*pow(radiusRatio, 2)*x - 1.0*x)*1.0/(pow(cx - x, 2) + pow(cy - y, 2))/(muRatio_ + 1),

                    -0.5*Uinf_*muRatio_*pow(radiusRatio, 2)*x*y*(2.0*cx - 2.0*x)*pow(pow(cx - x, 2) + pow(cy - y, 2), -2.0)/(muRatio_ + 1) - 0.5*Uinf_*muRatio_*pow(radiusRatio, 2)*y*1.0/(pow(cx - x, 2) + pow(cy - y, 2))/(muRatio_ + 1),


                    0, 

                    Uinf_*muRatio_*(2.0*cy - 2.0*y)*pow(pow(cx - x, 2) + pow(cy - y, 2), -2.0)*(0.5*pow(radiusRatio, 2)*pow(x, 2) + 1.0*pow(radiusRatio, 2)*pow(y, 2) - 0.5*pow(x, 2) - 0.5*pow(y, 2))/(muRatio_ + 1) + Uinf_*muRatio_*(2.0*pow(radiusRatio, 2)*y - 1.0*y)*1.0/(pow(cx - x, 2) + pow(cy - y, 2))/(muRatio_ + 1),


                    -0.5*Uinf_*muRatio_*pow(radiusRatio, 2)*x*y*(2.0*cy - 2.0*y)*pow(pow(cx - x, 2) + pow(cy - y, 2), -2.0)/(muRatio_ + 1) - 0.5*Uinf_*muRatio_*pow(radiusRatio, 2)*x*1.0/(pow(cx - x, 2) + pow(cy - y, 2))/(muRatio_ + 1),  

                    0, 

                    0, 
                    0, 
                    0
                );

            return Foam::tensor(
                    Uinf_*(2.0*cx - 2.0*x)*(radiusRatio*(-1.0*muRatio_*pow(x, 2) + 0.5*muRatio_*pow(y, 2) + 0.5*pow(radiusRatio, 2)*pow(x, 2) + 0.25*pow(radiusRatio, 2)*pow(y, 2) - 1.5*pow(x, 2) + 0.75*pow(y, 2)) + 1.0*(muRatio_ + 1)*(pow(x, 2) - pow(y, 2)))*pow(pow(cx - x, 2) + pow(cy - y, 2), -2.0)/(muRatio_ + 1) + Uinf_*(radiusRatio*(-2.0*muRatio_*x + 1.0*pow(radiusRatio, 2)*x - 3.0*x) + 2*x*(1.0*muRatio_ + 1.0))*1.0/(pow(cx - x, 2) + pow(cy - y, 2))/(muRatio_ + 1),
                    Uinf_*x*y*(2.0*cx - 2.0*x)*pow(pow(cx - x, 2) + pow(cy - y, 2), -2.0)*(2.0*muRatio_ + radiusRatio*(-1.5*muRatio_ + 0.25*pow(radiusRatio, 2) - 2.25) + 2.0)/(muRatio_ + 1) + Uinf_*y*1.0/(pow(cx - x, 2) + pow(cy - y, 2))*(2.0*muRatio_ + radiusRatio*(-1.5*muRatio_ + 0.25*pow(radiusRatio, 2) - 2.25) + 2.0)/(muRatio_ + 1),
                    0, 
                    Uinf_*(2.0*cy - 2.0*y)*(radiusRatio*(-1.0*muRatio_*pow(x, 2) + 0.5*muRatio_*pow(y, 2) + 0.5*pow(radiusRatio, 2)*pow(x, 2) + 0.25*pow(radiusRatio, 2)*pow(y, 2) - 1.5*pow(x, 2) + 0.75*pow(y, 2)) + 1.0*(muRatio_ + 1)*(pow(x, 2) - pow(y, 2)))*pow(pow(cx - x, 2) + pow(cy - y, 2), -2.0)/(muRatio_ + 1) + Uinf_*(radiusRatio*(1.0*muRatio_*y + 0.5*pow(radiusRatio, 2)*y + 1.5*y) - 2*y*(1.0*muRatio_ + 1.0))*1.0/(pow(cx - x, 2) + pow(cy - y, 2))/(muRatio_ + 1),
                    Uinf_*x*y*(2.0*cy - 2.0*y)*pow(pow(cx - x, 2) + pow(cy - y, 2), -2.0)*(2.0*muRatio_ + radiusRatio*(-1.5*muRatio_ + 0.25*pow(radiusRatio, 2) - 2.25) + 2.0)/(muRatio_ + 1) + Uinf_*x*1.0/(pow(cx - x, 2) + pow(cy - y, 2))*(2.0*muRatio_ + radiusRatio*(-1.5*muRatio_ + 0.25*pow(radiusRatio, 2) - 2.25) + 2.0)/(muRatio_ + 1),
                    0, 
                    0,
                    0, 
                    0
                );
        }
};

class accelerated1D 
{
    scalar t_; 
    scalar T_; 

    public: 
        static const string name() { return "ACCELERATED_1D"; }

        accelerated1D()
            :
                t_(0.), 
                T_(1.)
        {}

        accelerated1D(std::ostream& os)
            :
                accelerated1D()
        {
            os << this->metadata(); 
        }

        accelerated1D(scalar t, scalar T)
            :
                t_(t), 
                T_(T)
        {}

        accelerated1D(scalar t, scalar T, std::ostream& os)
            :
                accelerated1D(t, T)
        {
            os << this->metadata();
        }

        const std::string metadata() const { 
            std::stringstream ss;
            ss << "# TEST: " << accelerated1D::name() << ", METADATA: "
                << "t = " << t_ << ", T = " << T_ << "\n";  
            return ss.str();
        }

        template<typename Vector> 
        Vector velocity(Vector const& point) const
        {
            return vector(0.5*point[0]*point[0], 0, 0); 
        }

        template<typename Vector> 
        Foam::tensor velocityGradient(Vector const& point) const
        {
            return Foam::tensor(point[0],0,0,0,0,0,0,0,0);
        }
};

template<typename Vector>
Vector shear3Dvelocity
(
    Vector const& point, 
    scalar t=0., 
    scalar T=1., 
    scalar Umax=1.0,
    scalar x0 = 0.5, 
    scalar y0 = 0.5,
    scalar R = 0.5
)
{
    Vector U = shear2Dvelocity(point,t, T);

    scalar r = sqrt(sqr(point[0] - x0) + sqr(point[1] - y0));
    U[2] = Umax * sqr(1. - r / R) * cos(M_PI * t / T);

    return U; 
};

template<typename Tensor, typename Vector>
Tensor shear3DvelocityGradient
(
    Vector const& point, 
    scalar t=0., 
    scalar T=1., 
    scalar Umax=1.0,
    scalar x0 = 0.5, 
    scalar y0 = 0.5,
    scalar R = 0.5
)
{
    double x = point[0]; 
    double y = point[1]; 

    return Tensor(
        // First 6
        2*M_PI*sin(M_PI*x)*sin(2*M_PI*y)*cos(M_PI*x)*cos(M_PI*t/T), 2*M_PI*pow(sin(M_PI*x), 2)*cos(2*M_PI*y)*cos(M_PI*t/T), 0, 
        -2*M_PI*pow(sin(M_PI*y), 2)*cos(2*M_PI*x)*cos(M_PI*t/T), -2*M_PI*sin(2*M_PI*x)*sin(M_PI*y)*cos(M_PI*y)*cos(M_PI*t/T), 0,
        // Last three
        -2*Umax*(1 - sqrt(pow(x - x0, 2) + pow(y - y0, 2))/R)*(1.0*x - 1.0*x0)*pow(pow(x - x0, 2) + pow(y - y0, 2), -0.5)*cos(M_PI*t/T)/R, 
        -2*Umax*(1 - sqrt(pow(x - x0, 2) + pow(y - y0, 2))/R)*(1.0*y - 1.0*y0)*pow(pow(x - x0, 2) + pow(y - y0, 2), -0.5)*cos(M_PI*t/T)/R, 
        0
    );
};


class shear3D
{
    scalar t_, T_, Umax_,x0_,y0_,R_;

    public:

        static const string name() { return "SHEAR_3D"; }

        const std::string metadata() const { 
            std::stringstream ss;
            ss << "# TEST: " << shear3D::name() << ", METADATA: "
                << "t = " << t_ << ", T = " << T_  
                << ", Umax = " << Umax_ << ", x0 = " << x0_  
                << ", y0 = " << y0_ << ", R = " << R_ << "\n";
            return ss.str();
        }

        shear3D()
            :
                t_(0.),
                T_(1.),
                Umax_(1.0),
                x0_(0.5),
                y0_(0.5),
                R_(0.5)
        {}

        shear3D(std::ostream& os) 
        : 
            shear3D()
        {
            os << this->metadata();
        }

        shear3D
        (
            scalar t, 
            scalar T, 
            scalar Umax, 
            scalar x0, 
            scalar y0, 
            scalar R
        )
        : 
            t_(t), 
            T_(T),
            Umax_(Umax),
            x0_(x0),
            y0_(y0),
            R_(R)
        {}

        shear3D
        (
            scalar t, 
            scalar T, 
            scalar Umax, 
            scalar x0, 
            scalar y0, 
            scalar R,
            std::ostream& os 
        )
        :
            shear3D(t,T,Umax,x0,y0,R)
        {
            os << this->metadata();
        }

        template<typename Vector> 
        Vector velocity(Vector const& point) const
        {
            return shear3Dvelocity(point,t_,T_,Umax_,x0_,y0_,R_);
        }

        template<typename Vector> 
        Foam::tensor velocityGradient(Vector const& point) const
        {
            return shear3DvelocityGradient<Foam::tensor>(point,t_,T_,Umax_,x0_,y0_,R_);
        }
};

template<typename Vector>
Vector deformation3Dvelocity 
(
    Vector const& point, 
    scalar t=0., 
    scalar T=1.
)
{
    return Vector{
        2.*sin(2*M_PI*point[1])*sqr(sin(M_PI*point[0]))*sin(2*M_PI*point[2])*cos(M_PI*t/T),
        -sin(2*M_PI*point[0])*sqr(sin(M_PI*point[1]))*sin(2*M_PI*point[2])*cos(M_PI*t/T),
        -sin(2*M_PI*point[0])*sin(M_PI*point[1])*sqr(sin(M_PI*point[2]))*cos(M_PI*t/T)
    };
}

template<typename Tensor, typename Vector>
Tensor deformation3DvelocityGradient
(
    Vector const& point, 
    scalar t=0., 
    scalar T=1.
)
{
    double x = point[0]; 
    double y = point[1]; 
    double z = point[2];

    return Tensor(
    );


    return Tensor(
        4*M_PI*sin(M_PI*x)*sin(2*M_PI*y)*sin(2*M_PI*z)*cos(M_PI*x)*cos(M_PI*t/T), 
        4*M_PI*pow(sin(M_PI*x), 2)*sin(2*M_PI*z)*cos(2*M_PI*y)*cos(M_PI*t/T), 
        4*M_PI*pow(sin(M_PI*x), 2)*sin(2*M_PI*y)*cos(2*M_PI*z)*cos(M_PI*t/T), 
        -2*M_PI*pow(sin(M_PI*y), 2)*sin(2*M_PI*z)*cos(2*M_PI*x)*cos(M_PI*t/T), 
        -2*M_PI*sin(2*M_PI*x)*sin(M_PI*y)*sin(2*M_PI*z)*cos(M_PI*y)*cos(M_PI*t/T), 
        -2*M_PI*sin(2*M_PI*x)*pow(sin(M_PI*y), 2)*cos(2*M_PI*z)*cos(M_PI*t/T), 
        -2*M_PI*sin(2*M_PI*y)*pow(sin(M_PI*z), 2)*cos(2*M_PI*x)*cos(M_PI*t/T), 
        -2*M_PI*sin(2*M_PI*x)*pow(sin(M_PI*z), 2)*cos(2*M_PI*y)*cos(M_PI*t/T), 
        -2*M_PI*sin(2*M_PI*x)*sin(2*M_PI*y)*sin(M_PI*z)*cos(M_PI*z)*cos(M_PI*t/T)
        );
    
};

class deformation3D 
{
    scalar t_, T_; 

    public: 

        deformation3D()
            :
                t_(0), 
                T_(1)
        {}

        deformation3D(std::ostream& os)
            :
                deformation3D()
        {
            os << metadata();
        }

        deformation3D(scalar t, scalar T)
            :
                t_(t), 
                T_(T)
        {
        }

        deformation3D(scalar t, scalar T, std::ostream& os)
            :
                deformation3D(t, T)
        {
            os << this->metadata();
        }

        static const string name() { return "DEFORMATION_3D"; }

        const std::string metadata() const { 
            std::stringstream ss;
            ss << "# TEST: " << deformation3D::name() << ", METADATA: "
                << "t = " << t_ << ", T = " << T_ << "\n";
            return ss.str();
        }

        template<typename Vector> 
        Vector velocity(Vector const& point) const
        {
            return deformation3Dvelocity(point,t_,T_);
        }

        template<typename Vector> 
        Foam::tensor velocityGradient(Vector const& point) const
        {
            return deformation3DvelocityGradient<Foam::tensor>(point,t_,T_);
        }
};

template<typename Test> 
void test(
    Test test, 
    const fvMesh& mesh, 
    const surfaceVectorField& SfHat // Unit face area normal vectors.
)
{
    auto testFileName = Test::name() + ".csv";  
    namespace fs = std::experimental::filesystem;
    bool testFileExists = fs::exists(testFileName); 

    std::fstream testFile(testFileName,std::ios_base::app); 

    if (! testFileExists) 
    {
        testFile << test.metadata(); 
        testFile << "VELOCITY_MODEL,H,L_INF,EPSILON_R_EXACT_MAX,N_CELLS,SSUM_PHI\n"; 
    }

    #include "createFields.H"
    // Name the fields after the test. 
    U.rename("U_" + Test::name());
    phi.rename("phi_" + Test::name()); 
    epsilonRmag.rename("epsilonRmag_" + Test::name());
    epsilonRexact.rename("epsilonRexact_" + Test::name());

    // Mesh geometry references
    // - Cell centers
    const auto& C = mesh.C();
    // - Face centers
    const auto& Sf = mesh.Sf(); 
    const auto& Sfb = Sf.boundaryField();
    // - Face area normal vectors 
    const auto& Cf = mesh.Cf(); 
    const auto& Cfb = Cf.boundaryField();
    // Unit face area normal vectors: boundary field.
    const auto& SfHatb = SfHat.boundaryField(); 

    // Set cell centered velocity field and velocity gradient. 
    forAll(U, cellI)
    {
        U[cellI] = test.velocity(C[cellI]);
        gradU[cellI] = test.velocityGradient(C[cellI]); 
    }

    // Set the velocity field on the boundaries. 
    auto& Ub = U.boundaryFieldRef(); 
    forAll(Ub, patchI)
    {
        auto& Up = Ub[patchI]; 
        const auto& Cfp = Cfb[patchI]; 
        forAll(Up, faceI)
            Up[faceI] = test.velocity(Cfp[faceI]);
    }


    // Set the flux field.
    forAll(phi, faceI)
        phi[faceI] = dot(Sf[faceI],test.velocity(Cf[faceI]));

    // Set the flux field on the boundaries.
    auto& phib = phi.boundaryFieldRef();
    forAll(phib, patchI)
    {
        auto& phip = phib[patchI];
        const auto& Cfp = Cfb[patchI]; 
        const auto& Sfp = Sfb[patchI]; 
        forAll(phip, faceI)
            phip[faceI] = dot(Sfp[faceI], test.velocity(Cfp[faceI]));
    }
    
    // Reconstruct the velocity field from the flux field.
    volVectorField Ur (fvc::reconstruct(phi)); 
    Ur.rename("Ur_" + Test::name()); 

    // Compute the max discretization length. 
    scalar hMax = 1./ min(mesh.deltaCoeffs()).value(); 

    // Compute the discrete divergence of the volumetric flux.
    scalar phiSumMax = max(fvc::surfaceIntegrate(phi)).value(); 

    // Compute the error predictor and exact error. 
    
    volTensorField AsInv (inv(fvc::surfaceSum(SfHat*mesh.Sf())));

    const auto& own = mesh.owner();
    const auto& nei = mesh.neighbour(); 
    forAll(Cf, faceI)
    {
        // Exact error 
        epsilonRexactArea[own[faceI]] += 
            SfHat[faceI] * (dot(Sf[faceI], U[own[faceI]]) - phi[faceI]);
        epsilonRexactArea[nei[faceI]] += 
            SfHat[faceI] * (dot(Sf[faceI], U[nei[faceI]]) - phi[faceI]);

        // Estimated error 
        epsilonRmag[own[faceI]] += mag(
            dot(AsInv[own[faceI]],
                SfHat[faceI] * mag(
                    dot(Sf[faceI],
                    dot(
                        gradU[own[faceI]], 
                        Cf[faceI] - C[own[faceI]]
                    )
                    )
                )
            )
        );
        epsilonRmag[nei[faceI]] += mag(
            dot(AsInv[nei[faceI]],
                SfHat[faceI] * mag(
                    dot(Sf[faceI],
                    dot(
                        gradU[nei[faceI]], 
                        Cf[faceI] - C[nei[faceI]]
                    )
                    )
                )
            )
        );
    }


    forAll(Cfb, patchI)
    {
        const auto& Cfp = Cfb[patchI]; 
        const auto& Sfp = Sfb[patchI]; 
        const auto& SfHatp = SfHatb[patchI];
        const auto& phip = phib[patchI];

        const auto& fPatch = Cfp.patch(); 
        if (! isA<emptyFvPatch>(fPatch))
        {
            const labelUList& pFaceCells =
                mesh.boundary()[patchI].faceCells();

            forAll(Cfp, faceI)
            {
                // Exact error 
                epsilonRexactArea[pFaceCells[faceI]] += 
                    SfHatp[faceI] * (dot(Sfp[faceI], U[pFaceCells[faceI]]) - phip[faceI]);

                // Error estimator
                epsilonRmag[pFaceCells[faceI]] += mag(
                    dot(AsInv[nei[faceI]],
                        SfHatp[faceI] * mag(dot(
                            Sfp[faceI],dot(gradU[pFaceCells[faceI]], 
                            Cfp[faceI] - C[pFaceCells[faceI]])
                        ))
                    )
                );
            }
        }
    }
    // Finish the exact reconstruction error. 
    epsilonRexact = AsInv & epsilonRexactArea;

    // Compute the actual L_INF velocity error. 
    volVectorField Udiff (U-Ur); 
    Udiff.rename("Udiff_" + Test::name()); 
    scalar Linf = max(mag(Udiff.internalField())).value(); 

    // Check that epsilonRexact corresponds exactly to Udiff. 
    // Consider only differences at cell centers, boundary values make no sense
    // for Udiff and epsilonRexact.
    auto exactErrorDiffs  = mag(Udiff.internalField() - epsilonRexact.internalField());
    scalar exactErrorDiff = max(exactErrorDiffs).value();
    if ((exactErrorDiff < 1e-14))
    {
        Info << Test::name() << "\n"
            << "max(||Udiff - epsilonRexact||) = " << exactErrorDiff << " : PASS" << endl;
    }

    //double maxEpsilonR =  max(epsilonRmag.internalField()).value(); // TODO: Error estimation.
    double maxEpsilonRexact = max(mag(epsilonRexact.internalField())).value();

    testFile << std::setprecision(20) 
        << Test::name() << "," 
        << hMax << "," 
        << Linf << "," 
        << maxEpsilonRexact << "," 
        //<< maxEpsilonR << "," // Skip error estimation.
        << mesh.nCells() << "," 
        << phiSumMax << "\n";
    
    // Write all fields to disk.
    epsilonRmag.write();
    epsilonRexact.write();
    U.write(); 
    phi.write(); 
    Ur.write();
    Udiff.write(); 

}

} // End namespace Foam

#endif