autd3_link_visualizer/backend/
plotters.rs

1use std::ffi::OsString;
2
3use plotters::{coord::Shift, prelude::*};
4use scarlet::colormap::{ColorMap, ListedColorMap};
5
6use crate::{colormap, error::VisualizerError, Backend};
7
8use autd3_driver::{autd3_device::AUTD3, geometry::Geometry};
9
10#[derive(Clone, Debug)]
11pub struct PlotConfig {
12    pub figsize: (u32, u32),
13    pub cbar_size: f32,
14    pub font_size: u32,
15    pub label_area_size: u32,
16    pub margin: u32,
17    pub ticks_step: f32,
18    pub cmap: ListedColorMap,
19    pub fname: OsString,
20}
21
22impl Default for PlotConfig {
23    fn default() -> Self {
24        Self {
25            figsize: (960, 640),
26            cbar_size: 0.15,
27            ticks_step: 10.,
28            label_area_size: 80,
29            margin: 10,
30            font_size: 24,
31            cmap: colormap::jet(),
32            fname: OsString::new(),
33        }
34    }
35}
36
37impl PartialEq for PlotConfig {
38    fn eq(&self, other: &Self) -> bool {
39        self.figsize == other.figsize
40            && self.cbar_size == other.cbar_size
41            && self.font_size == other.font_size
42            && self.label_area_size == other.label_area_size
43            && self.margin == other.margin
44            && self.ticks_step == other.ticks_step
45            && self.cmap.vals == other.cmap.vals
46            && self.fname == other.fname
47    }
48}
49
50pub struct PlottersBackend {}
51
52impl PlottersBackend {
53    fn plot_modulation_impl<B: plotters::backend::DrawingBackend>(
54        root: DrawingArea<B, Shift>,
55        modulation: Vec<f32>,
56        config: &PlotConfig,
57    ) -> Result<(), crate::error::VisualizerError>
58    where
59        VisualizerError:
60            From<DrawingAreaErrorKind<<B as plotters::backend::DrawingBackend>::ErrorType>>,
61    {
62        root.fill(&WHITE)?;
63
64        let mut chart = ChartBuilder::on(&root)
65            .margin(config.margin)
66            .x_label_area_size(config.label_area_size)
67            .y_label_area_size(config.label_area_size)
68            .build_cartesian_2d::<_, std::ops::Range<f32>>(0..modulation.len(), 0.0..1.0)?;
69
70        chart
71            .configure_mesh()
72            .disable_x_mesh()
73            .disable_y_mesh()
74            .x_label_style(("sans-serif", config.font_size).into_text_style(&root))
75            .y_label_style(("sans-serif", config.font_size).into_text_style(&root))
76            .x_desc("Index")
77            .y_desc("Modulation")
78            .draw()?;
79
80        chart.draw_series(LineSeries::new(
81            modulation.iter().enumerate().map(|(i, &v)| (i, v)),
82            BLUE.stroke_width(2),
83        ))?;
84
85        root.present()?;
86
87        Ok(())
88    }
89
90    fn plot_1d_impl<B: plotters::backend::DrawingBackend>(
91        root: &DrawingArea<B, Shift>,
92        observe_points: &[f32],
93        acoustic_pressures: &[autd3_driver::defined::Complex],
94        x_label: &str,
95        yrange: (f32, f32),
96        config: &PlotConfig,
97    ) -> Result<(), crate::error::VisualizerError>
98    where
99        VisualizerError:
100            From<DrawingAreaErrorKind<<B as plotters::backend::DrawingBackend>::ErrorType>>,
101    {
102        root.fill(&WHITE)?;
103
104        let xrange = observe_points
105            .iter()
106            .fold((f32::MAX, f32::MIN), |acc, &x| (acc.0.min(x), acc.1.max(x)));
107
108        let x_labels = ((xrange.1 - xrange.0).floor() / config.ticks_step) as usize + 1;
109
110        let mut chart = ChartBuilder::on(root)
111            .margin(config.margin)
112            .x_label_area_size(config.label_area_size)
113            .y_label_area_size(config.label_area_size)
114            .build_cartesian_2d(xrange.0..xrange.1, yrange.0..yrange.1)?;
115
116        chart
117            .configure_mesh()
118            .disable_x_mesh()
119            .disable_y_mesh()
120            .x_labels(x_labels)
121            .x_label_style(("sans-serif", config.font_size).into_text_style(root))
122            .y_label_style(("sans-serif", config.font_size).into_text_style(root))
123            .x_desc(x_label)
124            .y_desc("Amplitude [-]")
125            .draw()?;
126
127        chart.draw_series(LineSeries::new(
128            observe_points
129                .iter()
130                .zip(acoustic_pressures.iter())
131                .map(|(&x, v)| (x, v.norm())),
132            BLUE.stroke_width(2),
133        ))?;
134
135        root.present()?;
136
137        Ok(())
138    }
139
140    #[allow(clippy::too_many_arguments)]
141    fn plot_2d_impl<B: plotters::backend::DrawingBackend>(
142        root: &DrawingArea<B, Shift>,
143        observe_points_x: &[f32],
144        observe_points_y: &[f32],
145        acoustic_pressures: &[autd3_driver::defined::Complex],
146        x_label: &str,
147        y_label: &str,
148        zrange: (f32, f32),
149        resolution: f32,
150        config: &PlotConfig,
151    ) -> Result<(), crate::error::VisualizerError>
152    where
153        VisualizerError:
154            From<DrawingAreaErrorKind<<B as plotters::backend::DrawingBackend>::ErrorType>>,
155    {
156        root.fill(&WHITE)?;
157
158        let main_area_size_x = (config.figsize.0 as f32 * (1.0 - config.cbar_size)) as u32;
159
160        let (main_area, cbar_area) = root.split_horizontally(main_area_size_x);
161
162        let color_map_size = 1000;
163        let cmap: Vec<scarlet::color::RGBColor> = config
164            .cmap
165            .transform((0..=color_map_size).map(|x| x as f64 / color_map_size as f64));
166
167        {
168            let xrange = observe_points_x
169                .iter()
170                .fold((f32::MAX, f32::MIN), |acc, &x| (acc.0.min(x), acc.1.max(x)));
171            let yrange = observe_points_y
172                .iter()
173                .fold((f32::MAX, f32::MIN), |acc, &x| (acc.0.min(x), acc.1.max(x)));
174
175            let plot_range_x = xrange.1 - xrange.0;
176            let plot_range_y = yrange.1 - yrange.0;
177
178            let x_labels = (plot_range_x.floor() / config.ticks_step) as usize + 1;
179            let y_labels = (plot_range_y.floor() / config.ticks_step) as usize + 1;
180
181            let available_size_x = main_area_size_x - config.label_area_size - config.margin;
182            let available_size_y = config.figsize.1 - config.label_area_size - config.margin * 2;
183
184            let px_per_ps = (available_size_x as f32 / plot_range_x)
185                .min(available_size_y as f32 / plot_range_y);
186
187            let plot_size_x = (plot_range_x * px_per_ps) as u32;
188            let plot_size_y = (plot_range_y * px_per_ps) as u32;
189
190            let left_margin = config.margin
191                + (main_area_size_x - plot_size_x - config.label_area_size - config.margin).max(0)
192                    / 2;
193            let right_margin = config.margin
194                + (main_area_size_x - plot_size_x - config.label_area_size - config.margin).max(0)
195                    / 2;
196            let top_margin = config.margin
197                + (config.figsize.1 - plot_size_y - config.label_area_size - config.margin).max(0)
198                    / 2;
199            let bottom_margin = config.margin
200                + (config.figsize.1 - plot_size_y - config.label_area_size - config.margin).max(0)
201                    / 2;
202
203            let mut chart = ChartBuilder::on(&main_area)
204                .margin_left(left_margin)
205                .margin_top(top_margin)
206                .margin_bottom(bottom_margin)
207                .margin_right(right_margin)
208                .x_label_area_size(config.label_area_size)
209                .y_label_area_size(config.label_area_size)
210                .build_cartesian_2d(xrange.0..xrange.1, yrange.0..yrange.1)?;
211
212            chart
213                .configure_mesh()
214                .x_labels(x_labels)
215                .y_labels(y_labels)
216                .disable_x_mesh()
217                .disable_y_mesh()
218                .label_style(("sans-serif", config.font_size))
219                .x_desc(x_label)
220                .y_desc(y_label)
221                .draw()?;
222
223            chart.draw_series(
224                itertools::iproduct!(observe_points_y, observe_points_x)
225                    .zip(acoustic_pressures.iter())
226                    .map(|((&y, &x), c)| {
227                        #[allow(clippy::unnecessary_cast)]
228                        let c: scarlet::color::RGBColor = config.cmap.transform_single(
229                            ((c.norm() - zrange.0) / (zrange.1 - zrange.0)) as f64,
230                        );
231                        Rectangle::new(
232                            [(x, y), (x + resolution, y + resolution)],
233                            RGBAColor(c.int_r(), c.int_g(), c.int_b(), 1.0).filled(),
234                        )
235                    }),
236            )?;
237        }
238
239        {
240            let mut chart = ChartBuilder::on(&cbar_area)
241                .margin_left(config.margin)
242                .margin_top(config.margin)
243                .margin_bottom(config.margin + config.label_area_size)
244                .margin_right(config.margin)
245                .y_label_area_size(config.label_area_size)
246                .set_label_area_size(LabelAreaPosition::Left, 0)
247                .set_label_area_size(LabelAreaPosition::Right, 80)
248                .build_cartesian_2d(0i32..1i32, 0i32..color_map_size)?;
249
250            chart
251                .configure_mesh()
252                .disable_x_axis()
253                .disable_x_mesh()
254                .disable_y_mesh()
255                .axis_style(BLACK.stroke_width(1))
256                .label_style(("sans-serif", config.font_size))
257                .y_label_formatter(&|&v| {
258                    format!(
259                        "{:.2}",
260                        zrange.0 + (zrange.1 - zrange.0) * v as f32 / color_map_size as f32
261                    )
262                })
263                .y_desc("Amplitude [-]")
264                .draw()?;
265
266            chart.draw_series(cmap.iter().enumerate().map(|(i, c)| {
267                Rectangle::new(
268                    [(0, i as i32), (1, i as i32 + 1)],
269                    RGBAColor(c.int_r(), c.int_g(), c.int_b(), 1.0).filled(),
270                )
271            }))?;
272
273            chart.draw_series([Rectangle::new(
274                [(0, 0), (1, color_map_size + 1)],
275                BLACK.stroke_width(1),
276            )])?;
277        }
278
279        root.present()?;
280
281        Ok(())
282    }
283
284    fn plot_phase_impl<B: plotters::backend::DrawingBackend>(
285        root: DrawingArea<B, Shift>,
286        config: &PlotConfig,
287        geometry: &Geometry,
288        phases: Vec<f32>,
289    ) -> Result<(), crate::error::VisualizerError>
290    where
291        VisualizerError:
292            From<DrawingAreaErrorKind<<B as plotters::backend::DrawingBackend>::ErrorType>>,
293    {
294        root.fill(&WHITE)?;
295
296        let main_area_size_x = (config.figsize.0 as f32 * (1.0 - config.cbar_size)) as u32;
297
298        let (main_area, cbar_area) = root.split_horizontally(main_area_size_x);
299
300        let color_map_size = 1000;
301        let cmap: Vec<scarlet::color::RGBColor> = config
302            .cmap
303            .transform((0..=color_map_size).map(|x| x as f64 / color_map_size as f64));
304
305        {
306            let p = geometry
307                .iter()
308                .flat_map(|dev| dev.iter().map(|t| (t.position().x, t.position().y)))
309                .collect::<Vec<_>>();
310
311            let min_x =
312                p.iter().fold(f32::MAX, |acc, &(x, _)| acc.min(x)) - AUTD3::TRANS_SPACING / 2.0;
313            let min_y =
314                p.iter().fold(f32::MAX, |acc, &(_, y)| acc.min(y)) - AUTD3::TRANS_SPACING / 2.0;
315            let max_x =
316                p.iter().fold(f32::MIN, |acc, &(x, _)| acc.max(x)) + AUTD3::TRANS_SPACING / 2.0;
317            let max_y =
318                p.iter().fold(f32::MIN, |acc, &(_, y)| acc.max(y)) + AUTD3::TRANS_SPACING / 2.0;
319
320            let plot_range_x = max_x - min_x;
321            let plot_range_y = max_y - min_y;
322
323            let available_size_x = main_area_size_x - config.label_area_size - config.margin;
324            let available_size_y = config.figsize.1 - config.label_area_size - config.margin * 2;
325
326            let px_per_ps = (available_size_x as f32 / plot_range_x)
327                .min(available_size_y as f32 / plot_range_y);
328
329            let plot_size_x = (plot_range_x * px_per_ps) as u32;
330            let plot_size_y = (plot_range_y * px_per_ps) as u32;
331
332            let left_margin = config.margin
333                + (main_area_size_x - plot_size_x - config.label_area_size - config.margin).max(0)
334                    / 2;
335            let right_margin = config.margin
336                + (main_area_size_x - plot_size_x - config.label_area_size - config.margin).max(0)
337                    / 2;
338            let top_margin = config.margin
339                + (config.figsize.1 - plot_size_y - config.label_area_size - config.margin).max(0)
340                    / 2;
341            let bottom_margin = config.margin
342                + (config.figsize.1 - plot_size_y - config.label_area_size - config.margin).max(0)
343                    / 2;
344
345            let mut scatter_ctx = ChartBuilder::on(&main_area)
346                .margin_left(left_margin)
347                .margin_right(right_margin)
348                .margin_top(top_margin)
349                .margin_bottom(bottom_margin)
350                .x_label_area_size(config.label_area_size)
351                .y_label_area_size(config.label_area_size)
352                .build_cartesian_2d(min_x..max_x, min_y..max_y)?;
353            scatter_ctx
354                .configure_mesh()
355                .disable_x_mesh()
356                .disable_y_mesh()
357                .x_label_formatter(&|v| format!("{:.1}", v))
358                .y_label_formatter(&|v| format!("{:.1}", v))
359                .x_label_style(("sans-serif", config.font_size).into_text_style(&main_area))
360                .y_label_style(("sans-serif", config.font_size).into_text_style(&main_area))
361                .x_desc("x [mm]")
362                .y_desc("y [mm]")
363                .draw()?;
364
365            scatter_ctx.draw_series(p.iter().zip(phases.iter()).map(|(&(x, y), &p)| {
366                let v = (p / (2.0 * autd3_driver::defined::PI)) % 1.;
367                let c = cmap[((v * color_map_size as f32) as usize).clamp(0, cmap.len() - 1)];
368                Circle::new(
369                    (x, y),
370                    AUTD3::TRANS_SPACING * px_per_ps / 2.0,
371                    RGBColor(c.int_r(), c.int_g(), c.int_b())
372                        .filled()
373                        .stroke_width(0),
374                )
375            }))?;
376        }
377
378        {
379            let mut chart = ChartBuilder::on(&cbar_area)
380                .margin_left(config.margin)
381                .margin_top(config.margin)
382                .margin_bottom(config.margin + config.label_area_size)
383                .margin_right(config.margin)
384                .y_label_area_size(config.label_area_size)
385                .set_label_area_size(LabelAreaPosition::Left, 0)
386                .set_label_area_size(LabelAreaPosition::Right, 80)
387                .build_cartesian_2d(0i32..1i32, 0i32..color_map_size)?;
388
389            chart
390                .configure_mesh()
391                .disable_x_axis()
392                .y_labels(3)
393                .disable_x_mesh()
394                .disable_y_mesh()
395                .axis_style(BLACK.stroke_width(1))
396                .label_style(("sans-serif", config.font_size))
397                .y_label_formatter(&|&v| {
398                    if v == 0 {
399                        "0".to_owned()
400                    } else if v == color_map_size / 2 {
401                        "π".to_owned()
402                    } else {
403                        "2π".to_owned()
404                    }
405                })
406                .draw()?;
407
408            chart.draw_series(cmap.iter().enumerate().map(|(i, c)| {
409                Rectangle::new(
410                    [(0, i as i32), (1, i as i32 + 1)],
411                    RGBAColor(c.int_r(), c.int_g(), c.int_b(), 1.0).filled(),
412                )
413            }))?;
414
415            chart.draw_series([Rectangle::new(
416                [(0, 0), (1, color_map_size + 1)],
417                BLACK.stroke_width(1),
418            )])?;
419        }
420
421        root.present()?;
422
423        Ok(())
424    }
425}
426
427impl Backend for PlottersBackend {
428    type PlotConfig = PlotConfig;
429
430    fn new() -> Self {
431        Self {}
432    }
433
434    fn initialize(&mut self) -> Result<(), crate::error::VisualizerError> {
435        Ok(())
436    }
437
438    fn plot_1d(
439        observe_points: Vec<f32>,
440        acoustic_pressures: Vec<autd3_driver::defined::Complex>,
441        _resolution: f32,
442        x_label: &str,
443        config: Self::PlotConfig,
444    ) -> Result<(), crate::error::VisualizerError> {
445        let path = std::path::Path::new(&config.fname);
446        if !path.parent().map_or(true, |p| p.exists()) {
447            std::fs::create_dir_all(path.parent().unwrap())?;
448        }
449
450        let yrange = acoustic_pressures
451            .iter()
452            .fold((f32::MAX, f32::MIN), |acc, &x| {
453                (acc.0.min(x.norm()), acc.1.max(x.norm()))
454            });
455
456        if path.extension().map_or(false, |e| e == "svg") {
457            Self::plot_1d_impl(
458                &SVGBackend::new(&config.fname, config.figsize).into_drawing_area(),
459                &observe_points,
460                &acoustic_pressures,
461                x_label,
462                yrange,
463                &config,
464            )
465        } else {
466            Self::plot_1d_impl(
467                &BitMapBackend::new(&config.fname, config.figsize).into_drawing_area(),
468                &observe_points,
469                &acoustic_pressures,
470                x_label,
471                yrange,
472                &config,
473            )
474        }
475    }
476
477    fn plot_2d(
478        observe_x: Vec<f32>,
479        observe_y: Vec<f32>,
480        acoustic_pressures: Vec<autd3_driver::defined::Complex>,
481        resolution: f32,
482        x_label: &str,
483        y_label: &str,
484        config: Self::PlotConfig,
485    ) -> Result<(), crate::error::VisualizerError> {
486        let path = std::path::Path::new(&config.fname);
487        if !path.parent().map_or(true, |p| p.exists()) {
488            std::fs::create_dir_all(path.parent().unwrap())?;
489        }
490
491        let zrange = acoustic_pressures
492            .iter()
493            .fold((f32::MAX, f32::MIN), |acc, &x| {
494                (acc.0.min(x.norm()), acc.1.max(x.norm()))
495            });
496
497        if path.extension().map_or(false, |e| e == "svg") {
498            Self::plot_2d_impl(
499                &SVGBackend::new(&config.fname, config.figsize).into_drawing_area(),
500                &observe_x,
501                &observe_y,
502                &acoustic_pressures,
503                x_label,
504                y_label,
505                zrange,
506                resolution,
507                &config,
508            )
509        } else {
510            Self::plot_2d_impl(
511                &BitMapBackend::new(&config.fname, config.figsize).into_drawing_area(),
512                &observe_x,
513                &observe_y,
514                &acoustic_pressures,
515                x_label,
516                y_label,
517                zrange,
518                resolution,
519                &config,
520            )
521        }
522    }
523
524    fn plot_modulation(
525        modulation: Vec<f32>,
526        config: Self::PlotConfig,
527    ) -> Result<(), crate::error::VisualizerError> {
528        let path = std::path::Path::new(&config.fname);
529        if !path.parent().map_or(true, |p| p.exists()) {
530            std::fs::create_dir_all(path.parent().unwrap())?;
531        }
532
533        if path.extension().map_or(false, |e| e == "svg") {
534            Self::plot_modulation_impl(
535                SVGBackend::new(&config.fname, config.figsize).into_drawing_area(),
536                modulation,
537                &config,
538            )
539        } else {
540            Self::plot_modulation_impl(
541                BitMapBackend::new(&config.fname, config.figsize).into_drawing_area(),
542                modulation,
543                &config,
544            )
545        }
546    }
547
548    fn plot_phase(
549        config: Self::PlotConfig,
550        geometry: &autd3_driver::geometry::Geometry,
551        phases: Vec<f32>,
552    ) -> Result<(), crate::error::VisualizerError> {
553        let path = std::path::Path::new(&config.fname);
554        if !path.parent().map_or(true, |p| p.exists()) {
555            std::fs::create_dir_all(path.parent().unwrap())?;
556        }
557
558        if path.extension().map_or(false, |e| e == "svg") {
559            Self::plot_phase_impl(
560                SVGBackend::new(&config.fname, config.figsize).into_drawing_area(),
561                &config,
562                geometry,
563                phases,
564            )
565        } else {
566            Self::plot_phase_impl(
567                BitMapBackend::new(&config.fname, config.figsize).into_drawing_area(),
568                &config,
569                geometry,
570                phases,
571            )
572        }
573    }
574}