dsfb-semiconductor 0.1.0

Deterministic DSFB semiconductor benchmark companion for SECOM and PHM-style dataset adapters
Documentation
#[cfg(feature = "std")]
use crate::error::Result;
use crate::sign::FeatureSignPoint;
use crate::syntax::MotifTimelinePoint;
use serde::{Deserialize, Serialize};
#[cfg(feature = "std")]
use std::collections::BTreeMap;
#[cfg(not(feature = "std"))]
use alloc::{collections::BTreeMap, string::{String, ToString}, vec::Vec};

#[cfg(not(feature = "std"))]
#[inline]
fn maybe_sqrt(x: f64) -> f64 {
    if x <= 0.0 {
        return 0.0;
    }
    let mut s = x / 2.0;
    for _ in 0..32 {
        s = (s + x / s) * 0.5;
    }
    s
}

pub const ALLOWED_GRAMMAR_STATES: [&str; 6] = [
    "Admissible",
    "BoundaryGrazing",
    "SustainedDrift",
    "TransientViolation",
    "PersistentViolation",
    "Recovery",
];

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct GrammarState {
    pub feature_id: String,
    pub state: String,
    pub timestamp: f64,
}

pub fn build_grammar_states(
    signs: &[FeatureSignPoint],
    motifs: &[MotifTimelinePoint],
) -> Vec<GrammarState> {
    let mut motif_map = BTreeMap::<(&str, u64), &str>::new();
    for motif in motifs {
        motif_map.insert(
            (motif.feature_id.as_str(), motif.timestamp.to_bits()),
            motif.motif_type.as_str(),
        );
    }

    let mut grouped = BTreeMap::<&str, Vec<&FeatureSignPoint>>::new();
    for sign in signs {
        grouped
            .entry(sign.feature_id.as_str())
            .or_default()
            .push(sign);
    }

    let mut states = Vec::new();
    for (feature_id, series) in grouped {
        let envelope = feature_envelope(&series);
        let mut violation_streak = 0usize;
        let mut drift_streak = 0usize;
        let mut previous_non_admissible = false;

        for point in series {
            let motif = motif_map
                .get(&(feature_id, point.timestamp.to_bits()))
                .copied()
                .unwrap_or("null");
            let abs_r = point.r.abs();
            let state = if abs_r >= envelope {
                violation_streak += 1;
                drift_streak = 0;
                previous_non_admissible = true;
                if violation_streak >= 2
                    || motif == "persistent_instability"
                    || motif == "burst_instability"
                {
                    "PersistentViolation"
                } else {
                    "TransientViolation"
                }
            } else if motif == "slow_drift_precursor" && abs_r >= 0.60 * envelope && point.d > 0.0 {
                violation_streak = 0;
                drift_streak += 1;
                previous_non_admissible = true;
                if drift_streak >= 3 {
                    "SustainedDrift"
                } else {
                    "BoundaryGrazing"
                }
            } else if motif == "boundary_grazing" && abs_r >= 0.50 * envelope {
                violation_streak = 0;
                drift_streak = 0;
                previous_non_admissible = true;
                "BoundaryGrazing"
            } else if previous_non_admissible && motif == "recovery_pattern" {
                violation_streak = 0;
                drift_streak = 0;
                previous_non_admissible = false;
                "Recovery"
            } else {
                violation_streak = 0;
                drift_streak = 0;
                previous_non_admissible = false;
                "Admissible"
            };

            states.push(GrammarState {
                feature_id: feature_id.to_string(),
                state: state.to_string(),
                timestamp: point.timestamp,
            });
        }
    }

    states.sort_by(|left, right| {
        left.timestamp
            .total_cmp(&right.timestamp)
            .then_with(|| left.feature_id.cmp(&right.feature_id))
    });
    states
}

#[cfg(feature = "std")]
pub fn write_grammar_states_csv(path: &std::path::Path, rows: &[GrammarState]) -> Result<()> {
    let mut writer = csv::Writer::from_path(path)?;
    for row in rows {
        writer.serialize(row)?;
    }
    writer.flush()?;
    Ok(())
}

fn feature_envelope(points: &[&FeatureSignPoint]) -> f64 {
    let mean = points.iter().map(|point| point.r.abs()).sum::<f64>() / points.len().max(1) as f64;
    let variance = points
        .iter()
        .map(|point| {
            let centered = point.r.abs() - mean;
            centered * centered
        })
        .sum::<f64>()
        / points.len().max(1) as f64;
    #[cfg(feature = "std")]
    return (mean + variance.sqrt()).max(1.0);
    #[cfg(not(feature = "std"))]
    return (mean + maybe_sqrt(variance)).max(1.0);
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::sign::FeatureSignPoint;
    use crate::syntax::MotifTimelinePoint;

    fn point(timestamp: f64, r: f64, d: f64, s: f64) -> FeatureSignPoint {
        FeatureSignPoint {
            timestamp,
            feature_id: "S001".into(),
            r,
            d,
            s,
        }
    }

    #[test]
    fn grammar_depends_on_persistence_and_trajectory() {
        let signs = vec![
            point(0.0, 1.0, 0.0, 0.0),
            point(1.0, 2.0, 1.0, 1.0),
            point(2.0, 3.0, 1.0, 0.0),
            point(3.0, 3.1, 0.1, -0.9),
            point(4.0, 3.2, 0.1, 0.0),
        ];
        let motifs = vec![
            MotifTimelinePoint {
                feature_id: "S001".into(),
                motif_type: "null".into(),
                timestamp: 0.0,
            },
            MotifTimelinePoint {
                feature_id: "S001".into(),
                motif_type: "slow_drift_precursor".into(),
                timestamp: 1.0,
            },
            MotifTimelinePoint {
                feature_id: "S001".into(),
                motif_type: "slow_drift_precursor".into(),
                timestamp: 2.0,
            },
            MotifTimelinePoint {
                feature_id: "S001".into(),
                motif_type: "slow_drift_precursor".into(),
                timestamp: 3.0,
            },
            MotifTimelinePoint {
                feature_id: "S001".into(),
                motif_type: "slow_drift_precursor".into(),
                timestamp: 4.0,
            },
        ];
        let grammar = build_grammar_states(&signs, &motifs);
        assert!(grammar.iter().any(|row| row.state == "SustainedDrift"));
    }

    #[test]
    fn recovery_requires_prior_non_admissible_state() {
        let signs = vec![
            point(0.0, 5.0, 0.0, 0.0),
            point(1.0, 6.0, 1.0, 1.0),
            point(2.0, 3.0, -3.0, -4.0),
        ];
        let motifs = vec![
            MotifTimelinePoint {
                feature_id: "S001".into(),
                motif_type: "persistent_instability".into(),
                timestamp: 0.0,
            },
            MotifTimelinePoint {
                feature_id: "S001".into(),
                motif_type: "burst_instability".into(),
                timestamp: 1.0,
            },
            MotifTimelinePoint {
                feature_id: "S001".into(),
                motif_type: "recovery_pattern".into(),
                timestamp: 2.0,
            },
        ];
        let grammar = build_grammar_states(&signs, &motifs);
        assert_eq!(grammar.last().unwrap().state, "Recovery");
    }
}