Exudyn coding style and conventions
Source of truth. This file. It began as a transcription of the Code style and conventions sections of the LaTeX introduction, which was converted to docs/manual/introduction.md and no longer carries them; the abbreviation table is generated into
docs/generated/abbreviations.md. There is one place for a rule again.
1. General rules
Write simple code — no complicated structures, no uncommon constructs.
Write readable code: variables and functions named for their content or functionality. Avoid abbreviations (§4).
Put a header in every file (§7).
Put a comment on every global function, member function, data member and template parameter.
Always use curly brackets, including for single statements:
if (i < n) { i += 1; }4-space tabs, expanded to spaces. VS: Extras → Options → C/C++ → Tabstopps: tab size 4, keep spaces = yes.
Use C++11 where appropriate, but not exhaustively - prefer readability for engineering programmers.
Use C++14 - C++20, if very helpful and reducing the overall code size, or to avoid larger workarounds and to enable future extensibility; switching to C++20 features shall be reported by Claude.
Standards after C++20 should be used with care: we like to have multi-platform compatibility, mini computers (Raspberry) often have older setups, so we should not require too modern compilers.
One class, one file — except for collectors of single implementation functions.
Avoid large classes: more than 45 member functions or more than 30 data members is a warning; more than 60 member functions is a god class and should be split - except for some autogenerated containers or similar.
Mark changed code with your name and the date.
Stated priorities, in order: developer-friendliness, error minimization, user-friendliness, efficiency.
2. Notation conventions
The source states these with “no exceptions”.
kind |
case |
examples |
|---|---|---|
variables, class members, consts, defines |
|
|
functions, classes, structs |
|
|
Exception: algorithms that follow a formula may mirror the formula —
f = M*q_tt + K*q, GBar.
Abbreviations inside CamelCase — the one rule that trips people up:
In functions and classes, an upper-case abbreviation continues upper case:
ODESystem,Point2DClass,ANCFCable2D,ANCFALE,ComputeODE1Equations. (“Not always nice to read, but has become a standard and will be further used.”)In variables and class members, it continues lower case:
nODE1variables,dim2Dspecial,ANCFsize.At the beginning of a name: functions and classes may lead with the abbreviation (
ODEComputeCoords()); variables must not — writenODEorodeCoordinates.
Further:
American English throughout (
Visualization, notVisualisation).Avoid consecutive capitalized words — avoid
ODEAE.No underscore in names. Exception: derivatives (§3).
Name exactly what the thing is:
numberOfItems, notsizeorl.Get/Set convention:
Getwhen data is retrieved from a class,Setwhen something is set. Useconst T& Get()/T& Get()where direct access is needed. Same convention for pybind11. Examples:Real* GetDataPointer(),Vector::SetAll(Real),GetTransposed(),SetRotationalParameters(...),SetColor(...).Use
Real, notdoubleorfloat— compatibility, and single/double precision under AVX.Use
Indexfor sizes and indices, notsize_torint.No magic numbers — not even
3for 3D or a rotation-parameter count. Define aconst Indexorconstexpr.
3. Suffix conventions
suffix |
meaning |
|---|---|
|
first / second time derivative — |
|
spatial derivatives |
|
derivative with respect to coordinates (scalar quantity) |
|
derivative with respect to velocity coordinates |
|
argument initializing a member — |
For components, elements and entries of vectors, arrays and matrices, use the word item
throughout.
4. The no-abbreviations rule
The code uses a minimum set of abbreviations. In general: do not abbreviate function, class or
variable names — GetDataPointer(), not GetPtr(). Short loop-local names (cnt, i, j, k,
x, v) are acceptable only where the meaning is unmistakable, in short (about 5-line) functions.
The permitted exceptions, in full: ODE, ODE2, ODE1, AE (as in AEcoordinates),
C[...] for Computational (CNode = ComputationalNode), mbs, min, max, abs, rel,
trig, quad, RHS, LHS, EP, Rxyz, coeffs, pos, T66.
Historical note: ODE2 corresponds to SOS2 / EvalF2 in HOTINT, ODE1 to ES / EvalF.
5. Abbreviations used in the documentation and code
abbrev |
meaning |
|---|---|
2D |
two dimensions or planar |
3D |
three dimensions or spatial |
abs |
absolute (e.g. absolute error), absolute value |
AE |
algebraic equations |
CMS |
component mode synthesis |
coeffs |
coefficients |
COM |
center of mass |
DOF |
degree of freedom |
EOM |
equations of motion |
EP |
Euler parameters |
FFRF |
floating frame of reference formulation |
HT |
homogeneous transformation |
LHS |
Left-Hand-Side (of equation) |
LTG |
local-to-global |
mbs |
multibody system |
min |
minimum |
max |
maximum |
ODE |
ordinary differential equation |
ODE1 |
first order ordinary differential equations |
ODE2 |
second order ordinary differential equations |
pos |
position |
quad |
quadrangle, polygon with 4 vertices |
rel |
relative (e.g. relative error) |
RHS |
Right-Hand-Side (of equation) |
Rot |
rotation |
Rxyz |
rotation parameterization: consecutive rotations around x, y and z axis (Tait-Bryan) |
STL |
STereoLithography |
T66 |
Pluecker transformation |
trig |
triangle |
6. Item and dimensionality conventions
An item is a node, object, marker, load or sensor — the five registered kinds.
Each item has up to three halves:
CObject(computational),MainObject(Python interface),VObject(visualization).All indices start at 0, in Python and in C++. Ranges are usually written
range(0,3), where 3 is the index after the last valid component.Quantities are 3D by default. Planar items carry the suffix
2D; 3D items carry no suffix. Rare exceptions exist where a Python name-space conflict would arise (e.g.Beam3D).Input and output of objects, markers and loads is usually 3D even when the node is 2D: a
NodePoint2Dhas 2DreferenceCoordinatesbut a 3D measured position, and a force applied throughMarkerNodePosition+LoadForceVectoris 3D. A local position in a 2D object is given as a 3D vector.
References in comments
A comment that explains why code is the way it is cites the issue (#2411), not a step of the
revision plan: comments stay for years, issue numbers are stable and the issue names its plan step.
Plan steps are cited in issues and commit messages as “revision2026 step R4.10.3” (see
exudynRevisionInfo2026.md §1).
Documentation says what IS, not what it was
This holds for every text a reader meets — a manual page, a docstring, the Details: header of a
model or an example, a figure caption — and it is the rule the maintainer gave on 2026-09-24
(#2646):
describe the thing, not its history. A reader of Building Exudyn from source wants to know how to build, not that it used to be described in three places. A reader of a test model wants to know what the model computes, not what the file was called before.
never open a section with history. If the first sentence is about a change, the reader has to read past it to reach the subject.
cite the issue, not the plan step, for the same reason as in a comment — and the issue number is a link a reader of the documentation can actually follow, which is why it stays.
a reason for a change, where it helps, goes last and stays short — one clause after the informative text, not before it.
What changed between releases is said once, for users, in Revisions. That is where a restructuring belongs, not in the pages it restructured.
7. File headers
C++ — Doxygen-style block at the top of the file:
/** ***********************************************************************************************
* @class Vector
* @brief A templated vector class ...
* @details Details: ...
*
* @author Gerstmayr Johannes
* @date 2018-04-30 (generated)
* @date 2024-01-15 (last modified)
* @pre ...
* @copyright This file is part of Exudyn. Exudyn is free software: ... see 'LICENSE.txt'
* @todo ...
*
* *** Example code ***
* @code{.cpp}
* ...
* @endcode
************************************************************************************************ */
Python — banner comment:
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# This is an EXUDYN python utility library
#
# Details: ...
#
# Author: Johannes Gerstmayr
# Date: 2020-03-10 (created)
# Copyright:This file is part of Exudyn. Exudyn is free software: ... see 'LICENSE.txt'
#
#+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
8. Deprecated and in transition
These rules are documented in introduction.tex but are not consistently followed in the tree.
Do not enforce them retroactively, and do not introduce them into files that lack them.
rule |
status |
|---|---|
Doxygen comment style ( |
deprecated, and the toolchain is gone. |
“Add a complete unit test to every function” (LEST) |
not obeyed. The LEST tests are gated on |
The |
replaced 2026-09-15 by Google-style docstrings: summary, then |
The operative rule when editing an existing file: follow that file’s local style. A consistent file beats a globally consistent repository during a migration.
9. Adding a new item (node, object, marker, load, sensor)
NEW_ITEM.md: the definition, what the generator writes, the C++ to write, what to run.
10. Reporting an error from C++
Not a transcription of
introduction.tex: this section is written here and nowhere else . The C++ side of it lives in the comments ofsrc/Utilities/ReleaseAssert.handsrc/Main/Stdoutput.h, which say the same thing to someone reading the code; this is the version for someone about to write a check.
An Exudyn error carries a type, not only a message. The type is what lets a user write
except exudyn.NotImplementedFeatureError instead of matching on message text, and it is what
tells them whether the mistake was theirs.
10.1 Which helper
helper |
what it does |
throws? |
|---|---|---|
|
a condition that must hold |
yes, if false |
|
an unconditional throw: a |
always |
|
a condition with no message; always |
yes, if false |
|
prints a User ERROR block with the Python file and line, then throws |
always |
|
prints a SYSTEM ERROR block, then throws; defaults to |
always |
|
prints a warning |
no |
|
a real Python |
no |
The three CHECKandTHROW* macros are compiled out in exudynCPPfast
(__FAST_EXUDYN_LINALG). PyError and SysError are not. So a validation the user must get in
every module — a parameter check at the Python boundary — belongs in PyError,
not in a macro. A macro is for a check that protects the computation and may cost a comparison
per element.
The helper does not decide the type. The same macro states a user’s index mistake in one
place and an Exudyn invariant in the next; that is measured, not assumed (tools/errorTriage.py).
Choose the type from what the check means.
10.2 Which type
The nine classes are declared in src/Utilities/ReleaseAssert.h. Each derives from
ExudynError and from the Python built-in that fits, so except exudyn.ExudynError catches
everything Exudyn raises while an existing except IndexError keeps working.
ask yourself |
class |
|
Python base |
|---|---|---|---|
the object cannot be that parameter at all |
|
|
|
the kind is right and the value is not (size, shape, range) |
|
|
|
an index outside its range |
|
|
|
the model as built is illegal, or is being used in the wrong order |
|
|
|
the solver cannot continue: singular, no convergence, divergence |
|
|
|
this feature or this combination does not exist (yet) |
|
|
|
division by zero, root of a negative number |
|
|
|
an Exudyn invariant broke — our bug, not theirs |
|
|
|
ExudynError itself is the root and is never raised directly.
Three of these earn their place by what they save a user:
ExudynNotImplementedErroranswers “is this my mistake, a bug, or simply not there?” — the question that otherwise costs an email.ExudynModelErroris for the model checked as a whole, typically atAssemble(). A message naming an invalid marker number is still a model error, not an index error: the number is the symptom, the model is what broke.ExudynInternalErrormeans please report this. Do not use it for anything a user can provoke from Python.
10.3 When to write no type at all
Leave a CHECKandTHROW* untyped when the honest answer is ExudynInternalError. The
untyped form is the default form and means ExudynInternalError at every macro site, so
writing it out by hand adds a diff and changes nothing. The same holds for SysError, which already defaults to it.
This does not hold for PyError, whose default is runtimeError. A PyError without a type
is a site not yet decided.
Also leave the type off where the code genuinely does not know it — a catch that restates
someone else’s exception. Putting a confident name on a lost type is worse than RuntimeError.
10.4 Two traps that do not announce themselves
Catch order. EXUexception is a macro for std::runtime_error, and every Exudyn class
derives from it. So in any try block, catch (const ExudynError&) and
catch (const py::builtin_exception&) must come before catch (const EXUexception&), or the
base catch takes it first and the type is silently flattened back to RuntimeError. This has
been found four times (#2432, #2516).
Registration. A class added to ReleaseAssert.h and not added to the registration block of
src/Pymodules/PybindModule.cpp compiles perfectly and arrives in Python as a plain
RuntimeError, because pybind11 translates it with its built-in std::runtime_error rule. The
opposite direction does not compile, so only this one needs saying. In that block the base must
be registered first: pybind11 tries translators in reverse order of registration.
python/testing/test_exceptions.py::test_everyCppClassIsRegistered compares the two lists.
10.5 Deprecating something
Use PyDeprecated(source, name, message), never PyWarning; from Python, exu.special.deprecations.Warn(source, name, message). source is simulationSettings, visualizationSettings, items or functions, name the deprecated
name. It raises a real Python DeprecationWarning, so -W error::DeprecationWarning finds every use before a release
removes the old name; each name warns once per session (exu.special.deprecations.warnOnce = False: on every use), and
every use is counted in exu.sys['deprecationUse'][source][name] (recordUse), which is what a test reads (#2804,
#2806). The warning is raised where the user’s script calls into Exudyn - mbs.AddObject(...), the setter of a setting -
so it names the line that wrote the name.
A setting (simulationSettings, visualizationSettings) is renamed or moved in its definition
file: the new member takes the place of the old, and the old name stays as a
StructureParameter(..., cFlags=SFDeprecated, deprecated=Deprecated(since, expires), defaultValue=NoDefaultValue, description=r'<new>'); <new> is the new name if the member stays in
its structure, else its path from the top structure (view0.scene.drawWorldBasis). The generator
writes the forwarding, the warning and the line of the reference manual (#2588). An item
parameter is renamed the same way, with ItemParameter(..., deprecated=Deprecated(...), description=r'<new name>') - see definitions/README.md, Members (#2589). An item parameter that stays but is
deprecated - there is no new name, as for rotationMarker0/1, whose rotation moves to the markers - keeps its
definition and gets deprecated=Deprecated(since, expires, advice='<what to do instead>'): the generated
SetWithDictionary and SetParameter warn when it is set other than its default, the page lists it below the table, and
exudev scripts reports it (#2804, #2805). The library itself uses no deprecated name.
A function of the Python library is decorated with @Deprecated(since, expires, use='...') from
exudyn.misc.deprecation, and its docstring starts with DEPRECATED; an argument is reported where the function sees
it, with DeprecatedArgument(name, since, expires, use='...'). A function of the C++ module gets
deprecated=Deprecated(since, expires) in its DefPyFunctionAccess of definitions/pybind*.py, beside the
PyDeprecated call in C++. expires is a year, five years after the deprecation unless decided otherwise.
tools/checkDeprecations.py (in exudev generate --all-checks) fails for a deprecation whose year has come - it is
removed then - and for one said in a text but not declared; the list of all of them is
docs/generated/deprecations.md (#2807).
10.6 Writing the message
Say what is wrong and what to change, not that something is wrong.
Put the information into the exception, which is the only thing the console shows:
PyErrorandSysErrorwrite their block to the log file and never to the console, because the exception already carries the same message and the same location, and because a caught exception must not flood the terminal of a GUI or a parameter variation that handles its own errors. Sostr(exception)has to be worth reading.The location in both is the outermost frame outside the
exudynpackage, so a solver-time error names the user’s own line and notsolver.py.
10.7 Which channel gets what
There are three channels and one rule per kind of message.
All of them write the same text, from ErrorMessageBlock — do not add a sentence to one of
them.
console |
|
solver file |
|
|---|---|---|---|
error — |
never |
always |
always |
warning — |
|
|
only where the call passes the file |
solver progress — |
|
|
|
An error never goes to the console, because the exception already carries the same message and the same location, and a caught exception would otherwise flood the terminal of a GUI or a parameter variation that handles its own errors (§10.6).
An error always reaches every open log file, and you do not write it. PyError and
SysError used to take an std::ofstream&; they no longer do. The solver file is written by
CSolverBase::SolveSystem, which catches what ends a run where the file is known — so a
CHECKandTHROW, which can pass no file at all and is 1100+ call sites, reaches the file exactly
like a PyError. Before R6.8 that depended on which helper a check happened to be written with.
PyWarning keeps its std::ofstream& overload: a warning throws nothing, so that catch can
never see it, and passing the file is the only way a warning reaches the solver log.
exudyn.config.suppressWarnings suppresses a warning on both channels, deliberately: a
warning the user switched off should not fill the log file either.
Name the function:
MainSystem::GetObject: access to invalid object number 7.Do not write
ERROR:orPython ERROR:— the helper adds the heading.
10.7 Checking what is where
python tools/errorTriage.py every call site, by area, helper and kind
python tools/errorTriage.py --sites UNPLACED the ones the rules cannot decide
python tools/errorTriage.py --csv FILE one row per site, for reading through
The tool classifies by text patterns, so it is repeatable rather than always right — its purpose is that a number in a plan can be re-derived.