C++ Code
This section covers some information on the C++ code. For more information see the Open source code and use doxygen.
Exudyn was developed for the efficient simulation of flexible multi-body systems. Exudyn was designed for rapid implementation and testing of new formulations and algorithms in multibody systems, whereby these algorithms can be easily implemented in efficient C++ code. The code is applied to industry-related research projects and applications.
Focus of the C++ code
The code focuses on four principles, starting with highest priority:
developer-friendly
error minimization
user-friendliness
efficiency
The focus is therefore on:
A developer-friendly basic structure regarding the C++ class library and the possibility to add new components.
The basic libraries are slim, but extensively tested; only the necessary components are available
Complete unit tests are added to new program parts during development; for more complex processes, tests are available in Python
In order to implement the sometimes difficult formulations and algorithms without errors, error avoidance is always prioritized.
To generate efficient code, classes for parallelization (vectorization and multithreading) are provided. We live the principle that parallelization takes place on multi-core processors with a central main memory, and thus an increase in efficiency through parallelization is only possible with small systems, as long as the program runs largely in the cache of the processor cores. Vectorization is tailored to SIMD commands as they have Intel processors, but could also be extended to GPGPUs in the future.
The user interface (Python) provides a nearly 1:1 image of the system and the processes running in it, which can be controlled with the extensive possibilities of Python.
C++ Code structure
The following entry points into the C++ code can be found:
Python – C++: the creation of the module
exudyn
is found in:
main/src/Pymodules/PybindModule.cpp
it includes large header files, which are automatically created for binding C++ code with Python.
The object factory for creation of items (calling
mbs.AddNode(...)
and similar):main/src/Main/MainObjectFactory.h / .cpp
Using the VisualStudio
.sln
file and using the Debug mode allows you to smoothly walk from Python to C++ code (though that this takes some time to start up and it does not work always; and it does not work for graphics if it runs in a separate thread).
The functionality of the code is mainly based on systems (MainSystem and CSystem), items and solvers representing the multibody system or similar physical systems to be simulated. Parts of the core structure of Exudyn are:
CSystem / MainSystem: a multibody system which consists of nodes, objects, markers, loads, etc.
SystemContainer: holds a set of systems; connects to visualization (container)
items: node, (computational) object, marker, load, sensor
computational objects: efficient objects for computation = bodies, connectors, connectors, loads, nodes, …
visualization objects: interface between computational objects and 3D graphics
main (manager) objects: do all tasks (e.g. interface to visualization objects, GUI, Python, …) which are not needed during computation
static solver, kinematic solver, time integration
Python interface via pybind11; items are accessed with a dictionary interface; system structures and settings read/written by direct access to the structure (e.g. SimulationSettings, VisualizationSettings)
interfaces to linear solvers; future: optimizer, eigenvalue solver, … (mostly external or in Python)
autogenerated: this folder in
main/src
contains many item definitions as well as other interface files; they are all automatically generated by some Python code and should not be changed manually as they will be overwritten.
C++ Code: Modules
The following internal modules are used, which are represented by directories in main/src
:
Autogenerated: item (nodes, objects, markers and loads) classes split into main (management, Python connection), visualization and computation
Graphics: a general data structure for 2D and 3D graphical objects and a tiny openGL visualization; linkage to GLFW
Linalg: Linear algebra with vectors and matrices; separate classes for small vectors (SlimVector), large vectors (Vector and ResizableVector), vectors without copying data (LinkedDataVector), and vectors with constant size (ConstVector)
Main: mainly contains SystemContainer, System and ObjectFactory
Objects: contains the implementation part of the autogenerated items
Pymodules: manually created libraries for linkage to Python via pybind; remaining linking to Python is located in autogenerated folder
pythonGenerator: contains Python files for automatic generation of C++ interfaces and Python interfaces of items;
Solver: contains all solvers for solving a CSystem
System: contains core item files (e.g., MainNode, CNode, MainObject, CObject, …)
Tests: files for testing of internal linalg (vector/matrix), data structure libraries (array, etc.) and functions
Utilities: array structures for administrative/managing tasks (indexes of objects … bodies, forces, connectors, …); basic classes with templates and definitions
The following main external libraries are linked to Exudyn:
LEST: for testing of internal functions (e.g. linalg)
GLFW: 3D graphics with openGL; cross-platform capabilities
Eigen: linear algebra for large matrices, linear solvers, sparse matrices and link to special solvers
pybind11: linking of C++ to Python
Code style and conventions
This section provides general coding rules and conventions, partly applicable to the C++ and Python parts of the code. Many rules follow common conventions (e.g., google code style, but not always – see notation):
write simple code (no complicated structures or uncommon coding)
write readable code (e.g., variables and functions with names that represent the content or functionality; AVOID abbreviations)
put a header in every file, according to Doxygen format
put a comment to every (global) function, member function, data member, template parameter
ALWAYS USE curly brackets for single statements in ‘if’, ‘for’, etc.; example: if (i<n) {i += 1;}
use Doxygen-style comments (use ‘//!’ Qt style and ‘@ date’ with ‘@’ instead of ‘' for commands)
use Doxygen (with preceeding ‘@’) ‘test’ for tests, ‘todo’ for todos and ‘bug’ for bugs
USE 4-spaces-tab
use C++11 standards when appropriate, but not exhaustively
ONE class ONE file rule (except for some collectors of single implementation functions)
add complete unit test to every function (every file has link to LEST library)
avoid large classes (>30 member functions; > 15 data members)
split up god classes (>60 member functions)
mark changed code with your name and date
REPLACE tabs by spaces: Extras->Options->C/C++->Tabstopps: tab stopp size = 4 (=standard) + KEEP SPACES=YES
Notation conventions
The following notation conventions are applied (no exceptions!):
use lowerCamelCase for names of variables (including class member variables), consts, c-define variables, …; EXCEPTION: for algorithms following formulas, e.g., \(f = M*q_{tt} + K*q\), GBar, …
use UpperCamelCase for functions, classes, structs, …
Special cases for CamelCase (with some exceptions that happened in the past …):
continue upper case after upper case abbreviations in case of functions or classes: ‘ODESystem’, ‘Point2DClass’, ‘ANCFCable2D’, ‘ANCFALE’, ‘ComputeODE1Equations’, … (this is not always nice to read, but has become a standard and will be further used!)
for variables and class member variables continue lower case: ‘nODE1variables’, ‘dim2Dspecial’, ‘ANCFsize’
abbreviations at beginning of expressions: for functions or classes use
ODEComputeCoords()
, for variables avoid ‘ODE’ at beginning: use ‘nODE’ or write ‘odeCoordinates’‘[…]Init’ … in arguments, for initialization of variables; e.g. ‘valueInit’ for initialization of member variable ‘value’
use American English throughout: Visualization, etc.
AVOID consecutive capitalized words, e.g., avoid ‘ODEAE’
do not use ‘_’ within variable or function names; exception: derivatives
use name which exactly describes the function/variable: ‘numberOfItems’ instead of ‘size’ or ‘l’
examples for variable names: secondOrderSize, massMatrix, mThetaTheta
examples for function/class names:
SecondOrderSize
,EvaluateMassMatrix
,Position(const Vector3D\& localPosition)
use the Get/Set…() convention if data is retrieved from a class (Get) or something is set in a class (Set); Use
const T\& Get()/T\& Get
if direct access to variables is needed; Use Get/Set for pybind11example Get/Set:
Real* GetDataPointer()
,Vector::SetAll(Real)
,GetTransposed()
,SetRotationalParameters(...)
,SetColor(...)
, …use ‘Real’ instead of double or float: for compatibility, also for AVX with SP/DP
use ‘Index’ for array/vector size and index instead of size_t or int
item: object, node, marker, load: anything handled within the computational/visualization systems
Do not use numbers (3 for 3D or any other number which represents, e.g., the number of rotation parameters). Use const Index or constexpr to define constants.
No-abbreviations-rule
The code uses a minimum set of abbreviations; however, the following abbreviation rules are used throughout: In general: DO NOT ABBREVIATE function, class or variable names: GetDataPointer() instead of GetPtr(); exception: cnt, i, j, k, x or v in cases where it is really clear (short, 5-line member functions).
Exceptions to the NO-ABBREVIATIONS-RULE:
ODE2: marks parts related to second order differential equations (SOS2, EvalF2 in HOTINT)
ODE1: marks parts related to first order differential equations (ES, EvalF in HOTINT)
AE; note: using the term ‘AEcoordinates’ for ‘algebraicEquationsCoordinates’
‘C[…]’ … Computational, e.g. for ComputationalNode ==> use ‘CNode’
T66; based on \(6\times 6\) matrix transformations
write time derivatives with underscore: _t, _tt; example: Position_t, Position_tt, …
write space-wise derivatives ith underscore: _x, _xx, _y, …
if a scalar, write coordinate derivative with underscore: _q, _v (derivative w.r.t. velocity coordinates)
for components, elements or entries of vectors, arrays, matrices: use ‘item’ throughout
‘[…]Init’ … in arguments, for initialization of variables; e.g. ‘valueInit’ for initialization of member variable ‘value’
Implementation of new computational items in C++
This section should sketch which changes will be needed to integrate new C++ items.
In general, it is recommended to first start with a Python implementation with user functions based on
NodeGeneric...
, ObjectGeneric...
, ObjectConnectorCoordinateVector
for constraints and
any suitable connector for new nodes or objects. New sensors can be based on the SensorUserFunction
.
If such an implementation is successful, but too slow, a C++ implementation can be considered. In the following, two use cases are shown, which show the simplicity of the procedure:
Case 1: user object (body):
It is recommended to first search for a body with a similar behavior. Copy the definition of such an object inside the file
objectDefinition.py
and edit the according lines. There is not much description of this file yet (except from the first lines of the file), as it will be transformed into another format in the future. Basically, you need to edit the interface, which contains parameters (which are linked to Python) and functions, which go to the header file. When you finished editing, runpythonAutoGenerateObjects.py
. This generates the header file insrc/autogenerated
but also adds description to some docs files and adds thepybind11
interface. Now copy the implementation (.cpp
) file of the same connector from which you copied from and rename and edit all functions. For the body
ComputeMassMatrix
: computes the mass matrix either in sparse or dense mode; this function is performance-critical if the mass matrix is non-constantComputeODE2LHS
: computes the LHS generalized forces of the body; this function is performance-criticalGetAccessFunctionTypes
: specifies, which access functions are available inGetAccessFunctionBody(...)
GetAccessFunctionBody
: needs to compute functions for ‘access’ to the body, in the sense that e.g. forces or torques can be applied.GetAvailableJacobians
: shall return the flags which jacobians ofComputeODE2LHS
need to be computed and which are available as functions; binary flags added upGetOutputVariableBody
: function needs to implement the output variables, such as position, acceleration, forces, etc. as defined inGetOutputVariableTypes()
HasConstantMassMatrix
: specifies, if mass matrix is constantGetNumberOfNodes
: number of nodes of objectGetODE2Size
: total number of ODE2 coordinatesGetType
: some flags for objects, such asBody
,SingleNoded
,SuperElement
, …; these flags are needed for connectivity and special treatment in the systemGetPosition, GetVelocity, ...
: provide this functions as far as possible; rigid bodies need to provide positions and rotation matrix, as well as velocity and angular velocity for markers; if functions do not exist, some marker or sensor functions may fail… possibly some helper functions, which you should implement for the functionality of your object.
Case 2: user connector:
It is recommended to search for a connector with similar behavior; first check, if you would like to implement an algebraic constraint or a spring-damper-like connector. Again, copy a similar connector in
objectDefinition.py
and edit the according lines. When you finished editing, runpythonAutoGenerateObjects.py
and make a copy of the copied implementation (.cpp
) file. The implementation file usually consists of
ComputeODE2LHS
: this function shall compute the LHS generalized forces on the two marker objectsComputeJacobianODE2_ODE2
: computes theGetAvailableJacobians()
is not providing any ‘…_function’ flag, which indicates that these jacobians are available as functionGetOutputVariableConnector
: this function needs to compute all output variables as given inGetOutputVariableTypes()
… possibly some helper functions, which you should implement for the functionality of your object.