OpenPFC developer style guide¶
This document summarizes how we organize code, name things, and shape APIs (free functions, data-centric types) in OpenPFC so new contributors can align with existing practice. It complements CONTRIBUTING.md (legal and contribution flow), INSTALL.md (build and dependencies), and architecture.md (layers and dependency rules).
Language and tooling¶
C++20 is required (
CMAKE_CXX_STANDARDis 20; extensions off).Formatting: follow the repository
.clang-formatat the root. Use clang-format 20 locally (same major as CI’sahojukka5/clang-format-action@mainconfiguration). CI runs the same check in advisory mode (logs violations but does not fail the job). Local hook: rungit config core.hooksPath .githooksonce from the repo root so staged C++ files are checked viascripts/pre-commit-hookbefore each commit.Static analysis: optional
clang-tidyvia CMake optionUSE_CLANG_TIDY(seecmake/CompilerSettings.cmake).MPI: OpenPFC is built and tested with OpenMPI in typical workflows; use the same MPI stack for HeFFTe and OpenPFC (see INSTALL.md).
License and file headers¶
The project is AGPL-3.0-or-later. New source files should carry the standard SPDX lines used elsewhere, for example:
// SPDX-FileCopyrightText: YYYY VTT Technical Research Centre of Finland Ltd // SPDX-License-Identifier: AGPL-3.0-or-later
Use the same pattern in CMake files that already use SPDX comments (
# SPDX-FileCopyrightText: ...).Contributing implies the copyright transfer described in CONTRIBUTING.md; read that file before large changes.
Repository layout (where things go)¶
Area |
Role |
|---|---|
Public API — headers only, mirroring the kernel / runtime / frontend split (see below). |
|
Library implementation — |
|
Full applications (e.g. scenario-specific drivers, JSON inputs). Each app has its own |
|
Small, teaching executables — prefer clear names; many tutorials use numeric prefixes ( |
|
Unit tests (Catch2), grouped under |
|
Integration tests — multi-component or heavier scenarios. |
|
Benchmarks (optional). Sources are compiled only when |
|
Build logic included from the root |
|
Human-readable architecture, guides, reference pages, and integrated API documentation. |
HeFFTe (and similar large third-party trees) must not live inside the OpenPFC clone; install to a prefix (e.g. under $HOME/opt/heffte/...) as described in INSTALL.md.
Layers: kernel, runtime, frontend¶
The mental model is fixed: kernel → runtime → frontend in terms of allowed dependencies (frontend may use kernel + runtime; runtime uses kernel only; kernel must not include or depend on runtime or frontend). Details and diagrams are in architecture.md.
Practical rules:
Kernel (
include/openpfc/kernel/...): backend-agnostic simulation core (data, decomposition, execution abstractions on CPU/host, field ops, FFT interface, simulation, MPI helpers, profiling,kernel/utils/logging.hppfor structured logs). Do not add#ifdef OpenPFC_ENABLE_CUDA/ HIP switches here; GPU code belongs in runtime.Runtime (
include/openpfc/runtime/...): cpu, cuda, hip, and common (shared between backends). CUDA/HIP tags, device memory, deviceparallel_for, and backend FFT implementations live here.Frontend (
include/openpfc/frontend/...): optional application-facing pieces (UI, JSON/TOML helpers, extra I/O). Core logging is not frontend-only; usekernel/utils/logging.hpp. Minimal simulations can avoid the frontend layer entirely.
When adding a feature, choose the lowest layer that can express it without breaking the dependency graph.
Naming conventions¶
Files and directories¶
Headers:
snake_case.hpp, implementation:snake_case.cpp.Directory names:
snake_case, describing content (e.g.decomposition,initial_conditions). Avoid vague catch-alls likecoreorall.commonis reserved for code shared by sibling components (e.g.runtime/commonfor HeFFTe adapter code used by multiple backends).Unit tests:
test_<topic>.cppin the subdirectory that matches the component under test.
C++ identifiers¶
Namespaces: top-level
pfc, with nested namespaces for areas (pfc::world,pfc::decomposition, …) matching headers and responsibility. Prefer narrow namespaces over dumping everything intopfc.Classes / structs / enums:
PascalCase(e.g.HaloExchanger,Decomposition).Functions and variables:
snake_casefor most APIs; follow the style of the file you are editing.Non-static data members:
m_prefix plussnake_case(e.g.m_tic,m_lap_started,m_min_level). Prefer this for new code and when editing a type for other reasons. Avoid trailing underscores on members (tic_,duration_) in new or heavily touched types—some older headers still use suffix style; migrate opportunistically rather than mass-renaming unrelated files.Macros / compile-time flags:
OPENPFC_*or existing macro families (e.g. profiling macros inkernel/profiling); avoid introducing generic unprefixed macros.
CMake¶
Targets: the main library is
openpfc(aliasOpenPFC). Tests aggregate asopenpfc-tests. Match existing executable names inexamples/andapps/.Options:
OpenPFC_*prefix for project-specific cache variables (seecmake/ProjectSetup.cmakeand related modules).
API shape: free functions and data-centric types¶
OpenPFC favors a laboratory, not fortress style: code should be easy to read, experiment with, and compose. Prefer exposing behavior as free functions in the appropriate namespace over member functions when the operation is a natural query or transformation on a value, rather than something that must stay tied to hidden invariants.
Examples already in the tree:
pfc::domain::get_size(domain, …)takesDomainas an argument;get_worldis a free function forDecompositionandField(seeget_world(const Decomposition&)andget_world(const Field<T>&)in their headers). Preferget_world(decomp)/get_world(field)over a member spelling when both exist.Model and Simulator: prefer
pfc::get_world,pfc::get_fft,pfc::is_rank0,pfc::has_field/has_real_field/has_complex_field,pfc::get_real_field/get_complex_field(useget_real_field(model, "default")when you rely on the legacy default name),pfc::add_real_field/add_complex_field/add_field,pfc::get_allocated_memory_bytes,pfc::initialize(model, dt),pfc::step(model, t), pluspfc::get_model(sim),pfc::get_time(sim),pfc::is_rank0(sim),pfc::get_world(sim),pfc::get_fft(sim), andsim.results_writers()for the registered writer map (seesimulation/model.hpp,simulation/simulator.hpp,simulation/results_writer.hpp). DeprecatedModel::get_field()remains only for backward compatibility.pfc::is_rank0(model)reflects the model’s rank-0 flag (fromMPI_COMM_WORLDat construction);pfc::is_rank0(sim)reflects rank 0 in the simulator’smpi_comm()(the same communicator passed to field modifiers).Name lookup inside
Modelsubclasses: unqualifiedget_fft(*this)can conflict with the inherited memberModel::get_fft(); preferpfc::get_fft(*this)andpfc::get_world(*this)in derived-class bodies.Older APIs: some types still expose members such as
model.get_world(). When you add or refactor nearby access, introduce a free overload in the same module (e.g.get_world(const Model&)) and call that in new or touched code. Full migration can be incremental; the direction of travel is free functions at namespace scope.Classes as data carriers: types should primarily hold state. Prefer
publicdata members when there is no concrete reason to hide them—in practice many types should read likestructs. Reserveprivatemembers (with them_convention) for cases where hiding genuinely prevents invalid states or where encapsulation is clearly justified, not as a default habit.Virtual bases are extension seams. Subclass
Model,FieldModifier, orResultsWriterwhen the framework must dispatch across app-defined types at runtime. Implement the bulk of physics, I/O, and wiring as free functions (and small POD-ish state) that those overrides call into—avoid growing wide hierarchies for “organization only.”
This sits alongside the layer rules in architecture.md: kernel/runtime/frontend boundaries still apply; openness is about how each type exposes its own fields and helpers, not about crossing forbidden includes.
Includes and public API¶
Prefer explicit includes with the full path under
openpfc/:#include <openpfc/kernel/data/world.hpp>Umbrella headers
openpfc/openpfc.hppandopenpfc/openpfc_minimal.hppare convenient but pull more than needed; prefer specific headers in library and example code for compile times.Anything under
include/openpfc/is treated as public API. Subdirectories nameddetail(or futureinternal) are not stability promises—do not rely on them from external projects.
See architecture.md for minimal-app include patterns and HeFFTe/runtime headers.
Adding or moving library code¶
Header in
include/openpfc/<layer>/.../name.hpp(or split headers if the module is large, as withworld_*).Source in
src/openpfc/<layer>/.../name.cppif not header-only.Register new
.cppfiles on theopenpfctarget incmake/LibraryConfiguration.cmake(generator expressions for CUDA/HIP files follow existing examples).Tests: add
test_*.cppunder the matchingtests/unit/...tree and list them in the nearestCMakeLists.txtviatarget_sources(openpfc-tests PRIVATE ...).Examples (optional): add
.cppunderexamples/and wire the executable inexamples/CMakeLists.txt.
After structural changes, update or add Doxygen on public types and functions where the rest of the module is documented (@file, @brief, @param, etc.).
Documentation and cross-references¶
Design / physics / algorithms: add or extend Markdown under
docs/and link from related headers (as withdocs/halo_exchange.md).User-facing build and HPC notes: INSTALL.md,
docs/build_cpu_gpu.md, site-specific guides (e.g.docs/INSTALL.LUMI.md).API reference: Doxygen extracts public headers as XML and Breathe renders the curated pages under
docs/api/.
Tests and quality gate¶
Framework: Catch2 v3 (fetched by CMake for tests).
Run the test target after changes, e.g.
cmake --build <build-dir> --target openpfc-testsand execute the test binary (see tests/README.md for conventions).Prefer deterministic unit tests; use MPI tests only where the behavior under rank layout is what you are validating.
Summary checklist for a typical change¶
Correct layer (kernel vs runtime vs frontend) and no upward dependencies.
snake_casefiles,PascalCasetypes,m_data members where members are private,pfc::namespaces consistent with neighbors; prefer free functions and public-by-default data where the API shape section applies.SPDX header on new files.
LibraryConfiguration.cmakeupdated for new.cppfiles.Unit tests where behavior is non-trivial or regression-prone.
clang-formatapplied; builds cleanly at least in the configuration you use (Debug recommended during development).
For questions not covered here, use architecture.md and nearby code in the same subdirectory as the canonical reference.
MPI cleanup-failure policy¶
Cleanup paths that run in noexcept contexts — destructors of RAII guards
(pfc::mpi::MPI_File_guard, pfc::MPI_Type_guard), their move-assignment
operators, and pfc::mpi::environment::~environment — must not throw.
Throwing from a destructor risks std::terminate during stack unwinding, and a
nonzero MPI error code during cleanup indicates corrupted MPI state that cannot
be recovered from locally.
The single policy is: call pfc::mpi::abort_on_mpi_error(err, what), which logs
to stderr and MPI_Aborts the world communicator on a nonzero code (no-op on
MPI_SUCCESS). Use pfc::mpi::throw_on_mpi_error only on normal (non-cleanup)
code paths where an exception can propagate safely.