Source code for orbix.kepler.shortcuts.fixed_e
"""Fixed eccentricity shortcuts for Kepler's equation.
These are useful if you are solving Kepler's equation many times with the same
eccentricity, since they pre-compute the eccentric anomaly for all mean
anomalies and create fast, JIT-compiled functions that either return the
nearest value (`E_lookup`) or interpolate between them (`E_linear_interp` and
`E_hermite_interp`).
The functions return a JIT-compiled function that takes a mean anomaly
and returns the eccentric anomaly. The eccentric anomaly is computed using
the `E_solve` function from the `kepler.core` module. Each method is optimized
to do as few operations as possible and pre-calculate any constants that are
not dependent on the mean anomaly. Those constants are included in the returned
function by closure.
"""
import jax
import jax.numpy as jnp
from hwoutils.constants import two_pi
from orbix.kepler.core import E_solve
[docs]
def E_lookup(e, n=2048):
"""Create a JIT'd function to lookup E for a fixed eccentricity.
Args:
e: The eccentricity of the orbit.
n: The number of mean anomaly values to use in the lookup table.
Returns:
A JIT-compiled function that takes M (as either a scalar or an array) and
returns E (as a scalar or an array).
"""
M_vals = jnp.linspace(0, two_pi, n, endpoint=False)
E_vals = E_solve(M_vals, e)
dM = M_vals[1] - M_vals[0]
inv_dM = 1.0 / dM
n_int = jnp.int32(n)
@jax.jit
def lookup(M):
M = jnp.mod(M, two_pi)
inds = ((M * inv_dM) + 0.5).astype(jnp.int32) % n_int
return E_vals[inds]
return lookup
[docs]
def E_linear_interp(e, n=2048):
"""Create a JIT'd function to interpolate E for a fixed eccentricity.
This method uses linear interpolation to find the eccentric anomaly. The
basic idea is to turn most operations into integer operations by scaling
the mean anomaly into the same index space as the lookup table, then using
the integer part of the index to index into the table and the fractional
part to linearly interpolate.
Args:
e: The eccentricity of the orbit.
n: The number of mean anomaly values to use in the lookup table.
Returns:
A JIT-compiled function that takes M (as either a scalar or an array) and
returns E (as a scalar or an array).
"""
# Linearly spaced mean anomaly values
M_vals = jnp.linspace(0, two_pi, n, endpoint=False)
dM = M_vals[1] - M_vals[0]
# Use the E_solve function to get the real eccentric anomaly values
# and the derivative of E (with respect to the array indices)
E_vals = E_solve(M_vals, e)
dE_dM = 1 / (1 - e * jnp.cos(E_vals))
dE_dind = dE_dM * dM
inv_dM = 1.0 / dM
n_int = jnp.int32(n)
@jax.jit
def linear_interp(M):
M = jnp.mod(M, two_pi)
# Scale M to index space
ind_M = M * inv_dM
# Get integer part of the index
ind_M_int = ind_M.astype(jnp.int32)
# Get the fractional part of the index
dind = ind_M - ind_M_int
# Wrap to [0, 2pi) indices with modulo n
final_inds = ind_M_int % n_int
# Linear interpolation
return E_vals[final_inds] + dE_dind[final_inds] * dind
return linear_interp
[docs]
def E_hermite_interp(e, n=2048):
"""Create a JIT'd function to interpolate E for a fixed eccentricity.
This method uses Hermite interpolation to find the eccentric anomaly. Like
in the linear interpolation method, we turn most operations into integer
operations by scaling the mean anomaly into the same index space as the
lookup table, then using the integer part of the index to index into the
table and the fractional part to do Hermite interpolation.
Args:
e: The eccentricity of the orbit.
n: The number of mean anomaly values to use in the lookup table.
Returns:
A JIT-compiled function that takes M (as either a scalar or an array) and
returns E (as a scalar or an array).
"""
# Generate linearly spaced mean anomaly values
M_vals = jnp.linspace(0, two_pi, n, endpoint=False)
# Compute the step size in mean anomaly per index
dM = M_vals[1] - M_vals[0]
# Solve for eccentric anomaly for each mean anomaly
E_vals = E_solve(M_vals, e)
# Compute the derivative of E with respect to the index
dE_dM = 1 / (1 - e * jnp.cos(E_vals))
dE_dind = dE_dM * dM
inv_dM = 1.0 / dM
n_int = jnp.int32(n)
@jax.jit
def hermite_interp(M):
M = jnp.mod(M, two_pi)
# Scale M to index space
ind_M = M * inv_dM
# Get integer part of the index
ind_M_int = ind_M.astype(jnp.int32)
# Get the fractional part of the index
dind = ind_M - ind_M_int
# Wrap to [0, 2pi) indices with modulo n
final_inds = ind_M_int % n_int
final_inds_next = (final_inds + 1) % n_int
# Retrieve function values and derivatives at the surrounding indices
E_i = E_vals[final_inds]
# Unwrap across the 2pi seam: the "next" node of the last cell is
# node 0, whose E is ~0 -- add 2pi so the blend stays monotonic.
E_ip1 = E_vals[final_inds_next] + jnp.where(
final_inds_next < final_inds, two_pi, 0.0
)
dE_i = dE_dind[final_inds]
dE_ip1 = dE_dind[final_inds_next]
# Hermite basis functions
dind2 = dind * dind
dind3 = dind2 * dind
h00 = 2 * dind3 - 3 * dind2 + 1
h10 = dind3 - 2 * dind2 + dind
h01 = -2 * dind3 + 3 * dind2
h11 = dind3 - dind2
# Perform Hermite interpolation
E_interp = h00 * E_i + h10 * dE_i + h01 * E_ip1 + h11 * dE_ip1
return E_interp
return hermite_interp