(Updated for WavePacket 0.1.1)
There are three common idioms that repeatedly occur in the code and which you should be familiar with to use the C++ version efficiently:
If you are familiar with these terms and keep nodding your head after thinking for a minute, keep these items in mind and jump to the next chapter. If not, the remainder of this section explains the how, what and why of these concepts.
All relevant objects (matrices, N-dimensional potentials etc.) are implemented as tensors using a particular library (https://github.com/juanjosegarciaripoll/tensor). In particular, wavefunctions and density operators are not encapsulated at all, but just passed around and returned as simple complex tensors. This allows you to directly access coefficients of the wave function or the values of the wave function at different positions.
For a, say, two-dimensional expansion into NxM grid points, you would have a tensor of dimensions (N,M). To access the element at (x_i, y_j), you then simply write
wavefunction(i,j)
(where indices start at 0). For a better understanding, you should read the discussion of the different expansions (DVR, FBR, DVR with weights) in include/grid/Grid.hpp. An introduction to the tensor library can be found at http://tream.dreamhosters.com/tensor/html/. As a side-note we remark that the tensor library uses a copy-on-write mechanism. Without going into details, this allows you to use tensors efficiently like primitive data types (similar to, say, Matlab), but optimizes away needless copy operations.
With the exception of primitive data types, tensors and functors, all objects in WavePacket are set up and passed as shared pointers. This is a pattern that you should also follow.
A shared pointer (also called "smart pointer") behaves like a normal pointer that can be shared between many clients / objects. It is "smart" insofar as it will automatically free the memory again as soon as noone uses the pointer any longer. Consider the following code to set up a free particle, and ignore the "auto" keyword for now
auto kinetic = std::make_shared<CartesianKineticEnergy>( ... ); auto hamilt = std::make_shared<SchroedingerOperator>(kinetic);
In the first line, we create a shared pointer and assign it to the variable "kinetic", in the second line, we use it as an input parameter for another (complex) operator, which actually shares the pointer here. Now the kinetic energy operator will not be removed as long as the variable kinetic and the SchroedingerOperator exists. The latter object is typically stored further in a propagator, which then pretty much guarantees that the kinetic energy operator lives through the whole simulation without further input from the user.
Using shared pointers relieves you as user from a lot of lifetime management, but still guarantees that the program always has valid data (except for weird setups like circular dependencies).
Note the following when using of shared pointers:
To call a function "doSomething()" in C++, you generally invoke it like
returnValue = doSomething(parameters);
A particular feature of C++ is that classes can overload the operator "()". This operator overload is then called in the same fashion as a function, i.e.,
ClassWithOverload myobject; returnValue = myobject(parameters); // actually calls myobject.operator()(parameters)
Note that the formal usage is the same as for a function call, only with the function name replaced by the name of the object. For this reason, both constructs are called functors, and the C++ standard library heavily supports an abstraction that allows to treat pure functions and objects with overloaded "()" operators on the same footing.
The important thing for you as a user to note is that whenever a functor is needed, you can just implement an own function, and pass the function name as parameter. This will then use your function for the setup. In particular, functors are used for three purposes:
Let us consider the last use-case as an example. We may have a laser pulse with sin^2-shape within the rotating wave approximation. The exact functional form is
cdouble sin2LaserField(double t) { double E0 = 0.1; // peak electric field double t0 = 0; // peak time double dur = 10 * 41.341; // duration of the pulse: 10 fs double cosval = std::cos(M_Pi/2 * (t-t0) / dur); return cdouble{E0 * cosval * cosval, 0}; }
To use this laser field, we need to pass it to an operator called TimeFunction. Its constructor takes the representation to operate in as well as a functor that takes the double time and returns a complex double value. This signature matches exactly our new function, so we can use it there:
auto E_t = std::make_shared<TimeFunction>(representation, sin2LaserField);
Similar considerations apply to potentials and initial wave functions, which are presented in subsequent sections of the tutorial.