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

lowerCamelCase

secondOrderSize, massMatrix, mThetaTheta

functions, classes, structs

UpperCamelCase

SecondOrderSize, EvaluateMassMatrix

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 — write nODE or odeCoordinates.

Further:

  • American English throughout (Visualization, not Visualisation).

  • Avoid consecutive capitalized words — avoid ODEAE.

  • No underscore in names. Exception: derivatives (§3).

  • Name exactly what the thing is: numberOfItems, not size or l.

  • Get/Set convention: Get when data is retrieved from a class, Set when something is set. Use const T& Get() / T& Get() where direct access is needed. Same convention for pybind11. Examples: Real* GetDataPointer(), Vector::SetAll(Real), GetTransposed(), SetRotationalParameters(...), SetColor(...).

  • Use Real, not double or float — compatibility, and single/double precision under AVX.

  • Use Index for sizes and indices, not size_t or int.

  • No magic numbers — not even 3 for 3D or a rotation-parameter count. Define a const Index or constexpr.

3. Suffix conventions

suffix

meaning

_t, _tt

first / second time derivative — Position_t, Position_tt

_x, _xx, _y, …

spatial derivatives

_q

derivative with respect to coordinates (scalar quantity)

_v

derivative with respect to velocity coordinates

[...]Init

argument initializing a member — valueInit initializes value

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 NodePoint2D has 2D referenceCoordinates but a 3D measured position, and a force applied through MarkerNodePosition + LoadForceVector is 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 (//!, @todo, @test, @bug)

deprecated, and the toolchain is gone. docs/doxygen/ was removed 2026-09-10 — it broke on project size, the PDF path never worked, and graph generation had already been switched off. So nothing consumes these comments any more. New code need not adopt the style; existing blocks stay until a dedicated cleanup step. For the architectural overview doxygen was once expected to give, see ARCHITECTURE.md.

“Add a complete unit test to every function” (LEST)

not obeyed. The LEST tests are gated on PERFORM_UNIT_TESTS, enabled only for Python 3.7, so they run nowhere.

The #** doc-comment convention in python/exudyn/

replaced 2026-09-15 by Google-style docstrings: summary, then Args:, Returns:, Note:, Example:, each section’s text indented by four spaces. author, date, status and public=False go into @docmeta(...) (exudyn/docmeta.py), a MainSystem method is marked @extends(exudyn.MainSystem). Every module in python/exudyn has an __all__ listing its public names (top-level functions, classes and assigned names not starting with _, except @docmeta(public=False)); python tools/checkAll.py --write updates it after adding a function, and CI fails if it is incomplete. The text is Markdown: code in backquotes, $...$ math, [Key] for a citation, [text](#label) for a reference ([Section](#sec:...), an abbreviation as [HT](#HT)), **bold**, *italics*, UTF-8 characters instead of LaTeX accents; no LaTeX macros outside math.

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 of src/Utilities/ReleaseAssert.h and src/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?

CHECKandTHROW(condition, message[, class])

a condition that must hold

yes, if false

CHECKandTHROWstring(message[, class])

an unconditional throw: a default: branch, an unreachable case

always

CHECKandTHROWcond(condition)

a condition with no message; always ExudynInternalError

yes, if false

PyError(message[, PyErrorType])

prints a User ERROR block with the Python file and line, then throws

always

SysError(message[, PyErrorType])

prints a SYSTEM ERROR block, then throws; defaults to internalError

always

PyWarning(message)

prints a warning

no

PyDeprecated(message)

a real Python DeprecationWarning, shown once per call site

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

PyErrorType

Python base

the object cannot be that parameter at all

ExudynTypeError

typeError

TypeError

the kind is right and the value is not (size, shape, range)

ExudynValueError

valueError

ValueError

an index outside its range

ExudynIndexError

indexError

IndexError

the model as built is illegal, or is being used in the wrong order

ExudynModelError

modelError

ValueError

the solver cannot continue: singular, no convergence, divergence

ExudynSolverError

solverError

RuntimeError

this feature or this combination does not exist (yet)

ExudynNotImplementedError

notImplementedError

NotImplementedError

division by zero, root of a negative number

ExudynArithmeticError

arithmeticError

ArithmeticError

an Exudyn invariant broke — our bug, not theirs

ExudynInternalError

internalError

RuntimeError

ExudynError itself is the root and is never raised directly.

Three of these earn their place by what they save a user:

  • ExudynNotImplementedError answers “is this my mistake, a bug, or simply not there?” — the question that otherwise costs an email.

  • ExudynModelError is for the model checked as a whole, typically at Assemble(). 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.

  • ExudynInternalError means 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: PyError and SysError write 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. So str(exception) has to be worth reading.

  • The location in both is the outermost frame outside the exudyn package, so a solver-time error names the user’s own line and not solver.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

pout log file (exu.SetWriteToFile)

solver file

error — PyError, SysError, and any macro throw

never

always

always

warning — PyWarning

writeToConsole

writeToFile

only where the call passes the file

solver progress — VerboseWrite

verboseMode

writeToFile

verboseModeFile

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: or Python 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.