1#![allow(dead_code)]
2
3use crate::args::Args;
8use crate::npy_output::{npz_sidecar_path, write_named_real_1d_npz, NamedNpz, NpyMeta};
9use crate::output::generate_output_names;
10use crate::png_compress::{compress_png, compress_png_with_mode, CompressQuality};
11use crate::processing::ProcessResult;
12use crate::utils::safe_arg;
13use chrono::{DateTime, TimeZone, Utc};
14use ndarray::Array2; use num_complex::Complex;
16use plotters::backend::RGBPixel;
17use plotters::coord::Shift;
18use plotters::prelude::*;
19use plotters::style::colors::colormaps::ViridisRGB;
20use plotters::style::text_anchor::{HPos, Pos, VPos};
21use rayon::prelude::*;
22use std::error::Error;
23use std::f64::consts::PI;
24use std::path::{Path, PathBuf};
25use std::sync::OnceLock;
26use std::{fs::File, io::Write};
27
28static PLOT_POOL: OnceLock<rayon::ThreadPool> = OnceLock::new();
29const PLOT_MAX_THREADS: usize = 3;
30
31fn plot_pool() -> &'static rayon::ThreadPool {
32 PLOT_POOL.get_or_init(|| {
33 rayon::ThreadPoolBuilder::new()
34 .num_threads(PLOT_MAX_THREADS)
35 .build()
36 .expect("failed to build plot thread pool")
37 })
38}
39
40fn build_heatmap_rgb_buffer<F>(
41 width: u32,
42 height: u32,
43 x_min: f64,
44 x_max: f64,
45 y_min: f64,
46 y_max: f64,
47 amplitude_norm: f64,
48 heatmap_func: &F,
49) -> Vec<u8>
50where
51 F: Fn(f64, f64) -> f64 + Sync,
52{
53 let width_usize = width as usize;
54 let height_usize = height as usize;
55 let x_den = (width_usize.saturating_sub(1)).max(1) as f64;
56 let y_den = (height_usize.saturating_sub(1)).max(1) as f64;
57 let amp = amplitude_norm.max(1e-30);
58 let mut buffer = vec![0u8; width_usize * height_usize * 3];
59
60 plot_pool().install(|| {
61 buffer
62 .par_chunks_mut(3)
63 .enumerate()
64 .for_each(|(idx, pixel)| {
65 let px = idx % width_usize;
66 let py = idx / width_usize;
67 let x = x_min + (x_max - x_min) * px as f64 / x_den;
68 let y = y_max - (y_max - y_min) * py as f64 / y_den;
69 let normalized = (heatmap_func(x, y) / amp).clamp(0.0, 1.0);
70 let (r, g, b) = HSLColor((1.0 - normalized) * 0.7, 1.0, 0.5).rgb();
71 pixel[0] = r;
72 pixel[1] = g;
73 pixel[2] = b;
74 });
75 });
76
77 buffer
78}
79
80pub fn delay_plane(
81 delay_profile: &[(f64, f64)],
82 delay_profile_pre_bp: Option<&[(f64, f64)]>,
83 rate_profile: &[(f64, f64)],
84 rate_profile_pre_bp: Option<&[(f64, f64)]>,
85 heatmap_func: impl Fn(f64, f64) -> f64 + Sync,
86 stat_keys: &[&str],
87 stat_vals: &[&str],
88 output_path: &str,
89 rate_range: &[f32],
90 length: f32,
91 effective_integration_length: f32,
92 drange: &[f32],
93 rrange: &[f32],
94 mask: Option<(f32, f32, f32, f32)>,
95 max_amplitude: f64,
96 _heatmap_res_x: usize,
97 _heatmap_res_y: usize,
98 heatmap_only: bool,
99 ) -> Result<(), Box<dyn std::error::Error>> {
101 let width = 1400;
102 let height = 1000;
103 let root = BitMapBackend::new(output_path, (width, height)).into_drawing_area();
104 root.fill(&WHITE)?;
105
106 let (upper, lower) = root.split_vertically(height / 2);
107 let (upper_left, upper_right) = upper.split_horizontally(width / 2 - 10);
108 let (lower_left, lower_right) = lower.split_horizontally(width / 2 - 10);
109 let (heatmap_area, colorbar_area) = upper_right.split_horizontally((width / 2) - 110);
110
111 let delay_max = delay_profile
113 .iter()
114 .map(|(_, y)| *y)
115 .chain(
116 delay_profile_pre_bp
117 .into_iter()
118 .flat_map(|profile| profile.iter().map(|(_, y)| *y)),
119 )
120 .fold(f64::NEG_INFINITY, f64::max)
121 * 1.1;
122 let rate_max = rate_profile
123 .iter()
124 .map(|(_, y)| *y)
125 .chain(
126 rate_profile_pre_bp
127 .into_iter()
128 .flat_map(|profile| profile.iter().map(|(_, y)| *y)),
129 )
130 .fold(f64::NEG_INFINITY, f64::max)
131 * 1.1;
132
133 let (delay_line_min, delay_line_max, heatmap_delay_min, heatmap_delay_max) =
134 if drange.is_empty() {
135 (-32.0, 32.0, -10.0, 10.0)
136 } else {
137 (
138 drange[0] as f64,
139 drange[1] as f64,
140 drange[0] as f64,
141 drange[1] as f64,
142 )
143 };
144
145 let (rate_line_min, rate_line_max, heatmap_rate_min, heatmap_rate_max) = if rrange.is_empty() {
146 let rate_min_x = rate_profile
147 .iter()
148 .map(|(x, _)| *x)
149 .fold(f64::INFINITY, f64::min);
150 let rate_max_x = rate_profile
151 .iter()
152 .map(|(x, _)| *x)
153 .fold(f64::NEG_INFINITY, f64::max);
154 let rate_win_range_low = if (-8.0 / length as f64) < rate_range[0] as f64 {
155 rate_range[0] as f64 * effective_integration_length as f64
156 } else {
157 -4.0 / (length as f64 * effective_integration_length as f64)
158 };
159 let rate_win_range_high = if (8.0 / length as f64) > *rate_range.last().unwrap() as f64 {
160 *rate_range.last().unwrap() as f64 * effective_integration_length as f64
161 } else {
162 4.0 / (length as f64 * effective_integration_length as f64)
163 };
164 (
165 rate_min_x,
166 rate_max_x,
167 rate_win_range_low,
168 rate_win_range_high,
169 )
170 } else {
171 (
172 rrange[0] as f64,
173 rrange[1] as f64,
174 rrange[0] as f64,
175 rrange[1] as f64,
176 )
177 };
178
179 if heatmap_only {
180 let (heatmap_area, colorbar_area) = root.split_horizontally(width - 120);
181 let mut chart = ChartBuilder::on(&heatmap_area)
182 .margin(20)
183 .x_label_area_size(65)
184 .y_label_area_size(95)
185 .build_cartesian_2d(
186 heatmap_delay_min..heatmap_delay_max,
187 heatmap_rate_min..heatmap_rate_max,
188 )?;
189
190 chart
191 .configure_mesh()
192 .x_desc("Delay [Sample]")
193 .y_desc("Rate [Hz]")
194 .x_labels(9)
195 .y_labels(10)
196 .x_max_light_lines(0)
197 .y_max_light_lines(0)
198 .label_style(("sans-serif ", 28))
199 .x_label_formatter(&|v| format!("{:.0}", v))
200 .y_label_formatter(&|v| format!("{:.2e}", v))
201 .draw()?;
202
203 let amplitude_norm = if max_amplitude.is_finite() && max_amplitude > 0.0 {
204 max_amplitude
205 } else {
206 1e-30
207 };
208 let (plot_x, plot_y) = chart.plotting_area().get_pixel_range();
209 let bitmap_width = plot_x.len().max(1) as u32;
210 let bitmap_height = plot_y.len().max(1) as u32;
211 let heatmap_buffer = build_heatmap_rgb_buffer(
212 bitmap_width,
213 bitmap_height,
214 heatmap_delay_min,
215 heatmap_delay_max,
216 heatmap_rate_min,
217 heatmap_rate_max,
218 amplitude_norm,
219 &heatmap_func,
220 );
221 let bitmap = BitMapElement::<_, RGBPixel>::with_owned_buffer(
222 (heatmap_delay_min, heatmap_rate_max),
223 (bitmap_width, bitmap_height),
224 heatmap_buffer,
225 )
226 .ok_or("failed to create heatmap bitmap")?;
227 chart.draw_series(std::iter::once(bitmap))?;
228 if let Some((mask_delay_min, mask_delay_max, mask_rate_min, mask_rate_max)) = mask {
229 let x0 = mask_delay_min as f64;
230 let x1 = mask_delay_max as f64;
231 let y0 = mask_rate_min as f64;
232 let y1 = mask_rate_max as f64;
233 chart.draw_series(std::iter::once(Rectangle::new(
234 [(x0, y0), (x1, y1)],
235 WHITE.mix(0.18).filled(),
236 )))?;
237 let span_x = (x1 - x0).abs();
238 let span_y = (y1 - y0).abs();
239 let hatch_step = (span_x.min(span_y) / 12.0).max(0.2);
240 let mut offset = -span_y;
241 while offset <= span_x {
242 let sx = x0 + offset.max(0.0);
243 let sy = y0 + (-offset).max(0.0);
244 let ex = x0 + (offset + span_y).min(span_x);
245 let ey = y0 + (span_y + offset).min(span_y);
246 chart.draw_series(std::iter::once(PathElement::new(
247 vec![(sx, sy), (ex, ey)],
248 BLACK.mix(0.35).stroke_width(1),
249 )))?;
250 offset += hatch_step;
251 }
252 chart.draw_series(std::iter::once(Rectangle::new(
253 [(x0, y0), (x1, y1)],
254 BLACK.mix(0.45).stroke_width(1),
255 )))?;
256 }
257
258 let mut colorbar = ChartBuilder::on(&colorbar_area)
259 .margin_top(15)
260 .margin_bottom(35)
261 .set_label_area_size(LabelAreaPosition::Right, 90)
262 .set_label_area_size(LabelAreaPosition::Left, 0)
263 .set_label_area_size(LabelAreaPosition::Top, 10)
264 .set_label_area_size(LabelAreaPosition::Bottom, 20)
265 .build_cartesian_2d(0.0..1.0, 0.0..max_amplitude)?;
266
267 let steps = 100;
268 for i in 0..steps {
269 let value = i as f64 / (steps - 1) as f64;
270 let color = HSLColor(((1.0 - value) * 0.7).into(), 1.0, 0.5);
271 colorbar.draw_series(std::iter::once(Rectangle::new(
272 [
273 (0.0, value * max_amplitude),
274 (1.0, (value + 1.0 / steps as f64) * max_amplitude),
275 ],
276 color.filled(),
277 )))?;
278 }
279
280 colorbar
281 .configure_mesh()
282 .disable_x_mesh()
283 .disable_y_mesh()
284 .disable_x_axis()
285 .y_labels(7)
286 .y_label_style(("sans-serif ", 24))
287 .y_label_formatter(&|v| format!("{:.1e}", v))
288 .draw()?;
289
290 root.present()?;
291 compress_png_with_mode(output_path, CompressQuality::High);
292 return Ok(());
293 }
294
295 let mut chart1 = ChartBuilder::on(&upper_left)
297 .margin_top(20)
298 .margin_left(15)
299 .x_label_area_size(65)
300 .y_label_area_size(120)
301 .build_cartesian_2d(delay_line_min..delay_line_max, 0.0..delay_max)?;
302
303 chart1
304 .configure_mesh()
305 .x_desc("Delay [Sample]")
306 .y_desc("Amplitude")
307 .x_labels(7)
308 .x_max_light_lines(0)
310 .y_max_light_lines(0)
311 .axis_style(BLACK.stroke_width(1))
312 .x_label_formatter(&|v| format!("{:.0}", v))
313 .y_label_formatter(&|v| format!("{:.1e}", v))
314 .label_style(("sans-serif ", 30))
315 .draw()?;
316
317 chart1.draw_series(LineSeries::new(
318 delay_profile
319 .iter()
320 .filter(|(x, y)| {
321 *x >= delay_line_min && *x <= delay_line_max && x.is_finite() && y.is_finite()
322 })
323 .cloned(),
324 GREEN,
325 ))?;
326 if let Some(pre_bp) = delay_profile_pre_bp {
327 chart1.draw_series(LineSeries::new(
328 pre_bp
329 .iter()
330 .filter(|(x, y)| {
331 *x >= delay_line_min && *x <= delay_line_max && x.is_finite() && y.is_finite()
332 })
333 .cloned(),
334 RED.stroke_width(1),
335 ))?;
336 }
337 if let Some((mask_delay_min, mask_delay_max, _, _)) = mask {
338 let x0 = mask_delay_min as f64;
339 let x1 = mask_delay_max as f64;
340 chart1.draw_series(std::iter::once(Rectangle::new(
341 [(x0, 0.0), (x1, delay_max)],
342 WHITE.mix(0.18).filled(),
343 )))?;
344 let hatch_step = ((x1 - x0).abs() / 20.0).max(0.1);
345 let mut x = x0 - delay_max;
346 while x <= x1 {
347 let sx = x.max(x0);
348 let sy = (x0 - x).max(0.0);
349 let ex = (x + delay_max).min(x1);
350 let ey = (ex - x).max(0.0).min(delay_max);
351 chart1.draw_series(std::iter::once(PathElement::new(
352 vec![(sx, sy), (ex, ey)],
353 BLACK.mix(0.35).stroke_width(1),
354 )))?;
355 x += hatch_step;
356 }
357 chart1.draw_series(std::iter::once(Rectangle::new(
358 [(x0, 0.0), (x1, delay_max)],
359 BLACK.mix(0.45).stroke_width(1),
360 )))?;
361 }
362
363 let x_spec = chart1.x_range();
365 let y_spec = chart1.y_range();
366 chart1.draw_series(std::iter::once(Rectangle::new(
367 [(x_spec.start, y_spec.start), (x_spec.end, y_spec.end)],
368 BLACK.stroke_width(1),
369 )))?;
370
371 let mut chart2 = ChartBuilder::on(&lower_left)
373 .margin_top(10)
374 .margin_left(15)
375 .x_label_area_size(65)
376 .y_label_area_size(120)
377 .build_cartesian_2d(rate_line_min..rate_line_max, 0.0..rate_max)?;
378
379 chart2
380 .configure_mesh()
381 .x_desc("Rate [Hz]")
382 .y_desc("Amplitude")
383 .x_labels(5)
384 .x_max_light_lines(0)
386 .y_max_light_lines(0)
387 .axis_style(BLACK.stroke_width(1))
388 .y_label_formatter(&|v| format!("{:.1e}", v))
390 .label_style(("sans-serif ", 30))
391 .draw()?;
392
393 chart2.draw_series(LineSeries::new(
394 rate_profile
395 .iter()
396 .filter(|(x, y)| {
397 *x >= rate_line_min && *x <= rate_line_max && x.is_finite() && y.is_finite()
398 })
399 .cloned(),
400 GREEN,
401 ))?;
402 if let Some(pre_bp) = rate_profile_pre_bp {
403 chart2.draw_series(LineSeries::new(
404 pre_bp
405 .iter()
406 .filter(|(x, y)| {
407 *x >= rate_line_min && *x <= rate_line_max && x.is_finite() && y.is_finite()
408 })
409 .cloned(),
410 RED.stroke_width(1),
411 ))?;
412 }
413 if let Some((_, _, mask_rate_min, mask_rate_max)) = mask {
414 let x0 = mask_rate_min as f64;
415 let x1 = mask_rate_max as f64;
416 chart2.draw_series(std::iter::once(Rectangle::new(
417 [(x0, 0.0), (x1, rate_max)],
418 WHITE.mix(0.18).filled(),
419 )))?;
420 let hatch_step = ((x1 - x0).abs() / 20.0).max(0.001);
421 let mut x = x0 - rate_max;
422 while x <= x1 {
423 let sx = x.max(x0);
424 let sy = (x0 - x).max(0.0);
425 let ex = (x + rate_max).min(x1);
426 let ey = (ex - x).max(0.0).min(rate_max);
427 chart2.draw_series(std::iter::once(PathElement::new(
428 vec![(sx, sy), (ex, ey)],
429 BLACK.mix(0.35).stroke_width(1),
430 )))?;
431 x += hatch_step;
432 }
433 chart2.draw_series(std::iter::once(Rectangle::new(
434 [(x0, 0.0), (x1, rate_max)],
435 BLACK.mix(0.45).stroke_width(1),
436 )))?;
437 }
438
439 let x_spec = chart2.x_range();
441 let y_spec = chart2.y_range();
442 chart2.draw_series(std::iter::once(Rectangle::new(
443 [(x_spec.start, y_spec.start), (x_spec.end, y_spec.end)],
444 BLACK.stroke_width(1),
445 )))?;
446
447 let mut chart3 = ChartBuilder::on(&heatmap_area)
449 .margin(20)
450 .x_label_area_size(55)
451 .y_label_area_size(120)
452 .build_cartesian_2d(
453 heatmap_delay_min..heatmap_delay_max,
454 heatmap_rate_min..heatmap_rate_max,
455 )?;
456
457 chart3
458 .configure_mesh()
459 .x_desc("Delay [Sample]")
460 .y_desc("Rate [Hz]")
461 .x_labels(7)
462 .y_labels(10)
463 .x_max_light_lines(0)
464 .y_max_light_lines(0)
465 .label_style(("sans-serif ", 30))
466 .x_label_formatter(&|v| format!("{:.0}", v))
467 .y_label_formatter(&|v| format!("{:.2e}", v))
468 .draw()?;
469
470 let (delay_min, delay_max_hm) = (heatmap_delay_min, heatmap_delay_max);
471 let (rate_min_hm, rate_max_hm) = (heatmap_rate_min, heatmap_rate_max);
472
473 let amplitude_norm = if max_amplitude.is_finite() && max_amplitude > 0.0 {
474 max_amplitude
475 } else {
476 1e-30
477 };
478
479 let (plot_x, plot_y) = chart3.plotting_area().get_pixel_range();
480 let bitmap_width = plot_x.len().max(1) as u32;
481 let bitmap_height = plot_y.len().max(1) as u32;
482 let heatmap_buffer = build_heatmap_rgb_buffer(
483 bitmap_width,
484 bitmap_height,
485 delay_min,
486 delay_max_hm,
487 rate_min_hm,
488 rate_max_hm,
489 amplitude_norm,
490 &heatmap_func,
491 );
492 let bitmap = BitMapElement::<_, RGBPixel>::with_owned_buffer(
493 (delay_min, rate_max_hm),
494 (bitmap_width, bitmap_height),
495 heatmap_buffer,
496 )
497 .ok_or("failed to create heatmap bitmap")?;
498 chart3.draw_series(std::iter::once(bitmap))?;
499 if let Some((mask_delay_min, mask_delay_max, mask_rate_min, mask_rate_max)) = mask {
500 let x0 = mask_delay_min as f64;
501 let x1 = mask_delay_max as f64;
502 let y0 = mask_rate_min as f64;
503 let y1 = mask_rate_max as f64;
504 chart3.draw_series(std::iter::once(Rectangle::new(
505 [(x0, y0), (x1, y1)],
506 WHITE.mix(0.18).filled(),
507 )))?;
508 let span_x = (x1 - x0).abs();
509 let span_y = (y1 - y0).abs();
510 let hatch_step = (span_x.min(span_y) / 12.0).max(0.2);
511 let mut offset = -span_y;
512 while offset <= span_x {
513 let sx = x0 + offset.max(0.0);
514 let sy = y0 + (-offset).max(0.0);
515 let ex = x0 + (offset + span_y).min(span_x);
516 let ey = y0 + (span_y + offset).min(span_y);
517 chart3.draw_series(std::iter::once(PathElement::new(
518 vec![(sx, sy), (ex, ey)],
519 BLACK.mix(0.35).stroke_width(1),
520 )))?;
521 offset += hatch_step;
522 }
523 chart3.draw_series(std::iter::once(Rectangle::new(
524 [(x0, y0), (x1, y1)],
525 BLACK.mix(0.45).stroke_width(1),
526 )))?;
527 }
528
529 let mut colorbar = ChartBuilder::on(&colorbar_area)
531 .margin_top(10)
532 .margin_bottom(50)
533 .set_label_area_size(LabelAreaPosition::Right, 100)
534 .set_label_area_size(LabelAreaPosition::Left, 0)
535 .set_label_area_size(LabelAreaPosition::Top, 10)
536 .set_label_area_size(LabelAreaPosition::Bottom, 25)
537 .build_cartesian_2d(0.0..1.0, 0.0..max_amplitude)?;
538
539 let steps = 100;
540 for i in 0..steps {
541 let value = i as f64 / (steps - 1) as f64;
542 let color = HSLColor(((1.0 - value) * 0.7).into(), 1.0, 0.5);
543 colorbar.draw_series(std::iter::once(Rectangle::new(
544 [
545 (0.0, value * max_amplitude),
546 (1.0, (value + 1.0 / steps as f64) * max_amplitude),
547 ],
548 color.filled(),
549 )))?;
550 }
551
552 colorbar
553 .configure_mesh()
554 .disable_x_mesh()
555 .disable_y_mesh()
556 .disable_x_axis()
557 .y_labels(7)
558 .y_label_style(("sans-serif ", 30))
559 .y_label_formatter(&|v| format!("{:.1e}", v))
560 .draw()?;
561
562 let font = ("sans-serif ", 35).into_font();
564 let left_x = 30;
565 let right_x = 670;
566 let mut y = 20;
567 let text_style = TextStyle::from(font.clone()).color(&BLACK);
568 for (k, v) in stat_keys.iter().zip(stat_vals.iter()) {
569 lower_right.draw(&Text::new(k.to_string(), (left_x, y), text_style.clone()))?;
570 lower_right.draw(&Text::new(
571 v.to_string(),
572 (right_x, y + 15),
573 text_style.clone().pos(Pos::new(HPos::Right, VPos::Center)),
574 ))?;
575 y += 35;
576 }
577 if delay_profile_pre_bp.is_some() || rate_profile_pre_bp.is_some() {
578 let legend_font = ("sans-serif ", 28).into_font();
579 lower_right.draw(&Text::new(
580 "Green: BP corrected / Red: pre-BP",
581 (left_x, y + 20),
582 TextStyle::from(legend_font).color(&BLACK),
583 ))?;
584 }
585
586 root.present()?;
587 compress_png_with_mode(output_path, CompressQuality::High);
588 Ok(())
589}
590
591pub fn frequency_plane(
592 freq_amp_profile: &[(f64, f64)],
593 freq_amp_profile_pre_bp: Option<&[(f64, f64)]>,
594 freq_phase_profile: &[(f64, f64)],
595 freq_phase_profile_pre_bp: Option<&[(f64, f64)]>,
596 rate_profile: &[(f64, f64)],
597 rate_profile_pre_bp: Option<&[(f64, f64)]>,
598 heatmap_func: impl Fn(f64, f64) -> f64 + Sync,
599 stat_keys: &[&str],
600 stat_vals: &[&str],
601 output_path: &str,
602 bw: f64,
603 max_amplitude: f64,
604 frange: &[f32],
605 heatmap_res_x: usize,
606 heatmap_res_y: usize,
607) -> Result<(), Box<dyn std::error::Error>> {
608 let width = 1400;
609 let height = 1000;
610 let root = BitMapBackend::new(output_path, (width, height)).into_drawing_area();
611 root.fill(&WHITE)?;
612
613 let (left_area, right_area) = root.split_horizontally(width / 2);
614 let (top_left_area, rate_area) = left_area.split_vertically(height / 2 + 50);
615 let (phase_area, amp_area) =
616 top_left_area.split_vertically(top_left_area.get_pixel_range().1.len() as u32 / 4 - 10);
617 let (heatmap_and_colorbar_area, stats_area) = right_area.split_vertically(height / 2 + 50);
618 let (heatmap_area, colorbar_area) = heatmap_and_colorbar_area
619 .split_horizontally(heatmap_and_colorbar_area.get_pixel_range().0.len() as u32 - 120);
620
621 let amp_max_y = freq_amp_profile
623 .iter()
624 .map(|(_, y)| *y)
625 .chain(
626 freq_amp_profile_pre_bp
627 .into_iter()
628 .flat_map(|profile| profile.iter().map(|(_, y)| *y)),
629 )
630 .fold(f64::NEG_INFINITY, f64::max)
631 * 1.1;
632 let rate_max_y = rate_profile
633 .iter()
634 .map(|(_, y)| *y)
635 .chain(
636 rate_profile_pre_bp
637 .into_iter()
638 .flat_map(|profile| profile.iter().map(|(_, y)| *y)),
639 )
640 .fold(f64::NEG_INFINITY, f64::max)
641 * 1.1;
642 let rate_min_x = rate_profile
643 .iter()
644 .map(|(x, _)| *x)
645 .fold(f64::INFINITY, f64::min);
646 let rate_max_x = rate_profile
647 .iter()
648 .map(|(x, _)| *x)
649 .fold(f64::NEG_INFINITY, f64::max);
650
651 let (freq_min, freq_max) = if frange.len() == 2 {
652 (frange[0] as f64, frange[1] as f64)
653 } else {
654 (0.0, bw)
655 };
656
657 let mut phase_chart = ChartBuilder::on(&phase_area)
659 .margin_top(20)
660 .margin_left(15)
661 .x_label_area_size(0)
662 .y_label_area_size(120)
663 .build_cartesian_2d(freq_min..freq_max, -180.0..180.0)?;
664 phase_chart
665 .configure_mesh()
666 .x_labels(7)
669 .x_max_light_lines(0)
670 .y_max_light_lines(0)
671 .axis_style(BLACK.stroke_width(1))
672 .y_desc("Phase")
673 .y_label_formatter(&|y| format!("{:.0}", y))
674 .y_labels(6)
675 .label_style(("sans-serif ", 30))
676 .draw()?;
677 phase_chart.draw_series(LineSeries::new(
678 freq_phase_profile
679 .iter()
680 .filter(|(x, y)| *x >= freq_min && *x <= freq_max && x.is_finite() && y.is_finite())
681 .cloned(),
682 GREEN.stroke_width(1),
683 ))?;
684 if let Some(pre_bp) = freq_phase_profile_pre_bp {
685 phase_chart.draw_series(LineSeries::new(
686 pre_bp
687 .iter()
688 .filter(|(x, y)| *x >= freq_min && *x <= freq_max && x.is_finite() && y.is_finite())
689 .cloned(),
690 RED.stroke_width(1),
691 ))?;
692 }
693
694 let x_spec = phase_chart.x_range();
696 let y_spec = phase_chart.y_range();
697 phase_chart.draw_series(std::iter::once(Rectangle::new(
698 [(x_spec.start, y_spec.start), (x_spec.end, y_spec.end)],
699 BLACK.stroke_width(1),
700 )))?;
701
702 let mut amp_chart = ChartBuilder::on(&_area)
704 .margin_left(15)
705 .x_label_area_size(65)
706 .y_label_area_size(120)
707 .build_cartesian_2d(freq_min..freq_max, 0.0..amp_max_y)?;
708 amp_chart
709 .configure_mesh()
710 .x_desc("Frequency [MHz]")
711 .y_desc("Amplitude")
712 .x_max_light_lines(0)
713 .y_max_light_lines(0)
714 .x_labels(7)
715 .y_labels(7)
716 .axis_style(BLACK.stroke_width(1))
717 .label_style(("sans-serif ", 30))
718 .x_label_formatter(&|y| format!("{:.0}", y))
719 .y_label_formatter(&|y| format!("{:.1e}", y))
720 .draw()?;
721 amp_chart.draw_series(LineSeries::new(
722 freq_amp_profile
723 .iter()
724 .filter(|(x, y)| *x >= freq_min && *x <= freq_max && x.is_finite() && y.is_finite())
725 .cloned(),
726 GREEN.stroke_width(1),
727 ))?;
728 if let Some(pre_bp) = freq_amp_profile_pre_bp {
729 amp_chart.draw_series(LineSeries::new(
730 pre_bp
731 .iter()
732 .filter(|(x, y)| *x >= freq_min && *x <= freq_max && x.is_finite() && y.is_finite())
733 .cloned(),
734 RED.stroke_width(1),
735 ))?;
736 }
737
738 let x_spec = amp_chart.x_range();
740 let y_spec = amp_chart.y_range();
741 amp_chart.draw_series(std::iter::once(Rectangle::new(
742 [(x_spec.start, y_spec.start), (x_spec.end, y_spec.end)],
743 BLACK.stroke_width(1),
744 )))?;
745
746 let mut rate_chart = ChartBuilder::on(&rate_area)
748 .margin_top(10)
749 .margin_left(15)
750 .x_label_area_size(75)
751 .y_label_area_size(120)
752 .build_cartesian_2d(rate_min_x..rate_max_x, 0.0..rate_max_y)?;
753 rate_chart
754 .configure_mesh()
755 .x_desc("Rate [Hz]")
756 .y_desc("Amplitude")
757 .x_max_light_lines(0)
758 .y_max_light_lines(0)
759 .x_labels(7)
760 .axis_style(BLACK.stroke_width(1))
762 .label_style(("sans-serif ", 30))
763 .y_label_formatter(&|y| format!("{:.1e}", y))
764 .draw()?;
765 rate_chart.draw_series(LineSeries::new(
766 rate_profile
767 .iter()
768 .filter(|(x, y)| *x >= rate_min_x && *x <= rate_max_x && x.is_finite() && y.is_finite())
769 .cloned(),
770 GREEN.stroke_width(1),
771 ))?;
772 if let Some(pre_bp) = rate_profile_pre_bp {
773 rate_chart.draw_series(LineSeries::new(
774 pre_bp
775 .iter()
776 .filter(|(x, y)| {
777 *x >= rate_min_x && *x <= rate_max_x && x.is_finite() && y.is_finite()
778 })
779 .cloned(),
780 RED.stroke_width(1),
781 ))?;
782 }
783
784 let x_spec = rate_chart.x_range();
786 let y_spec = rate_chart.y_range();
787 rate_chart.draw_series(std::iter::once(Rectangle::new(
788 [(x_spec.start, y_spec.start), (x_spec.end, y_spec.end)],
789 BLACK.stroke_width(1),
790 )))?;
791
792 let mut heatmap_chart = ChartBuilder::on(&heatmap_area)
794 .margin(20)
795 .x_label_area_size(55)
796 .y_label_area_size(85)
797 .build_cartesian_2d(0.0..bw, rate_min_x..rate_max_x)?;
798 heatmap_chart
799 .configure_mesh()
800 .x_desc("Frequency [MHz]")
801 .y_desc("Rate [Hz]")
802 .x_labels(7)
803 .y_labels(7)
804 .x_max_light_lines(0)
805 .y_max_light_lines(0)
806 .x_label_formatter(&|y| format!("{:.0}", y))
807 .y_label_formatter(&|y| format!("{:.1}", y))
808 .label_style(("sans-serif ", 30))
809 .draw()?;
810
811 let resolution_x = heatmap_res_x.max(2);
812 let resolution_y = heatmap_res_y.max(2);
813 let mut heatmap_values = Vec::new();
814 let mut heatmap_data_max_val = f64::NEG_INFINITY;
815 for i in 0..resolution_y {
816 for j in 0..resolution_x {
817 let y = rate_min_x + (rate_max_x - rate_min_x) * i as f64 / (resolution_y - 1) as f64;
818 let x = bw * j as f64 / (resolution_x - 1) as f64;
819 let val = heatmap_func(x, y);
820 heatmap_values.push(val);
821 if val > heatmap_data_max_val {
822 heatmap_data_max_val = val;
823 }
824 }
825 }
826
827 for (idx, val) in heatmap_values.iter().enumerate() {
828 let i = idx / resolution_x;
829 let j = idx % resolution_x;
830 let y = rate_min_x + (rate_max_x - rate_min_x) * i as f64 / (resolution_y - 1) as f64;
831 let x = bw * j as f64 / (resolution_x - 1) as f64;
832 let x_step = bw / (resolution_x - 1) as f64;
833 let y_step = (rate_max_x - rate_min_x) / (resolution_y - 1) as f64;
834 let normalized_val = if heatmap_data_max_val > 0.0 {
835 *val / heatmap_data_max_val
836 } else {
837 0.0
838 };
839 heatmap_chart.draw_series(std::iter::once(Rectangle::new(
840 [(x, y), (x + x_step, y + y_step)],
841 HSLColor((1.0 - normalized_val) * 0.7, 1.0, 0.5).filled(),
842 )))?;
843 }
844
845 let mut colorbar = ChartBuilder::on(&colorbar_area)
847 .margin_top(10)
848 .margin_bottom(50)
849 .set_label_area_size(LabelAreaPosition::Right, 100)
850 .set_label_area_size(LabelAreaPosition::Left, 0)
851 .set_label_area_size(LabelAreaPosition::Top, 10)
852 .set_label_area_size(LabelAreaPosition::Bottom, 25)
853 .build_cartesian_2d(0.0..1.0, 0.0..max_amplitude)?;
854
855 let steps = 100;
856 for i in 0..steps {
857 let value = i as f64 / (steps - 1) as f64;
858 let color = HSLColor(((1.0 - value) * 0.7).into(), 1.0, 0.5);
859 colorbar.draw_series(std::iter::once(Rectangle::new(
860 [
861 (0.0, value * max_amplitude),
862 (1.0, (value + 1.0 / steps as f64) * max_amplitude),
863 ],
864 color.filled(),
865 )))?;
866 }
867
868 colorbar
869 .configure_mesh()
870 .disable_x_mesh()
871 .disable_y_mesh()
872 .disable_x_axis()
873 .y_labels(7)
874 .y_label_style(("sans-serif ", 30))
875 .y_label_formatter(&|v| format!("{:.1e}", v))
876 .draw()?;
877
878 let font = ("sans-serif ", 35).into_font();
880 let left_x = 30;
881 let right_x = stats_area.get_pixel_range().0.len() as i32 - 30;
882 let mut y = 20;
883 let text_style = TextStyle::from(font.clone()).color(&BLACK);
884 for (k, v) in stat_keys.iter().zip(stat_vals.iter()) {
885 stats_area.draw(&Text::new(k.to_string(), (left_x, y), text_style.clone()))?;
886 stats_area.draw(&Text::new(
887 v.to_string(),
888 (right_x, y),
889 text_style.clone().pos(Pos::new(HPos::Right, VPos::Top)),
890 ))?;
891 y += 35;
892 }
893 if freq_amp_profile_pre_bp.is_some()
894 || freq_phase_profile_pre_bp.is_some()
895 || rate_profile_pre_bp.is_some()
896 {
897 let legend_font = ("sans-serif ", 28).into_font();
898 stats_area.draw(&Text::new(
899 "Green: BP corrected / Red: pre-BP",
900 (left_x, y + 20),
901 TextStyle::from(legend_font).color(&BLACK),
902 ))?;
903 }
904
905 root.present()?;
906 compress_png_with_mode(output_path, CompressQuality::High);
907 Ok(())
908}
909
910use crate::utils;
911
912fn make_base_filename(args: &Args, result: &ProcessResult) -> String {
913 let mut base = generate_output_names(
914 &result.header,
915 &result.obs_time,
916 &result
917 .label
918 .iter()
919 .map(|s| s.as_str())
920 .collect::<Vec<&str>>(),
921 !args.rfi.is_empty(),
922 args.frequency,
923 args.bandpass.is_some(),
924 result.length_arg,
925 );
926 if args.in_beam && !base.ends_with("_inbeam") {
927 base.push_str("_inbeam");
928 }
929 base
930}
931
932pub fn write_cumulate_outputs(
933 args: &Args,
934 result: &ProcessResult,
935 frinz_dir: &Path,
936) -> Result<(), Box<dyn Error>> {
937 if args.cumulate == 0 {
938 return Ok(());
939 }
940
941 let path = if args.in_beam {
942 frinz_dir.to_path_buf()
943 } else {
944 frinz_dir.join(format!("cumulate/len{}s", args.cumulate))
945 };
946 std::fs::create_dir_all(&path)?;
947 cumulate_plot(
948 &result.cumulate_len,
949 &result.cumulate_snr,
950 &path,
951 &result.header,
952 &result
953 .label
954 .iter()
955 .map(|s| s.as_str())
956 .collect::<Vec<&str>>(),
957 &result.obs_time,
958 args.cumulate,
959 args.in_beam,
960 )?;
961 if args.npz
962 && !result.cumulate_len.is_empty()
963 && result.cumulate_len.len() == result.cumulate_snr.len()
964 {
965 let axis: Vec<f64> = result
966 .cumulate_len
967 .iter()
968 .map(|&value| value as f64)
969 .collect();
970 let npy_path = path.join(format!("{}_cumulate.npz", make_base_filename(args, result)));
971 let mut npz = NamedNpz::new(NpyMeta::new(
972 "cumulate",
973 result.header.fft_point as u32,
974 result.header.number_of_sector as u32,
975 ));
976 npz.add_f64_1d("integration_time_s", &axis);
977 npz.add_f32_1d("snr", &result.cumulate_snr);
978 npz.write(&npy_path)?;
979 }
980 Ok(())
981}
982
983pub fn write_add_plot_outputs(
984 args: &Args,
985 result: &ProcessResult,
986 frinz_dir: &Path,
987) -> Result<String, Box<dyn Error>> {
988 let base_filename = make_base_filename(args, result);
989 if !args.add_plot {
990 return Ok(base_filename);
991 }
992
993 let path: PathBuf = if args.in_beam {
994 frinz_dir.to_path_buf()
995 } else {
996 frinz_dir.join("add_plot")
997 };
998 std::fs::create_dir_all(&path)?;
999 let add_plot_filepath = path.join(&base_filename);
1000 let _ = std::fs::remove_file(path.join(format!("{}_add_plot_data.tsv", base_filename)));
1001
1002 if !result.add_plot_times.is_empty() {
1003 let first_time = result.add_plot_times[0];
1004 let elapsed_times_f32: Vec<f32> = result
1005 .add_plot_times
1006 .iter()
1007 .map(|dt| (*dt - first_time).num_seconds() as f32)
1008 .collect();
1009
1010 add_plot(
1011 add_plot_filepath.to_str().unwrap(),
1012 &elapsed_times_f32,
1013 &result.add_plot_amp,
1014 &result.add_plot_snr,
1015 &result.add_plot_phase,
1016 &result.add_plot_noise,
1017 &result.add_plot_res_delay,
1018 &result.add_plot_res_rate,
1019 &result.header.source_name,
1020 result.length_sec,
1021 &result.obs_time,
1022 )?;
1023
1024 let axis: Vec<f64> = elapsed_times_f32
1025 .iter()
1026 .map(|&value| value as f64)
1027 .collect();
1028 if args.npz {
1029 let rows = [
1030 axis.len(),
1031 result.add_plot_amp.len(),
1032 result.add_plot_snr.len(),
1033 result.add_plot_phase.len(),
1034 result.add_plot_noise.len(),
1035 result.add_plot_res_delay.len(),
1036 result.add_plot_res_rate.len(),
1037 ]
1038 .into_iter()
1039 .min()
1040 .unwrap_or(0);
1041
1042 let old_flags = [
1043 "add_plot_amp",
1044 "add_plot_snr",
1045 "add_plot_phase",
1046 "add_plot_noise",
1047 "add_plot_resdelay",
1048 "add_plot_resrate",
1049 ];
1050 for flag in old_flags {
1051 let _ = std::fs::remove_file(npz_sidecar_path(&add_plot_filepath, flag));
1052 }
1053
1054 let time_s: Vec<f64> = axis[..rows].to_vec();
1055 let amp_percent: Vec<f64> = result.add_plot_amp[..rows]
1056 .iter()
1057 .map(|&value| value as f64)
1058 .collect();
1059 let phase_deg: Vec<f64> = result.add_plot_phase[..rows]
1060 .iter()
1061 .map(|&value| value as f64)
1062 .collect();
1063 let snr: Vec<f64> = result.add_plot_snr[..rows]
1064 .iter()
1065 .map(|&value| value as f64)
1066 .collect();
1067 let res_delay_sample: Vec<f64> = result.add_plot_res_delay[..rows]
1068 .iter()
1069 .map(|&value| value as f64)
1070 .collect();
1071 let res_rate_hz: Vec<f64> = result.add_plot_res_rate[..rows]
1072 .iter()
1073 .map(|&value| value as f64)
1074 .collect();
1075 let noise_percent: Vec<f64> = result.add_plot_noise[..rows]
1076 .iter()
1077 .map(|&value| value as f64)
1078 .collect();
1079 write_named_real_1d_npz(
1080 &npz_sidecar_path(&add_plot_filepath, "add_plot"),
1081 NpyMeta::new(
1082 "add_plot",
1083 result.header.fft_point.max(0) as u32,
1084 result.header.number_of_sector.max(0) as u32,
1085 ),
1086 &[
1087 ("time_s", &time_s),
1088 ("amp_percent", &_percent),
1089 ("phase_deg", &phase_deg),
1090 ("snr", &snr),
1091 ("res_delay_sample", &res_delay_sample),
1092 ("res_rate_hz", &res_rate_hz),
1093 ("noise_percent", &noise_percent),
1094 ],
1095 )?;
1096 }
1097 }
1098
1099 Ok(base_filename)
1100}
1101
1102pub fn add_plot(
1103 output_path: &str,
1104 length: &[f32],
1105 amp: &[f32],
1106 snr: &[f32],
1107 phase: &[f32],
1108 noise: &[f32],
1109 res_delay: &[f32],
1110 res_rate: &[f32],
1111 source_name: &str,
1112 len_val_sec: f32,
1113 obs_start_time: &DateTime<Utc>,
1114) -> Result<(), Box<dyn std::error::Error>> {
1115 let phase_unwrapped = utils::unwrap_phase_with_rate(phase, length, res_rate);
1116 let phase_unwrapped_slice: &[f32] = phase_unwrapped.as_slice();
1117
1118 let plots = vec![
1119 (amp, "Amplitude [%]", "amp"),
1120 (snr, "SNR", "snr"),
1121 (phase, "Phase [deg]", "phase"),
1122 (
1123 phase_unwrapped_slice,
1124 "Phase (unwrapped) [deg]",
1125 "phase_unwrapped",
1126 ),
1127 (noise, "Noise Level [%]", "noise"),
1128 (res_delay, "Residual Delay [sample]", "resdelay"),
1129 (res_rate, "Residual Rate [Hz]", "resrate"),
1130 ];
1131
1132 for (data, y_label, filename_suffix) in plots {
1133 if data.is_empty() {
1134 continue;
1135 }
1136 let file_path = format!("{}_{}{}", output_path, filename_suffix, ".png");
1137 let root = BitMapBackend::new(&file_path, (900, 600)).into_drawing_area();
1138 root.fill(&WHITE)?;
1139
1140 let mut y_min = data.iter().cloned().fold(f32::INFINITY, f32::min);
1141 let mut y_max = data.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1142
1143 if filename_suffix == "phase" {
1144 y_min = -180.0;
1145 y_max = 180.0;
1146 }
1147
1148 let x_range = if length.len() > 1 {
1149 *length.first().unwrap()..*length.last().unwrap()
1150 } else {
1151 let center = length.first().unwrap_or(&0.0);
1153 (center - 1.0)..(center + 1.0)
1154 };
1155
1156 let mut chart = ChartBuilder::on(&root)
1157 .caption(
1158 format!("{}, length: {:.3} s", source_name, len_val_sec),
1159 ("sans-serif ", 25).into_font(),
1160 )
1161 .margin(10)
1162 .x_label_area_size(60)
1163 .y_label_area_size(100)
1164 .build_cartesian_2d(x_range, y_min..y_max)?;
1165
1166 chart
1167 .configure_mesh()
1168 .x_desc(&format!(
1169 "The elapsed time since {} UT",
1170 obs_start_time.format("%Y/%j %H:%M:%S")
1171 ))
1172 .y_desc(y_label)
1173 .x_label_formatter(&|v| format!("{:.0}", v))
1174 .y_label_formatter(&|v| {
1175 if filename_suffix == "phase" {
1176 format!("{:.0}", v)
1177 } else if filename_suffix == "snr" {
1178 format!("{:.0}", v)
1179 } else if filename_suffix == "resdelay" {
1180 format!("{:.3}", v)
1181 } else if filename_suffix == "resrate" {
1182 format!("{:.2e}", v)
1183 } else {
1184 format!("{:.1e}", v)
1185 }
1186 })
1187 .y_labels(if filename_suffix == "phase" { 7 } else { 5 })
1188 .x_max_light_lines(0)
1189 .y_max_light_lines(0)
1190 .label_style(("sans-serif ", 25).into_font())
1191 .draw()?;
1192
1193 chart.draw_series(PointSeries::of_element(
1194 length.iter().zip(data.iter()).map(|(x, y)| (*x, *y)),
1195 5,
1196 GREEN,
1197 &|c, s, st| {
1198 return EmptyElement::at(c) + Circle::new((0,0),s,st.filled()); },
1201 ))?;
1202
1203 root.present()?;
1204 compress_png_with_mode(&file_path, CompressQuality::Low);
1205 }
1206
1207 Ok(())
1208}
1209
1210pub fn cumulate_plot(
1211 cumulate_len: &[f32],
1212 cumulate_snr: &[f32],
1213 cumulate_path: &std::path::PathBuf,
1214 header: &crate::header::CorHeader,
1215 label: &[&str],
1216 obs_time: &chrono::DateTime<chrono::Utc>,
1217 cumulate_arg: i32,
1218 in_beam: bool,
1219) -> Result<(), Box<dyn std::error::Error>> {
1220 let mut base_filename = crate::output::generate_output_names(
1221 header,
1222 obs_time,
1223 label,
1224 false,
1225 false,
1226 false,
1227 cumulate_arg,
1228 );
1229 if in_beam && !base_filename.ends_with("_inbeam") {
1230 base_filename.push_str("_inbeam");
1231 }
1232 let cumulate_filename = format!(
1233 "{}_{}_cumulate{}.png",
1234 base_filename, header.source_name, cumulate_arg
1235 );
1236 let cumulate_filepath = cumulate_path.join(cumulate_filename);
1237
1238 let root = BitMapBackend::new(&cumulate_filepath, (900, 600)).into_drawing_area();
1239 root.fill(&WHITE)?;
1240
1241 let data_min_snr = cumulate_snr
1243 .iter()
1244 .cloned()
1245 .filter(|&x| x > 0.0)
1246 .fold(f32::INFINITY, f32::min);
1247 let data_max_snr = *cumulate_snr
1248 .iter()
1249 .max_by(|a, b| a.partial_cmp(b).unwrap())
1250 .unwrap_or(&100.0);
1251
1252 let y_axis_start = if data_min_snr.is_finite() && data_min_snr > 0.0 {
1253 10.0_f32.powf(data_min_snr.log10().floor())
1255 } else {
1256 1.0
1258 };
1259 let y_axis_end = data_max_snr * 1.05;
1260
1261 let mut chart = ChartBuilder::on(&root)
1262 .margin(10)
1263 .x_label_area_size(60)
1264 .y_label_area_size(100)
1265 .build_cartesian_2d(
1266 (*cumulate_len.first().unwrap()..*cumulate_len.last().unwrap()).log_scale(),
1267 (y_axis_start..y_axis_end).log_scale(),
1268 )?;
1269
1270 chart
1271 .configure_mesh()
1272 .x_desc("Integration Time [s]")
1273 .y_desc("S/N ")
1274 .x_label_formatter(&|v| format!("{:.0}", v))
1275 .y_label_formatter(&|v| format!("{:.0}", v))
1276 .x_labels(10)
1277 .y_labels(10)
1278 .label_style(("sans-serif ", 25).into_font())
1279 .max_light_lines(9)
1280 .light_line_style(&BLACK.mix(0.15))
1281 .draw()?;
1282
1283 chart
1284 .draw_series(
1285 LineSeries::new(
1286 cumulate_len
1287 .iter()
1288 .zip(cumulate_snr.iter())
1289 .map(|(x, y)| (*x, *y)),
1290 GREEN.filled(),
1291 )
1292 .point_size(5),
1293 )
1294 .unwrap();
1295
1296 if cumulate_len.len() >= 2 {
1298 let ln_x: Vec<f64> = cumulate_len
1299 .iter()
1300 .filter(|&&v| v > 0.0)
1301 .map(|&v| (v as f64).ln())
1302 .collect();
1303 let ln_y: Vec<f64> = cumulate_snr
1304 .iter()
1305 .zip(cumulate_len.iter())
1306 .filter_map(|(&y, &x)| {
1307 if x > 0.0 && y > 0.0 {
1308 Some((y as f64).ln())
1309 } else {
1310 None
1311 }
1312 })
1313 .collect();
1314 if ln_x.len() == ln_y.len() && ln_x.len() >= 2 {
1315 let n = ln_x.len() as f64;
1316 let mean_x = ln_x.iter().sum::<f64>() / n;
1317 let mean_y = ln_y.iter().sum::<f64>() / n;
1318 let cov_xy = ln_x
1319 .iter()
1320 .zip(ln_y.iter())
1321 .map(|(x, y)| (x - mean_x) * (y - mean_y))
1322 .sum::<f64>();
1323 let var_x = ln_x
1324 .iter()
1325 .map(|x| (x - mean_x) * (x - mean_x))
1326 .sum::<f64>();
1327 if var_x > 0.0 {
1328 let b_fit = cov_xy / var_x;
1329 let a_fit = (mean_y - b_fit * mean_x).exp();
1330 let x_min = *cumulate_len.first().unwrap() as f64;
1331 let x_max = *cumulate_len.last().unwrap() as f64;
1332 let fit_series = LineSeries::new(
1333 (0..200).map(|i| {
1334 let t = i as f64 / 199.0;
1335 let x = x_min * (x_max / x_min).powf(t);
1336 let y = a_fit * x.powf(b_fit);
1337 (x as f32, y as f32)
1338 }),
1339 &RED,
1340 );
1341 chart
1342 .draw_series(fit_series)?
1343 .label(format!("fit: y = {:.2e} x^{:.3}", a_fit, b_fit));
1344
1345 let a_ref = (cumulate_snr[0] as f64) / (cumulate_len[0] as f64).powf(0.5);
1347 let ref_series = LineSeries::new(
1348 (0..200).map(|i| {
1349 let t = i as f64 / 199.0;
1350 let x = x_min * (x_max / x_min).powf(t);
1351 let y = a_ref * x.powf(0.5);
1352 (x as f32, y as f32)
1353 }),
1354 &BLUE,
1355 );
1356 chart
1357 .draw_series(ref_series)?
1358 .label(format!("b=0.5: y = {:.2e} x^{:.1}", a_ref, 0.5));
1359
1360 chart
1361 .configure_series_labels()
1362 .background_style(&WHITE.mix(0.8))
1363 .border_style(&BLACK)
1364 .label_font(("sans-serif", 18).into_font())
1365 .draw()?;
1366 }
1367 }
1368 }
1369
1370 root.present()?;
1371 compress_png_with_mode(&cumulate_filepath, CompressQuality::Low);
1372 Ok(())
1373}
1374
1375pub fn phase_reference_plot(
1376 cal_times: &[DateTime<Utc>],
1377 original_cal_phases: &[f32],
1378 fitted_cal_phases: &[f32],
1379 target_times: &[DateTime<Utc>],
1380 original_target_phases: &[f32],
1381 residual_target_phases: &[f32],
1382 output_path: &str,
1383) -> Result<(), Box<dyn std::error::Error>> {
1384 let root = BitMapBackend::new(output_path, (900, 600)).into_drawing_area();
1385 root.fill(&WHITE)?;
1386
1387 let all_times: Vec<&DateTime<Utc>> = cal_times.iter().chain(target_times.iter()).collect();
1388 if all_times.is_empty() {
1389 println!("Warning: No data to plot in phase_reference_plot.");
1390 return Ok(());
1391 }
1392 let mut x_min = **all_times.iter().min().unwrap();
1393 let mut x_max = **all_times.iter().max().unwrap();
1394
1395 if x_max == x_min {
1396 let duration = chrono::Duration::seconds(1);
1397 x_min = x_min - duration;
1398 x_max = x_max + duration;
1399 }
1400
1401 let all_phases: Vec<f32> = original_cal_phases
1403 .iter()
1404 .chain(fitted_cal_phases.iter())
1405 .chain(original_target_phases.iter())
1406 .chain(residual_target_phases.iter())
1407 .cloned()
1408 .collect();
1409
1410 let y_min = all_phases.iter().cloned().fold(f32::INFINITY, f32::min);
1411 let y_max = all_phases.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1412 let y_range_padding = (y_max - y_min) * 0.1;
1413
1414 let mut chart = ChartBuilder::on(&root)
1415 .margin(10)
1417 .x_label_area_size(60)
1418 .y_label_area_size(100)
1419 .build_cartesian_2d(
1420 x_min.timestamp() as f32..x_max.timestamp() as f32,
1421 (y_min - y_range_padding)..(y_max + y_range_padding),
1422 )?;
1423
1424 chart
1425 .configure_mesh()
1426 .x_desc("Time [UTC]")
1427 .y_desc("Phase [deg]")
1428 .x_max_light_lines(0)
1429 .y_max_light_lines(0)
1430 .light_line_style(&TRANSPARENT)
1431 .x_label_formatter(&|ts| {
1432 chrono::Utc
1433 .timestamp_opt(*ts as i64, 0)
1434 .unwrap()
1435 .format("%H:%M:%S")
1436 .to_string()
1437 })
1438 .y_label_formatter(&|v| format!("{:.0}", v))
1439 .y_labels(7)
1440 .label_style(("sans-serif", 25).into_font())
1441 .draw()?;
1442
1443 chart
1445 .draw_series(PointSeries::of_element(
1446 cal_times
1447 .iter()
1448 .zip(original_cal_phases.iter())
1449 .map(|(x, y)| (x.timestamp() as f32, *y)),
1450 5,
1451 &RED,
1452 &|c, s, st| Circle::new(c, s, st.filled()),
1453 ))?
1454 .label("Calibrator (Original)")
1455 .legend(|(x, y)| Circle::new((x, y), 5, RED.filled()));
1456
1457 if !fitted_cal_phases.is_empty() {
1459 chart
1460 .draw_series(LineSeries::new(
1461 cal_times
1462 .iter()
1463 .zip(fitted_cal_phases.iter())
1464 .map(|(x, y)| (x.timestamp() as f32, *y)),
1465 &BLUE,
1466 ))?
1467 .label("Calibrator (Fitted)")
1468 .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], BLUE.filled()));
1469 }
1470
1471 chart
1473 .draw_series(PointSeries::of_element(
1474 target_times
1475 .iter()
1476 .zip(original_target_phases.iter())
1477 .map(|(x, y)| (x.timestamp() as f32, *y)),
1478 5,
1479 &GREEN,
1480 &|c, s, st| Circle::new(c, s, st.filled()),
1481 ))?
1482 .label("Target (Original)")
1483 .legend(|(x, y)| Circle::new((x, y), 5, GREEN.filled()));
1484
1485 chart
1487 .draw_series(PointSeries::of_element(
1488 target_times
1489 .iter()
1490 .zip(residual_target_phases.iter())
1491 .map(|(x, y)| (x.timestamp() as f32, *y)),
1492 5,
1493 &BLACK,
1494 &|c, s, st| Circle::new(c, s, st.filled()),
1495 ))?
1496 .label("Residual (Calibrator Fit)")
1497 .legend(|(x, y)| Circle::new((x, y), 5, BLACK.filled()));
1498
1499 chart
1500 .configure_series_labels()
1501 .background_style(&WHITE.mix(0.8))
1502 .border_style(&BLACK)
1503 .draw()?;
1504
1505 root.present()?;
1506 compress_png_with_mode(output_path, CompressQuality::Low);
1507 Ok(())
1508}
1509
1510pub fn plot_allan_deviation(
1511 output_path: &str,
1512 data: &[(f32, f32)],
1513 source_name: &str,
1514) -> Result<(), Box<dyn std::error::Error>> {
1515 let root = BitMapBackend::new(output_path, (900, 600)).into_drawing_area();
1516 root.fill(&WHITE)?;
1517
1518 if data.is_empty() {
1519 return Ok(());
1520 }
1521
1522 let (min_tau, max_tau) = data
1523 .iter()
1524 .fold((f32::INFINITY, f32::NEG_INFINITY), |(min, max), (t, _)| {
1525 (min.min(*t), max.max(*t))
1526 });
1527 let (min_adev, max_adev) = data
1528 .iter()
1529 .fold((f32::INFINITY, f32::NEG_INFINITY), |(min, max), (_, a)| {
1530 (min.min(*a), max.max(*a))
1531 });
1532
1533 let mut chart = ChartBuilder::on(&root)
1534 .caption(
1535 format!("Allan Deviation for {}", source_name),
1536 ("sans-serif", 25).into_font(),
1537 )
1538 .margin(10)
1539 .x_label_area_size(60)
1540 .y_label_area_size(100)
1541 .build_cartesian_2d(
1542 (min_tau..max_tau).log_scale(),
1543 (min_adev..max_adev * 1.1).log_scale(),
1544 )?;
1545
1546 chart
1547 .configure_mesh()
1548 .x_desc("Averaging Time (τ) [s]")
1549 .y_desc("Allan Deviation (σ_y(τ))")
1550 .x_labels(10)
1551 .y_labels(10)
1552 .label_style(("sans-serif", 25).into_font())
1553 .draw()?;
1554
1555 chart.draw_series(LineSeries::new(data.iter().map(|(t, a)| (*t, *a)), &RED))?;
1556 chart.draw_series(PointSeries::of_element(
1557 data.iter().map(|(t, a)| (*t, *a)),
1558 5,
1559 &RED,
1560 &|c, s, st| Circle::new(c, s, st.filled()),
1561 ))?;
1562
1563 root.present()?;
1564 compress_png_with_mode(output_path, CompressQuality::Low);
1565 Ok(())
1566}
1567
1568pub fn plot_acel_search_result<P: AsRef<Path>>(
1569 output_path: P,
1570 times: &[f64],
1571 observed: &[f64],
1572 fitted: Option<&[f64]>,
1573 residuals: Option<&[f64]>,
1574 title: &str,
1575 y_label: &str,
1576) -> Result<(), Box<dyn std::error::Error>> {
1577 if times.is_empty() || observed.is_empty() {
1578 println!("Warning: No data to plot for {}.", title);
1579 return Ok(());
1580 }
1581
1582 let mut min_time = f64::INFINITY;
1583 let mut max_time = f64::NEG_INFINITY;
1584 let mut min_val = f64::INFINITY;
1585 let mut max_val = f64::NEG_INFINITY;
1586
1587 for (&t, &y) in times.iter().zip(observed.iter()) {
1588 min_time = min_time.min(t);
1589 max_time = max_time.max(t);
1590 min_val = min_val.min(y);
1591 max_val = max_val.max(y);
1592 }
1593
1594 if let Some(fit) = fitted {
1595 for &y in fit {
1596 min_val = min_val.min(y);
1597 max_val = max_val.max(y);
1598 }
1599 }
1600
1601 if (max_time - min_time).abs() < f64::EPSILON {
1602 max_time = min_time + 1.0;
1603 }
1604 if (max_val - min_val).abs() < f64::EPSILON {
1605 max_val += 1.0;
1606 min_val -= 1.0;
1607 } else {
1608 let pad = (max_val - min_val).abs() * 0.08;
1609 max_val += pad;
1610 min_val -= pad;
1611 }
1612
1613 let rate_like = y_label.contains("Rate");
1614 let left_formatter = move |v: &f64| {
1615 if rate_like {
1616 format!("{:.2e}", v)
1617 } else {
1618 format!("{:.2}", v)
1619 }
1620 };
1621 let right_formatter = move |v: &f64| {
1622 if rate_like {
1623 format!("{:.2e}", v)
1624 } else {
1625 format!("{:.2}", v)
1626 }
1627 };
1628
1629 let root = BitMapBackend::new(output_path.as_ref(), (900, 600)).into_drawing_area();
1630 root.fill(&WHITE)?;
1631
1632 if let Some(res) = residuals.filter(|r| !r.is_empty()) {
1633 let mut min_res = f64::INFINITY;
1634 let mut max_res = f64::NEG_INFINITY;
1635 for &y in res {
1636 min_res = min_res.min(y);
1637 max_res = max_res.max(y);
1638 }
1639 if (max_res - min_res).abs() < f64::EPSILON {
1640 max_res += 1.0;
1641 min_res -= 1.0;
1642 } else {
1643 let pad = (max_res - min_res).abs() * 0.12;
1644 max_res += pad;
1645 min_res -= pad;
1646 }
1647
1648 let mut chart = ChartBuilder::on(&root)
1649 .caption(title, ("sans-serif", 25).into_font())
1650 .margin(10)
1651 .x_label_area_size(60)
1652 .y_label_area_size(100)
1653 .right_y_label_area_size(100)
1654 .build_cartesian_2d(min_time..max_time, min_val..max_val)?
1655 .set_secondary_coord(min_time..max_time, min_res..max_res);
1656
1657 chart
1658 .configure_mesh()
1659 .x_desc("Time [s]")
1660 .y_desc(y_label)
1661 .x_label_formatter(&|v| format!("{:.0}", v))
1662 .y_label_formatter(&left_formatter)
1663 .x_labels(10)
1664 .y_labels(10)
1665 .x_max_light_lines(0)
1666 .y_max_light_lines(0)
1667 .label_style(("sans-serif", 20).into_font())
1668 .draw()?;
1669
1670 chart
1671 .configure_secondary_axes()
1672 .y_desc(format!("Residual ({})", y_label))
1673 .y_label_formatter(&right_formatter)
1674 .label_style(("sans-serif", 20).into_font())
1675 .draw()?;
1676
1677 chart
1678 .draw_series(PointSeries::of_element(
1679 times.iter().zip(observed.iter()).map(|(&t, &y)| (t, y)),
1680 3,
1681 &BLUE,
1682 &|c, s, st| Circle::new(c, s, st.filled()),
1683 ))?
1684 .label("Observed")
1685 .legend(|(x, y)| Circle::new((x, y), 4, BLUE.filled()));
1686
1687 if let Some(fit) = fitted {
1688 chart
1689 .draw_series(LineSeries::new(
1690 times.iter().zip(fit.iter()).map(|(&t, &y)| (t, y)),
1691 &RED,
1692 ))?
1693 .label("Fitted")
1694 .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED));
1695 }
1696
1697 chart
1698 .draw_secondary_series(LineSeries::new(
1699 times.iter().zip(res.iter()).map(|(&t, &y)| (t, y)),
1700 &GREEN,
1701 ))?
1702 .label("Residual (right)")
1703 .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], GREEN));
1704
1705 chart
1706 .configure_series_labels()
1707 .background_style(&WHITE.mix(0.8))
1708 .border_style(&BLACK)
1709 .label_font(("sans-serif", 18).into_font())
1710 .draw()?;
1711 } else {
1712 let mut chart = ChartBuilder::on(&root)
1713 .caption(title, ("sans-serif", 25).into_font())
1714 .margin(10)
1715 .x_label_area_size(60)
1716 .y_label_area_size(100)
1717 .build_cartesian_2d(min_time..max_time, min_val..max_val)?;
1718
1719 chart
1720 .configure_mesh()
1721 .x_desc("Time [s]")
1722 .y_desc(y_label)
1723 .x_label_formatter(&|v| format!("{:.0}", v))
1724 .y_label_formatter(&left_formatter)
1725 .x_labels(10)
1726 .y_labels(10)
1727 .x_max_light_lines(0)
1728 .y_max_light_lines(0)
1729 .label_style(("sans-serif", 20).into_font())
1730 .draw()?;
1731
1732 chart
1733 .draw_series(PointSeries::of_element(
1734 times.iter().zip(observed.iter()).map(|(&t, &y)| (t, y)),
1735 3,
1736 &BLUE,
1737 &|c, s, st| Circle::new(c, s, st.filled()),
1738 ))?
1739 .label("Observed")
1740 .legend(|(x, y)| Circle::new((x, y), 4, BLUE.filled()));
1741
1742 if let Some(fit) = fitted {
1743 chart
1744 .draw_series(LineSeries::new(
1745 times.iter().zip(fit.iter()).map(|(&t, &y)| (t, y)),
1746 &RED,
1747 ))?
1748 .label("Fitted")
1749 .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED));
1750 }
1751
1752 chart
1753 .configure_series_labels()
1754 .background_style(&WHITE.mix(0.8))
1755 .border_style(&BLACK)
1756 .label_font(("sans-serif", 18).into_font())
1757 .draw()?;
1758 }
1759
1760 root.present()?;
1761 compress_png_with_mode(output_path.as_ref(), CompressQuality::Low);
1762 Ok(())
1763}
1764
1765pub fn plot_sky_map<P: AsRef<Path>>(
1766 output_path: P,
1767 map_data: &ndarray::Array2<f32>,
1768 cell_size_rad: f64,
1769 max_x_idx: usize,
1770 max_y_idx: usize,
1771) -> Result<(), Box<dyn std::error::Error>> {
1772 let (height, width) = map_data.dim();
1773 let backend_width = 1024;
1775 let backend_height = 800;
1776 let root = BitMapBackend::new(output_path.as_ref(), (backend_width, backend_height))
1777 .into_drawing_area();
1778 root.fill(&WHITE)?;
1779
1780 let (main_area, colorbar_area) = root.split_horizontally(backend_width - 120);
1781
1782 let max_val = map_data.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
1783 let min_val = map_data.iter().fold(f32::INFINITY, |a, &b| a.min(b));
1784
1785 let rad_to_mas = 180.0 / PI * 3600.0 * 1_000.0;
1786 let l_mas_max = (width as f64 / 2.0) * cell_size_rad * rad_to_mas;
1787 let m_mas_max = (height as f64 / 2.0) * cell_size_rad * rad_to_mas;
1788 let l_range = l_mas_max..-l_mas_max; let m_range = -m_mas_max..m_mas_max;
1790
1791 let mut chart = ChartBuilder::on(&main_area)
1792 .x_label_area_size(65)
1793 .y_label_area_size(80)
1794 .margin(10)
1795 .caption("Fringe Rate Map", ("sans-serif", 25))
1796 .build_cartesian_2d(l_range.clone(), m_range.clone())?;
1797
1798 chart
1799 .configure_mesh()
1800 .x_desc("ΔRA (mas)")
1801 .y_desc("ΔDec (mas)")
1802 .x_label_formatter(&|x| format!("{:.0}", x))
1803 .y_label_formatter(&|y| format!("{:.0}", y))
1804 .label_style(("sans-serif", 30))
1805 .draw()?;
1806
1807 chart.draw_series(map_data.indexed_iter().map(|((y, x), &val)| {
1809 let l_mas = ((x as f64) - (width as f64 / 2.0)) * cell_size_rad * rad_to_mas;
1810 let m_mas = ((height as f64 / 2.0) - y as f64) * cell_size_rad * rad_to_mas;
1811 let cell_l_mas = cell_size_rad * rad_to_mas;
1812 let cell_m_mas = cell_size_rad * rad_to_mas;
1813
1814 let mut norm_val = if max_val > min_val {
1815 (val - min_val) / (max_val - min_val)
1816 } else {
1817 0.0
1818 };
1819 if norm_val.is_nan() {
1820 norm_val = 0.0;
1821 }
1822 norm_val = norm_val.clamp(0.0, 1.0);
1823 let color = ViridisRGB.get_color(norm_val as f64);
1824
1825 Rectangle::new(
1826 [(l_mas, m_mas), (l_mas + cell_l_mas, m_mas + cell_m_mas)],
1827 color.filled(),
1828 )
1829 }))?;
1830
1831 chart.draw_series(PointSeries::of_element(
1833 vec![(0.0, 0.0)], 10, &WHITE, &|c, s, st| Cross::new(c, s, st.stroke_width(2)), ))?;
1838
1839 let (height, width) = map_data.dim(); let l_mas_max_val = ((max_x_idx as f64) - (width as f64 / 2.0)) * cell_size_rad * rad_to_mas;
1842 let m_mas_max_val = ((height as f64 / 2.0) - max_y_idx as f64) * cell_size_rad * rad_to_mas;
1843
1844 chart.draw_series(PointSeries::of_element(
1845 vec![(l_mas_max_val, m_mas_max_val)], 10, &RED, &|c, s, st| Cross::new(c, s, st.stroke_width(2)), ))?;
1850
1851 let mut colorbar_chart = ChartBuilder::on(&colorbar_area)
1853 .margin(20)
1854 .set_label_area_size(LabelAreaPosition::Right, 40)
1855 .build_cartesian_2d(0f32..1f32, min_val..max_val)?;
1856
1857 colorbar_chart
1858 .configure_mesh()
1859 .disable_x_mesh()
1860 .disable_x_axis()
1861 .y_label_formatter(&|y| format!("{:.1e}", y))
1862 .y_label_style(("sans-serif", 20))
1863 .draw()?;
1864
1865 let color_map = ViridisRGB;
1866 colorbar_chart.draw_series((0..200).map(|y| {
1867 let y_val = min_val + (max_val - min_val) * y as f32 / 199.0;
1868 let mut norm_val = if max_val > min_val {
1869 (y_val - min_val) / (max_val - min_val)
1870 } else {
1871 0.0
1872 };
1873 if norm_val.is_nan() {
1874 norm_val = 0.0;
1875 }
1876 norm_val = norm_val.clamp(0.0, 1.0);
1877 let color = color_map.get_color(norm_val as f64);
1878 Rectangle::new(
1879 [(0.0, y_val), (1.0, y_val + (max_val - min_val) / 199.0)],
1880 color.filled(),
1881 )
1882 }))?;
1883
1884 root.present()?;
1885 compress_png_with_mode(output_path.as_ref(), CompressQuality::Low);
1886 Ok(())
1887}
1888
1889pub fn plot_dynamic_spectrum_freq(
1890 output_path: &str,
1891 spectrum_array: &Array2<Complex<f32>>,
1892 header: &crate::header::CorHeader,
1893 obs_time: &DateTime<Utc>,
1894 _length: i32,
1895 _effective_integration_time: f32,
1896) -> Result<(), Box<dyn std::error::Error>> {
1897 let (time_samples, freq_channels) = spectrum_array.dim();
1898
1899 let width = 1200;
1900 let height = 1000;
1901 let root = BitMapBackend::new(output_path, (width, height)).into_drawing_area();
1902 root.fill(&WHITE)?;
1903
1904 let (upper, lower) = root.split_vertically(height / 2);
1905
1906 let freq_range = 0..freq_channels;
1907 let time_range = 0..time_samples;
1908
1909 let amplitudes: Vec<f32> = spectrum_array.iter().map(|c| c.norm()).collect();
1911 let max_amp = amplitudes.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1912 let min_amp = amplitudes.iter().cloned().fold(f32::INFINITY, f32::min);
1913
1914 let mut amp_chart = ChartBuilder::on(&upper)
1915 .caption(
1916 format!(
1917 "Dynamic Spectrum (Amplitude) - {} - {}",
1918 header.source_name,
1919 obs_time.format("%Y-%m-%d %H:%M:%S")
1920 ),
1921 ("sans-serif", 20),
1922 )
1923 .margin(10)
1924 .x_label_area_size(5)
1925 .y_label_area_size(100)
1926 .build_cartesian_2d(freq_range.clone(), time_range.clone())?;
1927
1928 amp_chart
1929 .configure_mesh()
1930 .y_desc("Time [PP]")
1931 .label_style(("sans-serif", 25).into_font())
1933 .draw()?;
1934
1935 amp_chart.draw_series(spectrum_array.indexed_iter().map(|((t, f), c)| {
1936 let norm_val = if max_amp > min_amp {
1937 (c.norm() - min_amp) / (max_amp - min_amp)
1938 } else {
1939 0.0
1940 };
1941 let color = ViridisRGB.get_color(norm_val as f64);
1942 Rectangle::new([(f, t), (f + 1, t + 1)], color.filled())
1943 }))?;
1944
1945 let mut phase_chart = ChartBuilder::on(&lower)
1947 .caption(
1948 format!(
1949 "Dynamic Spectrum (Phase) - {} - {}",
1950 header.source_name,
1951 obs_time.format("%Y-%m-%d %H:%M:%S")
1952 ),
1953 ("sans-serif", 20),
1954 )
1955 .margin(10)
1956 .x_label_area_size(55)
1957 .y_label_area_size(100)
1958 .build_cartesian_2d(freq_range.clone(), time_range.clone())?;
1959
1960 phase_chart
1961 .configure_mesh()
1962 .y_desc("Time [PP]")
1963 .x_desc("Frequency [channels]")
1964 .label_style(("sans-serif", 25).into_font())
1965 .draw()?;
1966
1967 phase_chart.draw_series(spectrum_array.indexed_iter().map(|((t, f), c)| {
1968 let norm_val = (safe_arg(c).to_degrees() + 180.0) / 360.0;
1969 let color = ViridisRGB.get_color(norm_val as f64);
1970 Rectangle::new([(f, t), (f + 1, t + 1)], color.filled())
1971 }))?;
1972
1973 root.present()?;
1974 compress_png(output_path);
1975 Ok(())
1976}
1977
1978pub fn plot_dynamic_spectrum_lag(
1979 output_path: &str,
1980 lag_data: &Array2<f32>,
1981 header: &crate::header::CorHeader,
1982 obs_time: &DateTime<Utc>,
1983 _length: i32,
1984 _effective_integration_time: f32,
1985) -> Result<(), Box<dyn std::error::Error>> {
1986 let (time_samples, lag_samples) = lag_data.dim();
1987
1988 let width = 1200;
1989 let height = 600;
1990 let root = BitMapBackend::new(output_path, (width, height)).into_drawing_area();
1991 root.fill(&WHITE)?;
1992
1993 let lag_range_min = -(lag_samples as i32 / 2);
1994 let lag_range_max = lag_samples as i32 / 2;
1995 let lag_range = lag_range_min..lag_range_max;
1996 let time_range = 0..time_samples;
1997
1998 let max_val = lag_data.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
1999 let min_val = lag_data.iter().fold(f32::INFINITY, |a, &b| a.min(b));
2000
2001 let mut chart = ChartBuilder::on(&root)
2002 .caption(
2003 format!(
2004 "Dynamic Spectrum (Time Lag) - {} - {}",
2005 header.source_name,
2006 obs_time.format("%Y-%m-%d %H:%M:%S")
2007 ),
2008 ("sans-serif", 20),
2009 )
2010 .margin(10)
2011 .x_label_area_size(55)
2012 .y_label_area_size(100)
2013 .build_cartesian_2d(lag_range.clone(), time_range.clone())?;
2014
2015 chart
2016 .configure_mesh()
2017 .y_desc("Time [PP]")
2018 .x_desc("Lag [samples]")
2019 .label_style(("sans-serif", 25).into_font())
2020 .draw()?;
2021
2022 chart.draw_series(lag_data.indexed_iter().map(|((t, l), &val)| {
2023 let x = lag_range_min + l as i32;
2024 let norm_val = if max_val > min_val {
2025 (val - min_val) / (max_val - min_val)
2026 } else {
2027 0.0
2028 };
2029 let color = ViridisRGB.get_color(norm_val as f64);
2030 Rectangle::new([(x, t), (x + 1, t + 1)], color.filled())
2031 }))?;
2032
2033 root.present()?;
2034 compress_png(output_path);
2035 Ok(())
2036}
2037
2038fn gaussian_blur_2d(data: &Vec<Vec<f32>>, sigma: f32) -> Vec<Vec<f32>> {
2039 if sigma <= 0.0 {
2040 return data.clone();
2041 }
2042
2043 let rows = data.len();
2044 if rows == 0 {
2045 return Vec::new();
2046 }
2047 let cols = data[0].len();
2048 if cols == 0 {
2049 return data.clone();
2050 }
2051
2052 let kernel_radius = (sigma * 3.0).ceil() as usize;
2053 let kernel_size = 2 * kernel_radius + 1;
2054 let mut kernel = vec![0.0; kernel_size];
2055 let mut kernel_sum = 0.0;
2056
2057 for i in 0..kernel_size {
2058 let x = i as f32 - kernel_radius as f32;
2059 kernel[i] = (-0.5 * (x / sigma).powi(2)).exp();
2060 kernel_sum += kernel[i];
2061 }
2062
2063 for i in 0..kernel_size {
2064 kernel[i] /= kernel_sum;
2065 }
2066
2067 let mut blurred_data = vec![vec![0.0; cols]; rows];
2068 let mut temp_data = vec![vec![0.0; cols]; rows];
2069
2070 for r in 0..rows {
2072 for c in 0..cols {
2073 let mut sum = 0.0;
2074 for k_idx in 0..kernel_size {
2075 let col_idx = (c as isize + k_idx as isize - kernel_radius as isize)
2076 .clamp(0, cols as isize - 1) as usize;
2077 sum += data[r][col_idx] * kernel[k_idx];
2078 }
2079 temp_data[r][c] = sum;
2080 }
2081 }
2082
2083 for r in 0..rows {
2085 for c in 0..cols {
2086 let mut sum = 0.0;
2087 for k_idx in 0..kernel_size {
2088 let row_idx = (r as isize + k_idx as isize - kernel_radius as isize)
2089 .clamp(0, rows as isize - 1) as usize;
2090 sum += temp_data[row_idx][c] * kernel[k_idx];
2091 }
2092 blurred_data[r][c] = sum;
2093 }
2094 }
2095
2096 blurred_data
2097}
2098
2099fn draw_heatmap_with_colorbar(
2100 area: &DrawingArea<BitMapBackend, Shift>,
2101 data: &Vec<Vec<f32>>,
2102 x_desc: &str,
2103 y_desc: &str,
2104 color_bar_title: &str,
2105 min_val: f32,
2106 max_val: f32,
2107 num_color_bar_labels: usize,
2108 color_value_normalizer: impl Fn(f32) -> f64,
2109 color_bar_label_formatter: impl Fn(f32) -> String,
2110) -> Result<(), Box<dyn std::error::Error>> {
2111 let (rows, cols) = (data.len(), data[0].len());
2112
2113 let (area_width, _area_height) = area.dim_in_pixel();
2114 let color_bar_area_width: u32 = 150;
2115 let chart_width = area_width.saturating_sub(color_bar_area_width);
2116
2117 area.fill(&WHITE)?;
2118
2119 let (chart_area, color_bar_area) = area.split_horizontally(chart_width);
2120
2121 let top_margin = 10;
2122 let bottom_margin = 10;
2123 let x_label_area_size = 35;
2124 let y_label_area_size = 45;
2125
2126 let mut chart = ChartBuilder::on(&chart_area)
2127 .margin_top(top_margin)
2129 .margin_bottom(bottom_margin)
2130 .margin_left(10)
2131 .margin_right(10)
2132 .x_label_area_size(x_label_area_size)
2133 .y_label_area_size(y_label_area_size)
2134 .build_cartesian_2d(0..cols, 0..rows)?;
2135
2136 chart
2137 .configure_mesh()
2138 .x_desc(x_desc)
2139 .y_desc(y_desc)
2140 .x_label_style(("sans-serif", 18).into_font())
2141 .y_label_style(("sans-serif", 18).into_font())
2142 .x_label_formatter(&|v| format!("{v}"))
2143 .y_label_formatter(&|v| format!("{v}"))
2144 .draw()?;
2145
2146 chart.draw_series(
2147 (0..cols)
2148 .flat_map(|x| (0..rows).map(move |y| (x, y)))
2149 .map(|(x, y)| {
2150 let val = data[y][x];
2151 let color_value = color_value_normalizer(val);
2152 let color = ViridisRGB.get_color(color_value);
2153 Rectangle::new([(x, y), (x + 1, y + 1)], color.filled())
2154 }),
2155 )?;
2156
2157 if num_color_bar_labels == 0 {
2159 return Ok(());
2160 }
2161 let color_bar_left_pad: u32 = 12;
2162 let color_bar_width: u32 = 30;
2163 let axis_bottom_pad = bottom_margin + x_label_area_size;
2164
2165 let (_, remainder_area) = color_bar_area.split_horizontally(color_bar_left_pad);
2166 let (bar_strip_raw, label_strip_raw) = if remainder_area.dim_in_pixel().0 > color_bar_width {
2167 remainder_area.split_horizontally(color_bar_width)
2168 } else {
2169 let width = remainder_area.dim_in_pixel().0;
2170 remainder_area.split_horizontally(width)
2171 };
2172 let bar_strip = bar_strip_raw.margin(top_margin as u32, axis_bottom_pad as u32, 0, 0);
2173 let label_strip = label_strip_raw.margin(top_margin as u32, axis_bottom_pad as u32, 6, 0);
2174
2175 let (bar_strip_width, bar_strip_height) = bar_strip.dim_in_pixel();
2176 if bar_strip_height == 0 {
2177 return Ok(());
2178 }
2179 let height_norm = (bar_strip_height.saturating_sub(1)).max(1) as f64;
2180
2181 for i in 0..bar_strip_height as i32 {
2182 let frac = 1.0 - (i as f64 / height_norm);
2183 let color = ViridisRGB.get_color(frac);
2184 bar_strip.draw(&Rectangle::new(
2185 [(0, i), (bar_strip_width as i32, i + 1)],
2186 color.filled(),
2187 ))?;
2188 }
2189
2190 let step = std::cmp::max(1, bar_strip_height / 80);
2191 let mut label_count = std::cmp::max(5usize, step as usize);
2192 if label_count == 1 {
2193 label_count = 2;
2194 }
2195 for i in 0..label_count {
2196 let frac = if label_count <= 1 {
2197 0.0
2198 } else {
2199 i as f64 / (label_count - 1) as f64
2200 };
2201 let value = min_val + (max_val - min_val) * (1.0 - frac as f32);
2202 let y_pos = ((1.0 - frac) * height_norm).round() as i32;
2203 label_strip.draw_text(
2204 &color_bar_label_formatter(value),
2205 &TextStyle::from(("sans-serif", 18).into_font()).color(&BLACK),
2206 (4, y_pos - 9),
2207 )?;
2208 }
2209
2210 let (label_strip_width, _) = label_strip.dim_in_pixel();
2211 let title_x = (label_strip_width as i32 - 34).max(24);
2212 label_strip.draw_text(
2213 color_bar_title,
2214 &TextStyle::from(("sans-serif", 18).into_font())
2215 .color(&BLACK)
2216 .transform(FontTransform::Rotate270),
2217 (title_x, (bar_strip_height / 2) as i32),
2218 )?;
2219 Ok(())
2220}
2221
2222pub fn plot_spectrum_heatmaps<P: AsRef<Path>>(
2223 output_path: P,
2224 spectrum_data: &Vec<Vec<Complex<f32>>>,
2225 sigma: f32,
2226) -> Result<(), Box<dyn std::error::Error>> {
2227 if spectrum_data.is_empty() || spectrum_data[0].is_empty() {
2228 return Err("Spectrum data for heatmap is empty".into());
2229 }
2230
2231 let panel_width = 610u32;
2232 let total_width = panel_width * 2;
2233 let total_height = 384u32;
2234 let root =
2235 BitMapBackend::new(output_path.as_ref(), (total_width, total_height)).into_drawing_area();
2236 root.fill(&WHITE)?;
2237 let (left_area, right_area) = root.split_horizontally(panel_width);
2238
2239 let amplitudes_2d: Vec<Vec<f32>> = spectrum_data
2241 .iter()
2242 .map(|row| row.iter().map(|c| c.norm()).collect())
2243 .collect();
2244 let blurred_amplitudes = gaussian_blur_2d(&litudes_2d, sigma);
2245 let max_amp = blurred_amplitudes
2246 .iter()
2247 .flatten()
2248 .cloned()
2249 .fold(0.0, f32::max);
2250 let min_amp = blurred_amplitudes
2251 .iter()
2252 .flatten()
2253 .cloned()
2254 .fold(f32::MAX, f32::min);
2255
2256 draw_heatmap_with_colorbar(
2257 &left_area,
2258 &blurred_amplitudes,
2259 "Channels",
2260 "PP",
2261 "Amplitude (a.u.)",
2262 min_amp,
2263 max_amp,
2264 5,
2265 |v| {
2266 if max_amp > min_amp {
2267 ((v - min_amp) / (max_amp - min_amp)) as f64
2268 } else {
2269 0.0
2270 }
2271 },
2272 |v| format!("{:.1e}", v),
2273 )?;
2274
2275 let phases_2d: Vec<Vec<f32>> = spectrum_data
2277 .iter()
2278 .map(|row| row.iter().map(|c| safe_arg(c).to_degrees()).collect())
2279 .collect();
2280 let blurred_phases = gaussian_blur_2d(&phases_2d, sigma);
2281
2282 draw_heatmap_with_colorbar(
2283 &right_area,
2284 &blurred_phases,
2285 "Channels",
2286 "PP",
2287 "Phase (deg)",
2288 -180.0,
2289 180.0,
2290 9,
2291 |v| ((v + 180.0) / 360.0) as f64,
2292 |v| format!("{:.0}", v),
2293 )?;
2294
2295 root.present()?;
2296 compress_png(output_path.as_ref());
2297 Ok(())
2298}
2299
2300pub fn plot_spectrum_amplitude_heatmap<P: AsRef<Path>>(
2301 output_path: P,
2302 spectrum_data: &Vec<Vec<Complex<f32>>>,
2303 sigma: f32,
2304) -> Result<(), Box<dyn std::error::Error>> {
2305 if spectrum_data.is_empty() || spectrum_data[0].is_empty() {
2306 return Err("Spectrum data for heatmap is empty".into());
2307 }
2308
2309 let root = BitMapBackend::new(output_path.as_ref(), (915, 576)).into_drawing_area();
2310 root.fill(&WHITE)?;
2311
2312 let amplitudes_2d: Vec<Vec<f32>> = spectrum_data
2313 .iter()
2314 .map(|row| row.iter().map(|c| c.norm()).collect())
2315 .collect();
2316 let blurred_amplitudes = gaussian_blur_2d(&litudes_2d, sigma);
2317 let max_amp = blurred_amplitudes
2318 .iter()
2319 .flatten()
2320 .cloned()
2321 .fold(0.0, f32::max);
2322 let min_amp = blurred_amplitudes
2323 .iter()
2324 .flatten()
2325 .cloned()
2326 .fold(f32::MAX, f32::min);
2327
2328 draw_heatmap_with_colorbar(
2329 &root,
2330 &blurred_amplitudes,
2331 "Channels",
2332 "PP",
2333 "Amplitude (a.u.)",
2334 min_amp,
2335 max_amp,
2336 5,
2337 |v| {
2338 if max_amp > min_amp {
2339 ((v - min_amp) / (max_amp - min_amp)) as f64
2340 } else {
2341 0.0
2342 }
2343 },
2344 |v| format!("{:.1e}", v),
2345 )?;
2346
2347 root.present()?;
2348 compress_png(output_path.as_ref());
2349 Ok(())
2350}
2351
2352pub fn plot_spectrum_phase_heatmap<P: AsRef<Path>>(
2353 output_path: P,
2354 spectrum_data: &Vec<Vec<Complex<f32>>>,
2355 sigma: f32,
2356) -> Result<(), Box<dyn std::error::Error>> {
2357 if spectrum_data.is_empty() || spectrum_data[0].is_empty() {
2358 return Err("Spectrum data for heatmap is empty".into());
2359 }
2360
2361 let root = BitMapBackend::new(output_path.as_ref(), (915, 576)).into_drawing_area();
2362 root.fill(&WHITE)?;
2363
2364 let phases_2d: Vec<Vec<f32>> = spectrum_data
2365 .iter()
2366 .map(|row| row.iter().map(|c| safe_arg(c).to_degrees()).collect())
2367 .collect();
2368 let blurred_phases = gaussian_blur_2d(&phases_2d, sigma);
2369
2370 draw_heatmap_with_colorbar(
2371 &root,
2372 &blurred_phases,
2373 "Channels",
2374 "PP",
2375 "Phase (deg)",
2376 -180.0,
2377 180.0,
2378 9,
2379 |v| ((v + 180.0) / 360.0) as f64,
2380 |v| format!("{:.0}", v),
2381 )?;
2382
2383 root.present()?;
2384 compress_png(output_path.as_ref());
2385 Ok(())
2386}
2387
2388pub fn plot_complex_scatter<P: AsRef<Path>>(
2389 output_path: P,
2390 real_values: &[f32],
2391 imag_values: &[f32],
2392) -> Result<(), Box<dyn std::error::Error>> {
2393 if real_values.is_empty() || real_values.len() != imag_values.len() {
2394 return Err("散布図を描画するための複素データが不足しています".into());
2395 }
2396
2397 let mut min_real = real_values.iter().fold(f32::INFINITY, |acc, &v| acc.min(v));
2398 let mut max_real = real_values
2399 .iter()
2400 .fold(f32::NEG_INFINITY, |acc, &v| acc.max(v));
2401 let mut min_imag = imag_values.iter().fold(f32::INFINITY, |acc, &v| acc.min(v));
2402 let mut max_imag = imag_values
2403 .iter()
2404 .fold(f32::NEG_INFINITY, |acc, &v| acc.max(v));
2405
2406 min_real = min_real.min(0.0);
2407 max_real = max_real.max(0.0);
2408 min_imag = min_imag.min(0.0);
2409 max_imag = max_imag.max(0.0);
2410
2411 let real_margin = ((max_real - min_real) * 0.05).max(1e-6);
2412 let imag_margin = ((max_imag - min_imag) * 0.05).max(1e-6);
2413
2414 let root = BitMapBackend::new(output_path.as_ref(), (1000, 1000)).into_drawing_area();
2415 root.fill(&WHITE)?;
2416
2417 let mut chart = ChartBuilder::on(&root)
2418 .margin(25)
2419 .x_label_area_size(60)
2420 .y_label_area_size(60)
2421 .build_cartesian_2d(
2422 (min_real - real_margin) as f64..(max_real + real_margin) as f64,
2423 (min_imag - imag_margin) as f64..(max_imag + imag_margin) as f64,
2424 )?;
2425
2426 chart
2427 .configure_mesh()
2428 .x_desc("Real")
2429 .y_desc("Imag")
2430 .label_style(("sans-serif", 30))
2431 .x_label_formatter(&|v| format!("{:.2e}", v))
2432 .y_label_formatter(&|v| format!("{:.2e}", v))
2433 .draw()?;
2434
2435 let total_points = real_values.len();
2436 let max_points = 200_000;
2437 let stride = (total_points / max_points).max(1);
2438
2439 chart.draw_series(
2440 real_values
2441 .iter()
2442 .zip(imag_values.iter())
2443 .enumerate()
2444 .filter_map(|(idx, (&re, &im))| {
2445 if idx % stride == 0 {
2446 Some(Circle::new(
2447 (re as f64, im as f64),
2448 1,
2449 BLUE.mix(0.4).filled(),
2450 ))
2451 } else {
2452 None
2453 }
2454 }),
2455 )?;
2456
2457 chart.draw_series(std::iter::once(Circle::new((0.0, 0.0), 6, RED.filled())))?;
2458
2459 root.present()?;
2460 compress_png(output_path.as_ref());
2461 Ok(())
2462}
2463
2464pub fn plot_amp_phase_scatter<P: AsRef<Path>>(
2465 output_path: P,
2466 amp_values: &[f32],
2467 phase_values_rad: &[f32],
2468) -> Result<(), Box<dyn std::error::Error>> {
2469 if amp_values.is_empty() || amp_values.len() != phase_values_rad.len() {
2470 return Err("Amp-Phase 散布図を描画するためのデータが不足しています".into());
2471 }
2472
2473 let phase_deg: Vec<f64> = phase_values_rad
2474 .iter()
2475 .map(|&phi| wrap_degrees((phi as f64).to_degrees()))
2476 .collect();
2477
2478 let min_amp = amp_values.iter().fold(f32::INFINITY, |acc, &v| acc.min(v));
2479 let max_amp = amp_values
2480 .iter()
2481 .fold(f32::NEG_INFINITY, |acc, &v| acc.max(v));
2482
2483 let amp_margin = ((max_amp - min_amp) * 0.05).max(1e-6);
2484
2485 let root = BitMapBackend::new(output_path.as_ref(), (1000, 600)).into_drawing_area();
2486 root.fill(&WHITE)?;
2487
2488 let mut chart = ChartBuilder::on(&root)
2489 .margin(25)
2490 .x_label_area_size(60)
2491 .y_label_area_size(60)
2492 .build_cartesian_2d(
2493 (min_amp - amp_margin) as f64..(max_amp + amp_margin) as f64,
2494 -180.0f64..180.0f64,
2495 )?;
2496
2497 chart
2498 .configure_mesh()
2499 .disable_mesh()
2500 .x_desc("Amplitude")
2501 .y_desc("Phase (deg)")
2502 .label_style(("sans-serif", 30))
2503 .x_label_formatter(&|v| format!("{:.2e}", v))
2504 .y_label_formatter(&|v| format!("{:.0}", v))
2505 .draw()?;
2506
2507 let total_points = amp_values.len();
2508 let max_points = 200_000;
2509 let stride = (total_points / max_points).max(1);
2510
2511 chart.draw_series(
2512 amp_values
2513 .iter()
2514 .zip(phase_deg.iter())
2515 .enumerate()
2516 .filter_map(|(idx, (&, &phase))| {
2517 if idx % stride == 0 {
2518 Some(Circle::new((amp as f64, phase), 1, BLUE.mix(0.4).filled()))
2519 } else {
2520 None
2521 }
2522 }),
2523 )?;
2524
2525 root.present()?;
2526 compress_png(output_path.as_ref());
2527 Ok(())
2528}
2529
2530pub fn plot_complex_histograms<P: AsRef<Path>, Q: AsRef<Path>>(
2531 output_path: P,
2532 log_output_path: Q,
2533 real_values: &[f32],
2534 imag_values: &[f32],
2535 amp_values: &[f32],
2536 phase_values: &[f32],
2537) -> Result<(), Box<dyn std::error::Error>> {
2538 if real_values.is_empty()
2539 || imag_values.is_empty()
2540 || amp_values.is_empty()
2541 || real_values.len() != imag_values.len()
2542 || real_values.len() != amp_values.len()
2543 || phase_values.len() != real_values.len()
2544 || phase_values.is_empty()
2545 {
2546 return Err("ヒストグラムを描画するための複素データが不足しています".into());
2547 }
2548
2549 let log_path = log_output_path.as_ref();
2550
2551 let root = BitMapBackend::new(output_path.as_ref(), (1600, 900)).into_drawing_area();
2552 root.fill(&WHITE)?;
2553 let (upper, lower) = root.split_vertically(450);
2554 let (left, right) = upper.split_horizontally(800);
2555 let (lower_left, lower_right) = lower.split_horizontally(800);
2556
2557 let mut reports = Vec::new();
2558
2559 reports.push(plot_histogram_panel(&left, "Real", real_values)?);
2560 reports.push(plot_histogram_panel(&right, "Imag", imag_values)?);
2561 reports.push(plot_histogram_panel(&lower_left, "Amplitude", amp_values)?);
2562 reports.push(plot_phase_histogram_panel(&lower_right, phase_values)?);
2563
2564 write_histogram_report(&reports, log_path)?;
2565
2566 Ok(())
2567}
2568
2569fn plot_histogram_panel(
2570 area: &DrawingArea<BitMapBackend, Shift>,
2571 label: &str,
2572 values: &[f32],
2573) -> Result<HistogramReport, Box<dyn std::error::Error>> {
2574 let values_f64: Vec<f64> = values.iter().map(|&v| v as f64).collect();
2575 let mut min_val = values_f64.iter().fold(f64::INFINITY, |acc, &v| acc.min(v));
2576 let mut max_val = values_f64
2577 .iter()
2578 .fold(f64::NEG_INFINITY, |acc, &v| acc.max(v));
2579
2580 if (max_val - min_val).abs() < f64::EPSILON {
2581 let center = values_f64.first().copied().unwrap_or(0.0);
2582 min_val = center - 1.0;
2583 max_val = center + 1.0;
2584 }
2585
2586 let bin_count = select_histogram_bins(&values_f64, min_val, max_val);
2587 let (hist_data, bin_width) = compute_histogram(&values_f64, bin_count, min_val, max_val);
2588 let max_count = hist_data
2589 .iter()
2590 .fold(0.0f64, |acc, (_, count)| acc.max(*count));
2591
2592 let mut chart = ChartBuilder::on(area)
2593 .margin(20)
2594 .x_label_area_size(60)
2595 .y_label_area_size(80)
2596 .build_cartesian_2d(min_val..max_val, 0.0..max_count * 1.2)?;
2597
2598 chart
2599 .configure_mesh()
2600 .x_desc(label)
2601 .y_desc("Counts")
2602 .disable_mesh()
2603 .label_style(("sans-serif", 30))
2604 .x_label_formatter(&|v| format!("{:.2e}", v))
2605 .y_label_formatter(&|v| format!("{:.1e}", v))
2606 .draw()?;
2607
2608 chart.draw_series(hist_data.iter().map(|(center, count)| {
2609 let half_bin = bin_width / 2.0;
2610 let left = center - half_bin;
2611 let right = center + half_bin;
2612 Rectangle::new([(left, 0.0), (right, *count)], BLUE.mix(0.4).filled())
2613 }))?;
2614
2615 let n = values_f64.len() as f64;
2616 let mean = values_f64.iter().sum::<f64>() / n;
2617 let variance = values_f64.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
2618 let sigma = variance.sqrt();
2619
2620 Ok(HistogramReport::from_lines(
2621 label,
2622 vec![
2623 format!("Samples: {}", values_f64.len()),
2624 format!("Mean: {:.6e}", mean),
2625 format!("Sigma: {:.6e}", sigma),
2626 format!("Min: {:.6e}", min_val),
2627 format!("Max: {:.6e}", max_val),
2628 ],
2629 ))
2630}
2631
2632fn plot_phase_histogram_panel(
2633 area: &DrawingArea<BitMapBackend, Shift>,
2634 phase_values: &[f32],
2635) -> Result<HistogramReport, Box<dyn std::error::Error>> {
2636 let values_rad: Vec<f64> = phase_values.iter().map(|&v| v as f64).collect();
2637 let values_deg: Vec<f64> = values_rad
2638 .iter()
2639 .map(|rad| wrap_degrees(rad.to_degrees()))
2640 .collect();
2641 let min_val = -180.0;
2642 let max_val = 180.0;
2643 let bin_count = 180usize;
2644 let (hist_data, bin_width) = compute_histogram(&values_deg, bin_count, min_val, max_val);
2645 let max_count = hist_data
2646 .iter()
2647 .fold(0.0f64, |acc, (_, count)| acc.max(*count));
2648
2649 let mut chart = ChartBuilder::on(area)
2650 .margin(20)
2651 .x_label_area_size(65)
2652 .y_label_area_size(80)
2653 .build_cartesian_2d(min_val..max_val, 0.0..max_count * 1.2)?;
2654
2655 chart
2656 .configure_mesh()
2657 .x_desc("Phase (deg)")
2658 .y_desc("Counts")
2659 .disable_mesh()
2660 .label_style(("sans-serif", 30))
2661 .x_labels(7)
2662 .x_label_formatter(&|v| format!("{:.0}", v))
2663 .y_label_formatter(&|v| format!("{:.1e}", v))
2664 .draw()?;
2665
2666 chart.draw_series(hist_data.iter().map(|(center_deg, count)| {
2667 let half_bin = bin_width / 2.0;
2668 let left = center_deg - half_bin;
2669 let right = center_deg + half_bin;
2670 Rectangle::new([(left, 0.0), (right, *count)], BLUE.mix(0.4).filled())
2671 }))?;
2672
2673 let total_count = values_deg.len() as f64;
2674 let expected_uniform = if bin_count > 0 {
2675 total_count / bin_count as f64
2676 } else {
2677 0.0
2678 };
2679 chart
2680 .draw_series(LineSeries::new(
2681 vec![(min_val, expected_uniform), (max_val, expected_uniform)],
2682 GREEN.stroke_width(3),
2683 ))?
2684 .label(format!("Uniform N/bin={:.1e}", expected_uniform))
2685 .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 30, y)], &GREEN));
2686
2687 let circular_mean = compute_circular_mean(&values_rad);
2688 let circular_sigma = compute_circular_sigma(&values_rad);
2689
2690 Ok(HistogramReport::from_lines(
2691 "Phase",
2692 vec![
2693 format!("Samples: {}", values_rad.len()),
2694 format!(
2695 "Circular mean (deg): {:.2}",
2696 wrap_degrees(circular_mean.to_degrees())
2697 ),
2698 format!("Circular sigma (deg): {:.2}", circular_sigma.to_degrees()),
2699 ],
2700 ))
2701}
2702
2703pub fn plot_cross_section(
2704 output_path: &str,
2705 horizontal_data: &[(f64, f32)], vertical_data: &[(f64, f32)], max_intensity: f32,
2708 delta_ra_mas: f64,
2709 delta_dec_mas: f64,
2710) -> Result<(), Box<dyn std::error::Error>> {
2711 let root = BitMapBackend::new(output_path, (1000, 700)).into_drawing_area();
2712 root.fill(&WHITE)?;
2713
2714 let (h_min, h_max) = horizontal_data
2715 .iter()
2716 .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), (x, _)| {
2717 (min.min(*x), max.max(*x))
2718 });
2719 let (v_min, v_max) = vertical_data
2720 .iter()
2721 .fold((f64::INFINITY, f64::NEG_INFINITY), |(min, max), (x, _)| {
2722 (min.min(*x), max.max(*x))
2723 });
2724 let x_min = h_min.min(v_min);
2725 let x_max = h_max.max(v_max);
2726
2727 let mut chart = ChartBuilder::on(&root)
2728 .caption(
2729 "Fringe Rate Map Cross-section at Peak",
2730 ("sans-serif", 25).into_font(),
2731 )
2732 .margin(10)
2733 .x_label_area_size(60)
2734 .y_label_area_size(80)
2735 .build_cartesian_2d(x_max..x_min, 0.0..1.1f64)?;
2736
2737 chart
2738 .configure_mesh()
2739 .x_desc("Offset (mas)")
2740 .y_desc("Normalized Intensity")
2741 .x_label_formatter(&|v| format!("{:.0}", v))
2742 .y_label_formatter(&|v| format!("{:.1}", v))
2743 .label_style(("sans-serif", 30).into_font())
2744 .draw()?;
2745
2746 chart
2747 .draw_series(LineSeries::new(
2748 horizontal_data
2749 .iter()
2750 .map(|(x, y)| (*x, (*y / max_intensity) as f64)),
2751 &BLUE,
2752 ))?
2753 .label("RA Cross-section")
2754 .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], BLUE.filled()));
2755
2756 chart
2757 .draw_series(LineSeries::new(
2758 vertical_data
2759 .iter()
2760 .map(|(x, y)| (*x, (*y / max_intensity) as f64)),
2761 &RED,
2762 ))?
2763 .label("Dec Cross-section")
2764 .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED.filled()));
2765
2766 chart
2767 .configure_series_labels()
2768 .background_style(&WHITE.mix(0.8))
2769 .border_style(&BLACK)
2770 .draw()?;
2771
2772 let font = ("sans-serif", 35).into_font();
2773 let text_style = TextStyle::from(font.clone()).color(&BLACK);
2774 let x_pos = root.get_pixel_range().0.end as i32 - 300;
2775 let mut y_pos = root.get_pixel_range().1.start as i32 + 20;
2776
2777 root.draw(&Text::new(
2778 format!("ΔRA: {:.3} mas", delta_ra_mas),
2779 (x_pos, y_pos),
2780 text_style.clone(),
2781 ))?;
2782 y_pos += 25;
2783 root.draw(&Text::new(
2784 format!("ΔDec: {:.3} mas", delta_dec_mas),
2785 (x_pos, y_pos),
2786 text_style.clone(),
2787 ))?;
2788
2789 root.present()?;
2790 compress_png(output_path);
2791 Ok(())
2792}
2793
2794pub fn plot_uv_coverage<P: AsRef<Path>>(
2795 output_path: P,
2796 uv_data: &[(f32, f32)],
2797) -> Result<(), Box<dyn std::error::Error>> {
2798 let root = BitMapBackend::new(output_path.as_ref(), (800, 800)).into_drawing_area();
2799 root.fill(&WHITE)?;
2800
2801 if uv_data.is_empty() {
2802 return Ok(());
2803 }
2804
2805 let u_max = uv_data.iter().map(|(u, _)| u.abs()).fold(0.0, f32::max);
2806 let v_max = uv_data.iter().map(|(_, v)| v.abs()).fold(0.0, f32::max);
2807 let max_abs = u_max.max(v_max) * 1.1;
2808
2809 let mut chart = ChartBuilder::on(&root)
2810 .caption("UV Coverage", ("sans-serif", 30))
2811 .margin(10)
2812 .x_label_area_size(60)
2813 .y_label_area_size(80)
2814 .build_cartesian_2d(-max_abs..max_abs, -max_abs..max_abs)?;
2815
2816 chart
2817 .configure_mesh()
2818 .x_desc("U (meters)")
2819 .y_desc("V (meters)")
2820 .x_label_formatter(&|x| format!("{:.0}", x))
2821 .y_label_formatter(&|y| format!("{:.0}", y))
2822 .label_style(("sans-serif", 25))
2823 .draw()?;
2824
2825 chart.draw_series(
2826 uv_data
2827 .iter()
2828 .map(|(u, v)| Circle::new((*u, *v), 2, BLUE.filled())),
2829 )?;
2830 chart.draw_series(
2831 uv_data
2832 .iter()
2833 .map(|(u, v)| Circle::new((-*u, -*v), 2, RED.filled())),
2834 )?;
2835
2836 root.present()?;
2837 Ok(())
2838}
2839
2840pub fn plot_uv_tracks<P: AsRef<Path>>(
2841 output_path: P,
2842 accessible: &[(f32, f32)],
2843 observed: &[(f32, f32)],
2844 accessible_baseline: &[(f32, f32)],
2845 observed_baseline: &[(f32, f32)],
2846 center_frequency_hz: f64,
2847 uv_mode: i32,
2848) -> Result<(), Box<dyn std::error::Error>> {
2849 let root = BitMapBackend::new(output_path.as_ref(), (1500, 600)).into_drawing_area();
2850 root.fill(&WHITE)?;
2851
2852 let (left_area, right_area) = root.split_horizontally(700);
2853
2854 let max_uv_value = accessible
2855 .iter()
2856 .chain(observed.iter())
2857 .flat_map(|(u, v)| [u.abs(), v.abs()])
2858 .fold(0.0f32, f32::max)
2859 .max(1.0);
2860
2861 let (uv_scale, uv_unit) = if max_uv_value >= 1_000_000.0 {
2862 (1_000_000.0, "Mm")
2863 } else if max_uv_value >= 1_000.0 {
2864 (1_000.0, "km")
2865 } else {
2866 (1.0, "m")
2867 };
2868
2869 let axis_limit = (max_uv_value / uv_scale) * 1.1;
2870
2871 let scaled_accessible: Vec<(f32, f32)> = accessible
2872 .iter()
2873 .map(|(u, v)| (*u / uv_scale, *v / uv_scale))
2874 .collect();
2875 let scaled_observed: Vec<(f32, f32)> = observed
2876 .iter()
2877 .map(|(u, v)| (*u / uv_scale, *v / uv_scale))
2878 .collect();
2879
2880 let mut uv_chart = ChartBuilder::on(&left_area)
2881 .caption("UV Coverage", ("sans-serif", 30))
2882 .margin(15)
2883 .x_label_area_size(60)
2884 .y_label_area_size(80)
2885 .build_cartesian_2d(-axis_limit..axis_limit, -axis_limit..axis_limit)?;
2886
2887 uv_chart
2888 .configure_mesh()
2889 .x_desc(&format!("U ({})", uv_unit))
2890 .y_desc(&format!("V ({})", uv_unit))
2891 .x_label_formatter(&|x| format!("{:.0}", x))
2892 .y_label_formatter(&|y| format!("{:.0}", y))
2893 .label_style(("sans-serif", 24))
2894 .light_line_style(&WHITE)
2895 .draw()?;
2896
2897 let accessible_style = ShapeStyle::from(&BLACK).stroke_width(2);
2898 if scaled_accessible.len() >= 2 {
2899 for (idx, segment) in scaled_accessible.windows(2).enumerate() {
2900 if idx % 2 == 0 {
2901 uv_chart.draw_series(std::iter::once(PathElement::new(
2902 vec![segment[0], segment[1]],
2903 accessible_style,
2904 )))?;
2905 }
2906 }
2907
2908 let mirrored: Vec<(f32, f32)> = scaled_accessible
2909 .iter()
2910 .rev()
2911 .map(|(u, v)| (-*u, -*v))
2912 .collect();
2913 for (idx, segment) in mirrored.windows(2).enumerate() {
2914 if idx % 2 == 0 {
2915 uv_chart.draw_series(std::iter::once(PathElement::new(
2916 vec![segment[0], segment[1]],
2917 accessible_style,
2918 )))?;
2919 }
2920 }
2921 }
2922
2923 if scaled_observed.len() >= 2 {
2924 let observed_style = ShapeStyle::from(&RED).stroke_width(2);
2925 uv_chart.draw_series(LineSeries::new(
2926 scaled_observed.iter().cloned(),
2927 observed_style,
2928 ))?;
2929 let mirrored: Vec<(f32, f32)> = scaled_observed
2930 .iter()
2931 .rev()
2932 .map(|(u, v)| (-*u, -*v))
2933 .collect();
2934 if mirrored.len() >= 2 {
2935 uv_chart.draw_series(LineSeries::new(mirrored, observed_style))?;
2936 }
2937 }
2938
2939 if !scaled_accessible.is_empty() {
2940 let accessible_points: Vec<(f32, f32)> = scaled_accessible
2941 .iter()
2942 .flat_map(|(u, v)| [(*u, *v), (-*u, -*v)])
2943 .collect();
2944 uv_chart
2945 .draw_series(PointSeries::of_element(
2946 accessible_points.into_iter(),
2947 4,
2948 ShapeStyle::from(&BLACK),
2949 &|coord, size, style| EmptyElement::at(coord) + Circle::new((0, 0), size, style),
2950 ))?
2951 .label(format!("EL ≥ 5° (uv_3d={})", uv_mode))
2952 .legend(|(x, y)| Circle::new((x, y), 5, ShapeStyle::from(&BLACK)));
2953 }
2954
2955 if !scaled_observed.is_empty() {
2956 let observed_points: Vec<(f32, f32)> = scaled_observed
2957 .iter()
2958 .flat_map(|(u, v)| [(*u, *v), (-*u, -*v)])
2959 .collect();
2960 let red_style = ShapeStyle::from(&RED).filled();
2961 uv_chart
2962 .draw_series(PointSeries::of_element(
2963 observed_points.into_iter(),
2964 4,
2965 red_style,
2966 &|coord, size, style| EmptyElement::at(coord) + Circle::new((0, 0), size, style),
2967 ))?
2968 .label(format!("Observation (uv_3d={})", uv_mode))
2969 .legend(|(x, y)| Circle::new((x, y), 5, ShapeStyle::from(&RED).filled()));
2970 }
2971
2972 uv_chart
2973 .configure_series_labels()
2974 .border_style(&BLACK)
2975 .background_style(&WHITE.mix(0.8))
2976 .draw()?;
2977
2978 let max_baseline = accessible_baseline
2979 .iter()
2980 .chain(observed_baseline.iter())
2981 .map(|(_, b)| *b)
2982 .fold(0.0f32, f32::max)
2983 .max(1.0);
2984
2985 let lambda = 299_792_458.0 / center_frequency_hz;
2986 let max_spatial = (max_baseline as f64 / lambda).max(1e-6) as f32;
2987
2988 let baseline_scale = if max_baseline >= 1_000_000.0 {
2989 1_000_000.0
2990 } else if max_baseline >= 1_000.0 {
2991 1_000.0
2992 } else {
2993 1.0
2994 };
2995 let baseline_unit = if baseline_scale == 1_000_000.0 {
2996 "Mm"
2997 } else if baseline_scale == 1_000.0 {
2998 "km"
2999 } else {
3000 "m"
3001 };
3002 let baseline_label = format!("Projected Baseline ({})", baseline_unit);
3003
3004 let spatial_scale = if max_spatial >= 1_000_000.0 {
3005 1_000_000.0
3006 } else if max_spatial >= 1_000.0 {
3007 1_000.0
3008 } else {
3009 1.0
3010 };
3011 let spatial_unit = if spatial_scale == 1_000_000.0 {
3012 "Mλ"
3013 } else if spatial_scale == 1_000.0 {
3014 "kλ"
3015 } else {
3016 "λ"
3017 };
3018 let spatial_label = format!("Spatial Frequency ({})", spatial_unit);
3019
3020 let baseline_upper = (max_baseline / baseline_scale) * 1.05;
3021 let spatial_upper = (max_spatial / spatial_scale) * 1.05;
3022
3023 let scaled_accessible_baseline: Vec<(f32, f32)> = accessible_baseline
3024 .iter()
3025 .map(|(t, b)| (*t, *b / baseline_scale))
3026 .collect();
3027 let scaled_observed_baseline: Vec<(f32, f32)> = observed_baseline
3028 .iter()
3029 .map(|(t, b)| (*t, *b / baseline_scale))
3030 .collect();
3031
3032 let mut baseline_chart = ChartBuilder::on(&right_area)
3033 .margin(15)
3034 .x_label_area_size(50)
3035 .y_label_area_size(80)
3036 .right_y_label_area_size(80)
3037 .build_cartesian_2d(0f32..24f32, 0f32..baseline_upper)?
3038 .set_secondary_coord(0f32..24f32, 0f32..spatial_upper);
3039
3040 baseline_chart
3041 .configure_mesh()
3042 .x_desc("UT (hour)")
3043 .y_desc(baseline_label.clone())
3044 .x_label_formatter(&|x| format!("{:.0}", x))
3045 .x_labels(25)
3046 .y_label_formatter(&|y| format!("{:.0}", y))
3047 .light_line_style(&WHITE)
3048 .label_style(("sans-serif", 22))
3049 .draw()?;
3050
3051 baseline_chart
3052 .configure_secondary_axes()
3053 .y_desc(spatial_label)
3054 .y_label_formatter(&|y| format!("{:.0}", y))
3055 .draw()?;
3056
3057 if scaled_accessible_baseline.len() >= 2 {
3058 let accessible_line_style = ShapeStyle::from(&BLACK).stroke_width(2);
3059 for (idx, segment) in scaled_accessible_baseline.windows(2).enumerate() {
3060 if idx % 2 == 0 {
3061 baseline_chart.draw_series(std::iter::once(PathElement::new(
3062 vec![segment[0], segment[1]],
3063 accessible_line_style,
3064 )))?;
3065 }
3066 }
3067 }
3068 if scaled_observed_baseline.len() >= 2 {
3069 let observed_line_style = ShapeStyle::from(&RED).stroke_width(2);
3070 baseline_chart.draw_series(LineSeries::new(
3071 scaled_observed_baseline.iter().cloned(),
3072 observed_line_style,
3073 ))?;
3074 }
3075
3076 if !scaled_accessible_baseline.is_empty() {
3077 baseline_chart
3078 .draw_series(PointSeries::of_element(
3079 scaled_accessible_baseline.iter().cloned(),
3080 4,
3081 ShapeStyle::from(&BLACK),
3082 &|coord, size, style| EmptyElement::at(coord) + Circle::new((0, 0), size, style),
3083 ))?
3084 .label(format!("EL ≥ 5° (uv_3d={})", uv_mode))
3085 .legend(|(x, y)| Circle::new((x, y), 5, ShapeStyle::from(&BLACK)));
3086 }
3087
3088 if !scaled_observed_baseline.is_empty() {
3089 let filled_style = ShapeStyle::from(&RED).filled();
3090 baseline_chart
3091 .draw_series(PointSeries::of_element(
3092 scaled_observed_baseline.iter().cloned(),
3093 4,
3094 filled_style,
3095 &|coord, size, style| EmptyElement::at(coord) + Circle::new((0, 0), size, style),
3096 ))?
3097 .label(format!("Observation (uv_3d={})", uv_mode))
3098 .legend(|(x, y)| Circle::new((x, y), 5, ShapeStyle::from(&RED).filled()));
3099 }
3100
3101 baseline_chart
3102 .configure_series_labels()
3103 .border_style(&BLACK)
3104 .background_style(&WHITE.mix(0.8))
3105 .draw()?;
3106
3107 root.present()?;
3108 Ok(())
3109}
3110
3111fn wrap_degrees(mut deg: f64) -> f64 {
3112 while deg <= -180.0 {
3113 deg += 360.0;
3114 }
3115 while deg > 180.0 {
3116 deg -= 360.0;
3117 }
3118 deg
3119}
3120
3121fn compute_circular_mean(values: &[f64]) -> f64 {
3122 if values.is_empty() {
3123 return 0.0;
3124 }
3125 let sum_sin: f64 = values.iter().map(|v| v.sin()).sum();
3126 let sum_cos: f64 = values.iter().map(|v| v.cos()).sum();
3127 sum_sin.atan2(sum_cos)
3128}
3129
3130fn compute_circular_sigma(values: &[f64]) -> f64 {
3131 if values.is_empty() {
3132 return 0.0;
3133 }
3134 let sum_sin: f64 = values.iter().map(|v| v.sin()).sum();
3135 let sum_cos: f64 = values.iter().map(|v| v.cos()).sum();
3136 let r = (sum_sin.powi(2) + sum_cos.powi(2)).sqrt() / values.len() as f64;
3137 if r < 1e-6 {
3138 PI / 2.0
3139 } else {
3140 (-2.0 * r.ln()).max(1e-8).sqrt()
3141 }
3142}
3143
3144fn select_histogram_bins(values: &[f64], min_val: f64, max_val: f64) -> usize {
3145 let n = values.len();
3146 if n <= 1 {
3147 return 32;
3148 }
3149
3150 let mut sorted = values.to_vec();
3151 sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
3152
3153 let q1 = percentile(&sorted, 0.25);
3154 let q3 = percentile(&sorted, 0.75);
3155 let iqr = (q3 - q1).abs();
3156
3157 let n_f64 = n as f64;
3158 let mut bin_width = if iqr > 0.0 {
3159 2.0 * iqr / n_f64.powf(1.0 / 3.0)
3160 } else {
3161 let (_, sigma) = compute_gaussian_params(values);
3162 if sigma > 0.0 {
3163 3.49 * sigma / n_f64.powf(1.0 / 3.0)
3164 } else {
3165 (max_val - min_val).abs() / n_f64.max(1.0)
3166 }
3167 };
3168
3169 if !bin_width.is_finite() || bin_width <= 0.0 {
3170 bin_width = (max_val - min_val).abs() / n_f64.max(1.0);
3171 }
3172
3173 let range = (max_val - min_val).abs();
3174 let mut bin_count = if bin_width > 0.0 {
3175 (range / bin_width).ceil() as usize
3176 } else {
3177 32
3178 };
3179
3180 bin_count = bin_count.clamp(32, 512);
3181 if bin_count == 0 {
3182 bin_count = 32;
3183 }
3184
3185 bin_count
3186}
3187
3188fn percentile(sorted: &[f64], p: f64) -> f64 {
3189 if sorted.is_empty() {
3190 return 0.0;
3191 }
3192 let clamped_p = p.clamp(0.0, 1.0);
3193 let pos = clamped_p * (sorted.len() - 1) as f64;
3194 let lower_idx = pos.floor() as usize;
3195 let upper_idx = pos.ceil() as usize;
3196 if lower_idx == upper_idx {
3197 return sorted[lower_idx];
3198 }
3199 let weight = pos - lower_idx as f64;
3200 sorted[lower_idx] * (1.0 - weight) + sorted[upper_idx] * weight
3201}
3202
3203fn compute_histogram(
3204 values: &[f64],
3205 bin_count: usize,
3206 min_val: f64,
3207 max_val: f64,
3208) -> (Vec<(f64, f64)>, f64) {
3209 let mut counts = vec![0usize; bin_count];
3210 let mut centers = vec![0f64; bin_count];
3211 let range = max_val - min_val;
3212 let bin_width = if bin_count > 0 {
3213 let width = range / bin_count as f64;
3214 if width.is_finite() && width > 0.0 {
3215 width
3216 } else {
3217 1.0
3218 }
3219 } else {
3220 1.0
3221 };
3222
3223 for (idx, count) in centers.iter_mut().enumerate() {
3224 *count = min_val + (idx as f64 + 0.5) * bin_width;
3225 }
3226
3227 for &value in values {
3228 let mut bin_idx = ((value - min_val) / bin_width) as isize;
3229 if bin_idx < 0 {
3230 bin_idx = 0;
3231 } else if bin_idx as usize >= bin_count {
3232 bin_idx = bin_count as isize - 1;
3233 }
3234 counts[bin_idx as usize] += 1;
3235 }
3236
3237 let hist_data = counts
3238 .into_iter()
3239 .zip(centers.into_iter())
3240 .map(|(count, center)| (center, count as f64))
3241 .collect();
3242
3243 (hist_data, bin_width)
3244}
3245
3246fn compute_gaussian_params(values: &[f64]) -> (f64, f64) {
3247 let n = values.len() as f64;
3248 if n <= 1.0 {
3249 return (0.0, 0.0);
3250 }
3251 let mean = values.iter().sum::<f64>() / n;
3252 let variance = values.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / n;
3253 (mean, variance.sqrt())
3254}
3255
3256#[derive(Clone, Debug)]
3257struct HistogramReport {
3258 title: String,
3259 lines: Vec<String>,
3260}
3261
3262impl HistogramReport {
3263 fn from_lines(title: &str, mut lines: Vec<String>) -> Self {
3264 if lines.is_empty() {
3265 lines.push("(no data)".to_string());
3266 }
3267 Self {
3268 title: title.to_string(),
3269 lines,
3270 }
3271 }
3272}
3273
3274fn write_histogram_report(
3275 reports: &[HistogramReport],
3276 output_path: &Path,
3277) -> Result<(), Box<dyn std::error::Error>> {
3278 let mut aggregated = String::new();
3279 for report in reports {
3280 let header = format!("== {} ==", report.title);
3281 println!("{}", header);
3282 aggregated.push_str(&header);
3283 aggregated.push('\n');
3284 for line in &report.lines {
3285 println!("{}", line);
3286 aggregated.push_str(line);
3287 aggregated.push('\n');
3288 }
3289 println!();
3290 aggregated.push('\n');
3291 }
3292 let mut file = File::create(output_path)?;
3293 file.write_all(aggregated.as_bytes())?;
3294 Ok(())
3295}
3296
3297pub fn frequency_plane_msb(
3299 c_band_amp_profile: &[(f64, f64)],
3300 c_band_phase_profile: &[(f64, f64)],
3301 x_band_amp_profile: &[(f64, f64)],
3302 x_band_phase_profile: &[(f64, f64)],
3303 _x_band_uncalibrated_phase_profile: &[(f64, f64)],
3304 c_band_rate_profile: &[(f64, f64)], x_band_rate_profile: &[(f64, f64)], heatmap_func: impl Fn(f64, f64) -> f64,
3307 stat_keys: &[&str],
3308 stat_vals: &[&str],
3309 output_path: &str,
3310 bw: f64,
3311 max_amplitude: f64,
3312) -> Result<(), Box<dyn std::error::Error>> {
3313 let width = 1400;
3314 let height = 1000;
3315 let root = BitMapBackend::new(output_path, (width, height)).into_drawing_area();
3316 root.fill(&WHITE)?;
3317
3318 let (left_area, right_area) = root.split_horizontally(width / 2);
3319 let (top_left_area, rate_area) = left_area.split_vertically(height / 2 + 50);
3320 let (phase_area, amp_area) =
3321 top_left_area.split_vertically(top_left_area.get_pixel_range().1.len() as u32 / 4 - 10);
3322 let (heatmap_and_colorbar_area, stats_area) = right_area.split_vertically(height / 2 + 50);
3323 let (heatmap_area, colorbar_area) = heatmap_and_colorbar_area
3324 .split_horizontally(heatmap_and_colorbar_area.get_pixel_range().0.len() as u32 - 120);
3325
3326 let c_amp_max_y = c_band_amp_profile
3328 .iter()
3329 .map(|(_, y)| *y)
3330 .fold(f64::NEG_INFINITY, f64::max);
3331 let x_amp_max_y = x_band_amp_profile
3332 .iter()
3333 .map(|(_, y)| *y)
3334 .fold(f64::NEG_INFINITY, f64::max);
3335 let amp_max_y = c_amp_max_y.max(x_amp_max_y) * 1.1;
3336 let c_band_rate_max_y = c_band_rate_profile
3337 .iter()
3338 .map(|(_, y)| *y)
3339 .fold(f64::NEG_INFINITY, f64::max);
3340 let x_band_rate_max_y = x_band_rate_profile
3341 .iter()
3342 .map(|(_, y)| *y)
3343 .fold(f64::NEG_INFINITY, f64::max);
3344 let rate_max_y = c_band_rate_max_y.max(x_band_rate_max_y) * 1.1;
3345
3346 let c_band_rate_min_x = c_band_rate_profile
3347 .iter()
3348 .map(|(x, _)| *x)
3349 .fold(f64::INFINITY, f64::min);
3350 let c_band_rate_max_x = c_band_rate_profile
3351 .iter()
3352 .map(|(x, _)| *x)
3353 .fold(f64::NEG_INFINITY, f64::max);
3354 let x_band_rate_min_x = x_band_rate_profile
3355 .iter()
3356 .map(|(x, _)| *x)
3357 .fold(f64::INFINITY, f64::min);
3358 let x_band_rate_max_x = x_band_rate_profile
3359 .iter()
3360 .map(|(x, _)| *x)
3361 .fold(f64::NEG_INFINITY, f64::max);
3362
3363 let rate_min_x = c_band_rate_min_x.min(x_band_rate_min_x);
3364 let rate_max_x = c_band_rate_max_x.max(x_band_rate_max_x);
3365
3366 let mut phase_chart = ChartBuilder::on(&phase_area)
3368 .margin_top(20)
3369 .margin_left(15)
3370 .x_label_area_size(0)
3371 .y_label_area_size(120)
3372 .build_cartesian_2d(0.0..bw, -180.0..180.0)?;
3373 phase_chart
3374 .configure_mesh()
3375 .x_labels(7)
3378 .x_max_light_lines(0)
3379 .y_max_light_lines(0)
3380 .axis_style(BLACK.stroke_width(1))
3381 .y_desc("Phase")
3382 .y_label_formatter(&|y| format!("{:.0}", y))
3383 .y_labels(6)
3384 .label_style(("sans-serif ", 30))
3385 .draw()?;
3386 phase_chart.draw_series(LineSeries::new(
3387 c_band_phase_profile.iter().cloned(),
3388 RED.stroke_width(1),
3389 ))?;
3390 phase_chart.draw_series(LineSeries::new(
3391 x_band_phase_profile.iter().cloned(),
3392 BLUE.stroke_width(1),
3393 ))?;
3394 let x_spec = phase_chart.x_range();
3398 let y_spec = phase_chart.y_range();
3399 phase_chart.draw_series(std::iter::once(Rectangle::new(
3400 [(x_spec.start, y_spec.start), (x_spec.end, y_spec.end)],
3401 BLACK.stroke_width(1),
3402 )))?;
3403
3404 let mut amp_chart = ChartBuilder::on(&_area)
3406 .margin_left(15)
3407 .x_label_area_size(65)
3408 .y_label_area_size(120)
3409 .build_cartesian_2d(0.0..bw, 0.0..amp_max_y)?;
3410 amp_chart
3411 .configure_mesh()
3412 .x_desc("Frequency [MHz]")
3413 .y_desc("Amplitude")
3414 .x_max_light_lines(0)
3415 .y_max_light_lines(0)
3416 .x_labels(7)
3417 .y_labels(7)
3418 .axis_style(BLACK.stroke_width(1))
3419 .label_style(("sans-serif ", 30))
3420 .x_label_formatter(&|y| format!("{:.0}", y))
3421 .y_label_formatter(&|y| format!("{:.1e}", y))
3422 .draw()?;
3423 amp_chart.draw_series(LineSeries::new(
3424 c_band_amp_profile.iter().cloned(),
3425 RED.stroke_width(1),
3426 ))?;
3427 amp_chart.draw_series(LineSeries::new(
3428 x_band_amp_profile.iter().cloned(),
3429 BLUE.stroke_width(1),
3430 ))?;
3431
3432 let x_spec = amp_chart.x_range();
3434 let y_spec = amp_chart.y_range();
3435 amp_chart.draw_series(std::iter::once(Rectangle::new(
3436 [(x_spec.start, y_spec.start), (x_spec.end, y_spec.end)],
3437 BLACK.stroke_width(1),
3438 )))?;
3439
3440 let mut rate_chart = ChartBuilder::on(&rate_area)
3442 .margin_top(10)
3443 .margin_left(15)
3444 .x_label_area_size(75)
3445 .y_label_area_size(120)
3446 .build_cartesian_2d(rate_min_x..rate_max_x, 0.0..rate_max_y)?;
3447 rate_chart
3448 .configure_mesh()
3449 .x_desc("Rate [Hz]")
3450 .y_desc("Amplitude")
3451 .x_max_light_lines(0)
3452 .y_max_light_lines(0)
3453 .x_labels(7)
3454 .axis_style(BLACK.stroke_width(1))
3456 .label_style(("sans-serif ", 30))
3457 .y_label_formatter(&|y| format!("{:.1e}", y))
3458 .draw()?;
3459 rate_chart.draw_series(LineSeries::new(
3460 c_band_rate_profile.iter().cloned(),
3461 RED.stroke_width(1),
3462 ))?; rate_chart.draw_series(LineSeries::new(
3466 x_band_rate_profile.iter().cloned(),
3467 BLUE.stroke_width(1),
3468 ))?; let x_spec = rate_chart.x_range();
3479 let y_spec = rate_chart.y_range();
3480 rate_chart.draw_series(std::iter::once(Rectangle::new(
3481 [(x_spec.start, y_spec.start), (x_spec.end, y_spec.end)],
3482 BLACK.stroke_width(1),
3483 )))?;
3484
3485 let mut heatmap_chart = ChartBuilder::on(&heatmap_area)
3487 .margin(20)
3488 .x_label_area_size(55)
3489 .y_label_area_size(85)
3490 .build_cartesian_2d(0.0..bw, rate_min_x..rate_max_x)?;
3491 heatmap_chart
3492 .configure_mesh()
3493 .x_desc("Frequency [MHz]")
3494 .y_desc("Rate [Hz]")
3495 .x_labels(7)
3496 .y_labels(7)
3497 .x_label_formatter(&|y| format!("{:.0}", y))
3498 .y_label_formatter(&|y| format!("{:.1}", y))
3499 .label_style(("sans-serif ", 30))
3500 .draw()?;
3501
3502 let resolution = 400;
3503 let mut heatmap_values = Vec::new();
3504 let mut heatmap_data_max_val = f64::NEG_INFINITY;
3505 for i in 0..resolution {
3506 for j in 0..resolution {
3507 let y = rate_min_x + (rate_max_x - rate_min_x) * i as f64 / (resolution - 1) as f64;
3508 let x = 0.0 + bw * j as f64 / (resolution - 1) as f64;
3509 let val = heatmap_func(x, y);
3510 heatmap_values.push(val);
3511 if val > heatmap_data_max_val {
3512 heatmap_data_max_val = val;
3513 }
3514 }
3515 }
3516
3517 for (idx, val) in heatmap_values.iter().enumerate() {
3518 let i = idx / resolution;
3519 let j = idx % resolution;
3520 let y = rate_min_x + (rate_max_x - rate_min_x) * i as f64 / (resolution - 1) as f64;
3521 let x = 0.0 + bw * j as f64 / (resolution - 1) as f64;
3522 let x_step = bw / (resolution - 1) as f64;
3523 let y_step = (rate_max_x - rate_min_x) / (resolution - 1) as f64;
3524 let normalized_val = if heatmap_data_max_val > 0.0 {
3525 *val / heatmap_data_max_val
3526 } else {
3527 0.0
3528 };
3529 heatmap_chart.draw_series(std::iter::once(Rectangle::new(
3530 [(x, y), (x + x_step, y + y_step)],
3531 HSLColor((1.0 - normalized_val) * 0.7, 1.0, 0.5).filled(),
3532 )))?;
3533 }
3534
3535 let mut colorbar = ChartBuilder::on(&colorbar_area)
3537 .margin_top(10)
3538 .margin_bottom(50)
3539 .set_label_area_size(LabelAreaPosition::Right, 100)
3540 .set_label_area_size(LabelAreaPosition::Left, 0)
3541 .set_label_area_size(LabelAreaPosition::Top, 10)
3542 .set_label_area_size(LabelAreaPosition::Bottom, 25)
3543 .build_cartesian_2d(0.0..1.0, 0.0..max_amplitude)?;
3544
3545 let steps = 100;
3546 for i in 0..steps {
3547 let value = i as f64 / (steps - 1) as f64;
3548 let color = HSLColor(((1.0 - value) * 0.7).into(), 1.0, 0.5);
3549 colorbar.draw_series(std::iter::once(Rectangle::new(
3550 [
3551 (0.0, value * max_amplitude),
3552 (1.0, (value + 1.0 / steps as f64) * max_amplitude),
3553 ],
3554 color.filled(),
3555 )))?;
3556 }
3557
3558 colorbar
3559 .configure_mesh()
3560 .disable_x_mesh()
3561 .disable_y_mesh()
3562 .disable_x_axis()
3563 .y_labels(7)
3564 .y_label_style(("sans-serif ", 30))
3565 .y_label_formatter(&|v| format!("{:.1e}", v))
3566 .draw()?;
3567
3568 let font_large = ("sans-serif ", 35).into_font();
3570 let font_small = ("sans-serif ", 25).into_font(); let left_x = 30;
3572 let right_x = stats_area.get_pixel_range().0.len() as i32 - 30;
3573 let mut y = 20;
3574
3575 for (k, v) in stat_keys.iter().zip(stat_vals.iter()) {
3576 let current_font = if v.contains(".cor") || v.contains(".bin") {
3577 &font_small
3578 } else {
3579 &font_large
3580 };
3581 let text_style = TextStyle::from(current_font.clone()).color(&BLACK);
3582
3583 stats_area.draw(&Text::new(k.to_string(), (left_x, y), text_style.clone()))?;
3584 stats_area.draw(&Text::new(
3585 v.to_string(),
3586 (right_x, y),
3587 text_style.clone().pos(Pos::new(HPos::Right, VPos::Top)),
3588 ))?;
3589 y += 40;
3590 }
3591
3592 root.present()?;
3593 Ok(())
3594}