C++ Bindings
thor-bindings exposes a subset of THOR's C++ routines to Python via nanobind: line profiles, scattering phase functions, emission line setup, and atomic data lookup.
Installation
Requires THOR's build environment (see Installation). From the repository root:
uv pip install -e python/thor-bindings
Modules
Modules are lazy-loaded:
from thor.bindings import core, profiles, physics, atomic
core — Fundamental Types
Vector types, bounding boxes, and physical constants.
from thor.bindings import core
# 3D vector (double precision)
v = core.Vec3fp(1.0, 2.0, 3.0)
print(v.x, v.y, v.z)
print(v.norm())
print(v.dot(core.Vec3fp(1, 0, 0)))
w = v.cross(core.Vec3fp(0, 0, 1))
# 2D vector
v2 = core.Vec2fp(1.0, 2.0)
# Bounding box
bb = core.BoundingBox(core.Vec3fp(0, 0, 0), core.Vec3fp(1, 1, 1))
print(bb.is_valid())
# Physical constants (CGS and SI submodules)
print(core.CGS.cval) # speed of light in cm/s
print(core.CGS.kB) # Boltzmann constant
print(core.SI.mp) # proton mass in kg
print(core.pi)
| Type |
Description |
Vec3fp |
3D vector with x, y, z, dot(), cross(), norm(), norm2(), normalize(), arithmetic operators, indexing |
Vec2fp |
2D vector with x, y, dot(), norm(), norm2(), normalize(), arithmetic operators |
BoundingBox |
Axis-aligned bounding box with min, max (Vec3fp), is_valid(), invalidate() |
CGS |
Submodule: Msun, cval, mu, mp, me, pc, kpc, Mpc, kB, e, h, llya, Elya |
SI |
Submodule: Msun, cval, mu, mp, me, pc, kB, e, eps0, h |
profiles — Line Profiles
Spectral line profile evaluation functions. All are plain functions taking (x, a) and returning a float.
from thor.bindings import profiles
# Voigt profile H(a, x) — default Smith+15 implementation
value = profiles.VoigtProfile(1.0, 4.7e-4)
# Other Voigt implementations
value = profiles.VoigtProfile_Tasitsiomi06(1.0, 4.7e-4)
value = profiles.VoigtProfile_Schreier18(1.0, 4.7e-4)
value = profiles.VoigtProfile_HumlicekW4(1.0, 4.7e-4)
# Gaussian and Lorentz profiles
gauss = profiles.GaussianProfile(1.0, 4.7e-4)
lorentz = profiles.LorentzProfile(1.0, 4.7e-4)
# Core-wing boundary frequency functions (take a only)
xcw = profiles.xcw_s15(4.7e-4) # Smith+15
xcw = profiles.xcw_b25estrin(4.7e-4) # Byrohl+25 (Estrin)
xcw = profiles.xcw_b25horner(4.7e-4) # Byrohl+25 (Horner)
| Function |
Arguments |
Description |
VoigtProfile |
(x, a) |
Default Voigt profile (Smith+15) |
VoigtProfile_Tasitsiomi06 |
(x, a) |
Tasitsiomi 2006 approximation |
VoigtProfile_Schreier18 |
(x, a) |
Schreier 2018 implementation |
VoigtProfile_Schreier18branched |
(x, a) |
Schreier 2018 with asymptotic branching |
VoigtProfile_HumlicekW4 |
(x, a) |
Humlicek W4 algorithm |
GaussianProfile |
(x, a) |
Pure Gaussian profile |
LorentzProfile |
(x, a) |
Pure Lorentz profile: \(1/(1+x^2)\) |
xcw_s15 |
(a) |
Smith+15 core-wing boundary frequency |
xcw_s15_fastlog |
(a) |
Smith+15 with fast log approximation |
xcw_b25estrin |
(a) |
Byrohl+25 core-wing boundary (Estrin) |
xcw_b25estrin_fastlog |
(a) |
Byrohl+25 with fast log (Estrin) |
xcw_b25horner |
(a) |
Byrohl+25 core-wing boundary (Horner) |
xcw_b25horner_fastlog |
(a) |
Byrohl+25 with fast log (Horner) |
physics — Physical Processes
from thor.bindings import physics
# Set up an emission line by name
el = physics.EmissionLine()
el.setup("Lya")
print(el.get_lambda0()) # rest wavelength in Angstrom
print(el.get_afactor()) # Voigt damping parameter
# Or set up from atomic parameters
el.setup(lambda0=1215.67, f12=0.4162, A21=6.265e8, Amass=1.008)
# Cross-section and frequency conversions
sigma0 = el.get_sigma0(temperature=1e4)
x = el.dfreq_to_x(dfreq=0.1, vthermal=12.8)
dlambda = el.dfreq_to_dlambda(0.1)
# Photon state
photon = physics.Photon()
print(photon.position) # Vec3fp
print(photon.direction) # Vec3fp
print(photon.weight)
EmissionLine
| Method |
Description |
setup(linename, verbose=False) |
Initialize from line name (e.g., "Lya", "CIV", "MgII") |
setup(lambda0, f12, A21, Amass, verbose=False) |
Initialize from atomic parameters |
get_lambda0() |
Rest wavelength in Angstrom |
get_nu0() |
Rest frequency |
get_afactor() |
Voigt damping parameter \(a\) |
get_sigma0(temperature) |
Line-center cross-section at given temperature |
get_sigma() |
Cross-section including Doppler effects |
get_local_thermal_velocity(temperature, vel_turb=0.0) |
Local thermal velocity including turbulence |
get_atom_velocity() |
Atom velocity |
scatter() |
Perform scattering event |
dfreq_to_x(dfreq, vthermal) |
Frequency offset to dimensionless \(x\) |
x_to_dfreq(x, vthermal) |
Dimensionless \(x\) to frequency offset |
dlambda_to_freq() |
Wavelength shift to frequency |
dlambda_to_dfreq() |
Wavelength shift to frequency offset |
dfreq_to_dlambda() |
Frequency offset to wavelength shift |
Photon
| Attribute |
Type |
Description |
position |
Vec3fp |
Current position |
direction |
Vec3fp |
Propagation direction |
origin |
Vec3fp |
Emission position |
dfreq |
float |
Frequency offset |
weight |
float |
Luminosity weight |
id |
int |
Photon ID |
state |
int |
State flag |
current_cell |
int |
Current cell index |
next_cell |
int |
Next cell index |
tau_seen |
float |
Optical depth accumulated |
tau_target |
float |
Target optical depth |
lsp_dist |
float |
Distance to last scattering point |
nscatterings |
int |
Number of scattering events |
Phase Functions (physics.phase)
mu = 0.5 # cosine of scattering angle
pdf = physics.phase.IsotropicPhase_pdf(mu)
pdf = physics.phase.RayleighPhase_pdf(mu)
pdf = physics.phase.GreensteinPhase_pdf(mu, g=0.73)
pdf = physics.phase.LyaCore2P32Phase_pdf(mu)
| Function |
Description |
IsotropicPhase_pdf(mu) |
Isotropic: returns 0.5 |
RayleighPhase_pdf(mu) |
Rayleigh (dipole): \(\propto 1 + \mu^2\) |
GreensteinPhase_pdf(mu, g=0.73) |
Henyey-Greenstein with asymmetry parameter \(g\) |
LyaCore2P32Phase_pdf(mu) |
Lya core \(2P_{3/2}\): \(1 + \frac{3}{7}\mu^2\) |
Lyman-alpha Helpers (physics.lya)
T = 1e4 # temperature in K
frac = physics.lya.frec_B(T) # Case B recombination fraction
alpha = physics.lya.alpha_B(T) # Case B recombination coefficient [cm^3/s]
eps = physics.lya.epsilon_recB(T, ne=1.0, nHII=1.0) # emissivity [erg/s/cm^3]
| Function |
Arguments |
Description |
frec_A(T) |
temperature [K] |
Case A recombination fraction |
frec_B(T) |
temperature [K] |
Case B recombination fraction |
alpha_A(T) |
temperature [K] |
Case A recombination coefficient [cm\(^3\)/s] |
alpha_B(T) |
temperature [K] |
Case B recombination coefficient [cm\(^3\)/s] |
Gamma_1s2p(T) |
temperature [K] |
Collisional excitation rate coefficient [cm\(^3\)/s] |
gamma_1s2p(T) |
temperature [K] |
Maxwell-averaged collisional excitation rate [cm\(^3\)/s] |
epsilon_recB(T, ne, nHII) |
T [K], electron/HII densities [cm\(^{-3}\)] |
Case B recombination emissivity [erg/s/cm\(^3\)] |
epsilon_exc(T, ne, nHI) |
T [K], electron/HI densities [cm\(^{-3}\)] |
Collisional excitation emissivity [erg/s/cm\(^3\)] |
atomic — Atomic Data
Atomic element database, solar abundances, and ion parsing.
from thor.bindings import atomic
# Look up an element by name or symbol
elem = atomic.findElement("Hydrogen")
print(elem.number) # 1
print(elem.symbol) # "H"
print(elem.mass) # atomic mass in AMU
print(elem.solar_abundance) # n/nH (Grevesse+ 2010)
# Look up by atomic number
fe = atomic.getElement(26)
print(fe.name) # "Iron"
# Solar mass fractions
print(atomic.SOLAR_X) # hydrogen
print(atomic.SOLAR_Y) # helium
print(atomic.SOLAR_Z) # metals
# Ion parsing
ion = atomic.parseIonName("FeXXV")
print(ion.element_symbol) # "Fe"
print(ion.ion_number) # 25
ElementInfo
Returned by findElement() and getElement():
| Attribute |
Description |
number |
Atomic number |
name |
Full element name |
symbol |
Chemical symbol |
mass |
Atomic mass [AMU] |
solar_abundance |
Solar abundance [n/nH] (Grevesse+ 2010) |
isotopes |
Mass numbers of isotopes |
ion_energies |
Ionization energies [eV] |
Functions
| Function |
Description |
findElement(name_or_symbol) |
Look up element by name or symbol. Returns None if not found. |
getElement(atomic_number) |
Look up element by Z (1-30). Returns None if not found. |
getSolarAbundance(element) |
Solar abundance [n/nH] by name or symbol |
getAtomicMass(element) |
Atomic mass [AMU] by name or symbol |
solarAbundanceToMassRatio(element) |
Convert solar abundance to mass ratio |
parseIonName(ion) |
Parse ion name (e.g., "FeXXV", "HI") to IonInfo |
getElementSymbolFromIon(ion) |
Element symbol from ion name (e.g., "FeXXV" -> "Fe") |
nameToSymbol(name) |
Element name to symbol (e.g., "Hydrogen" -> "H") |
symbolToName(symbol) |
Symbol to element name (e.g., "H" -> "Hydrogen") |
romanToInt(roman) |
Roman numeral to integer (e.g., "VI" -> 6) |
intToRoman(number) |
Integer to roman numeral (e.g., 6 -> "VI") |
Legacy Modules (Deprecated)
Legacy module names are still importable but emit deprecation warnings:
| Legacy Module |
Replacement |
thor_lineprofile |
profiles |
thor_emissionline |
physics |
thor_xwing |
profiles |
# Deprecated — triggers DeprecationWarning
from thor.bindings import thor_lineprofile
# Use instead:
from thor.bindings import profiles