Skip to main content

dsfb_semiconductor/grammar/
layer.rs

1#[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}