Particle Physics¶
Particle physicists work in natural units — a system where
ℏ = c = 1 and every quantity becomes an energy expressed in electron-volts.
That's a different basis from the SI you'd use to engineer the detector. A
particle-physics calculation usually has to cross between the two.
This walkthrough uses ucon's UnitSystem to keep a natural-units calculation
isolated from the surrounding SI pipeline. Every example threads the same
UnitSystem through three entry points: parsing, conversion, and dimensional
checking.
For the rationale, see UnitSystem Value Type. For the isolation pattern, see Building Isolated UnitSystems.
Setting Up: Two Systems¶
from ucon import UnitSystem, active_system, use, units
# SI system for detector engineering
si_system = active_system()
# Natural-units system for particle calculations
# (NATURAL basis is built in; the NATURAL_TO_SI transform is pre-wired,
# so cross-basis conversions work out of the box.)
natural_system = active_system() # same registries, ready for use(...)
Both UnitSystems share the same underlying registries — the difference is
which BaseUnits you treat as canonical when you ask "express this in base
form". BaseUnits for SI is units.si. For a pure particle-physics
calculation you'll often work in electron_volt and let the conversion graph
do the rest.
Calculation 1: Pion Rest Energy¶
The neutral pion has a rest mass of about 135 MeV/c². In natural units it's just 135 MeV. In SI it's a mass in kilograms.
from ucon import Scale, units, parse
from ucon.system import use
with use(natural_system):
# Particle physicist's mental model: m = 135 MeV
m_natural = parse("135 MeV")
print(m_natural) # <135 MeV>
with use(si_system):
# Detector engineer's view: same particle, kg
m_si = m_natural.to(Scale.kilo * units.gram, system=si_system)
print(m_si) # ~2.406e-28 kg
The system= kwarg on .to(...) lets you cross between snapshots without
nesting use(...) blocks. Threading explicitly is preferred when one call
needs a different world than its surroundings.
Calculation 2: Branching Ratio with Dimensional Safety¶
Branching ratios are dimensionless — the ratio of partial widths Γᵢ / Γ_total.
A common bug is forgetting to convert one width into the same units as the
other before dividing. enforce_dimensions catches it.
from ucon import enforce_dimensions, parse, Dimension, use
@enforce_dimensions(
partial=Dimension.energy,
total=Dimension.energy,
returns=None, # dimensionless ratio
)
def branching_ratio(partial, total):
return partial / total
with use(natural_system):
gamma_partial = parse("83.4 keV")
gamma_total = parse("167 keV")
# OK: both widths are energies
br = branching_ratio(partial=gamma_partial, total=gamma_total)
print(float(br.quantity)) # 0.499...
# WRONG: passing a cross-section here raises DimensionMismatch
sigma = parse("40 mbarn")
branching_ratio(partial=sigma, total=gamma_total)
# → ucon.core.DimensionMismatch
enforce_dimensions reads the active UnitSystem for dimension lookup. If
you'd registered a custom dimension on natural_system that si_system
doesn't have, the decorator would see only what the active snapshot provides.
Calculation 3: Cross-Section in barns ↔ cm²¶
Particle physicists report cross-sections in barns (1 barn = 10⁻²⁸ m²). Detector simulations expect cm² (CGS). Two non-SI scales, same dimension.
from ucon import units
from ucon.system import use
with use(natural_system):
sigma = parse("40 mbarn")
# In particle-physics mental units
print(sigma) # <40 mbarn>
with use(si_system):
# Convert to detector-friendly cm²
sigma_cm2 = sigma.to(units.centimeter ** 2, system=si_system)
print(sigma_cm2) # ~4.0e-26 cm²
This conversion stays within the SI basis (barn is registered as an area
unit). No basis_transform is needed — the dual graph picks barn → m² →
cm².
Calculation 4: Compton Wavelength via Natural-Units Cross-Basis¶
The Compton wavelength of the electron is λ_C = h / (m_e c). The natural-units identity λ_C = 1 / m_e (in units of ℏ = c = 1) collapses that to a single inverse-mass.
from ucon.system import use
from ucon import parse, units
with use(natural_system):
m_e = parse("0.5110 MeV")
# In natural units, 1/m_e *is* a length
# ucon's NATURAL basis encodes this: NATURAL has a single component
# (energy). Inverse energy maps to SI length via NATURAL_TO_SI.
lam_natural = parse("1 / 0.5110 MeV") # natural-units expression
# Cross to SI for the geometric interpretation
lam_si = lam_natural.to(units.meter, system=si_system)
print(lam_si) # ~3.86e-13 m (the reduced Compton wavelength)
The basis crossing happens at .to(...). natural_system and si_system
share the same basis_graph, which contains the NATURAL_TO_SI transform —
that's what lets the BFS find a path between an inverse energy in NATURAL and
a length in SI.
Calculation 5: Lifetime ↔ Width (Heisenberg)¶
Particle lifetimes and decay widths are related by τ = ℏ / Γ. In natural units it's just τ = 1 / Γ.
Again the basis crossing happens at the SI conversion — natural-units inverse
energy is identified with SI time via NATURAL_TO_SI.
Pinning a Run¶
For a full analysis run, snapshot once at startup and pin it through the
pipeline. Two captures of active_system() compare equal, so dict-keying or
hashing the system stays stable:
def run_analysis(events, *, system):
"""Pure: nothing about this function reads module-level state."""
with use(system):
for ev in events:
yield analyze(ev)
if __name__ == "__main__":
natural_system = active_system()
results = list(run_analysis(load_events(), system=natural_system))
Every dimension lookup, every parse, every conversion sees the same snapshot. If a co-resident library mutates the global registries mid-run, your pinned snapshot continues to use the registry references it captured at construction.
Key Takeaways¶
-
Two systems, one process.
active_system()is cheap and safe to call per pipeline. Particle calculations live in one system, engineering in another. -
Cross-basis is automatic. ucon ships
NATURAL_TO_SI. The conversion graph's BFS finds paths across the basis edge without per-call setup. -
system=for one-off crossings. Useuse(...)for blocks. Usesystem=when one call wants a different world than its surroundings. -
enforce_dimensionsreads the active system. Dimension equality is checked against the snapshot's registries — register a custom dimension on your snapshot and the decorator sees it without polluting globals. -
Systems are values. Two
active_system()calls back-to-back compare equal. Pin one at startup and pass it as a parameter, not a global.
Further Reading¶
- Building Isolated UnitSystems — the pattern this walkthrough applies
- UnitSystem Value Type — why the snapshot pattern works
- Natural Units — the NATURAL basis in depth
- Domain-Specific Bases — the other built-in bases