dsfb_semiconductor/syntax/
mod.rs1#[cfg(feature = "std")]
2use crate::error::Result;
3use crate::sign::FeatureSignPoint;
4use serde::{Deserialize, Serialize};
5#[cfg(feature = "std")]
6use std::collections::BTreeMap;
7#[cfg(not(feature = "std"))]
8use alloc::{collections::BTreeMap, string::{String, ToString}, vec::Vec};
9
10#[cfg(not(feature = "std"))]
11#[inline]
12fn maybe_sqrt(x: f64) -> f64 {
13 if x <= 0.0 {
14 return 0.0;
15 }
16 let mut s = x / 2.0;
17 for _ in 0..32 {
18 s = (s + x / s) * 0.5;
19 }
20 s
21}
22
23pub const ALLOWED_MOTIFS: [&str; 8] = [
24 "slow_drift_precursor",
25 "boundary_grazing",
26 "transient_excursion",
27 "persistent_instability",
28 "burst_instability",
29 "recovery_pattern",
30 "noise_like",
31 "null",
32];
33
34#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
35pub struct Motif {
36 pub feature_id: String,
37 pub motif_type: String,
38 pub start_time: f64,
39 pub end_time: f64,
40}
41
42#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43pub struct MotifTimelinePoint {
44 pub feature_id: String,
45 pub motif_type: String,
46 pub timestamp: f64,
47}
48
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
50pub struct SyntaxArtifacts {
51 pub motifs: Vec<Motif>,
52 pub timeline: Vec<MotifTimelinePoint>,
53}
54
55pub fn build_motifs(signs: &[FeatureSignPoint]) -> SyntaxArtifacts {
56 let mut grouped = BTreeMap::<&str, Vec<&FeatureSignPoint>>::new();
57 for sign in signs {
58 grouped
59 .entry(sign.feature_id.as_str())
60 .or_default()
61 .push(sign);
62 }
63
64 let mut timeline = Vec::new();
65 let mut motifs = Vec::new();
66 for (feature_id, series) in grouped {
67 let envelope = feature_envelope(&series);
68 let labels = series
69 .iter()
70 .enumerate()
71 .map(|(index, point)| classify_point(&series, index, point, envelope))
72 .collect::<Vec<_>>();
73
74 for (point, label) in series.iter().zip(&labels) {
75 timeline.push(MotifTimelinePoint {
76 feature_id: feature_id.to_string(),
77 motif_type: (*label).to_string(),
78 timestamp: point.timestamp,
79 });
80 }
81
82 let mut start = 0usize;
83 while start < series.len() {
84 let label = labels[start];
85 let mut end = start;
86 while end + 1 < series.len() && labels[end + 1] == label {
87 end += 1;
88 }
89 motifs.push(Motif {
90 feature_id: feature_id.to_string(),
91 motif_type: label.to_string(),
92 start_time: series[start].timestamp,
93 end_time: series[end].timestamp,
94 });
95 start = end + 1;
96 }
97 }
98
99 timeline.sort_by(|left, right| {
100 left.timestamp
101 .total_cmp(&right.timestamp)
102 .then_with(|| left.feature_id.cmp(&right.feature_id))
103 });
104 motifs.sort_by(|left, right| {
105 left.start_time
106 .total_cmp(&right.start_time)
107 .then_with(|| left.feature_id.cmp(&right.feature_id))
108 });
109
110 SyntaxArtifacts { motifs, timeline }
111}
112
113#[cfg(feature = "std")]
114pub fn write_motifs_csv(path: &std::path::Path, rows: &[Motif]) -> Result<()> {
115 let mut writer = csv::Writer::from_path(path)?;
116 for row in rows {
117 writer.serialize(row)?;
118 }
119 writer.flush()?;
120 Ok(())
121}
122
123#[cfg(feature = "std")]
124pub fn write_feature_motif_timeline_csv(
125 path: &std::path::Path,
126 rows: &[MotifTimelinePoint],
127) -> Result<()> {
128 let mut writer = csv::Writer::from_path(path)?;
129 for row in rows {
130 writer.serialize(row)?;
131 }
132 writer.flush()?;
133 Ok(())
134}
135
136fn feature_envelope(points: &[&FeatureSignPoint]) -> f64 {
137 let mean = points.iter().map(|point| point.r.abs()).sum::<f64>() / points.len().max(1) as f64;
138 let variance = points
139 .iter()
140 .map(|point| {
141 let centered = point.r.abs() - mean;
142 centered * centered
143 })
144 .sum::<f64>()
145 / points.len().max(1) as f64;
146 #[cfg(feature = "std")]
147 return (mean + variance.sqrt()).max(1.0);
148 #[cfg(not(feature = "std"))]
149 return (mean + maybe_sqrt(variance)).max(1.0);
150}
151
152fn classify_point(
153 series: &[&FeatureSignPoint],
154 index: usize,
155 point: &FeatureSignPoint,
156 envelope: f64,
157) -> &'static str {
158 let abs_r = point.r.abs();
159 let abs_s = point.s.abs();
160 let prev = index
161 .checked_sub(1)
162 .and_then(|idx| series.get(idx))
163 .copied();
164 let prev_prev = index
165 .checked_sub(2)
166 .and_then(|idx| series.get(idx))
167 .copied();
168 let next = series.get(index + 1).copied();
169 let prev_abs = prev.map(|row| row.r.abs()).unwrap_or(abs_r);
170 let next_abs = next.map(|row| row.r.abs()).unwrap_or(abs_r);
171 let drift_stable = prev
172 .zip(prev_prev)
173 .map(|(left, right)| {
174 point.d.signum() != 0.0
175 && point.d.signum() == left.d.signum()
176 && left.d.signum() == right.d.signum()
177 && point.d.abs() >= 0.05 * envelope
178 })
179 .unwrap_or(false);
180 let oscillatory = prev
181 .map(|row| {
182 point.d.signum() != 0.0
183 && row.d.signum() != 0.0
184 && point.d.signum() != row.d.signum()
185 && point.d.abs() >= 0.05 * envelope
186 })
187 .unwrap_or(false);
188 let burst_cluster = index >= 2
189 && series[index - 2..=index]
190 .iter()
191 .filter(|row| row.s.abs() >= 0.20 * envelope)
192 .count()
193 >= 2;
194 let recovering = prev
195 .map(|row| row.r.abs() >= 0.60 * envelope && row.r.abs() > abs_r && point.d < 0.0)
196 .unwrap_or(false);
197
198 if recovering {
199 "recovery_pattern"
200 } else if burst_cluster && abs_r >= 0.70 * envelope {
201 "burst_instability"
202 } else if abs_r >= envelope && oscillatory {
203 "persistent_instability"
204 } else if abs_s >= 0.25 * envelope && abs_r >= 0.50 * envelope {
205 "transient_excursion"
206 } else if drift_stable && abs_r >= 0.55 * envelope && next_abs >= abs_r {
207 "slow_drift_precursor"
208 } else if abs_r >= 0.55 * envelope
209 && (oscillatory || (prev_abs >= 0.55 * envelope && next_abs >= 0.55 * envelope))
210 {
211 "boundary_grazing"
212 } else if abs_s >= 0.20 * envelope && abs_r < 0.40 * envelope {
213 "noise_like"
214 } else {
215 "null"
216 }
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222
223 fn point(timestamp: f64, r: f64, d: f64, s: f64) -> FeatureSignPoint {
224 FeatureSignPoint {
225 timestamp,
226 feature_id: "S001".into(),
227 r,
228 d,
229 s,
230 }
231 }
232
233 #[test]
234 fn motif_logic_is_not_just_threshold_labeling() {
235 let flat_high = vec![
236 point(0.0, 4.0, 0.0, 0.0),
237 point(1.0, 4.0, 0.0, 0.0),
238 point(2.0, 4.0, 0.0, 0.0),
239 ];
240 let drifting = vec![
241 point(0.0, 1.0, 0.0, 0.0),
242 point(1.0, 2.0, 1.0, 1.0),
243 point(2.0, 3.0, 1.0, 0.0),
244 point(3.0, 4.0, 1.0, 0.0),
245 ];
246 let flat = build_motifs(&flat_high);
247 let drift = build_motifs(&drifting);
248
249 assert!(flat
250 .timeline
251 .iter()
252 .all(|row| row.motif_type != "slow_drift_precursor"));
253 assert!(drift
254 .timeline
255 .iter()
256 .any(|row| row.motif_type == "slow_drift_precursor"));
257 }
258}