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
//! MPC observatory code → observer state queries.
use crate::context::Context;
use crate::coordinate::{Frame, Origin, int_to_frame};
use crate::error::{Error, Result};
use std::ffi::CString;
/// Observer state at one epoch.
#[derive(Debug, Clone, PartialEq)]
pub struct Observer {
/// MPC observatory code (3 characters).
pub obs_code: String,
/// Epoch.
pub epoch: crate::Epoch,
/// Position in the [`frame`](Self::frame) / [`origin`](Self::origin)
/// basis (AU).
pub position: [f64; 3],
/// Velocity in the [`frame`](Self::frame) / [`origin`](Self::origin)
/// basis (AU/day).
pub velocity: [f64; 3],
/// Observing night as YYYYMMDD integer, or -1 if unavailable
/// (space-based observers, or a site without a longitude).
///
/// The night is the local calendar date on which the observing night
/// began: the UTC epoch is shifted by the site's east longitude to
/// local mean solar time, and epochs before local noon stamp the
/// previous date. MPC east-of-Greenwich longitudes in \\( [0, 360) \\)
/// are wrapped to signed \\( [-180, 180] \\) before the fold, so a
/// site west of Greenwich (e.g. \\( 289^\circ \\)E in Chile) shifts by
/// \\( -71^\circ \\), not \\( +19 \\) hours.
pub observing_night: i32,
/// Reference frame [`position`](Self::position) and
/// [`velocity`](Self::velocity) are expressed in.
///
/// Read off the returned state rather than echoed from the request,
/// so a row always reports the basis that actually produced it.
pub frame: Frame,
/// Body the state is relative to.
pub origin: Origin,
}
impl Observer {
pub(crate) fn from_ffi(o: &empyrean_sys::EmpyreanObserver) -> Result<Self> {
Ok(Self {
obs_code: obs_code_from_bytes(&o.obs_code),
epoch: crate::Epoch::from_mjd_tdb(o.epoch_mjd_tdb),
position: [o.x, o.y, o.z],
velocity: [o.vx, o.vy, o.vz],
observing_night: o.observing_night,
frame: int_to_frame(o.frame)?,
origin: Origin::from_naif_id(o.origin).ok_or_else(|| {
Error::invalid_input(format!(
"libempyrean returned an unknown origin NAIF id: {}",
o.origin
))
})?,
})
}
}
pub(crate) fn obs_code_from_bytes(bytes: &[u8; 4]) -> String {
let end = bytes.iter().position(|&b| b == 0).unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).into_owned()
}
impl Context {
/// Compute observer states (cross product: codes × epochs) in a
/// caller-chosen `(frame, origin)` basis.
///
/// Returns `obs_codes.len() * epochs.len()` observer entries in
/// code-major order: all epochs for `obs_codes[0]`, then all epochs
/// for `obs_codes[1]`, so `result[i * epochs.len() + j]` is
/// `(obs_codes[i], epochs[j])`.
///
/// # Choosing a basis
///
/// [`Frame::ICRF`] with [`Origin::SSB`] is the **construction
/// basis** — the one every consumer of an observer state requires,
/// and the one to pass when the observers are headed for ephemeris
/// generation or orbit determination. Requesting it takes no
/// transform at all: the observers come back exactly as constructed,
/// bit for bit. Any other basis rotates and/or translates them, which
/// is what a consumer plotting observer geometry wants (e.g.
/// heliocentric ecliptic site positions via
/// `(Frame::EclipticJ2000, Origin::SUN)`).
///
/// # Errors
///
/// Every rejection aborts the whole call — there is no partial output
/// and no silently untransformed entry. The failures keep the
/// engine's two axes rather than being flattened onto one:
/// [`Error::code`] is `-1` when the remedy is different arguments (an
/// unknown observatory code, a frame an observer cannot be rotated
/// into, an origin that is an MPC site rather than an SPK body) and
/// `-2` when the remedy is to load or fetch data (an unfetched
/// spacecraft kernel, an SPK coverage gap, a BPC window that ends
/// before the requested epoch).
pub fn get_observers(
&self,
obs_codes: &[&str],
epochs: &[crate::Epoch],
frame: Frame,
origin: Origin,
) -> Result<Vec<Observer>> {
let cstrings: Vec<CString> = obs_codes
.iter()
.map(|&c| {
CString::new(c).map_err(|_| Error::invalid_input("observatory code has NUL byte"))
})
.collect::<Result<Vec<_>>>()?;
let ptrs: Vec<*const std::ffi::c_char> = cstrings.iter().map(|s| s.as_ptr()).collect();
let epochs_mjd_tdb: Vec<f64> = epochs
.iter()
.map(|e| e.mjd_tdb())
.collect::<Result<Vec<_>>>()?;
let mut result = empyrean_sys::EmpyreanObserverResult {
observers: std::ptr::null_mut(),
num_observers: 0,
};
let code = unsafe {
empyrean_sys::empyrean_get_observers(
self.as_raw(),
ptrs.as_ptr(),
ptrs.len(),
epochs_mjd_tdb.as_ptr(),
epochs_mjd_tdb.len(),
frame as i32,
origin.naif_id(),
&mut result,
)
};
if code != 0 {
return Err(Error::capture(code));
}
let observers: Result<Vec<Observer>> = unsafe {
std::slice::from_raw_parts(result.observers, result.num_observers)
.iter()
.map(Observer::from_ffi)
.collect()
};
// Free before propagating: the native array is owned regardless
// of whether marshaling every row succeeded.
unsafe { empyrean_sys::empyrean_observer_result_free(&mut result) };
observers
}
}