Coding conventions#
Formatting#
C++ code follows .clang-format in the repository root (clang-format >= 14):
2-space indentation, attached braces, Type * name, unindented namespaces,
120 columns, and includes are never reordered. .editorconfig carries the
basics (spaces, LF, final newline) to editors without clang-format support.
All HemoCell sources follow this style. Format what you change before committing:
git clang-format(staged changes) orgit clang-format <base commit>.scripts/format.sh [--check] [paths]formats (or checks) whole files.Pull requests are checked by CI on the changed lines only (this keeps the check independent of small differences between clang-format versions).
Commits that only reformat are listed in
.git-blame-ignore-revs; enable it locally withgit config blame.ignoreRevsFile .git-blame-ignore-revs(GitHub uses it automatically).Vendored and historical code (
external/,legacy/, the generatedpalabos/) keeps its own formatting. Changes to Palabos belong inpatch/palabos.patch.
Naming#
Follow the surrounding code; for new code:
Types:
CamelCase(HemoCellParticleField); functions and variables:camelCase(applyRepulsionForce,repulsionCutoff); constants and compile-time options:UPPER_CASE(FORCE_LIMIT,OUTPUT_FORCE).Everything in
namespace hemo, constants included (hemo::OUTPUT_FORCE,hemo::RBC_FROM_SPHERE); macros only for compile-time options.No
using namespacein headers (.h/.hh): qualifystd::andplb::names there. Source files of the library may useusing namespace std;andusing namespace plb;; case files declare what they import themselves (usuallyhemo,plbandstd).No process-wide state that outlives a simulation: anything static is reset in
resetGlobalState()(core/hemoCell.cpp), which~HemoCellcalls; per-simulation data belongs to an object (as inHemoCellStretch).Existing misspellings in the public API (e.g.
...TimeScaleSeperation) are kept for compatibility; add a correctly spelled alias rather than renaming.
Units#
Internally everything is in lattice units (dx, dt, dm from
hemo::Parameters, also available as param::). Interfaces mix SI values,
micrometres, iterations and lattice units, so every public function and XML
setting documents its unit, and conversion happens once, where a value enters
(e.g. setRepulsion() converts k from pN and the cutoff from
micrometres). Never retune a
material constant, force cap or resolution to hide a regression.
Performance rules#
HemoCell’s cost is dominated by per-particle and per-node loops. In those loops:
no heap allocation, no
std::maplookups, no XML reads, no logging;no validity checks that can be done once: validate inputs at setup (see
HemoCell::sanityCheck(), run before the first step) and keep the loops free of branches that never trigger;prefer indices into contiguous storage over pointers, and keep data that is used together close together;
hoist invariants (e.g.
param::values, block sizes) out of loops.
Measure with the built-in profiler (hemo::global.statistics) on the same
case, ranks and workload before and after a change, and check that the results
are unchanged (bit-identical where the change should not alter numerics).
Parallel correctness#
Collective operations must be called by all ranks in the same order; a function that gathers data or writes coordinated output is collective even if only rank 0 writes.
Point-to-point exchanges use a private communicator and receive from known source ranks (see Communication).
Runs are bit-for-bit reproducible for a given rank count and decomposition. Keep it that way: when the order of processing received data affects the order of particles or of floating-point sums, process it in a fixed order (e.g. by source rank), never in MPI arrival order. Reproducibility is what makes exact before/after comparisons possible; checkpoints of identical runs are byte-identical (
cmptheir.datfiles). Records written as raw bytes need their padding zeroed for that (seeHemoCellParticle).Output and statistics must not change the simulation state: read complete cells inside a
HemoCellFields::CompleteCellsScope(a copy of the particles, discarded afterwards) instead of callingsyncEnvelopes()ordeleteIncompleteCells(), and leave deleting cells toiterate().Cell IDs are
plintand grow with every periodic crossing; never store them in anint.A single-rank run does not test communication. Changes to particle transfer, ownership, periodicity or decomposition need runs on several ranks, including block and periodic crossings, and a restart from a checkpoint.
Errors and logging#
Use
hlog(stdout and log file, rank 0) andhlogfile(log file only) for messages,pcoutfor console-only output; never log per particle.Report errors with
hemo::fatal(...)(config/error.h), neverexit(): it prints the message with the MPI rank on the rank that calls it (hlogprints nothing on the other ranks) and ends the job withMPI_Abort.hemo::warning(...)reports from any rank. Both stream their arguments into the message. Under aFatalErrorsThrowguard,fatal()throwsFatalError, so tests can check error paths (only where every rank reaches the error).Configuration values are read with
read<T>(), which throwsstd::invalid_argumentfor missing, empty or invalid values. Optional settings catch it and use a default; required ones let it propagate.Do not promise validation that the code does not perform.
Documentation#
Public functions and non-trivial numerical helpers get Doxygen comments (
@brief,@param[in/out],@return) that state units, coordinate frames, array layouts, ownership and whether all ranks must call them.Behaviour changes update the user guide (
doc/user_guide) andCHANGELOG.mdin the same change.
Building and testing#
See AGENTS.md (build, presets, test commands) and
Adding tests. Before opening a pull request, run
the unit tests (ctest -L unit), the validation tests when touching physics,
mechanics or communication (ctest -L validation), and the unit tests with
the sanitize preset. scripts/ci/smoke_drivers.py runs every built
example and case for 20 steps on 2 ranks, with output and a checkpoint, in a
temporary copy of its directory (CI does so in the gcc release job; the large
drivers it lists in SLOW run only with --slow); run it after
changing an interface the drivers use.