In this example, we perform time integration in a 2d domain of particles of a following partial differential equation:
\[ \frac{\partial\vec{C}}{dt}=\vec{V}.\nabla C + 0.1\Delta C \]
in 2d domain [-1,-1]*[1,1] with a fixed velocity \(\vec{V}(x,y)=(-ye^{-10(x^2+y^2)},xe^{-10(x^2+y^2)})\), and the boundary conditions on the walls as no-slip for velocity \(\vec{V}=0\) and sink for the chemicals \(\vec{C}=0\) for all time \(t\).
- Further, we start with the initial condition for the concentration as
\[\vec{C}=\begin{cases}
(1,0)\text{ for } x=0,-0.5<y<0\\
(0,1)\text{ for } x=0, 0<y<0.5\\
(0,0) \text{ for the rest of the domain}\\
\end{cases} \]
We do that by employing a Lagrangian frame of reference. Hence the PDE is transformed to a system of PDEs:
\[\begin{align}
\frac{\partial\vec{X}}{dt}=\vec{V}\\
\frac{\partial\vec{C}}{dt}=0.1\Delta_{\{x,y\}} \vec{C}
\end{align} \]
Where \(\vec{X}\) is the position vector. Hence, we have decoupled the moving of the particles from the evolution of the chemicals. As it can be expensive to recompute derivatives at every stage of a time integrator in a single step, we will integrate the PDEs with seperate techniques (Euler step for moving the particles).
Output: Time series data of the PDE Solution.
Including the headers
These are the header files that we need to include:
#include "Operators/Vector/vector_dist_operators.hpp"
#include "Vector/vector_dist_subset.hpp"
#include "DCPSE/DCPSE_op/DCPSE_op.hpp"
#include "OdeIntegrators/OdeIntegrators.hpp"
Initialization of the global parameters
We start with
- Initializing certain global parameteres we will use: such as x,y to refer to the dimensions 0 and 1. (Makes it easier to read equations in code)
- Creating empty pointers for coupling openfpm distributed vector with odeint. One for the entire distributed vector and another for the subset or bulk. We seperate bulk and the entire distribution as it makes it easier to impose boundary conditions. (Which will be more apparant in ComputeRHS of the PDE) Note that a subset expression always comes at the left hand side of a computation. (The semantics of the expressions is by denoting what we want to update from regular expressions)
Creating aliases of the types of the datasructures we are going to use in OpenFPM. Property_type as the type of properties we wish to use. dist_vector_type as the 2d openfpm distributed vector type dist_vector_type as the 2d openfpm distributed subset vector type
constexpr int x = 0;
constexpr int y = 1;
double dt;
void *PointerDistGlobal, *PointerDistSubset;
This class implement the point shape in an N-dimensional space.
aggregate of properties, from a list of object if create a struct that follow the OPENFPM native stru...
Creating the RHS Functor
Odeint works with certain specific state_types. We offer certain state types such as 'state_type_2d_ofp' for making openfpm work with odeint.
Now we create the RHS functor. Please refer to ODE_int for more detials. Note that we have templated it with two operator types DXX and DYY as we need to compute Laplacian at each stage. We will pass the DCPSE operators to an instance of this functor.
All RHS computations needs to happen in the operator (). Odeint expects the arguments here to be an input state_type X, an output state_tyoe dxdt and time t. We pass on the openfpm distributed state types as void operator()( const state_type_2d_ofp &X , state_type_2d_ofp &dxdt , const double t ) const
Since we would like to use openfpm here. We cast back the global pointers created before to access the Openfpm distributed vector here. (Note that these pointers needs to initialized in the main(). Further, 'state_type_2d_ofp' is a temporal structure, which means it does not have the ghost. Hence we copy the current state back to the openfpm vector from the openfpm state type X. We do our computations as required. Then we copy back the output into the state_type dxdt)
template<typename DXX,typename DYY>
{
DXX &Dxx;
DYY &Dyy;
{}
{
auto C = getV<0>(Particles);
auto C_bulk = getV<0>(Particles_bulk);
auto dC = getV<2>(Particles);
auto dC_bulk = getV<2>(Particles_bulk);
C_bulk[x]=X.data.get<0>();
C_bulk[y]=X.data.get<1>();
dC_bulk[x] = 0.1*(Dxx(C[x])+Dyy(C[x]));
dC_bulk[y] = 0.1*(Dxx(C[y])+Dyy(C[y]));
dxdt.data.get<0>()=dC[x];
dxdt.data.get<1>()=dC[y];
}
};
void ghost_get(size_t opt=WITH_POSITION)
It synchronize the properties and position of the ghost particles.
A 2d Odeint and Openfpm compatible structure.
Initializating OpenFPM
We start with
int main(int argc, char* argv[]) {
openfpm_init(&argc, &argv);
Creating Particles and assigning subsets
We create a particle distribution we certain rCut for the domain [-1,-1] to [1,1].
Also, we fill the initial concentration as C_1(x=0,y>0 & y<0.5,t=0)=1,C_2(x=0,y<0 & y>-0.5,t=0)=1 and 0 everywhere else.
const size_t sz[2] = {41, 41};
size_t bc[2] = {NON_PERIODIC, NON_PERIODIC};
double spacing[2];
spacing[0] = 2.0 / (sz[0] - 1);
spacing[1] = 2.0 / (sz[1] - 1);
double rCut = 3.1 * spacing[0];
Particles.setPropNames({"Concentration", "Velocity", "TempConcentration"});
auto it = Particles.getGridIterator(sz);
while (it.isNext()) {
Particles.add();
auto key = it.get();
double x = -1.0 + key.get(0) * spacing[0];
Particles.getLastPos()[0] = x;
double y = -1.0 + key.get(1) * spacing[1];
Particles.getLastPos()[1] = y;
if (x != -1.0 && x != 1.0 && y != -1.0 && y != 1) {
Particles.getLastSubset(0);
} else {
Particles.getLastSubset(1);
}
if (x == 0.0 && y > -0.5 && y < 0) {
Particles.template getLastProp<0>()[0] = 1.0;
Particles.template getLastProp<0>()[1] = 0.0;
} else if (x == 0.0 && y > 0 && y < 0.5) {
Particles.template getLastProp<0>()[0] = 0.0;
Particles.template getLastProp<0>()[1] = 1.0;
} else {
Particles.template getLastProp<0>()[0] = 0.0;
Particles.template getLastProp<0>()[1] = 0.0;
}
++it;
}
Particles.map();
Particles.ghost_get<0>();
Particles.write("Init");
This class represent an N-dimensional box.
Create the subset and Cast Global Pointers
On the particles we just created we need to constructed the subset object based on the numbering. Further, We cast the Global Pointers so that Odeint RHS functor can recognize our openfpm distributed structure.
PointerDistGlobal = (void *) &Particles;
PointerDistSubset = (void *) &Particles_bulk;
Creating DCPSE Operators and aliases for expressions
Here we create two dcpse based operators and alias the particle properties.
Derivative_xx Dxx(Particles, 2, rCut);
Derivative_yy Dyy(Particles, 2, rCut);
auto Pos = getV<PROP_POS>(Particles);
auto C = getV<0>(Particles);
auto V = getV<1>(Particles);
auto dC = getV<2>(Particles);
auto C_bulk = getV<0>(Particles_bulk);
auto V_bulk = getV<1>(Particles_bulk);
auto dC_bulk = getV<2>(Particles_bulk);
Creating Odeint Objects
Now we create a odeint stepper object (RK4 in this case. Please refer to odeint on more such methods). Since we are in 2d, we are going to use "state_type_2d_ofp". Which is a structure or state_type compatible with odeint. We further pass all the parameters including "boost::numeric::odeint::vector_space_algebra_ofp",which tell odeint to use openfpm algebra. The template parameters are: state_type_2d_ofp (state type of X), double (type of the value inside the state), state_type_2d_ofp (state type of DxDt), double (type of the time), boost::numeric::odeint::vector_space_algebra_ofp (our algebra).
We further create the an instance of the RHS Functor defined before main. This instance is needed by odeint to compute the stages.
Also, we create the state type compatible with odeint and initialize the concentration in it.
boost::numeric::odeint::runge_kutta4<state_type_2d_ofp, double, state_type_2d_ofp, double, boost::numeric::odeint::vector_space_algebra_ofp> Odeint_rk4;
X.data.get<x>() = C[x];
X.data.get<y>() = C[y];
Time Integration Loop
We create a counter for counting the step and initiliaze the time loop.
Inside the time loop, we first compute the Velocity at the current position and observe the state of the sysem by writing the solution.
We then Call the Odeint_rk4 method created above to do a rk4 step with arguments as the System, the state_type, current time t and the stepsize dt.
Odeint updates X in place. hence we copy the computations of the time step back to Concentration (Bulk only as we dont want to change the boundary conditions).
We then advect the particles (an Euler step) and do a map and ghost_get as needed after moving particles.
We finally update the subset bulk and the DCPSE operators. And also update ctr and t.
After the time loop. we deallocate the DCPSE operators and finalize the library.
int ctr = 0;
double t = 0, tf = 1, dt = 1e-2;
while (t < tf) {
V_bulk[x] = -Pos[y] * exp(-10.0 * (Pos[x] * Pos[x] + Pos[y] * Pos[y]));
V_bulk[y] = Pos[x] * exp(-10.0 * (Pos[x] * Pos[x] + Pos[y] * Pos[y]));
Particles.deleteGhost();
Particles.write_frame("PDE_Sol", ctr);
Particles.ghost_get<0>();
Odeint_rk4.do_step(
System, X, t, dt);
C_bulk[x] = X.data.get<0>();
C_bulk[y] = X.data.get<1>();
Pos = Pos + dt * V;
Particles.map();
Particles.ghost_get<0>();
Particles_bulk.update();
Particles_boundary.update();
Dxx.update(Particles);
Dyy.update(Particles);
X.data.get<0>()=C[x];
X.data.get<1>()=C[y];
ctr++;
t += dt;
}
Dxx.deallocate(Particles);
Dyy.deallocate(Particles);
openfpm_finalize();
return 0;
}
Full code
#include "Operators/Vector/vector_dist_operators.hpp"
#include "Vector/vector_dist_subset.hpp"
#include "DCPSE/DCPSE_op/DCPSE_op.hpp"
#include "OdeIntegrators/OdeIntegrators.hpp"
constexpr int x = 0;
constexpr int y = 1;
double dt;
void *PointerDistGlobal, *PointerDistSubset;
template<typename DXX,typename DYY>
{
DXX &Dxx;
DYY &Dyy;
{}
{
auto C = getV<0>(Particles);
auto C_bulk = getV<0>(Particles_bulk);
auto dC = getV<2>(Particles);
auto dC_bulk = getV<2>(Particles_bulk);
C_bulk[x]=X.data.get<0>();
C_bulk[y]=X.data.get<1>();
dC_bulk[x] = 0.1*(Dxx(C[x])+Dyy(C[x]));
dC_bulk[y] = 0.1*(Dxx(C[y])+Dyy(C[y]));
dxdt.data.get<0>()=dC[x];
dxdt.data.get<1>()=dC[y];
}
};
int main(int argc, char* argv[]) {
openfpm_init(&argc, &argv);
const size_t sz[2] = {41, 41};
size_t bc[2] = {NON_PERIODIC, NON_PERIODIC};
double spacing[2];
spacing[0] = 2.0 / (sz[0] - 1);
spacing[1] = 2.0 / (sz[1] - 1);
double rCut = 3.1 * spacing[0];
Particles.setPropNames({"Concentration", "Velocity", "TempConcentration"});
auto it = Particles.getGridIterator(sz);
while (it.isNext()) {
Particles.add();
auto key = it.get();
double x = -1.0 + key.get(0) * spacing[0];
Particles.getLastPos()[0] = x;
double y = -1.0 + key.get(1) * spacing[1];
Particles.getLastPos()[1] = y;
if (x != -1.0 && x != 1.0 && y != -1.0 && y != 1) {
Particles.getLastSubset(0);
} else {
Particles.getLastSubset(1);
}
if (x == 0.0 && y > -0.5 && y < 0) {
Particles.template getLastProp<0>()[0] = 1.0;
Particles.template getLastProp<0>()[1] = 0.0;
} else if (x == 0.0 && y > 0 && y < 0.5) {
Particles.template getLastProp<0>()[0] = 0.0;
Particles.template getLastProp<0>()[1] = 1.0;
} else {
Particles.template getLastProp<0>()[0] = 0.0;
Particles.template getLastProp<0>()[1] = 0.0;
}
++it;
}
Particles.map();
Particles.ghost_get<0>();
Particles.write("Init");
PointerDistGlobal = (void *) &Particles;
PointerDistSubset = (void *) &Particles_bulk;
Derivative_xx Dxx(Particles, 2, rCut);
Derivative_yy Dyy(Particles, 2, rCut);
auto Pos = getV<PROP_POS>(Particles);
auto C = getV<0>(Particles);
auto V = getV<1>(Particles);
auto dC = getV<2>(Particles);
auto C_bulk = getV<0>(Particles_bulk);
auto V_bulk = getV<1>(Particles_bulk);
auto dC_bulk = getV<2>(Particles_bulk);
boost::numeric::odeint::runge_kutta4<state_type_2d_ofp, double, state_type_2d_ofp, double, boost::numeric::odeint::vector_space_algebra_ofp> Odeint_rk4;
X.data.get<x>() = C[x];
X.data.get<y>() = C[y];
int ctr = 0;
double t = 0, tf = 1, dt = 1e-2;
while (t < tf) {
V_bulk[x] = -Pos[y] * exp(-10.0 * (Pos[x] * Pos[x] + Pos[y] * Pos[y]));
V_bulk[y] = Pos[x] * exp(-10.0 * (Pos[x] * Pos[x] + Pos[y] * Pos[y]));
Particles.deleteGhost();
Particles.write_frame("PDE_Sol", ctr);
Particles.ghost_get<0>();
Odeint_rk4.do_step(
System, X, t, dt);
C_bulk[x] = X.data.get<0>();
C_bulk[y] = X.data.get<1>();
Pos = Pos + dt * V;
Particles.map();
Particles.ghost_get<0>();
Particles_bulk.update();
Particles_boundary.update();
Dxx.update(Particles);
Dyy.update(Particles);
X.data.get<0>()=C[x];
X.data.get<1>()=C[y];
ctr++;
t += dt;
}
Dxx.deallocate(Particles);
Dyy.deallocate(Particles);
openfpm_finalize();
return 0;
}