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}