diff --git a/CHANGELOG.md b/CHANGELOG.md index 2a34a529..14037086 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,10 @@ Only recent releases are listed. Older entries are in this file's git history (` - `update_datafiles()` no longer downloads files that are compiled into the library: the IERS tables and gravity models are `default: false` in the manifest (still pinned and fetchable by name), and the unused `leap-seconds.list` (nothing ever read it — the runtime leap-second table is a compiled-in constant) is removed from the manifest entirely. The only static download left is the JPL ephemeris, alongside the daily EOP / space-weather / solar-cycle refreshes ([#163](https://github.com/ssmichael1/satkit/pull/163)) +### Docs + +- Every Python docstring (`.pyi` stubs and runtime `__doc__`) states units of measure for dimensioned arguments, returns and attributes (SI: meters, m/s, m/s², radians unless the name ends in `_deg`); `sgp4` outputs explicitly noted as meters / m/s in the TEME frame, not the km / km/s of other SGP4 libraries ([#169](https://github.com/ssmichael1/satkit/pull/169)) + ## 0.21.2 - 2026-08-30 ### Changed diff --git a/python/satkit/frametransform.pyi b/python/satkit/frametransform.pyi index f95ab6ac..27ffe416 100644 --- a/python/satkit/frametransform.pyi +++ b/python/satkit/frametransform.pyi @@ -700,7 +700,8 @@ def itrf_to_gcrf_state( Returns: A 2-tuple ``(pos_gcrf, vel_gcrf)`` of numpy arrays with the - state expressed in GCRF. + state expressed in GCRF: position in meters, velocity in m/s + (shape ``(3,)`` each, or ``(N, 3)`` for batched input). Example: ```python @@ -744,7 +745,8 @@ def gcrf_to_itrf_state( Returns: A 2-tuple ``(pos_itrf, vel_itrf)`` where ``vel_itrf`` is the - velocity as observed in ITRF. + velocity as observed in ITRF: position in meters, velocity in m/s + (shape ``(3,)`` each, or ``(N, 3)`` for batched input). """ ... @@ -760,6 +762,14 @@ def itrf_to_gcrf_state_approx( 2010 precision is not required. Neglects polar motion, so the Earth-rotation sweep ``omega_earth x r`` is evaluated in ITRF directly. Accepts scalar or batched inputs like :func:`itrf_to_gcrf_state`. + + Args: + pos_itrf: ``(3,)`` or ``(N, 3)`` position vector in ITRF, meters + vel_itrf: ``(3,)`` or ``(N, 3)`` velocity vector as observed in ITRF, m/s + time: Epoch of the state (length-``N`` array/list for batched input) + + Returns: + A 2-tuple ``(pos_gcrf, vel_gcrf)``: position in meters, velocity in m/s """ ... @@ -771,6 +781,15 @@ def gcrf_to_itrf_state_approx( """Approximate GCRF → ITRF state transform using the IAU-76/FK5 reduction. Inverse of :func:`itrf_to_gcrf_state_approx`; accurate to ~1 arcsec on position. Accepts scalar or batched inputs. + + Args: + pos_gcrf: ``(3,)`` or ``(N, 3)`` position vector in GCRF, meters + vel_gcrf: ``(3,)`` or ``(N, 3)`` velocity vector in GCRF, m/s + time: Epoch of the state (length-``N`` array/list for batched input) + + Returns: + A 2-tuple ``(pos_itrf, vel_itrf)``: position in meters, velocity + as observed in ITRF in m/s """ ... @@ -946,7 +965,7 @@ def transform_state( vel: 3-element velocity vector [m/s] Returns: - ``(pos, vel)`` in ``to_frame``. + ``(pos, vel)`` in ``to_frame``: position in meters, velocity in m/s. """ ... @@ -959,5 +978,15 @@ def transform_state_approx( ) -> tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: """State transform using the IAU-76/FK5 approximate reduction. Same supported-pair set as :func:`transform_state`. + + Args: + from_frame: Source frame + to_frame: Destination frame + tm: Epoch + pos: 3-element position vector, meters + vel: 3-element velocity vector, m/s + + Returns: + ``(pos, vel)`` in ``to_frame``: position in meters, velocity in m/s. """ ... diff --git a/python/satkit/moon.pyi b/python/satkit/moon.pyi index e31ceda3..08c4078d 100644 --- a/python/satkit/moon.pyi +++ b/python/satkit/moon.pyi @@ -14,31 +14,34 @@ from .satkit import TimeScalar, TimeArrayLike, TimeInput class moonphase: """ Enum representing moon phases + + Each value covers a range of the moon phase angle (see :func:`phase`), + given here in degrees. """ NewMoon: ClassVar[moonphase] - """New Moon (0 - 22.5)""" + """New Moon (phase angle 0 - 22.5 degrees, or 337.5 - 360 degrees)""" WaxingCrescent: ClassVar[moonphase] - """Waxing Crescent (22.5 - 67.5)""" + """Waxing Crescent (phase angle 22.5 - 67.5 degrees)""" FirstQuarter: ClassVar[moonphase] - """First Quarter (67.5 - 112.5)""" + """First Quarter (phase angle 67.5 - 112.5 degrees)""" WaxingGibbous: ClassVar[moonphase] - """Waxing Gibbous (112.5 - 157.5)""" + """Waxing Gibbous (phase angle 112.5 - 157.5 degrees)""" FullMoon: ClassVar[moonphase] - """Full Moon (157.5 - 202.5)""" + """Full Moon (phase angle 157.5 - 202.5 degrees)""" WaningGibbous: ClassVar[moonphase] - """Waning Gibbous (202.5 - 247.5)""" + """Waning Gibbous (phase angle 202.5 - 247.5 degrees)""" LastQuarter: ClassVar[moonphase] - """Last Quarter (247.5 - 292.5)""" + """Last Quarter (phase angle 247.5 - 292.5 degrees)""" WaningCrescent: ClassVar[moonphase] - """Waning Crescent (292.5 - 337.5)""" + """Waning Crescent (phase angle 292.5 - 337.5 degrees)""" @typing.overload def pos_gcrf(time: TimeScalar) -> npt.NDArray[np.float64]: @@ -100,7 +103,7 @@ def illumination(time: TimeScalar) -> float: time (satkit.time | datetime.datetime): scalar time at which to compute illumination Returns: - float: fractional illumination of moon at the given time + float: fractional illumination of moon at the given time, unitless, range 0.0 to 1.0 Example: ```python @@ -120,7 +123,7 @@ def illumination(time: TimeArrayLike) -> list[float]: time (TimeArrayLike): list or numpy array of times at which to compute illumination Returns: - list[float]: fractional illumination of moon at each given time + list[float]: fractional illumination of moon at each given time, unitless, range 0.0 to 1.0 """ ... diff --git a/python/satkit/satkit.pyi b/python/satkit/satkit.pyi index 3a253488..f95f5a4f 100644 --- a/python/satkit/satkit.pyi +++ b/python/satkit/satkit.pyi @@ -202,12 +202,12 @@ class TLE: @property def eccen(self) -> float: - """Satellite eccentricity, in range [0,1]""" + """Satellite eccentricity, unitless, in range [0,1]""" ... @eccen.setter def eccen(self, value: float) -> None: - """Set the satellite eccentricity""" + """Set the satellite eccentricity, unitless, in range [0,1]""" ... @property @@ -217,7 +217,7 @@ class TLE: @mean_anomaly.setter def mean_anomaly(self, value: float) -> None: - """Set the satellite mean anomaly""" + """Set the satellite mean anomaly, degrees""" ... @property @@ -227,7 +227,7 @@ class TLE: @mean_motion.setter def mean_motion(self, value: float) -> None: - """Set the satellite mean motion""" + """Set the satellite mean motion, revs / day""" ... @property @@ -301,7 +301,7 @@ class TLE: @property def bstar(self) -> float: - """Drag of the satellite + """Drag term (B*) of the satellite, in units of 1 / Earth radii should be rho0 * Cd * A / 2 / m @@ -312,7 +312,7 @@ class TLE: @bstar.setter def bstar(self, value: float) -> None: - """Set the drag of the satellite""" + """Set the drag term (B*) of the satellite, in units of 1 / Earth radii""" ... def to_2line(self) -> list[str]: @@ -415,7 +415,7 @@ def sgp4( Args: tle (TLE | list[TLE] | dict): TLE or OMM (or list of TLES) on which to operate - tm (time | list[time] | list[datetime.datetime] | npt.ArrayLike[time] | npt.ArrayLike[datetime.datetime]): time(s) at which to compute position and velocity + time (time | list[time] | list[datetime.datetime] | npt.ArrayLike[time] | npt.ArrayLike[datetime.datetime]): time(s) at which to compute position and velocity Keyword Args: gravconst (satkit.sgp4_gravconst): gravity constant to use. Default is gravconst.wgs72 @@ -425,13 +425,20 @@ def sgp4( (this may also flag a typing error ... I can't figure out how to get rid of it) Returns: - position and velocity - in meters and meters/second, respectively, + tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: position and velocity + in **meters** and **meters/second**, respectively, in the TEME frame at each of the "Ntime" input times and each of the "Ntle" tles. + Shape is (3,) for a single TLE and single time, (Ntime, 3) for a single TLE + and multiple times, (Ntle, 3) for a list of TLEs and a single time, and + (Ntle, Ntime, 3) for a list of TLEs and multiple times. Additional return value if errflag is True: list[sgp4_error] with error conditions for each TLE and time output. Notes: + - **Units:** the canonical Vallado SGP4 implementation (and most other SGP4 + libraries) return position in kilometers and velocity in kilometers/second. + satkit converts these to meters and meters/second so that SGP4 output is + consistent with every other position and velocity in the library. - Now supports propagation of OMM (Orbital Mean-Element Message) dictionaries. The dictionaries must follow the structure used by or @@ -1114,7 +1121,7 @@ class time: """Return a time object representing input Julian date and time scale Args: - jd (float): Julian date + jd (float): Julian date, days scale (timescale, optional): Time scale. Default is satkit.timescale.UTC Returns: @@ -1154,7 +1161,7 @@ class time: Args: week: GPS week number - sec: GPS seconds of week + seconds: GPS seconds of week, seconds Returns: Time object representing input GPS week and second @@ -1184,7 +1191,7 @@ class time: """Return a time object representing input modified Julian date and time scale Args: - mjd (float): Modified Julian date + mjd (float): Modified Julian date, days scale (satkit.timescale, optional): Time scale. Default is satkit.timescale.UTC Returns: @@ -1329,6 +1336,9 @@ class time: with the provided time scale If no time scale is provided, default is satkit.timescale.UTC + + Returns: + float: Modified Julian Date, days """ ... @@ -1338,6 +1348,9 @@ class time: the provided time scale If no time scale is provided, default is satkit.timescale.UTC + + Returns: + float: Julian Date, days """ ... @@ -1348,6 +1361,9 @@ class time: (seconds since Jan 1, 1970 UTC, excluding leap seconds) Includes fractional component of seconds + + Returns: + float: Unix time, seconds """ ... @@ -1999,14 +2015,14 @@ class quaternion: @staticmethod def from_axis_angle(axis: npt.NDArray[np.float64], angle: float) -> quaternion: - """Quaternion representing right-handed rotation of vector by "angle" degrees about the given axis + """Quaternion representing right-handed rotation of vector by "angle" radians about the given axis Args: - axis (npt.ArrayLike[np.float64]): 3-element array representing axis of rotation + axis (npt.ArrayLike[np.float64]): 3-element array representing axis of rotation (unitless direction; need not be normalized) angle (float): angle of rotation in radians Returns: - Quaternion representing rotation by "angle" degrees about the given axis + Quaternion representing rotation by "angle" radians about the given axis """ ... @@ -2760,7 +2776,7 @@ class itrfcoord: (T. Vincenty, Survey Review 23(176), 1975, ) Returns: - (distance in meters, initial heading in radians, heading at destination in radians) + itrfcoord: New ITRF coordinate after moving ``distance`` meters along the geodesic Example: ```python @@ -2780,7 +2796,7 @@ class consts: """WGS-84 semiparameter, in meters""" wgs84_f: ClassVar[float] - """WGS-84 flattening in meters""" + """WGS-84 flattening, unitless""" earth_radius: ClassVar[float] """Earth radius along major axis, meters""" @@ -2882,7 +2898,8 @@ class satstate: time (satkit.time): Epoch of the state pos (npt.NDArray[np.float64]): Position in meters, GCRF frame vel (npt.NDArray[np.float64]): Velocity in m/s, GCRF frame - cov (npt.NDArray[np.float64]|None, optional): 6x6 covariance matrix in GCRF. Defaults to None. + cov (npt.NDArray[np.float64]|None, optional): 6x6 covariance matrix in GCRF + (position block m^2, velocity block (m/s)^2, cross blocks m^2/s). Defaults to None. Example: ```python @@ -3285,7 +3302,9 @@ class propresult: """State transition matrix Returns: - 6x6 numpy array representing state transition matrix or None if not computed + 6x6 numpy array representing state transition matrix or None if not computed. + Maps a perturbation of the begin state (meters, m/s) to the end state + (meters, m/s), so blocks are unitless, seconds, 1/seconds, unitless. """ ... @@ -3320,7 +3339,7 @@ class propresult: Returns: tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]: (state, phi) where state is a 6-element - vector and phi is a 6x6 state transition matrix + vector [x, y, z, vx, vy, vz] in meters and m/s, and phi is a 6x6 state transition matrix """ ... @@ -3337,7 +3356,7 @@ class propresult: output_phi: Must be False (default) Returns: - list[npt.NDArray[np.float64]]: List of 6-element state vectors + list[npt.NDArray[np.float64]]: List of 6-element state vectors [x, y, z, vx, vy, vz] in meters and m/s """ ... @@ -3354,7 +3373,8 @@ class propresult: output_phi: Must be True Returns: - list[tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]]: List of (state, phi) tuples + list[tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]]: List of (state, phi) tuples; + each state is a 6-element vector in meters and m/s, each phi a 6x6 state transition matrix """ ... @@ -3830,8 +3850,9 @@ class propsettings: """Create propagation settings object used to configure high-precision orbit propagator Args: - abs_error: Maximum absolute value of error for any element in propagated state following ODE integration. Default is 1e-8 - rel_error: Maximum relative error of any element in propagated state following ODE integration. Default is 1e-8 + abs_error: Maximum absolute value of error for any element in propagated state following ODE integration, + in the units of the state (meters for position elements, m/s for velocity elements). Default is 1e-8 + rel_error: Maximum relative error of any element in propagated state following ODE integration, unitless. Default is 1e-8 gravity_degree: Maximum degree of spherical harmonic gravity model, at most 40 (``ValueError`` above that). Default is 4 gravity_order: Maximum order of spherical harmonic gravity model. Must be <= gravity_degree (and so at most 40). Default is same as gravity_degree gravity_model: Gravity model to use. Default is gravmodel.egm96 @@ -3883,7 +3904,8 @@ class propsettings: """Maximum absolute value of error for any element in propagated state following ODE integration Returns: - Maximum absolute value of error for any element in propagated state following ODE integration, default is 1e-8 + Maximum absolute value of error for any element in propagated state following ODE integration, + in the units of the state (meters for position elements, m/s for velocity elements); default is 1e-8 """ ... @@ -3894,7 +3916,7 @@ class propsettings: """Maximum relative error of any element in propagated state following ODE integration Returns: - Maximum relative error of any element in propagated state following ODE integration, default is 1e-8 + Maximum relative error of any element in propagated state following ODE integration, unitless; default is 1e-8 """ ... diff --git a/python/satkit/spaceweather.pyi b/python/satkit/spaceweather.pyi index 114d17ba..c28df963 100644 --- a/python/satkit/spaceweather.pyi +++ b/python/satkit/spaceweather.pyi @@ -27,10 +27,10 @@ def get(time: TimeScalar) -> dict: * ``kp_sum`` (int) — daily Kp sum * ``ap`` (list[int]) — eight 3-hourly Ap indices * ``ap_avg`` (int) — daily average Ap - * ``f10p7_obs`` (float) — observed F10.7 solar flux - * ``f10p7_adj`` (float) — F10.7 adjusted to 1 AU - * ``f10p7_obs_c81`` / ``f10p7_obs_l81`` (float) — 81-day centered / last-81-day observed averages - * ``f10p7_adj_c81`` / ``f10p7_adj_l81`` (float) — 81-day centered / last-81-day adjusted averages + * ``f10p7_obs`` (float) — observed F10.7 solar flux, sfu (10^-22 W m^-2 Hz^-1) + * ``f10p7_adj`` (float) — F10.7 adjusted to 1 AU, sfu + * ``f10p7_obs_c81`` / ``f10p7_obs_l81`` (float) — 81-day centered / last-81-day observed averages, sfu + * ``f10p7_adj_c81`` / ``f10p7_adj_l81`` (float) — 81-day centered / last-81-day adjusted averages, sfu * ``isn`` (int) — international sunspot number * ``cp`` (float) — planetary daily character figure * ``c9`` (int) — Cp scaled to [0, 9] @@ -54,8 +54,8 @@ def predicted_f107(time: TimeScalar) -> float | None: time (satkit.time | datetime.datetime): Time for which to return predicted F10.7 Returns: - float | None: Predicted F10.7 solar flux, or None if the time is - outside the forecast range + float | None: Predicted F10.7 solar flux in sfu (10^-22 W m^-2 Hz^-1), + or None if the time is outside the forecast range """ ... diff --git a/python/src/pyconsts.rs b/python/src/pyconsts.rs index 837dd601..6bab6936 100644 --- a/python/src/pyconsts.rs +++ b/python/src/pyconsts.rs @@ -13,7 +13,7 @@ impl Consts { const wgs84_a: f64 = cconsts::WGS84_A; #[classattr] - /// WGS-84 flattening + /// WGS-84 flattening, unitless const wgs84_f: f64 = cconsts::WGS84_F; #[classattr] @@ -56,7 +56,7 @@ impl Consts { #[classattr] const moon_radius: f64 = cconsts::MOON_RADIUS; - /// Earth-moon mass ratio + /// Earth-moon mass ratio (Earth mass over Moon mass), unitless #[classattr] const earth_moon_mass_ratio: f64 = cconsts::EARTH_MOON_MASS_RATIO; @@ -68,11 +68,11 @@ impl Consts { #[classattr] const jgm3_mu: f64 = cconsts::JGM3_MU; - /// Earth semiparameter per JGM3 gravity model, m^3/s^2 + /// Earth semiparameter per JGM3 gravity model, meters #[classattr] const jgm3_a: f64 = cconsts::JGM3_A; - /// Earth J2 per JGM3 gravity model + /// Earth J2 (oblateness) per JGM3 gravity model, unitless #[classattr] const jgm3_j2: f64 = cconsts::JGM3_J2; } diff --git a/python/src/pyframetransform.rs b/python/src/pyframetransform.rs index 69c796ce..41a7c1fd 100644 --- a/python/src/pyframetransform.rs +++ b/python/src/pyframetransform.rs @@ -26,9 +26,16 @@ pub fn gmst(tm: &Bound<'_, PyAny>) -> Result> { py_func_of_time_arr(ft::gmst, tm) } +/// Equation of the Equinoxes /// -/// Equation of Equinoxes +/// The difference between apparent and mean sidereal time (GAST - GMST), +/// arising from nutation of the Earth's axis. /// +/// Args: +/// tm (satkit.time|datetime.datetime|list|numpy.array): Time[s] at which to calculate output +/// +/// Returns: +/// float|numpy.array: Equation of the equinoxes at input time[s] in radians #[pyfunction] pub fn eqeq(tm: &Bound<'_, PyAny>) -> Result> { py_func_of_time_arr(ft::eqeq, tm) @@ -375,7 +382,8 @@ pub fn qtod2mod_approx(tm: &Bound<'_, PyAny>) -> Result> { /// /// Returns: /// (numpy.ndarray, numpy.ndarray): Tuple ``(pos_gcrf, vel_gcrf)`` of -/// the state expressed in GCRF. +/// the state expressed in GCRF: position in meters, velocity in m/s +/// (shape (3,) each, or (N, 3) for batched input). #[pyfunction] pub fn itrf_to_gcrf_state( pos_itrf: &Bound<'_, PyAny>, @@ -401,8 +409,9 @@ pub fn itrf_to_gcrf_state( /// time (satkit.time): Epoch of the state /// /// Returns: -/// (numpy.ndarray, numpy.ndarray): Tuple ``(pos_itrf, vel_itrf)``. -/// ``vel_itrf`` is the velocity as observed in ITRF. +/// (numpy.ndarray, numpy.ndarray): Tuple ``(pos_itrf, vel_itrf)``: +/// position in meters, velocity as observed in ITRF in m/s +/// (shape (3,) each, or (N, 3) for batched input). #[pyfunction] pub fn gcrf_to_itrf_state( pos_gcrf: &Bound<'_, PyAny>, @@ -420,6 +429,15 @@ pub fn gcrf_to_itrf_state( /// polar motion, so the Earth-rotation sweep ``omega_earth x r`` is /// evaluated in ITRF directly. Accepts scalar or batched inputs like /// :func:`itrf_to_gcrf_state`. +/// +/// Args: +/// pos_itrf (array-like): (3,) or (N, 3) position vector in ITRF, meters +/// vel_itrf (array-like): (3,) or (N, 3) velocity vector as observed in ITRF, m/s +/// time (satkit.time): Epoch of the state (length-N array/list for batched input) +/// +/// Returns: +/// (numpy.ndarray, numpy.ndarray): Tuple ``(pos_gcrf, vel_gcrf)``: +/// position in meters, velocity in m/s #[pyfunction] pub fn itrf_to_gcrf_state_approx( pos_itrf: &Bound<'_, PyAny>, @@ -434,6 +452,15 @@ pub fn itrf_to_gcrf_state_approx( /// Inverse of :func:`itrf_to_gcrf_state_approx`; accurate to ~1 arcsec on /// position. Accepts scalar or batched inputs like /// :func:`gcrf_to_itrf_state`. +/// +/// Args: +/// pos_gcrf (array-like): (3,) or (N, 3) position vector in GCRF, meters +/// vel_gcrf (array-like): (3,) or (N, 3) velocity vector in GCRF, m/s +/// time (satkit.time): Epoch of the state (length-N array/list for batched input) +/// +/// Returns: +/// (numpy.ndarray, numpy.ndarray): Tuple ``(pos_itrf, vel_itrf)``: +/// position in meters, velocity as observed in ITRF in m/s #[pyfunction] pub fn gcrf_to_itrf_state_approx( pos_gcrf: &Bound<'_, PyAny>, @@ -633,7 +660,8 @@ fn rotation_dispatch_batch( /// vel (numpy.ndarray): 3-element velocity vector [m/s] /// /// Returns: -/// tuple[numpy.ndarray, numpy.ndarray]: (pos, vel) in ``to_frame``. +/// tuple[numpy.ndarray, numpy.ndarray]: (pos, vel) in ``to_frame``: +/// position in meters, velocity in m/s. #[pyfunction] pub fn transform_state( from_frame: crate::pyframes::PyFrame, @@ -657,6 +685,17 @@ pub fn transform_state( /// State transform using the IAU-76/FK5 approximate reduction. Same /// supported-pair set as :func:`transform_state`. +/// +/// Args: +/// from_frame (satkit.frame): Source frame +/// to_frame (satkit.frame): Destination frame +/// tm (satkit.time|datetime.datetime): Epoch +/// pos (numpy.ndarray): 3-element position vector, meters +/// vel (numpy.ndarray): 3-element velocity vector, m/s +/// +/// Returns: +/// tuple[numpy.ndarray, numpy.ndarray]: (pos, vel) in ``to_frame``: +/// position in meters, velocity in m/s. #[pyfunction] pub fn transform_state_approx( from_frame: crate::pyframes::PyFrame, diff --git a/python/src/pyinstant.rs b/python/src/pyinstant.rs index 19e547b7..39e56aa7 100644 --- a/python/src/pyinstant.rs +++ b/python/src/pyinstant.rs @@ -422,7 +422,7 @@ impl PyInstant { /// Return time object representing input modified Julian date and time scale /// /// Args: - /// mjd (float): The Modified Julian Date + /// mjd (float): The Modified Julian Date, days /// scale (satkit.timescale, optional): The time scale. Default is satkit.timescale.UTC /// /// Returns: @@ -437,7 +437,7 @@ impl PyInstant { /// since Jan 1, 1970 00:00:00 (not counting leap seconds) /// /// Args: - /// unixtime (float): the unixtime + /// unixtime (float): the unixtime, UTC seconds since 1970-01-01 00:00:00 (excluding leap seconds) /// /// Returns: /// satkit.time: Time object representing instant of input unixtime @@ -449,7 +449,7 @@ impl PyInstant { /// Return time object representing input Julian date and time scale /// /// Args: - /// jd (float): The Julian Date + /// jd (float): The Julian Date, days /// scale (satkit.timescale, optional): The time scale. Default is satkit.timescale.UTC /// /// Returns: @@ -576,7 +576,7 @@ impl PyInstant { /// scale (satkit.timescale, optional): Time scale to use for conversion, default is satkit.timescale.UTC /// /// Returns: - /// float: Modified Julian Date + /// float: Modified Julian Date, days #[pyo3(signature=(scale=&PyTimeScale::UTC))] fn as_mjd(&self, scale: &PyTimeScale) -> f64 { self.0.as_mjd_with_scale(scale.into()) @@ -588,7 +588,7 @@ impl PyInstant { /// scale (satkit.timescale, optional: Time scale to use for conversion, default is satkit.timescale.UTC /// /// Returns: - /// float: Julian Date + /// float: Julian Date, days #[pyo3(signature=(scale=&PyTimeScale::UTC))] fn as_jd(&self, scale: &PyTimeScale) -> f64 { self.0.as_jd_with_scale(scale.into()) @@ -603,6 +603,14 @@ impl PyInstant { self.0.as_unixtime() } + /// Return time object representing input GPS week and seconds of week + /// + /// Args: + /// week (int): GPS week number + /// seconds (float): GPS seconds of week, seconds + /// + /// Returns: + /// satkit.time: Time object representing input GPS week and second #[staticmethod] fn from_gps_week_and_second(week: i32, seconds: f64) -> Self { Self(Instant::from_gps_week_and_second(week, seconds)) @@ -692,7 +700,8 @@ impl PyInstant { /// Subtract duration or take difference in times /// /// Args: - /// other (duration|list|numpy.ndarray|float|satkit.time): Duration or list of durations to subtract, or time object to take difference + /// other (duration|list|numpy.ndarray|float|satkit.time): Duration or list of durations to subtract, or time object to take difference. + /// If type is float, units are days /// /// Returns: /// satkit.time|numpy.ndarray|satkit.duration: New time object or numpy array of time objects representing input time minus input duration(s), or duration object representing difference between two time objects diff --git a/python/src/pylpephem_sun.rs b/python/src/pylpephem_sun.rs index 40d061c0..00fb18f8 100644 --- a/python/src/pylpephem_sun.rs +++ b/python/src/pylpephem_sun.rs @@ -15,7 +15,7 @@ use satkit::lpephem::sun; /// time (satkit.time, numpy array, or list): time[s] at which to compute position /// /// Returns: -/// numpy.ndarray: 3-element array or Nx3 array representing sun position in GCRF frame at input time[s] +/// numpy.ndarray: 3-element array or Nx3 array representing sun position in GCRF frame at input time[s]. Units are meters #[pyfunction] pub fn pos_gcrf(time: &Bound<'_, PyAny>) -> Result> { pyutils::py_vec3_of_time_arr(&sun::pos_gcrf, time) @@ -30,7 +30,7 @@ pub fn pos_gcrf(time: &Bound<'_, PyAny>) -> Result> { /// time (Instant, numpy array, or list): time[s] at which to compute position /// /// Returns: -/// numpy.ndarray: 3-element array or Nx3 array representing sun position in MOD frame at input time[s] +/// numpy.ndarray: 3-element array or Nx3 array representing sun position in MOD frame at input time[s]. Units are meters #[pyfunction] pub fn pos_mod(time: &Bound<'_, PyAny>) -> Result> { pyutils::py_vec3_of_time_arr(&sun::pos_mod, time) @@ -71,6 +71,17 @@ pub fn rise_set( } } +/// Is satellite in Earth shadow given sun position +/// +/// Notes: +/// * See algorithm in Section 3.4.2 of Montenbruck and Gill for calculation +/// +/// Args: +/// sunpos (numpy.ndarray): 3-element geocentric Sun position, meters +/// satpos (numpy.ndarray): 3-element geocentric satellite position, meters +/// +/// Returns: +/// float: unitless number in range [0,1] indicating no sun (0) or full sun (1, no occlusion) hitting satellite #[pyfunction(signature=(sunpos, satpos))] pub fn shadowfunc(sunpos: Bound<'_, PyAny>, satpos: Bound<'_, PyAny>) -> Result { let satpos = pyutils::py_to_smatrix::<3, 1>(&satpos)?; diff --git a/python/src/pypropagate.rs b/python/src/pypropagate.rs index 9c5b66b0..4c9ce31b 100644 --- a/python/src/pypropagate.rs +++ b/python/src/pypropagate.rs @@ -25,7 +25,7 @@ use anyhow::Result; /// /// Inputs: /// -/// state0 (npt.ArrayLike[float], optional): 6-element numpy array representing satellite position & velocity +/// state0 (npt.ArrayLike[float], optional): 6-element numpy array representing satellite GCRF position & velocity, in meters and meters/second /// begin (satkit.time, optional): Begin time of propagation, time of "state0" /// end (satkit.time, optional): End time of propagation /// @@ -60,7 +60,7 @@ use anyhow::Result; /// /// Returns: /// -/// satkit.propresult: object with new position and velocity, and possibly +/// satkit.propresult: object with new GCRF position (meters) and velocity (meters/second), and possibly /// state transition matrix between "begintime" and "endtime", /// and dense ODE solution that allow for interpolation, if requested /// diff --git a/python/src/pypropresult.rs b/python/src/pypropresult.rs index 0a0be8d2..93f1f6e1 100644 --- a/python/src/pypropresult.rs +++ b/python/src/pypropresult.rs @@ -75,9 +75,10 @@ impl PyPropStats { /// pos: 3-element numpy array representing the final position of the satellite in GCRF meters /// vel: 3-element numpy array representing the final velocity of the satellite in GCRF m/s /// state: 6-element numpy array representing the final state of the satellite in GCRF, -/// a concatenation of pos and vel +/// a concatenation of pos (meters) and vel (m/s) /// phi: 6x6 numpy array representing the state transition matrix between -/// the begin and end times, if requested +/// the begin and end times, if requested (maps a begin-state perturbation +/// in meters, m/s to the end state in meters, m/s) /// can_interp: boolean indicating whether the result includes a dense ODE /// solution that can be used for interpolation /// of states between the begin and end times @@ -131,7 +132,7 @@ impl PyPropResult { }))) } - // Get begin time + /// Get the begin time (time at which state_begin is valid) #[getter] fn time_begin(&self) -> PyInstant { PyInstant(match &self.0 { @@ -158,6 +159,10 @@ impl PyPropResult { }) } + /// Statistics of the propagation + /// + /// Returns: + /// satkit.propstats: function-evaluation and step counts #[getter] fn stats(&self) -> PyPropStats { match &self.0 { @@ -174,6 +179,10 @@ impl PyPropResult { } } + /// GCRF position of satellite at end of propagation + /// + /// Returns: + /// numpy.ndarray: 3-element GCRF position, meters #[getter] fn pos(&self) -> PyResult> { pyo3::Python::attach(|py| -> PyResult> { @@ -188,6 +197,10 @@ impl PyPropResult { }) } + /// GCRF velocity of satellite at end of propagation + /// + /// Returns: + /// numpy.ndarray: 3-element GCRF velocity, meters/second #[getter] fn vel(&self) -> PyResult> { pyo3::Python::attach(|py| -> PyResult> { @@ -205,6 +218,10 @@ impl PyPropResult { }) } + /// 6-element GCRF state (pos + vel) at end of propagation (same as state_end) + /// + /// Returns: + /// numpy.ndarray: [x, y, z, vx, vy, vz] in meters and meters/second #[getter] fn state(&self) -> PyResult> { pyo3::Python::attach(|py| -> PyResult> { @@ -219,6 +236,10 @@ impl PyPropResult { }) } + /// 6-element GCRF state (pos + vel) at end of propagation + /// + /// Returns: + /// numpy.ndarray: [x, y, z, vx, vy, vz] in meters and meters/second #[getter] fn state_end(&self) -> PyResult> { pyo3::Python::attach(|py| -> PyResult> { @@ -233,6 +254,10 @@ impl PyPropResult { }) } + /// 6-element GCRF state (pos + vel) at begin of propagation + /// + /// Returns: + /// numpy.ndarray: [x, y, z, vx, vy, vz] in meters and meters/second #[getter] fn state_begin(&self) -> PyResult> { pyo3::Python::attach(|py| -> PyResult> { @@ -247,6 +272,12 @@ impl PyPropResult { }) } + /// State transition matrix between begin and end times + /// + /// Returns: + /// numpy.ndarray | None: 6x6 state transition matrix, or None if not computed. + /// Maps a perturbation of the begin state (meters, m/s) to the end state + /// (meters, m/s), so blocks are unitless, seconds, 1/seconds, unitless. #[getter] fn phi(&self) -> PyResult> { pyo3::Python::attach(|py| -> PyResult> { @@ -275,6 +306,7 @@ impl PyPropResult { } } + /// Whether this result supports interpolation (dense output is available) #[getter] const fn can_interp(&self) -> bool { match &self.0 { @@ -307,6 +339,16 @@ impl PyPropResult { PyBytes::new(py, p.as_slice()).into_py_any(py) } + /// Interpolate the GCRF state at one or more times between the begin and end times + /// + /// Args: + /// time (satkit.time | datetime.datetime | list): time(s) at which to interpolate + /// output_phi (bool): also return the 6x6 state transition matrix. Default False + /// + /// Returns: + /// numpy.ndarray: 6-element state [x, y, z, vx, vy, vz] in meters and m/s + /// (a list of them for a list of times); with output_phi=True, (state, phi) + /// tuples where phi is the 6x6 state transition matrix #[pyo3(signature=(time, output_phi=false))] fn interp(&self, time: Bound<'_, PyAny>, output_phi: bool) -> PyResult> { let is_list = time.is_instance_of::() diff --git a/python/src/pypropsettings.rs b/python/src/pypropsettings.rs index 31136be7..2ca06950 100644 --- a/python/src/pypropsettings.rs +++ b/python/src/pypropsettings.rs @@ -94,6 +94,25 @@ impl From for PyTideModel { } } +/// Settings for the high-precision orbit propagator +/// +/// Keyword Args: +/// abs_error (float): Maximum absolute error of any element in the propagated state, in the +/// units of the state (meters for position elements, m/s for velocity elements). Default 1e-8 +/// rel_error (float): Maximum relative error of any element in the propagated state, unitless. Default 1e-8 +/// gravity_degree (int): Maximum degree of spherical harmonic gravity model (at most 40). Default 4 +/// gravity_order (int): Maximum order of spherical harmonic gravity model. Default same as gravity_degree +/// gravity_model (satkit.gravmodel): Gravity model. Default gravmodel.egm96 +/// use_spaceweather (bool): Use space weather data for atmospheric density. Default True +/// use_sun_gravity (bool): Include sun third-body gravity. Default True +/// use_moon_gravity (bool): Include moon third-body gravity. Default True +/// tide_model (satkit.tidemodel): Solid Earth tide model. Default tidemodel.solid_step1 +/// use_relativistic_correction (bool): Include general-relativistic acceleration. Default True +/// enable_interp (bool): Store dense output for interpolation. Default True +/// integrator (satkit.integrator): ODE integrator. Default integrator.rkv98 +/// gj_step_seconds (float): Fixed step size for integrator.gauss_jackson8, seconds. Default 60.0 +/// max_steps (int): Maximum number of integrator steps. Default 1_000_000 +/// require_eop_coverage (bool): Raise if the span extends past the EOP table. Default False #[pyclass(name = "propsettings", module = "satkit", from_py_object)] #[derive(Clone, Debug)] pub struct PyPropSettings(pub PropSettings); @@ -207,6 +226,8 @@ impl PyPropSettings { Ok(Self(ps)) } + /// Maximum absolute error of any element in the propagated state, in the units + /// of the state (meters for position elements, m/s for velocity elements). Default 1e-8 #[getter] fn get_abs_error(&self) -> f64 { self.0.abs_error @@ -218,6 +239,7 @@ impl PyPropSettings { Ok(()) } + /// Maximum relative error of any element in the propagated state, unitless. Default 1e-8 #[getter] fn get_rel_error(&self) -> f64 { self.0.rel_error @@ -326,6 +348,8 @@ impl PyPropSettings { Ok(()) } + /// Fixed step size used by integrator.gauss_jackson8, seconds. Ignored by + /// adaptive integrators. Default 60.0 #[getter] fn get_gj_step_seconds(&self) -> f64 { self.0.gj_step_seconds @@ -417,6 +441,13 @@ impl PyPropSettings { Ok(()) } + /// Precompute sun/moon terms for fast propagation of many satellites over the same span + /// + /// Args: + /// begin (satkit.time): Begin time of propagation + /// end (satkit.time): End time of propagation + /// step (satkit.duration | float | datetime.timedelta, optional): Table step; + /// a float is interpreted as seconds. Default 60 seconds #[pyo3(signature=(begin, end, step=None))] fn precompute_terms( &mut self, diff --git a/python/src/pyquaternion.rs b/python/src/pyquaternion.rs index bbcfbc42..af9c1874 100644 --- a/python/src/pyquaternion.rs +++ b/python/src/pyquaternion.rs @@ -60,7 +60,7 @@ impl PyQuaternion { } } - /// Quaternion representing rotation about xhat axis by `theta-rad` degrees + /// Quaternion representing rotation about xhat axis by `theta_rad` radians /// /// Args: /// theta_rad: Angle in radians to rotate about xhat axis @@ -76,7 +76,7 @@ impl PyQuaternion { Ok(Quaternion::rotx(theta_rad).into()) } - /// Quaternion representing rotation about yhat axis by `theta-rad` degrees + /// Quaternion representing rotation about yhat axis by `theta_rad` radians /// /// Args: /// theta_rad: Angle in radians to rotate about yhat axis @@ -93,8 +93,17 @@ impl PyQuaternion { Ok(Quaternion::roty(theta_rad).into()) } - /// Quaternion representing rotation about - /// zhat axis by `theta-rad` degrees + /// Quaternion representing rotation about zhat axis by `theta_rad` radians + /// + /// Args: + /// theta_rad: Angle in radians to rotate about zhat axis + /// + /// Returns: + /// quaternion: Quaternion representing rotation about zhat axis + /// + /// Notes: + /// This is a right-handed rotation of the vector + /// e.g. rotation of +xhat 90 degrees by +zhat gives +yhat #[staticmethod] fn rotz(theta_rad: f64) -> Result { Ok(Quaternion::rotz(theta_rad).into()) diff --git a/python/src/pysatstate.rs b/python/src/pysatstate.rs index 064ad31b..bed6b855 100644 --- a/python/src/pysatstate.rs +++ b/python/src/pysatstate.rs @@ -19,6 +19,14 @@ use satkit::{Frame, Instant}; use anyhow::{bail, Result}; +/// Satellite state: GCRF position, velocity, optional covariance, and maneuvers +/// +/// Args: +/// time (satkit.time): Epoch of the state +/// pos (numpy.ndarray): 3-element GCRF position, meters +/// vel (numpy.ndarray): 3-element GCRF velocity, m/s +/// cov (numpy.ndarray, optional): 6x6 GCRF state covariance +/// (position block m^2, velocity block (m/s)^2, cross blocks m^2/s). Default None. #[pyclass(name = "satstate", module = "satkit", from_py_object)] #[derive(Clone, Debug)] pub struct PySatState(SatState); @@ -141,7 +149,8 @@ impl PySatState { /// Set full 6x6 state covariance matrix /// /// Args: - /// cov (numpy.ndarray): 6x6 numpy array with state covariance matrix for position (meters) and velocity (m/s) + /// cov (numpy.ndarray): 6x6 numpy array with GCRF state covariance for position (meters) and velocity (m/s): + /// position block m^2, velocity block (m/s)^2, cross blocks m^2/s /// /// Returns: /// None @@ -166,11 +175,19 @@ impl PySatState { Ok(()) } + /// Epoch of this satellite state + /// + /// Returns: + /// satkit.time: Epoch of the state #[getter] fn get_time(&self) -> PyInstant { PyInstant(self.0.time) } + /// GCRF position of the satellite + /// + /// Returns: + /// numpy.ndarray: 3-element GCRF position, meters #[getter] fn get_pos_gcrf(&self) -> Py { pyo3::Python::attach(|py| -> Py { @@ -180,6 +197,10 @@ impl PySatState { }) } + /// GCRF velocity of the satellite + /// + /// Returns: + /// numpy.ndarray: 3-element GCRF velocity, m/s #[getter] fn get_vel_gcrf(&self) -> Py { pyo3::Python::attach(|py| -> Py { @@ -192,7 +213,8 @@ impl PySatState { /// Get full 6x6 state covariance matrix /// /// Returns: - /// numpy.ndarray: 6x6 numpy array with state covariance matrix for position (meters) and velocity (m/s) + /// numpy.ndarray: 6x6 numpy array with GCRF state covariance for position (meters) and velocity (m/s): + /// position block m^2, velocity block (m/s)^2, cross blocks m^2/s. None if not set #[getter] fn get_cov(&self) -> Py { pyo3::Python::attach(|py| -> Py { @@ -225,13 +247,13 @@ impl PySatState { self.0.qgcrf2lvlh().into() } - /// Alias for pos_gcrf + /// Alias for pos_gcrf: 3-element GCRF position, meters #[getter] fn get_pos(&self) -> Py { self.get_pos_gcrf() } - /// Alias for vel_gcrf + /// Alias for vel_gcrf: 3-element GCRF velocity, m/s #[getter] fn get_vel(&self) -> Py { self.get_vel_gcrf() diff --git a/python/src/pysgp4.rs b/python/src/pysgp4.rs index ce190671..748f8ef9 100644 --- a/python/src/pysgp4.rs +++ b/python/src/pysgp4.rs @@ -273,7 +273,7 @@ fn pack_sgp4_result(states: &psgp4::SGP4State, output_err: bool) -> Result Result Result) -> anyhow::Result> { /// time (satkit.time|datetime.datetime): Time for which to return predicted F10.7 /// /// Returns: -/// float | None: Predicted F10.7 solar flux, or None if the time is -/// outside the forecast range (or no forecast data is available) +/// float | None: Predicted F10.7 solar flux in sfu (10^-22 W m^-2 Hz^-1), +/// or None if the time is outside the forecast range (or no forecast data is available) #[pyfunction] pub fn predicted_f107(time: &Bound<'_, PyAny>) -> anyhow::Result> { let tm = instant_from_pyany(time)?; diff --git a/python/src/pythrust.rs b/python/src/pythrust.rs index e5ce9e5e..93ab8e20 100644 --- a/python/src/pythrust.rs +++ b/python/src/pythrust.rs @@ -48,6 +48,9 @@ impl PyThrust { } /// Get the acceleration vector + /// + /// Returns: + /// list[float]: 3-element acceleration vector in the thrust's frame [m/s^2] #[getter] fn get_accel(&self) -> [f64; 3] { [self.0.accel[0], self.0.accel[1], self.0.accel[2]] diff --git a/python/src/pytle.rs b/python/src/pytle.rs index f3ebbe4e..2c7bfd6d 100644 --- a/python/src/pytle.rs +++ b/python/src/pytle.rs @@ -209,7 +209,7 @@ impl PyTLE { self.0.rev_num = value; } - /// Orbit eccentricity + /// Orbit eccentricity, unitless, in range [0,1] #[getter(eccen)] const fn get_eccen(&self) -> f64 { self.0.eccen @@ -315,7 +315,7 @@ impl PyTLE { self.0.name = value; } - // Drag + /// Drag term (B*) of the satellite, in units of 1 / Earth radii #[getter(bstar)] const fn bstar(&self) -> f64 { self.0.bstar @@ -346,12 +346,22 @@ impl PyTLE { Ok(self.0.to_2line()?) } - // Output as 2 canonical TLE lines preceded by a name line (3-line element set) + /// Output as 2 canonical TLE lines preceded by a name line (3-line element set) fn to_3line(&self) -> Result<[String; 3]> { Ok(self.0.to_3line()?) } - // Fit a TLE from GCRF states and times + /// Perform non-linear least squares fit of TLE parameters to a list of GCRF states + /// + /// Args: + /// states (list[numpy.ndarray]): List of GCRF states to fit to. Each state is a + /// 6-element vector: the first 3 values are position in meters, the last 3 + /// values are velocity in meters / second + /// times (list[satkit.time]): Times corresponding to the states + /// epoch (satkit.time): Epoch time for the TLE. Must be within range of times + /// + /// Returns: + /// tuple[TLE, dict]: Fitted TLE and fitting results in a dictionary #[staticmethod] fn fit_from_states( py: Python,