big_o/
complexity.rs

1use crate::error::Error;
2use crate::linalg;
3use crate::name;
4use crate::name::Name;
5use crate::params::Params;
6
7/// A structure to describe asymptotic computational complexity
8#[derive(Clone, Debug)]
9pub struct Complexity {
10    /// Human-readable name
11    pub name: Name,
12
13    /// Big O notation
14    pub notation: &'static str,
15
16    /// Approximation function parameters
17    pub params: Params,
18
19    /// Relative rank to compare complexities
20    pub rank: u32,
21}
22
23/// Returns a calculated approximation function `f(x)`
24fn get_function(name: Name, params: Params) -> Result<Box<dyn Fn(f64) -> f64>, Error> {
25    if let (Some(a), Some(b)) = match name {
26        Name::Polynomial => (params.gain, params.power),
27        Name::Exponential => (params.gain, params.base),
28        _other => (params.gain, params.offset),
29    } {
30        let f: Box<dyn Fn(f64) -> f64> = match name {
31            Name::Constant => Box::new(move |_x| b),
32            Name::Logarithmic => Box::new(move |x| a * x.ln() + b),
33            Name::Linear => Box::new(move |x| a * x + b),
34            Name::Linearithmic => Box::new(move |x| a * x * x.ln() + b),
35            Name::Quadratic => Box::new(move |x| a * x.powi(2) + b),
36            Name::Cubic => Box::new(move |x| a * x.powi(3) + b),
37            Name::Polynomial => Box::new(move |x| a * x.powf(b)),
38            Name::Exponential => Box::new(move |x| a * b.powf(x)),
39        };
40        Ok(f)
41    } else {
42        Err(Error::MissingFunctionCoeffsError)
43    }
44}
45
46/// Computes values of `f(x)` given `x`
47#[allow(dead_code)]
48fn compute_f(name: Name, params: Params, x: Vec<f64>) -> Result<Vec<f64>, Error> {
49    let f = get_function(name, params)?;
50    let y = x.into_iter().map(f).collect();
51    Ok(y)
52}
53
54pub struct ComplexityBuilder {
55    name: Name,
56    params: Option<Params>,
57}
58
59impl ComplexityBuilder {
60    pub fn new(name: Name) -> Self {
61        Self { name, params: None }
62    }
63
64    #[allow(dead_code)] // Used in tests.
65    pub fn power(&mut self, x: f64) -> &mut Self {
66        self.params = Some(Params::new().power(x).build());
67        self
68    }
69
70    pub fn build(&self) -> Complexity {
71        let mut params = Params::new().build();
72        if let Some(p) = &self.params {
73            params = p.clone();
74        }
75        let rank = rank(self.name, params.clone());
76        Complexity {
77            name: self.name,
78            notation: name::notation(self.name),
79            params,
80            rank,
81        }
82    }
83}
84
85/// Transforms input data into linear complexity.
86fn linearize(name: Name, x: f64, y: f64) -> (f64, f64) {
87    match name {
88        Name::Constant => (0.0, y),
89        Name::Logarithmic => (x.ln(), y),
90        Name::Linear => (x, y),
91        Name::Linearithmic => (x * x.ln(), y),
92        Name::Quadratic => (x.powi(2), y),
93        Name::Cubic => (x.powi(3), y),
94        Name::Polynomial => (x.ln(), y.ln()),
95        Name::Exponential => (x, y.ln()),
96    }
97}
98
99/// Converts linear coeffs `gain` and `offset` to corresponding complexity params.
100fn delinearize(name: Name, gain: f64, offset: f64) -> Params {
101    // Delinearize coeffs.
102    let (a, b) = match name {
103        Name::Polynomial => (offset.exp(), gain),
104        Name::Exponential => (offset.exp(), gain.exp()),
105        _other => (gain, offset),
106    };
107    // Convert coeffs to params.
108    match name {
109        Name::Polynomial => Params::new().gain(a).power(b).build(),
110        Name::Exponential => Params::new().gain(a).base(b).build(),
111        _other => Params::new().gain(a).offset(b).build(),
112    }
113}
114
115fn calculate_residuals(name: Name, params: Params, data: Vec<(f64, f64)>) -> Result<f64, Error> {
116    let f = get_function(name, params)?;
117    let residuals = data.into_iter().map(|(x, y)| (y - f(x)).abs()).sum();
118
119    Ok(residuals)
120}
121
122fn rank(name: Name, params: Params) -> u32 {
123    // Rank is similar to a degree of a corresponding polynomial:
124    // - constant: 0, f(x) = x ^ 0.000
125    // - logarithmic: 130, empirical value k for a big x in f(x) = x ^ k
126    //     base 1_000_000 log of 6 is 0.130
127    //     approx. f(x) = x ^ 0.130
128    // - linear: 1_000, f(x) = x ^ 1.000
129    // - linearithmic: 1_130, approx. f(x) = x ^ 1.130
130    // - quadratic: 2_000, f(x) = x ^ 2.000
131    // - cubic: 3_000, f(x) = x ^ 3.000
132    // - polynomial: depends on polynomial degree
133    // - exponential: 1_000_000, practically there is no sense in polynomial degree > 1_000.000
134    match name {
135        Name::Constant => 0,
136        Name::Logarithmic => 130,
137        Name::Linear => 1_000,
138        Name::Linearithmic => 1_130,
139        Name::Quadratic => 2_000,
140        Name::Cubic => 3_000,
141        Name::Polynomial => match params.power {
142            Some(power) => std::cmp::min((1_000.0 * power) as u32, 1_000_000),
143            None => panic!("Polynomial is missing its power parameter"),
144        },
145        Name::Exponential => 1_000_000,
146    }
147}
148
149/// Fits a function of given complexity into input data.
150pub fn fit(name: Name, data: Vec<(f64, f64)>) -> Result<Complexity, Error> {
151    let linearized = data
152        .clone()
153        .into_iter()
154        .map(|(x, y)| linearize(name, x, y))
155        .collect();
156
157    let (gain, offset, _residuals) = linalg::fit_line(linearized)?;
158    let params = delinearize(name, gain, offset);
159    // Calculate delinearized residuals.
160    let residuals = calculate_residuals(name, params.clone(), data)?;
161    let params = Params {
162        residuals: Some(residuals),
163        ..params
164    };
165    let rank = rank(name, params.clone());
166
167    Ok(Complexity {
168        name,
169        notation: name::notation(name),
170        params,
171        rank,
172    })
173}
174
175/// Creates `Complexity` from string.
176///
177/// # Example
178/// ```
179/// use big_o::{complexity, Name::*};
180///
181/// let linear = complexity("O(n)").unwrap();
182/// assert_eq!(linear.name, Linear);
183///
184/// let cubic = complexity("O(n^3)").unwrap();
185/// assert_eq!(cubic.name, Cubic);
186///
187/// assert!(linear.rank < cubic.rank);
188/// ```
189pub fn complexity(string: &str) -> Result<Complexity, Error> {
190    let name: Name = string.try_into()?;
191    Ok(crate::complexity::ComplexityBuilder::new(name).build())
192}
193
194#[cfg(test)]
195mod tests {
196    use super::*;
197
198    fn constant() -> Complexity {
199        ComplexityBuilder::new(Name::Constant).build()
200    }
201
202    fn logarithmic() -> Complexity {
203        ComplexityBuilder::new(Name::Logarithmic).build()
204    }
205
206    fn linear() -> Complexity {
207        ComplexityBuilder::new(Name::Linear).build()
208    }
209
210    fn linearithmic() -> Complexity {
211        ComplexityBuilder::new(Name::Linearithmic).build()
212    }
213
214    fn quadratic() -> Complexity {
215        ComplexityBuilder::new(Name::Quadratic).build()
216    }
217
218    fn cubic() -> Complexity {
219        ComplexityBuilder::new(Name::Cubic).build()
220    }
221
222    fn exponential() -> Complexity {
223        ComplexityBuilder::new(Name::Exponential).build()
224    }
225
226    fn polynomial(power: f64) -> Complexity {
227        ComplexityBuilder::new(Name::Polynomial)
228            .power(power)
229            .build()
230    }
231
232    #[test]
233    fn test_complecity_rank() {
234        // O(1) < ... < O(n)
235        assert!(constant().rank < logarithmic().rank);
236        assert!(logarithmic().rank < polynomial(0.5).rank);
237        assert!(polynomial(0.5).rank < linear().rank);
238
239        // O(n) < ... < O(n^2)
240        assert!(linear().rank < linearithmic().rank);
241        assert!(linearithmic().rank < polynomial(1.5).rank);
242        assert!(polynomial(1.5).rank < quadratic().rank);
243
244        // O(n^2) < ... < O(n^3)
245        assert!(quadratic().rank < polynomial(2.5).rank);
246        assert!(polynomial(2.5).rank < cubic().rank);
247
248        // O(n^3) < ... < O(c^n)
249        assert!(cubic().rank < polynomial(3.5).rank);
250        assert!(polynomial(3.5).rank < exponential().rank);
251    }
252}