Skip to content

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 from (UnitMass/h) / (UnitLength/h)³. At AGORA h = 0.702: ×0.493.
  • Velocity × √a. Gadget stores v_peculiar / √a. At AGORA z = 4: ×0.447.
  • Lyα luminosity × 1/h³. Volume was missing h and (1+z)³ factors; downstream luminosity recipes also carried a stale × UnitLength³. At AGORA h = 0.702: ×2.89 (~3× brighter).
  • Tipsy / ENZO / ART velocity. Tipsy now applies the UnitVelocity factor it had been skipping. ENZO drops a spurious × (1 + z_init). ART matches yt's formula (also affects ART temperature, which is derived from ).

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-loader build_unit_context
  • Tests: tests/tests_unit_normalization.cpp, tests/tests_gadget_reader.cpp
  • Validation: validations/agora-sph/projection-sanity/ in thor_setups
  • Upstream: Springel & Hernquist, "Gadget-2: User's Guide"; yt yt.frontends.gadget