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 std::sync::Mutex::new(crate::derive_support::buffer_for(
69 window_size.max(2),
70 "fft_analyze window",
71 ))
72}
73
74fn fft_output(filename: &str) -> std::sync::Mutex<FftOutput> {
79 std::sync::Mutex::new(FftOutput {
80 path: filename.to_string(),
81 writer: None,
82 open_attempted: false,
83 })
84}
85
86#[crate::polydat_node(
104 category = Diagnostic,
105 purity = Nondeterministic("accumulates signal buffer across calls; writes JSONL on window emit"),
106)]
107fn fft_analyze(
108 signal: f64,
109 #[poly_default("fft.jsonl")] filename: crate::derive_support::Const<&str>,
110 #[poly_default(256u64)] window_size: crate::derive_support::Const<u64>,
111 #[poly_const(fft_buffer, from = window_size)] buffer: &std::sync::Mutex<Vec<f64>>,
112 #[poly_const(fft_output, from = filename)] output: &std::sync::Mutex<FftOutput>,
113) -> u64 {
114 let _ = filename; let window = (*window_size).max(2) as usize;
116
117 let mut buf = buffer.lock().unwrap();
118 let current_len = buf.len() as u64;
119
120 buf.push(signal);
121
122 if buf.len() >= window {
123 let n = buf.len();
125 let mut magnitudes = Vec::with_capacity(n / 2 + 1);
126 let mut phases = Vec::with_capacity(n / 2 + 1);
127
128 for k in 0..=(n / 2) {
129 let mut re = 0.0f64;
130 let mut im = 0.0f64;
131 for (i, &x) in buf.iter().enumerate() {
132 let angle = -2.0 * std::f64::consts::PI * (k as f64) * (i as f64) / (n as f64);
133 re += x * angle.cos();
134 im += x * angle.sin();
135 }
136 magnitudes.push((re * re + im * im).sqrt() / n as f64);
137 phases.push(im.atan2(re));
138 }
139
140 if let Ok(mut out) = output.lock() {
145 if !out.open_attempted {
146 out.open_attempted = true;
147 out.writer = std::fs::File::create(&out.path)
148 .ok()
149 .map(std::io::BufWriter::new);
150 }
151 if let Some(ref mut writer) = out.writer {
152 use std::io::Write;
153 let json = serde_json::json!({
154 "window_size": n,
155 "magnitudes": magnitudes,
156 "phases": phases,
157 "dc": magnitudes.first().copied().unwrap_or(0.0),
158 "fundamental": magnitudes.get(1).copied().unwrap_or(0.0),
159 });
160 let _ = writeln!(writer, "{}", json);
161 let _ = writer.flush();
162 }
163 }
164
165 buf.clear();
166 }
167
168 current_len
169}
170
171#[cfg(test)]
172mod tests {
173 use super::*;
174
175 use crate::ast::{PolydatNode, PortType};
176
177 #[test]
178 fn type_of_u64() {
179 let node = TypeOf::new(PortType::U64);
180 let mut out = [Value::None];
181 node.eval(&[Value::U64(42)], &mut out);
182 assert_eq!(out[0].as_str(), "u64");
183 }
184
185 #[test]
186 fn type_of_f64() {
187 let node = TypeOf::new(PortType::F64);
188 let mut out = [Value::None];
189 node.eval(&[Value::F64(3.14)], &mut out);
190 assert_eq!(out[0].as_str(), "f64");
191 }
192
193 #[test]
194 fn type_of_str() {
195 let node = TypeOf::new(PortType::Str);
196 let mut out = [Value::None];
197 node.eval(&[Value::Str("hello".into())], &mut out);
198 assert_eq!(out[0].as_str(), "String");
199 }
200
201 #[test]
202 fn debug_repr_u64() {
203 let node = DebugRepr::new(PortType::U64);
204 let mut out = [Value::None];
205 node.eval(&[Value::U64(42)], &mut out);
206 assert_eq!(out[0].as_str(), "U64(42)");
207 }
208
209 #[test]
210 fn debug_repr_str() {
211 let node = DebugRepr::new(PortType::Str);
212 let mut out = [Value::None];
213 node.eval(&[Value::Str("hello".into())], &mut out);
214 assert!(out[0].as_str().contains("hello"));
215 }
216
217 #[test]
218 fn inspect_passthrough() {
219 let node = Inspect::new(PortType::U64, "test".to_string());
220 let mut out = [Value::None];
221 node.eval(&[Value::U64(42)], &mut out);
222 assert_eq!(out[0].as_u64(), 42);
223 }
224
225 #[test]
226 fn fft_analyzer_collects_and_writes() {
227 let tmp = std::env::temp_dir().join("test_fft_diag.jsonl");
228 let path = tmp.to_str().unwrap();
229 let node = FftAnalyze::new(path.to_string(), 4u64);
230 let mut out = [Value::None];
231
232 for i in 0..4 {
234 node.eval(&[Value::F64(1.0)], &mut out);
235 assert_eq!(out[0].as_u64(), i as u64);
237 }
238
239 node.eval(&[Value::F64(1.0)], &mut out);
242 assert_eq!(out[0].as_u64(), 0);
243
244 let contents = std::fs::read_to_string(path).unwrap();
246 assert!(!contents.is_empty(), "JSONL file should not be empty");
247 let line: serde_json::Value =
248 serde_json::from_str(contents.lines().next().unwrap()).unwrap();
249 assert_eq!(line["window_size"], 4);
250 let dc = line["dc"].as_f64().unwrap();
253 assert!(
254 (dc - 1.0).abs() < 0.001,
255 "DC component of constant signal should be ~1.0, got {dc}"
256 );
257
258 let _ = std::fs::remove_file(path);
260 }
261}