diff --git a/Cargo.toml b/Cargo.toml index 3239ab0..61df850 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "big_o" description = "Infers asymptotic computational complexity" -version = "0.1.2" +version = "0.1.3" edition = "2021" authors = ["Maksym Arutyunyan"] repository = "https://github.com/maksym-arutyunyan/big_o" diff --git a/src/complexity.rs b/src/complexity.rs index 95ea164..9e07999 100644 --- a/src/complexity.rs +++ b/src/complexity.rs @@ -19,37 +19,35 @@ pub struct Complexity { pub rank: u32, } -impl Complexity { - /// Returns a calculated approximation function `f(x)` - pub fn get_function(&self) -> Result f64>, &'static str> { - let p = &self.params; - if let (Some(a), Some(b)) = match self.name { - Name::Polynomial => (p.gain, p.power), - Name::Exponential => (p.gain, p.base), - _other => (p.gain, p.offset), - } { - let f: Box f64> = match self.name { - Name::Constant => Box::new(move |_x| b), - Name::Logarithmic => Box::new(move |x| a * x.ln() + b), - Name::Linear => Box::new(move |x| a * x + b), - Name::Linearithmic => Box::new(move |x| a * x * x.ln() + b), - Name::Quadratic => Box::new(move |x| a * x.powi(2) + b), - Name::Cubic => Box::new(move |x| a * x.powi(3) + b), - Name::Polynomial => Box::new(move |x| a * x.powf(b)), - Name::Exponential => Box::new(move |x| a * b.powf(x)), - }; - Ok(f) - } else { - Err("No cofficients to compute f(x)") - } +/// Returns a calculated approximation function `f(x)` +fn get_function(name: Name, params: Params) -> Result f64>, &'static str> { + if let (Some(a), Some(b)) = match name { + Name::Polynomial => (params.gain, params.power), + Name::Exponential => (params.gain, params.base), + _other => (params.gain, params.offset), + } { + let f: Box f64> = match name { + Name::Constant => Box::new(move |_x| b), + Name::Logarithmic => Box::new(move |x| a * x.ln() + b), + Name::Linear => Box::new(move |x| a * x + b), + Name::Linearithmic => Box::new(move |x| a * x * x.ln() + b), + Name::Quadratic => Box::new(move |x| a * x.powi(2) + b), + Name::Cubic => Box::new(move |x| a * x.powi(3) + b), + Name::Polynomial => Box::new(move |x| a * x.powf(b)), + Name::Exponential => Box::new(move |x| a * b.powf(x)), + }; + Ok(f) + } else { + Err("No cofficients to compute f(x)") } +} - /// Computes values of `f(x)` given `x` - pub fn compute_f(&self, x: Vec) -> Result, &'static str> { - let f = self.get_function()?; - let y = x.into_iter().map(f).collect(); - Ok(y) - } +/// Computes values of `f(x)` given `x` +#[allow(dead_code)] +fn compute_f(name: Name, params: Params, x: Vec) -> Result, &'static str> { + let f = get_function(name, params)?; + let y = x.into_iter().map(f).collect(); + Ok(y) } pub struct ComplexityBuilder { @@ -62,7 +60,7 @@ impl ComplexityBuilder { Self { name, params: None } } - #[allow(dead_code)] // Used in tests. + #[allow(dead_code)] // Used in tests. pub fn power(&mut self, x: f64) -> &mut Self { self.params = Some(Params::new().power(x).build()); self @@ -98,7 +96,7 @@ fn linearize(name: Name, x: f64, y: f64) -> (f64, f64) { } /// Converts linear coeffs `gain` and `offset` to corresponding complexity params. -fn delinearize(name: Name, gain: f64, offset: f64, residuals: f64) -> Params { +fn delinearize(name: Name, gain: f64, offset: f64) -> Params { // Delinearize coeffs. let (a, b) = match name { Name::Polynomial => (offset.exp(), gain), @@ -107,12 +105,23 @@ fn delinearize(name: Name, gain: f64, offset: f64, residuals: f64) -> Params { }; // Convert coeffs to params. match name { - Name::Polynomial => Params::new().gain(a).power(b).residuals(residuals).build(), - Name::Exponential => Params::new().gain(a).base(b).residuals(residuals).build(), - _other => Params::new().gain(a).offset(b).residuals(residuals).build(), + Name::Polynomial => Params::new().gain(a).power(b).build(), + Name::Exponential => Params::new().gain(a).base(b).build(), + _other => Params::new().gain(a).offset(b).build(), } } +fn calculate_residuals( + name: Name, + params: Params, + data: Vec<(f64, f64)>, +) -> Result { + let f = get_function(name, params)?; + let residuals = data.into_iter().map(|(x, y)| (y - f(x)).abs()).sum(); + + Ok(residuals) +} + fn rank(name: Name, params: Params) -> u32 { // Rank is similar to a degree of a corresponding polynomial: // - constant: 0, f(x) = x ^ 0.000 @@ -132,12 +141,10 @@ fn rank(name: Name, params: Params) -> u32 { Name::Linearithmic => 1_130, Name::Quadratic => 2_000, Name::Cubic => 3_000, - Name::Polynomial => { - match params.power { - Some(power) => std::cmp::min((1_000.0 * power) as u32, 1_000_000), - None => panic!("Polynomial is missing its power parameter"), - } - } + Name::Polynomial => match params.power { + Some(power) => std::cmp::min((1_000.0 * power) as u32, 1_000_000), + None => panic!("Polynomial is missing its power parameter"), + }, Name::Exponential => 1_000_000, } } @@ -145,12 +152,19 @@ fn rank(name: Name, params: Params) -> u32 { /// Fits a function of given complexity into input data. pub fn fit(name: Name, data: Vec<(f64, f64)>) -> Result { let linearized = data + .clone() .into_iter() .map(|(x, y)| linearize(name, x, y)) .collect(); - let (gain, offset, residuals) = linalg::fit_line(linearized)?; - let params = delinearize(name, gain, offset, residuals); + let (gain, offset, _residuals) = linalg::fit_line(linearized)?; + let params = delinearize(name, gain, offset); + // Calculate delinearized residuals. + let residuals = calculate_residuals(name, params.clone(), data)?; + let params = Params { + residuals: Some(residuals), + ..params + }; let rank = rank(name, params.clone()); Ok(Complexity { diff --git a/tests/regressions.rs b/tests/regressions.rs new file mode 100644 index 0000000..11938bf --- /dev/null +++ b/tests/regressions.rs @@ -0,0 +1,60 @@ +#[test] +fn regression_0001() { + let data: Vec<(f64, f64)> = vec![ + (1.0, 45385.0), + (2.0, 90769.0), + (3.0, 136155.0), + (4.0, 181539.0), + (5.0, 226925.0), + (6.0, 272309.0), + (7.0, 317695.0), + (8.0, 363079.0), + (9.0, 408465.0), + (10.0, 453849.0), + (11.0, 499235.0), + (12.0, 544619.0), + (13.0, 590004.0), + (14.0, 635389.0), + (15.0, 680773.0), + (16.0, 726159.0), + (17.0, 771543.0), + (18.0, 816929.0), + (19.0, 862313.0), + (20.0, 907699.0), + (21.0, 953083.0), + (22.0, 998469.0), + (23.0, 1043853.0), + (24.0, 1089239.0), + (25.0, 1134623.0), + (26.0, 1180008.0), + (27.0, 1225393.0), + (28.0, 1270777.0), + (29.0, 1316163.0), + (30.0, 1361547.0), + (31.0, 1406933.0), + (32.0, 1452317.0), + (33.0, 1497703.0), + (34.0, 1543087.0), + (35.0, 1588473.0), + (36.0, 1633857.0), + (37.0, 1679243.0), + (38.0, 1724627.0), + (39.0, 1770012.0), + (40.0, 1815397.0), + (41.0, 1860781.0), + (42.0, 1906167.0), + (43.0, 1951551.0), + (44.0, 1996937.0), + (45.0, 2042321.0), + (46.0, 2087707.0), + (47.0, 2133091.0), + (48.0, 2178477.0), + (49.0, 2223861.0), + ]; + let (complexity, all) = big_o::infer_complexity(data).unwrap(); + assert_eq!( + complexity.name, + big_o::Name::Linear, + "\n\ncomplexity={complexity:?}, \n\nall={all:?}" + ); +}