pub mod model_correction;
mod vfcc17;
pub use model_correction::ModelCorrection;
use std::{collections::HashMap, fmt, str::FromStr};
use nom::{
IResult, Parser,
bytes::complete::{tag, take_while1},
character::complete::{char, multispace0},
combinator::{map, opt},
number::complete::float,
sequence::{preceded, separated_pair, terminated},
};
use thiserror::Error;
use vfcc17::parse_vfcc17_line;
use crate::observer::mpc::MpcCode;
pub type CatalogCode = String;
pub type ErrorModelData = HashMap<(MpcCode, CatalogCode), (f32, f32)>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum ObsErrorModel {
FCCT14,
CBM10,
VFCC17,
}
impl fmt::Display for ObsErrorModel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let s = match self {
ObsErrorModel::FCCT14 => "FCCT14 (Farnocchia et al. 2014)",
ObsErrorModel::CBM10 => "CBM10 (Chesley, Baer & Monet 2010)",
ObsErrorModel::VFCC17 => "VFCC17 (Vereš et al. 2017)",
};
write!(f, "{s}")
}
}
static FCCT14_RULES: &str = include_str!("data_models/fcct14.rules");
static CBM10_RULES: &str = include_str!("data_models/cbm10.rules");
static VFCC17_RULES: &str = include_str!("data_models/vfcc17.rules");
pub(in crate::observer::error_model) type ParseResult<'a> =
IResult<&'a str, Vec<((String, CatalogCode), (f32, f32))>>;
fn parse_station(input: &str) -> IResult<&str, &str> {
terminated(take_while1(|c: char| c.is_alphanumeric()), char(':')).parse(input)
}
fn parse_catalog_codes(input: &str) -> IResult<&str, Vec<String>> {
preceded(
(multispace0, opt(tag("c="))),
map(
take_while1(|c: char| c.is_alphabetic() || c == '*'),
|s: &str| {
s.as_bytes()
.iter()
.map(|&b| String::from(b as char))
.collect()
},
),
)
.parse(input)
}
fn parse_rms_values(input: &str) -> IResult<&str, (f32, f32)> {
preceded(
(multispace0, char('@')),
separated_pair(
preceded(multispace0, float),
char(','),
preceded(multispace0, float),
),
)
.parse(input)
}
fn parse_full_line(input: &str) -> ParseResult<'_> {
let line = match input.find('!') {
Some(pos) => input[..pos].trim(),
None => input.trim(),
};
map(
(parse_station, parse_catalog_codes, parse_rms_values),
|(station, catalogs, (rmsa, rmsd))| {
catalogs
.into_iter()
.map(|cat| ((station.to_string(), cat), (rmsa, rmsd)))
.collect()
},
)
.parse(line)
}
#[derive(Debug, Error, Clone)]
pub enum ErrorModelParseError {
#[error("Nom parsing error on: {0}")]
NomParsingError(String),
#[error("Station code is not exactly 3 ASCII bytes: {0:?}")]
InvalidStationCode(String),
}
fn str_to_mpc_code(s: &str) -> Result<MpcCode, ErrorModelParseError> {
s.as_bytes()
.try_into()
.map_err(|_| ErrorModelParseError::InvalidStationCode(s.to_string()))
}
fn parse_full_file<F>(file: &str, parse_line: F) -> Result<ErrorModelData, ErrorModelParseError>
where
F: Fn(&str) -> ParseResult,
{
file.lines()
.filter(|line| {
let t = line.trim();
!t.is_empty() && !t.starts_with('!')
})
.try_fold(ErrorModelData::default(), |mut map, line| {
let (_, pairs) = parse_line(line)
.map_err(|_| ErrorModelParseError::NomParsingError(line.to_string()))?;
for ((station, cat), rms) in pairs {
let code = str_to_mpc_code(&station)?;
map.insert((code, cat), rms);
}
Ok(map)
})
}
impl ObsErrorModel {
pub fn read_error_model_file(&self) -> Result<ErrorModelData, ErrorModelParseError> {
match self {
ObsErrorModel::FCCT14 => parse_full_file(FCCT14_RULES, parse_full_line),
ObsErrorModel::CBM10 => parse_full_file(CBM10_RULES, parse_full_line),
ObsErrorModel::VFCC17 => parse_full_file(VFCC17_RULES, parse_vfcc17_line),
}
}
}
impl FromStr for ObsErrorModel {
type Err = ErrorModelParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"FCCT14" => Ok(ObsErrorModel::FCCT14),
"CBM10" => Ok(ObsErrorModel::CBM10),
"VFCC17" => Ok(ObsErrorModel::VFCC17),
_ => Err(ErrorModelParseError::NomParsingError(format!(
"Unknown error model: {s}"
))),
}
}
}
impl TryFrom<&str> for ObsErrorModel {
type Error = ErrorModelParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
value.parse()
}
}
pub fn get_bias_rms(
error_model: &ErrorModelData,
mpc_code: MpcCode,
catalog_code: &str,
) -> Option<(f32, f32)> {
let lookup = |code: MpcCode, cat: &str| -> Option<(f32, f32)> {
error_model.get(&(code, cat.to_string())).copied()
};
lookup(mpc_code, catalog_code)
.or_else(|| lookup(mpc_code, "e"))
.or_else(|| lookup(mpc_code, "c"))
.or_else(|| lookup(*b"ALL", catalog_code))
.or_else(|| lookup(*b"ALL", "e"))
.or_else(|| lookup(*b"ALL", "c"))
}
#[cfg(test)]
mod test_error_model {
use super::*;
#[test]
fn test_parse_fcct14_line() {
let line = "ALL: c=eqru @ 0.33, 0.30";
let result = parse_full_line(line);
assert!(result.is_ok());
let ((mpc_code, catalog_code), (rmsa, rmsd)) = &result.unwrap().1[0];
assert_eq!(mpc_code, "ALL");
assert_eq!(catalog_code, "e");
assert_eq!(*rmsa, 0.33);
assert_eq!(*rmsd, 0.3);
let line = "ALL: c=cd @ 0.51, 0.40 ! CBM Generic Catalog weights";
let result = parse_full_line(line);
assert!(result.is_ok());
let ((mpc_code, catalog_code), (rmsa, rmsd)) = &result.unwrap().1[0];
assert_eq!(mpc_code, "ALL");
assert_eq!(catalog_code, "c");
assert_eq!(*rmsa, 0.51);
assert_eq!(*rmsd, 0.4);
let line = "699:c @ 0.93, 0.78";
let result = parse_full_line(line);
assert!(result.is_ok());
let ((mpc_code, catalog_code), (rmsa, rmsd)) = &result.unwrap().1[0];
assert_eq!(mpc_code, "699");
assert_eq!(catalog_code, "c");
assert_eq!(*rmsa, 0.93);
assert_eq!(*rmsd, 0.78);
}
#[test]
fn test_read_error_model_file() {
let result = ObsErrorModel::FCCT14.read_error_model_file();
assert!(result.is_ok());
assert!(!result.unwrap().is_empty());
let result = ObsErrorModel::CBM10.read_error_model_file();
assert!(result.is_ok());
assert!(!result.unwrap().is_empty());
let result = ObsErrorModel::VFCC17.read_error_model_file();
assert!(result.is_ok());
assert!(!result.unwrap().is_empty());
}
#[test]
fn test_get_bias_rms() {
let model = ObsErrorModel::FCCT14.read_error_model_file().unwrap();
let (rmsa, rmsd) = get_bias_rms(&model, *b"ALL", "c").unwrap();
assert_eq!(rmsa, 0.51);
assert_eq!(rmsd, 0.4);
let (rmsa, rmsd) = get_bias_rms(&model, *b"699", "c").unwrap();
assert_eq!(rmsa, 0.47);
assert_eq!(rmsd, 0.39);
let model = ObsErrorModel::CBM10.read_error_model_file().unwrap();
let (rmsa, rmsd) = get_bias_rms(&model, *b"ALL", "c").unwrap();
assert_eq!(rmsa, 0.5);
assert_eq!(rmsd, 0.5);
let (rmsa, rmsd) = get_bias_rms(&model, *b"699", "c").unwrap();
assert_eq!(rmsa, 0.84);
assert_eq!(rmsd, 0.81);
let model = ObsErrorModel::VFCC17.read_error_model_file().unwrap();
let (rmsa, rmsd) = get_bias_rms(&model, *b"ALL", "U").unwrap();
assert_eq!(rmsa, 0.6);
assert_eq!(rmsd, 0.6);
let (rmsa, rmsd) = get_bias_rms(&model, *b"699", "*").unwrap();
assert_eq!(rmsa, 0.8);
assert_eq!(rmsd, 0.8);
}
}