dsfb_semiconductor/lib.rs
1//! `dsfb-semiconductor` — deterministic DSFB kernel and SECOM benchmark companion.
2//!
3//! # Non-Intrusion Guarantees
4//!
5//! DSFB is a **read-only supervisory system** operating on residual streams.
6//!
7//! | Guarantee | Enforcement |
8//! |-----------|-------------|
9//! | No mutation of upstream data | Observer API accepts only `&[f64]` (shared ref) |
10//! | No control-path influence | No write path into any upstream data structure |
11//! | Deterministic outputs under identical inputs | Pure function composition over fixed params |
12//! | Removable without system impact | Advisory outputs only; zero coupling |
13//!
14//! # Feature flags
15//!
16//! | Feature | Default | Effect |
17//! |---------|---------|--------|
18//! | `std` | yes | Enables CLI, I/O, plotting, networking, and all dataset adapters. |
19//! | *(none)* | — | Kernel-only build: sign, grammar, syntax, semantics, policy, process_context, units. Suitable for bare-metal / RTOS / FPGA deployments. |
20//!
21//! # `no_std` kernel surface
22//!
23//! When compiled with `--no-default-features`, the following modules are
24//! available and require only `alloc`:
25//!
26//! - [`process_context`] — recipe-step admissibility LUT and maintenance hysteresis
27//! - [`units`] — type-safe physical quantity newtypes
28//! - [`signs`] — residual sign computation (drift, slew)
29//! - [`sign`] — streaming sign point construction
30//! - [`grammar`] — three-state admissibility FSM with hysteresis
31//! - [`grammar::layer`] — six-state streaming grammar
32//! - [`syntax`] — motif classifier
33//! - [`policy`] — decision ranking
34//! - [`semantics`] — heuristics bank lookup
35//! - [`config`] — pipeline configuration
36//! - [`nominal`] — healthy-window model
37//! - [`residual`] — residual set construction
38//! - [`input`] — residual and alarm stream types
39
40#![cfg_attr(not(feature = "std"), no_std)]
41
42// When std is disabled, pull in alloc for Vec, String, BTreeMap, format!, etc.
43#[cfg(not(feature = "std"))]
44#[macro_use]
45extern crate alloc;
46
47#[cfg(not(feature = "std"))]
48use alloc::vec::Vec;
49
50// ── Minimal observer API ───────────────────────────────────────────────────────────────
51
52/// A structured episode produced by the DSFB observer layer.
53///
54/// Advisory only. No upstream state is modified.
55#[derive(Debug, Clone, PartialEq)]
56pub struct Episode {
57 /// Sample index within the input slice.
58 pub index: usize,
59 /// Squared residual norm (`|x - nominal|²`), avoiding `sqrt` for kernel compatibility.
60 pub residual_norm_sq: f64,
61 /// Rolling drift estimate (mean first-difference of absolute residuals over last 5 samples).
62 pub drift: f64,
63 /// Grammar state: `"Admissible"`, `"Boundary"`, or `"Violation"`.
64 pub grammar: &'static str,
65 /// Advisory decision: `"Silent"`, `"Review"`, or `"Escalate"`.
66 pub decision: &'static str,
67}
68
69/// A list of [`Episode`] values returned by [`observe`].
70pub type Episodes = Vec<Episode>;
71
72/// Read-only observation of a raw residual slice.
73///
74/// Accepts a **shared reference only** — no write-back, no upstream coupling,
75/// no side effects. Deterministic: identical inputs produce identical outputs.
76///
77/// NaN or non-finite samples are treated as imputed (missing data) and always
78/// return `grammar = "Admissible"`, `decision = "Silent"`.
79///
80/// # Key guarantees
81///
82/// - No mutable access to any upstream structure.
83/// - Deterministic: identical ordered inputs → identical episode sequence.
84/// - No side effects of any kind.
85///
86/// # Example
87///
88/// ```
89/// let residuals: &[f64] = &[0.1, 0.2, 0.5, 1.2, 2.1];
90/// let episodes = dsfb_semiconductor::observe(residuals);
91/// for e in &episodes {
92/// // advisory only — no write-back, no coupling
93/// println!("index={} grammar={} decision={}", e.index, e.grammar, e.decision);
94/// }
95/// ```
96pub fn observe(residuals: &[f64]) -> Episodes {
97 if residuals.is_empty() {
98 return Episodes::new();
99 }
100
101 const DRIFT_WINDOW: usize = 5;
102
103 // Nominal estimate from first 20 % of samples (at least 1, at most all).
104 let nominal_len = (residuals.len() / 5).max(1).min(residuals.len());
105 let mut nominal_sum = 0.0f64;
106 let mut nominal_count = 0usize;
107 for x in &residuals[..nominal_len] {
108 if x.is_finite() {
109 nominal_sum += x;
110 nominal_count += 1;
111 }
112 }
113 let nominal_mean = if nominal_count == 0 {
114 0.0
115 } else {
116 nominal_sum / nominal_count as f64
117 };
118
119 // Threshold: rho^2 = (3*std)^2 = 9 * var. Avoids sqrt for no_std compat.
120 let mut var_sum = 0.0f64;
121 for x in &residuals[..nominal_len] {
122 if x.is_finite() {
123 let d = x - nominal_mean;
124 var_sum += d * d;
125 }
126 }
127 let var = var_sum / nominal_count.max(1) as f64;
128 let rho_sq = (9.0 * var).max(1e-18);
129 let boundary_rho_sq = 0.25 * rho_sq; // (0.5 * rho)^2
130
131 let mut episodes = Episodes::with_capacity(residuals.len());
132
133 for i in 0..residuals.len() {
134 let x = residuals[i];
135
136 // Imputed (NaN / inf) samples never trigger violation.
137 if !x.is_finite() {
138 episodes.push(Episode {
139 index: i,
140 residual_norm_sq: 0.0,
141 drift: 0.0,
142 grammar: "Admissible",
143 decision: "Silent",
144 });
145 continue;
146 }
147
148 let r = x - nominal_mean;
149 let r_sq = r * r;
150
151 // Rolling drift: mean first-difference of |residual| over last DRIFT_WINDOW samples.
152 let drift = if i == 0 {
153 0.0
154 } else {
155 let start = i.saturating_sub(DRIFT_WINDOW);
156 let count = i - start;
157 let mut d_sum = 0.0f64;
158 for j in start..i {
159 let a = if residuals[j].is_finite() {
160 (residuals[j] - nominal_mean).abs()
161 } else {
162 0.0
163 };
164 let b = if residuals[j + 1].is_finite() {
165 (residuals[j + 1] - nominal_mean).abs()
166 } else {
167 0.0
168 };
169 d_sum += b - a;
170 }
171 d_sum / count as f64
172 };
173
174 let grammar = if r_sq > rho_sq {
175 "Violation"
176 } else if r_sq > boundary_rho_sq && drift > 0.0 {
177 "Boundary"
178 } else {
179 "Admissible"
180 };
181
182 let decision = match grammar {
183 "Violation" => "Escalate",
184 "Boundary" => "Review",
185 _ => "Silent",
186 };
187
188 episodes.push(Episode {
189 index: i,
190 residual_norm_sq: r_sq,
191 drift,
192 grammar,
193 decision,
194 });
195 }
196
197 episodes
198}
199
200// ── Kernel modules (always compiled) ───────────────────────────────────────────────────
201pub mod config;
202pub mod grammar;
203pub mod input;
204pub mod nominal;
205pub mod policy;
206pub mod process_context;
207pub mod residual;
208pub mod semantics;
209pub mod sign;
210pub mod signs;
211pub mod syntax;
212pub mod units;
213
214// ── std-only modules ────────────────────────────────────────────────────────────
215#[cfg(feature = "std")]
216pub mod baselines;
217#[cfg(feature = "std")]
218pub mod calibration;
219#[cfg(feature = "std")]
220pub mod cli;
221#[cfg(feature = "std")]
222pub mod cohort;
223#[cfg(feature = "std")]
224pub mod dataset;
225#[cfg(feature = "std")]
226pub mod error;
227#[cfg(feature = "std")]
228pub mod failure_driven;
229#[cfg(feature = "std")]
230pub mod heuristics;
231#[cfg(feature = "std")]
232pub mod interface;
233#[cfg(feature = "std")]
234pub mod metrics;
235#[cfg(feature = "std")]
236pub mod missingness;
237#[cfg(feature = "std")]
238pub mod multivariate_observer;
239#[cfg(feature = "std")]
240pub mod non_intrusive;
241#[cfg(feature = "std")]
242pub mod output_paths;
243#[cfg(feature = "std")]
244pub mod phm2018_loader;
245#[cfg(feature = "std")]
246pub mod pipeline;
247#[cfg(feature = "std")]
248pub mod plots;
249#[cfg(feature = "std")]
250pub mod precursor;
251#[cfg(feature = "std")]
252pub mod preprocessing;
253#[cfg(feature = "std")]
254pub mod report;
255#[cfg(feature = "std")]
256pub mod secom_addendum;
257#[cfg(feature = "std")]
258pub mod semiotics;
259#[cfg(feature = "std")]
260pub mod signature;
261#[cfg(feature = "std")]
262pub mod traceability;
263#[cfg(feature = "std")]
264pub mod unified_value_figure;