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.kinematics`

San Diego State University

progenax.kinematics

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

Module path: progenax/kinematics/

Public symbols: 14

Contents

kinematics.PlummerVelocityDF

class

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

PlummerVelocityDF(r_h: ArrayLike = 1.0, anisotropy_radius: ArrayLike | None = None)

Plummer (1911) velocity distribution function.

Samples velocity magnitudes from the exact Plummer DF using Beta distribution (no rejection sampling required). Velocities are isotropically distributed.

The distribution for q = v/v_esc is: g(q) ∝ q² (1 - q²)^(7/2) for q ∈ [0, 1]

This corresponds to the energy distribution: f(E) ∝ E^(7/2) where E = ψ - v²/2 is the binding energy

Sampling method: Let u = q², then u ~ Beta(3/2, 9/2) Therefore: q = sqrt(u), v = q × v_esc

This gives the exact velocity dispersion: <q²> = 1/4 => <v²> = v_esc²/4 => σ² = v_esc²/12

Which matches the Plummer formula: σ²(r) = GM/(6√(r²+a²)) with v_esc²(r) = 2GM/√(r²+a²)

Attributes

ParameterDescription
r_hHalf-mass radius [length units]
aPlummer scale radius [length units] (computed from r_h)

References. Plummer (1911), MNRAS, 71, 460 - Original Plummer model Aarseth (2003), “Gravitational N-Body Simulations”, Section 4.3.2 Binney & Tremaine (2008), “Galactic Dynamics”, Section 4.3 Merritt (1985), AJ, 90, 1027, Eq. 42 - explicit isotropic Plummer DF f(E) ∝ (−E)^(7/2)

Notes. - Beta(3/2, 9/2) sampling is EXACT (no rejection, 100% efficient)

Examples. >>> from progenax.profiles.plummer import PlummerProfile

import jax import jax.numpy as jnp

Create spatial profile and velocity DF

profile = PlummerProfile(r_h=1.0) velocity_df = PlummerVelocityDF(r_h=1.0)

Sample positions and velocities

masses = jnp.ones(100) key = jax.random.PRNGKey(42) key_pos, key_vel = jax.random.split(key)

positions = profile.sample_positions(masses, key_pos) from jaxstro.units import STELLAR velocities = velocity_df.sample_velocities(positions, masses, key_vel, G=STELLAR.G)

Source: src/progenax/kinematics/plummer_df.py#L36

kinematics.KingVelocityDF

class

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

KingVelocityDF(W0: ArrayLike = 5.0, r_c: ArrayLike = 1.0, xi_max: float | None = None, n_ode_points: int | None = None, speed_method: str = 'table')

King (1966) lowered-Maxwellian velocity distribution function.

A true equilibrium DF: velocities are sampled from the lowered Maxwellian whose radial speed structure follows the King potential psi(r), with the central velocity scale sigma fixed self-consistently from (G, M_total, r_c, W0). The resulting ICs are in virial equilibrium (Q = 0.5) without external rescaling, and all particles are bound (v < v_esc(r)).

Speed draws are TABLE-BACKED by default (speed_method=“table”): the inverse-CDF table is CACHED at construction (SpeedCDFTable.build(W0, g=1)) and the draw runs through a jitted core — at g=1 the LIMEPY lowered exponential reduces EXACTLY to the King lowering, E_gamma(1, x) = e^x - 1 (identity guarded to rtol 1e-12 in tests/unit/kinematics/test_king_df.py::TestKingTableRouting), so the table weight x^2 E_gamma(1, W(1-x^2)) IS the King speed weight u^2 (exp(W - u^2/2) - 1). speed_method=“quadrature” retains the exact per-star 256-point quadrature as the oracle (statistical agreement asserted in TestKingTableRouting). Reuse one DF instance for repeated draws: construction (ODE solve + table) dominates; per-draw cost is then milliseconds at any N.

Attributes

ParameterDescription
W0King concentration parameter (dimensionless central potential)
r_cCore radius [length units] (the King core radius) xi_grid, psi_grid: ODE solution of the King model (xi = r/r_c, psi(xi))
speed_methodstatic, “table” (default) or “quadrature” (exact oracle)

References. King (1966), AJ, 71, 64 Binney & Tremaine (2008), “Galactic Dynamics”, 2nd ed., Eq. 4.131

Source: src/progenax/kinematics/king_df.py#L65

kinematics.MichieVelocityDF

class

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

MichieVelocityDF(W0: ArrayLike = 7.0, r_c: ArrayLike = 1.0, r_a: ArrayLike = 10.0, xi_max: float = 800.0, n_ode_points: int = 3000, speed_method: str = 'table')

Michie-King anisotropic velocity DF (radially anisotropic, lowered-Maxwellian).

beta(r) ~ 0 at the centre, increasing outward; r_a -> infinity is the isotropic King DF. The velocity scale sigma is self-consistent (sigma^2 = G M / (9 r_c mu), mu the anisotropic dimensionless mass integral), so ICs are virial without external rescale.

Speed draws are TABLE-BACKED by default (speed_method=“table”): the speed marginal comes from the inverse-CDF table CACHED at construction (AnisoSpeedCDFTable.build(W0, p_box, g=1)) via a jitted core — Michie IS the g=1 anisotropic LIMEPY model (E_gamma(1, x) = e^x - 1 exactly; the standing distributional proof is test_g1_aniso_matches_michie_velocity_df). The angular conditional cos(theta)|u stays EXACT (_sample_costheta_given_u, weight exp(-(s^2 u^2/2)(1 - cos^2 theta)) = exp(-s^2 u_t^2/2) under u_t = u sin(theta)) — only the speed marginal is tabulated. speed_method=“quadrature” retains the exact per-star 2-D (u_t-marginal-then-u_r) inverse-CDF as the oracle (statistical agreement asserted in tests/unit/kinematics/test_michie_df.py:: TestMichieTableRouting). Reuse one DF instance for repeated draws: construction (ODE solve + mu integral + table) dominates; per-draw cost is then milliseconds at any N.

Attributes

ParameterDescription
muint rho_hat(psi, xi/ra_hat) xi^2 dxi (sets sigma).
speed_methodstatic, “table” (default) or “quadrature” (exact oracle).

References. Michie (1963), MNRAS 125, 127; King (1966), AJ 71, 64.

Source: src/progenax/kinematics/michie_df.py#L74

kinematics.LIMEPYVelocityDF

class

LIMEPYVelocityDF(W0: ArrayLike = 5.0, g: ArrayLike = 1.0, r_c: ArrayLike = 1.0, r_a: ArrayLike | None = None, xi_max: float = 300.0, n_ode_points: int = 2000, speed_method: str = 'table')

General-g LIMEPY lowered-isothermal velocity DF (isotropic or Michie/OM).

A true equilibrium DF; the central velocity scale s is self-consistent from (G, M_total, r_c, W0, g, r_a), so ICs are virial (Q=0.5) without rescaling and all stars are bound. r_a=None is isotropic (generalizes KingVelocityDF); a finite r_a adds radial anisotropy (generalizes MichieVelocityDF). At g=1 it reduces to King (isotropic) / Michie-King (anisotropic).

Speed draws are TABLE-BACKED by default (speed_method=“table”): the speed marginal comes from the inverse-CDF table CACHED at construction (SpeedCDFTable / AnisoSpeedCDFTable, the same machinery MultiComponentCluster uses), and the whole draw runs through a jitted core, so live memory is O(N) instead of the eager per-star quadrature’s O(N * 256 * 91) Poisson-sum buffers (measured at N=2e4 anisotropic: 10.87 GB pre-tables; draw-chain peak +0.20 GB cached+jitted; warm draw 0.015 s after the one-time ~0.5 s compile). The anisotropic angular conditional cos(theta)|u stays EXACT (_sample_costheta_given_u) -- only the speed marginal is tabulated. speed_method=“quadrature” retains the exact per-star quadrature as the oracle (statistical agreement asserted in tests/unit/kinematics/test_limepy_df.py::TestLimepyTableRouting). Cost structure: CONSTRUCTION dominates (~2.6 s / ~1.8 GB peak for an anisotropic model: two ODE solves + the mu integral + the cached table build) -- reuse one DF instance for repeated draws; per-draw cost is then milliseconds at any N.

Attributes

ParameterDescription
r_ttruncation radius. xi_grid, psi_grid: ODE solution W(xi).
mudimensionless mass integral int rho_tilde xi^2 dxi (sets s).
is_anisostatic flag selecting the anisotropic sampler.
speed_methodstatic, “table” (default) or “quadrature” (exact oracle).

Source: src/progenax/kinematics/limepy_df.py#L102

kinematics.EFFVelocityDF

class

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

EFFVelocityDF(a: ArrayLike = 1.0, gamma: ArrayLike = 3.0, r_t: ArrayLike = 10.0, anisotropy_radius: ArrayLike | None = None)

EFF (Elson-Fall-Freeman 1987) velocity DF via Eddington inversion.

Samples the exact ergodic DF f(E) of the (truncated) EFF density. The central velocity scale is fixed self-consistently from (G, M_total, a, gamma, r_t), so no external virial rescale is needed.

.. warning:: The DEFAULT (gamma=3, sharp truncation) is KNOWN ~8% sub-virial by construction: a truncated rho with rho(r_t)>0 cannot be represented by an f(E), so the sampled cluster is only approximately stationary (measured Q = T/|V| ~ 0.458, not 0.5; pinned by test_eff_physics test_gamma3_default_subvirial_offset_is_pinned). This is a documented limitation, NOT an inversion error. For mild truncation (gamma>=5) the offset shrinks to ~1% (Q ~ 0.495); for a strict lowered-DF equilibrium use the King/Michie models instead.

With anisotropy_radius (r_a) set, the DF is the Osipkov-Merritt radially anisotropic model for the same EFF density: f = f(Q), Q = E + J^2/2r_a^2, built by Eddington inversion of the augmented density rho_Q = (1 + r^2/r_a^2) rho (Merritt 1985). The realised anisotropy is beta(r) = r^2/(r^2 + r_a^2). r_a=None is isotropic.

Attributes

ParameterDescription
aScale radius [length units], must match the spatial profile
gammaPower-law index
r_tTidal/truncation radius [length units]
anisotropy_radiusOsipkov-Merritt radius r_a, or None for isotropic r_grid, Psi_grid: relative potential Psi(r) (dimensionless: G=1, rho_0=1) E_grid, f_grid: tabulated ergodic DF f(E) (augmented density if anisotropic)
muint_0^{r_t} rho_tilde r^2 dr (sets rho_0 = M_total / (4 pi mu))

References. Elson, Fall & Freeman (1987), ApJ, 323, 54 Binney & Tremaine (2008), “Galactic Dynamics”, 2nd ed., Eq. 4.46 (Eddington) Merritt (1985), AJ, 90, 1027 (Osipkov-Merritt anisotropy)

Source: src/progenax/kinematics/eff_df.py#L71

kinematics.apply_solid_body_rotation

function

∇ gradient-verified — 1 audit case

apply_solid_body_rotation(velocities: Float[Array, 'N 3'], positions: Float[Array, 'N 3'], omega: float, axis: Float[Array, '3']) -> Float[Array, 'N 3']

Add solid body rotation to velocities.

Adds streaming velocity v_rot = omega x r to existing velocities.

For solid body rotation, all particles rotate with the same angular velocity omega, giving v_phi = omega * R (cylindrical R).

.. warning:: Kinematic overlay — injects kinetic energy and L_z. The result is NOT a stationary equilibrium (Q = T/|V| rises above 0.5); see the module caveat (audit S3).

Args

ParameterDescription
velocitiesInput velocities (N, 3)
positionsParticle positions (N, 3)
omegaAngular velocity magnitude [rad/time]
axisRotation axis vector (3,), will be normalized. Must be nonzero.

Returns: Velocities with rotation added (N, 3)

Example. >>> # Add rotation around z-axis with omega = 0.1 rad/Myr

v_rot = apply_solid_body_rotation( ... velocities, positions, ... omega=0.1, ... axis=jnp.array([0., 0., 1.]) ... )

Reference: Binney & Tremaine (2008) Section 4.8

Source: src/progenax/kinematics/rotation.py#L41

kinematics.apply_differential_rotation

function

∇ gradient-verified — 2 audit cases

apply_differential_rotation(velocities: Float[Array, 'N 3'], positions: Float[Array, 'N 3'], v_peak: float, R_peak: float, axis: Float[Array, '3']) -> Float[Array, 'N 3']

Add differential rotation with peaked rotation curve.

Rotation curve: v_phi(R) = v_peak * (R/R_peak) * exp(1 - R/R_peak)

This gives: - v_phi(0) = 0 (no rotation at center) - v_phi(R_peak) = v_peak (maximum rotation) - v_phi -> 0 as R -> infinity (decreasing at large R)

.. warning:: Kinematic overlay — injects kinetic energy and L_z. The result is NOT a stationary equilibrium (Q rises above 0.5); see the module caveat (S3).

Args

ParameterDescription
velocitiesInput velocities (N, 3)
positionsParticle positions (N, 3)
v_peakPeak rotation velocity [velocity units]
R_peakRadius of peak rotation [length units]
axisRotation axis vector (3,), will be normalized. Must be nonzero.

Returns: Velocities with differential rotation added (N, 3)

Note. The peaked form v_phi(R) = v_peak (R/R_peak) exp(1 - R/R_peak) is a phenomenological rotation curve (smooth rise to a single peak at R_peak, then decay) chosen for convenience -- it is NOT taken from a specific paper. Lynden-Bell (1960), MNRAS 120, 204 is the classic reference for rotating stellar systems in general, not for this functional form.

Source: src/progenax/kinematics/rotation.py#L90

kinematics.jeans_dispersion

function

∇ gradient-verified — 3 audit cases

jeans_dispersion(profile, r_a, r, M, G, n_s: int = 4000, beta_fn=None) -> progenax.kinematics.dispersion.DispersionProfile

3-D anisotropic Jeans dispersion of profile under OM r_a.

Returns the equilibrium (sigma_r, sigma_t, sigma_1d, beta) of the spatial profile (which owns rho, M, Phi) for an Osipkov-Merritt (1985) anisotropy radius r_a (None -> isotropic). The radial dispersion is the anisotropic Jeans solution (master tables from :func:_jeans_tables, evaluated by :func:_sigma_r2_from_tables); the tangential / 1-D components and beta follow from OM (:func:_sigma_components).

Anisotropy model (IMPORTANT — read before using on a Michie/King profile). This function imposes the Osipkov-Merritt anisotropy law beta(r) = r^2 / (r^2 + r_a^2) on the density of profile, regardless of the profile’s own intrinsic anisotropy. Consequences:

Units follow the caller-supplied G and M; the returned velocities are in whatever sqrt(G M / length) implies. Differentiated with reverse-mode AD (the supported/tested path); forward-mode also works for analytic-density profiles (Plummer/EFF) but not through the King/Michie equilibrium-solver profiles — see the module docstring.

General-beta(r) anisotropy (Tier A, Phase 0.5 D1). Pass a callable beta_fn(r) -> beta to use an ARBITRARY anisotropy profile (native Michie/King anisotropy, custom beta(r)) instead of the OM law, via the general integrating factor f(r) = exp(2 int beta(s)/s ds)::

rho sigma_r^2(r) = (1/f(r)) int_r^inf f(s) rho(s) G M(<s)/s^2 ds
sigma_t^2 = (1 - beta) sigma_r^2 ;  sigma_1d^2 = (sigma_r^2 + 2 sigma_t^2)/3

f is built numerically (max-subtracted log form for stability); the OM special case f = r^2 + r_a^2 (beta = r^2/(r^2 + r_a^2)) is the analytic default and is BIT-PRESERVED when beta_fn is None. When beta_fn is given, r_a is unused and the Plummer-OM r_a >= 0.75 a validity guard (an OM-only domain) is skipped. Note: the beta_fn path carries an O(h^2) error from the numerically-built integrating factor (the trapezoid F = 2 int beta/s ds), whereas the analytic-OM default has none.

The enclosed mass M(<s) is a quadrature of profile.density (builder-quality, no re-differentiated Psi): M_enc = M * cumtrap(rho s^2) / cumtrap_total(rho s^2). A fine radial s-grid runs to profile.r_t if present (King/EFF); for Plummer (no finite cutoff) the semi-infinite domain is algebraically compactified via s = a t/(1-t) (:func:_jeans_tables), so the full outward tail is integrated (no truncation bias).

Parameters

profile : spatial profile exposing density(r) (and optionally r_t, a). r_a : Osipkov-Merritt anisotropy radius, or None for isotropic. r : query radii (array-like; broadcast to at least 1-D). M : total mass normalising the enclosed-mass quadrature. G : gravitational constant (sets the velocity units). n_s : number of points on the fine radial s-grid (default 4000). The trapezoid quadrature is O(h^2 = (r_max/n_s)^2); exposed so a convergence study can refine it. Backward-compatible (keyword, default unchanged) and traced-safe (a static Python int, not a JAX value). beta_fn : optional callable beta_fn(r) -> beta for general anisotropy. When given, the numerical integrating-factor path is used (r_a ignored); None (default) keeps the bit-preserved analytic OM/isotropic path.

Returns

DispersionProfile

Notes

For a concrete (non-traced) r_a the Plummer OM DF requires r_a >= 0.75 a (Merritt 1985, Eq. 46); a smaller value is unphysical (negative phase-space DF) and raises ValueError. The check is eager, skipped under tracing so jax.grad/jax.jit over r_a still work, and gated on hasattr(profile, "a") so non-Plummer profiles (different validity domains) do not trip it.

Near a truncation-divergence the forward map is stiff. For a strongly anisotropic Michie profile the tidal radius runs away as W0 approaches the no-finite-truncation edge (e.g. at r_a = 5 r_c the Michie r_t grows ~28 -> 545 over W0 6 -> 7 and the model has no finite r_t past W0 ~ 7.1). The sigma(W0) gradient stays CORRECT there (reverse-mode AD matches a converged finite difference), but a single fixed-step FD is an unreliable truth-proxy because the map’s curvature is near-singular — so the Michie-W0 gradient gate is exercised in the well-truncated regime (W0=6); the high-W0 correctness is pinned by a Richardson-FD test (test_grad_jeans_michie_high_W0_ad_correct).

Source: src/progenax/kinematics/dispersion.py#L436

kinematics.project_dispersion

function

∇ gradient-verified — 3 audit cases

project_dispersion(profile, r_a, R, M, G, n_u: int = 4000) -> progenax.kinematics.dispersion.ProjectedDispersion

Observed projected dispersions of profile (Binney & Mamon 1982 LOS + Strigari+2007 proper motions).

Projects the 3-D anisotropic Jeans model (:func:jeans_dispersion) onto the sky along the line of sight, returning the OBSERVED dispersions at on-sky radii R. The line-of-sight kernel (1 - beta R^2/r^2) is Binney & Mamon (1982), MNRAS 200, 361, Eq. 7 (the only projection formula in that paper); the two proper-motion kernels (1 - beta + beta R^2/r^2) (on-sky radial) and (1 - beta) (on-sky tangential) are Strigari, Bullock & Kaplinghat (2007), ApJ 657, L1, Eqs. 2-3 (earliest first-principles derivation: Leonard & Merritt 1989, ApJ 339, 195). All three share the same projection measure r/sqrt(r^2-R^2) and the weight rho sr^2. With Osipkov-Merritt anisotropy beta(r) = r^2 / (r^2 + r_a^2)::

Sigma(R)            = 2 int_R^inf rho                       r/sqrt(r^2-R^2) dr
Sigma sigma_los^2   = 2 int_R^inf (1 - beta R^2/r^2)  rho sr^2 r/sqrt(r^2-R^2) dr  # B&M82 Eq. 7
Sigma sigma_pmR^2   = 2 int_R^inf (1 - beta + beta R^2/r^2) rho sr^2 r/sqrt(...) dr  # Strigari+2007 Eq. 2
Sigma sigma_pmT^2   = 2 int_R^inf (1 - beta)         rho sr^2 r/sqrt(r^2-R^2) dr  # Strigari+2007 Eq. 3

where sr^2 = sigma_r^2(r) (radial), sigma_los is the line-of-sight (the RV channel), and sigma_pmR / sigma_pmT are the on-sky radial / tangential proper-motion (PM) channels. rho cancels in the sigma ratios; in Sigma it is the projected surface density in profile.density units. The isotropic limit (r_a=None -> beta=0) collapses all three kernels to 1, so sigma_los = sigma_pm_r = sigma_pm_t; anisotropy lives in the ratios.

Isotropic-Plummer sigma_los oracle: for the isotropic Plummer member this projection has the closed form sigma_los^2(R) = (3 pi / 64) G M / sqrt(a^2 + R^2) (Dejonghe 1987, MNRAS 224, 13), used as the tight absolute validation anchor in test_dispersion_physics.py.

Singularity removal (load-bearing, keeps it differentiable): the 1/sqrt(r^2-R^2) pole at r=R is removed ANALYTICALLY by the substitution r^2 = R^2 + u^2 (so r dr / sqrt(r^2-R^2) = du)::

int_R^inf g(r) r/sqrt(r^2-R^2) dr = int_0^Umax g(sqrt(R^2+u^2)) du

so NO 1/sqrt(r^2-R^2) is ever evaluated — only a smooth trapezoid quadrature in u (Umax = sqrt(r_t^2 - R^2) for finite r_t; Umax = inf for Plummer, see below). sigma_r^2(r) is read from the ONE master Jeans solve — :func:_jeans_tables is built once (hoisted out of the per-R vmap) and :func:_sigma_r2_from_tables interpolates it per integration radius — and beta(r) is the closed-form OM law; rho from profile.density.

The outward u-grid is branched on profile.r_t:

The per-R u-integral is vmapped over the R array. Fully reverse-mode differentiable; jax.jit-able.

Parameters

profile : spatial profile exposing density(r) (and optionally r_t, a). r_a : Osipkov-Merritt anisotropy radius, or None for isotropic (beta=0). R : projected (on-sky) radii (array-like; broadcast to at least 1-D). M : total mass normalising the enclosed-mass quadrature (passed through to :func:jeans_dispersion). G : gravitational constant (sets the velocity units). n_u : number of points on the uniform u-quadrature grid (default 4000).

Returns

ProjectedDispersion

Source: src/progenax/kinematics/dispersion.py#L692

kinematics.df_moment_dispersion

function

∇ gradient-verified — 2 audit cases

df_moment_dispersion(df, r, M, G, n_w: int = 256, n_alpha: int = 128) -> progenax.kinematics.dispersion.DispersionProfile

Exact second-moment dispersion of the anisotropic Michie-King DF (Tier B).

Computes (sigma_r, sigma_t, sigma_1d, beta) by directly integrating the Michie (1963) velocity DF’s second velocity moments at each radius — the native Michie equilibrium, correct at ALL radii (unlike :func:jeans_dispersion with an OM r_a, which imposes the OM anisotropy law and is valid only in the inner region where OM approximates the Michie law). This is the DF-side truth that the Michie sampler draws from.

Physics (verified vs michie_df.py / _michie_beta_oracle; Michie 1963, King 1966). With dimensionless speed u = v/sigma, at radius r::

W(r)  = interp(r/r_c, xi_grid, psi_grid, left=W0, right=0), clamped >= 0
s     = r/r_a
sigma = sqrt(G M / (9 r_c mu))     (self-consistent Michie velocity scale)
f~(u_r, u_t) = exp(-s^2 u_t^2/2) [exp(W - (u_r^2+u_t^2)/2) - 1]
               on the bound region u_r^2 + u_t^2 <= 2W.

The bound region is integrated by a fixed-domain POLAR quadrature that needs NO boundary mask (fully differentiable): u_r = w cos(alpha), u_t = w sin(alpha) with w in [0, sqrt(2W)] and alpha in [0, pi] (the energy bound becomes exactly the w upper limit). The velocity-space measure d^3u = 2 pi u_t du_r du_t = 2 pi w^2 sin(alpha) dw dalpha — the 2 pi (and any constant) cancels in the moment RATIOS, so the integration weight is simply w^2 sin(alpha)::

<u_r^2> = int f~ (w cos a)^2 w^2 sin a / int f~ w^2 sin a
<u_t^2> = int f~ (w sin a)^2 w^2 sin a / int f~ w^2 sin a

Then sigma_r = sigma sqrt(<u_r^2>), sigma_t = sigma sqrt(<u_t^2>/2) (since <v_t^2> = 2 sigma_t^2), and::

beta     = 1 - sigma_t^2/sigma_r^2 = 1 - <u_t^2>/(2 <u_r^2>)
sigma_1d = sqrt((sigma_r^2 + 2 sigma_t^2)/3).

The beta here matches _michie_beta_oracle’s 1 - ut2/(2 ur2) exactly. A fixed w_hat in [0,1] grid is scaled per-r by sqrt(2W) (clamped so W <= 0 outside the system gives sigma_r = sigma_t -> 0 naturally — the integrand vanishes); the per-r 2-D moment is jax.vmaped over r. Fully reverse-mode differentiable; jnp.trapezoid (integrate alpha then w). Zero new deps.

Parameters

df : :class:~progenax.kinematics.MichieVelocityDF exposing W0, r_c, r_a, xi_grid, psi_grid, mu. r : query radii (array-like; broadcast to at least 1-D). M : total mass setting the self-consistent velocity scale sigma. G : gravitational constant (sets the velocity units). n_w, n_alpha : quadrature resolutions in w (speed) and alpha (angle).

Returns

DispersionProfile

Source: src/progenax/kinematics/dispersion.py#L570

kinematics.VelocityDF

protocol

VelocityDF(*args, **kwargs)

Protocol for velocity distribution functions.

Implementations must sample velocities given positions and masses. Enables composability: mix Plummer positions + King velocities.

Source: src/progenax/protocols.py#L55

kinematics.RotationParams

class

RotationParams(solid_body: bool = False, differential: bool = False, pattern_speed: float = 0.0, v_peak: float = 0.0, r_peak: float = 1.0, axis: Optional[tuple[float, float, float]] = None) -> None

Parameters for rotation velocity transforms.

Supports both solid-body and differential rotation. Both can be applied together if desired.

Attributes

ParameterDescription
solid_bodyWhether to apply solid-body rotation.
differentialWhether to apply differential rotation.
pattern_speedAngular velocity for solid-body rotation [rad/time]. Maps to omega in apply_solid_body_rotation.
v_peakPeak rotation velocity for differential rotation [velocity units].
r_peakRadius of peak rotation [length units].
axisRotation axis vector (3,). If None, uses z-axis [0, 0, 1]. Will be normalized internally.

Notes. Solid-body: v_rot = omega x r (constant angular velocity) Differential: v_phi(R) = v_peak * (R/R_peak) * exp(1 - R/R_peak)

References. Lynden-Bell (1960) MNRAS 120, 204 Binney & Tremaine (2008) Section 4.8

Source: src/progenax/kinematics/api.py#L57

kinematics.VelocityModel

class

VelocityModel(df: progenax.protocols.VelocityDF, rotation: Optional[progenax.kinematics.api.RotationParams] = None, target_Q: Optional[float] = None) -> None

Complete velocity model specification.

Bundles a distribution function with optional rotation and a target virial ratio. Radial anisotropy is a property of the DF itself: pass anisotropy_radius to PlummerVelocityDF/EFFVelocityDF for an Osipkov-Merritt DF (beta(r)=r^2/(r^2+r_a^2)).

Attributes

ParameterDescription
dfVelocity distribution function (PlummerVelocityDF, KingVelocityDF, etc.; anisotropic DFs supply their own radial anisotropy).
rotationOptional rotation parameters.
target_QTarget virial ratio Q = T / |V|, or None (default). The Plummer/King/EFF DFs are already sampled in detailed equilibrium, so target_Q=None keeps their native equilibrium (no rescale). Pass a float only to deliberately force a virial ratio (e.g. 0.5 for equilibrium, <0.5 subvirial, >0.5 supervirial), or when adding rotation / mixing an inconsistent profile+DF combination.

Example. >>> model = VelocityModel( ... df=PlummerVelocityDF(r_h=1.0, anisotropy_radius=2.0), ... rotation=RotationParams(solid_body=True, pattern_speed=0.1), ... )

Source: src/progenax/kinematics/api.py#L91

kinematics.sample_velocities_pipeline

function

sample_velocities_pipeline(key: PRNGKeyArray, positions: Float[Array, 'N 3'], masses: Float[Array, 'N'], model: progenax.kinematics.api.VelocityModel, G: float) -> Float[Array, 'N 3']

Velocity pipeline: DF sampling -> rotation -> optional virial rescale.

This is the main entry point for velocity generation in progenax. Radial anisotropy lives on the DF (pass anisotropy_radius), so the pipeline only layers optional rotation and an optional virial rescale on top of the DF sample.

Pipeline stages: 1. Sample velocities from distribution function (model.df; carries any Osipkov-Merritt anisotropy intrinsically) 2. Apply rotation (solid-body and/or differential) if configured 3. Rescale to target virial ratio (only if model.target_Q is not None), then remove COM motion

Args

ParameterDescription
keyJAX random key.
positionsParticle positions (N, 3) [length units].
massesParticle masses (N,) [mass units].
modelVelocityModel specifying DF + rotation + target Q.
GGravitational constant (REQUIRED, explicit-units policy; e.g. STELLAR.G).

Returns: velocities: Particle velocities (N, 3) [velocity units].

Example. >>> from progenax.kinematics import ( ... PlummerVelocityDF, VelocityModel, sample_velocities_pipeline ... )

import jax

from jaxstro.units import STELLAR model = VelocityModel(df=PlummerVelocityDF(r_h=1.0), target_Q=0.5) key = jax.random.PRNGKey(42) velocities = sample_velocities_pipeline( ... key, positions, masses, model, G=STELLAR.G ... )

Notes. - All stages are JAX-compatible and differentiable

Source: src/progenax/kinematics/api.py#L122