fastiron_stats/structures/
processed.rs

1//! Processed data structures code
2//!
3//! This module contains code to process, save and optionally plot the raw data provided
4//! by the user.
5
6use std::{
7    fs::{File, OpenOptions},
8    io::Write,
9};
10
11use gnuplot::{
12    AutoOption, AxesCommon, ColorType,
13    Coordinate::Graph,
14    DashType, Figure,
15    LabelOption::{Rotate, TextOffset},
16    MarginSide::{MarginLeft, MarginTop},
17    PaletteType,
18    PlotOption::{Caption, Color, LineStyle, PointSymbol},
19    Tick,
20    TickOption::{Inward, Mirror},
21};
22
23use crate::command_line::ScalingParams;
24
25use super::raw::{
26    correlation, TalliedData, TalliesReport, TimerReport, TimerSV, N_TIMERS, TIMERS_ARR,
27};
28
29//~~~~~~~~~~~~~~~~~
30// Comparison data
31//~~~~~~~~~~~~~~~~~
32
33/// Structure used to hold comparison study results.
34pub struct ComparisonResults {
35    /// Old timer data.
36    pub old: TimerReport,
37    /// New timer data.
38    pub new: TimerReport,
39    /// Relative change in percents.
40    pub percents: [f64; N_TIMERS],
41}
42
43impl ComparisonResults {
44    /// Serializing function.
45    pub fn save(&self) {
46        let mut file = OpenOptions::new()
47            .write(true)
48            .create(true)
49            .truncate(true)
50            .open("comparison.csv")
51            .unwrap();
52        writeln!(file, "section,old,new,change").unwrap();
53        TIMERS_ARR.iter().for_each(|section| {
54            writeln!(
55                file,
56                "{},{},{},{}",
57                *section,
58                self.old[*section].total,
59                self.new[*section].total,
60                self.percents[*section as usize]
61            )
62            .unwrap();
63        });
64    }
65
66    /// Plotting function.
67    pub fn plot(&self) {
68        // create figure & adjust characteristics
69        let mut fg = Figure::new();
70        fg.set_terminal("pngcairo size 800, 800", "comparison.png")
71            .set_title("Timers Reports Comparison");
72        // prepare data
73        let x_coords: [usize; N_TIMERS] = core::array::from_fn(|i| i);
74        let x_tics = TIMERS_ARR.map(|section| {
75            Tick::Major(
76                section as usize,
77                AutoOption::Fix(match section {
78                    TimerSV::Main => "Main Program",
79                    TimerSV::PopulationControl => "Population Control",
80                    TimerSV::CycleTracking => "Tracking Phase",
81                    TimerSV::CycleTrackingProcess => "Process Phase",
82                    TimerSV::CycleTrackingSort => "Sorting Phase",
83                    TimerSV::CycleSync => "Sync Phase",
84                }),
85            )
86        });
87        let width = 0.25;
88
89        let old_y: [f64; N_TIMERS] = TIMERS_ARR.map(|t| self.old[t].total / 1.0e6);
90        let new_y: [f64; N_TIMERS] = TIMERS_ARR.map(|t| self.new[t].total / 1.0e6);
91        let new_color = if self.percents[0] > 0.0 {
92            Color(ColorType::RGBString("#FF0000")) // total exec time increase => red
93        } else {
94            Color(ColorType::RGBString("#00BB00")) // total exec time decrease => green
95        };
96
97        // plot data
98        fg.axes2d()
99            .set_x_ticks_custom(x_tics, &[Inward(false), Mirror(false)], &[Rotate(-45.0)])
100            .set_x_label("Section", &[TextOffset(0.0, 1.5)])
101            .set_y_grid(true)
102            .set_y_minor_grid(true)
103            .set_y_label("Total Time Spent in Section (s)", &[])
104            .set_y_log(Some(10.0))
105            .boxes(
106                x_coords.iter().map(|x| *x as f64 - width / 2.0),
107                old_y,
108                &[Caption("Old times"), Color(ColorType::RGBString("#000077"))],
109            )
110            .boxes(
111                x_coords.iter().map(|x| *x as f64 + width / 2.0),
112                new_y,
113                &[Caption("New times"), new_color],
114            )
115            .set_box_width(width, false);
116
117        fg.show().unwrap();
118    }
119}
120
121/// Custom [`From`] implementation used to process the raw data at initialization.
122impl From<(TimerReport, TimerReport)> for ComparisonResults {
123    fn from((old, new): (TimerReport, TimerReport)) -> Self {
124        let relative_change =
125            |section: TimerSV| (new[section].mean - old[section].mean) / old[section].mean;
126
127        let percents = [
128            TimerSV::Main,
129            TimerSV::PopulationControl,
130            TimerSV::CycleTracking,
131            TimerSV::CycleTrackingProcess,
132            TimerSV::CycleTrackingSort,
133            TimerSV::CycleSync,
134        ]
135        .map(|section| relative_change(section) * 100.0);
136
137        Self { old, new, percents }
138    }
139}
140
141//~~~~~~~~~~~~~~~~~~
142// Correlation data
143//~~~~~~~~~~~~~~~~~~
144
145/// Columns of the computed correlation matrix.
146pub const CORRELATION_COLS: [TalliedData; 11] = [
147    TalliedData::Start,
148    TalliedData::Rr,
149    TalliedData::Split,
150    TalliedData::Absorb,
151    TalliedData::Scatter,
152    TalliedData::Fission,
153    TalliedData::Produce,
154    TalliedData::Collision,
155    TalliedData::Escape,
156    TalliedData::Census,
157    TalliedData::NumSeg,
158];
159
160/// Rows of the computed correlation matrix.
161pub const CORRELATION_ROWS: [TalliedData; 4] = [
162    TalliedData::CycleSync,
163    TalliedData::CycleTracking,
164    TalliedData::PopulationControl,
165    TalliedData::Cycle,
166];
167
168/// Structure used to hold correlation study results.
169pub struct CorrelationResults {
170    /// Raw data.
171    pub corr_data: [f64; 11 * 4],
172}
173
174impl CorrelationResults {
175    /// Serializing function.
176    pub fn save(&self) {
177        let mut file = OpenOptions::new()
178            .write(true)
179            .create(true)
180            .truncate(true)
181            .open("correlation.csv")
182            .unwrap();
183        writeln!(
184            file,
185            ",Start,Rr,Split,Absorb,Scatter,Fission,Produce,Collision,Escape,Census,NumSeg"
186        )
187        .unwrap();
188        (0..4).for_each(|idx: usize| {
189            writeln!(
190                file,
191                "{},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6},{:.6}",
192                match idx {
193                    0 => "CycleSync",
194                    1 => "CycleTracking",
195                    2 => "PopulationControl",
196                    3 => "Cycle",
197                    _ => unreachable!(),
198                },
199                self.corr_data[11 * idx],
200                self.corr_data[1 + 11 * idx],
201                self.corr_data[2 + 11 * idx],
202                self.corr_data[3 + 11 * idx],
203                self.corr_data[4 + 11 * idx],
204                self.corr_data[5 + 11 * idx],
205                self.corr_data[6 + 11 * idx],
206                self.corr_data[7 + 11 * idx],
207                self.corr_data[8 + 11 * idx],
208                self.corr_data[9 + 11 * idx],
209                self.corr_data[10 + 11 * idx],
210            )
211            .unwrap();
212        });
213    }
214
215    /// Plotting function.
216    pub fn plot(&self) {
217        // create figure & adjust characteristics
218        let mut fg = Figure::new();
219        fg.set_terminal("pngcairo size 1300, 600", "correlation.png")
220            .set_title("Event Correlation Matrix");
221        // prepare data
222        let n_col = CORRELATION_COLS.len();
223        let n_row = CORRELATION_ROWS.len();
224        let x: [usize; CORRELATION_COLS.len()] = core::array::from_fn(|i| i);
225        let y: [usize; CORRELATION_ROWS.len()] = core::array::from_fn(|i| i);
226        let x_tics = x.map(|event_idx| {
227            Tick::Major(
228                event_idx as f64 - 0.5,
229                AutoOption::Fix(CORRELATION_COLS[event_idx].to_string()),
230            )
231        });
232        let y_tics = y.map(|event_idx| {
233            Tick::Major(
234                event_idx as f64 - 0.5,
235                AutoOption::Fix(CORRELATION_ROWS[event_idx].to_string()),
236            )
237        });
238
239        // plot data
240        fg.axes2d()
241            .set_aspect_ratio(AutoOption::Fix(0.35))
242            .set_margins(&[MarginLeft(0.10)])
243            .set_x_ticks_custom(
244                x_tics,
245                &[Inward(false), Mirror(false)],
246                &[Rotate(-45.0), TextOffset(4.0, -1.0)],
247            )
248            .set_y_ticks_custom(
249                y_tics,
250                &[Inward(false), Mirror(false)],
251                &[Rotate(45.0), TextOffset(0.0, 2.0)],
252            )
253            .set_cb_grid(true)
254            .set_grid_options(true, &[LineStyle(DashType::Solid)])
255            .set_x_grid(true)
256            .set_y_grid(true)
257            .set_palette(PaletteType::Custom(&[
258                (-5.0, 0.0, 0.0, 1.0),
259                (0.0, 1.0, 1.0, 1.0),
260                (5.0, 1.0, 0.0, 0.0),
261            ]))
262            .image(self.corr_data, n_row, n_col, None, &[]);
263
264        fg.show().unwrap();
265    }
266}
267
268/// Custom [`From`] implementation used to process the raw data at initialization.
269impl From<TalliesReport> for CorrelationResults {
270    fn from(report: TalliesReport) -> Self {
271        // compute correlations
272        let table: [[f64; 11]; 4] = CORRELATION_ROWS.map(|tallied_data| {
273            CORRELATION_COLS
274                .map(|tallied_event| correlation(&report[tallied_data], &report[tallied_event]))
275        });
276
277        // a little black magic to flatten the array
278        let flat_table: &[f64; 11 * 4] = unsafe { std::mem::transmute(&table) };
279
280        Self {
281            corr_data: *flat_table,
282        }
283    }
284}
285
286//~~~~~~~~~~~~~~
287// Scaling data
288//~~~~~~~~~~~~~~
289
290/// Enum used to represent the type of scaling study.
291pub enum ScalingType {
292    /// Weak scaling, i.e. the size of the problem grows with the number of threads.
293    Weak,
294    /// Strong scaling, i.e. the size of the problem does not grow with the number of threads.
295    Strong(usize),
296}
297
298/// Structure used to hold scaling study results.
299pub struct ScalingResults {
300    /// Number of threads used for each simulation run.
301    pub n_threads: Vec<usize>,
302    /// Total execution time of each simulation run.
303    pub total_exec_times: Vec<f64>,
304    /// Average population control time of each simulation run.
305    pub population_control_avgs: Vec<f64>,
306    /// Average tracking time of each simulation run.
307    pub tracking_avgs: Vec<f64>,
308    /// Average processing time of each simulation run.
309    pub tracking_process_avgs: Vec<f64>,
310    /// Average sorting time of each simulation run.
311    pub tracking_sort_avgs: Vec<f64>,
312    /// Average synchronization time of each simulation run.
313    pub sync_avgs: Vec<f64>,
314    /// Scaling type.
315    pub scaling_type: ScalingType,
316}
317
318impl ScalingResults {
319    /// Serializing function.
320    pub fn save_tracking(&self) {
321        let mut file = OpenOptions::new()
322            .write(true)
323            .create(true)
324            .truncate(true)
325            .open(match self.scaling_type {
326                ScalingType::Weak => "weak_scaling_tracking.csv",
327                ScalingType::Strong(_) => "strong_scaling_tracking.csv",
328            })
329            .unwrap();
330        writeln!(
331            file,
332            "n_threads,TrackingAvg,TrackingAvgIdeal,TrackingProcessAvg,TrackingSortAvg"
333        )
334        .unwrap();
335        let avg_ref = self.tracking_avgs[0];
336        let n_elem = self.n_threads.len();
337        assert_eq!(self.tracking_avgs.len(), n_elem);
338        assert_eq!(self.tracking_process_avgs.len(), n_elem);
339        assert_eq!(self.tracking_sort_avgs.len(), n_elem);
340        for idx in 0..n_elem {
341            let ideal = match self.scaling_type {
342                ScalingType::Weak => avg_ref,
343                ScalingType::Strong(factor) => avg_ref / (factor.pow(idx as u32) as f64),
344            };
345            writeln!(
346                file,
347                "{},{},{},{},{}",
348                self.n_threads[idx],
349                self.tracking_avgs[idx],
350                ideal,
351                self.tracking_process_avgs[idx],
352                self.tracking_sort_avgs[idx]
353            )
354            .unwrap();
355        }
356    }
357
358    /// Serializing function.
359    pub fn save_others(&self) {
360        let mut file = OpenOptions::new()
361            .write(true)
362            .create(true)
363            .truncate(true)
364            .open(match self.scaling_type {
365                ScalingType::Weak => "weak_scaling_others.csv",
366                ScalingType::Strong(_) => "strong_scaling_others.csv",
367            })
368            .unwrap();
369        writeln!(file, "n_threads,TotalExecTime,PopulationControlAvg,SyncAvg").unwrap();
370        let n_elem = self.n_threads.len();
371        assert_eq!(self.total_exec_times.len(), n_elem);
372        assert_eq!(self.population_control_avgs.len(), n_elem);
373        assert_eq!(self.sync_avgs.len(), n_elem);
374        for idx in 0..n_elem {
375            writeln!(
376                file,
377                "{},{},{},{}",
378                self.n_threads[idx],
379                self.total_exec_times[idx],
380                self.population_control_avgs[idx],
381                self.sync_avgs[idx]
382            )
383            .unwrap();
384        }
385    }
386
387    /// Computes & plot the speedup / efficiency (depending on the scaling type).
388    pub fn plot_se(&self) {
389        let mut fg = Figure::new();
390        let (out, title) = match self.scaling_type {
391            ScalingType::Weak => ("efficiency.png", "Ideal vs Real Efficiency"),
392            ScalingType::Strong(_) => ("speedup.png", "Ideal vs Real Speedup"),
393        };
394        fg.set_terminal("pngcairo", out).set_title(title);
395
396        let y_ideal: Vec<f64> = self
397            .n_threads
398            .iter()
399            .map(|n_thread| match self.scaling_type {
400                ScalingType::Weak => 1.0,
401                ScalingType::Strong(_) => *n_thread as f64,
402            })
403            .collect();
404
405        let y_real: Vec<f64> = self
406            .n_threads
407            .iter()
408            .enumerate()
409            .map(|(idx, _)| self.tracking_avgs[0] / self.tracking_avgs[idx])
410            .collect();
411
412        fg.axes2d()
413            .set_x_log(Some(self.n_threads[1] as f64 / self.n_threads[0] as f64))
414            .set_x_label("Number of Threads Used for Execution", &[])
415            .set_x_ticks(Some((AutoOption::Auto, 0)), &[Inward(false)], &[])
416            .set_y_log(match self.scaling_type {
417                ScalingType::Weak => None,
418                ScalingType::Strong(factor) => Some(factor as f64),
419            })
420            .set_y_label(
421                match self.scaling_type {
422                    ScalingType::Weak => "Efficiency",
423                    ScalingType::Strong(_) => "Speedup",
424                },
425                &[],
426            )
427            .set_y_grid(true)
428            .lines_points(
429                &self.n_threads,
430                &y_ideal,
431                &[
432                    Caption("Ideal"),
433                    Color(ColorType::RGBString("#00FF00")),
434                    PointSymbol('x'),
435                ],
436            )
437            .lines_points(
438                &self.n_threads,
439                &y_real,
440                &[
441                    Caption("Real"),
442                    Color(ColorType::RGBString("#008800")),
443                    PointSymbol('x'),
444                ],
445            );
446
447        fg.show().unwrap();
448    }
449
450    /// Plotting function.
451    pub fn plot_tracking(&self) {
452        // create figure & adjust characteristics
453        let mut fg = Figure::new();
454        let (out, title) = match self.scaling_type {
455            ScalingType::Weak => ("weak_scaling_tracking.png", "Weak Scaling Characteristics"),
456            ScalingType::Strong(_) => (
457                "strong_scaling_tracking.png",
458                "Strong Scaling Characteristics",
459            ),
460        };
461        fg.set_terminal("pngcairo", out).set_title(title);
462        // prepare data
463        // let x = self.n_threads.clone();
464        let y_ideal: Vec<f64> = self
465            .n_threads
466            .iter()
467            .enumerate()
468            .map(|(idx, _)| match self.scaling_type {
469                ScalingType::Weak => self.tracking_avgs[0],
470                ScalingType::Strong(factor) => {
471                    self.tracking_avgs[0] / (factor.pow(idx as u32) as f64)
472                }
473            })
474            .collect();
475
476        // plot data
477        fg.axes2d()
478            .set_x_log(Some(self.n_threads[1] as f64 / self.n_threads[0] as f64))
479            .set_x_label("Number of Threads Used for Execution", &[])
480            .set_x_ticks(Some((AutoOption::Auto, 0)), &[Inward(false)], &[])
481            .set_y_log(match self.scaling_type {
482                ScalingType::Weak => None,
483                ScalingType::Strong(_) => Some(self.n_threads[1] as f64 / self.n_threads[0] as f64),
484            })
485            .set_y_label("Time (µs)", &[])
486            .set_y_grid(true)
487            .lines_points(
488                &self.n_threads,
489                &y_ideal,
490                &[
491                    Caption("Ideal Average Tracking time"),
492                    Color(ColorType::RGBString("#00FF00")),
493                    PointSymbol('x'),
494                ],
495            )
496            .lines_points(
497                &self.n_threads,
498                &self.tracking_avgs,
499                &[
500                    Caption("Average Tracking time"),
501                    Color(ColorType::RGBString("#008800")),
502                    PointSymbol('x'),
503                ],
504            )
505            .lines_points(
506                &self.n_threads,
507                &self.tracking_process_avgs,
508                &[
509                    Caption("Average Processing time"),
510                    Color(ColorType::RGBString("#CCCC00")),
511                    PointSymbol('x'),
512                ],
513            )
514            .lines_points(
515                &self.n_threads,
516                &self.tracking_sort_avgs,
517                &[
518                    Caption("Average Sorting time"),
519                    Color(ColorType::RGBString("#999900")),
520                    PointSymbol('x'),
521                ],
522            );
523
524        fg.show().unwrap();
525    }
526
527    /// Plotting function.
528    pub fn plot_others(&self) {
529        // create figure & adjust characteristics
530        let mut fg = Figure::new();
531        let (out, title) = match self.scaling_type {
532            ScalingType::Weak => ("weak_scaling_others.png", "Weak Scaling Characteristics"),
533            ScalingType::Strong(_) => (
534                "strong_scaling_others.png",
535                "Strong Scaling Characteristics",
536            ),
537        };
538        fg.set_terminal("pngcairo", out).set_title(title);
539
540        // plot data
541        fg.axes2d()
542            .set_x_log(Some(self.n_threads[1] as f64 / self.n_threads[0] as f64))
543            .set_x_label("Number of Threads Used for Execution", &[])
544            .set_x_ticks(
545                Some((AutoOption::Auto, 0)),
546                &[Inward(false), Mirror(false)],
547                &[],
548            )
549            .set_y_range(AutoOption::Auto, AutoOption::Auto)
550            .set_y_label("Time (µs)", &[])
551            .set_y_grid(true)
552            .set_margins(&[MarginTop(0.8)])
553            .set_legend(Graph(1.0), Graph(1.15), &[], &[])
554            .lines_points(
555                &self.n_threads,
556                &self.population_control_avgs,
557                &[
558                    Caption("Average Pop. Control Time"),
559                    Color(ColorType::RGBString("#00AA00")),
560                    PointSymbol('x'),
561                ],
562            )
563            .lines_points(
564                &self.n_threads,
565                &self.sync_avgs,
566                &[
567                    Caption("Average Synchronization Time"),
568                    Color(ColorType::RGBString("#AAAA00")),
569                    PointSymbol('x'),
570                ],
571            );
572
573        fg.show().unwrap();
574    }
575}
576
577/// Custom [`From`] implementation used to process the raw data at initialization.
578impl From<(&str, &ScalingParams, ScalingType)> for ScalingResults {
579    fn from((root_path, params, scaling_type): (&str, &ScalingParams, ScalingType)) -> Self {
580        // fetch data from files
581        let n_threads: Vec<usize> = (0..params.t_iter.unwrap())
582            .map(|idx| params.t_init.unwrap() * params.t_factor.unwrap().pow(idx as u32))
583            .collect();
584        let reports: Vec<TimerReport> = n_threads
585            .iter()
586            .map(|n_thread| {
587                let filename = format!("{}{}.csv", root_path, n_thread);
588                TimerReport::from(File::open(filename).unwrap())
589            })
590            .collect();
591
592        // use data to init structure
593        let mut total_exec_times: Vec<f64> = Vec::with_capacity(n_threads.len());
594        let mut population_control_avgs: Vec<f64> = Vec::with_capacity(n_threads.len());
595        let mut tracking_avgs: Vec<f64> = Vec::with_capacity(n_threads.len());
596        let mut tracking_process_avgs: Vec<f64> = Vec::with_capacity(n_threads.len());
597        let mut tracking_sort_avgs: Vec<f64> = Vec::with_capacity(n_threads.len());
598        let mut sync_avgs: Vec<f64> = Vec::with_capacity(n_threads.len());
599
600        reports.iter().for_each(|report| {
601            total_exec_times.push(report[TimerSV::Main].mean);
602            population_control_avgs.push(report[TimerSV::PopulationControl].mean);
603            tracking_avgs.push(report[TimerSV::CycleTracking].mean);
604            tracking_process_avgs.push(report[TimerSV::CycleTrackingProcess].mean);
605            tracking_sort_avgs.push(report[TimerSV::CycleTrackingSort].mean);
606            sync_avgs.push(report[TimerSV::CycleSync].mean);
607        });
608
609        Self {
610            n_threads,
611            total_exec_times,
612            population_control_avgs,
613            tracking_avgs,
614            tracking_process_avgs,
615            tracking_sort_avgs,
616            sync_avgs,
617            scaling_type,
618        }
619    }
620}