use std::cmp::Ordering;
use std::collections::BTreeMap;
use std::fmt;
use std::io;
use std::io::BufReader;
use std::io::prelude::*;
use std::path::Path;
use crate::eop::c04_parser::parse_c04_line;
use crate::eop::eop_provider::EarthOrientationProvider;
use crate::eop::eop_types::{EOPExtrapolation, EOPType};
use crate::eop::standard_parser::parse_standard_line;
use crate::utils::BraheError;
type EOPData = (f64, f64, f64, Option<f64>, Option<f64>, Option<f64>);
type EOPDataMap = BTreeMap<EOPKey, EOPData>;
static PACKAGED_C04_FILE: &[u8] = include_bytes!("../../data/eop/EOP_20_C04_one_file_1962-now.txt");
static PACKAGED_STANDARD2000_FILE: &[u8] = include_bytes!("../../data/eop/finals.all.iau2000.txt");
#[derive(Clone, PartialEq)]
struct EOPKey(f64);
impl PartialOrd for EOPKey {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for EOPKey {
fn cmp(&self, other: &Self) -> Ordering {
self.0.partial_cmp(&other.0).unwrap_or(Ordering::Equal)
}
}
impl Eq for EOPKey {}
#[derive(Clone)]
pub struct FileEOPProvider {
initialized: bool,
pub eop_type: EOPType,
data: EOPDataMap,
pub extrapolate: EOPExtrapolation,
pub interpolate: bool,
pub mjd_min: f64,
pub mjd_max: f64,
pub mjd_last_lod: f64,
pub mjd_last_dxdy: f64,
}
impl fmt::Display for FileEOPProvider {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"FileEOPProvider - type: {}, {} entries, mjd_min: {}, mjd_max: {}, mjd_last_lod: \
{}, mjd_last_dxdy: {}, extrapolation: {}, interpolation: {}",
self.eop_type(),
self.len(),
self.mjd_min(),
self.mjd_max(),
self.mjd_last_lod(),
self.mjd_last_dxdy(),
self.extrapolation(),
self.interpolation()
)
}
}
impl fmt::Debug for FileEOPProvider {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"FileEOPProvider<Type: {}, Length: {}, mjd_min: {}, mjd_max: {}, mjd_last_lod: \
{}, mjd_last_dxdy: {}, extrapolation: {}, interpolation: {}>",
self.eop_type(),
self.len(),
self.mjd_min(),
self.mjd_max(),
self.mjd_last_lod(),
self.mjd_last_dxdy(),
self.extrapolation(),
self.interpolation()
)
}
}
fn detect_eop_file_type(filepath: &Path) -> Result<EOPType, io::Error> {
let file = std::fs::File::open(filepath)?;
let reader = BufReader::new(file);
let lines: Vec<String> = reader.lines().collect::<Result<_, _>>()?;
if let Some(line) = lines.get(1)
&& line.contains("C04")
{
if let Some(line) = lines.get(6)
&& parse_c04_line(line.to_owned()).is_ok()
{
return Ok(EOPType::C04);
}
}
if let Some(line) = lines.first()
&& parse_standard_line(line.to_owned()).is_ok()
{
return Ok(EOPType::StandardBulletinA);
}
Ok(EOPType::Unknown)
}
impl Default for FileEOPProvider {
fn default() -> Self {
Self::new()
}
}
impl FileEOPProvider {
pub fn new() -> Self {
let data: EOPDataMap = BTreeMap::new();
Self {
initialized: false,
eop_type: EOPType::Unknown,
data,
extrapolate: EOPExtrapolation::Zero,
interpolate: false,
mjd_min: 0.0,
mjd_max: 0.0,
mjd_last_lod: 0.0,
mjd_last_dxdy: 0.0,
}
}
pub fn from_c04_file(
filepath: &Path,
interpolate: bool,
extrapolate: EOPExtrapolation,
) -> Result<Self, BraheError> {
let file = std::fs::File::open(filepath)?;
let reader = BufReader::new(file);
Self::from_c04_file_bufreader(reader, interpolate, extrapolate)
}
fn from_c04_file_bufreader<T: Read>(
reader: BufReader<T>,
interpolate: bool,
extrapolate: EOPExtrapolation,
) -> Result<Self, BraheError> {
let mut mjd_min = 0.0;
let mut mjd_max = 0.0;
let mut data: EOPDataMap = BTreeMap::new();
for (line_num, line_str) in reader.lines().enumerate() {
if line_num < 6 {
continue;
}
let line = match line_str {
Ok(l) => l,
Err(e) => {
return Err(BraheError::EOPError(format!(
"Failed to parse EOP file on line {}: {}",
line_num, e
)));
}
};
let eop_data = parse_c04_line(line)?;
let mjd = eop_data.0;
if mjd_min == 0.0 {
mjd_min = mjd;
}
if (mjd_max == 0.0) || (mjd > mjd_max) {
mjd_max = mjd;
}
data.insert(
EOPKey(mjd),
(
eop_data.1, eop_data.2, eop_data.3, eop_data.4, eop_data.5, eop_data.6,
),
);
}
if data.is_empty() {
return Err(BraheError::EOPError(
"EOP C04 file contained no valid data entries (file may be empty or corrupt)"
.to_string(),
));
}
Ok(Self {
initialized: true,
eop_type: EOPType::C04,
data,
extrapolate,
interpolate,
mjd_min,
mjd_max,
mjd_last_lod: mjd_max,
mjd_last_dxdy: mjd_max,
})
}
pub fn from_standard_file(
filepath: &Path,
interpolate: bool,
extrapolate: EOPExtrapolation,
) -> Result<Self, BraheError> {
let file = std::fs::File::open(filepath)?;
let reader = BufReader::new(file);
Self::from_standard_file_bufreader(reader, interpolate, extrapolate)
}
fn from_standard_file_bufreader<T: Read>(
reader: BufReader<T>,
interpolate: bool,
extrapolate: EOPExtrapolation,
) -> Result<Self, BraheError> {
let mut mjd_min = 0.0;
let mut mjd_max = 0.0;
let mut mjd_last_lod = 0.0;
let mut mjd_last_dxdy = 0.0;
let mut data: EOPDataMap = BTreeMap::new();
for (line_num, line_str) in reader.lines().enumerate() {
let line = match line_str {
Ok(l) => l,
Err(e) => {
return Err(BraheError::EOPError(format!(
"Failed to parse EOP file on line {}: {}",
line_num, e
)));
}
};
let eop_data = match parse_standard_line(line) {
Ok(data) => data,
Err(e) => {
if e.to_string()
.contains("Failed to parse pm_x from ' '")
{
break;
} else {
return Err(e);
}
}
};
let mjd = eop_data.0;
if mjd_min == 0.0 {
mjd_min = mjd;
}
if (mjd_max == 0.0) || (mjd > mjd_max) {
mjd_max = mjd;
}
if eop_data.6.is_some() {
mjd_last_lod = mjd;
}
if eop_data.4.is_some() && eop_data.5.is_some() {
mjd_last_dxdy = mjd;
}
data.insert(
EOPKey(mjd),
(
eop_data.1, eop_data.2, eop_data.3, eop_data.4, eop_data.5, eop_data.6,
),
);
}
if data.is_empty() {
return Err(BraheError::EOPError(
"EOP Standard file contained no valid data entries (file may be empty or corrupt)"
.to_string(),
));
}
Ok(Self {
initialized: true,
eop_type: EOPType::StandardBulletinA,
data,
extrapolate,
interpolate,
mjd_min,
mjd_max,
mjd_last_lod,
mjd_last_dxdy,
})
}
pub fn from_file(
filepath: &Path,
interpolate: bool,
extrapolate: EOPExtrapolation,
) -> Result<Self, BraheError> {
match detect_eop_file_type(filepath)? {
EOPType::C04 => Self::from_c04_file(filepath, interpolate, extrapolate),
EOPType::StandardBulletinA => {
Self::from_standard_file(filepath, interpolate, extrapolate)
}
_ => Err(BraheError::EOPError(format!(
"File does not match supported EOP file format: {}",
filepath.display()
))),
}
}
pub fn from_default_c04(
interpolate: bool,
extrapolate: EOPExtrapolation,
) -> Result<Self, BraheError> {
let reader = BufReader::new(PACKAGED_C04_FILE);
Self::from_c04_file_bufreader(reader, interpolate, extrapolate)
}
pub fn from_default_standard(
interpolate: bool,
extrapolate: EOPExtrapolation,
) -> Result<Self, BraheError> {
let reader = BufReader::new(PACKAGED_STANDARD2000_FILE);
Self::from_standard_file_bufreader(reader, interpolate, extrapolate)
}
pub fn from_default_file(
eop_type: EOPType,
interpolate: bool,
extrapolate: EOPExtrapolation,
) -> Result<Self, BraheError> {
match eop_type {
EOPType::C04 => Self::from_default_c04(interpolate, extrapolate),
EOPType::StandardBulletinA => Self::from_default_standard(interpolate, extrapolate),
_ => Err(BraheError::EOPError(format!(
"Unsupported EOP file type: {:?}",
eop_type
))),
}
}
fn get_data(&self, mjd: f64) -> Result<&EOPData, BraheError> {
self.data.get(&EOPKey(mjd)).ok_or_else(|| {
BraheError::EOPError(format!(
"EOP data missing for MJD {} (data may be corrupt or incomplete)",
mjd
))
})
}
}
fn require_eop_field(value: Option<f64>, field_name: &str) -> Result<f64, BraheError> {
value.ok_or_else(|| {
BraheError::EOPError(format!(
"Missing {} value in EOP data (file may be corrupt or incomplete)",
field_name
))
})
}
impl EarthOrientationProvider for FileEOPProvider {
fn is_initialized(&self) -> bool {
self.initialized
}
fn len(&self) -> usize {
self.data.len()
}
fn eop_type(&self) -> EOPType {
self.eop_type
}
fn extrapolation(&self) -> EOPExtrapolation {
self.extrapolate
}
fn interpolation(&self) -> bool {
self.interpolate
}
fn mjd_min(&self) -> f64 {
self.mjd_min
}
fn mjd_max(&self) -> f64 {
self.mjd_max
}
fn mjd_last_lod(&self) -> f64 {
self.mjd_last_lod
}
fn mjd_last_dxdy(&self) -> f64 {
self.mjd_last_dxdy
}
fn get_ut1_utc(&self, mjd: f64) -> Result<f64, BraheError> {
if self.initialized {
if mjd < self.mjd_min {
match self.extrapolate {
EOPExtrapolation::Zero => Ok(0.0),
EOPExtrapolation::Hold => Ok(self.get_data(self.mjd_min)?.2),
EOPExtrapolation::Error => Err(BraheError::OutOfBoundsError(format!(
"Attempted EOP retrieval before start of loaded data. Accessed: {}, Min MJD: {}",
mjd, self.mjd_min
))),
}
} else if mjd <= self.mjd_max {
if self.interpolate {
let prev_opt = self.data.range(..=EOPKey(mjd)).next_back();
let next_opt = self.data.range(EOPKey(mjd)..).next();
match (prev_opt, next_opt) {
(Some((t1_key, data1)), Some((t2_key, data2))) => {
let t1 = t1_key.0;
let t2 = t2_key.0;
let y1 = data1.2;
let y2 = data2.2;
if t1 == t2 {
Ok(y1)
} else {
Ok((y2 - y1) / (t2 - t1) * (mjd - t1) + y1)
}
}
(Some((_t1_key, data1)), None) => Ok(data1.2),
(None, Some((_t2_key, data2))) => Ok(data2.2),
(None, None) => Err(BraheError::EOPError(String::from(
"No EOP data available for interpolation",
))),
}
} else if let Some(data) = self.data.get(&EOPKey(mjd)) {
Ok(data.2)
} else {
match self.data.range(..=EOPKey(mjd)).next_back() {
Some((_, data)) => Ok(data.2),
None => Ok(self.get_data(self.mjd_min)?.2),
}
}
} else {
match self.extrapolate {
EOPExtrapolation::Zero => Ok(0.0),
EOPExtrapolation::Hold => Ok(self.get_data(self.mjd_max)?.2),
EOPExtrapolation::Error => Err(BraheError::OutOfBoundsError(format!(
"Attempted EOP retrieval beyond end of loaded data. Accessed: {}, Max MJD: {}",
mjd, self.mjd_max
))),
}
}
} else {
Err(BraheError::EOPError(String::from(
"EOP provider not initialized",
)))
}
}
fn get_pm(&self, mjd: f64) -> Result<(f64, f64), BraheError> {
if self.initialized {
if mjd < self.mjd_min {
match self.extrapolate {
EOPExtrapolation::Zero => Ok((0.0, 0.0)),
EOPExtrapolation::Hold => {
let first = self.get_data(self.mjd_min)?;
Ok((first.0, first.1))
}
EOPExtrapolation::Error => Err(BraheError::OutOfBoundsError(format!(
"Attempted EOP retrieval before start of loaded data. Accessed: {}, Min MJD: {}",
mjd, self.mjd_min
))),
}
} else if mjd <= self.mjd_max {
if self.interpolate {
let prev_opt = self.data.range(..=EOPKey(mjd)).next_back();
let next_opt = self.data.range(EOPKey(mjd)..).next();
match (prev_opt, next_opt) {
(Some((t1_key, data1)), Some((t2_key, data2))) => {
let t1 = t1_key.0;
let t2 = t2_key.0;
let pm_x1 = data1.0;
let pm_x2 = data2.0;
let pm_y1 = data1.1;
let pm_y2 = data2.1;
if t1 == t2 {
Ok((pm_x1, pm_y1))
} else {
Ok((
(pm_x2 - pm_x1) / (t2 - t1) * (mjd - t1) + pm_x1,
(pm_y2 - pm_y1) / (t2 - t1) * (mjd - t1) + pm_y1,
))
}
}
(Some((_t1_key, data1)), None) => Ok((data1.0, data1.1)),
(None, Some((_t2_key, data2))) => Ok((data2.0, data2.1)),
(None, None) => Err(BraheError::EOPError(String::from(
"No EOP data available for interpolation",
))),
}
} else if let Some(data) = self.data.get(&EOPKey(mjd)) {
Ok((data.0, data.1))
} else {
match self.data.range(..=EOPKey(mjd)).next_back() {
Some((_, data)) => Ok((data.0, data.1)),
None => {
let first = self.get_data(self.mjd_min)?;
Ok((first.0, first.1))
}
}
}
} else {
match self.extrapolate {
EOPExtrapolation::Zero => Ok((0.0, 0.0)),
EOPExtrapolation::Hold => {
let last = self.get_data(self.mjd_max)?;
Ok((last.0, last.1))
}
EOPExtrapolation::Error => Err(BraheError::OutOfBoundsError(format!(
"Attempted EOP retrieval beyond end of loaded data. Accessed: {}, Max MJD: {}",
mjd, self.mjd_max
))),
}
}
} else {
Err(BraheError::EOPError(String::from(
"EOP provider not initialized",
)))
}
}
fn get_dxdy(&self, mjd: f64) -> Result<(f64, f64), BraheError> {
if self.initialized {
if mjd < self.mjd_min {
match self.extrapolate {
EOPExtrapolation::Zero => Ok((0.0, 0.0)),
EOPExtrapolation::Hold => {
let first = self.get_data(self.mjd_min)?;
Ok((
require_eop_field(first.3, "dX")?,
require_eop_field(first.4, "dY")?,
))
}
EOPExtrapolation::Error => Err(BraheError::OutOfBoundsError(format!(
"Attempted EOP retrieval before start of loaded data. Accessed: {}, Min MJD: {}",
mjd, self.mjd_min
))),
}
} else if mjd <= self.mjd_last_dxdy {
if self.interpolate {
let prev_opt = self.data.range(..=EOPKey(mjd)).next_back();
let next_opt = self.data.range(EOPKey(mjd)..).next();
match (prev_opt, next_opt) {
(Some((t1_key, data1)), Some((t2_key, data2))) => {
let t1 = t1_key.0;
let t2 = t2_key.0;
let dx1 = require_eop_field(data1.3, "dX")?;
let dx2 = require_eop_field(data2.3, "dX")?;
let dy1 = require_eop_field(data1.4, "dY")?;
let dy2 = require_eop_field(data2.4, "dY")?;
if t1 == t2 {
Ok((dx1, dy1))
} else {
Ok((
(dx2 - dx1) / (t2 - t1) * (mjd - t1) + dx1,
(dy2 - dy1) / (t2 - t1) * (mjd - t1) + dy1,
))
}
}
(Some((_t1_key, data1)), None) => Ok((
require_eop_field(data1.3, "dX")?,
require_eop_field(data1.4, "dY")?,
)),
(None, Some((_t2_key, data2))) => Ok((
require_eop_field(data2.3, "dX")?,
require_eop_field(data2.4, "dY")?,
)),
(None, None) => Err(BraheError::EOPError(String::from(
"No EOP data available for interpolation",
))),
}
} else if let Some(data) = self.data.get(&EOPKey(mjd)) {
Ok((
require_eop_field(data.3, "dX")?,
require_eop_field(data.4, "dY")?,
))
} else {
match self.data.range(..=EOPKey(mjd)).next_back() {
Some((_, data)) => Ok((
require_eop_field(data.3, "dX")?,
require_eop_field(data.4, "dY")?,
)),
None => {
let first = self.get_data(self.mjd_min)?;
Ok((
require_eop_field(first.3, "dX")?,
require_eop_field(first.4, "dY")?,
))
}
}
}
} else {
match self.extrapolate {
EOPExtrapolation::Zero => Ok((0.0, 0.0)),
EOPExtrapolation::Hold => {
let last = self.get_data(self.mjd_last_dxdy)?;
Ok((
require_eop_field(last.3, "dX")?,
require_eop_field(last.4, "dY")?,
))
}
EOPExtrapolation::Error => Err(BraheError::OutOfBoundsError(format!(
"Attempted EOP retrieval beyond end of loaded data. Accessed: {}, Max MJD: {}",
mjd, self.mjd_max
))),
}
}
} else {
Err(BraheError::EOPError(String::from(
"EOP provider not initialized",
)))
}
}
fn get_lod(&self, mjd: f64) -> Result<f64, BraheError> {
if self.initialized {
if mjd < self.mjd_min {
match self.extrapolate {
EOPExtrapolation::Zero => Ok(0.0),
EOPExtrapolation::Hold => {
Ok(require_eop_field(self.get_data(self.mjd_min)?.5, "LOD")?)
}
EOPExtrapolation::Error => Err(BraheError::OutOfBoundsError(format!(
"Attempted EOP retrieval before start of loaded data. Accessed: {}, Min MJD: {}",
mjd, self.mjd_min
))),
}
} else if mjd <= self.mjd_last_lod {
if self.interpolate {
let prev_opt = self.data.range(..=EOPKey(mjd)).next_back();
let next_opt = self.data.range(EOPKey(mjd)..).next();
match (prev_opt, next_opt) {
(Some((t1_key, data1)), Some((t2_key, data2))) => {
let t1 = t1_key.0;
let t2 = t2_key.0;
let y1 = require_eop_field(data1.5, "LOD")?;
let y2 = require_eop_field(data2.5, "LOD")?;
if t1 == t2 {
Ok(y1)
} else {
Ok((y2 - y1) / (t2 - t1) * (mjd - t1) + y1)
}
}
(Some((_t1_key, data1)), None) => Ok(require_eop_field(data1.5, "LOD")?),
(None, Some((_t2_key, data2))) => Ok(require_eop_field(data2.5, "LOD")?),
(None, None) => Err(BraheError::EOPError(String::from(
"No EOP data available for interpolation",
))),
}
} else if let Some(data) = self.data.get(&EOPKey(mjd)) {
Ok(require_eop_field(data.5, "LOD")?)
} else {
match self.data.range(..=EOPKey(mjd)).next_back() {
Some((_, data)) => Ok(require_eop_field(data.5, "LOD")?),
None => Ok(require_eop_field(self.get_data(self.mjd_min)?.5, "LOD")?),
}
}
} else {
match self.extrapolate {
EOPExtrapolation::Zero => Ok(0.0),
EOPExtrapolation::Hold => Ok(require_eop_field(
self.get_data(self.mjd_last_lod)?.5,
"LOD",
)?),
EOPExtrapolation::Error => Err(BraheError::OutOfBoundsError(format!(
"Attempted EOP retrieval beyond end of loaded data. Accessed: {}, Max MJD: {}",
mjd, self.mjd_max
))),
}
}
} else {
Err(BraheError::EOPError(String::from(
"EOP provider not initialized",
)))
}
}
#[allow(non_snake_case)]
fn get_eop(&self, mjd: f64) -> Result<(f64, f64, f64, f64, f64, f64), BraheError> {
let (pm_x, pm_y) = self.get_pm(mjd)?;
let ut1_utc = self.get_ut1_utc(mjd)?;
let (dX, dY) = self.get_dxdy(mjd)?;
let lod = self.get_lod(mjd)?;
Ok((pm_x, pm_y, ut1_utc, dX, dY, lod))
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
mod tests {
use std::env;
use approx::assert_abs_diff_eq;
use crate::constants::AS2RAD;
use super::*;
fn setup_test_eop(
eop_interpolation: bool,
eop_extrapolation: EOPExtrapolation,
) -> FileEOPProvider {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let filepath = Path::new(&manifest_dir)
.join("test_assets")
.join("finals.all.iau2000.txt");
let eop = FileEOPProvider::from_file(&filepath, eop_interpolation, eop_extrapolation)
.expect("Failed to load EOP file for tests");
assert!(eop.initialized);
eop
}
#[test]
fn test_detect_eop_file_type() {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let filepath = Path::new(&manifest_dir).join("test_assets");
let c04_file = "EOP_20_C04_one_file_1962-now.txt";
let standard_file = "finals.all.iau2000.txt";
let unknown_file = "bad_eop_file.txt";
assert_eq!(
detect_eop_file_type(&filepath.clone().join(c04_file)).unwrap(),
EOPType::C04
);
assert_eq!(
detect_eop_file_type(&filepath.clone().join(standard_file)).unwrap(),
EOPType::StandardBulletinA
);
assert_eq!(
detect_eop_file_type(&filepath.clone().join(unknown_file)).unwrap(),
EOPType::Unknown
);
}
#[test]
fn test_from_c04_file() {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let filepath = Path::new(&manifest_dir)
.join("test_assets")
.join("EOP_20_C04_one_file_1962-now.txt");
let eop = FileEOPProvider::from_file(&filepath, true, EOPExtrapolation::Hold).unwrap();
assert!(eop.is_initialized());
assert_eq!(eop.len(), 22605);
assert_eq!(eop.mjd_min(), 37665.0);
assert_eq!(eop.mjd_max(), 60269.0);
assert_eq!(eop.eop_type(), EOPType::C04);
assert_eq!(eop.extrapolation(), EOPExtrapolation::Hold);
assert!(eop.interpolation());
}
#[test]
fn test_from_default_c04() {
let eop =
FileEOPProvider::from_default_file(EOPType::C04, true, EOPExtrapolation::Hold).unwrap();
assert!(eop.is_initialized());
assert_ne!(eop.len(), 0);
assert_eq!(eop.mjd_min(), 37665.0);
assert!(eop.mjd_max() >= 60269.0);
assert_eq!(eop.eop_type(), EOPType::C04);
assert_eq!(eop.extrapolation(), EOPExtrapolation::Hold);
assert!(eop.interpolation());
}
#[test]
fn test_from_standard_file() {
let manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
let filepath = Path::new(&manifest_dir)
.join("test_assets")
.join("finals.all.iau2000.txt");
let eop = FileEOPProvider::from_file(&filepath, true, EOPExtrapolation::Hold).unwrap();
assert!(eop.is_initialized());
assert_eq!(eop.len(), 18989);
assert_eq!(eop.mjd_min(), 41684.0);
assert_eq!(eop.mjd_max(), 60672.0);
assert_eq!(eop.eop_type(), EOPType::StandardBulletinA);
assert_eq!(eop.extrapolation(), EOPExtrapolation::Hold);
assert!(eop.interpolation());
}
#[test]
fn test_from_default_standard() {
let eop = FileEOPProvider::from_default_file(
EOPType::StandardBulletinA,
true,
EOPExtrapolation::Hold,
)
.unwrap();
assert!(eop.is_initialized());
assert_ne!(eop.len(), 0);
assert_eq!(eop.mjd_min(), 41684.0);
assert!(eop.mjd_max() >= 60672.0);
assert_eq!(eop.eop_type(), EOPType::StandardBulletinA);
assert_eq!(eop.extrapolation(), EOPExtrapolation::Hold);
assert!(eop.interpolation());
}
#[test]
fn test_get_ut1_utc() {
let eop = setup_test_eop(true, EOPExtrapolation::Hold);
let ut1_utc = eop.get_ut1_utc(59569.0).unwrap();
assert_eq!(ut1_utc, -0.1079939);
let ut1_utc = eop.get_ut1_utc(59569.5).unwrap();
assert_eq!(ut1_utc, (-0.1079939 + -0.1075984) / 2.0);
let ut1_utc = eop.get_ut1_utc(99999.0).unwrap();
assert_eq!(ut1_utc, 0.0420038);
let eop = setup_test_eop(true, EOPExtrapolation::Zero);
let ut1_utc = eop.get_ut1_utc(99999.0).unwrap();
assert_eq!(ut1_utc, 0.0);
let eop = setup_test_eop(false, EOPExtrapolation::Hold);
let ut1_utc = eop.get_ut1_utc(59569.5).unwrap();
assert_eq!(ut1_utc, -0.1079939);
}
#[test]
fn test_get_pm_xy() {
let eop = setup_test_eop(true, EOPExtrapolation::Hold);
let (pm_x, pm_y) = eop.get_pm(59569.0).unwrap();
assert_eq!(pm_x, 0.075382 * AS2RAD);
assert_eq!(pm_y, 0.263451 * AS2RAD);
let (pm_x, pm_y) = eop.get_pm(59569.5).unwrap();
assert_eq!(pm_x, (0.075382 * AS2RAD + 0.073157 * AS2RAD) / 2.0);
assert_eq!(pm_y, (0.263451 * AS2RAD + 0.264273 * AS2RAD) / 2.0);
let (pm_x, pm_y) = eop.get_pm(99999.0).unwrap();
assert_eq!(pm_x, 0.173369 * AS2RAD);
assert_eq!(pm_y, 0.266914 * AS2RAD);
let eop = setup_test_eop(true, EOPExtrapolation::Zero);
let (pm_x, pm_y) = eop.get_pm(99999.0).unwrap();
assert_eq!(pm_x, 0.0);
assert_eq!(pm_y, 0.0);
let eop = setup_test_eop(false, EOPExtrapolation::Hold);
let (pm_x, pm_y) = eop.get_pm(59569.5).unwrap();
assert_eq!(pm_x, 0.075382 * AS2RAD);
assert_eq!(pm_y, 0.263451 * AS2RAD);
}
#[test]
#[allow(non_snake_case)]
fn test_get_dxdy() {
let eop = setup_test_eop(true, EOPExtrapolation::Hold);
let (dX, dY) = eop.get_dxdy(59569.0).unwrap();
assert_eq!(dX, 0.265 * 1.0e-3 * AS2RAD);
assert_eq!(dY, -0.067 * 1.0e-3 * AS2RAD);
let (dX, dY) = eop.get_dxdy(59569.5).unwrap();
assert_eq!(dX, (0.265 * AS2RAD + 0.268 * AS2RAD) * 1.0e-3 / 2.0);
assert_abs_diff_eq!(
dY,
(-0.067 * AS2RAD + -0.067 * AS2RAD) * 1.0e-3 / 2.0,
epsilon = f64::EPSILON
);
let (dX, dY) = eop.get_dxdy(99999.0).unwrap();
assert_eq!(dX, 0.006 * 1.0e-3 * AS2RAD);
assert_eq!(dY, -0.118 * 1.0e-3 * AS2RAD);
let eop = setup_test_eop(true, EOPExtrapolation::Zero);
let (dX, dY) = eop.get_dxdy(99999.0).unwrap();
assert_eq!(dX, 0.0);
assert_eq!(dY, 0.0);
let eop = setup_test_eop(false, EOPExtrapolation::Hold);
let (dX, dY) = eop.get_dxdy(59569.5).unwrap();
assert_eq!(dX, 0.265 * 1.0e-3 * AS2RAD);
assert_eq!(dY, -0.067 * 1.0e-3 * AS2RAD);
}
#[test]
fn test_get_lod() {
let eop = setup_test_eop(true, EOPExtrapolation::Hold);
let lod = eop.get_lod(59569.0).unwrap();
assert_eq!(lod, -0.3999 * 1.0e-3);
let lod = eop.get_lod(59569.5).unwrap();
assert_eq!(lod, (-0.3999 + -0.3604) * 1.0e-3 / 2.0);
let lod = eop.get_lod(99999.0).unwrap();
assert_eq!(lod, 0.7706 * 1.0e-3);
let eop = setup_test_eop(true, EOPExtrapolation::Zero);
let lod = eop.get_lod(99999.0).unwrap();
assert_eq!(lod, 0.0);
let eop = setup_test_eop(false, EOPExtrapolation::Hold);
let lod = eop.get_lod(59569.5).unwrap();
assert_eq!(lod, -0.3999 * 1.0e-3);
}
#[test]
fn test_eop_extrapolation_error() {
let eop = setup_test_eop(true, EOPExtrapolation::Error);
assert!(eop.get_ut1_utc(99999.0).is_err());
assert!(eop.get_pm(99999.0).is_err());
assert!(eop.get_dxdy(99999.0).is_err());
assert!(eop.get_lod(99999.0).is_err());
}
#[test]
fn test_default_implementation() {
let eop_default = FileEOPProvider::default();
let eop_new = FileEOPProvider::new();
assert_eq!(eop_default.is_initialized(), eop_new.is_initialized());
assert!(!eop_default.is_initialized());
assert_eq!(eop_default.eop_type(), eop_new.eop_type());
assert_eq!(eop_default.eop_type(), EOPType::Unknown);
assert_eq!(eop_default.len(), eop_new.len());
assert_eq!(eop_default.len(), 0);
}
#[test]
fn test_display_format() {
let eop = setup_test_eop(true, EOPExtrapolation::Hold);
let display_string = format!("{}", eop);
assert!(display_string.contains("FileEOPProvider"));
assert!(display_string.contains("18989"));
assert!(display_string.contains("entries"));
assert!(display_string.contains("41684"));
assert!(display_string.contains("60672"));
assert!(display_string.contains("EOPExtrapolation::Hold"));
assert!(display_string.contains("true"));
}
#[test]
fn test_debug_format() {
let eop = setup_test_eop(true, EOPExtrapolation::Hold);
let debug_string = format!("{:?}", eop);
assert!(debug_string.contains("FileEOPProvider"));
assert!(debug_string.contains("18989"));
assert!(debug_string.contains("41684"));
assert!(debug_string.contains("60672"));
assert!(debug_string.contains("EOPExtrapolation::Hold"));
assert!(debug_string.contains("true"));
}
#[test]
fn test_eopkey_partial_cmp() {
let key1 = EOPKey(100.0);
let key2 = EOPKey(200.0);
let key3 = EOPKey(100.0);
assert!(key1 < key2);
assert!(key2 > key1);
assert!(key1 == key3);
assert!(key1 <= key3);
assert!(key1 >= key3);
}
#[test]
fn test_extrapolate_before_min_zero() {
let eop = setup_test_eop(true, EOPExtrapolation::Zero);
let mjd_before_min = 40000.0;
let ut1_utc = eop.get_ut1_utc(mjd_before_min).unwrap();
assert_eq!(ut1_utc, 0.0);
let (pm_x, pm_y) = eop.get_pm(mjd_before_min).unwrap();
assert_eq!(pm_x, 0.0);
assert_eq!(pm_y, 0.0);
let (dx, dy) = eop.get_dxdy(mjd_before_min).unwrap();
assert_eq!(dx, 0.0);
assert_eq!(dy, 0.0);
let lod = eop.get_lod(mjd_before_min).unwrap();
assert_eq!(lod, 0.0);
let eop_data = eop.get_eop(mjd_before_min).unwrap();
assert_eq!(eop_data.0, 0.0); assert_eq!(eop_data.1, 0.0); assert_eq!(eop_data.2, 0.0); assert_eq!(eop_data.3, 0.0); assert_eq!(eop_data.4, 0.0); assert_eq!(eop_data.5, 0.0); }
#[test]
fn test_extrapolate_before_min_hold() {
let eop = setup_test_eop(true, EOPExtrapolation::Hold);
let mjd_before_min = 40000.0;
let ut1_utc_first = eop.get_ut1_utc(41684.0).unwrap();
let (pm_x_first, pm_y_first) = eop.get_pm(41684.0).unwrap();
let (dx_first, dy_first) = eop.get_dxdy(41684.0).unwrap();
let lod_first = eop.get_lod(41684.0).unwrap();
let ut1_utc = eop.get_ut1_utc(mjd_before_min).unwrap();
assert_eq!(ut1_utc, ut1_utc_first);
let (pm_x, pm_y) = eop.get_pm(mjd_before_min).unwrap();
assert_eq!(pm_x, pm_x_first);
assert_eq!(pm_y, pm_y_first);
let (dx, dy) = eop.get_dxdy(mjd_before_min).unwrap();
assert_eq!(dx, dx_first);
assert_eq!(dy, dy_first);
let lod = eop.get_lod(mjd_before_min).unwrap();
assert_eq!(lod, lod_first);
let eop_data = eop.get_eop(mjd_before_min).unwrap();
assert_eq!(eop_data.0, pm_x_first); assert_eq!(eop_data.1, pm_y_first); assert_eq!(eop_data.2, ut1_utc_first); assert_eq!(eop_data.3, dx_first); assert_eq!(eop_data.4, dy_first); assert_eq!(eop_data.5, lod_first); }
#[test]
fn test_eop_lookup_performance() {
let eop = setup_test_eop(true, EOPExtrapolation::Hold);
let mjd_min = eop.mjd_min();
let mjd_max = eop.mjd_max();
let num_lookups = 1_000_000;
let start = std::time::Instant::now();
for i in 0..num_lookups {
let frac = (i as f64) / (num_lookups as f64);
let mjd = mjd_min + frac * (mjd_max - mjd_min);
let _ = eop.get_ut1_utc(mjd).unwrap();
}
let elapsed = start.elapsed();
println!(
"EOP lookup performance: {} lookups in {:.3}s ({:.0} lookups/sec)",
num_lookups,
elapsed.as_secs_f64(),
num_lookups as f64 / elapsed.as_secs_f64()
);
assert!(elapsed.as_secs() < 5, "EOP lookups too slow: {:?}", elapsed);
}
#[test]
fn test_extrapolate_before_min_error() {
let eop = setup_test_eop(true, EOPExtrapolation::Error);
let mjd_before_min = 40000.0;
assert!(eop.get_ut1_utc(mjd_before_min).is_err());
assert!(eop.get_pm(mjd_before_min).is_err());
assert!(eop.get_dxdy(mjd_before_min).is_err());
assert!(eop.get_lod(mjd_before_min).is_err());
assert!(eop.get_eop(mjd_before_min).is_err());
}
}