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