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_{\{x,y\}} U + 0.1*\Delta_{\{x,y\}} U \]
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 emplying a Lagrangian frame of reference. Hence the Pde is transformed to:
\[\begin{align} \frac{\partial\vec{X}}{dt}=\vec{V}\\ \frac{\partial\vec{C}}{dt}=0.1*\Delta_{\{x,y\}} U \end{align} \]
This is a system of PDEs. We decouple 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.
Output: Time series data of the PDE Solution.
These are the header files that we need to include:
We start with
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
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 pas 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
)
There are multiple ways in which the system can be integrated. For example, and ideally, we could put both the PDEs into the RHS functor (Moving the particles at every stage). This can be expensive. However, Often in numerical simulations, Both the PDEs can be integrated with seperate steppers. To achieve this we will use the observer functor. The observer functor is called before every time step evolution by odeint. Hence we can use it to update the position of the particles, with an euler step. and also update the operators and write/observe the solution.
Now we create the Observer functor. Please refer to ODE_int for more detials. Note that we have templated it with two operator types DXX and DYY again. But we never use them. It is just an example to show how versatile the observer can be.
All Observer computations needs to happen in the operator (). Odeint expects the arguments here to be an input state_type X, and time t. We pass on the openfpm distributed state types as void operator()( const state_type_2d_ofp &X , const double t ) const
Since we would like to use again 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.
We start with
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.
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.
Here we create two dcpse based operators and alias the particle properties.
Now we create a odeint stepper object (RK4 in this case. Please refer to odeint on more such methods or examples listed as comments after calling the method). 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.
We initiliaze the time variable t, step_size dt and final time tf.
We create a vector for storing the intermidiate time steps, as most odeint calls return such an object.
We then Call the Odeint_rk4 method created above to do a rk4 time integration from t0 to tf with arguments as the System, the state_type, current time t and the stepsize dt.
Odeint updates X in place. And automatically advect the particles (an Euler step) and do a map and ghost_get as needed after moving particles by calling the observer.
The observer also update the subset bulk and the DCPSE operators.
We finally deallocate the DCPSE operators and finalize the library.