use crate::ast::Value;
#[crate::polydat_node(category = Diagnostic)]
fn type_of(input: Value) -> String {
input.port_type().to_string()
}
#[crate::polydat_node(category = Diagnostic)]
fn debug_repr(input: Value) -> String {
format!("{input:?}")
}
#[crate::polydat_node(category = Diagnostic, purity = SideChannel(Stderr))]
fn inspect(
input: Value,
#[poly_default("inspect")] label: crate::derive_support::Const<&str>,
) -> Value {
eprintln!("[inspect:{}] {input:?}", label.0);
input
}
struct FftOutput {
path: String,
writer: Option<std::io::BufWriter<std::fs::File>>,
open_attempted: bool,
}
impl crate::derive_support::PolydatSetup for std::sync::Mutex<Vec<f64>> {}
impl crate::derive_support::PolydatSetup for std::sync::Mutex<FftOutput> {}
fn fft_buffer(window_size: u64) -> std::sync::Mutex<Vec<f64>> {
let cap = window_size.max(2) as usize;
std::sync::Mutex::new(Vec::with_capacity(cap))
}
fn fft_output(filename: &str) -> std::sync::Mutex<FftOutput> {
std::sync::Mutex::new(FftOutput {
path: filename.to_string(),
writer: None,
open_attempted: false,
})
}
#[crate::polydat_node(
category = Diagnostic,
purity = Nondeterministic("accumulates signal buffer across calls; writes JSONL on window emit"),
)]
fn fft_analyze(
signal: f64,
#[poly_default("fft.jsonl")] filename: crate::derive_support::Const<&str>,
#[poly_default(256u64)] window_size: crate::derive_support::Const<u64>,
#[poly_const(fft_buffer, from = window_size)]
buffer: &std::sync::Mutex<Vec<f64>>,
#[poly_const(fft_output, from = filename)]
output: &std::sync::Mutex<FftOutput>,
) -> u64 {
let _ = filename; let window = (*window_size).max(2) as usize;
let mut buf = buffer.lock().unwrap();
let current_len = buf.len() as u64;
buf.push(signal);
if buf.len() >= window {
let n = buf.len();
let mut magnitudes = Vec::with_capacity(n / 2 + 1);
let mut phases = Vec::with_capacity(n / 2 + 1);
for k in 0..=(n / 2) {
let mut re = 0.0f64;
let mut im = 0.0f64;
for (i, &x) in buf.iter().enumerate() {
let angle = -2.0 * std::f64::consts::PI * (k as f64) * (i as f64) / (n as f64);
re += x * angle.cos();
im += x * angle.sin();
}
magnitudes.push((re * re + im * im).sqrt() / n as f64);
phases.push(im.atan2(re));
}
if let Ok(mut out) = output.lock() {
if !out.open_attempted {
out.open_attempted = true;
out.writer = std::fs::File::create(&out.path).ok()
.map(std::io::BufWriter::new);
}
if let Some(ref mut writer) = out.writer {
use std::io::Write;
let json = serde_json::json!({
"window_size": n,
"magnitudes": magnitudes,
"phases": phases,
"dc": magnitudes.first().copied().unwrap_or(0.0),
"fundamental": magnitudes.get(1).copied().unwrap_or(0.0),
});
let _ = writeln!(writer, "{}", json);
let _ = writer.flush();
}
}
buf.clear();
}
current_len
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::{PolydatNode, PortType};
#[test]
fn type_of_u64() {
let node = TypeOf::new(PortType::U64);
let mut out = [Value::None];
node.eval(&[Value::U64(42)], &mut out);
assert_eq!(out[0].as_str(), "u64");
}
#[test]
fn type_of_f64() {
let node = TypeOf::new(PortType::F64);
let mut out = [Value::None];
node.eval(&[Value::F64(3.14)], &mut out);
assert_eq!(out[0].as_str(), "f64");
}
#[test]
fn type_of_str() {
let node = TypeOf::new(PortType::Str);
let mut out = [Value::None];
node.eval(&[Value::Str("hello".into())], &mut out);
assert_eq!(out[0].as_str(), "String");
}
#[test]
fn debug_repr_u64() {
let node = DebugRepr::new(PortType::U64);
let mut out = [Value::None];
node.eval(&[Value::U64(42)], &mut out);
assert_eq!(out[0].as_str(), "U64(42)");
}
#[test]
fn debug_repr_str() {
let node = DebugRepr::new(PortType::Str);
let mut out = [Value::None];
node.eval(&[Value::Str("hello".into())], &mut out);
assert!(out[0].as_str().contains("hello"));
}
#[test]
fn inspect_passthrough() {
let node = Inspect::new(PortType::U64, "test".to_string());
let mut out = [Value::None];
node.eval(&[Value::U64(42)], &mut out);
assert_eq!(out[0].as_u64(), 42);
}
#[test]
fn fft_analyzer_collects_and_writes() {
let tmp = std::env::temp_dir().join("test_fft_diag.jsonl");
let path = tmp.to_str().unwrap();
let node = FftAnalyze::new(path.to_string(), 4u64);
let mut out = [Value::None];
for i in 0..4 {
node.eval(&[Value::F64(1.0)], &mut out);
assert_eq!(out[0].as_u64(), i as u64);
}
node.eval(&[Value::F64(1.0)], &mut out);
assert_eq!(out[0].as_u64(), 0);
let contents = std::fs::read_to_string(path).unwrap();
assert!(!contents.is_empty(), "JSONL file should not be empty");
let line: serde_json::Value = serde_json::from_str(contents.lines().next().unwrap()).unwrap();
assert_eq!(line["window_size"], 4);
let dc = line["dc"].as_f64().unwrap();
assert!((dc - 1.0).abs() < 0.001, "DC component of constant signal should be ~1.0, got {dc}");
let _ = std::fs::remove_file(path);
}
}