Skip to main content

dsfb_semiconductor/syntax/
mod.rs

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