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