Merge branch '8-fix-2d-adi' into 'main'

Resolve "Fix 2D ADI"

Closes #8

See merge request mluebke/diffusion!17
This commit is contained in:
Max Lübke 2022-05-16 15:43:10 +02:00
commit bdc1bd9c27
4 changed files with 387 additions and 52 deletions

219
doc/Implementation.org Normal file
View File

@ -0,0 +1,219 @@
#+title: Implementation of 2D ADI BTCS
#+LaTeX_CLASS_OPTIONS: [a4paper,10pt]
#+LATEX_HEADER: \usepackage{fullpage}
#+LATEX_HEADER: \usepackage{amsmath, systeme}
#+OPTIONS: toc:nil
* Introduction
As described in [[./ADI_scheme.org]] the 1D-BTCS diffusion and the 2D-BTCS
diffusion using the ADI scheme are currently implemented. In the following
document the implementation is described.
To access columns and rows of input vectors and matrices with ease and to use
build-in solver algorithms we're using the [[https://eigen.tuxfamily.org/index.php?title=Main_Page][Eigen library]] inside the library.
However, this not imply to use Eigen as the on-top data structure of the
application. The API expects pointer to a continuous memory area, where
concentrations, alpha and boundary conditions are stored. Those memory areas are
mapped to the according Eigen data structure then.
Solving a linear equation system requires an $A$ matrix and $b$ and $x$ vectors.
All of them are also implemented using Eigen data structures, whereby the $A$
matrix is implemented as a [[https://eigen.tuxfamily.org/dox/group__TutorialSparse.html][sparse matrix]].
In the following all indices will start at 0.
** Internal data structures
During instantiating of an object the dimension size is required. According to
that parameter all other parameters are stored inside the instance of the
diffusion module and are set with the according getter and setters (see code
documentation for more details). Stored values are:
- grid dimension
- time step $\Delta t$
- number of grid cells $N$ in each direction
- domain size $x$ or $y$ resp. in each direction
- spatial discretization in each direction $\Delta x$ or $\Delta y$ resp.
Whenever a value is changed through according setter, the private member
function =updateInterals()= is called, updating all other dependent values.
** Diffusion in 1D
As described above to call =simulate()= properly, pointers to continuous memory
containing the field with its concentrations, alphas and boundary conditions are
required. The boundary condition data type is defined in the
=BoundayCondition.hpp= header file. There are three assignable types:
- =BC_CONSTANT= - constant/Dirichlet boundary condition with =value= set to a fixed value.
- =BC_CLOSED= - closed/Neumann boundary condition with =value= set to a gradient of 0
- =BC_FLUX= - flux/Chauchy boundary with =value= set to user defined gradient
\noindent See the code documentation for more details on that. Defining the
number of grid cells as $N$, the input values should have the following
dimensions:
- vector: field =c= $\to N$
- vector: alpha =alpha= $\to N$
- vector: boundary condition =bc= $\to N+2$, where =bc[0]= and =bc[N+1]=
describing the actual boundary condition on the left/right hand side. All
other values can be used to define constant inlet cells.
All input values are mapped to Eigen vectors and passed to the =simulate1D()=
function. With the only row (since we are just having a 1D vector) we will start
the simulation by calling =simulate_base()=. There, a =SparseMatrix A= with a
dimension of $N+1 \times N+1$ and a =VectorXd b= with a dimension of $N+1$ are
created with, filled with according values and used to solve the LES by the
built-in =SparseLU= solver of Eigen. The results are written to the =VectorXd x=
and copied back to the memory area of =c=.
*** Filling of sparse matrix =A=
To fill the =A= matrix various information about the field are needed. This includes:
- =alpha= for each cell of the field
- =bc= for each cell of the field and boundary conditions
- =size= of the field
- =dx= as the $\Delta x$
- =time_step= as the current time step size $\Delta t$
We are using indices $j$ from $0 \to N+1$ to address the correct field of the
boundary condition and $k$ from $0 \to N-1$ to address the according field of
=alpha=
First of all the left and right boundary condition are extracted from =bc=
evaluated. Then we will set the left boundary condition to the linear equation
system, which is the left boundary condition, to 1 ($A(0,0) = 1$). Now solving
for the leftmost inlet is required. Each $sx = (\alpha_k \cdot \Delta t)/ \Delta
x^2$ is stored in the same named variable for each index pointing to the current
cell of the input/output field.
- $A(1,0) = 2 \cdot sx$
- $A(1,1) = -1 -3 \cdot sx$
- $A(1,2) = sx$
After that we will iterate until $k = N-2$ or $j = N-1$ is reached respectively
and setting =A= with =alpha[k]= and =bc[j]= to the following values:
- $A(j,j-1) = sx$
- $A(j,j) = -1 -2 \cdot sx$
- $A(j,j+1) = sx$
Now the equation for the rightmost inlet cell is applied:
- $A(N,N-1) = sx$
- $A(N,N) = -1 -3 \cdot sx$
- $A(N,N+1) = 2 \cdot sx$
The right boundary condition is also set to $A(N+1,N+1) = 1$.
A special case occurs if one cell of the input/output field is set to a constant
value by setting the according =bc= field to =BC_CONSTANT=. Then, just the according
element of the matrix is set to 1. For example cell $k=4$ is marked as
constant will lead to $j = 5$ and $A(j,j) = 1$.
*** Filling of vector =b=
The right hand side of the equation system requires the following input values:
- =c= as the input field
- =alpha= for each cell of the grid
- =bc= for each cell of the field and boundary conditions
- =d_ortho= as the explicit parameter of left hand side of the equation for each cell of the field
(only interesting if using 2D ADI, otherwise this parameter is set to 0 for
all cells)
- =size= of the field
- =dx= as the $\Delta x$
- =time_step= as the current time step size $\Delta t$
Now we are able to iterating through the indices of $j$ from $0 \to N-1$ and
filling up the =b= vector by applying the following rule:
\begin{equation}\displaystyle
b_{j+1} = \begin{cases}
bc_{j+1}.\text{value} & bc_{j+1}.\text{type} = \texttt{BC\_CONSTANT} \\
-\texttt{c}_j - \texttt{d\_ortho}_j & \text{else}
\end{cases}
\end{equation}
\noindent With
\begin{equation}\displaystyle
\texttt{d\_ortho}_j = \Delta t \cdot \alpha_j \cdot (\frac{\bot d_j}{\Delta x^2})
\end{equation}
Both boundary conditions on the left and right side are assigned by either using
a constant value or a derived value from a given gradient, depending on the
assigned boundary condition type. Until now, only gradients of 0 (closed
boundary condition) are supported. This leads to:
\begin{equation}\displaystyle
b_{0} = \begin{cases}
bc_0.\text{value} & bc_0.\text{type} = \texttt{BC\_CONSTANT} \\
c_0 & \text{else}
\end{cases}
\end{equation}
\begin{equation}\displaystyle
b_{N+1} = \begin{cases}
bc_{N+1}.\text{value} & bc_{N+1}.\text{type} = \texttt{BC\_CONSTANT} \\
c_{N-1} & \text{else}
\end{cases}
\end{equation}
** Diffusion in 2D
Simulating a 2D diffusion works in overall just like the 1D diffusion. Input
parameters are should now be matrices or more precisly arrays, which can be
interpreted as matrices, no matter if stored column or row wise. Defining the
number of grid cells now as $N times M$, the input parameters should have the
following dimensions:
- vector: field =c= $\to N \times M$
- vector: alpha =alpha= $\to N \times M$
- vector: boundary condition =bc= $\to N+2 \times M+2$
Those input pointers will now be interpreted as Eigen matrices for further use.
In =simulate2D()= the matrix =d_ortho= is calculated in y-direction. Then, for
each row of matrix =c= =simulate_base()= is called. The according row of
=alpha=, =bc= and =d_ortho= are passed too and the time step is divided by two.
Now the 2D problem is divided into an 1D problem. After each row the new
resulting field is written back into the original memory area and serves as the
input of the second simulation in the y-direction.
For this, =d_ortho= will be recalculated, now in x-direction. Now, for every
column of =c=, the correspondending column =alpha=, =bc= and =d_ortho= is passed
to =simulate_base()= with also half the time step.
Finally, the result of the simulation of each column get written back to =c= and
the 2D simulation is finished.
*** Calculation of =d_ortho=
The function =cal_d_ortho= returns a matrix with the size of $N \times M$ with
each cell holding the $\bot d$ value of a cell from =c= at the same index. This
is done by first iterating over the first row from $0 \to N-1$. There, we apply
the following rule:
\begin{equation}\displaystyle
\bot d_{0,j} = \begin{cases}
\Delta t \cdot \alpha_{0,j} \cdot \left(\frac{2bc_{0,j+1}.\text{value} - 3 \cdot c_{0,j} + c_{1,j}}{\Delta x^2}\right) & bc_{0,j+1} = \texttt{BC\_CONSTANT} \\
\Delta t \cdot \alpha_{0,j} \cdot \left(\frac{c_{0,j} - 2 \cdot c_{0,j} + c_{1,j}}{\Delta x^2}\right) & \text{else}
\end{cases}
\end{equation}
\noindent Then we succeeding to the inner cells with $i = 1,\dots,M-2$ and $j =
0,\dots,N-1$:
\begin{equation}\displaystyle
\bot d_{i,j} = \Delta t \cdot \alpha_{i,j} \cdot \left(\frac{c_{i-1,j} - 2 \cdot c_{i,j} + c_{i+1,j}}{\Delta x^2}\right)
\end{equation}
\noindent And lastly calculating the last row by applying the following rule for
each $j = 0,\dots,N-1$:
\begin{equation}\displaystyle
\bot d_{M-1,j} = \begin{cases}
\Delta t \cdot \alpha_{M-1,j} \cdot \left(c_{M-2,j} -3 \cdot c_{M-1,j} + 2\frac{bc_{M+1,j+1}.\text{value}}{\Delta x^2}\right) & bc_{M+1,j+1} = \texttt{BC\_CONSTANT} \\
\Delta t \cdot \alpha_{M-1,j} \cdot \left(\frac{c_{M-2,j} - 2 \cdot c_{M-1,j} + c_{M-1,j}}{\Delta x^2}\right) & \text{else}
\end{cases}
\end{equation}

View File

@ -24,7 +24,7 @@ public:
/*!
* Creates a diffusion module.
*
* @param dim Number of dimensions. Should not be greater than 3 and not less
* \param dim Number of dimensions. Should not be greater than 3 and not less
* than 1.
*/
BTCSDiffusion(unsigned int dim);
@ -32,8 +32,8 @@ public:
/*!
* Define the grid in x direction.
*
* @param domain_size Size of the domain in x direction.
* @param n_grid_cells Number of grid cells in x direction the domain is
* \param domain_size Size of the domain in x direction.
* \param n_grid_cells Number of grid cells in x direction the domain is
* divided to.
*/
void setXDimensions(double domain_size, unsigned int n_grid_cells);
@ -43,8 +43,8 @@ public:
*
* Throws an error if the module wasn't initialized at least as a 2D model.
*
* @param domain_size Size of the domain in y direction.
* @param n_grid_cells Number of grid cells in y direction the domain is
* \param domain_size Size of the domain in y direction.
* \param n_grid_cells Number of grid cells in y direction the domain is
* divided to.
*/
void setYDimensions(double domain_size, unsigned int n_grid_cells);
@ -54,8 +54,8 @@ public:
*
* Throws an error if the module wasn't initialized at least as a 3D model.
*
* @param domain_size Size of the domain in z direction.
* @param n_grid_cells Number of grid cells in z direction the domain is
* \param domain_size Size of the domain in z direction.
* \param n_grid_cells Number of grid cells in z direction the domain is
* divided to.
*/
void setZDimensions(double domain_size, unsigned int n_grid_cells);
@ -89,13 +89,13 @@ public:
/*!
* With given ghost zones simulate diffusion. Only 1D allowed at this moment.
*
* @param c Pointer to continious memory describing the current concentration
* \param c Pointer to continious memory describing the current concentration
* state of each grid cell.
* @param alpha Pointer to memory area of diffusion coefficients for each grid
* \param alpha Pointer to memory area of diffusion coefficients for each grid
* element.
* @param bc Pointer to memory setting boundary conidition of each grid cell.
* \param bc Pointer to memory setting boundary conidition of each grid cell.
*
* @return Time in seconds [s] used to simulate one iteration.
* \return Time in seconds [s] used to simulate one iteration.
*/
auto simulate(double *c, double *alpha, Diffusion::boundary_condition *bc)
-> double;
@ -103,7 +103,7 @@ public:
/*!
* Set the timestep of the simulation
*
* @param time_step Time step (in seconds ???)
* \param time_step Time step (in seconds ???)
*/
void setTimestep(double time_step);
@ -121,7 +121,7 @@ private:
void simulate_base(DVectorRowMajor &c, const BCVectorRowMajor &bc,
const DVectorRowMajor &alpha, double dx, double time_step,
int size, const DVectorRowMajor &t0_c);
int size, const DVectorRowMajor &d_ortho);
void simulate1D(Eigen::Map<DVectorRowMajor> &c,
Eigen::Map<const DVectorRowMajor> &alpha,
@ -131,7 +131,7 @@ private:
Eigen::Map<const DMatrixRowMajor> &alpha,
Eigen::Map<const BCMatrixRowMajor> &bc);
auto calc_t0_c(const DMatrixRowMajor &c, const DMatrixRowMajor &alpha,
auto calc_d_ortho(const DMatrixRowMajor &c, const DMatrixRowMajor &alpha,
const BCMatrixRowMajor &bc, double time_step, double dx)
-> DMatrixRowMajor;
@ -144,7 +144,7 @@ private:
const DVectorRowMajor &c,
const DVectorRowMajor &alpha,
const BCVectorRowMajor &bc,
const DVectorRowMajor &t0_c, int size,
const DVectorRowMajor &d_ortho, int size,
double dx, double time_step);
void simulate3D(std::vector<double> &c);

124
scripts/Adi2D_Reference.R Normal file
View File

@ -0,0 +1,124 @@
## Brutal implementation of 2D ADI scheme
## Square NxN grid with dx=dy=1
ADI <- function(n, dt, iter, alpha) {
nx <- ny <- n
dx <- dy <- 1
field <- matrix(0, nx, ny)
## find out the center of the grid to apply conc=1
cen <- ceiling(n/2)
field[cen, cen] <- 1
## prepare containers for computations and outputs
tmpX <- tmpY <- res <- field
out <- vector(mode="list", length=iter)
for (it in seq(1, iter)) {
for (i in seq(1, ny))
tmpX[i,] <- SweepByRow(i, res, dt=dt, alpha=alpha)
resY <- t(tmpX)
for (i in seq(1, nx))
tmpY[i,] <- SweepByRow(i, resY, dt=dt, alpha=alpha)
res <- t(tmpY)
out[[it]] <- res }
return(out)
}
## Workhorse function to fill A, B and solve for a given *row* of the
## grid matrix
SweepByRow <- function(i, field, dt, alpha) {
dx <- 1 ## fixed in our test
A <- matrix(0, nrow(field), ncol(field))
Sx <- Sy <- alpha*dt/2/dx/dx
## diagonal of A at once
diag(A) <- -1-2*Sx
## adjacent diagonals "Sx"
for (ii in seq(1, nrow(field)-1)){
A[ii+1, ii] <- Sx
A[ii, ii+1] <- Sx
}
B <- numeric(ncol(field))
## We now distinguish the top and bottom rows
if (i == 1){
## top boundary, "i-1" doesn't exist or is at a ghost
## node/cell boundary (TODO)
for (ii in seq_along(B))
B[ii] <- (-1 +2*Sy)*field[i,ii] - Sy*field[i+1,ii]
} else if (i == nrow(field)){
## bottom boundary, "i+1" doesn't exist or is at a ghost
## node/cell boundary (TODO)
for (ii in seq_along(B))
B[ii] <- -Sy*field[i-1, ii] + (-1 +2*Sy)*field[i,ii]
} else {
## inner grid row, full expression
for (ii in seq_along(B))
B[ii] <- -Sy*field[i-1, ii] + (-1 +2*Sy)*field[i,ii] - Sy*field[i+1,ii]
}
x <- solve(A, B)
x
}
DoRef <- function(n, alpha, dt, iter) {
require(ReacTran)
require(deSolve)
N <- n # number of grid cells
XX <- n # total size
dy <- dx <- XX/N # grid size
Dy <- Dx <- alpha # diffusion coeff, X- and Y-direction
## The model equations
Diff2D <- function (t, y, parms) {
CONC <- matrix(nrow = N, ncol = N, y)
dCONC <- tran.2D(CONC, D.x = Dx, D.y = Dy, dx = dx, dy = dy)$dC
return (list(dCONC))
}
## initial condition: 0 everywhere, except in central point
y <- matrix(nrow = N, ncol = N, data = 0)
cen <- ceiling(N/2)
y[cen, cen] <- 1 ## initial concentration in the central point...
## solve for time units
times <- seq(0,iter)*dt
out <- ode.2D (y = y, func = Diff2D, t = times, parms = NULL,
dim = c(N,N), lrw=155412)
ref <- matrix(out[length(times),-1], N, N)
return(ref)
}
## test number 1
adi1 <- ADI(n=25, dt=100, iter=50, alpha=1E-3)
ref1 <- DoRef(n=25, alpha=1E-3, dt=100, iter=50)
plot(adi1[[length(adi1)]], ref1, log="xy", xlab="ADI", ylab="ode.2D (reference)",
las=1, xlim=c(1E-15, 1), ylim=c(1E-15, 1))
abline(0,1)
sapply(adi1, sum)
## test number 2
adi2 <- ADI(n=51, dt=10, iter=200, alpha=1E-3)
ref2 <- DoRef(n=51, alpha=1E-3, dt=10, iter=200)
plot(adi2[[length(adi2)]], ref2, log="xy", xlab="ADI", ylab="ode.2D (reference)",
las=1, xlim=c(1E-15, 1), ylim=c(1E-15, 1))
abline(0,1)

View File

@ -91,7 +91,7 @@ void Diffusion::BTCSDiffusion::simulate_base(DVectorRowMajor &c,
const DVectorRowMajor &alpha,
double dx, double time_step,
int size,
const DVectorRowMajor &t0_c) {
const DVectorRowMajor &d_ortho) {
Eigen::SparseMatrix<double> A_matrix;
Eigen::VectorXd b_vector;
@ -104,8 +104,7 @@ void Diffusion::BTCSDiffusion::simulate_base(DVectorRowMajor &c,
x_vector.resize(size + 2);
fillMatrixFromRow(A_matrix, alpha, bc, size, dx, time_step);
fillVectorFromRow(b_vector, c, alpha, bc, Eigen::VectorXd::Constant(size, 0),
size, dx, time_step);
fillVectorFromRow(b_vector, c, alpha, bc, d_ortho, size, dx, time_step);
// start to solve
Eigen::SparseLU<Eigen::SparseMatrix<double>, Eigen::COLAMDOrdering<int>>
@ -145,70 +144,68 @@ void Diffusion::BTCSDiffusion::simulate2D(
double local_dt = this->time_step / BTCS_2D_DT_SIZE;
DMatrixRowMajor t0_c = calc_t0_c(c, alpha, bc, local_dt, dx);
DMatrixRowMajor d_ortho = calc_d_ortho(c, alpha, bc, local_dt, dx);
#pragma omp parallel for schedule(dynamic)
for (int i = 0; i < n_rows; i++) {
DVectorRowMajor input_field = c.row(i);
simulate_base(input_field, bc.row(i + 1), alpha.row(i), dx, local_dt,
n_cols, t0_c.row(i));
n_cols, d_ortho.row(i));
c.row(i) << input_field;
}
dx = this->deltas[1];
t0_c =
calc_t0_c(c.transpose(), alpha.transpose(), bc.transpose(), local_dt, dx);
d_ortho = calc_d_ortho(c.transpose(), alpha.transpose(), bc.transpose(),
local_dt, dx);
#pragma omp parallel for schedule(dynamic)
for (int i = 0; i < n_cols; i++) {
DVectorRowMajor input_field = c.col(i);
simulate_base(input_field, bc.col(i + 1), alpha.col(i), dx, local_dt,
n_rows, t0_c.row(i));
n_rows, d_ortho.row(i));
c.col(i) << input_field.transpose();
}
}
auto Diffusion::BTCSDiffusion::calc_t0_c(const DMatrixRowMajor &c,
const DMatrixRowMajor &alpha,
const BCMatrixRowMajor &bc,
double time_step, double dx)
auto Diffusion::BTCSDiffusion::calc_d_ortho(const DMatrixRowMajor &c,
const DMatrixRowMajor &alpha,
const BCMatrixRowMajor &bc,
double time_step, double dx)
-> DMatrixRowMajor {
int n_rows = this->grid_cells[1];
int n_cols = this->grid_cells[0];
int n_rows = c.rows();
int n_cols = c.cols();
DMatrixRowMajor t0_c(n_rows, n_cols);
DMatrixRowMajor d_ortho(n_rows, n_cols);
std::array<double, 3> y_values{};
// first, iterate over first row
for (int j = 0; j < n_cols; j++) {
boundary_condition tmp_bc = bc(0, j + 1);
double sy = (time_step * alpha(0, j)) / (dx * dx);
if (tmp_bc.type == Diffusion::BC_CLOSED) {
continue;
}
y_values[0] = getBCFromFlux(tmp_bc, c(0, j), alpha(0, j));
y_values[0] = (tmp_bc.type == Diffusion::BC_CONSTANT
? tmp_bc.value
: getBCFromFlux(tmp_bc, c(0, j), alpha(0, j)));
y_values[1] = c(0, j);
y_values[2] = c(1, j);
t0_c(0, j) = time_step * alpha(0, j) *
(2 * y_values[0] - 3 * y_values[1] + y_values[2]) / (dx * dx);
d_ortho(0, j) = -sy * (2 * y_values[0] - 3 * y_values[1] + y_values[2]);
}
// then iterate over inlet
#pragma omp parallel for private(y_values) schedule(dynamic)
for (int i = 1; i < n_rows - 1; i++) {
for (int j = 0; j < n_cols; j++) {
double sy = (time_step * alpha(i, j)) / (dx * dx);
y_values[0] = c(i - 1, j);
y_values[1] = c(i, j);
y_values[2] = c(i + 1, j);
t0_c(i, j) = time_step * alpha(i, j) *
(y_values[0] - 2 * y_values[1] + y_values[2]) / (dx * dx);
d_ortho(i, j) = -sy * (y_values[0] - 2 * y_values[1] + y_values[2]);
}
}
@ -217,21 +214,18 @@ auto Diffusion::BTCSDiffusion::calc_t0_c(const DMatrixRowMajor &c,
// and finally over last row
for (int j = 0; j < n_cols; j++) {
boundary_condition tmp_bc = bc(end + 1, j + 1);
if (tmp_bc.type == Diffusion::BC_CLOSED) {
continue;
}
double sy = (time_step * alpha(end, j)) / (dx * dx);
y_values[0] = c(end - 1, j);
y_values[1] = c(end, j);
y_values[2] = getBCFromFlux(tmp_bc, c(end, j), alpha(end, j));
y_values[2] = (tmp_bc.type == Diffusion::BC_CONSTANT
? tmp_bc.value
: getBCFromFlux(tmp_bc, c(end, j), alpha(end, j)));
t0_c(end, j) = time_step * alpha(end, j) *
(y_values[0] - 3 * y_values[1] + 2 * y_values[2]) /
(dx * dx);
d_ortho(end, j) = -sy * (y_values[0] - 3 * y_values[1] + 2 * y_values[2]);
}
return t0_c;
return d_ortho;
}
void Diffusion::BTCSDiffusion::fillMatrixFromRow(
@ -285,7 +279,7 @@ void Diffusion::BTCSDiffusion::fillMatrixFromRow(
void Diffusion::BTCSDiffusion::fillVectorFromRow(
Eigen::VectorXd &b_vector, const DVectorRowMajor &c,
const DVectorRowMajor &alpha, const BCVectorRowMajor &bc,
const DVectorRowMajor &t0_c, int size, double dx, double time_step) {
const DVectorRowMajor &d_ortho, int size, double dx, double time_step) {
Diffusion::boundary_condition left = bc[0];
Diffusion::boundary_condition right = bc[size + 1];
@ -303,9 +297,7 @@ void Diffusion::BTCSDiffusion::fillVectorFromRow(
continue;
}
double t0_c_j = time_step * alpha[j] * (t0_c[j] / (dx * dx));
double value = (c[j] < DOUBLE_MACHINE_EPSILON ? .0 : c[j]);
b_vector[j + 1] = -value - t0_c_j;
b_vector[j + 1] = -c[j] + d_ortho[j];
}
// this is not correct currently.We will fix this when we are able to define