use core::fmt;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
#[repr(transparent)]
pub struct Sccm(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
#[repr(transparent)]
pub struct MilliTorr(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
#[repr(transparent)]
pub struct Watts(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
pub enum PhysicalValue {
GasFlow(Sccm),
Pressure(MilliTorr),
RfPower(Watts),
Dimensionless(f64),
}
impl PhysicalValue {
#[must_use]
pub fn raw_scalar(self) -> f64 {
match self {
Self::GasFlow(Sccm(v)) => v,
Self::Pressure(MilliTorr(v)) => v,
Self::RfPower(Watts(v)) => v,
Self::Dimensionless(v) => v,
}
}
#[must_use]
pub fn dimension(self) -> &'static str {
match self {
Self::GasFlow(_) => "sccm",
Self::Pressure(_) => "milli_torr",
Self::RfPower(_) => "watts",
Self::Dimensionless(_) => "dimensionless",
}
}
}
impl fmt::Display for PhysicalValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::GasFlow(Sccm(v)) => write!(f, "{v:.4} sccm"),
Self::Pressure(MilliTorr(v)) => write!(f, "{v:.4} mTorr"),
Self::RfPower(Watts(v)) => write!(f, "{v:.4} W"),
Self::Dimensionless(v) => write!(f, "{v:.6} (dimensionless)"),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UomScales {
pub gas_flow_unit: &'static str,
pub pressure_unit: &'static str,
pub rf_power_unit: &'static str,
pub normalisation: &'static str,
pub interoperability_note: &'static str,
}
impl Default for UomScales {
fn default() -> Self {
Self {
gas_flow_unit: "sccm",
pressure_unit: "milli_torr",
rf_power_unit: "watts",
normalisation: "z-score relative to healthy-phase empirical mean and sigma",
interoperability_note:
"All DSFB residuals are dimensionless after normalisation; \
the original unit tags are preserved in the traceability manifest \
for reverse-engineering and physical interpretation.",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn raw_scalar_round_trips() {
let v = PhysicalValue::GasFlow(Sccm(120.5));
assert!((v.raw_scalar() - 120.5).abs() < 1e-12);
let v2 = PhysicalValue::Pressure(MilliTorr(35.2));
assert!((v2.raw_scalar() - 35.2).abs() < 1e-12);
let v3 = PhysicalValue::RfPower(Watts(450.0));
assert!((v3.raw_scalar() - 450.0).abs() < 1e-12);
}
#[test]
fn dimension_tags_are_canonical() {
assert_eq!(PhysicalValue::GasFlow(Sccm(1.0)).dimension(), "sccm");
assert_eq!(
PhysicalValue::Pressure(MilliTorr(1.0)).dimension(),
"milli_torr"
);
assert_eq!(PhysicalValue::RfPower(Watts(1.0)).dimension(), "watts");
assert_eq!(PhysicalValue::Dimensionless(1.0).dimension(), "dimensionless");
}
#[test]
fn display_contains_unit_suffix() {
let s = format!("{}", PhysicalValue::GasFlow(Sccm(10.0)));
assert!(s.contains("sccm"), "expected 'sccm' in '{s}'");
let s2 = format!("{}", PhysicalValue::Pressure(MilliTorr(5.0)));
assert!(s2.contains("mTorr"), "expected 'mTorr' in '{s2}'");
}
#[test]
fn uom_scales_default_is_deterministic() {
let a = UomScales::default();
let b = UomScales::default();
assert_eq!(a.gas_flow_unit, b.gas_flow_unit);
assert_eq!(a.normalisation, b.normalisation);
}
}