polydat_core/library/
diagnostic.rs1use crate::ast::Value;
17
18#[crate::polydat_node(category = Diagnostic)]
25fn type_of(input: Value) -> String {
26 input.port_type().to_string()
27}
28
29#[crate::polydat_node(category = Diagnostic)]
31fn debug_repr(input: Value) -> String {
32 format!("{input:?}")
33}
34
35#[crate::polydat_node(category = Diagnostic, purity = SideChannel(Stderr))]
38fn inspect(
39 input: Value,
40 #[poly_default("inspect")] label: crate::derive_support::Const<&str>,
41) -> Value {
42 eprintln!("[inspect:{}] {input:?}", label.0);
43 input
44}
45
46struct FftOutput {
55 path: String,
56 writer: Option<std::io::BufWriter<std::fs::File>>,
57 open_attempted: bool,
58}
59
60impl crate::derive_support::PolydatSetup for std::sync::Mutex<Vec<f64>> {}
61impl crate::derive_support::PolydatSetup for std::sync::Mutex<FftOutput> {}
62
63fn fft_buffer(window_size: u64) -> std::sync::Mutex<Vec<f64>> {
68 let cap = window_size.max(2) as usize;
69 std::sync::Mutex::new(Vec::with_capacity(cap))
70}
71
72fn fft_output(filename: &str) -> std::sync::Mutex<FftOutput> {
77 std::sync::Mutex::new(FftOutput {
78 path: filename.to_string(),
79 writer: None,
80 open_attempted: false,
81 })
82}
83
84#[crate::polydat_node(
102 category = Diagnostic,
103 purity = Nondeterministic("accumulates signal buffer across calls; writes JSONL on window emit"),
104)]
105fn fft_analyze(
106 signal: f64,
107 #[poly_default("fft.jsonl")] filename: crate::derive_support::Const<&str>,
108 #[poly_default(256u64)] window_size: crate::derive_support::Const<u64>,
109 #[poly_const(fft_buffer, from = window_size)] buffer: &std::sync::Mutex<Vec<f64>>,
110 #[poly_const(fft_output, from = filename)] output: &std::sync::Mutex<FftOutput>,
111) -> u64 {
112 let _ = filename; let window = (*window_size).max(2) as usize;
114
115 let mut buf = buffer.lock().unwrap();
116 let current_len = buf.len() as u64;
117
118 buf.push(signal);
119
120 if buf.len() >= window {
121 let n = buf.len();
123 let mut magnitudes = Vec::with_capacity(n / 2 + 1);
124 let mut phases = Vec::with_capacity(n / 2 + 1);
125
126 for k in 0..=(n / 2) {
127 let mut re = 0.0f64;
128 let mut im = 0.0f64;
129 for (i, &x) in buf.iter().enumerate() {
130 let angle = -2.0 * std::f64::consts::PI * (k as f64) * (i as f64) / (n as f64);
131 re += x * angle.cos();
132 im += x * angle.sin();
133 }
134 magnitudes.push((re * re + im * im).sqrt() / n as f64);
135 phases.push(im.atan2(re));
136 }
137
138 if let Ok(mut out) = output.lock() {
143 if !out.open_attempted {
144 out.open_attempted = true;
145 out.writer = std::fs::File::create(&out.path)
146 .ok()
147 .map(std::io::BufWriter::new);
148 }
149 if let Some(ref mut writer) = out.writer {
150 use std::io::Write;
151 let json = serde_json::json!({
152 "window_size": n,
153 "magnitudes": magnitudes,
154 "phases": phases,
155 "dc": magnitudes.first().copied().unwrap_or(0.0),
156 "fundamental": magnitudes.get(1).copied().unwrap_or(0.0),
157 });
158 let _ = writeln!(writer, "{}", json);
159 let _ = writer.flush();
160 }
161 }
162
163 buf.clear();
164 }
165
166 current_len
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 use crate::ast::{PolydatNode, PortType};
174
175 #[test]
176 fn type_of_u64() {
177 let node = TypeOf::new(PortType::U64);
178 let mut out = [Value::None];
179 node.eval(&[Value::U64(42)], &mut out);
180 assert_eq!(out[0].as_str(), "u64");
181 }
182
183 #[test]
184 fn type_of_f64() {
185 let node = TypeOf::new(PortType::F64);
186 let mut out = [Value::None];
187 node.eval(&[Value::F64(3.14)], &mut out);
188 assert_eq!(out[0].as_str(), "f64");
189 }
190
191 #[test]
192 fn type_of_str() {
193 let node = TypeOf::new(PortType::Str);
194 let mut out = [Value::None];
195 node.eval(&[Value::Str("hello".into())], &mut out);
196 assert_eq!(out[0].as_str(), "String");
197 }
198
199 #[test]
200 fn debug_repr_u64() {
201 let node = DebugRepr::new(PortType::U64);
202 let mut out = [Value::None];
203 node.eval(&[Value::U64(42)], &mut out);
204 assert_eq!(out[0].as_str(), "U64(42)");
205 }
206
207 #[test]
208 fn debug_repr_str() {
209 let node = DebugRepr::new(PortType::Str);
210 let mut out = [Value::None];
211 node.eval(&[Value::Str("hello".into())], &mut out);
212 assert!(out[0].as_str().contains("hello"));
213 }
214
215 #[test]
216 fn inspect_passthrough() {
217 let node = Inspect::new(PortType::U64, "test".to_string());
218 let mut out = [Value::None];
219 node.eval(&[Value::U64(42)], &mut out);
220 assert_eq!(out[0].as_u64(), 42);
221 }
222
223 #[test]
224 fn fft_analyzer_collects_and_writes() {
225 let tmp = std::env::temp_dir().join("test_fft_diag.jsonl");
226 let path = tmp.to_str().unwrap();
227 let node = FftAnalyze::new(path.to_string(), 4u64);
228 let mut out = [Value::None];
229
230 for i in 0..4 {
232 node.eval(&[Value::F64(1.0)], &mut out);
233 assert_eq!(out[0].as_u64(), i as u64);
235 }
236
237 node.eval(&[Value::F64(1.0)], &mut out);
240 assert_eq!(out[0].as_u64(), 0);
241
242 let contents = std::fs::read_to_string(path).unwrap();
244 assert!(!contents.is_empty(), "JSONL file should not be empty");
245 let line: serde_json::Value =
246 serde_json::from_str(contents.lines().next().unwrap()).unwrap();
247 assert_eq!(line["window_size"], 4);
248 let dc = line["dc"].as_f64().unwrap();
251 assert!(
252 (dc - 1.0).abs() < 0.001,
253 "DC component of constant signal should be ~1.0, got {dc}"
254 );
255
256 let _ = std::fs::remove_file(path);
258 }
259}