Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

`progenax.binaries`

San Diego State University

progenax.binaries

Auto-generated by scripts/build_api_reference.py from progenax source. Re-run after public-API changes; the script is idempotent.

Module path: progenax/binaries/

Public symbols: 30

Contents

binaries.KeplerElements

class

📇 model card · ∇ gradient-verified — 5 audit cases

KeplerElements(a: ArrayLike, e: ArrayLike = 0.0, i: ArrayLike = 0.0, Omega: ArrayLike = 0.0, omega: ArrayLike = 0.0, M0: ArrayLike = 0.0)

Keplerian orbital elements as Equinox module.

Represents an orbit in the two-body problem using classical elements. All angles in radians, distances in current unit system.

Attributes

ParameterDescription
aSemi-major axis [length units]
eEccentricity (0 ≤ e < 1 for bound orbits)
iInclination [rad] (0 to π)
OmegaLongitude of ascending node [rad] (0 to 2π)
omegaArgument of periapsis [rad] (0 to 2π)
M0Mean anomaly at epoch [rad] (0 to 2π)

References. Murray & Dermott (1999) “Solar System Dynamics” Eq 2.122 Binney & Tremaine (2008) “Galactic Dynamics” Ch 3

Examples. >>> # Circular orbit

elements = KeplerElements(a=1.0, e=0.0, i=0.0, Omega=0.0, omega=0.0, M0=0.0) state = elements.to_state(M_total=1.0, G=1.0)

Eccentric orbit

import jax.numpy as jnp elements = KeplerElements( ... a=1.0, e=0.5, i=jnp.pi/4, ... Omega=0.0, omega=0.0, M0=0.0 ... ) state = elements.to_state(M_total=1.0, G=1.0)

Source: src/progenax/binaries/kepler.py#L49

binaries.CartesianState

class

CartesianState(position: Float[Array, '3'], velocity: Float[Array, '3'])

Cartesian phase-space state of a single body.

A JAX-pytree NamedTuple (transparent to jit/grad/vmap); also tuple-iterable, so pos, vel = state works.

Attributes

ParameterDescription
position(3,) Cartesian position [length units]
velocity(3,) Cartesian velocity [velocity units]

Source: src/progenax/binaries/kepler.py#L18

binaries.BinaryState

class

BinaryState(r1: Float[Array, '3'], v1: Float[Array, '3'], r2: Float[Array, '3'], v2: Float[Array, '3'])

Resolved barycentric phase-space state of a binary’s two components.

A JAX-pytree NamedTuple; tuple-iterable, so r1, v1, r2, v2 = state works.

Attributes: r1, v1: Position/velocity of the primary [length, velocity units] r2, v2: Position/velocity of the secondary [length, velocity units]

Source: src/progenax/binaries/kepler.py#L33

binaries.compute_period

function

📇 model card

compute_period(a: ArrayLike, M_total: ArrayLike, G: ArrayLike) -> Float[Array, '...']

Compute orbital period from semi-major axis using Kepler’s 3rd law.

Args

ParameterDescription
aSemi-major axis [length units]
M_totalTotal mass of binary system [M☉]
GGravitational constant (REQUIRED, no default)

Returns: period: Orbital period [time units] Formula: T = 2π√(a³/(GM))

Examples. >>> # Earth orbit: a=1 AU, M=1 M☉ → T≈1 year

G = 39.478 # AU³/Msun/yr² T = compute_period(a=1.0, M_total=1.0, G=G) print(f"Period: {T:.2f} years") Period: 1.00 years

Stellar cluster orbit: a=1 pc, M=1000 M☉

G = 0.00450 # pc³/Msun/Myr² T = compute_period(a=1.0, M_total=1000.0, G=G) print(f"Period: {T:.2f} Myr") Period: 0.94 Myr

References. Kepler’s 3rd Law: T² ∝ a³/M Murray & Dermott (1999) Eq 2.37

Source: src/progenax/binaries/kepler_period.py#L12

binaries.period_to_semimajor_axis

function

📇 model card

period_to_semimajor_axis(period: ArrayLike, M_total: ArrayLike, G: ArrayLike) -> Float[Array, '...']

Compute semi-major axis from orbital period using Kepler’s 3rd law.

Args

ParameterDescription
periodOrbital period [time units]
M_totalTotal mass of binary system [M☉]
GGravitational constant (REQUIRED, no default)

Returns: a: Semi-major axis [length units] Formula: a = (GMT²/(4π²))^(1/3)

Examples. >>> # Binary with 10 day period, M_total=2 M☉

G = 39.478 # AU³/Msun/yr² P_yr = 10.0 / 365.25 # Convert days to years a = period_to_semimajor_axis(P_yr, M_total=2.0, G=G) print(f"Semi-major axis: {a:.3f} AU") Semi-major axis: 0.089 AU

Star cluster binary: 10 Myr period, M_total=2 M☉

G = 0.00450 # pc³/Msun/Myr² a = period_to_semimajor_axis(10.0, M_total=2.0, G=G) print(f"Semi-major axis: {a:.2f} pc") Semi-major axis: 4.64 pc

References. Kepler’s 3rd Law: a³ ∝ T²M Murray & Dermott (1999) Eq 2.37

Source: src/progenax/binaries/kepler_period.py#L61

binaries.BinaryOrbitalState

class

BinaryOrbitalState(m1: "Float[Array, '']", m2: "Float[Array, '']", elements: 'KeplerElements') -> None

Binary orbital state for IC generation.

Combines component masses with Keplerian orbital elements. This is an IC-specific container - for general orbital mechanics, use KeplerElements directly.

Attributes

ParameterDescription
m1Primary mass [M_sun]
m2Secondary mass [M_sun]
elementsKeplerElements (a, e, i, Omega, omega, M0). The period/mean motion is derived on demand from (a, m1+m2, G) in to_state, not cached here.

Example. >>> from jaxstro.units import PLANETARY

PLANETARY.G is per-year, so convert the period days -> years.

state = BinaryOrbitalState.from_log_period( ... m1=1.0, m2=0.5, logP_days=2.0, e=0.3, ... inc=0.1, Omega=0.0, omega=0.0, M_anom=0.0, ... G=PLANETARY.G, day_in_time_units=1.0 / 365.25 ... ) r1, v1, r2, v2 = state.to_resolved_positions(G=PLANETARY.G)

Source: src/progenax/binaries/orbital_state.py#L20

binaries.batch_elements_to_resolved

function

batch_elements_to_resolved(m1: "Float[Array, 'N']", m2: "Float[Array, 'N']", logP_days: "Float[Array, 'N']", e: "Float[Array, 'N']", inc: "Float[Array, 'N']", Omega: "Float[Array, 'N']", omega: "Float[Array, 'N']", M_anom: "Float[Array, 'N']", *, G: 'float', day_in_time_units: 'float' = 1.0) -> "Tuple[Float[Array, 'N 3'], Float[Array, 'N 3'], Float[Array, 'N 3'], Float[Array, 'N 3']]"

Vectorized wrapper to get resolved (r1, v1, r2, v2) for N binaries.

Args

ParameterDescription
logP_dayslog10(P / day) [N]
eEccentricity [N]
incInclination [rad] [N]
OmegaLongitude of ascending node [rad] [N]
omegaArgument of periapsis [rad] [N]
M_anomMean anomaly [rad] [N]
GGravitational constant (REQUIRED, no default)
day_in_time_unitsConversion factor days -> code time units

Returns: r1, v1, r2, v2: Arrays of shape [N, 3]

Source: src/progenax/binaries/orbital_state.py#L195

binaries.LogUniformPeriod

class

📇 model card · ∇ gradient-verified — 1 audit case

LogUniformPeriod(log_P_min: 'float' = 0.0, log_P_max: 'float' = 8.0) -> None

Log-uniform (Öpik) period distribution.

Öpik’s law: binary periods are uniformly distributed in log space. p(log P) = const => p(P) ∝ 1/P

Reference: Öpik (1924) Publications de l’Observatoire Astronomique de l’Université de Tartu

Parameters: log_P_min: Minimum log10(P/days) (default: 0.0 = 1 day) log_P_max: Maximum log10(P/days) (default: 8.0 = ~27,000 years)

Source: src/progenax/binaries/period.py#L23

binaries.LogNormalPeriod

class

📇 model card · ∇ gradient-verified — 1 audit case

LogNormalPeriod(mu_log_P: 'float' = 4.8, sigma_log_P: 'float' = 2.3) -> None

Log-normal period distribution.

log10(P) ~ Normal(mu, sigma)

Reference: Duquennoy & Mayor (1991) A&A 248, 485 For solar-type stars: mu ≈ 4.8, sigma ≈ 2.3 (P in days)

Parameters: mu_log_P: Mean of log10(P/days) (default: 4.8) sigma_log_P: Std dev of log10(P/days) (default: 2.3)

Source: src/progenax/binaries/period.py#L65

binaries.SanaOBPeriod

class

📇 model card · ∇ gradient-verified — 1 audit case

SanaOBPeriod(log_P_min: 'float' = 0.15, log_P_max: 'float' = 3.5, power: 'float' = -0.55) -> None

Sana+2012 period distribution for O/B stars.

Power-law distribution in log-space: p(log P) ∝ (log P)^(-0.55)

for log P in [0.15, 3.5] (P in days).

This corresponds to shorter periods than solar-type binaries, consistent with observations of massive star binaries.

Reference: Sana et al. (2012) Science 337, 444 - O-star binary survey. The intrinsic period distribution (their Fig. 2, π = -0.55 ± 0.22) runs from P ~ 1.4 d (log P ~ 0.15) to ~9 yr (log P = 3.5).

Parameters: log_P_min: Minimum log10(P/days) (default: 0.15 = ~1.4 days; Sana 2012 Fig.2). Must be > 0: the model p(logP) ∝ (logP)^power is undefined for logP <= 0 (P <= 1 day), since that raises a non-positive base to a fractional power. log_P_max: Maximum log10(P/days) (default: 3.5 = ~3162 days ~ 9 yr) power: Power-law index (default: -0.55 from Sana+2012)

Source: src/progenax/binaries/period.py#L112

binaries.ThermalEccentricity

class

📇 model card · ∇ gradient-verified — 1 audit case

ThermalEccentricity(e_max: 'float' = 0.99) -> None

Thermal eccentricity distribution f(e) = 2e.

Arises from energy equipartition in dynamically relaxed systems. CDF: F(e) = e² => PPF: e = √u

Reference: Heggie (1975) MNRAS 173, 729 Jeans (1919) “Problems of Cosmogony and Stellar Dynamics”

Parameters: e_max: Maximum eccentricity (default: 0.99, avoids singularity)

Source: src/progenax/binaries/eccentricity.py#L24

binaries.UniformEccentricity

class

📇 model card · ∇ gradient-verified — 1 audit case

UniformEccentricity(e_min: 'float' = 0.0, e_max: 'float' = 0.9) -> None

Uniform eccentricity distribution.

Simple uniform distribution, useful for circular-dominated populations.

Parameters: e_min: Minimum eccentricity (default: 0.0) e_max: Maximum eccentricity (default: 0.9)

Source: src/progenax/binaries/eccentricity.py#L65

binaries.LogisticThermalEccentricity

class

∇ gradient-verified — 1 audit case

LogisticThermalEccentricity(P_circ: 'float' = 10.0, P_thermal: 'float' = 1000.0, e_max: 'float' = 0.99, transition_width: 'float' = 0.5) -> None

Smooth circular->thermal eccentricity heuristic (period-conditional).

A differentiable heuristic that interpolates a thermal f(e)=2e distribution from near-circular at short P to fully thermal at long P, via a logistic blend in log10(P): e = blend(P) * e_max * sqrt(u), u ~ U(0,1).

This captures the QUALITATIVE three-period structure of solar-type eccentricities measured by Duquennoy & Mayor (1991) (§6.1/§7.2): P < ~10 d tidally circularized (e ~ 0; their circularization period P_circ ~ 11.6 d), P > ~1000 d approaching thermal f(e)=2e (Ambartsumian 1937), with a smooth transition between. The logistic midpoint defaults to the geometric mean of P_circ=10 d and P_thermal=1000 d (log10 P = 2.0).

It is NOT Moe & Di Stefano’s (2017) f(e) ∝ e^η(P, M1) law — for that, use :class:MoeEccentricity. This class is a smooth, mass-independent surrogate motivated by the tidal-circularization physics (Zahn 1977).

References. Duquennoy & Mayor (1991) A&A 248, 485 §6.1/§7.2 - three-period e model. Ambartsumian (1937); Heggie (1975) MNRAS 173, 729 - thermal f(e)=2e. Zahn (1977) A&A 57, 383 - tidal circularization.

Parameters: P_circ: Circularization period [days] (default: 10.0; DM91 ~11.6 d) P_thermal: Thermalization period [days] (default: 1000.0; DM91 wide onset) e_max: Maximum eccentricity (default: 0.99) transition_width: Width of transition region in log10(P) (default: 0.5)

Source: src/progenax/binaries/eccentricity.py#L98

binaries.MoeEccentricity

class

📇 model card · ∇ gradient-verified — 1 audit case

MoeEccentricity(e_max: 'float' = 0.99) -> None

Moe & Di Stefano (2017) eccentricity distribution p(e) ∝ e^η(logP, M1).

Faithful implementation of Moe & Di Stefano (2017) §9.2 (Figure 36): the eccentricity follows a power law p(e) ∝ e^η on 0 <= e <= e_max, with the slope η a function of orbital period AND primary mass:

Eq. 17 (late-type, 0.8 < M1 < 3 Msun):  η = 0.6 - 0.7 / (logP - 0.5)
Eq. 18 (early-type, M1 > 7 Msun):       η = 0.9 - 0.2 / (logP - 0.5)

with a linear interpolation in M1 for 3 <= M1 <= 7 Msun. η = 0 is uniform ( = 0.5); η = 1 is thermal f(e) = 2e ( = 2/3). Eqs. 17-18 themselves drive short-period (logP ≲ 1) massive binaries to η < 0 (tidal circularization); solar-type binaries asymptote to η ≈ 0.5 at long P; intermediate-period massive binaries reach η ≈ 0.8 (near-thermal). For short periods where η <= -1 (e^η non-normalizable), the orbit is tidally circularized and this returns e ≈ 0 (Moe notes η is “not well defined” for logP ≲ 1).

Sampling uses the inverse CDF e = e_max(P) * u^(1/(η+1)); as η -> -1+ the exponent diverges so e -> 0 (circular) by construction.

The upper limit is the period-dependent Roche-lobe ceiling (their Eq. 3),

e_max(P) = 1 - (P / 2 days)^(-2/3)   for P > 2 d,

clipped to [0, e_max], so the components do not overflow their Roche lobes at periapsis (e.g. e_max(10 d) ≈ 0.66, e_max(100 d) ≈ 0.93); P <= 2 d -> circular. The e_max field is the long-period numerical ceiling (avoids the e -> 1 singularity), reached only where the Roche relation itself approaches 1.

Reference: Moe & Di Stefano (2017) ApJS 230, 15, §9.2 Eqs. 17-18, Eq. 3, Fig. 36. Sana et al. (2012) Science 337, 444 - short-period O-star binaries are eccentricity-poor; the precise slope is in their supplementary Table S3 (not reproduced in the held main report).

Parameters: e_max: Numerical eccentricity ceiling at long P (default: 0.99); the physical cap is the period-dependent Roche relation (Eq. 3).

Source: src/progenax/binaries/eccentricity.py#L176

binaries.sample_isotropic_orientations

function

sample_isotropic_orientations(key: 'PRNGKeyArray', n: 'int') -> "Tuple[Float[Array, 'n'], Float[Array, 'n'], Float[Array, 'n'], Float[Array, 'n']]"

Sample isotropic orbital orientations.

For randomly oriented orbits in 3D space:

Args

ParameterDescription
keyJAX random key
nNumber of orientations to sample

Returns: Tuple of (inclination, Omega, omega, M_anom) arrays, each shape (n,) - inclination: [0, π] radians - Omega: [0, 2π) radians - omega: [0, 2π) radians - M_anom: [0, 2π) radians Reference: Binney & Tremaine (2008) “Galactic Dynamics” Section 3.1

Source: src/progenax/binaries/orientation.py#L16

binaries.RadialBinaryFraction

class

RadialBinaryFraction(fb0: 'float' = 0.5, A: 'float' = 0.5, alpha: 'float' = 1.0, r_scale: 'float' = 1.0) -> None

Phenomenological radially-varying binary fraction.

A simple parametric knob for a spatially varying binary fraction:

f_b(r) = fb0 × (1 + A × (r/r_scale)^(-α)),  clipped to [0, 1]

where: - A > 0: core-enhanced (more binaries in center) - A < 0: core-depleted (fewer binaries in center) - A = 0: constant binary fraction everywhere

NOTE: this functional form is a PHENOMENOLOGICAL model, NOT taken from any specific paper. The references below establish that the binary fraction varies with primary mass / environment (motivating a spatial knob), but none provides this closed-form radial f_b(r) profile. The (r/r_scale)^(-α) term diverges as r -> 0 for α > 0; the clip to [0, 1] caps it (so the core value is set by the clip, not the power law) — intended for r > 0.

References (motivation only — not the source of this functional form): Raghavan et al. (2010) ApJS 190, 1 - solar-neighborhood multiplicity census. Sana et al. (2012) Science 337, 444 - O-star binary fraction. Moe & Di Stefano (2017) ApJS 230, 15 - binary statistics review.

Parameters: fb0: Baseline binary fraction (default: 0.5) A: Amplitude of radial variation (default: 0.5 for core-enhanced) alpha: Power-law index (default: 1.0) r_scale: Scale radius for radial variation (default: 1.0)

Examples. >>> # Core-enhanced: more binaries in center

rbf = RadialBinaryFraction(fb0=0.5, A=0.5, alpha=1.0, r_scale=1.0) radii = jnp.array([0.1, 1.0, 5.0]) fb_r = rbf.compute(radii) # Higher at r=0.1, lower at r=5.0

Sample binary membership

key = jax.random.PRNGKey(42) is_binary = rbf.sample_membership(radii, key)

Source: src/progenax/binaries/mass_dependent.py#L34

binaries.CombinedBinaryFraction

class

CombinedBinaryFraction(mass_model: 'eqx.Module', radial_model: 'eqx.Module') -> None

Binary fraction that modulates a mass-based fraction by a radial one.

f_bin(m, r) = clip(mass_model.probability(m) * radial_model.probability(m, r), 0, 1).

Lets a Moe/Raghavan mass-dependent fraction vary spatially (e.g. core-enhanced binarity) without conflating the two effects. Both sub-models are duck-typed BinaryFractionModel instances. Requires radii at evaluation.

Parameters: mass_model: a mass-based BinaryFractionModel (ignores radii). radial_model: a RadialBinaryFraction-like BinaryFractionModel (uses radii).

Source: src/progenax/binaries/mass_dependent.py#L129

binaries.MassDependentBinaryConfig

class

MassDependentBinaryConfig(m_break: 'float', low_mass_period: 'LogNormalPeriod | LogUniformPeriod', high_mass_period: 'SanaOBPeriod', low_mass_eccentricity: 'ThermalEccentricity | UniformEccentricity', high_mass_eccentricity: 'MoeEccentricity | LogisticThermalEccentricity') -> None

Configuration for mass-dependent binary orbital parameter sampling.

Routes stars to different period/eccentricity distributions based on mass:

References. Sana et al. (2012) Science 337, 444 - O-star binary fraction Moe & Di Stefano (2017) ApJS 230, 15 - Binary statistics review Duquennoy & Mayor (1991) A&A 248, 485 - Solar-type binary periods

Parameters: m_break: Mass threshold [Msun] separating low/high-mass prescriptions (default: 8.0) low_mass_period: Period distribution for M < m_break high_mass_period: Period distribution for M >= m_break low_mass_eccentricity: Eccentricity distribution for M < m_break high_mass_eccentricity: Eccentricity distribution for M >= m_break

Example. >>> config = MassDependentBinaryConfig( ... m_break=8.0, ... low_mass_period=LogNormalPeriod(mu_log_P=4.8, sigma_log_P=2.3), ... high_mass_period=SanaOBPeriod(), ... low_mass_eccentricity=ThermalEccentricity(), ... high_mass_eccentricity=MoeEccentricity(), ... )

masses = jnp.array([1.0, 5.0, 10.0, 20.0]) key = jax.random.PRNGKey(42) periods, ecc = sample_mass_dependent_orbits(masses, config, key)

Source: src/progenax/binaries/mass_dependent.py#L161

binaries.sample_mass_dependent_orbits

function

sample_mass_dependent_orbits(masses: "Float[Array, 'N']", config: 'MassDependentBinaryConfig', key: 'PRNGKeyArray') -> "Tuple[Float[Array, 'N'], Float[Array, 'N']]"

Sample orbital parameters with mass-dependent prescriptions.

Routes each star to appropriate period/eccentricity distribution based on mass:

Uses JAX-native branching (jnp.where) for JIT compatibility.

Args

ParameterDescription
massesStellar masses [Msun] (shape N,)
configMass-dependent binary configuration
keyJAX random key

Returns: Tuple of: - periods: Orbital periods [days] (shape N,) - eccentricities: Orbital eccentricities (shape N,)

Note. Only the high-mass branch supports period-dependent eccentricity (MoeEccentricity, conditioned on the high-mass periods). The low-mass eccentricity is sampled unconditionally (Thermal/Uniform), enforced by the config type hints.

Example. >>> config = MassDependentBinaryConfig( ... m_break=8.0, ... low_mass_period=LogNormalPeriod(), ... high_mass_period=SanaOBPeriod(), ... low_mass_eccentricity=ThermalEccentricity(), ... high_mass_eccentricity=MoeEccentricity(), ... )

masses = jnp.array([1.0, 5.0, 10.0, 20.0]) key = jax.random.PRNGKey(42) periods, ecc = sample_mass_dependent_orbits(masses, config, key)

Source: src/progenax/binaries/mass_dependent.py#L200

binaries.ResolvedBinaries

class

ResolvedBinaries(positions: ForwardRef("Float[Array, 'M 3']"), velocities: ForwardRef("Float[Array, 'M 3']"), masses: ForwardRef("Float[Array, 'M']"), is_real: ForwardRef("Bool[Array, 'M']"), primordial_system_id: ForwardRef("Int[Array, 'M']"), is_primordial_secondary: ForwardRef("Bool[Array, 'M']"))

Masked fixed-shape (2N) particle set from resolving N systems.

Attributes

ParameterDescription
positions(2N, 3) component positions [length units].
velocities(2N, 3) component velocities [velocity units].
masses(2N,) component masses [M_sun] (ghosts = 0).
is_real(2N,) bool — True for real particles (all primaries + binary secondaries); False for single-star ghost secondaries.
primordial_system_id(2N,) int — slots 2i, 2i+1 both = i.
is_primordial_secondary(2N,) bool — True on the odd (secondary) slots.

Source: src/progenax/binaries/assembly.py#L27

binaries.resolve_binary_components

function

📇 model card · ∇ gradient-verified — 2 audit cases

resolve_binary_components(com_pos: "Float[Array, 'N 3']", com_vel: "Float[Array, 'N 3']", m1: "Float[Array, 'N']", m2: "Float[Array, 'N']", is_binary: "Bool[Array, 'N']", a: "Float[Array, 'N']", e: "Float[Array, 'N']", inc: "Float[Array, 'N']", Omega: "Float[Array, 'N']", omega: "Float[Array, 'N']", M_anom: "Float[Array, 'N']", *, G: 'float') -> 'ResolvedBinaries'

Resolve N system COMs into a masked 2N component set (see module docstring).

Args

ParameterDescription
is_binary(N,) bool — True for binary systems. a, e, inc, Omega, omega, M_anom: (N,) Keplerian elements of the relative orbit (ignored for singles; sanitized internally so grads stay finite).
Ggravitational constant (REQUIRED).

Returns: ResolvedBinaries (2N slots + is_real mask + primordial provenance).

Source: src/progenax/binaries/assembly.py#L53

binaries.CompanionElements

class

CompanionElements(m2: ForwardRef("Float[Array, 'N']"), a: ForwardRef("Float[Array, 'N']"), e: ForwardRef("Float[Array, 'N']"), inc: ForwardRef("Float[Array, 'N']"), Omega: ForwardRef("Float[Array, 'N']"), omega: ForwardRef("Float[Array, 'N']"), M_anom: ForwardRef("Float[Array, 'N']"))

Per-system companion mass + relative-orbit elements (singles: m2=0, a sanitized).

Plugs directly into resolve_binary_components(..., a, e, inc, Omega, omega, M_anom).

Source: src/progenax/binaries/companions.py#L35

binaries.IndependentCompanions

class

∇ gradient-verified — 1 audit case

IndependentCompanions(binary_fraction: 'Any', q_distribution: 'Any', period_distribution: 'Any', eccentricity_distribution: 'Any') -> None

Versatile companion model: independent f_b x q x P x e marginals.

Multiplicity from binary_fraction(m1); q from q_distribution (mass-dependent or unconditional); period and eccentricity sampled independently. Reproduces the period-averaged default of build_binary_cluster today. The mass-ratio is the single owner of q -> m2 = m1 * q (0 for singles).

Entropy layout (the equivalence contract): split(key, 5) -> [is_binary, q, period, eccentricity, orientation].

Source: src/progenax/binaries/companions.py#L84

binaries.MoeCompanions

class

📇 model card · ∇ gradient-verified — 1 audit case

MoeCompanions(q_min: 'float' = 0.1) -> None

Faithful Moe & Di Stefano (2017) companion model — the P-q-e interrelation.

Multiplicity from Moe’s own mass-dependent MassDependentBinaryFraction (no f_b supplied — it is set by the IMF masses); orbital parameters from the joint MoeJointOrbit (logP ~ MoePeriod(M1), q ~ MoeDiStefano2017Full(M1,P), e ~ MoeEccentricity(P,M1)). The same q sets m2 = m1 * q (self-consistent), so short-period binaries carry larger q than long-period ones.

Reference: Moe & Di Stefano (2017) ApJS 230, 15 (full joint distribution).

Source: src/progenax/binaries/companions.py#L133

binaries.relative_energy

function

relative_energy(r_i: "Float[Array, '3']", r_j: "Float[Array, '3']", v_i: "Float[Array, '3']", v_j: "Float[Array, '3']", m_i: "Float[Array, '']", m_j: "Float[Array, '']", *, G: 'float') -> "Float[Array, '']"

The (internal) two-body orbital energy of the pair (i, j).

E_rel = ½ μ |v_j - v_i|² − G m_i m_j / |r_j - r_i|, μ = m_i m_j / (m_i + m_j). E_rel < 0 ⇒ the pair is gravitationally bound. For a bound orbit of semi-major axis a, E_rel = −G m_i m_j / (2a). Differentiable (separation guarded).

Source: src/progenax/binaries/diagnostics.py#L35

binaries.find_bound_pairs

function

find_bound_pairs(positions: "Float[Array, 'N 3']", velocities: "Float[Array, 'N 3']", masses: "Float[Array, 'N']", *, G: 'float')

Detect bound two-body pairs: mutual nearest neighbours with E_rel < 0.

Returns (pair_idx, E_rel) where pair_idx is (K, 2) with i < j per row, and E_rel is (K,) the pair binding energies. Each particle appears in at most one pair (mutual-NN is a matching). O(N^2); eager (dynamic K).

Scaling. The full N×N separation matrix is materialized, so this is intended for N ≲ a few×10^3 (memory ~ N^2). For larger snapshots use an accelerated neighbour-list finder (cell list / kd-tree) — tracked for a future gravax acceleration, which already has the Hermite-AC neighbour machinery.

Source: src/progenax/binaries/diagnostics.py#L59

binaries.find_bound_multiples

function

find_bound_multiples(positions: "Float[Array, 'N 3']", velocities: "Float[Array, 'N 3']", masses: "Float[Array, 'N']", *, G: 'float', max_levels: 'int' = 3)

Detect hierarchical bound systems (binaries, triples, quadruples, …).

Iteratively collapses mutual-nearest-neighbour bound pairs into COM pseudo-bodies and re-runs on the reduced set, to a fixed max_levels depth: a single bound to a binary-COM becomes a triple, two binaries a quadruple, etc. Mutual-NN is a matching, so all disjoint merges at a level happen at once.

Fixed-shape (N body slots, bounded lax.scan) ⇒ jit-safe; uses argmin, so not differentiable (a diagnostic). O(N^2) per level (materializes the N×N separation matrix) — intended for N ≲ a few×10^3; an accelerated neighbour-list version is tracked for a future gravax acceleration.

Args

ParameterDescription
max_levelshierarchy depth to resolve (default 3 ⇒ up to ~octuples).

Returns: (system_id, multiplicity), each (N,): system_id[i] is the body slot of particle i (members of one hierarchy share it); multiplicity[i] is the number of particles in that system.

Source: src/progenax/binaries/diagnostics.py#L105

binaries.primordial_survival

function

primordial_survival(current_pairs, primordial_system_id: "Int[Array, 'N']") -> 'dict'

Compare the current bound pairing to the t=0 primordial labelling.

Args

ParameterDescription
current_pairs(K, 2) index pairs from :func:find_bound_pairs.
primordial_system_id(N,) the IC-time primordial_system_id (paired particles share an id).

Returns: dict with integer counts: survived (primordial binaries still bound), disrupted (primordial binaries no longer a current pair), and newly_formed (current pairs that were not primordial binaries).

Source: src/progenax/binaries/diagnostics.py#L187

binaries.BinaryEnergyBudget

class

BinaryEnergyBudget(E_internal: ForwardRef("Float[Array, '']"), T_com: ForwardRef("Float[Array, '']"), W_com: ForwardRef("Float[Array, '']"), Q_com: ForwardRef("Float[Array, '']"), Q_resolved: ForwardRef("Float[Array, '']"), n_binaries: ForwardRef('int'))

Two-scale energy budget of a primordial-binary cluster.

Attributes

ParameterDescription
E_internaltotal internal orbital energy of the primordial binaries (Σ relative_energy; < 0 if bound). The separate “reservoir” that the global virial scaling (Q) does NOT touch. NB the softening passed to the bound-pair finders does NOT soften this internal binding energy — it only regularizes the inter-system potential (audit S18). T_com, W_com: bulk kinetic / gravitational energy of the system COMs — the scale the cluster is virialized on.
Q_comT_com / |W_com| — the virial ratio the cluster was scaled to (≈ the Q passed to build_binary_cluster).
Q_resolvedT / |W| on the resolved stars — the naive ratio that mixes the cluster and internal-binary scales. The deep internal binary binding dominates |W|, so Q_resolved is DEFLATED below the cluster Q (measured ≈ 0.31 vs the 0.5 the cluster was scaled to), NOT inflated (audit S10).
n_binariesnumber of primordial binaries (two positive-mass members).

Source: src/progenax/binaries/diagnostics.py#L212

binaries.binary_energy_budget

function

binary_energy_budget(positions: "Float[Array, 'N 3']", velocities: "Float[Array, 'N 3']", masses: "Float[Array, 'N']", system_id: "Int[Array, 'N']", *, G: 'float', softening: 'float' = 0.0) -> 'BinaryEnergyBudget'

Separate the cluster-COM virial from the internal binary binding energy.

build_binary_cluster virializes the system COMs to Q (binaries as point masses) and leaves the internal binary binding energy as a separate reservoir (the McLuster scale-separation convention, Küpper+2011 §A8). This diagnostic makes that explicit: it reconstructs the system COMs (segment sums over system_id) and reports Q_com (recovers the virial target), E_internal (the reservoir), and the naive Q_resolved (which mixes the two scales and is therefore not ~Q).

Keyed on system_id, so it accepts either the compacted ICResult (primordial_system_id) or the masked ResolvedBinaries (ghost secondaries m=0 contribute exactly 0). The COM reconstruction is differentiable; the binary pairing is eager (like find_bound_pairs). Handles singles + binaries — systems with > 2 members (triples/quadruples) are skipped (the higher-multiplicity seam).

Args

ParameterDescription
system_idper-star system label (members of one system share it).
Ggravitational constant (REQUIRED).
softeningsoftening for the COM/resolved PE (default 0 = exact, matching the collisional build default).

Returns: :class:BinaryEnergyBudget.

Source: src/progenax/binaries/diagnostics.py#L261