dsfb_semiconductor/grammar/
layer.rs1#[cfg(feature = "std")]
2use crate::error::Result;
3use crate::sign::FeatureSignPoint;
4use crate::syntax::MotifTimelinePoint;
5use serde::{Deserialize, Serialize};
6#[cfg(feature = "std")]
7use std::collections::BTreeMap;
8#[cfg(not(feature = "std"))]
9use alloc::{collections::BTreeMap, string::{String, ToString}, vec::Vec};
10
11#[cfg(not(feature = "std"))]
12#[inline]
13fn maybe_sqrt(x: f64) -> f64 {
14 if x <= 0.0 {
15 return 0.0;
16 }
17 let mut s = x / 2.0;
18 for _ in 0..32 {
19 s = (s + x / s) * 0.5;
20 }
21 s
22}
23
24pub const ALLOWED_GRAMMAR_STATES: [&str; 6] = [
25 "Admissible",
26 "BoundaryGrazing",
27 "SustainedDrift",
28 "TransientViolation",
29 "PersistentViolation",
30 "Recovery",
31];
32
33#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
34pub struct GrammarState {
35 pub feature_id: String,
36 pub state: String,
37 pub timestamp: f64,
38}
39
40pub fn build_grammar_states(
41 signs: &[FeatureSignPoint],
42 motifs: &[MotifTimelinePoint],
43) -> Vec<GrammarState> {
44 let mut motif_map = BTreeMap::<(&str, u64), &str>::new();
45 for motif in motifs {
46 motif_map.insert(
47 (motif.feature_id.as_str(), motif.timestamp.to_bits()),
48 motif.motif_type.as_str(),
49 );
50 }
51
52 let mut grouped = BTreeMap::<&str, Vec<&FeatureSignPoint>>::new();
53 for sign in signs {
54 grouped
55 .entry(sign.feature_id.as_str())
56 .or_default()
57 .push(sign);
58 }
59
60 let mut states = Vec::new();
61 for (feature_id, series) in grouped {
62 let envelope = feature_envelope(&series);
63 let mut violation_streak = 0usize;
64 let mut drift_streak = 0usize;
65 let mut previous_non_admissible = false;
66
67 for point in series {
68 let motif = motif_map
69 .get(&(feature_id, point.timestamp.to_bits()))
70 .copied()
71 .unwrap_or("null");
72 let abs_r = point.r.abs();
73 let state = if abs_r >= envelope {
74 violation_streak += 1;
75 drift_streak = 0;
76 previous_non_admissible = true;
77 if violation_streak >= 2
78 || motif == "persistent_instability"
79 || motif == "burst_instability"
80 {
81 "PersistentViolation"
82 } else {
83 "TransientViolation"
84 }
85 } else if motif == "slow_drift_precursor" && abs_r >= 0.60 * envelope && point.d > 0.0 {
86 violation_streak = 0;
87 drift_streak += 1;
88 previous_non_admissible = true;
89 if drift_streak >= 3 {
90 "SustainedDrift"
91 } else {
92 "BoundaryGrazing"
93 }
94 } else if motif == "boundary_grazing" && abs_r >= 0.50 * envelope {
95 violation_streak = 0;
96 drift_streak = 0;
97 previous_non_admissible = true;
98 "BoundaryGrazing"
99 } else if previous_non_admissible && motif == "recovery_pattern" {
100 violation_streak = 0;
101 drift_streak = 0;
102 previous_non_admissible = false;
103 "Recovery"
104 } else {
105 violation_streak = 0;
106 drift_streak = 0;
107 previous_non_admissible = false;
108 "Admissible"
109 };
110
111 states.push(GrammarState {
112 feature_id: feature_id.to_string(),
113 state: state.to_string(),
114 timestamp: point.timestamp,
115 });
116 }
117 }
118
119 states.sort_by(|left, right| {
120 left.timestamp
121 .total_cmp(&right.timestamp)
122 .then_with(|| left.feature_id.cmp(&right.feature_id))
123 });
124 states
125}
126
127#[cfg(feature = "std")]
128pub fn write_grammar_states_csv(path: &std::path::Path, rows: &[GrammarState]) -> Result<()> {
129 let mut writer = csv::Writer::from_path(path)?;
130 for row in rows {
131 writer.serialize(row)?;
132 }
133 writer.flush()?;
134 Ok(())
135}
136
137fn feature_envelope(points: &[&FeatureSignPoint]) -> f64 {
138 let mean = points.iter().map(|point| point.r.abs()).sum::<f64>() / points.len().max(1) as f64;
139 let variance = points
140 .iter()
141 .map(|point| {
142 let centered = point.r.abs() - mean;
143 centered * centered
144 })
145 .sum::<f64>()
146 / points.len().max(1) as f64;
147 #[cfg(feature = "std")]
148 return (mean + variance.sqrt()).max(1.0);
149 #[cfg(not(feature = "std"))]
150 return (mean + maybe_sqrt(variance)).max(1.0);
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::sign::FeatureSignPoint;
157 use crate::syntax::MotifTimelinePoint;
158
159 fn point(timestamp: f64, r: f64, d: f64, s: f64) -> FeatureSignPoint {
160 FeatureSignPoint {
161 timestamp,
162 feature_id: "S001".into(),
163 r,
164 d,
165 s,
166 }
167 }
168
169 #[test]
170 fn grammar_depends_on_persistence_and_trajectory() {
171 let signs = vec![
172 point(0.0, 1.0, 0.0, 0.0),
173 point(1.0, 2.0, 1.0, 1.0),
174 point(2.0, 3.0, 1.0, 0.0),
175 point(3.0, 3.1, 0.1, -0.9),
176 point(4.0, 3.2, 0.1, 0.0),
177 ];
178 let motifs = vec![
179 MotifTimelinePoint {
180 feature_id: "S001".into(),
181 motif_type: "null".into(),
182 timestamp: 0.0,
183 },
184 MotifTimelinePoint {
185 feature_id: "S001".into(),
186 motif_type: "slow_drift_precursor".into(),
187 timestamp: 1.0,
188 },
189 MotifTimelinePoint {
190 feature_id: "S001".into(),
191 motif_type: "slow_drift_precursor".into(),
192 timestamp: 2.0,
193 },
194 MotifTimelinePoint {
195 feature_id: "S001".into(),
196 motif_type: "slow_drift_precursor".into(),
197 timestamp: 3.0,
198 },
199 MotifTimelinePoint {
200 feature_id: "S001".into(),
201 motif_type: "slow_drift_precursor".into(),
202 timestamp: 4.0,
203 },
204 ];
205 let grammar = build_grammar_states(&signs, &motifs);
206 assert!(grammar.iter().any(|row| row.state == "SustainedDrift"));
207 }
208
209 #[test]
210 fn recovery_requires_prior_non_admissible_state() {
211 let signs = vec![
212 point(0.0, 5.0, 0.0, 0.0),
213 point(1.0, 6.0, 1.0, 1.0),
214 point(2.0, 3.0, -3.0, -4.0),
215 ];
216 let motifs = vec![
217 MotifTimelinePoint {
218 feature_id: "S001".into(),
219 motif_type: "persistent_instability".into(),
220 timestamp: 0.0,
221 },
222 MotifTimelinePoint {
223 feature_id: "S001".into(),
224 motif_type: "burst_instability".into(),
225 timestamp: 1.0,
226 },
227 MotifTimelinePoint {
228 feature_id: "S001".into(),
229 motif_type: "recovery_pattern".into(),
230 timestamp: 2.0,
231 },
232 ];
233 let grammar = build_grammar_states(&signs, &motifs);
234 assert_eq!(grammar.last().unwrap().state, "Recovery");
235 }
236}