CEPS  24.01
Cardiac ElectroPhysiology Simulator
Implementation of a problem: example

This page will explain briefly what is going on in CEPS when running a simulation. This will be illustrated with the pseudo wave front problem, which is a manufactured problem that mimicks the planar propagation of an action potential, but with an analytic expression of the solution. The solution is basically the product of two hyperbolic tangent functions.

Note that the code snippets below are manually extracted. The actual source files may have changed by the time you read this page. If the API didn't change, the class and method names are clickable, so that you can access their documentation.

The main program: instanciation of the problem

# applications/ceps.cpp:38
void
{
AbstractPdeProblem* pb = nullptr;
CepsString s = ceps::toKey(p->getString("problem type"));
// ...
else if (s=="PSEUDOWAVEFRONT")
pb = new PseudoWaveFrontProblem(g,p);
// ...
pb->run();
return;
}
std::basic_string< CepsChar > CepsString
C++ format string.
Definition: CepsTypes.hpp:128
void doSingleRun(InputParameters *p, Geometry *g)
Runs a single simulation Problem type deduced from input string in InputParameters.
Definition: ceps.cpp:39
Base class for creating PDEs to solve.
virtual void run()
Computes the solution to the problem. Default does nothing, override it !
Encapsulates all the geometrical data.
Definition: Geometry.hpp:50
Reads and stores simulation configuration.
CepsString getString(const keyType &key) const
Reads a CepsString from configuration.
CepsString toKey(const CepsString &s)
Transform to key type a std::string, upper case and no spaces.
Definition: CepsString.cpp:157
void destroyObject(_Type &)
Destroy[delete] any type.
Definition: CepsMemory.hpp:116

Here, the input parameters have been read from the file passed in the command line, and the geometry is already initialized and partitioned. The type of problem to instantiate is deduced from parameters (what isn't?).

Let's see what happens in the constructor of the problem:

# pde/problem/PseudoWaveFrontProblem.cpp:32
PseudoWaveFrontProblem::PseudoWaveFrontProblem(Geometry* g, InputParameters *p) :
m_frontX0 (CepsMathVertex( 0,0,0)),
m_frontX1 (CepsMathVertex(-1,0,0)),
m_frontN (CepsMathVertex( 1,0,0)),
m_frontVel (1.),
m_frontEps0(1.e-2),
m_frontEps1(1.e-2)
{
PseudoWaveFrontProblem::setupWithParameters(p);
m_functions->addEntry("dicoSrc",new PseudoWaveFrontSourceTerm(this));
m_functions->addEntry("dicoSol",new PseudoWaveFrontSolution (this));
}
Eigen::Matrix< CepsScalar, 3, 1 > CepsMathVertex
Vertex, eigen format.
Definition: CepsTypes.hpp:135
Heat PDE, single unknown, constant diffusion coeff 1, homogeneous Neumann (no BC defined)
Definition: HeatProblem.hpp:36
CepsBool isValidPtr(_Type *ptr)
Tells if pointer is not null.
Definition: CepsMemory.hpp:61

which calls

# pde/problem/HeatProblem.cpp:33
: AbstractTimedPdeProblem (g,p), m_k(k)
{
}
Eigen::Matrix< CepsScalar, 3, 3 > CepsMathTensor
Tensor, eigen format.
Definition: CepsTypes.hpp:137
Astract Problem which does depend on time.
HeatProblem(Geometry *g, InputParameters *p=nullptr, const CepsMathTensor &k=CepsMathTensor::Identity())
Constructor from geometry and optional input file.
Definition: HeatProblem.cpp:33
void setupWithParameters(InputParameters *params) override
Set attributes from input file. Parameters are passed as arguments in case one wants to use other par...
Definition: HeatProblem.cpp:47
# pde/problem/AbstractPdeProblem.cpp:602
Geometry* geom,
):
AbstractPdeProblem(geom,params),
m_pdeStartTime(0.),
m_pdeEndTime(1.),
m_pdeTimeStep(0.1),
m_pdeSnapshotTime(0.1)
{
if (m_parameters)
}
AbstractTimedPdeProblem(Geometry *geom, InputParameters *params=nullptr)
Constructor with geometry and optional parameters.
void setupWithParameters(InputParameters *params) override
Set attributes from input file. Parameters are passed as arguments in case one wants to use other par...
# pde/problem/AbstractPdeProblem.cpp:370
Geometry* geom,
):
m_geom (geom),
m_parameters (params),
m_discr (nullptr),
m_ownedDiscr (false),
m_analyticSolution (nullptr),
m_ownedRefSol (false),
m_refSolFiles (""),
m_refSolSnapDt (1.),
m_functions (new FunctionDictionary()),
m_boundaryConditions(new BoundaryConditionManager(m_functions)),
m_sourceTerms (new SourceTermManager (m_functions)),
m_ownedFunctions (true),
m_ownedBCs (true),
m_ownedSrcs (true),
m_outputFileBase ("./result"),
m_outputFormat (CepsOutputFormat::PSERIES),
m_writeGlobalIDs (false),
m_probePoints ({})
{
"When instantiating pde problem : passed ptr to Geometry is null"
);
if (m_parameters)
}
CepsOutputFormat
Style of output files.
Definition: CepsEnums.hpp:179
#define CEPS_ABORT_IF(condition, message)
Stops the execution with a message if condition is true. If testing is enabled, only throws a runtime...
virtual void setupWithParameters(InputParameters *params)
Set attributes from input file. Parameters are passed as arguments in case one wants to use other par...
AbstractPdeProblem(Geometry *geom, InputParameters *params=nullptr)
Constructor with geometry and optional parameters.
Boundary condition to manage Dirichlet, Neumann and Robin conditions.
FunctionDictionary that holds functions which can be used to define source terms, boundary conditions...
Source term manager to create and manage SourceTerm objects.
CepsBool isNullPtr(_Type *ptr)
Tells if passed pointer is null.
Definition: CepsMemory.hpp:45

As you can see, the structures of the problem can be found at different levels of heritage as we tried to factorize in a reasonable way what could be. More importantly, each one of these constructors call a method named setupWithParameters which extracts the values required to initialize the problem from the input file.

Note that, at this stage, the spatial discretization is not created. m_discr is still a nullptr. Even if we have the geometry, we still do not know what defines the problems at first : its unknowns.

Defining functions for the problem

After the abstract problem, abstract timed problem and heat problem constructors have done their job and all parameters are read, you can see that we add two entries into the dictionary of functions:

# pde/problem/PseudoWaveFrontProblem.cpp:32
PseudoWaveFrontProblem::PseudoWaveFrontProblem(Geometry* g, InputParameters *p) :
m_frontX0 (CepsMathVertex( 0,0,0)),
m_frontX1 (CepsMathVertex(-1,0,0)),
m_frontN (CepsMathVertex( 1,0,0)),
m_frontVel (1.),
m_frontEps0(1.e-2),
m_frontEps1(1.e-2)
{
PseudoWaveFrontProblem::setupWithParameters(p);
--> m_functions->addEntry("dicoSrc",new PseudoWaveFrontSourceTerm(this)); <--
--> m_functions->addEntry("dicoSol",new PseudoWaveFrontSolution (this)); <--
}

These are the functions that are the source term of the problem, and its analytic solution, respectively. They are declared within the PseudoWaveFrontProblem class:

# pde/problem/PseudoWaveFrontProblem.hpp
class PseudoWaveFrontProblem : public HeatProblem
{
// ...
class PseudoWaveFrontSourceTerm: public ScalarSAFunc
{
public:
explicit PseudoWaveFrontSourceTerm(PseudoWaveFrontProblem* p);
eval(CepsStandardArgs args) override;
getFlags() const override;
private:
PseudoWaveFrontProblem* m_p;
};
class PseudoWaveFrontSolution: public ScalarSAFunc
{
public:
explicit PseudoWaveFrontSolution(PseudoWaveFrontProblem* p);
eval(CepsStandardArgs args) override;
getFlags() const override;
private:
PseudoWaveFrontProblem* m_p;
};
// ...
};
int CepsEnum
Enum type.
Definition: CepsTypes.hpp:216
float CepsReal
Need single precision floating point.
Definition: CepsTypes.hpp:100
A SAFunc is a ceps::Function that uses CepsStandardArgs as argument of call operator (),...
Definition: SAFunc.hpp:100
Structure used to pass arguments to SAFunc (see pde directory) The flags of the SAFunc allows extract...
Definition: CepsTypes.hpp:239

Both these classes are derived from ScalarSAFunc. SAFunc are functors that can return anything, but take only one argument for their evaluation : a variable of structured type CepsStandardArgs. You can extract the needed information from these arguments:

PseudoWaveFrontProblem::PseudoWaveFrontSolution::eval(CepsStandardArgs args)
{
CepsReal dotProd0 = 0.e0;
CepsReal dotProd1 = 0.e0;
for (uint i = 0; i < 3; i++)
{
dotProd0 += (args.x[i]-m_p->m_frontX0(i))*m_p->m_frontN(i);
dotProd1 += (args.x[i]-m_p->m_frontX1(i))*m_p->m_frontN(i);
}
return 0.5*(1+tanh((dotProd0-m_p->m_frontVel*args.t)/m_p->m_frontEps0))
0.5*(1-tanh((dotProd1-m_p->m_frontVel*args.t)/m_p->m_frontEps1));
}
CepsReal3D x
space
Definition: CepsTypes.hpp:241
CepsReal t
time
Definition: CepsTypes.hpp:240

Now run !

Once the problem is instantiated, the main application calls its run method. Since for this problem, we do exactly the same steps as for the heat equation, you can find the implementation in HeatProblem. Here it is:

# pde/problem/HeatProblem.cpp
void
{
getProfiler()->start("whole","solving the PDE (HeatProblem class)");
HeatSolver solver(this);
solver.solve();
m_errors = solver.getErrors();
getProfiler()->stop("whole");
}
CepsBool hasAnalyticSolution() const
Tells if there is an analytic or reference solution.
void initializeEquation() override
Initializes equations (unknowns, bc, source term) and creates the spatial discretization This method ...
CepsArray2< CepsArray3< CepsArray3< CepsReal > > > m_errors
Will store Linf, L1 and L2 relative errors.
Profiler * getProfiler() const
Access to profiler.
Definition: CepsObject.cpp:46
void run() override
Computes the solution.
Definition: HeatProblem.cpp:74
Solve heat equation.
Definition: HeatSolver.hpp:39
void stop(CepsString lbl)
Stops the measure of a labeled chronometer.
void start(CepsString lbl, CepsString dspl="")
Creates or continue a labeled chronometer.
CepsBool isProfiling()
Check if we are currently profiling on the master proc (always false on slave procs).
Definition: CepsFlags.cpp:257

Side note: you can see that we use a profiler to track the time spent on a specific task !

The structure of the run is simple

  1. Initialize the equation : we will see later what it does
  2. Then a solver is created, and we directly call its solve method. It... solves the equation. But it also invokes a VtkWriter that will output the solution as the user asked. We won't go into the details of the numerical solver in this page, since there are too many possibilities and requires jumps back and forth between many files.
  3. Finally, since this problem has an analytic solution, we fetch the errors that were computed by the solver all along the computation.

Defining the equation

Let's see the contents of initializeEquation

void
{
}
virtual void initializeEquation()
Initializes equations (unknowns, bc, source term) and creates the spatial discretization This method ...
virtual void defineInitialCondition()
Sets the pointer on function for initial guess. Here it is set to nullptr, so it will be 0.

I said, the real initializeEquation()

void
{
this->defineUnknowns();
}
virtual void defineUnknowns()=0
Define all the unknowns of the problem here. Must be overriden, and call all necessary addUnknown,...
virtual void defineBoundaryConditions()
Define the boundary conditions. Should be defined in derived classes. Default is no BC.
virtual void defineAnalyticSolution()
Set directly the analytic function, default sets no solution, unless there is a collection of referen...
virtual void defineSourceTerms()
Define the source terms. Should be defined in derived classes. Default is no src term.
void createSpatialDiscretization()
Compute the discretization structure.

Perfect. You can see that it is mostly a succession of defines. These methods are virtual and can be overloaded at will. By default, they do nothing. There is one intruder : you can see that right after the unknowns of the problem are defined, we can determine how many degrees of freedom there will be on each element of the mesh. This is why we create the spatial discretization right after that. It is quite a heavy routine ! For finite elements, it creates the nodal points from which the basis functions are built (they can be different from geom nodes!), then the finite elements, then the degrees of freedom, then the distributed factory from which we can build the linear system.

Let's see the defines for our pseudo wave front:

# pde/problem/heatProblem.cpp:58 Yes it is the same as Heat Problem
void
{
// A single spatial unknown defined everywhere
addUnknown("u");
}
void addUnknown(const CepsString &label, CepsSet< CepsAttribute > attrs={}, CepsLocationFlag flag=CepsLocationFlag::Point, const CepsString &unit="")
Register a new unknown.
void defineUnknowns() override
Lists the unknowns of the problem (one here)
Definition: HeatProblem.cpp:61

Our problem has a single spatial unknown, and it is defined everywhere, so it is quite easy.

void
PseudoWaveFrontProblem::defineSourceTerms()
{
m_sourceTerms->add("manufacturedSource dicoSrc UNKNOWN 0");
}
void
PseudoWaveFrontProblem::defineBoundaryConditions()
{
m_boundaryConditions->add("dirichlet DIRICHLET dicoSol UNKNOWN 0");
}
void
PseudoWaveFrontProblem::defineInitialCondition()
{
m_initialCondition = m_functions->getScalar("dicoSol");
}
void
PseudoWaveFrontProblem::defineAnalyticSolution()
{
m_analyticSolution = m_functions->getScalar("dicoSol");
}

Here, note that the analytic solution and initial condition are directly picked from the functions dictionary of the problem. On the contrary, we must give additional information for the source term and the boundary condition (where do they apply, what is their type, which unknown)... The options that must be given to properly create boundary conditions and source terms are given here.

That being said, we covered most of the aspects you need to know to create a new problem in CEPS. But we barely scratched the surface, and there are of course many technical caveats. For that, you need to put on your spelunker hat and dig into the code.