1use num_traits::Float;
7use std::fmt::Debug;
8use tabled::{Table, Tabled, settings::Style};
9
10use crate::stats::Stats;
11
12pub fn rel_err<T: Float>(a: T, b: T) -> f64 {
14 let err_a = ((a - b) / a).abs();
15 let err_b = ((a - b) / b).abs();
16
17 let rel_err = err_a.max(err_b);
18 (rel_err / T::epsilon())
19 .to_f64()
20 .expect("Cannot convert to f64")
21}
22
23#[derive(Debug, Clone)]
24pub struct Case<T: Float> {
25 pub inputs: Vec<T>,
26 pub expected: T,
27}
28
29pub fn compute_errors_from_cases<T: Float + Debug>(
30 func: &dyn Fn(&Vec<T>) -> T,
31 cases: Vec<Case<T>>,
32) -> Vec<f64> {
33 cases
34 .iter()
35 .map(|case| {
36 if case.expected.is_finite() {
37 let res = func(&case.inputs);
38 let err = rel_err(res, case.expected);
39 err
46 } else {
47 f64::NAN
48 }
49 })
50 .collect()
51}
52
53pub fn format_float(value: &f64) -> String {
54 if value.is_nan() {
55 "NAN".to_string()
56 } else if *value >= 1e3 {
57 format!("{:.2e}", value)
58 } else {
59 format!("{:.2}", value)
60 }
61}
62
63fn format_mu(value: &u64) -> String {
64 if *value >= 10000 {
65 format!("{:e}", value)
66 } else {
67 format!("{}", value)
68 }
69}
70
71#[derive(Tabled)]
72pub struct ErrorEntry<'a> {
73 #[tabled(rename = "Function")]
74 name: &'a str,
75 #[tabled(rename = "Mean (ε)", display = "format_float")]
76 mean: f64,
77 #[tabled(rename = "Median (ε)", display = "format_float")]
78 median: f64,
79 #[tabled(rename = "P99 (ε)", display = "format_float")]
80 p99: f64,
81 #[tabled(rename = "Max (ε)", display = "format_float")]
82 max: f64,
83 #[tabled(rename = "Variance (ε²)", display = "format_float")]
84 variance: f64,
85 #[tabled(rename = "μ (ε²)", display = "format_mu")]
86 mu: u64,
87}
88
89pub fn generate_error_entry_from_file<T: Float + Debug>(
90 file_path: &str,
91 func: &dyn Fn(&Vec<T>) -> T,
92) -> Stats {
93 let result = crate::parser::read_wolfram_data(file_path);
94 match result {
95 Ok(cases) => Stats::from_vec(&compute_errors_from_cases(func, cases)),
96 Err(_) => Stats::nan(),
97 }
98}
99
100pub fn generate_error_table(entries: &[(&str, u64, Stats, Stats)]) -> [String; 2] {
101 let f64_rows: Vec<ErrorEntry> = entries
102 .iter()
103 .map(|(name, mu, f64_stats, _)| ErrorEntry {
104 name,
105 mean: f64_stats.mean,
106 median: f64_stats.median,
107 p99: f64_stats.p99,
108 max: f64_stats.max,
109 variance: f64_stats.variance,
110 mu: *mu,
111 })
112 .collect();
113
114 let f32_rows: Vec<ErrorEntry> = entries
115 .iter()
116 .map(|(name, mu, _, f32_stats)| ErrorEntry {
117 name,
118 mean: f32_stats.mean,
119 median: f32_stats.median,
120 p99: f32_stats.p99,
121 max: f32_stats.max,
122 variance: f32_stats.variance,
123 mu: *mu,
124 })
125 .collect();
126
127 [
128 Table::new(f64_rows).with(Style::markdown()).to_string(),
129 Table::new(f32_rows).with(Style::markdown()).to_string(),
130 ]
131}
132
133#[macro_export]
134macro_rules! get_entry {
135 ($file_name: expr, $name: expr, $func: expr, $arg_count: tt, $mu: expr) => {{
136 let f64_entries = {
137 let file_path_f64 = concat!["tests/data/", $file_name, ".csv"];
138 ellip_dev_utils::func_wrapper!($func, f64, $arg_count);
139 ellip_dev_utils::test_report::generate_error_entry_from_file(
140 &file_path_f64,
141 &wrapped_func,
142 )
143 };
144 let f32_entries = {
145 let file_path_f32 = concat!["tests/data/f32/", $file_name, ".csv"];
146 ellip_dev_utils::func_wrapper!($func, f32, $arg_count);
147 ellip_dev_utils::test_report::generate_error_entry_from_file(
148 &file_path_f32,
149 &wrapped_func,
150 )
151 };
152
153 ($name, $mu, f64_entries, f32_entries)
154 }};
155}
156
157pub fn format_performance(value: &f64) -> String {
158 if value.is_nan() {
159 "NAN".to_string()
160 } else if *value < 1000.0 {
161 format!("{:.1} ns", value)
162 } else if *value < 1_000_000.0 {
163 format!("{:.1} μs", value / 1000.0)
164 } else {
165 format!("{:.1} ms", value / 1_000_000.0)
166 }
167}
168
169#[derive(Tabled)]
170pub struct SummaryEntry<'a> {
171 #[tabled(rename = "Function")]
172 name: &'a str,
173 #[tabled(rename = "Median Error (ε)", display = "format_float")]
174 median_error: f64,
175 #[tabled(rename = "Max Error (ε)", display = "format_float")]
176 max_error: f64,
177 #[tabled(rename = "Mean Performance", display = "format_performance")]
178 mean_performance: f64,
179}
180
181pub fn generate_summary_table(entries: &[(&str, Stats, f64)]) -> String {
182 let rows: Vec<SummaryEntry> = entries
183 .iter()
184 .map(|(name, stats, perf)| SummaryEntry {
185 name,
186 median_error: stats.median,
187 max_error: stats.max,
188 mean_performance: *perf / stats.n as f64,
189 })
190 .collect();
191
192 Table::new(rows).with(Style::markdown()).to_string()
193}
194
195#[macro_export]
196macro_rules! get_summary_entry {
197 ($group:expr, $name:expr, $func:expr, $arg_count:tt, $test_file_name:expr) => {{
198 use ellip_dev_utils::{
199 benchmark, file, parser, stats,
200 test_report::{self, Case},
201 };
202 use std::path::Path;
203
204 ellip_dev_utils::func_wrapper!($func, f64, $arg_count);
205
206 let test_paths = file::find_test_files($test_file_name, "wolfram");
207 let cases = test_paths
208 .iter()
209 .flat_map(|test_path| parser::read_wolfram_data(test_path.to_str().unwrap()).unwrap())
210 .collect::<Vec<Case<f64>>>();
211 let stats = stats::Stats::from_vec(&test_report::compute_errors_from_cases(
212 &wrapped_func,
213 cases,
214 ));
215
216 let estimates_path_buf = Path::new("target/criterion")
218 .join($group)
219 .join(stringify!($func))
220 .join("new")
221 .join("estimates.json");
222 let perf = benchmark::extract_criterion_mean(&estimates_path_buf).unwrap_or(f64::NAN);
223
224 ($name, stats, perf)
225 }};
226 ($group:expr, $name:expr, $func:expr, $arg_count:tt) => {{
227 get_summary_entry! {$group, $name, $func, $arg_count, stringify!($func)}
228 }};
229}