1use crate::linalg::allocator::Allocator;
20use crate::linalg::{DefaultAllocator, DimName};
21use crate::md::trajectory::{Interpolatable, Traj};
22pub use crate::od::estimate::*;
23pub use crate::od::ground_station::*;
24use crate::od::msr::IntegrationRef;
25pub use crate::od::snc::*;
26pub use crate::od::*;
27use crate::propagators::Propagator;
28pub use crate::time::{Duration, Unit};
29use anise::prelude::Almanac;
30use indexmap::IndexSet;
31use log::{debug, error, info, warn};
32use msr::sensitivity::TrackerSensitivity;
33use snafu::prelude::*;
34use solution::kalman::KalmanVariant;
35use std::collections::BTreeMap;
36use std::marker::PhantomData;
37use std::ops::Add;
38use typed_builder::TypedBuilder;
39
40mod rejectcrit;
41use self::kalman::KalmanFilter;
42use self::msr::TrackingDataArc;
43pub use self::rejectcrit::SigmaRejection;
44mod solution;
45pub use solution::{NormalizedConsistency, ODSolution};
46mod initializers;
47
48#[derive(Clone, TypedBuilder)]
50#[builder(doc)]
51#[allow(clippy::upper_case_acronyms)]
52pub struct KalmanODProcess<
53 D: Dynamics,
54 MsrSize: DimName,
55 Accel: DimName,
56 Trk: TrackerSensitivity<D::StateType, D::StateType>,
57> where
58 D::StateType:
59 Interpolatable + Add<OVector<f64, <D::StateType as State>::Size>, Output = D::StateType>,
60 <DefaultAllocator as Allocator<<D::StateType as State>::VecLength>>::Buffer<f64>: Send,
61 <DefaultAllocator as Allocator<<D::StateType as State>::Size>>::Buffer<f64>: Copy,
62 <DefaultAllocator as Allocator<<D::StateType as State>::Size, <D::StateType as State>::Size>>::Buffer<f64>: Copy,
63 DefaultAllocator: Allocator<<D::StateType as State>::Size>
64 + Allocator<<D::StateType as State>::VecLength>
65 + Allocator<MsrSize>
66 + Allocator<MsrSize, <D::StateType as State>::Size>
67 + Allocator<<D::StateType as State>::Size, MsrSize>
68 + Allocator<MsrSize, MsrSize>
69 + Allocator<<D::StateType as State>::Size, <D::StateType as State>::Size>
70 + Allocator<Accel>
71 + Allocator<Accel, Accel>
72 + Allocator<<D::StateType as State>::Size, Accel>
73 + Allocator<Accel, <D::StateType as State>::Size>,
74{
75 pub prop: Propagator<D>,
77 #[builder(default)]
79 pub kf_variant: KalmanVariant,
80 #[builder(default, setter(strip_option))]
82 pub sigma_reject: Option<SigmaRejection>,
83 #[builder(default_code = "BTreeMap::new()")]
85 pub devices: BTreeMap<String, Trk>,
86 #[builder(default_code = "vec![]")]
88 pub process_noise: Vec<ProcessNoise<Accel>>,
89 #[builder(default_code = "1 * Unit::Minute")]
91 pub max_step: Duration,
92 #[builder(default_code = "1 * Unit::Microsecond")]
94 pub epoch_precision: Duration,
95 pub almanac: Arc<Almanac>,
96 #[builder(default_code = "PhantomData::<MsrSize>")]
97 _msr_size: PhantomData<MsrSize>,
98}
99
100impl<
101 D: Dynamics,
102 MsrSize: DimName,
103 Accel: DimName,
104 Trk: TrackerSensitivity<D::StateType, D::StateType>,
105 > KalmanODProcess<D, MsrSize, Accel, Trk>
106where
107 D::StateType:
108 Interpolatable + Add<OVector<f64, <D::StateType as State>::Size>, Output = D::StateType>,
109 <DefaultAllocator as Allocator<<D::StateType as State>::VecLength>>::Buffer<f64>: Send,
110 <DefaultAllocator as Allocator<<D::StateType as State>::Size>>::Buffer<f64>: Copy,
111 <DefaultAllocator as Allocator<<D::StateType as State>::Size, <D::StateType as State>::Size>>::Buffer<f64>: Copy,
112 DefaultAllocator: Allocator<<D::StateType as State>::Size>
113 + Allocator<<D::StateType as State>::VecLength>
114 + Allocator<MsrSize>
115 + Allocator<MsrSize, <D::StateType as State>::Size>
116 + Allocator<<D::StateType as State>::Size, MsrSize>
117 + Allocator<MsrSize, MsrSize>
118 + Allocator<<D::StateType as State>::Size, <D::StateType as State>::Size>
119 + Allocator<Accel>
120 + Allocator<Accel, Accel>
121 + Allocator<<D::StateType as State>::Size, Accel>
122 + Allocator<Accel, <D::StateType as State>::Size>
123 + Allocator<nalgebra::Const<1>, MsrSize>,
124{
125 #[allow(clippy::erasing_op)]
127 pub fn process_arc(
128 &self,
129 initial_estimate: KfEstimate<D::StateType>,
130 arc: &TrackingDataArc,
131 ) -> Result<ODSolution<D::StateType, KfEstimate<D::StateType>, MsrSize, Trk>, ODError> {
132 let mut od_sol = ODSolution::new(self.devices.clone(), arc.unique_types());
134
135 let measurements = &arc.measurements;
136 ensure!(
137 measurements.len() >= 2,
138 TooFewMeasurementsSnafu {
139 need: 2_usize,
140 action: "running a Kalman filter"
141 }
142 );
143
144 ensure!(
145 !self.max_step.is_negative() && self.max_step != Duration::ZERO,
146 StepSizeSnafu { step: self.max_step }
147 );
148
149 if MsrSize::DIM > arc.unique_types().len() {
151 error!("Filter misconfigured: expect high rejection count!");
152 error!(
153 "Arc only contains {} measurement types, but filter configured for {}.",
154 arc.unique_types().len(),
155 MsrSize::DIM
156 );
157 error!("Filter should be configured for these numbers to match.");
158 error!("Consider running subsequent arcs if ground stations provide different measurements or switch to Scalar processing.");
159 }
160
161 let mut cfg_errors = vec![];
162 for tracker in &arc.unique_aliases() {
163 if let Some(device) = self.devices.get(tracker) {
164 if let Err(e) =
165 device.is_compatible(tracker, &arc.clone().filter_by_tracker(tracker.clone())) {
166 cfg_errors.push(e);
167 }
168 } else {
169 error!("Tracker `{tracker}` from TrackingDataArc is not configured in the OD Process.");
170 error!("Measurements from `{tracker}` will be ignored!");
171 }
172 }
173 if !cfg_errors.is_empty() {
174 let msg = cfg_errors.iter().map(|e| e.to_string()).collect::<Vec<String>>().join("\n---\n");
176 return Err(ODError::ODConfigError { source: ConfigError::InvalidConfig { msg } })
177 }
178
179 let num_msrs = measurements.len();
181
182 let prop = self.prop.clone();
184 let mut prop_instance = prop.with(initial_estimate.nominal_state().with_stm(), self.almanac.clone()).quiet();
185
186 if !prop_instance.fixed_step {
188 prop_instance.set_step(self.max_step, false);
189 }
190
191 let prop_time = arc.end_epoch().unwrap() - initial_estimate.epoch();
192 info!("Navigation propagating for a total of {prop_time} with step size {}", self.max_step);
193
194 let resid_crit = if arc.force_reject {
195 warn!("Rejecting all measurements from {arc} (force_reject is True in TrackingDataArc)");
196 Some(SigmaRejection { num_sigmas: 0.0 })
197 } else {
198 self.sigma_reject
199 };
200
201 let mut epoch = prop_instance.state.epoch();
202
203 let mut reported = [false; 11];
204 reported[0] = true; info!(
206 "Processing {num_msrs} measurement epochs from {:?}",
207 arc.unique_aliases()
208 );
209
210 let mut kf = KalmanFilter::<D::StateType, Accel> {
212 prev_estimate: initial_estimate,
213 process_noise: self.process_noise.clone(),
214 variant: self.kf_variant,
215 prev_used_snc: 0,
216 };
217
218 kf.initialize_process_noises();
219
220 let mut devices = self.devices.clone();
221
222 let mut traj: Traj<D::StateType> = Traj::new();
224
225 let mut msr_accepted_cnt: usize = 0;
226 let mut msr_rejected_cnt: usize = 0;
227 let mut unknown_trackers = IndexSet::new();
228 let tick = Epoch::now().unwrap();
229
230 for (msr_cnt, msr) in measurements.iter().enumerate() {
231 let next_msr_epoch = msr.epoch;
232
233 loop {
235 let delta_t = next_msr_epoch - epoch;
236
237 let next_step_size = delta_t.min(prop_instance.step_size).min(self.max_step);
239
240 let keep_idx = traj.states.partition_point(|s| s.epoch() < epoch);
242 traj.states.truncate(keep_idx);
243
244 debug!("propagate for {next_step_size} (Δt to next msr: {delta_t})");
245 let (latest_state, traj_covar) = prop_instance
246 .for_duration_with_traj(next_step_size)
247 .context(ODPropSnafu)?;
248
249 for state in traj_covar.states {
250 traj.states.push(state);
253 }
254
255 let nominal_state = prop_instance.state;
259 epoch = nominal_state.epoch();
261
262 if (nominal_state.epoch() - next_msr_epoch).abs() < self.epoch_precision {
264 prop_instance.state.set_epoch(next_msr_epoch);
268
269 if msr.rejected {
270 debug!("Skipping manually rejected measurement at {epoch}");
271 let est = kf.time_update(nominal_state)?;
272 od_sol.push_time_update(est);
273 prop_instance.state.reset_stm();
274 msr_rejected_cnt += 1;
275 } else {
276 match devices.get_mut(&msr.tracker) {
278 Some(device) => {
279 let msr_types = device.measurement_types().clone();
280
281 let pre_lookahead_len = traj.states.len();
283 if let Some(dop_cfg) = msr.doppler_config && dop_cfg.integration_ref != IntegrationRef::End {
284 let lookahead_by = match dop_cfg.integration_ref {
285 IntegrationRef::Start => dop_cfg.integration_time,
286 IntegrationRef::Middle => dop_cfg.integration_time * 0.5,
287 _ => unreachable!()
288 };
289 let mut lookahead_prop = prop.with(latest_state, self.almanac.clone()).quiet();
290 let (_, lookahead_traj) = lookahead_prop
291 .for_duration_with_traj(lookahead_by)
292 .context(ODPropSnafu)?;
293
294 for state in lookahead_traj.states.into_iter().filter(|s| s.epoch() > latest_state.epoch()) {
295 traj.states.push(state);
296 }
297 }
298
299 let prior_nominal_state = prop_instance.state;
301 let mut current_state_estimate = prior_nominal_state;
302 let mut any_measurement_accepted = false;
303
304 let windows = msr_types.len() / MsrSize::DIM;
306 for wno in 0..=windows {
307 let cur_msr_types = msr_types
310 .iter()
311 .copied()
312 .skip(wno * MsrSize::DIM)
313 .take(MsrSize::DIM)
314 .collect::<IndexSet<_>>();
315
316 if cur_msr_types.is_empty() {
317 break;
319 }
320
321 if !msr.availability(&cur_msr_types)
323 .iter()
324 .any(|avail| *avail)
325 {
326 continue;
327 }
328
329 let mut real_obs: OVector<f64, MsrSize> =
331 msr.observation(&cur_msr_types);
332
333 for val in real_obs.iter().copied() {
335 ensure!(
336 val.is_finite(),
337 InvalidMeasurementSnafu {
338 epoch: next_msr_epoch,
339 val
340 }
341 );
342 }
343
344 let h_tilde = device.h_tilde::<MsrSize>(
349 msr,
350 &cur_msr_types,
351 &prior_nominal_state,
352 &self.almanac,
353 )?;
354
355 let measurement_covar = device
356 .measurement_covar_matrix(&cur_msr_types, epoch)?;
357
358 let computed_meas_res = device.measure(epoch, &traj, None, &self.almanac);
361
362 if let Some(computed_meas) = computed_meas_res?
363 {
364 let obs_bias = device.measurement_bias_vector::<MsrSize>(
366 &cur_msr_types,
367 epoch,
368 )?;
369
370 let mut computed_obs = computed_meas
371 .observation::<MsrSize>(&cur_msr_types)
372 - obs_bias;
373
374 if let Some(moduli) = &arc.moduli {
376 let mut obs_ambiguity =
377 OVector::<f64, MsrSize>::zeros();
378
379 for (i, msr_type) in cur_msr_types.iter().enumerate() {
380 if let Some(modulus) = moduli.get(msr_type) {
381 let k = computed_obs[i].div_euclid(*modulus);
382 obs_ambiguity[i] = k * *modulus;
384 }
385 }
386 real_obs += obs_ambiguity;
387 }
388
389 let delta_state = current_state_estimate.to_state_vector() - prior_nominal_state.to_state_vector();
391 let obs_shift = &h_tilde * delta_state;
392 computed_obs += obs_shift;
393
394 let (estimate, mut residual, gain) = kf.measurement_update(
396 current_state_estimate,
397 real_obs,
398 computed_obs,
399 measurement_covar,
400 h_tilde,
401 resid_crit,
402 )?;
403
404 debug!(
405 "processed measurement #{msr_cnt} for {cur_msr_types:?} @ {epoch} from {}",
406 device.name()
407 );
408
409 if residual.rejected {
410 msr_rejected_cnt += 1;
411 } else {
412 msr_accepted_cnt += 1;
413 current_state_estimate = estimate.state();
414 any_measurement_accepted = true;
415 }
416
417
418 residual.tracker = Some(device.name());
419 residual.msr_types = cur_msr_types;
420 od_sol.push_measurement_update(estimate, residual, gain);
421 } else {
422 debug!(
423 "Device {} does not expect measurement at {epoch}, skipping",
424 msr.tracker
425 );
426 msr_rejected_cnt += 1;
427 }
428 }
429
430 traj.states.truncate(pre_lookahead_len);
432
433 if any_measurement_accepted && kf.replace_state() {
434 prop_instance.state = current_state_estimate;
436 traj.states.pop();
437 traj.states.push(prop_instance.state);
438 }
439
440 prop_instance.state.reset_stm();
442 }
443 None => {
444 if !unknown_trackers.contains(&msr.tracker) {
445 error!(
446 "Tracker {} is not in the list of configured devices",
447 msr.tracker
448 );
449 unknown_trackers.insert(msr.tracker.clone());
450 }
451 }
452 }
453 }
454
455 let msr_prct = (10.0 * (msr_cnt as f64) / (num_msrs as f64)) as usize;
456 if !reported[msr_prct] {
457 let msg = format!(
458 "{:>3}% done - {epoch} - {msr_accepted_cnt:.0} measurements accepted, {:.0} rejected",
459 10 * msr_prct, msr_rejected_cnt
460 );
461 if msr_accepted_cnt < msr_rejected_cnt {
462 warn!("{msg}");
463 } else {
464 info!("{msg}");
465 }
466 reported[msr_prct] = true;
467 }
468
469 break;
470 } else {
471 let est = kf.time_update(nominal_state)?;
474 od_sol.push_time_update(est);
475 prop_instance.state.reset_stm();
476 }
477 }
478 }
479
480 if !reported[10] {
482 let tock_time = Epoch::now().unwrap() - tick;
483 info!(
484 "100% done - {epoch} - {msr_accepted_cnt} measurements accepted, {msr_rejected_cnt} rejected (done in {tock_time})",
485 );
486 }
487
488 Ok(od_sol)
489 }
490
491 pub fn predict_until(
493 &self,
494 initial_estimate: KfEstimate<D::StateType>,
495 end_epoch: Epoch,
496 ) -> Result<ODSolution<D::StateType, KfEstimate<D::StateType>, MsrSize, Trk>, ODError> {
497 let mut od_sol = ODSolution::new(self.devices.clone(), IndexSet::new());
499
500 od_sol.push_time_update(initial_estimate);
501
502 let prop = self.prop.clone();
504 let mut prop_instance = prop.with(initial_estimate.nominal_state().with_stm(), self.almanac.clone()).quiet();
505
506
507 let mut kf = KalmanFilter::<D::StateType, Accel> {
509 prev_estimate: initial_estimate,
510 process_noise: self.process_noise.clone(),
511 variant: self.kf_variant,
512 prev_used_snc: 0,
513 };
514
515 let prop_time = end_epoch - kf.previous_estimate().epoch();
516 info!("Mapping covariance for {prop_time} every {} until {end_epoch}", self.max_step);
517
518 loop {
519 let nominal_state = prop_instance.for_duration(self.max_step).context(ODPropSnafu)?;
520 let epoch = nominal_state.epoch();
523 debug!("time update {epoch}");
525 let est = kf.time_update(nominal_state)?;
526 od_sol.push_time_update(est);
527 prop_instance.state.reset_stm();
528 if epoch >= end_epoch {
529 break;
530 }
531 }
532
533 Ok(od_sol)
534 }
535
536 pub fn predict_for(
538 &self,
539 initial_estimate: KfEstimate<D::StateType>,
540 duration: Duration,
541 ) -> Result<ODSolution<D::StateType, KfEstimate<D::StateType>, MsrSize, Trk>, ODError> {
542 let end_epoch = initial_estimate.nominal_state().epoch() + duration;
543 self.predict_until(initial_estimate, end_epoch)
544 }
545}