1use std::error::Error;
4use std::path::Path;
5
6use plotters::coord::Shift;
7use plotters::prelude::*;
8use rustfft::num_complex::Complex32;
9
10use crate::debug::PassbandChannelCompareDump;
11
12fn constellation_bounds(points: &[Complex32]) -> Option<f32> {
13 let max_abs = points
14 .iter()
15 .map(|z| z.re.abs().max(z.im.abs()))
16 .fold(0.0f32, f32::max);
17 if max_abs <= 0.0 {
18 None
19 } else {
20 Some((1.2 * max_abs).max(1.25))
21 }
22}
23
24fn draw_constellation_panel(
25 area: &DrawingArea<BitMapBackend<'_>, Shift>,
26 title: &str,
27 points: &[Complex32],
28 lim: f32,
29) -> Result<(), Box<dyn Error>> {
30 let mut chart = ChartBuilder::on(area)
31 .caption(title, ("sans-serif", 24).into_font())
32 .margin(16)
33 .x_label_area_size(40)
34 .y_label_area_size(45)
35 .build_cartesian_2d(-lim..lim, -lim..lim)?;
36
37 chart
38 .configure_mesh()
39 .x_desc("In-Phase")
40 .y_desc("Quadrature")
41 .axis_desc_style(("sans-serif", 18))
42 .label_style(("sans-serif", 14))
43 .light_line_style(RGBAColor(0, 0, 0, 0.08))
44 .bold_line_style(RGBAColor(0, 0, 0, 0.18))
45 .draw()?;
46
47 chart.draw_series(std::iter::once(PathElement::new(
48 vec![(-lim, 0.0), (lim, 0.0)],
49 BLACK.mix(0.25),
50 )))?;
51 chart.draw_series(std::iter::once(PathElement::new(
52 vec![(0.0, -lim), (0.0, lim)],
53 BLACK.mix(0.25),
54 )))?;
55 chart.draw_series(points.iter().map(|z| {
56 Circle::new(
57 (z.re, z.im),
58 4,
59 ShapeStyle::from(&RGBColor(33, 145, 140)).stroke_width(1),
60 )
61 }))?;
62 Ok(())
63}
64
65pub fn save_constellation_comparison_png(
66 path: &Path,
67 pre_eq: &[Complex32],
68 post_eq: &[Complex32],
69) -> Result<(), Box<dyn Error>> {
70 if pre_eq.is_empty() && post_eq.is_empty() {
71 return Ok(());
72 }
73
74 let lim = constellation_bounds(pre_eq)
75 .into_iter()
76 .chain(constellation_bounds(post_eq))
77 .fold(1.25f32, f32::max);
78
79 let root = BitMapBackend::new(path, (1280, 720)).into_drawing_area();
80 root.fill(&RGBColor(245, 245, 240))?;
81 let (left, right) = root.split_horizontally(640);
82 draw_constellation_panel(&left, "Constellation: Pre-EQ", pre_eq, lim)?;
83 draw_constellation_panel(&right, "Constellation: Post-EQ", post_eq, lim)?;
84 root.present()?;
85 Ok(())
86}
87
88fn phase_deg(z: Complex32) -> f32 {
89 z.arg().to_degrees()
90}
91
92pub fn save_channel_compare_png(
93 path: &Path,
94 dump: &PassbandChannelCompareDump,
95) -> Result<(), Box<dyn Error>> {
96 if dump.rows.is_empty() {
97 return Ok(());
98 }
99
100 let mut bins = dump.rows.iter().map(|r| r.used_bin).collect::<Vec<_>>();
101 bins.sort_unstable();
102 bins.dedup();
103
104 let mut actual = Vec::with_capacity(bins.len());
105 let mut est_train = Vec::with_capacity(bins.len());
106 let mut est_pilot = Vec::with_capacity(bins.len());
107 for &bin in &bins {
108 let rows = dump
109 .rows
110 .iter()
111 .filter(|r| r.used_bin == bin)
112 .collect::<Vec<_>>();
113 let n = rows.len().max(1) as f32;
114 let a = rows
115 .iter()
116 .fold(Complex32::new(0.0, 0.0), |acc, r| acc + r.actual_h)
117 / n;
118 let t = rows
119 .iter()
120 .fold(Complex32::new(0.0, 0.0), |acc, r| acc + r.estimated_h_train)
121 / n;
122 let p = rows
123 .iter()
124 .fold(Complex32::new(0.0, 0.0), |acc, r| acc + r.estimated_h_pilot)
125 / n;
126 actual.push((bin as f32, a));
127 est_train.push((bin as f32, t));
128 est_pilot.push((bin as f32, p));
129 }
130
131 let x_min = *bins.first().unwrap() as f32 - 0.5;
132 let x_max = *bins.last().unwrap() as f32 + 0.5;
133 let mag_max = actual
134 .iter()
135 .chain(est_train.iter())
136 .chain(est_pilot.iter())
137 .map(|(_, z)| z.norm())
138 .fold(0.0f32, f32::max)
139 .max(1.0);
140
141 let root = BitMapBackend::new(path, (1280, 800)).into_drawing_area();
142 root.fill(&RGBColor(245, 245, 240))?;
143 let (top, bottom) = root.split_vertically(400);
144
145 {
146 let mut chart = ChartBuilder::on(&top)
147 .caption("Channel Envelope", ("sans-serif", 24).into_font())
148 .margin(16)
149 .x_label_area_size(40)
150 .y_label_area_size(55)
151 .build_cartesian_2d(x_min..x_max, 0.0f32..(1.2 * mag_max))?;
152 chart
153 .configure_mesh()
154 .x_desc("Used Bin")
155 .y_desc("|H|")
156 .axis_desc_style(("sans-serif", 18))
157 .label_style(("sans-serif", 14))
158 .draw()?;
159 chart
160 .draw_series(std::iter::once(PathElement::new(
161 actual
162 .iter()
163 .map(|(b, z)| (*b, z.norm()))
164 .collect::<Vec<_>>(),
165 RGBColor(214, 39, 40),
166 )))?
167 .label("Actual")
168 .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RGBColor(214, 39, 40)));
169 chart
170 .draw_series(std::iter::once(PathElement::new(
171 est_train
172 .iter()
173 .map(|(b, z)| (*b, z.norm()))
174 .collect::<Vec<_>>(),
175 RGBColor(31, 119, 180),
176 )))?
177 .label("Training Estimate")
178 .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RGBColor(31, 119, 180)));
179 chart
180 .draw_series(std::iter::once(PathElement::new(
181 est_pilot
182 .iter()
183 .map(|(b, z)| (*b, z.norm()))
184 .collect::<Vec<_>>(),
185 RGBColor(44, 160, 44),
186 )))?
187 .label("Pilot-Adjusted")
188 .legend(|(x, y)| PathElement::new(vec![(x, y), (x + 20, y)], RGBColor(44, 160, 44)));
189 chart
190 .configure_series_labels()
191 .border_style(BLACK.mix(0.3))
192 .background_style(WHITE.mix(0.8))
193 .draw()?;
194 }
195
196 {
197 let mut chart = ChartBuilder::on(&bottom)
198 .caption("Channel Phase", ("sans-serif", 24).into_font())
199 .margin(16)
200 .x_label_area_size(40)
201 .y_label_area_size(55)
202 .build_cartesian_2d(x_min..x_max, -180.0f32..180.0f32)?;
203 chart
204 .configure_mesh()
205 .x_desc("Used Bin")
206 .y_desc("Phase (deg)")
207 .axis_desc_style(("sans-serif", 18))
208 .label_style(("sans-serif", 14))
209 .draw()?;
210 chart.draw_series(std::iter::once(PathElement::new(
211 actual
212 .iter()
213 .map(|(b, z)| (*b, phase_deg(*z)))
214 .collect::<Vec<_>>(),
215 RGBColor(214, 39, 40),
216 )))?;
217 chart.draw_series(std::iter::once(PathElement::new(
218 est_train
219 .iter()
220 .map(|(b, z)| (*b, phase_deg(*z)))
221 .collect::<Vec<_>>(),
222 RGBColor(31, 119, 180),
223 )))?;
224 chart.draw_series(std::iter::once(PathElement::new(
225 est_pilot
226 .iter()
227 .map(|(b, z)| (*b, phase_deg(*z)))
228 .collect::<Vec<_>>(),
229 RGBColor(44, 160, 44),
230 )))?;
231 }
232
233 root.present()?;
234 Ok(())
235}
236
237