Source code for orbix.observatory.keepout

"""Sun/Earth keepout zone checks.

Determines whether a target star is observable based on angular separation
from bright solar system bodies.  Port of the keepout logic from
``EXOSIMS.Prototypes.Observatory.keepout``, simplified to Sun + Earth (the
Moon is dropped as negligible against the L2 geometry; see ``is_observable``).
"""

from __future__ import annotations

import jax.numpy as jnp
from jaxtyping import Array

from orbix.observatory.solar_system import (
    earth_position_ecliptic,
    radec_to_ecliptic,
)


[docs] def _unit_vector(v: jnp.ndarray) -> jnp.ndarray: """Normalize vector to unit length.""" return v / jnp.maximum(jnp.linalg.norm(v), 1e-30)
[docs] def _angular_sep(v1: jnp.ndarray, v2: jnp.ndarray) -> float: """Angular separation between two direction vectors (radians).""" u1 = _unit_vector(v1) u2 = _unit_vector(v2) cos_a = jnp.clip(jnp.dot(u1, u2), -1.0, 1.0) return jnp.arccos(cos_a)
[docs] def _target_ecliptic_dir(ra_rad: float, dec_rad: float, mjd: float) -> jnp.ndarray: """Unit vector toward a target in heliocentric ecliptic frame.""" lam, beta = radec_to_ecliptic(ra_rad, dec_rad, mjd) return jnp.array( [ jnp.cos(beta) * jnp.cos(lam), jnp.cos(beta) * jnp.sin(lam), jnp.sin(beta), ] )
[docs] def body_angle( obs_pos_eclip: jnp.ndarray, body_pos_eclip: jnp.ndarray, ra_rad: float, dec_rad: float, mjd: float, ) -> float: """Angle between a solar system body and a target as seen from the observatory. Args: obs_pos_eclip: Observatory heliocentric ecliptic position (AU). body_pos_eclip: Body heliocentric ecliptic position (AU). ra_rad: Target RA in radians. dec_rad: Target Dec in radians. mjd: MJD for coordinate conversion. Returns: Angular separation in radians. """ # Direction from observer to body body_dir = body_pos_eclip - obs_pos_eclip # Direction from observer to target (at infinity) target_dir = _target_ecliptic_dir(ra_rad, dec_rad, mjd) return _angular_sep(body_dir, target_dir)
[docs] def is_observable( obs_pos_eclip: jnp.ndarray, ra_rad: float, dec_rad: float, mjd: float, ko_sun_min_deg: float = 45.0, ko_sun_max_deg: float = 180.0, ko_earth_min_deg: float = 0.0, ko_earth_max_deg: float = 180.0, ) -> Array: """Check if a target is observable (outside all keepout zones). Args: obs_pos_eclip: Observatory heliocentric ecliptic position (AU), shape ``(3,)``. ra_rad: Target right ascension in radians. dec_rad: Target declination in radians. mjd: Modified Julian Date. ko_sun_min_deg: Minimum Sun keepout angle (degrees). ko_sun_max_deg: Maximum Sun keepout angle (degrees). ko_earth_min_deg: Minimum Earth keepout angle (degrees). ko_earth_max_deg: Maximum Earth keepout angle (degrees). Returns: True if the target is observable, False if it falls in a keepout zone. """ # Sun is at origin in heliocentric frame sun_pos = jnp.zeros(3) sun_angle_rad = body_angle(obs_pos_eclip, sun_pos, ra_rad, dec_rad, mjd) sun_angle_deg = jnp.degrees(sun_angle_rad) sun_ok = (sun_angle_deg >= ko_sun_min_deg) & (sun_angle_deg <= ko_sun_max_deg) # Earth earth_pos = earth_position_ecliptic(mjd) earth_angle_rad = body_angle(obs_pos_eclip, earth_pos, ra_rad, dec_rad, mjd) earth_angle_deg = jnp.degrees(earth_angle_rad) earth_ok = (earth_angle_deg >= ko_earth_min_deg) & ( earth_angle_deg <= ko_earth_max_deg ) # The Moon is ~0.0026 AU from Earth -- negligible against the L2 # geometry, so the Earth check stands in for it. Add explicit lunar # keepout only with a lunar ephemeris and its own angle limits. return sun_ok & earth_ok