Skip to content

Commit 2b875a6

Browse files
Initial work on LRO OD example
- Add ability to build Trajectory from BSP - Include line of sight (eclipsing) computation in OD measurements
1 parent e579b5d commit 2b875a6

File tree

10 files changed

+301
-11
lines changed

10 files changed

+301
-11
lines changed

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,8 @@ doc-scrape-examples = true
126126
name = "03_geo_sk"
127127
path = "examples/03_geo_analysis/stationkeeping.rs"
128128
doc-scrape-examples = true
129+
130+
[[example]]
131+
name = "04_lro_od"
132+
path = "examples/04_lro_od/main.rs"
133+
doc-scrape-examples = true

examples/04_lro_od/README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Orbit Determination of the Lunar Reconnaissance Orbiter
2+
3+
Spacecraft operations require high fidelity modeling of the orbital dynamics and high fidelity orbit determination.
4+
5+
In this example, you'll learn how to use an "as-flown" (_definitive_) SPICE BSP ephemeris file to simulate orbit determination measurements from ground stations. Then, you'll learn how to set up an orbit determination process in Nyx with high fidelity Moon dynamics and estimate the state of LRO. Finally, you'll learn how to compare two ephemerides in the Radial, In-track, Cross-track (RIC) frame.
6+
7+
To run this example, just execute:
8+
```sh
9+
RUST_LOG=info cargo run --example 04_lro_od --release
10+
```
11+
12+
Building in `release` mode will make the computation significantly faster. Specifying `RUST_LOG=info` will allow you to see all of the information messages happening in ANISE and Nyx throughout the execution of the program.

examples/04_lro_od/dsn-network.yaml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
- name: DSS-65 Madrid
2+
frame:
3+
ephemeris_id: 399
4+
orientation_id: 399
5+
mu_km3_s2: 398600.435436096
6+
shape: null
7+
elevation_mask_deg: 5.0
8+
range_noise_km:
9+
bias:
10+
tau: 24 h
11+
process_noise: 5.0e-3 # 5 m
12+
doppler_noise_km_s:
13+
bias:
14+
tau: 24 h
15+
process_noise: 50.0e-6 # 5 cm/s
16+
light_time_correction: false
17+
latitude_deg: 40.427222
18+
longitude_deg: 4.250556
19+
height_km: 0.834939
20+
21+
- name: DSS-34 Canberra
22+
frame:
23+
ephemeris_id: 399
24+
orientation_id: 399
25+
mu_km3_s2: 398600.435436096
26+
shape: null
27+
latitude_deg: -35.398333
28+
longitude_deg: 148.981944
29+
height_km: 0.691750
30+
elevation_mask_deg: 5.0
31+
range_noise_km:
32+
bias:
33+
tau: 24 h
34+
process_noise: 5.0e-3 # 5 m
35+
doppler_noise_km_s:
36+
bias:
37+
tau: 24 h
38+
process_noise: 50.0e-6 # 5 cm/s
39+
light_time_correction: false
40+
41+
- name: DSS-13 Goldstone
42+
frame:
43+
ephemeris_id: 399
44+
orientation_id: 399
45+
mu_km3_s2: 398600.435436096
46+
shape: null
47+
latitude_deg: 35.247164
48+
longitude_deg: 243.205
49+
height_km: 1.071149
50+
elevation_mask_deg: 5.0
51+
range_noise_km:
52+
bias:
53+
tau: 24 h
54+
process_noise: 5.0e-3 # 5 m
55+
doppler_noise_km_s:
56+
bias:
57+
tau: 24 h
58+
process_noise: 50.0e-6 # 5 cm/s
59+
light_time_correction: false
67.8 KB
Loading

examples/04_lro_od/main.rs

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
#![doc = include_str!("./README.md")]
2+
extern crate log;
3+
extern crate nyx_space as nyx;
4+
extern crate pretty_env_logger as pel;
5+
6+
use anise::{
7+
almanac::metaload::MetaFile,
8+
constants::{
9+
celestial_objects::{JUPITER_BARYCENTER, MOON, SUN},
10+
frames::{EARTH_J2000, MOON_J2000},
11+
},
12+
};
13+
use hifitime::{Epoch, TimeUnits, Unit};
14+
use nyx::{
15+
cosmic::{eclipse::EclipseLocator, Aberration, Frame, MetaAlmanac, SrpConfig},
16+
dynamics::{guidance::LocalFrame, OrbitalDynamics, SolarPressure, SpacecraftDynamics},
17+
io::{ConfigRepr, ExportCfg},
18+
mc::MonteCarlo,
19+
md::prelude::Traj,
20+
od::{
21+
msr::RangeDoppler,
22+
prelude::{TrackingArcSim, TrkConfig, KF},
23+
process::SpacecraftUncertainty,
24+
GroundStation, SpacecraftODProcess,
25+
},
26+
propagators::Propagator,
27+
Orbit, Spacecraft, State,
28+
};
29+
30+
use std::{collections::BTreeMap, error::Error, path::PathBuf, str::FromStr, sync::Arc};
31+
32+
fn main() -> Result<(), Box<dyn Error>> {
33+
pel::init();
34+
// Dynamics models require planetary constants and ephemerides to be defined.
35+
// Let's start by grabbing those by using ANISE's latest MetaAlmanac.
36+
// For details, refer to https://github.com/nyx-space/anise/blob/master/data/latest.dhall.
37+
38+
// Download the latest (of time of writing) LRO definitive ephemeris from the public Nyx Space cloud.
39+
// Note that the original file is in _big endian_ format, and my machine is little endian, so I've used the
40+
// `bingo` tool from https://naif.jpl.nasa.gov/naif/utilities_PC_Linux_64bit.html to convert the original file
41+
// to little endian and upload it to the cloud.
42+
// Refer to https://naif.jpl.nasa.gov/pub/naif/pds/data/lro-l-spice-6-v1.0/lrosp_1000/data/spk/?C=M;O=D for original file.
43+
let mut lro_def_ephem = MetaFile {
44+
uri: "http://public-data.nyxspace.com/nyx/examples/lrorg_2023349_2024075_v01_LE.bsp"
45+
.to_string(),
46+
crc32: Some(0xe76ce3b5),
47+
};
48+
lro_def_ephem.process()?;
49+
50+
// Load this ephem in the general Almanac we're using for this analysis.
51+
let almanac = Arc::new(
52+
MetaAlmanac::latest()
53+
.map_err(Box::new)?
54+
.load_from_metafile(lro_def_ephem)?,
55+
);
56+
57+
// Orbit determination requires a Trajectory structure, which can be saved as parquet file.
58+
// In our case, the trajectory comes from the BSP file, so we need to build a Trajectory from the almanac directly.
59+
// To query the Almanac, we need to build the LRO frame in the J2000 orientation in our case.
60+
// Inspecting the LRO BSP in the ANISE GUI shows us that NASA has assigned ID -85 to LRO.
61+
let lro_frame = Frame::from_ephem_j2000(-85);
62+
// To build the trajectory we need to provide a spacecraft template.
63+
let sc_template = Spacecraft::builder()
64+
.dry_mass_kg(1018.0) // Launch masses
65+
.fuel_mass_kg(900.0)
66+
.srp(SrpConfig {
67+
// SRP configuration is arbitrary, but we will be estimating it anyway.
68+
area_m2: 3.9 * 2.7,
69+
cr: 0.9,
70+
})
71+
.orbit(Orbit::zero(MOON_J2000)) // Setting a zero orbit here because it's just a template
72+
.build();
73+
// Now we can build the trajectory from the BSP file.
74+
// We'll arbitrarily set the tracking arc to 48 hours with a one minute time step.
75+
let trajectory = Traj::from_bsp(
76+
lro_frame,
77+
MOON_J2000,
78+
almanac.clone(),
79+
sc_template,
80+
5.seconds(),
81+
Some(Epoch::from_str("2024-01-01 00:00:00 UTC").unwrap()),
82+
Some(Epoch::from_str("2024-01-03 00:00:00 UTC").unwrap()),
83+
Aberration::LT,
84+
Some("LRO".to_string()),
85+
)?;
86+
87+
println!("{trajectory}");
88+
89+
// Load the Deep Space Network ground stations.
90+
// Nyx allows you to build these at runtime but it's pretty static so we can just load them from YAML.
91+
let ground_station_file: PathBuf = [
92+
env!("CARGO_MANIFEST_DIR"),
93+
"examples",
94+
"04_lro_od",
95+
"dsn-network.yaml",
96+
]
97+
.iter()
98+
.collect();
99+
100+
let devices = GroundStation::load_many(ground_station_file).unwrap();
101+
102+
// Typical OD software requires that you specify your own tracking schedule or you'll have overlapping measurements.
103+
// Nyx can build a tracking schedule for you based on the first station with access.
104+
let trkconfg_yaml: PathBuf = [
105+
env!("CARGO_MANIFEST_DIR"),
106+
"examples",
107+
"04_lro_od",
108+
"tracking-cfg.yaml",
109+
]
110+
.iter()
111+
.collect();
112+
113+
let configs: BTreeMap<String, TrkConfig> = TrkConfig::load_named(trkconfg_yaml).unwrap();
114+
115+
dbg!(&configs);
116+
117+
// Build the tracking arc simulation to generate a "standard measurement".
118+
let mut trk = TrackingArcSim::<Spacecraft, RangeDoppler, _>::with_seed(
119+
devices, trajectory, configs, 12345,
120+
)
121+
.unwrap();
122+
123+
trk.build_schedule(almanac.clone()).unwrap();
124+
let arc = trk.generate_measurements(almanac).unwrap();
125+
// Save the simulated tracking data
126+
arc.to_parquet_simple("./04_lro_simulated_tracking.parquet")?;
127+
128+
println!("{arc}");
129+
130+
Ok(())
131+
}

examples/04_lro_od/tracking-cfg.yaml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
DSS-65 Madrid:
2+
scheduler:
3+
handoff: Eager
4+
cadence: Continuous
5+
min_samples: 10
6+
sample_alignment: 10 s
7+
sampling: 1 min
8+
9+
DSS-34 Canberra:
10+
scheduler:
11+
handoff: Eager
12+
cadence: Continuous
13+
min_samples: 10
14+
sample_alignment: 10 s
15+
sampling: 1 min
16+
17+
DSS-13 Goldstone:
18+
scheduler:
19+
handoff: Eager
20+
cadence: Continuous
21+
min_samples: 10
22+
sample_alignment: 10 s
23+
sampling: 1 min

src/md/trajectory/sc_traj.rs

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,11 @@
1616
along with this program. If not, see <https://www.gnu.org/licenses/>.
1717
*/
1818

19+
use anise::astro::Aberration;
1920
use anise::constants::orientations::J2000;
21+
use anise::errors::AlmanacError;
2022
use anise::prelude::{Almanac, Frame, Orbit};
23+
use hifitime::TimeSeries;
2124
use snafu::ResultExt;
2225

2326
use super::TrajError;
@@ -40,6 +43,41 @@ use std::sync::Arc;
4043
use std::time::Instant;
4144

4245
impl Traj<Spacecraft> {
46+
/// Builds a new trajectory built from the SPICE BSP (SPK) file loaded in the provided Almanac, provided the start and stop epochs.
47+
///
48+
/// If the start and stop epochs are not provided, then the full domain of the trajectory will be used.
49+
pub fn from_bsp(
50+
target_frame: Frame,
51+
observer_frame: Frame,
52+
almanac: Arc<Almanac>,
53+
sc_template: Spacecraft,
54+
step: Duration,
55+
start_epoch: Option<Epoch>,
56+
end_epoch: Option<Epoch>,
57+
ab_corr: Option<Aberration>,
58+
name: Option<String>,
59+
) -> Result<Self, AlmanacError> {
60+
let (domain_start, domain_end) =
61+
almanac
62+
.spk_domain(target_frame.ephemeris_id)
63+
.map_err(|e| AlmanacError::Ephemeris {
64+
action: "could not fetch domain",
65+
source: Box::new(e),
66+
})?;
67+
68+
let start_epoch = start_epoch.unwrap_or(domain_start);
69+
let end_epoch = end_epoch.unwrap_or(domain_end);
70+
71+
let time_series = TimeSeries::inclusive(start_epoch, end_epoch, step);
72+
let mut states = Vec::with_capacity(time_series.len());
73+
for epoch in time_series {
74+
let orbit = almanac.transform(target_frame, observer_frame, epoch, ab_corr)?;
75+
76+
states.push(sc_template.with_orbit(orbit));
77+
}
78+
79+
Ok(Self { name, states })
80+
}
4381
/// Allows converting the source trajectory into the (almost) equivalent trajectory in another frame
4482
#[allow(clippy::map_clone)]
4583
pub fn to_frame(&self, new_frame: Frame, almanac: Arc<Almanac>) -> Result<Self, NyxError> {
@@ -129,11 +167,6 @@ impl Traj<Spacecraft> {
129167
traj.to_parquet(path, events, cfg, almanac)
130168
}
131169

132-
/// Convert this spacecraft trajectory into an Orbit trajectory, loosing all references to the spacecraft
133-
pub fn downcast(&self) -> Self {
134-
unimplemented!()
135-
}
136-
137170
/// Initialize a new spacecraft trajectory from the path to a CCSDS OEM file.
138171
///
139172
/// CCSDS OEM only contains the orbit information but Nyx builds spacecraft trajectories.

src/md/trajectory/traj.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ where
599599
f,
600600
"Trajectory {}in {} from {} to {} ({}, or {:.3} s) [{} states]",
601601
match &self.name {
602-
Some(name) => format!("of {name}"),
602+
Some(name) => format!("of {name} "),
603603
None => String::new(),
604604
},
605605
self.first().frame(),

src/od/ground_station.rs

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@ use anise::prelude::{Almanac, Frame, Orbit};
2222

2323
use super::msr::RangeDoppler;
2424
use super::noise::StochasticNoise;
25-
use super::{ODAlmanacSnafu, ODError, ODTrajSnafu, TrackingDeviceSim};
25+
use super::{ODAlmanacSnafu, ODError, ODPlanetaryDataSnafu, ODTrajSnafu, TrackingDeviceSim};
26+
use crate::cosmic::eclipse::{line_of_sight, EclipseState};
2627
use crate::errors::EventError;
2728
use crate::io::ConfigRepr;
2829
use crate::md::prelude::{Interpolatable, Traj};
@@ -157,11 +158,9 @@ impl GroundStation {
157158
}
158159

159160
/// Computes the azimuth and elevation of the provided object seen from this ground station, both in degrees.
160-
/// Also returns the ground station's orbit in the frame of the receiver
161+
/// This is a shortcut to almanac.azimuth_elevation_range_sez.
161162
pub fn azimuth_elevation_of(&self, rx: Orbit, almanac: &Almanac) -> AlmanacResult<AzElRange> {
162-
almanac
163-
.clone()
164-
.azimuth_elevation_range_sez(rx, self.to_orbit(rx.epoch, almanac).unwrap())
163+
almanac.azimuth_elevation_range_sez(rx, self.to_orbit(rx.epoch, almanac).unwrap())
165164
}
166165

167166
/// Return this ground station as an orbit in its current frame
@@ -256,6 +255,27 @@ impl TrackingDeviceSim<Spacecraft, RangeDoppler> for GroundStation {
256255
return Ok(None);
257256
}
258257

258+
// If the frame of the trajectory is different from that of the ground station, then check that there is no eclipse.
259+
if !self.frame.ephem_origin_match(rx_0.frame()) {
260+
let observer = self.to_orbit(rx_0.orbit.epoch, &almanac).unwrap();
261+
if line_of_sight(
262+
observer,
263+
rx_0.orbit,
264+
almanac
265+
.frame_from_uid(rx_0.frame())
266+
.context(ODPlanetaryDataSnafu {
267+
action: "computing line of sight",
268+
})?,
269+
&almanac,
270+
)
271+
.context(ODAlmanacSnafu {
272+
action: "computing line of sight",
273+
})? == EclipseState::Umbra
274+
{
275+
return Ok(None);
276+
}
277+
}
278+
259279
// Noises are computed at the midpoint of the integration time.
260280
let (timestamp_noise_s, range_noise_km, doppler_noise_km_s) =
261281
self.noises(epoch - integration_time * 0.5, rng)?;

src/od/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use crate::propagators::PropagationError;
2626
use crate::time::Epoch;
2727
use crate::Orbit;
2828
pub use crate::{State, TimeTagged};
29+
use anise::almanac::planetary::PlanetaryDataError;
2930
use anise::errors::AlmanacError;
3031
use hifitime::Duration;
3132
use snafu::prelude::Snafu;
@@ -204,6 +205,12 @@ pub enum ODError {
204205
source: Box<AlmanacError>,
205206
action: &'static str,
206207
},
208+
#[snafu(display("OD failed due to planetary data in Almanac: {action} {source}"))]
209+
ODPlanetaryData {
210+
#[snafu(source(from(PlanetaryDataError, Box::new)))]
211+
source: Box<PlanetaryDataError>,
212+
action: &'static str,
213+
},
207214
#[snafu(display("not enough residuals to {action}"))]
208215
ODNoResiduals { action: &'static str },
209216
}

0 commit comments

Comments
 (0)