Source code for parametric_ellipse

import jax

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


[docs] @jax.jit def poly_to_parametric_helper( 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, jax.Array, jax.Array, jax.Array]: """A helper function for :func:`poly_to_parametric`. Args: rho_xx (Array [Dimensionless]): Coefficient of x^2 rho_xy (Array [Dimensionless]): Coefficient of xy rho_x0 (Array [Dimensionless]): Coefficient of x rho_yy (Array [Dimensionless]): Coefficient of y^2 rho_y0 (Array [Dimensionless]): Coefficient of y rho_00 (Array [Dimensionless]): Constant term Returns: Tuple: - r1 (Array [Rstar]): Semi-major axis of the projected ellipse - r2 (Array [Rstar]): Semi-minor axis of the projected ellipse - xc (Array [Rstar]): x-coordinate of the center of the ellipse - yc (Array [Rstar]): y-coordinate of the center of the ellipse - cosa (Array [Dimensionless]): Cosine of the rotation angle - sina (Array [Dimensionless]): Sine of the rotation angle """ # (* base eq *) # pxx x^2 + pxy x y + px0 x + pyy y^2 + py0 y + p00 == 1 # (* normalize to get rid of p0 *) # pxx/(1 - p00) x^2 + pxy /(1 - p00) x y + px0/(1 - p00) x + # pyy/(1 - p00) y^2 + py0 /(1 - p00) y == 1 # (* solve for the ellipse center *) # CoefficientRules[ # pxx/(1 - p00) x^2 + pxy /(1 - p00) x y + px0/(1 - p00) x + # pyy/(1 - p00) y^2 + py0 /(1 - p00) y /. {x -> x - xc, # y -> y - yc}, {x, y}] # Solve[{px0/(1 - p00) - (2 pxx xc)/(1 - p00) - (pxy yc)/(1 - p00) == 0, # py0/(1 - p00) - (pxy xc)/(1 - p00) - (2 pyy yc)/(1 - p00) == # 0 }, {xc, yc}] # (* plug back in *) # Simplify[ # CoefficientRules[ # pxx/(1 - p00) x^2 + pxy /(1 - p00) x y + px0/(1 - p00) x + # pyy/(1 - p00) y^2 + py0 /(1 - p00) y /. {x -> x - xc, # y -> y - yc} /. {xc -> -((-pxy py0 + 2 px0 pyy)/( # pxy^2 - 4 pxx pyy)), # yc -> -((-px0 pxy + 2 pxx py0)/(pxy^2 - 4 pxx pyy))}, {x, y}]] # (* normalize again to get the final coeffs *) # pxxShift = # Simplify[(pxx/( # 1 - p00)) /(1 - ( # px0 pxy py0 - pxx py0^2 - # px0^2 pyy)/((-1 + p00) (pxy^2 - 4 pxx pyy)))] # pxyShift = # Simplify[(pxy/( # 1 - p00))/(1 - ( # px0 pxy py0 - pxx py0^2 - # px0^2 pyy)/((-1 + p00) (pxy^2 - 4 pxx pyy)))] # pyyShift = # Simplify[(pyy/( # 1 - p00)) /(1 - ( # px0 pxy py0 - pxx py0^2 - # px0^2 pyy)/((-1 + p00) (pxy^2 - 4 pxx pyy)))] # the center of the ellipse xc = (rho_xy * rho_y0 - 2 * rho_yy * rho_x0) / (4 * rho_xx * rho_yy - rho_xy**2) yc = (rho_xy * rho_x0 - 2 * rho_xx * rho_y0) / (4 * rho_xx * rho_yy - rho_xy**2) # get new coefficients for the centered ellipse: all others are zero now, # explicitly got rid of rho_00 so there's a lot more division rho_xx_shift = -( (rho_xx * (rho_xy**2 - 4 * rho_xx * rho_yy)) / ( (-1 + rho_00) * rho_xy**2 - rho_x0 * rho_xy * rho_y0 + rho_x0**2 * rho_yy + rho_xx * (rho_y0**2 + 4 * rho_yy - 4 * rho_00 * rho_yy) ) ) rho_xy_shift = (-(rho_xy**3) + 4 * rho_xx * rho_xy * rho_yy) / ( (-1 + rho_00) * rho_xy**2 - rho_x0 * rho_xy * rho_y0 + rho_x0**2 * rho_yy + rho_xx * (rho_y0**2 + 4 * rho_yy - 4 * rho_00 * rho_yy) ) rho_yy_shift = -( (rho_yy * (rho_xy**2 - 4 * rho_xx * rho_yy)) / ( (-1 + rho_00) * rho_xy**2 - rho_x0 * rho_xy * rho_y0 + rho_x0**2 * rho_yy + rho_xx * (rho_y0**2 + 4 * rho_yy - 4 * rho_00 * rho_yy) ) ) # get the rotation angle (edge case gives you nans if there's no rotation) theta = jnp.where( rho_xx_shift - rho_yy_shift != 0.0, 0.5 * jnp.arctan2(rho_xy_shift, (rho_xx_shift - rho_yy_shift)) + jnp.pi / 2, 0.0, ) theta = jnp.where(theta < 0.0, theta + jnp.pi, theta) # jax.debug.print("{x}", x=theta) cosa = jnp.cos(theta) sina = jnp.sin(theta) # get the semi-major and semi-minor axes a = ( rho_xx_shift * jnp.cos(theta) ** 2 + rho_xy_shift * jnp.cos(theta) * jnp.sin(theta) + rho_yy_shift * jnp.sin(theta) ** 2 ) b = ( rho_xx_shift * jnp.sin(theta) ** 2 - rho_xy_shift * jnp.cos(theta) * jnp.sin(theta) + rho_yy_shift * jnp.cos(theta) ** 2 ) r1 = 1 / jnp.sqrt(a) r2 = 1 / jnp.sqrt(b) return r1, r2, xc, yc, cosa, sina
[docs] @jax.jit def poly_to_parametric( rho_xx: jax.Array, rho_xy: jax.Array, rho_x0: jax.Array, rho_yy: jax.Array, rho_y0: jax.Array, rho_00: jax.Array, ) -> dict[str, jax.Array]: """Convert between the coefficients that describe an implicit to those defining a parametric ellipse. The input coefficients are those of the implicit ellipse equation: .. math:: \\rho_{xx} x^2 + \\rho_{xy} xy + \\rho_{x0} x + \\rho_{yy} y^2 + \\rho_{y0} y + \\rho_{00} = 1 Args: rho_xx (Array [Dimensionless]): Coefficient of x^2 rho_xy (Array [Dimensionless]): Coefficient of xy rho_x0 (Array [Dimensionless]): Coefficient of x rho_yy (Array [Dimensionless]): Coefficient of y^2 rho_y0 (Array [Dimensionless]): Coefficient of y rho_00 (Array [Dimensionless]): Constant term Returns: dict: Dictionary of coefficients for the parametric ellipse. The ellipse can now be described by the following parametric equations for parameter :math:`\\alpha`: .. math:: x = c_{x1} * \\cos(\\alpha) + c_{x2} * \\sin(\\alpha) + c_{x3} y = c_{y1} * \\cos(\\alpha) + c_{y2} * \\sin(\\alpha) + c_{y3} """ r1, r2, xc, yc, cosa, sina = poly_to_parametric_helper( rho_xx, rho_xy, rho_x0, rho_yy, rho_y0, rho_00 ) return { "c_x1": r1 * cosa, "c_x2": -r2 * sina, "c_x3": xc, "c_y1": r1 * sina, "c_y2": r2 * cosa, "c_y3": yc, }
[docs] @jax.jit def parametric_to_poly( c_x1: jax.Array, c_x2: jax.Array, c_x3: jax.Array, c_y1: jax.Array, c_y2: jax.Array, c_y3: jax.Array, ) -> dict[str, jax.Array]: """Convert the coefficients of a parametric ellipse to those of an implicit one. This is the inverse of :func:`poly_to_parametric`. The input coefficients describe the ellipse as a parametric curve for parameter :math:`\\alpha`: .. math:: x = c_{x1} * \\cos(\\alpha) + c_{x2} * \\sin(\\alpha) + c_{x3} y = c_{y1} * \\cos(\\alpha) + c_{y2} * \\sin(\\alpha) + c_{y3} Note that the returned coefficients involve division by the squared determinant of the parametric coefficient matrix, so they blow up for nearly-degenerate (nearly zero-area) ellipses. Prefer keeping such curves in parametric form. Args: c_x1 (Array [Rstar]): Coefficient in the parametric equation c_x2 (Array [Rstar]): Coefficient in the parametric equation c_x3 (Array [Rstar]): Coefficient in the parametric equation c_y1 (Array [Rstar]): Coefficient in the parametric equation c_y2 (Array [Rstar]): Coefficient in the parametric equation c_y3 (Array [Rstar]): Coefficient in the parametric equation Returns: dict: Dictionary of coefficients for the implicit ellipse equation: .. math:: \\rho_{xx} x^2 + \\rho_{xy} xy + \\rho_{x0} x + \\rho_{yy} y^2 + \\rho_{y0} y + \\rho_{00} = 1 """ rho_xx = (c_y1**2 + c_y2**2) / (c_x2 * c_y1 - c_x1 * c_y2) ** 2 rho_xy = (-2 * (c_x1 * c_y1 + c_x2 * c_y2)) / (c_x2 * c_y1 - c_x1 * c_y2) ** 2 rho_x0 = ( -2 * c_x3 * (c_y1**2 + c_y2**2) + 2 * (c_x1 * c_y1 + c_x2 * c_y2) * c_y3 ) / (c_x2 * c_y1 - c_x1 * c_y2) ** 2 rho_yy = (c_x1**2 + c_x2**2) / (c_x2 * c_y1 - c_x1 * c_y2) ** 2 rho_y0 = ( 2 * c_x3 * (c_x1 * c_y1 + c_x2 * c_y2) - 2 * (c_x1**2 + c_x2**2) * c_y3 ) / (c_x2 * c_y1 - c_x1 * c_y2) ** 2 rho_00 = ( c_x3**2 * (c_y1**2 + c_y2**2) - 2 * c_x3 * (c_x1 * c_y1 + c_x2 * c_y2) * c_y3 + (c_x1**2 + c_x2**2) * c_y3**2 ) / (c_x2 * c_y1 - c_x1 * c_y2) ** 2 coeffs = { "rho_xx": rho_xx, "rho_xy": rho_xy, "rho_x0": rho_x0, "rho_yy": rho_yy, "rho_y0": rho_y0, "rho_00": rho_00, } # rho_xx, rho_xy, and rho_yy don't involve c_x3 or c_y3 and can end up as scalars # even when the center coordinates carry a time axis if coeffs["rho_xx"].shape != coeffs["rho_x0"].shape: coeffs["rho_xx"] = jnp.ones_like(coeffs["rho_x0"]) * coeffs["rho_xx"] coeffs["rho_xy"] = jnp.ones_like(coeffs["rho_x0"]) * coeffs["rho_xy"] coeffs["rho_yy"] = jnp.ones_like(coeffs["rho_x0"]) * coeffs["rho_yy"] return coeffs
[docs] @jax.jit def point_in_ellipse( x: jax.Array, y: jax.Array, c_x1: jax.Array, c_x2: jax.Array, c_x3: jax.Array, c_y1: jax.Array, c_y2: jax.Array, c_y3: jax.Array, ) -> jax.Array: """Test whether a point is strictly inside a parametrically-defined ellipse. Solves the 2x2 linear system for :math:`(\\cos\\alpha, \\sin\\alpha)` via the adjugate, so the test involves no division and remains well-conditioned even for very thin ellipses (unlike evaluating the implicit form, whose coefficients scale as the inverse squared semi-minor axis). A degenerate (zero-area) ellipse has an empty interior and always returns False. Args: x (Array [Rstar]): x-coordinate(s) of the point(s) to test y (Array [Rstar]): y-coordinate(s) of the point(s) to test c_x1 (Array [Rstar]): Coefficient in the parametric equation c_x2 (Array [Rstar]): Coefficient in the parametric equation c_x3 (Array [Rstar]): Coefficient in the parametric equation c_y1 (Array [Rstar]): Coefficient in the parametric equation c_y2 (Array [Rstar]): Coefficient in the parametric equation c_y3 (Array [Rstar]): Coefficient in the parametric equation Returns: Array [Dimensionless]: Boolean array, True where the point is inside the ellipse. """ det = c_x1 * c_y2 - c_x2 * c_y1 dx = x - c_x3 dy = y - c_y3 # adjugate solve: cos(alpha) = (c_y2*dx - c_x2*dy)/det, # sin(alpha) = (c_x1*dy - c_y1*dx)/det; inside iff cos^2 + sin^2 < 1, # multiplied through by det^2 to avoid the division entirely cos_term = c_y2 * dx - c_x2 * dy sin_term = c_x1 * dy - c_y1 * dx return cos_term**2 + sin_term**2 < det**2
[docs] @jax.jit def cartesian_intersection_to_parametric_angle( xs: jax.Array, ys: jax.Array, c_x1: jax.Array, c_x2: jax.Array, c_x3: jax.Array, c_y1: jax.Array, c_y2: jax.Array, c_y3: jax.Array, ) -> jax.Array: """Given a set of x and y coordinates corresponding to the intersection of the planet and star, compute the angle :math:`\\alpha` that corresponds to each point. Here, :math:`\\alpha` is the parameter in the parametric equations of the ellipse. See :func:`poly_to_parametric` for more details. Args: xs (Array [Rstar]): x-coordinates of the intersection points ys (Array [Rstar]): y-coordinates of the intersection points c_x1 (Array [Dimensionless]): Coefficient of x^2 c_x2 (Array [Dimensionless]): Coefficient of xy c_x3 (Array [Dimensionless]): Coefficient of x c_y1 (Array [Dimensionless]): Coefficient of y^2 c_y2 (Array [Dimensionless]): Coefficient of y c_y3 (Array [Dimensionless]): Constant term Returns: Array [Rstar]: The angle :math:`\\alpha` corresponding to each intersection point """ # center the ellipse xs -= c_x3 ys -= c_y3 # the x, y positions are now linear combinations of just cosa, sina # linear solve for those inv = jnp.linalg.inv(jnp.array([[c_x1, c_x2], [c_y1, c_y2]])) matrix = jax.vmap(lambda x, y: jnp.matmul(inv, jnp.array([x, y])))(xs, ys) cosa = matrix[:, 0] sina = matrix[:, 1] # convert to alpha alpha = jnp.arctan2(sina, cosa) return alpha