Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,44 @@ repos:
- id: check-merge-conflict
- id: mixed-line-ending

# Unit-safety fences (see docs/units.md). pygrep hooks fail on any match.
- repo: local
hooks:
- id: forbid-hardcoded-c
name: forbid hardcoded speed of light
language: pygrep
# Digit-anchored so it does not fire inside a longer number (e.g. a
# seed like 12997924580 that merely contains the c digits). Covers the
# integer (optional ' separators), decimal, and 2.99792458e8 forms.
entry: '(?<![\d.])299''?792''?458(?!\d)|(?<!\d)299\.792458(?!\d)|(?<![\d.])2\.99792458[eE][+]?0*8(?!\d)'
types_or: [c++, c]
Comment thread
coderabbitai[bot] marked this conversation as resolved.

- id: forbid-g4-system-of-units
name: forbid G4SystemOfUnits.hh (use units/clhep_bridge.hpp)
language: pygrep
# Match the include directive only, so the ban is repo-wide without
# tripping on prose (e.g. the bridge header's own comment about it).
entry: '#\s*include\s*[<"][^>"]*G4SystemOfUnits\.hh'
types_or: [c++, c]

- id: forbid-clhep-units-outside-bridge
name: forbid CLHEP units outside the bridge
language: pygrep
# Include path or CLHEP:: symbols; the bridge is the one file allowed both.
entry: 'CLHEP/Units/|\bCLHEP::'
types_or: [c++, c]
exclude: '^src/units/clhep_bridge\.hpp$'

- id: forbid-magic-unit-factors
name: forbid hand-typed power-of-ten unit conversions
language: pygrep
# Any 1e{3,6,9}-style factor (signed exponent, decimal mantissa and
# f/l suffixes included) next to a conversion comment (-> or "to", in
# a // or /* */ comment). The marker keeps it from firing on unrelated
# powers of ten.
entry: '(?<![\w.])[0-9]*\.?[0-9]+[eE][-+]?[369][fFlL]?\b.*(?://|/\*).*(->|\bto\b)'
types_or: [c++, c]
Comment thread
coderabbitai[bot] marked this conversation as resolved.

# Tools below come from the pixi `lint` environment via language: system.
- repo: local
hooks:
Expand Down
60 changes: 60 additions & 0 deletions docs/units.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<!--
SPDX-FileCopyrightText: 2026 CERN for the benefit of the SHiP Collaboration

SPDX-License-Identifier: LGPL-3.0-or-later
-->

# Unit handling

FairShip was plagued by unit-mismatch bugs; aegir prevents them
structurally. The rules:

## Canonical units and types

The framework-wide vocabulary lives in `SHiP/Units.hpp` (shipdatamodel,
`SHiP::SHiPUnits` target), built on [mp-units](https://mpusz.github.io/mp-units/):

| Quantity | Type | Storage unit |
|-----------------|-----------------------|--------------|
| Length/position | `ship::Length` | mm |
| Time | `ship::Time` | ns |
| Energy | `ship::Energy` | GeV |
| Momentum | `ship::Momentum` | GeV/c |
| Mass | `ship::Mass` | GeV/c² |
| Angle | `ship::Angle` | rad |
| Magnetic field | `ship::MagneticField` | T |

`GeV/c` and `mm/c` are real derived units built from the exact SI
definition of c, so `energy + momentum` is a compile error and
conversions such as Pythia's `tProd()` (mm/c → ns) are derived rather
than hand-typed. Persisted data-model structs stay plain doubles in the
storage units (ROOT cannot stream quantity types); `SHiP/QuantityView.hpp`
provides typed views over them.

## The one rule at boundaries

**A raw double acquires its unit on the line it enters, and only leaves
a quantity via an explicit conversion.**

- Geant4/CLHEP: only through `src/units/clhep_bridge.hpp` — the single
file allowed to reference CLHEP unit constants. `G4SystemOfUnits.hh`
is banned repo-wide (it injects global `mm`, `GeV`, `tesla`, ... that
collide with the mp-units vocabulary).
- Pythia8 (GeV, mm, mm/c), GENIE (SI): values are wrapped at the read
site (`p.tProd() * su::mm_per_c`, `vtx[0] * su::m`).
- Configuration: workflow files stay plain numbers; C++ reads them with
`aegir::get_quantity(config, "beam_energy", 400.0 * su::GeV)`, making
the parameter definition the single source of truth for the unit.

Never `using namespace ship::units` in a translation unit that includes
CLHEP or Geant4 headers — always qualify.

## Enforcement

prek hooks (see `.pre-commit-config.yaml`) fail on: hardcoded c
(299792458, 299.792458 or 2.99792458e8, digit separators allowed), any
repo-wide `G4SystemOfUnits.hh` include, `CLHEP/Units/` includes or
`CLHEP::` symbols outside `src/units/clhep_bridge.hpp`, and
`1e±3`/`1e±9`-style factors next to unit conversion comments. Review rule: no raw double crosses a function
boundary with an implicit unit — either the parameter is a quantity
type, or the conversion happens on the line the value is read.
36 changes: 18 additions & 18 deletions workflows/lib.libsonnet
Original file line number Diff line number Diff line change
Expand Up @@ -40,38 +40,38 @@
gun:: {
cpp: 'particle_gun_source',
pdg: 13,
p_min: 10.0,
p_max: 100.0,
max_theta: 0.1,
vertex_z: -500.0,
p_min: 10.0, // GeV/c
p_max: 100.0, // GeV/c
max_theta: 0.1, // rad
vertex_z: -500.0, // mm
},

// Pencil beam for the field smokes: straight 20 GeV μ− from z = −2 m.
pencil_gun:: {
cpp: 'particle_gun_source',
pdg: 13,
p_min: 20.0,
p_max: 20.0,
max_theta: 0.0,
vertex_z: -2000.0,
p_min: 20.0, // GeV/c
p_max: 20.0, // GeV/c
max_theta: 0.0, // rad
vertex_z: -2000.0, // mm
},

pythia8:: {
cpp: 'pythia8_source',
beam_energy: 400.0,
beam_energy: 400.0, // GeV
process: 'SoftQCD:inelastic',
},

fixed_target:: {
cpp: 'fixed_target_source',
beam_energy: 400.0,
target_z: 74,
beam_energy: 400.0, // GeV
target_z: 74, // proton number, not a coordinate
target_a: 184,
// Target z extents to be determined from GeoModel geometry
target_z_start: 0.0,
target_z_end: 1164.0,
interaction_length: 191.9,
tau0_threshold: 1.0,
target_z_start: 0.0, // mm
target_z_end: 1164.0, // mm
interaction_length: 191.9, // mm
tau0_threshold: 1.0, // mm/c
},

// Neutrino interactions pre-generated with GENIE, read from a rootracker
Expand All @@ -95,10 +95,10 @@
cpp: 'geant4_module',
physics_list: 'FTFP_BERT',
sd_mode: 'crossing',
ke_threshold: 0.5,
ke_threshold: 0.5, // GeV
energy_cut: true,
particle_ke_cut: 1.0,
regions: { '/SHiP/target': 50, '/SHiP/muon_shield/magn_absorb': 50 },
particle_ke_cut: 1.0, // GeV
regions: { '/SHiP/target': 50, '/SHiP/muon_shield/magn_absorb': 50 }, // production cuts [mm]
},

noop_output:: { cpp: 'sim_output_module', mode: 'noop' },
Expand Down
Loading