Skip to main content

frinZ/
plot.rs

1#![allow(dead_code)]
2
3// Plot utilities for frinZ:
4// - delay/rate/frequency planes and diagnostics
5// - UV-related plots
6// - multi-sideband summary plot (merged from former plot_msb.rs)
7use 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; // Added for dynamic spectrum
15use 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    //cmap_time: bool,
100) -> 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    // --- Determine axis ranges ---
112    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    // 1. Horizontal slice (Delay Profile)
296    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        //.y_labels(7)
309        .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    // Draw bounding box for chart1
364    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    // 2. Vertical slice (Rate Profile)
372    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        //.y_labels(7)
385        .x_max_light_lines(0)
386        .y_max_light_lines(0)
387        .axis_style(BLACK.stroke_width(1))
388        //.x_label_formatter(&|v| format!("{:.1}", v))
389        .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    // Draw bounding box for chart2
440    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    // 3. 2D heatmap
448    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    // 4. Colorbar
530    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    // 5. Stats
563    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    // --- Determine axis ranges ---
622    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    // --- Phase Chart ---
658    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        //.disable_x_mesh()
667        //.x_labels(0)
668        .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    // Draw bounding box for phase_chart
695    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    // --- Amplitude Chart ---
703    let mut amp_chart = ChartBuilder::on(&amp_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    // Draw bounding box for amp_chart
739    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    // --- Rate Chart ---
747    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        //.y_labels(7)
761        .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    // Draw bounding box for rate_chart
785    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    // --- Heatmap Chart ---
793    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    // 4. Colorbar
846    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    // 5. Stats
879    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", &amp_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            // Handle case with a single data point
1152            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)    // We want to construct a composed element on-the-fly
1199                    + Circle::new((0,0),s,st.filled()); // At point center, draw a circle
1200            },
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    // Determine the Y-axis range dynamically
1242    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        // Calculate the nearest power of 10 below the minimum data point
1254        10.0_f32.powf(data_min_snr.log10().floor())
1255    } else {
1256        // Fallback if data is empty or contains no positive values
1257        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    // Power-law fitting on log-log scale: y = a * x^b
1297    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                // Reference line with b = 0.5, forced to pass the first point
1346                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    // Determine y-axis range from all relevant phase data
1402    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        //.caption("Phase Reference Plot", ("sans-serif", 40).into_font())
1416        .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    // 1. Original Calibrator Phase (Red Points)
1444    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    // 2. Fitted Calibrator Phase (Blue Line)
1458    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    // 3. Original Target Phase (Green Circles)
1472    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    // 4. Residual Phases from Calibrator Fit (Black Circles)
1486    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    // Define a fixed output image size for consistent plot appearance
1774    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; // Invert RA axis
1789    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    // Draw the heatmap
1808    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    // Add a white 'X' mark at (0,0)
1832    chart.draw_series(PointSeries::of_element(
1833        vec![(0.0, 0.0)],                                 // Center of the 'X'
1834        10,                                               // Size of the 'X'
1835        &WHITE,                                           // Color of the 'X'
1836        &|c, s, st| Cross::new(c, s, st.stroke_width(2)), // Draw a cross
1837    ))?;
1838
1839    // Add a red 'X' mark at the maximum value position
1840    let (height, width) = map_data.dim(); // Get dimensions for calculation
1841    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)], // Center of the 'X'
1846        10,                                   // Size of the 'X'
1847        &RED,                                 // Color of the 'X'
1848        &|c, s, st| Cross::new(c, s, st.stroke_width(2)), // Draw a cross
1849    ))?;
1850
1851    // Draw color bar
1852    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    // --- Amplitude Heatmap ---
1910    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        //.x_desc("Frequency [channels]")
1932        .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    // --- Phase Heatmap ---
1946    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    // Apply horizontal blur
2071    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    // Apply vertical blur
2084    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        //.caption(title, ("sans-serif", 20).into_font())
2128        .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    // Draw color bar aligned with plotting area height
2158    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    // --- Amplitude Heatmap ---
2240    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(&amplitudes_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    // --- Phase Heatmap ---
2276    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(&amplitudes_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, (&amp, &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)], // (RA offset, Intensity)
2706    vertical_data: &[(f64, f32)],   // (Dec offset, Intensity)
2707    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
3297// Multi-sideband (C/X) frequency-domain summary plot merged from former plot_msb.rs.
3298pub 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)], // New parameter for C-band rate
3305    x_band_rate_profile: &[(f64, f64)], // New parameter for X-band rate
3306    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    // --- Determine axis ranges ---
3327    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    // --- Phase Chart ---
3367    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        //.disable_x_mesh()
3376        //.x_labels(0)
3377        .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    //phase_chart.draw_series(LineSeries::new(x_band_uncalibrated_phase_profile.iter().cloned(), BLACK.stroke_width(1)))?;
3395
3396    // Draw bounding box for phase_chart
3397    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    // --- Amplitude Chart ---
3405    let mut amp_chart = ChartBuilder::on(&amp_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    // Draw bounding box for amp_chart
3433    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    // --- Rate Chart ---
3441    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        //.y_labels(7)
3455        .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    ))?; // C-band in RED
3463         //.label("C-band Rate")
3464         //.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RED.filled()));
3465    rate_chart.draw_series(LineSeries::new(
3466        x_band_rate_profile.iter().cloned(),
3467        BLUE.stroke_width(1),
3468    ))?; // X-band in BLUE
3469         //.label("X-band Rate")
3470         //.legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], BLUE.filled()));
3471
3472    //rate_chart.configure_series_labels()
3473    //    .background_style(&WHITE.mix(0.8))
3474    //    .border_style(&BLACK)
3475    //    .draw()?;
3476
3477    // Draw bounding box for rate_chart
3478    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    // --- Heatmap Chart ---
3486    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    // 4. Colorbar
3536    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    // 5. Stats
3569    let font_large = ("sans-serif ", 35).into_font();
3570    let font_small = ("sans-serif ", 25).into_font(); // Smaller font
3571    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}