bitbelay_cli/commands/
correlation.rs

1//! A command for running the correlation test suite.
2
3use std::hash::BuildHasher;
4use std::num::NonZeroUsize;
5
6use anyhow::anyhow;
7use anyhow::bail;
8use anyhow::Context;
9use bitbelay_providers::Provider;
10use bitbelay_report::Config;
11use bitbelay_suites::r#trait::Suite;
12use bitbelay_tests::correlation::bitwise;
13use clap::ArgAction;
14use colored::Colorize as _;
15
16/// The first colorstop for a printed correlation matrix.
17const CORRELATION_MATRIX_STOP_ONE: f64 = 0.05;
18
19/// The second colorstop for a printed correlation matrix.
20const CORRELATION_MATRIX_STOP_TWO: f64 = 0.25;
21
22/// The third colorstop for a printed correlation matrix.
23const CORRELATION_MATRIX_STOP_THREE: f64 = 0.50;
24
25/// Arguments for the correlation command.
26#[derive(clap::Args, Debug)]
27pub struct Args {
28    /// The number of iterations to carry out for the Bitwise test.
29    #[arg(short, long, default_value_t = 1 << 16)]
30    iterations: usize,
31
32    /// The threshold of correlation at which any non-diagonal value causes the
33    /// test to fail.
34    #[arg(long, default_value_t = 0.05)]
35    threshold: f64,
36
37    /// Prints the full correlation matrix to the terminal.
38    #[clap(long, action = ArgAction::SetTrue)]
39    correlation_matrix: bool,
40
41    /// Sets the width of each cell in the correlation matrix.
42    #[clap(long, default_value_t = 2)]
43    correlation_matrix_cell_width: usize,
44}
45
46/// The main function for the correlation command.
47pub fn main<H: BuildHasher, const N: usize>(
48    args: Args,
49    build_hasher: H,
50    provider: Box<dyn Provider>,
51) -> anyhow::Result<()> {
52    tracing::info!("Starting correlation test suite.");
53
54    let iterations = NonZeroUsize::try_from(args.iterations)
55        .map_err(|_| anyhow!("--iterations per experiment must be non-zero!"))?;
56
57    let threshold = if (0.0..=1.0).contains(&args.threshold) {
58        args.threshold
59    } else {
60        bail!("--threshold must be between 0.0 and 1.0!");
61    };
62
63    let correlation_matrix_cell_width = NonZeroUsize::try_from(args.correlation_matrix_cell_width)
64        .map_err(|_| anyhow!("--correlation-matrix-cell-width must be non-zero!"))
65        .and_then(|cell_size| {
66            if cell_size.get() >= 2 {
67                Ok(cell_size)
68            } else {
69                Err(anyhow!(
70                    "--correlation-matrix-cell-width must be 2 or greater!"
71                ))
72            }
73        })?;
74
75    let mut suite = bitbelay_suites::correlation::suite::Builder::<H>::default()
76        .build_hasher(&build_hasher)?
77        .try_build::<N>()?;
78
79    suite
80        .run_bitwise_test(provider, iterations, threshold)
81        .with_context(|| "running bitwise test")?;
82
83    suite
84        .report()
85        .write_to(&mut std::io::stderr(), &Config::default())?;
86
87    if args.correlation_matrix {
88        // SAFETY: this first test should always be a bitwise test based on the order of
89        let mut bitwise_tests = suite
90            .tests()
91            .iter()
92            .filter_map(|test| test.as_bitwise_test())
93            .collect::<Vec<_>>();
94
95        match bitwise_tests.len() {
96            0 => bail!(
97                "there should be at least one bitwise test! This is an issue and should be looked \
98                 at by the developers (please report this issue!)"
99            ),
100            1 => print_correlation_table::<N>(
101                correlation_matrix_cell_width.get(),
102                // SAFETY: for the first unwrap, we just checked to ensure there is exactly one
103                // bitwise test, so this will always unwrap.
104                //
105                // SAFETY: for the second unwrap, this command _requires_ that at least
106                // one test iteration is run. As such, this will always unwrap.
107                bitwise_tests.pop().unwrap().results().unwrap(),
108            ),
109            v => bail!(
110                "there are {} bitwise tests, and it's not clear what correlation matrix to print \
111                 (please report this issue!)",
112                v
113            ),
114        }
115    }
116    Ok(())
117}
118
119/// Prints a correlation table to stdout.
120fn print_correlation_table<const N: usize>(width: usize, correlations: bitwise::Results) {
121    if width == 0 {
122        panic!("width of correlation table entries cannot be 0!");
123    }
124
125    // Print the legend.
126    println!("{}", "Legend".bold().underline());
127
128    println!(
129        "{} => a correlation value in the range [0.0, {}]",
130        " ".on_black(),
131        CORRELATION_MATRIX_STOP_ONE
132    );
133    println!(
134        "{} => a correlation value in the range ({}, {}]",
135        " ".on_red(),
136        CORRELATION_MATRIX_STOP_ONE,
137        CORRELATION_MATRIX_STOP_TWO
138    );
139    println!(
140        "{} => a correlation value in the range ({}, {}]",
141        " ".on_yellow(),
142        CORRELATION_MATRIX_STOP_TWO,
143        CORRELATION_MATRIX_STOP_THREE
144    );
145
146    println!(
147        "{} => a correlation value in the range [{}, 1.0]",
148        " ".on_green(),
149        CORRELATION_MATRIX_STOP_THREE,
150    );
151
152    println!();
153
154    // Print the header.
155    for i in 1..=N {
156        if i < 10 || i % 10 == 0 {
157            print!("{:^width$}", i.to_string().bold().underline())
158        } else {
159            print!("{:^width$}", i % 10, width = width);
160        };
161    }
162
163    println!();
164
165    // Print each correlation value.
166    for i in 0..N {
167        for j in 0..N {
168            // SAFETY: for the first unwrap, due to the construction of this [`HashMap`]
169            // always containing correlations of NxN size, this will always
170            // unwrap.
171            let value = *correlations.get(&(i, j)).unwrap();
172            let cell = " ".repeat(width);
173
174            if let Some(value) = value {
175                // The correlation was able to be computed.
176                if value > CORRELATION_MATRIX_STOP_THREE {
177                    print!("{}", cell.on_green());
178                } else if value > CORRELATION_MATRIX_STOP_TWO {
179                    print!("{}", cell.on_yellow());
180                } else if value > CORRELATION_MATRIX_STOP_ONE {
181                    print!("{}", cell.on_red());
182                } else {
183                    print!("{}", cell.on_black());
184                };
185            } else {
186                // The correlation was not able to be computed for some reason.
187                print!("{}", cell.on_bright_purple());
188            }
189        }
190
191        println!();
192    }
193}