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 pybind11

  • example 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:

  • ODE

  • 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’

  • mbs

  • min, max

  • abs, rel

  • trig

  • quad

  • RHS

  • LHS

  • EP

  • Rxyz

  • coeffs

  • pos

  • 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, run pythonAutoGenerateObjects.py. This generates the header file in src/autogeneratedbut also adds description to some docs files and adds the pybind11 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-constant

  • ComputeODE2LHS: computes the LHS generalized forces of the body; this function is performance-critical

  • GetAccessFunctionTypes: specifies, which access functions are available in GetAccessFunctionBody(...)

  • 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 of ComputeODE2LHS need to be computed and which are available as functions; binary flags added up

  • GetOutputVariableBody: function needs to implement the output variables, such as position, acceleration, forces, etc. as defined in GetOutputVariableTypes()

  • HasConstantMassMatrix: specifies, if mass matrix is constant

  • GetNumberOfNodes: number of nodes of object

  • GetODE2Size: total number of ODE2 coordinates

  • GetType: some flags for objects, such as Body, SingleNoded, SuperElement, …; these flags are needed for connectivity and special treatment in the system

  • GetPosition, 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, run pythonAutoGenerateObjects.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 objects

  • ComputeJacobianODE2_ODE2: computes the GetAvailableJacobians() is not providing any ‘…_function’ flag, which indicates that these jacobians are available as function

  • GetOutputVariableConnector: this function needs to compute all output variables as given in GetOutputVariableTypes()

  • … possibly some helper functions, which you should implement for the functionality of your object.