# 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](../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`](https://github.com/jgerstmayr/EXUDYN/blob/master/docs/revision/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 {ref}`sec-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: ```cpp /** *********************************************************************************************** * @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: ```python #+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ # 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](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](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'')`; `` 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'')` - 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='')`: 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.