use crate::time::JulianDate;
use super::error::PodObservationsError;
use crate::pod::observation::obs_trait::{AnyObservation, CartesianState, ObsResidual};
use crate::pod::observation::provider_bundle::ProviderBundle;
pub struct ObservationBatch {
obs: Vec<Box<dyn AnyObservation>>,
}
impl ObservationBatch {
pub fn new() -> Self {
Self { obs: Vec::new() }
}
pub fn push(&mut self, o: Box<dyn AnyObservation>) {
self.obs.push(o);
}
pub fn len(&self) -> usize {
self.obs.len()
}
pub fn is_empty(&self) -> bool {
self.obs.is_empty()
}
pub fn epochs(&self) -> Vec<JulianDate> {
self.obs.iter().map(|o| o.epoch()).collect()
}
pub fn residuals_at_epoch(
&self,
epoch: JulianDate,
) -> Vec<Result<ObsResidual, PodObservationsError>> {
self.obs
.iter()
.filter(|o| epochs_match(o.epoch(), epoch))
.map(|_| Err(PodObservationsError::LightTimeNotConverged))
.collect()
}
pub fn residuals_at_epoch_with(
&self,
epoch: JulianDate,
state: &CartesianState,
providers: &dyn ProviderBundle,
) -> Vec<Result<ObsResidual, PodObservationsError>> {
self.obs
.iter()
.filter(|o| epochs_match(o.epoch(), epoch))
.map(|o| o.any_residual(state, providers))
.collect()
}
}
impl Default for ObservationBatch {
fn default() -> Self {
Self::new()
}
}
fn epochs_match(a: JulianDate, b: JulianDate) -> bool {
const TOL_DAYS: f64 = 5.8e-12;
(a.value() - b.value()).abs() < TOL_DAYS
}
#[cfg(test)]
mod tests {
use super::*;
use crate::astro::dynamics::{Position, Velocity};
use crate::coordinates::frames::GCRS;
use crate::pod::observation::obs_trait::{ObsType, Observation};
use crate::pod::observation::provider_bundle::NullProviderBundle;
struct ConstObs {
epoch: JulianDate,
residual: f64,
}
impl Observation for ConstObs {
type Residual = f64;
fn residual(
&self,
_: &CartesianState,
_: &dyn ProviderBundle,
) -> Result<f64, PodObservationsError> {
Ok(self.residual)
}
fn obs_type(&self) -> ObsType {
ObsType::GnssPseudorange
}
fn epoch(&self) -> JulianDate {
self.epoch
}
fn sigma(&self) -> qtty::Meter {
qtty::Meter::new(1.0)
}
}
fn make_state(epoch: JulianDate) -> CartesianState {
CartesianState::new(
epoch.to_j2000s(),
Position::<GCRS>::new(7_000.0, 0.0, 0.0),
Velocity::<GCRS>::new(0.0, 7.5, 0.0),
)
}
#[test]
fn batch_residuals_correct_epoch() {
let epoch = JulianDate::new(2_451_545.0);
let other = JulianDate::new(2_451_546.0);
let state = make_state(epoch);
let mut batch = ObservationBatch::new();
batch.push(Box::new(ConstObs {
epoch,
residual: 1.0,
}));
batch.push(Box::new(ConstObs {
epoch: other,
residual: 0.0,
}));
let res = batch.residuals_at_epoch_with(epoch, &state, &NullProviderBundle);
assert_eq!(res.len(), 1);
if let Ok(ObsResidual::Scalar(v)) = &res[0] {
assert!((*v - 1.0).abs() < 1e-10);
} else {
panic!("Expected Scalar residual");
}
}
#[test]
fn batch_empty_for_unknown_epoch() {
let epoch = JulianDate::new(2_451_545.0);
let query = JulianDate::new(2_451_600.0);
let state = make_state(epoch);
let mut batch = ObservationBatch::new();
batch.push(Box::new(ConstObs {
epoch,
residual: 1.0,
}));
let res = batch.residuals_at_epoch_with(query, &state, &NullProviderBundle);
assert!(res.is_empty());
}
}