use serde_json::Value;
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MetricValue {
Na,
Raw(String),
}
impl MetricValue {
pub fn as_raw(&self) -> Option<&str> {
match self {
MetricValue::Na => None,
MetricValue::Raw(raw) => Some(raw),
}
}
pub fn as_finite(&self) -> Option<&str> {
self.as_raw().filter(|raw| is_finite_num(raw))
}
}
#[non_exhaustive]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FidelityRecord {
pub label: String,
pub kind: Option<String>,
pub esr: MetricValue,
pub esr_db: MetricValue,
pub snr_db: MetricValue,
pub mse: MetricValue,
pub mrstft: MetricValue,
pub esr_f64: MetricValue,
}
pub(crate) fn fidelity_from_json(value: &Value) -> Option<FidelityRecord> {
let kind = match value.get("kind") {
None => None,
Some(Value::String(kind)) => Some(kind.clone()),
Some(_) => return None,
};
if let Some(kind) = kind.as_deref()
&& kind != "fidelity"
{
return None;
}
let label = match value.get("label") {
Some(Value::String(label)) if !label.is_empty() && label != "null" => label.clone(),
_ => return None,
};
Some(FidelityRecord {
label,
kind,
esr: canon_metric(value.get("esr")),
esr_db: canon_metric(value.get("esr_db")),
snr_db: canon_metric(value.get("snr_db")),
mse: canon_metric(value.get("mse")),
mrstft: canon_metric(value.get("mrstft")),
esr_f64: canon_metric(value.get("esr_f64")),
})
}
pub fn parse_fidelity_jsonl(input: &str) -> Result<Vec<FidelityRecord>, MetricsError> {
let mut records = Vec::new();
for (line_no, raw_line) in input.lines().enumerate() {
let line = raw_line.trim();
if line.is_empty() {
continue;
}
let value: Value =
serde_json::from_str(line).map_err(|source| MetricsError::MalformedLine {
line: line_no + 1,
source,
})?;
if let Some(record) = fidelity_from_json(&value) {
records.push(record);
}
}
Ok(records)
}
pub fn parse_fidelity_jsonl_file(
path: impl AsRef<std::path::Path>,
) -> Result<Vec<FidelityRecord>, MetricsError> {
let input = std::fs::read_to_string(path)?;
parse_fidelity_jsonl(&input)
}
#[derive(Debug, thiserror::Error)]
pub enum MetricsError {
#[error("malformed JSONL line {line}: {source}")]
MalformedLine {
line: usize,
source: serde_json::Error,
},
#[error("cannot read metrics JSONL: {0}")]
Io(#[from] std::io::Error),
}
pub fn is_finite_num(v: &str) -> bool {
if v.is_empty() {
return false;
}
if matches!(
v.to_ascii_lowercase().as_str(),
"inf" | "-inf" | "+inf" | "infinity" | "-infinity" | "nan" | "-nan"
) {
return false;
}
let bytes = v.as_bytes();
let mut i = 0;
if matches!(bytes[0], b'+' | b'-') {
i += 1;
}
let mut int_digits = 0;
while i < bytes.len() && bytes[i].is_ascii_digit() {
int_digits += 1;
i += 1;
}
let mut frac_digits = 0;
if i < bytes.len() && bytes[i] == b'.' {
i += 1;
while i < bytes.len() && bytes[i].is_ascii_digit() {
frac_digits += 1;
i += 1;
}
}
if int_digits == 0 && frac_digits == 0 {
return false;
}
if i < bytes.len() && matches!(bytes[i], b'e' | b'E') {
i += 1;
if i < bytes.len() && matches!(bytes[i], b'+' | b'-') {
i += 1;
}
let mut exp_digits = 0;
while i < bytes.len() && bytes[i].is_ascii_digit() {
exp_digits += 1;
i += 1;
}
if exp_digits == 0 {
return false;
}
}
i == bytes.len()
}
fn canon_metric(value: Option<&Value>) -> MetricValue {
match value {
None | Some(Value::Null) => MetricValue::Na,
Some(Value::String(s)) if s.is_empty() => MetricValue::Na,
Some(Value::String(s)) => MetricValue::Raw(s.clone()),
Some(Value::Number(n)) => MetricValue::Raw(n.to_string()),
Some(Value::Bool(b)) => MetricValue::Raw((*b).to_string()),
Some(other) => MetricValue::Raw(other.to_string()),
}
}
#[cfg(test)]
#[path = "metrics_test.rs"]
mod metrics_test;