1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
//! Geometric quantities derived from the apparent-position pipeline.
//!
//! This module sits inside the ephemeris pipeline and is responsible for
//! computing the five observational quantities stored in [`BodyGeometry`]:
//! phase angle, solar elongation, radial velocity, and the apparent angular
//! rates in right ascension and declination. All quantities are derived from
//! the same [`PropagatedState`] that feeds [`ApparentPosition`], so the two
//! types share a single orbit propagation when computed together.
//!
//! # Role in the ephemeris pipeline
//!
//! ```text
//! OrbitalElements
//! │
//! ▼ propagate (TwoBody or NBody)
//! PropagatedState [equatorial mean J2000]
//! │
//! ├──────────────────────────────────────────────────┐
//! ▼ apparent_position::assemble_apparent_position ▼ geometry::compute_geometry
//! ApparentPosition BodyGeometry
//! { coord, geocentric_dist, { phase_angle, solar_elongation,
//! heliocentric_dist } radial_velocity, d_ra_dt, d_dec_dt }
//! ```
//!
//! # Coordinate frame
//!
//! All vectors consumed by this module — body position, observer position, and
//! their velocities — are expressed in the **equatorial mean J2000** frame
//! (the same frame used for the final [`ApparentPosition`] output).
//! Positions are in **AU**, velocities in **AU/day**, and angles in **radians**.
//!
//! # Relationship to [`ApparentPosition`]
//!
//! Both types are computed from the same [`PropagatedState`]. When both are
//! needed, use [`OrbitalElements::apparent_position_and_geometry`] to avoid
//! propagating the orbit twice. When only one is needed, use
//! [`OrbitalElements::apparent_position`] or
//! [`OrbitalElements::body_geometry`] respectively.
//!
//! # API
//!
//! Use [`crate::OrbitalElements::compute`] with [`crate::Geometry`] or
//! [`crate::Combined`] to obtain [`BodyGeometry`] values.
use Vector3;
use crate::;
use ;
// ---------------------------------------------------------------------------
// BodyGeometry
// ---------------------------------------------------------------------------
/// Geometric quantities for a solar system body at a given epoch.
///
/// Returned as the payload of [`crate::EphemerisResult`] when using
/// [`crate::Geometry`] or [`crate::Combined`] as the output marker.
///
/// All angles are in **radians**, velocities in **AU/day**.
///
/// # Physical definitions
///
/// | Field | Definition |
/// |---|---|
/// | [`phase_angle`](Self::phase_angle) | Angle at the body between the Sun and the observer: Sun–body–observer |
/// | [`solar_elongation`](Self::solar_elongation) | Angle at the observer between the Sun and the body: Sun–observer–body |
/// | [`radial_velocity`](Self::radial_velocity) | Rate of change of the observer–body distance |
/// | [`d_ra_dt`](Self::d_ra_dt) | Apparent angular rate in right ascension |
/// | [`d_dec_dt`](Self::d_dec_dt) | Apparent angular rate in declination |
///
/// # Usage
///
/// ```rust,ignore
/// use outfit::{Combined, EphemerisConfig, EphemerisMode, EphemerisRequest};
/// use hifitime::Epoch;
///
/// let times = vec![
/// Epoch::from_mjd_tt(60310.0),
/// Epoch::from_mjd_tt(60320.0),
/// Epoch::from_mjd_tt(60330.0),
/// ];
///
/// let result = elements.compute(
/// &EphemerisRequest::<Combined>::new(EphemerisConfig::default())
/// .add(observer, EphemerisMode::At(times)),
/// &jpl,
/// &ut1,
/// );
///
/// for entry in result.successes() {
/// let (pos, geo) = entry.result.as_ref().unwrap();
/// println!(
/// "{}: phase={:.4} elong={:.4} rv={:.6} dRA={:.6} dDec={:.6}",
/// entry.epoch,
/// geo.phase_angle, geo.solar_elongation,
/// geo.radial_velocity, geo.d_ra_dt, geo.d_dec_dt,
/// );
/// }
/// ```
///
/// # See also
///
/// - [`crate::Geometry`] — output marker for geometry-only requests.
/// - [`crate::Combined`] — output marker to get both position and geometry in one pass.
// ---------------------------------------------------------------------------
// Computation
// ---------------------------------------------------------------------------
/// Compute the [`BodyGeometry`] from a [`PropagatedState`].
///
/// This function is the single entry point for geometry computation inside the
/// ephemeris pipeline. It executes the following steps in order:
///
/// 1. **Aberration correction** — builds the raw topocentric vector
/// $\mathbf{d}_0 = \mathbf{r}_\text{body} - \mathbf{r}_\text{obs}$
/// and applies either the first-order or the second-order stellar-aberration
/// model (controlled by `aberration`) to obtain the corrected line-of-sight
/// $\mathbf{d}$.
/// 2. **Norms** — computes the topocentric distance $\rho = |\mathbf{d}|$,
/// the heliocentric distance $r_\text{helio} = |\mathbf{r}_\text{body}|$,
/// and the observer–Sun distance $r_\text{obs} = |\mathbf{r}_\text{obs}|$.
/// 3. **Phase angle** $\phi$ — via [`phase_angle`] (Sun–body–observer,
/// dot product clamped before `acos`).
/// 4. **Solar elongation** $\varepsilon$ — via [`solar_elongation`]
/// (Sun–observer–body, same clamping strategy).
/// 5. **True topocentric velocity** —
/// $\mathbf{v}_\text{topo} = \mathbf{v}_\text{body} - \mathbf{v}_\text{obs}$.
/// 6. **Radial velocity** $\dot{\rho}$ — via [`radial_velocity`], the
/// projection of $\mathbf{v}_\text{topo}$ onto the line of sight.
/// 7. **Angular rates** $(\dot{\alpha}, \dot{\delta})$ — via
/// [`angular_rates`], using the geometric Jacobian of the spherical-coordinate
/// transform.
///
/// All input vectors are expected in the **equatorial mean J2000** frame with
/// positions in AU and velocities in AU/day. The only additional cost over
/// computing [`ApparentPosition`] alone is one `acos` per angle and a few
/// dot products.
///
/// # Arguments
///
/// - `state` – Propagated body + observer state at the observation epoch.
/// - `elements` – Orbital elements (needed by second-order aberration).
/// - `aberration` – Aberration correction order.
///
/// # Panics
///
/// This function does not panic under any input. Potential division-by-zero
/// conditions (zero topocentric distance, body on the celestial pole) are
/// guarded by [`radial_velocity`] and [`angular_rates`].
///
/// # Errors
///
/// Returns [`OutfitError`] if the second-order aberration back-propagation
/// fails. The first-order path is always infallible.
pub
// ---------------------------------------------------------------------------
// Private helpers
// ---------------------------------------------------------------------------
/// Compute the phase angle (Sun–body–observer) \[rad\].
///
/// The phase angle $\phi$ is the angle at the body between the direction
/// toward the Sun and the direction toward the observer. It determines the
/// fraction of the illuminated disk visible to the observer: $\phi = 0$
/// corresponds to full illumination (opposition), $\phi = \pi$ to a dark
/// face (superior conjunction).
///
/// $$\phi = \arccos\left(\frac{\mathbf{r}_\text{body} \cdot \mathbf{d}}{r_\text{helio}\,\rho}\right)$$
///
/// The cosine argument is clamped to $[-1, 1]$ before calling `acos` to
/// guard against floating-point rounding that would otherwise produce `NaN`
/// when the body is exactly at opposition or conjunction.
/// Compute the solar elongation (Sun–observer–body) \[rad\].
///
/// The solar elongation $\varepsilon$ is the angle at the observer
/// between the direction toward the Sun and the direction toward the body.
/// Small values ($\varepsilon \lesssim 20°$) indicate that the body is
/// close to the Sun on the sky and may be unobservable from the ground.
///
/// $$\varepsilon = \arccos\left(\frac{-\mathbf{r}_\text{obs} \cdot \mathbf{d}}{|\mathbf{r}_\text{obs}|\,\rho}\right)$$
///
/// The negation of $\mathbf{r}_\text{obs}$ converts from the
/// observer-to-Sun direction to the Sun-to-observer direction, giving the
/// correct sign convention. As with [`phase_angle`], the cosine argument is
/// clamped to $[-1, 1]$ to prevent `NaN` from rounding.
/// Compute the observer-relative radial velocity \[AU/day\].
///
/// The radial velocity $\dot{\rho}$ is the rate of change of the
/// topocentric distance: positive when the body is receding from the observer,
/// negative when it is approaching. It is the component of the true
/// topocentric velocity $\mathbf{v}_\text{topo}$ projected onto the
/// unit line-of-sight vector.
///
/// $$\dot{\rho} = \frac{\mathbf{d} \cdot \mathbf{v}_\text{topo}}{\rho}$$
///
/// No special edge-case handling is needed here: $\rho = 0$ would mean
/// the observer is at the body, which is physically impossible in practice.
/// Compute the apparent angular rates $(\dot{\alpha}, \dot{\delta})$
/// \[rad/day\].
///
/// The angular rates give how fast the body moves across the sky as seen by
/// the observer: $\dot{\alpha}$ is positive eastward and $\dot{\delta}$
/// is positive northward. Both include the contribution from the observer's
/// own velocity (diurnal and annual motion).
///
/// Uses the geometric Jacobians
///
/// $$\frac{\partial\alpha}{\partial\mathbf{r}} = \frac{1}{d_x^2+d_y^2}\begin{pmatrix}-d_y\\d_x\\0\end{pmatrix}$$
///
/// $$\frac{\partial\delta}{\partial\mathbf{r}} = \frac{1}{\rho^2\sqrt{d_x^2+d_y^2}}\begin{pmatrix}-d_z d_x\\-d_z d_y\\d_x^2+d_y^2\end{pmatrix}$$
///
/// where $\mathbf{d} = (d_x, d_y, d_z)$ is the aberration-corrected
/// topocentric vector.
///
/// **Degenerate pole case**: when the body lies on (or very close to) the
/// celestial pole, $d_x^2 + d_y^2 \approx 0$ and the Jacobians are
/// singular. The function detects this condition — specifically when
/// $\sqrt{d_x^2+d_y^2} < \varepsilon_\text{machine} \cdot \rho$ — and
/// returns $(0, 0)$ in that case.
///
/// Returns $(\dot{\alpha}, \dot{\delta})$ in rad/day.