Skip to main content

dsfb_semiconductor/
units.rs

1//! Type-safe physical quantity wrappers for semiconductor process variables.
2//!
3//! # Design Rationale
4//! Every value entering the public DSFB observer API must carry an explicit
5//! unit context.  Raw `f64` scalars are prohibited in externally-facing
6//! function signatures; this module provides lightweight newtypes that make
7//! dimensional intent part of the type system.
8//!
9//! # No-std Compatibility
10//! This module is `no_std`-compatible.  All traits are from `core`, and the
11//! `Display` implementation uses `core::fmt`.
12//!
13//! | Physical quantity               | Newtype      | Typical semiconductor use                |
14//! |---------------------------------|--------------|------------------------------------------|
15//! | Gas mass-flow                   | [`Sccm`]     | MFC setpoint / feedback residual         |
16//! | Chamber pressure                | [`MilliTorr`]| Manometer / throttle valve residual      |
17//! | RF generator power              | [`Watts`]    | Plasma source / bias power residual      |
18//!
19//! # Examples
20//! ```
21//! use dsfb_semiconductor::units::{PhysicalValue, Sccm, MilliTorr, Watts};
22//!
23//! let flow  = PhysicalValue::GasFlow(Sccm(120.5));
24//! let press = PhysicalValue::Pressure(MilliTorr(35.2));
25//! let power = PhysicalValue::RfPower(Watts(450.0));
26//!
27//! assert_eq!(flow.dimension(), "sccm");
28//! assert!((press.raw_scalar() - 35.2).abs() < 1e-9);
29//! ```
30
31use core::fmt;
32
33use serde::{Deserialize, Serialize};
34
35// ─── Newtypes ─────────────────────────────────────────────────────────────────
36
37/// Standard cubic centimetres per minute — the SI-derived unit for gas
38/// mass-flow controllers (MFCs) in semiconductor process chambers.
39#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
40#[repr(transparent)]
41pub struct Sccm(pub f64);
42
43/// Chamber pressure in milli-Torr — the practical unit for high-vacuum
44/// semiconductor process chambers (typical range: 1–500 mTorr).
45#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
46#[repr(transparent)]
47pub struct MilliTorr(pub f64);
48
49/// RF generator / bias-power in Watts — used for plasma source and substrate
50/// bias power in etch, deposition, and clean processes.
51#[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)]
52#[repr(transparent)]
53pub struct Watts(pub f64);
54
55// ─── Tagged union ────────────────────────────────────────────────────────────
56
57/// A tagged physical observation: a scalar value together with its dimensional
58/// classification.  The DSFB public observer API accepts `PhysicalValue` rather
59/// than bare `f64`.
60#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
61#[non_exhaustive]
62pub enum PhysicalValue {
63    /// Gas mass-flow controller value.
64    GasFlow(Sccm),
65    /// Chamber pressure sensor value.
66    Pressure(MilliTorr),
67    /// RF generator power value.
68    RfPower(Watts),
69    /// Dimensionless normalised residual (z-score or fractional deviation).
70    Dimensionless(f64),
71}
72
73impl PhysicalValue {
74    /// Extract the raw scalar.
75    ///
76    /// Callers that depend on the physical meaning of the value must preserve
77    /// [`PhysicalValue::dimension`] alongside any downstream use of this scalar.
78    #[must_use]
79    pub fn raw_scalar(self) -> f64 {
80        match self {
81            Self::GasFlow(Sccm(v)) => v,
82            Self::Pressure(MilliTorr(v)) => v,
83            Self::RfPower(Watts(v)) => v,
84            Self::Dimensionless(v) => v,
85        }
86    }
87
88    /// The dimension tag as a canonical lowercase string, suitable for
89    /// embedding in traceability manifests and JSON signature files.
90    #[must_use]
91    pub fn dimension(self) -> &'static str {
92        match self {
93            Self::GasFlow(_) => "sccm",
94            Self::Pressure(_) => "milli_torr",
95            Self::RfPower(_) => "watts",
96            Self::Dimensionless(_) => "dimensionless",
97        }
98    }
99}
100
101impl fmt::Display for PhysicalValue {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        match self {
104            Self::GasFlow(Sccm(v)) => write!(f, "{v:.4} sccm"),
105            Self::Pressure(MilliTorr(v)) => write!(f, "{v:.4} mTorr"),
106            Self::RfPower(Watts(v)) => write!(f, "{v:.4} W"),
107            Self::Dimensionless(v) => write!(f, "{v:.6} (dimensionless)"),
108        }
109    }
110}
111
112// ─── Unit scale manifest ──────────────────────────────────────────────────────
113
114/// Describes the physical unit conventions and normalisation strategy used
115/// during a DSFB run.  Emitted verbatim in every `dsfb_run_manifest.json`.
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub struct UomScales {
118    /// Unit for gas mass-flow observables.
119    pub gas_flow_unit: &'static str,
120    /// Unit for chamber pressure observables.
121    pub pressure_unit: &'static str,
122    /// Unit for RF power observables.
123    pub rf_power_unit: &'static str,
124    /// Strategy used to normalise raw sensor values before DSFB ingestion.
125    pub normalisation: &'static str,
126    /// Scale convention note for future interoperability.
127    pub interoperability_note: &'static str,
128}
129
130impl Default for UomScales {
131    fn default() -> Self {
132        Self {
133            gas_flow_unit: "sccm",
134            pressure_unit: "milli_torr",
135            rf_power_unit: "watts",
136            normalisation: "z-score relative to healthy-phase empirical mean and sigma",
137            interoperability_note:
138                "All DSFB residuals are dimensionless after normalisation; \
139                 the original unit tags are preserved in the traceability manifest \
140                 for reverse-engineering and physical interpretation.",
141        }
142    }
143}
144
145// ─── Unit tests ───────────────────────────────────────────────────────────────
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn raw_scalar_round_trips() {
153        let v = PhysicalValue::GasFlow(Sccm(120.5));
154        assert!((v.raw_scalar() - 120.5).abs() < 1e-12);
155
156        let v2 = PhysicalValue::Pressure(MilliTorr(35.2));
157        assert!((v2.raw_scalar() - 35.2).abs() < 1e-12);
158
159        let v3 = PhysicalValue::RfPower(Watts(450.0));
160        assert!((v3.raw_scalar() - 450.0).abs() < 1e-12);
161    }
162
163    #[test]
164    fn dimension_tags_are_canonical() {
165        assert_eq!(PhysicalValue::GasFlow(Sccm(1.0)).dimension(), "sccm");
166        assert_eq!(
167            PhysicalValue::Pressure(MilliTorr(1.0)).dimension(),
168            "milli_torr"
169        );
170        assert_eq!(PhysicalValue::RfPower(Watts(1.0)).dimension(), "watts");
171        assert_eq!(PhysicalValue::Dimensionless(1.0).dimension(), "dimensionless");
172    }
173
174    #[test]
175    fn display_contains_unit_suffix() {
176        let s = format!("{}", PhysicalValue::GasFlow(Sccm(10.0)));
177        assert!(s.contains("sccm"), "expected 'sccm' in '{s}'");
178
179        let s2 = format!("{}", PhysicalValue::Pressure(MilliTorr(5.0)));
180        assert!(s2.contains("mTorr"), "expected 'mTorr' in '{s2}'");
181    }
182
183    #[test]
184    fn uom_scales_default_is_deterministic() {
185        let a = UomScales::default();
186        let b = UomScales::default();
187        assert_eq!(a.gas_flow_unit, b.gas_flow_unit);
188        assert_eq!(a.normalisation, b.normalisation);
189    }
190}