Celeritas is a particle physics library for detector simulation. It's a C++17 codebase with CUDA/HIP device support and integrates with Geant4.
These three behaviors apply unconditionally, every session. Read them before starting any task.
Avoid mixing user changes with assistant changes. Before calling any file-editing tool for the first time in a session — including when transitioning from analysis to applying edits — check the repository for uncommitted changes and commit with git commit -a --no-verify -m "WIP: user changes" if so. Alert the user if this happens.
When the user corrects your behavior (tells you something you should have done, points out a missed step, or says you should have known better), your very next action must be:
- Identify the root cause: at what decision point did the existing instruction fail to trigger the right behavior?
- Edit AGENTS.md so the corrected behavior is a concrete, checkable step at that decision point — not vague prose buried elsewhere.
- Include the AGENTS.md change in the current or next commit.
Do not just acknowledge the correction and move on. If you skip updating AGENTS.md, you will repeat the same mistake in future sessions.
Commit immediately when all todos are done. Do not wait to be told. Do not defer across turns. Do not batch documentation changes.
Pre-commit checklist — execute in order:
- Tests: Find the corresponding
test/file (mirror thesrc/path, replace.hh/.ccwith.test.cc). If you added or changed any public API — including adding a method to an existing class — add or update tests there. This applies to all changes, not just new classes. - Format: run
pre-commit run, then re-git addany files it modified. - Compile: confirm the build still succeeds.
Inline -m strings break with multi-line messages in the shell. Instead,
write the commit message to <build>/commit_msg.txt (gitignored) and use
the helper script. Use create_file to write it (never exists after a
successful commit):
# Write message to file first, then commit (script handles add/format/rm)
scripts/dev/agent-commit.sh <build>/commit_msg.txt "<agentic-tool>" "<model-name>"The script runs git add -A, pre-commit run, git commit --trailer "Assisted-by: <agentic-tool> (<model-name>)", and rm <build>/commit_msg.txt. Pass
--no-verify as an extra argument only if pre-commit is already known to
pass.
The commit message format for build/commit_msg.txt:
<Imperative-mood subject, no tags>
<Body summarizes changes>
Prompt: <verbatim user prompt plain text, wrapped in quotes, no metadata or
attachments>
Common failure modes:
- Treating follow-up instructions within one feature as "incomplete" and deferring the commit indefinitely. Each self-contained feature or refactor warrants its own commit even if the user continues asking questions afterward.
- Skipping the test-file check because the change "only" added a method to an existing class rather than creating a new one. Always check.
corecel/: GPU abstractions, data structures, utilitiesgeocel/: Geometry interfaces (ORANGE, VecGeom, Geant4)orange/: Native Celeritas geometry engineceleritas/: Physics (EM processes, particles, materials)accel/: Geant4 integration layer
Most code relies on external user-installed packages (Geant4), so prefer to use a local environment's build directory. To build a minimal version from scratch:
cmake -B build -G Ninja && cd build && ninja && ctestObject files and tests may have different paths and test names than you expect (src/celeritas/ext/GeantImporter.cc → src/celeritas/CMakeFiles/celeritas_geant4.dir/ext/GeantImporter.cc.o and celeritas/ext/GeantImporter.test.cc → test/celeritas/ext_GeantImporter), and some test executables are run as distinct CTest tests due to environment variables and side effects (ctest --show-only | grep GeantImporter → Test #211: celeritas/ext/GeantImporter:DuneCryostat.*).
- Add Doxygen documentation to definitions, not declarations, when adding code. Prefer doxygen-style markup
\c,<code>to Markdown in such blocks. - Document equations and algorithmic descriptions, as applicable, in the class definition's docs, as those are often rendered in the user manual. All
operator()behavior goes in the class definition's docs. - Always add
\sa {file}.test.ccunderneath\file {file}.hhto locate tests that break thesrc/{path}.hh→test/{path}.test.ccrule - Use only ASCII characters in CMake/C++/CUDA/shell files
Celeritas sets up problems on CPU and executes on GPU or CPU with the same code. The CELER_FUNCTION macro is __host__ __device__ when CUDA/HIP is active and decorates runtime functions.
Celeritas separates immutable setup from mutable runtime data:
- Params: Shared problem data (physics tables, geometry) - build once
- States: Per-track mutable data (particle states, RNG) - one per track slot
- Ownership:
value(owns),reference(mutable),const_reference(immutable) - MemSpace:
host(CPU) ordevice(GPU)
Data flow: Build params on host → copy to device → access via Views
// Params: immutable setup data
struct MyParamsData { Collection<Material> materials; /* ... */ };
// View: lightweight accessor for device code
class MyView {
MyParamsData<const_reference, MemSpace::native> const& data_;
public:
CELER_FUNCTION Material const& get(MaterialId id) const;
};Data structs must have operator bool to check construction/assignment.
The stepping loop uses three layers:
- Action (StepActionInterface): Defines when to run, launches kernels
- Executor: Filters tracks, handles track-level logic
- Interactor: Pure physics functor (MaterialView → Interaction)
// In Model::step()
auto execute = make_action_track_executor(
params.ptr<MemSpace::native>(), state.ptr(), this->action_id(),
InteractionApplier{MyModelExecutor{this->host_ref()}});
launch_action(*this, params, state, execute);See src/celeritas/em/model/KleinNishinaModel.{cc,cu}
Use inserter classes to populate Collections with deduplication:
class XsGridInserter {
public:
GridId operator()(inp::XsGrid const& grid);
private:
DedupeCollectionBuilder<real_type> reals_;
CollectionBuilder<XsGridRecord> grids_;
};ItemRange<T>: Contiguous slice [begin, end)ItemMap<K, V>: Offset-based mapping (not hash map)
struct MyParamsData {
Collection<Material> materials;
Collection<Element> elements; // Backend storage
// Material stores ItemRange<Element> into elements collection
};State collections need resize(size) operators for track slots.
| Concept | Convention | Example |
|---|---|---|
| Classes/structs | CapWords |
PhysicsTrackView |
| Functions/variables | snake_case |
calc_energy |
| Private members | trailing underscore | data_ |
| Type-safe IDs | FooId |
MaterialId |
| Input classes | match what they construct | inp::Material → Material |
| Extension | Purpose |
|---|---|
.hh |
Headers, host+device compatible (use CELER_FUNCTION) |
.cc |
Host-only implementation — most code goes here, not .cu |
.cu |
CUDA kernel launches only (HIP-compatible via macros) |
.test.cc |
Unit tests, mirroring src/ under test/ |
| Macro | When to use |
|---|---|
CELER_EXPECT |
Preconditions at function entry |
CELER_ASSERT |
Internal invariants (debug only) |
CELER_ENSURE |
Postconditions at function exit |
CELER_VALIDATE |
User input validation (always active) |
- In public headers, function/block-scope
using namespace celeritas::literals;is allowed. Never introduce namespace-scopeusing namespacethere.
| Type | Purpose |
|---|---|
OpaqueId<T> |
Type-safe index — never use raw integers for indices |
Collection<T> |
GPU-compatible array with ownership semantics |
Span<T> |
Non-owning array view |
Array<T, N> |
Fixed-size stack array |
using FooId = OpaqueId<Foo>;
Collection<Foo, Ownership::value, MemSpace::host> foos; // Owns data
Collection<Foo, Ownership::const_reference, MemSpace::device> device_foos; // View- Separate data from behavior:
FooData,FooParams,FooView - Define any nontrivial member function out-of-line, decorating the function declaration with
inline - Write unit tests in
test/(namespaceceleritas::A::testforceleritas::A::Foo) - Ensure consistency across the stack:
- Input:
inp::Fooconstructs the data - Data: Members,
operator bool(),operator=,resize(for states) - View: Lightweight accessor with
CELER_FUNCTIONmethods - Executor/Interactor: Physics implementation
- Input:
| Don't | Do instead |
|---|---|
| Copy-paste code across call sites | Extract to helper function (anonymous namespace for file-local, utility header for reusable) |
| Leave repeated patterns unrefactored | Refactor before extending: extract the common pattern first |
| Write functions over ~100 lines | Break into focused helper functions with descriptive names |
| Use raw integers for public indices | Use OpaqueId<T> |
Omit CELER_FUNCTION from View member functions |
Always decorate functions that can be called on device |
| Edit files via terminal commands or Python scripts | Use agentic tools; the terminal is for building and testing only |