Unit Normalization
THOR reads snapshots from many codes (Gadget, Tipsy, RAMSES, ENZO, ART). This page covers how raw file values become physical CGS that the raytracer can share, and flags numerical differences if you ran Gadget data before the unit-normalization refactor.
Canonical units
After normalization, every canonical field is in physical CGS:
| Field | Unit |
|---|---|
Density |
g/cm³ |
InternalEnergy |
erg/g |
Velocities |
cm/s |
Derived recipes (Temperature, HIdensity, cloudyemission_*, Lya_recB, ...)
emit results in the unit declared in their description string.
Design
Unit conversion lives in a per-recipes sidecar of
UnitConversion {scale, unit, provenance} entries, not as inline arithmetic in
readers and recipes. Each loader implements build_unit_context();
thor::units::normalize_fields() walks the canonical fields and installs
scale factors. getField applies the conversion lazily on first access and
caches the canonical result.
Implementations live in src/UnitNormalization.{h,cpp},
src/FieldRecipes.{h,cpp}, and one build_unit_context per loader
(GadgetReader, TipsyReader, RamsesLoader, EnzoLoader, ArtLoader).
Numerical changes vs. pre-refactor
If you ran THOR on Gadget cosmological data before the refactor, your output is numerically different now. The differences are corrections that bring THOR into bit-exact agreement with yt's Gadget frontend, not regressions:
- Density × h². Gadget code density carries
h²from(UnitMass/h) / (UnitLength/h)³. At AGORAh = 0.702: ×0.493. - Velocity × √a. Gadget stores
v_peculiar / √a. At AGORAz = 4: ×0.447. - Lyα luminosity × 1/h³.
Volumewas missinghand(1+z)³factors; downstream luminosity recipes also carried a stale× UnitLength³. At AGORAh = 0.702: ×2.89 (~3× brighter). - Tipsy / ENZO / ART velocity. Tipsy now applies the
UnitVelocityfactor it had been skipping. ENZO drops a spurious× (1 + z_init). ART matches yt's formula (also affects ART temperature, which is derived fromv²).
There is no backwards-compat flag — to reproduce pre-refactor numbers, check out a commit before the merge.
YAML overrides
Most Gadget snapshots use the standard convention: UnitLength_in_cm means
"cm per code_length / h comoving" with h not pre-baked. If your config has
h already folded into the unit values, disable the automatic correction:
pointcloud_voronoi:
gadget:
UnitLength_in_cm: 4.395e21 # h already folded in
UnitMass_in_g: 2.83e43
length_has_h_factor: false
mass_has_h_factor: false
At init, THOR logs the effective physical box size in cm/kpc/Mpc. If that
matches your mental model, the defaults are right; if it is ×h off, flip the
flags.
Cross-validation
Two independent checks: per-particle bit-exact agreement with yt 4.4.2 on the
AGORA GIZMO z = 4 snapshot, and the validations/agora-sph/projection-sanity/
setup in thor_setups gating
physical-CGS ranges across all 8 AGORA codes (currently 40 / 40 PASS, velocity
RMS spread 0.30 dex).
References
- Code:
src/UnitNormalization.{h,cpp},src/FieldRecipes.{h,cpp}, per-loaderbuild_unit_context - Tests:
tests/tests_unit_normalization.cpp,tests/tests_gadget_reader.cpp - Validation:
validations/agora-sph/projection-sanity/inthor_setups - Upstream: Springel & Hernquist, "Gadget-2: User's Guide";
yt
yt.frontends.gadget