Source code for rings

import jax

jax.config.update("jax_enable_x64", True)
import jax.numpy as jnp


[docs] @jax.jit def para_eval(alpha: jax.Array, coeffs: dict[str, jax.Array]) -> tuple: """Evaluate a parametric ellipse at the given angle(s). Args: alpha (Array [Radian]): The parametric angle(s) at which to evaluate the curve. coeffs (dict): Dictionary of the parametric coefficients c_x1 through c_y3. Returns: Tuple: The x and y coordinates of the curve at each alpha. """ return ( coeffs["c_x1"] * jnp.cos(alpha) + coeffs["c_x2"] * jnp.sin(alpha) + coeffs["c_x3"], coeffs["c_y1"] * jnp.cos(alpha) + coeffs["c_y2"] * jnp.sin(alpha) + coeffs["c_y3"], )
[docs] @jax.jit def ring_para_coeffs( a: jax.Array, e: jax.Array, f: jax.Array, Omega: jax.Array, i: jax.Array, omega: jax.Array, rRing: jax.Array, ring_obliq: jax.Array, ring_prec: jax.Array, **kwargs: jax.Array, ) -> dict[str, jax.Array]: """ Compute the coefficients describing the parametric form of a ring in the sky plane. Remember that ring_obliq and ring_prec are defined *in the planet's orbital plane, at f=0*. That implies that to get a face-on ring, you need ring_obliq=ring_prec=90 deg. Some other examples all at inc=90 deg: - ring_obliq=0, ring_prec=0: the ring is an edge-on horizontal line - ring_obliq=jnp.pi/4, ring_prec=0: the ring is still a line, but now it's tilted 45 degrees in the sky frame. You've tipped the north pole away from the star. - ring_obliq=0, ring_prec=anything: the ring is still a line - ring_obliq=90, ring_prec=0: the ring is a face-on circle Making inc != 90 deg will alter these: the angles are defined in the orbital plane, so if you tilt the orbit away from face-on, you'll also tilt the ring away from face-on. The returned coefficients are always oriented so that the curve is traversed counterclockwise on the sky as the parametric angle increases, as required for the Green's theorem boundary integrals. Args: a (Array [Rstar]): The semi-major axis of the planet. e (Array [Dimensionless]): The eccentricity of the planet. f (Array [Radian]): The true anomaly of the planet. Omega (Array [Radian]): The longitude of the ascending node of the planet. i (Array [Radian]): The inclination of the planet. omega (Array [Radian]): The argument of periapsis of the planet. rRing (Array [Rstar]): The radius of the ring. ring_obliq (Array [Radian]): The obliquity of the ring. ring_prec (Array [Radian]): The precession angle of the ring. **kwargs: Additional (unused) keyword arguments. Returns: dict: A dictionary with keys for each of the coefficients of the parametric form of the ring. """ cx1 = rRing * ( -(jnp.sin(i) * jnp.sin(ring_obliq) * jnp.sin(Omega)) - jnp.cos(ring_obliq) * jnp.sin(ring_prec) * ( jnp.cos(Omega) * jnp.sin(omega) + jnp.cos(i) * jnp.cos(omega) * jnp.sin(Omega) ) + jnp.cos(ring_prec) * jnp.cos(ring_obliq) * ( jnp.cos(omega) * jnp.cos(Omega) - jnp.cos(i) * jnp.sin(omega) * jnp.sin(Omega) ) ) cx2 = -( rRing * ( jnp.cos(omega) * ( jnp.cos(Omega) * jnp.sin(ring_prec) + jnp.cos(i) * jnp.cos(ring_prec) * jnp.sin(Omega) ) + jnp.sin(omega) * ( jnp.cos(ring_prec) * jnp.cos(Omega) - jnp.cos(i) * jnp.sin(ring_prec) * jnp.sin(Omega) ) ) ) cx3 = ( a * (-1 + e**2) * ( jnp.sin(f) * ( jnp.cos(Omega) * jnp.sin(omega) + jnp.cos(i) * jnp.cos(omega) * jnp.sin(Omega) ) + jnp.cos(f) * ( -(jnp.cos(omega) * jnp.cos(Omega)) + jnp.cos(i) * jnp.sin(omega) * jnp.sin(Omega) ) ) ) / (1 + e * jnp.cos(f)) cy1 = rRing * ( jnp.cos(Omega) * ( jnp.sin(i) * jnp.sin(ring_obliq) + jnp.cos(i) * jnp.cos(ring_obliq) * jnp.sin(ring_prec + omega) ) + jnp.cos(ring_obliq) * jnp.cos(ring_prec + omega) * jnp.sin(Omega) ) cy2 = rRing * ( jnp.cos(i) * jnp.cos(ring_prec + omega) * jnp.cos(Omega) - jnp.sin(ring_prec + omega) * jnp.sin(Omega) ) cy3 = -( ( a * (-1 + e**2) * ( jnp.cos(i) * jnp.cos(Omega) * jnp.sin(f + omega) + jnp.cos(f + omega) * jnp.sin(Omega) ) ) / (1 + e * jnp.cos(f)) ) # the raw parameterization's traversal direction flips sign with viewing # geometry; enforce counterclockwise (positive determinant) by reversing the # direction (alpha -> -alpha, i.e. flipping the sin terms) where needed det = cx1 * cy2 - cx2 * cy1 cx2 = jnp.where(det < 0, -cx2, cx2) cy2 = jnp.where(det < 0, -cy2, cy2) coeffs = { "c_x1": cx1, "c_x2": cx2, "c_x3": cx3, "c_y1": cy1, "c_y2": cy2, "c_y3": cy3, } if coeffs["c_x1"].shape != coeffs["c_x3"].shape: coeffs["c_x1"] = jnp.ones_like(coeffs["c_x3"]) * coeffs["c_x1"] coeffs["c_x2"] = jnp.ones_like(coeffs["c_x3"]) * coeffs["c_x2"] coeffs["c_y1"] = jnp.ones_like(coeffs["c_x3"]) * coeffs["c_y1"] coeffs["c_y2"] = jnp.ones_like(coeffs["c_x3"]) * coeffs["c_y2"] return coeffs
[docs] def ring_prelude(state: dict) -> tuple[dict, dict]: """Per-timestep parametric coefficients for both ring edges. Expects ``state["f"]`` to already hold the per-timestep true anomalies (i.e. run :func:`polynomial_limb_darkened_transit._lightcurve_setup` first). When the ring orientation tracks the planet's (``ring_tracks_planet``), a tidally locked planet drags the ring precession angle along with the true anomaly, mirroring the ``prec`` override in :func:`polynomial_limb_darkened_transit.outline_prelude`. Args: state (dict): A :func:`RingedSystem` ``state`` dictionary. Uses the orbital elements plus ``ring_inner_r``, ``ring_outer_r``, ``ring_obliq``, ``ring_prec``, ``ring_tracks_planet``, and ``tidally_locked``. Returns: Tuple: Two dictionaries of parametric coefficients (each broadcast to the time axis): the outer ring edge and the inner ring edge. """ # rings that track the planet take their orientation from the planet's obliq/prec # at call time, so parameter updates passed to lightcurve() stay consistent ring_obliq = jnp.where( state["ring_tracks_planet"], state["obliq"], state["ring_obliq"] ) ring_prec = jnp.where( state["ring_tracks_planet"], state["prec"], state["ring_prec"] ) ring_prec = jnp.where( jnp.logical_and(state["tidally_locked"], state["ring_tracks_planet"]), state["f"], ring_prec, ) shared = dict( a=state["a"], e=state["e"], f=state["f"], Omega=state["Omega"], i=state["i"], omega=state["omega"], ring_obliq=ring_obliq, ring_prec=ring_prec, ) para_outer = ring_para_coeffs(rRing=state["ring_outer_r"], **shared) para_inner = ring_para_coeffs(rRing=state["ring_inner_r"], **shared) return para_outer, para_inner
[docs] @jax.jit def parametric_conic_intersections( c_x1: jax.Array, c_x2: jax.Array, c_x3: jax.Array, c_y1: jax.Array, c_y2: jax.Array, c_y3: jax.Array, rho_xx: jax.Array, rho_xy: jax.Array, rho_x0: jax.Array, rho_yy: jax.Array, rho_y0: jax.Array, rho_00: jax.Array, ) -> tuple[jax.Array, jax.Array, jax.Array]: """Intersections between a parametric ellipse and an implicitly-defined conic. This is a more general version of :func:`_single_intersection_points` in `polynomial_limb_darkened_transit`, which assumes the implicit curve is the unit circle (the star). Substituting the parametric curve into the implicit equation via the tangent half-angle substitution :math:`t = \\tan(\\alpha/2)` yields a quartic in t. The parametric curve should be the one at risk of degeneracy (e.g. a nearly edge-on ring): its coefficients stay bounded, whereas the implicit form of a thin ellipse has coefficients that diverge as the inverse squared semi-minor axis. The implicit coefficients satisfy the following on the conic: .. math:: \\rho_{xx} x^2 + \\rho_{xy} xy + \\rho_{x0} x + \\rho_{yy} y^2 + \\rho_{y0} y + \\rho_{00} = 1 And the c coefficients describe the parametric curve: .. math:: x = c_{x1} \\cos(\\alpha) + c_{x2} \\sin(\\alpha) + c_{x3} y = c_{y1} \\cos(\\alpha) + c_{y2} \\sin(\\alpha) + c_{y3} Args: c_x1 (Array [Rstar]): Coefficient in the parametric form of the first curve. c_x2 (Array [Rstar]): Coefficient in the parametric form of the first curve. c_x3 (Array [Rstar]): Coefficient in the parametric form of the first curve. c_y1 (Array [Rstar]): Coefficient in the parametric form of the first curve. c_y2 (Array [Rstar]): Coefficient in the parametric form of the first curve. c_y3 (Array [Rstar]): Coefficient in the parametric form of the first curve. rho_xx (Array [Dimensionless]): Coefficient in the implicit form of the second curve. rho_xy (Array [Dimensionless]): Coefficient in the implicit form of the second curve. rho_x0 (Array [Dimensionless]): Coefficient in the implicit form of the second curve. rho_yy (Array [Dimensionless]): Coefficient in the implicit form of the second curve. rho_y0 (Array [Dimensionless]): Coefficient in the implicit form of the second curve. rho_00 (Array [Dimensionless]): Coefficient in the implicit form of the second curve. Returns: Tuple: Three arrays of length 4: the parametric angles alpha of the intersection points on the first curve (in (-pi, pi]), and the x and y coordinates of those points. Slots corresponding to imaginary quartic roots (no intersection) are filled with 999. """ t4 = ( -1 + rho_00 + c_x1**2 * rho_xx + c_x3**2 * rho_xx + c_x3 * (rho_x0 + (-c_y1 + c_y3) * rho_xy) - c_x1 * (rho_x0 + 2 * c_x3 * rho_xx + (-c_y1 + c_y3) * rho_xy) - c_y1 * rho_y0 + c_y3 * rho_y0 + (c_y1 - c_y3) ** 2 * rho_yy ) t3 = 2 * ( c_x2 * ( rho_x0 - 2 * c_x1 * rho_xx + 2 * c_x3 * rho_xx - c_y1 * rho_xy + c_y3 * rho_xy ) + c_y2 * ( -(c_x1 * rho_xy) + c_x3 * rho_xy + rho_y0 - 2 * c_y1 * rho_yy + 2 * c_y3 * rho_yy ) ) t2 = 2 * ( -1 + rho_00 - c_x1**2 * rho_xx + c_x3**2 * rho_xx - c_x1 * c_y1 * rho_xy + 2 * c_x2 * (c_x2 * rho_xx + c_y2 * rho_xy) + c_x3 * (rho_x0 + c_y3 * rho_xy) + c_y3 * rho_y0 + (-(c_y1**2) + 2 * c_y2**2 + c_y3**2) * rho_yy ) t1 = 2 * ( c_x2 * (rho_x0 + 2 * (c_x1 + c_x3) * rho_xx + (c_y1 + c_y3) * rho_xy) + c_y2 * ((c_x1 + c_x3) * rho_xy + rho_y0 + 2 * (c_y1 + c_y3) * rho_yy) ) t0 = ( -1 + rho_00 + c_x1**2 * rho_xx + c_x3**2 * rho_xx + c_x3 * (rho_x0 + (c_y1 + c_y3) * rho_xy) + c_x1 * (rho_x0 + 2 * c_x3 * rho_xx + (c_y1 + c_y3) * rho_xy) + (c_y1 + c_y3) * (rho_y0 + (c_y1 + c_y3) * rho_yy) ) polys = jnp.array([t4, t3, t2, t1, t0]) roots = jnp.roots(polys, strip_zeros=False) # strip_zeros must be False to jit ts = jnp.where(jnp.imag(roots) == 0, jnp.real(roots), 999) alphas = jnp.where(ts != 999, 2 * jnp.arctan(ts), 999) cos_t = (1 - ts**2) / (1 + ts**2) sin_t = 2 * ts / (1 + ts**2) xs = jnp.where(ts != 999, c_x1 * cos_t + c_x2 * sin_t + c_x3, ts) ys = jnp.where(ts != 999, c_y1 * cos_t + c_y2 * sin_t + c_y3, ts) return alphas, xs, ys