Implementation of tutorial 9 which presents DEIM-ROM for a Heat Conduction Problem. It is the ROM extension of the previous tutorial 8.
Introduction
In this tutorial we implement a test where we use the Discrete Empirical Interpolation Method for a case where we have a non-linear dependency with respect to the input parameters. A ROM is then used to solve the problem at new parameter instances faster. The following image illustrates the computational domain which is the same as the previous example:

The physical problem is given by a heat transfer problem which is described by the Poisson equation:
$$\nabla \cdot (\nu \nabla T) = S$$
The parametric diffusivity is described by a parametric Gaussian function:
$$\nu(\mathbf{x},\mathbf{\mu}) = e^{-2(x-{\mu}_x-1)^2 - 2(y-{\mu}_y-0.5)^2} +1.$$
The problem is then discretized as:
$$ A(\mathbf{\mu})T = b.$$
In this case, even if the problem is linear, due to non-linearity with respect to the input parameter of the conductivity constant it is not possible to have an affine decomposition of the discretized differential operator.
We seek therefore an approximate affine expansion of the differential operator of this type:
$$A(\mathbf{\mu}) = \sum_{i = 1}^{N_D} \theta_i(\mathbf{\mu}) A_i$$
using the Discrete Empirical Interpolation Method.
A detailed look into the code
This section explains the main steps necessary to construct the tutorial N°9.
The necessary header files
The following header files are included for their respective functionalities:
#include "hyperReduction.templates.H"
#include <chrono>
#include "fvMeshSubset.H"
#include "DEIM.H"
#include <Eigen/SVD>
#include <Eigen/SparseLU>
Header file of the EigenFunctions class.
Header file of the Foam2Eigen class.
Header file of the ITHACAPOD class.
Header file of the ITHACAstream class, it contains the implementation of several methods for input ou...
Header file of the laplacianProblem class.
Functions useful for the main function
In the DEIM_function class, DEIM is used to approximate the discretized scalar PDE operator, and it constructs indeed a fvScalarMatrix:
{
DEIM(PtrList< T > &SnapShotsMatrix, label MaxModes, word FunctionName, word FieldName)
Construct DEIM for non-linear function.
evaluate_expression evaluates the diffusitivity coefficient $\nu$, which depends on the parameters and on the spatial coordinates X and Y:
public:
static fvScalarMatrix evaluate_expression(volScalarField& T, Eigen::MatrixXd mu)
{
volScalarField yPos = T.mesh().C().component(vector::Y).ref();
volScalarField xPos = T.mesh().C().component(vector::X).ref();
volScalarField nu(T);
for (auto i = 0; i < nu.size(); i++)
{
nu[i] = std::exp(- 2 * std::pow(xPos[i] - mu(0) - 1,
2) - 2 * std::pow(yPos[i] - mu(1) - 0.5, 2)) + 1;
}
nu.correctBoundaryConditions();
and builds the finite-volume parameter-dependent laplacian operator:
fvScalarMatrix TiEqn22
(
fvm::laplacian(nu, T, "Gauss linear")
);
return TiEqn22;
}
The following part defines a matrix which collects the online DEIM coefficients associated with the operator. In particular, it initializes a matrix theta, and evaluates the operator at the current parameter $\mathbf{\mu}$:
Eigen::MatrixXd onlineCoeffsA(Eigen::MatrixXd mu)
{
Eigen::MatrixXd theta(magicPointsAcol().size(), 1);
fvScalarMatrix Aof = evaluate_expression(fieldA(), mu);
Then, it converts the OpenFOAM matrix into Eigen objects:
Eigen::SparseMatrix<double> Mr;
Eigen::VectorXd br;
static void fvMatrix2Eigen(fvMatrix< type_foam_matrix > foam_matrix, type_A &A, type_B &b)
Convert a FvMatrix OpenFOAM matrix (Linear System) into a Eigen Matrix A and a source vector b.
It samples only the entries corresponding to the DEIM magic points, and stores the sampled values in the matrix theta. This is the main source of online efficiency.
for (int i = 0; i < magicPointsAcol().size(); i++)
{
int ind_row = localMagicPointsArow[i] + xyz_Arow()[i] *
fieldA().size();
int ind_col = localMagicPointsAcol[i] + xyz_Acol()[i] *
fieldA().size();
theta(i) = Mr.coeffRef(ind_row, ind_col);
}
return theta;
}
The following part does the same operation as onlineCoeffsA, but applied on the right-hand-side/vector contribution. Hence, it evaluates the operator and extracts the vector part br:
Eigen::MatrixXd onlineCoeffsB(Eigen::MatrixXd mu)
{
Eigen::MatrixXd theta(magicPointsB().size(), 1);
fvScalarMatrix Aof = evaluate_expression(fieldB(), mu);
Eigen::SparseMatrix<double> Mr;
Eigen::VectorXd br;
Then, it samples the vector only at the DEIM magic points, and return the corresponding coefficients theta:
for (int i = 0; i < magicPointsB().size(); i++)
{
int ind_row = localMagicPointsB[i] + xyz_B()[i] * fieldB().size();
theta(i) = br(ind_row);
}
return theta;
}
Then, some DEIM "containers" for fields are initialized:
autoPtr<volScalarField> fieldA;
autoPtr<volScalarField> fieldB;
PtrList<volScalarField> fieldsA;
PtrList<volScalarField> fieldsB;
};
Now, it's time to initialize the ROM problem class, which is inherited from laplacianProblem:
Class to implement a full order laplacian parametrized problem.
This constructor initializes the parent Laplacian problem, gets the references to the main fields (diffusivity, source, solution T), and reads the reduction parameters from the file system/ITHACAdict:
:
{
(
IOobject
(
"ITHACAdict",
"./system",
mesh,
IOobject::MUST_READ,
IOobject::NO_WRITE
)
);
NmodesDEIMA = readInt(
ITHACAdict->lookup(
"N_modes_DEIM_A"));
NmodesDEIMB = readInt(
ITHACAdict->lookup(
"N_modes_DEIM_B"));
}
autoPtr< volScalarField > _T
Temperature field.
autoPtr< volScalarField > _nu
Diffusivity.
autoPtr< fvMesh > _mesh
Mesh.
autoPtr< volScalarField > _S
Source Term.
label NTmodes
Number of modes reduced problem.
IOdictionary * ITHACAdict
dictionary to store input output infos
Then, the variables that store the ROM ingredients are stored:
volScalarField& nu;
volScalarField& S;
volScalarField& T;
PtrList<fvScalarMatrix> Mlist;
Eigen::MatrixXd ModesTEig;
std::vector<Eigen::MatrixXd> ReducedMatricesA;
std::vector<Eigen::MatrixXd> ReducedVectorsB;
int NmodesDEIMA;
int NmodesDEIMB;
double time_full;
double time_rom;
In particular, Mlist contains the FOM operators collected offline, ModesTEig contains the POD basis of the solution field in Eigen format, ReducedMatricesA and ReducedVectorsB contain the reduced DEIM basis matrices and vectors. Finally, time_full and time_rom collects the timing variables to compare performances.
Then, the offline snapshots are generated and exported (if not existing), or read (if already collected). evaluate_expression is used to build the full order operators.
void OfflineSolve(Eigen::MatrixXd par, word Folder)
{
{
}
else
{
for (int i = 0; i < par.rows(); i++)
{
fvScalarMatrix Teqn = DEIMmatrice->evaluate_expression(T, par.row(i));
Teqn.solve();
Mlist.append((Teqn).clone());
}
}
}
PtrList< volScalarField > Tfield
List of snapshots for the solution.
bool offline
Boolean variable, it is 1 if the Offline phase has already been computed, else 0.
void exportSolution(GeometricField< Type, PatchField, GeoMesh > &s, fileName subfolder, fileName folder, word fieldName)
Export a field to file in a certain folder and subfolder.
void read_fields(PtrList< GeometricField< Type, PatchField, GeoMesh > > &Lfield, word Name, fileName casename, int first_snap, int n_snap)
Function to read a list of fields from the name of the field and casename.
Then, we solve the FOM for a new set of parameters, just for comparison in terms of efficiency and accuracy with the ROM.
void OnlineSolveFull(Eigen::MatrixXd par, word Folder)
{
...
for (int i = 0; i < par.rows(); i++)
{
fvScalarMatrix Teqn = DEIMmatrice->evaluate_expression(T, par.row(i));
t1 = std::chrono::high_resolution_clock::now();
Teqn.solve();
t2 = std::chrono::high_resolution_clock::now();
...
time_full += time_span.count();
}
}
Then, a wrapper is created to call the detailed setup function using the mode numbers loaded from the ITHACAdict:
void PODDEIM()
{
PODDEIM(
NTmodes, NmodesDEIMA, NmodesDEIMB);
}
In the following part, the DEIM is initialized and trained using the list of offline matrices Mlist:
void PODDEIM(int NmodesT, int NmodesDEIMA, int NmodesDEIMB)
{
DEIMmatrice =
new DEIM_function(Mlist, NmodesDEIMA, NmodesDEIMB,
"T_matrix");
The DEIM submeshes are generated:
fvMesh& mesh = const_cast<fvMesh&>(T.mesh());
DEIMmatrice->fieldA = autoPtr<volScalarField>(new volScalarField(
DEIMmatrice->fieldB = autoPtr<volScalarField>(new volScalarField(
S generateSubmeshMatrix(label layers, const fvMesh &mesh, S field, label secondTime=0)
Function to generate the submesh for the nonlinear matrix function case (RHS).
S generateSubmeshVector(label layers, const fvMesh &mesh, S field, label secondTime=0)
Function to generate the submesh for the nonlinear matrix function case (LHS).
The POD modes are converted to Eigen and converted to the desired shape (reduced dimension).
ModesTEig.conservativeResize(ModesTEig.rows(), NmodesT);
static Eigen::MatrixXd PtrList2Eigen(PtrList< GeometricField< Type, PatchField, GeoMesh > > &fields, label Nfields=-1)
Convert a PtrList of snapshots to Eigen matrix (only internal field).
volScalarModes Tmodes
List of POD modes.
The reduced operators are assembled:
ReducedMatricesA.resize(NmodesDEIMA);
ReducedVectorsB.resize(NmodesDEIMB);
for (int i = 0; i < NmodesDEIMA; i++)
{
ReducedMatricesA[i] = ModesTEig.transpose() * DEIMmatrice->
MatrixOnlineA[i] * ModesTEig;
}
for (int i = 0; i < NmodesDEIMB; i++)
{
ReducedVectorsB[i] = ModesTEig.transpose() * DEIMmatrice->
MatrixOnlineB;
}
}
List< Eigen::SparseMatrix< double > > MatrixOnlineA
Online Matrices.
Eigen::MatrixXd MatrixOnlineB
Online Matrices.
Then, the reduced online solve, which is the heart of this tutorial, is finally performed:
void OnlineSolve(Eigen::MatrixXd par_new, word Folder)
{
...
It computes the DEIM coefficients via onlineCoeffsA and onlineCoeffsB:
for (int i = 0; i < par_new.rows(); i++)
{
Eigen::MatrixXd thetaonA = DEIMmatrice->onlineCoeffsA(par_new.row(i));
Eigen::MatrixXd thetaonB = DEIMmatrice->onlineCoeffsB(par_new.row(i));
Then, it reconstructs the reduced system, namely: $A=\sum_i (\theta A)_i A_i^r$ and $B=\sum_i (\theta B)_i B_i^r$.
Eigen::SparseMatrix< T > MVproduct(List< Eigen::SparseMatrix< T > > &A, Eigen::DenseBase< Eigen::Matrix< T, Eigen::Dynamic, Eigen::Dynamic > > &C)
Sparse Matrix-Vector product between a list of sparse matrices and a vector of coefficients.
The system is then solved via the fullPivLu utility, and lifted back to the full space:
Eigen::VectorXd x = A.fullPivLu().solve(B);
Eigen::VectorXd full = ModesTEig * x;
The solution is then converted to an OpenFOAM field and saved to file.
...
volScalarField Tred("Tred", T);
}
}
static GeometricField< scalar, PatchField, GeoMesh > Eigen2field(GeometricField< scalar, PatchField, GeoMesh > &field, Eigen::VectorXd &eigen_vector, bool correctBC=true)
Convert a vector in Eigen format into an OpenFOAM scalar GeometricField.
PtrList< volScalarField > Tonline
List of snapshots for the solution.
The main function
Inside the main function, the problem is first created:
The parameters are then read and initialized:
example._runTime());
Class for the definition of some general parameters, the parameters must be defined from the file ITH...
static ITHACAparameters * getInstance()
Gets an instance of ITHACAparameters, to be used if the instance is already existing.
The offline training parameters are then generated (100 random 2D parameters, each in range [-0.5, 0.5]):
Eigen::MatrixXd rand(label rows, label cols, double min, double max)
Generates random matrix with random values in an interval.
The FOM is solved, the POD modes are extracted, and the POD-DEIM reduced order model is built:
example.OfflineSolve(example.mu, "Offline");
example.podex, 0, 0, 20);
example.PODDEIM();
void getModes(PtrList< GeometricField< Type, PatchField, GeoMesh > > &snapshots, PtrList< GeometricField< Type, PatchField, GeoMesh > > &modes, word fieldName, bool podex, bool supex, bool sup, label nmodes, bool correctBC)
Computes the bases or reads them for a field.
The ROM is ready and tested on new test parameters:
example.OnlineSolve(par_new1, "Online_red");
Then, the FOM is solved on the same test parameters for comparison:
example_new.OnlineSolveFull(par_new1, "Online_full");
The timings are finally printed:
Info << endl << "The FOM Solve took: " << example_new.time_full << " seconds." << endl;
Info << endl << "The ROM Solve took: " << example.time_rom << " seconds." << endl;
Info << endl << "The Speed-up is: " << example_new.time_full / example.time_rom << endl << endl;
Eigen::MatrixXd error = ITHACAutilities::errorL2Abs(example_new.Tfield,
example.Tonline);
Info << "The mean L2 error is: " << error.mean() << endl;
The time depends on the machine used for computations, but we expect a speed-up of $\approx 200$.
The plain program
The plain code is available here.