What is Mpacts?¶
Mpacts is not a program or a C++ library, it is a set of python modules which can be combined in python to create flexible and fast simulations. As a consequence of this:
- The full flexibility of Python is available to the user to define dynamical simulations
- Time critical components are written in C++ and placed into a C++ framework guaranteeing top level execution speeds
- By adding/removing (C++ or pure) Python modules, the framework can be easily extended or changed.
- By using fine grained modules, reuse is improved while dependencies and compile times are reduced.
In this documentation some basic concepts are explained. Additionally all python modules are automatically documented, and the usage of some of these modules is shown in the examples or simulation archive section.
Any C++ object that can be constructed from python is a “BaseObject”. These can be organized in a tree structure. An example of such a tree structure is shown below.
- The image above could represent a real Mpacts simulation. The first thing that one should notice is indeed that anything in the tree (Simulations, Commands, ...) all inherits from BaseObject, and therefore all have the functionality provided by BaseObject. The full documentation of BaseObject is available in the API documentation. But the main features are:
- Any BaseObject can receive another BaseObject and store it as its child using
- Any BaseObject can be parented to another BaseObject. Either by performing an add_child on the parent, or by passing the parent to the constructor of the child. However there are the following conditions:
- The child must have a name
- The child may not be parented already
Freestanding objects are completely legal, it is not “required” to put objects in a tree, nor is it required to have only one tree.
- Any BaseObject may be initialized using the
mpacts.core.baseobject.BaseObject.set()method, allowing full initialization from python. The properties that can be given to set are documented for every object.
- Accessing the whole tree is possible using the ‘()’ operator. For instance b(”../otherobject”) will go to the parent of b, and look for the otherobject BaseObject there.
- Any BaseObject can receive another BaseObject and store it as its child using
Let us have another look at the figure, since most objects there are common Mpacts objects. At the top of the tree there is a simulation object, which acts as a “container” for the whole simulation and is usually (but not obligatory) the root of the tree. This object is explained in more detail in The Simulation object.
To the simulation root object, an ArrayManager is attached with as a name “Spheres”. An arraymanager is a container class, that groups several arrays of different types, but of identical length. As an example, the position and velocity arrays “x” and “v” are attached to this ArrayManager. An ArrayManager is often used to create a Particle Container, a datastructure that contains all data of one specific type of particles. It is perfectly possible to put an ArrayManager child under another ArrayManager in order to represent more complicated types of particles.
On the other side, the children of the CommandList represent two Commands, one to integrate a certain Particle Container (ArrayManager), and one to add gravitation. Commands are the workhorse of any simulation. Any simulation can be seen as a collection of Commands working on data, usually ArrayManagers. Commands are explained in more detail in Commands.
The Simulation object¶
Most Mpacts simulations will contain at least one simulation object. This small object has two important functionalities:
- It keeps track of the timestep in the simulations, many commands need this timestep, and will request the timestep from the simulation
- It defines a predefined collection of CommandLists in which user-commands can be placed.
The predefined layout of the simulation commandlists is as follows:
The “init_cmds” are executed only once, and they are automatically executed when the first “run_n” or “run_until” command is given. All “loop_cmds” are executed at every time step in the order in which they are created.
The order in which they will be executed is the same as the order that will appear when printing the tree using print sim.print_tree() or print sim.print_command_tree()
When you want to add a command between two already created commands, this can be done using “add_before” or “add_after” on either of the existing commands.
Although all commands can be explicitly placed in any list, most commands have a default location. This helps reducing the amount of typework for the user. As an example GravityCommand will by default be placed in “body_force_cmds” when the parent given to GravityCommand upon construction is a simulation object.
This can be circumvent by giving a parent object that is not a simulation object like this: gravcmd = GravityCommand(“Gravity”, sim(“loop_cmds/output_cmds”))
Finally, the “done_cmds” are executed upon every completion of a “run_n” or “run_until” and might as a consequence be called more then once.
Commands are the workhorse of any simulation, however there basic principle is very easy. They are basically BaseObjects that have one additional feature: They can be executed. All commands have an “execute” function that “performs” their job.
All information they need to be able to execute is already present upon execution, which is why no arguments can be provided to the execute function. Usually Commands get all information they need when they are created. This principle is illustrated below.
A contactmodel is a model that describes the contact between two different objects (particles). In the most common case, this model will calculate the overlap, and potentially other geometrical parameters, between two objects and add up a resulting force to both objects. A well known example of a contactmodel is Hertz.
ContactModels do not neccessarely calculate forces, in principle their only defining trait is that they work on a pair of particles rather than a single particle (unlike for instance Bodyforces)
Since Mpacts supports a large number of geometries, there exist a very large number of ContactModels for all possible Geometrical combinations. However when creating a ContactModel from python, an automatic search for a suitable ContactModel for the given geometries is performed. This search is based on the “GeometryTags” of both Particle Containers (ArrayManagers). This tag is basically nothing more than an indication from the user to the program that a certain ArrayManager represents for instance Spheres. The contactmodel will then search for the appropriate data-arrays (such as “x” for position, “r” for radius of the spheres) and complain if it fails to find any required array.
Not all models are defined for all geometries. For example it is not possible to create a Hertz contact between two flat planes, since there is no contact point that can be defined but a contact plane. Hence, instantation of Hertz with e.g. two RigidTriangle particle containers will fail.
ContactModels can only be defined between ParticleContainers (ArrayManager) and not between individual particles
A contact model in itself does not “DO” anything, it is basically just a description of how a contact would look like. It has to be combined with a ContactDetector to compute actual interactions.
A contactmodel gets the two indices of the particles between which a contact-calculation is required by a contactdetector and updates all required arrays. It optionally also returns a double that can be interpreted as “relevance” which is usually the distance between the two particles.
An illustration of the interaction between ContactModels and ContactDetectors is shown below.
ContactDetectors are responsible for finding out which contacts are potentially interesting. They always require a ContactModel that they will use to actually “do” the contact that they find. This implies that the ContactDetector knows nothing about the “type” of contact. As a consequence, the number of ContactDetectors is fairly limited. They are shortly explained here.
- MultiGrid: This is the most commonly used ContactDetector. It divides space up into different grids, and does a very efficient contactdetection between these grids. In order to divide the objects into the grids, Axis Aligned Bounding Boxes (AABB) are used. These are automatically generated and updated by the MultiGrid ContactDetector. In general this is the recommended ContactDetector since it is fast, easy and scales O(N).
- SmartBruteForce: This is a safe but slow contactdetector, that basically “brute-forces” all contacts. This means that it will offer all particles in PC1 to all particles in PC2. It is called “smart” because it will keep a list of “relevant” contacts that can be repeated a number of times to speed things up. This can be a good choice for very small number of particles, but given its scaling of O(N^2) it becomes slow very quickly.
- FixedList: This contactdetector keeps a user-defined list of contacts and does no actual detection. This is useful for fixed connectivity objects such as spring-networks.
- GridBased: An older version of MultiGrid that uses only a single grid. Since it is slower, and less easy to use, it is not recommended except for special cases.
- SweepAndPrune: This is a very efficient contactdetector that is easy to use, however it currently only works for contactdetection in the same particle container. It is also not recommended for very large number of particles.
- ReuseExisting: Sometimes it is required to run two different contactmodels on the same set of “interactions”. This can be done by ReuseExisting, which has to be given a contactdetector. The contacts of the given contactdetector are then given to another contactmodel.
- NoContactStateBruteForce: Really stupid brute force, that does not store the “relevance” of the contacts. Although this is a very inefficient contactdetector, it is useful when N^2 contacts are really necessary and there are memory constraints.
All contactdetector are also Commands and can be executed like any other command.
Sometimes it is not desirable to execute a certain command on all particles in a given particle container. A very simple example is the removal of particles from the simulation. Sometimes particles have to be removed from a simulation because they have reached the end of the domain of interest. One can use the RemoveParticleCommand for this, but this would remove all particles from the simulation. This is why there are predicates: These are small objects that have a “check” function that can be called for all particles. The return value is always a boolean that will determine whether the Command is executed for this particle or not. This allows the same command “RemoveParticle” to be used for particles that get outside a sphere, under a plane, or even have a too high mass or velocity.
Almost all commands support an optional predicate that will decide whether or not the command is executed.