use super::linear_regression::{EwLinearRegressionF64, LinearRegressionF32, LinearRegressionF64};
macro_rules! impl_exponential_regression {
($name:ident, $inner:ident, $ty:ty) => {
#[doc = concat!("use nexus_stats::regression::", stringify!($name), ";")]
#[doc = concat!("let mut r = ", stringify!($name), "::new();")]
#[doc = concat!(" let y = 2.0 as ", stringify!($ty), " * (0.05 as ", stringify!($ty), " * x as ", stringify!($ty), ").exp();")]
#[derive(Debug, Clone)]
pub struct $name {
inner: $inner,
}
impl $name {
#[inline]
#[must_use]
pub fn new() -> Self {
Self { inner: $inner::new() }
}
#[inline]
pub fn update(&mut self, x: $ty, y: $ty) -> Result<(), crate::DataError> {
check_finite!(x);
check_finite!(y);
if y > 0.0 as $ty {
#[allow(clippy::cast_possible_truncation)]
let ln_y = crate::math::ln(y as f64) as $ty;
self.inner.update(x, ln_y)?;
}
Ok(())
}
#[must_use]
pub fn growth_rate(&self) -> Option<$ty> {
self.inner.slope()
}
#[must_use]
pub fn scale(&self) -> Option<$ty> {
self.inner.intercept_value().map(|v| {
#[allow(clippy::cast_possible_truncation)]
{ crate::math::exp(v as f64) as $ty }
})
}
#[must_use]
pub fn r_squared(&self) -> Option<$ty> {
self.inner.r_squared()
}
#[must_use]
pub fn predict(&self, x: $ty) -> Option<$ty> {
self.inner.predict(x).map(|ln_y| {
#[allow(clippy::cast_possible_truncation)]
{ crate::math::exp(ln_y as f64) as $ty }
})
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.inner.count()
}
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool {
self.inner.is_primed()
}
#[inline]
pub fn reset(&mut self) {
self.inner.reset();
}
}
impl Default for $name {
#[inline]
fn default() -> Self {
Self::new()
}
}
};
}
impl_exponential_regression!(ExponentialRegressionF64, LinearRegressionF64, f64);
impl_exponential_regression!(ExponentialRegressionF32, LinearRegressionF32, f32);
macro_rules! impl_logarithmic_regression {
($name:ident, $inner:ident, $ty:ty) => {
#[doc = concat!("use nexus_stats::regression::", stringify!($name), ";")]
#[doc = concat!("let mut r = ", stringify!($name), "::new();")]
#[doc = concat!(" let y = 3.0 as ", stringify!($ty), " * (x as ", stringify!($ty), ").ln() + 1.0 as ", stringify!($ty), ";")]
#[derive(Debug, Clone)]
pub struct $name {
inner: $inner,
}
impl $name {
#[inline]
#[must_use]
pub fn new() -> Self {
Self { inner: $inner::new() }
}
#[inline]
pub fn update(&mut self, x: $ty, y: $ty) -> Result<(), crate::DataError> {
check_finite!(x);
check_finite!(y);
if x > 0.0 as $ty {
#[allow(clippy::cast_possible_truncation)]
let ln_x = crate::math::ln(x as f64) as $ty;
self.inner.update(ln_x, y)?;
}
Ok(())
}
#[must_use]
pub fn slope(&self) -> Option<$ty> {
self.inner.slope()
}
#[must_use]
pub fn intercept_value(&self) -> Option<$ty> {
self.inner.intercept_value()
}
#[must_use]
pub fn r_squared(&self) -> Option<$ty> {
self.inner.r_squared()
}
#[must_use]
pub fn predict(&self, x: $ty) -> Option<$ty> {
if x <= 0.0 as $ty {
return Option::None;
}
#[allow(clippy::cast_possible_truncation)]
let ln_x = crate::math::ln(x as f64) as $ty;
self.inner.predict(ln_x)
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.inner.count()
}
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool {
self.inner.is_primed()
}
#[inline]
pub fn reset(&mut self) {
self.inner.reset();
}
}
impl Default for $name {
#[inline]
fn default() -> Self {
Self::new()
}
}
};
}
impl_logarithmic_regression!(LogarithmicRegressionF64, LinearRegressionF64, f64);
impl_logarithmic_regression!(LogarithmicRegressionF32, LinearRegressionF32, f32);
macro_rules! impl_power_regression {
($name:ident, $inner:ident, $ty:ty) => {
#[doc = concat!("use nexus_stats::regression::", stringify!($name), ";")]
#[doc = concat!("let mut r = ", stringify!($name), "::new();")]
#[doc = concat!(" let y = 4.0 as ", stringify!($ty), " * (x as ", stringify!($ty), ").powf(2.5);")]
#[derive(Debug, Clone)]
pub struct $name {
inner: $inner,
}
impl $name {
#[inline]
#[must_use]
pub fn new() -> Self {
Self { inner: $inner::new() }
}
#[inline]
pub fn update(&mut self, x: $ty, y: $ty) -> Result<(), crate::DataError> {
check_finite!(x);
check_finite!(y);
if x > 0.0 as $ty && y > 0.0 as $ty {
#[allow(clippy::cast_possible_truncation)]
let ln_x = crate::math::ln(x as f64) as $ty;
#[allow(clippy::cast_possible_truncation)]
let ln_y = crate::math::ln(y as f64) as $ty;
self.inner.update(ln_x, ln_y)?;
}
Ok(())
}
#[must_use]
pub fn exponent(&self) -> Option<$ty> {
self.inner.slope()
}
#[must_use]
pub fn scale(&self) -> Option<$ty> {
self.inner.intercept_value().map(|v| {
#[allow(clippy::cast_possible_truncation)]
{ crate::math::exp(v as f64) as $ty }
})
}
#[must_use]
pub fn r_squared(&self) -> Option<$ty> {
self.inner.r_squared()
}
#[must_use]
pub fn predict(&self, x: $ty) -> Option<$ty> {
if x <= 0.0 as $ty {
return Option::None;
}
let intercept = self.inner.intercept_value()?;
let slope = self.inner.slope()?;
#[allow(clippy::cast_possible_truncation)]
{
let a = crate::math::exp(intercept as f64);
let b = slope as f64;
let result = a * (x as f64).powf(b);
Option::Some(result as $ty)
}
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.inner.count()
}
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool {
self.inner.is_primed()
}
#[inline]
pub fn reset(&mut self) {
self.inner.reset();
}
}
impl Default for $name {
#[inline]
fn default() -> Self {
Self::new()
}
}
};
}
impl_power_regression!(PowerRegressionF64, LinearRegressionF64, f64);
impl_power_regression!(PowerRegressionF32, LinearRegressionF32, f32);
macro_rules! impl_ew_exponential_regression {
($name:ident, $builder:ident, $inner:ident, $ty:ty) => {
#[derive(Debug, Clone)]
pub struct $name {
inner: $inner,
}
#[doc = concat!("Builder for [`", stringify!($name), "`].")]
#[derive(Debug, Clone)]
pub struct $builder {
alpha: Option<$ty>,
}
impl $name {
#[inline]
#[must_use]
pub fn builder() -> $builder {
$builder {
alpha: Option::None,
}
}
#[inline]
pub fn update(&mut self, x: $ty, y: $ty) -> Result<(), crate::DataError> {
check_finite!(x);
check_finite!(y);
if y > 0.0 as $ty {
#[allow(clippy::cast_possible_truncation)]
let ln_y = crate::math::ln(y as f64) as $ty;
self.inner.update(x, ln_y)?;
}
Ok(())
}
#[must_use]
pub fn growth_rate(&self) -> Option<$ty> {
self.inner.slope()
}
#[must_use]
pub fn scale(&self) -> Option<$ty> {
self.inner.intercept_value().map(|v| {
#[allow(clippy::cast_possible_truncation)]
{
crate::math::exp(v as f64) as $ty
}
})
}
#[must_use]
pub fn r_squared(&self) -> Option<$ty> {
self.inner.r_squared()
}
#[must_use]
pub fn predict(&self, x: $ty) -> Option<$ty> {
self.inner.predict(x).map(|ln_y| {
#[allow(clippy::cast_possible_truncation)]
{
crate::math::exp(ln_y as f64) as $ty
}
})
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.inner.count()
}
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool {
self.inner.is_primed()
}
#[inline]
pub fn reset(&mut self) {
self.inner.reset();
}
}
impl $builder {
#[inline]
#[must_use]
pub fn alpha(mut self, alpha: $ty) -> Self {
self.alpha = Option::Some(alpha);
self
}
pub fn build(self) -> Result<$name, crate::ConfigError> {
let alpha = self.alpha.ok_or(crate::ConfigError::Missing("alpha"))?;
let inner = $inner::builder().alpha(alpha).build()?;
Ok($name { inner })
}
}
};
}
impl_ew_exponential_regression!(
EwExponentialRegressionF64,
EwExponentialRegressionF64Builder,
EwLinearRegressionF64,
f64
);
macro_rules! impl_ew_logarithmic_regression {
($name:ident, $builder:ident, $inner:ident, $ty:ty) => {
#[derive(Debug, Clone)]
pub struct $name {
inner: $inner,
}
#[doc = concat!("Builder for [`", stringify!($name), "`].")]
#[derive(Debug, Clone)]
pub struct $builder {
alpha: Option<$ty>,
}
impl $name {
#[inline]
#[must_use]
pub fn builder() -> $builder {
$builder {
alpha: Option::None,
}
}
#[inline]
pub fn update(&mut self, x: $ty, y: $ty) -> Result<(), crate::DataError> {
check_finite!(x);
check_finite!(y);
if x > 0.0 as $ty {
#[allow(clippy::cast_possible_truncation)]
let ln_x = crate::math::ln(x as f64) as $ty;
self.inner.update(ln_x, y)?;
}
Ok(())
}
#[must_use]
pub fn slope(&self) -> Option<$ty> {
self.inner.slope()
}
#[must_use]
pub fn intercept_value(&self) -> Option<$ty> {
self.inner.intercept_value()
}
#[must_use]
pub fn r_squared(&self) -> Option<$ty> {
self.inner.r_squared()
}
#[must_use]
pub fn predict(&self, x: $ty) -> Option<$ty> {
if x <= 0.0 as $ty {
return Option::None;
}
#[allow(clippy::cast_possible_truncation)]
let ln_x = crate::math::ln(x as f64) as $ty;
self.inner.predict(ln_x)
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.inner.count()
}
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool {
self.inner.is_primed()
}
#[inline]
pub fn reset(&mut self) {
self.inner.reset();
}
}
impl $builder {
#[inline]
#[must_use]
pub fn alpha(mut self, alpha: $ty) -> Self {
self.alpha = Option::Some(alpha);
self
}
pub fn build(self) -> Result<$name, crate::ConfigError> {
let alpha = self.alpha.ok_or(crate::ConfigError::Missing("alpha"))?;
let inner = $inner::builder().alpha(alpha).build()?;
Ok($name { inner })
}
}
};
}
impl_ew_logarithmic_regression!(
EwLogarithmicRegressionF64,
EwLogarithmicRegressionF64Builder,
EwLinearRegressionF64,
f64
);
macro_rules! impl_ew_power_regression {
($name:ident, $builder:ident, $inner:ident, $ty:ty) => {
#[derive(Debug, Clone)]
pub struct $name {
inner: $inner,
}
#[doc = concat!("Builder for [`", stringify!($name), "`].")]
#[derive(Debug, Clone)]
pub struct $builder {
alpha: Option<$ty>,
}
impl $name {
#[inline]
#[must_use]
pub fn builder() -> $builder {
$builder {
alpha: Option::None,
}
}
#[inline]
pub fn update(&mut self, x: $ty, y: $ty) -> Result<(), crate::DataError> {
check_finite!(x);
check_finite!(y);
if x > 0.0 as $ty && y > 0.0 as $ty {
#[allow(clippy::cast_possible_truncation)]
let ln_x = crate::math::ln(x as f64) as $ty;
#[allow(clippy::cast_possible_truncation)]
let ln_y = crate::math::ln(y as f64) as $ty;
self.inner.update(ln_x, ln_y)?;
}
Ok(())
}
#[must_use]
pub fn exponent(&self) -> Option<$ty> {
self.inner.slope()
}
#[must_use]
pub fn scale(&self) -> Option<$ty> {
self.inner.intercept_value().map(|v| {
#[allow(clippy::cast_possible_truncation)]
{
crate::math::exp(v as f64) as $ty
}
})
}
#[must_use]
pub fn r_squared(&self) -> Option<$ty> {
self.inner.r_squared()
}
#[must_use]
pub fn predict(&self, x: $ty) -> Option<$ty> {
if x <= 0.0 as $ty {
return Option::None;
}
let intercept = self.inner.intercept_value()?;
let slope = self.inner.slope()?;
#[allow(clippy::cast_possible_truncation)]
{
let a = crate::math::exp(intercept as f64);
let b = slope as f64;
let result = a * (x as f64).powf(b);
Option::Some(result as $ty)
}
}
#[inline]
#[must_use]
pub fn count(&self) -> u64 {
self.inner.count()
}
#[inline]
#[must_use]
pub fn is_primed(&self) -> bool {
self.inner.is_primed()
}
#[inline]
pub fn reset(&mut self) {
self.inner.reset();
}
}
impl $builder {
#[inline]
#[must_use]
pub fn alpha(mut self, alpha: $ty) -> Self {
self.alpha = Option::Some(alpha);
self
}
pub fn build(self) -> Result<$name, crate::ConfigError> {
let alpha = self.alpha.ok_or(crate::ConfigError::Missing("alpha"))?;
let inner = $inner::builder().alpha(alpha).build()?;
Ok($name { inner })
}
}
};
}
impl_ew_power_regression!(
EwPowerRegressionF64,
EwPowerRegressionF64Builder,
EwLinearRegressionF64,
f64
);
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exponential_exact_fit() {
let mut r = ExponentialRegressionF64::new();
for x in 0..100 {
let xf = x as f64;
let y = 2.0 * (0.05 * xf).exp();
r.update(xf, y).unwrap();
}
let rate = r.growth_rate().unwrap();
assert!((rate - 0.05).abs() < 1e-8, "growth rate = {rate}");
let scale = r.scale().unwrap();
assert!((scale - 2.0).abs() < 1e-6, "scale = {scale}");
assert!((r.r_squared().unwrap() - 1.0).abs() < 1e-10);
}
#[test]
fn exponential_predict() {
let mut r = ExponentialRegressionF64::new();
for x in 0..100 {
let xf = x as f64;
r.update(xf, 2.0 * (0.05 * xf).exp()).unwrap();
}
let y = r.predict(10.0).unwrap();
let expected = 2.0 * (0.05 * 10.0_f64).exp();
assert!((y - expected).abs() < 1e-4, "predict(10) = {y}");
}
#[test]
fn exponential_skips_negative_y() {
let mut r = ExponentialRegressionF64::new();
r.update(1.0, -5.0).unwrap(); r.update(2.0, 0.0).unwrap(); assert_eq!(r.count(), 0);
}
#[test]
fn logarithmic_exact_fit() {
let mut r = LogarithmicRegressionF64::new();
for x in 1..200 {
let xf = x as f64;
r.update(xf, 3.0 * xf.ln() + 1.0).unwrap();
}
let slope = r.slope().unwrap();
assert!((slope - 3.0).abs() < 1e-6, "slope = {slope}");
let intercept = r.intercept_value().unwrap();
assert!((intercept - 1.0).abs() < 1e-6, "intercept = {intercept}");
}
#[test]
fn logarithmic_skips_negative_x() {
let mut r = LogarithmicRegressionF64::new();
r.update(-1.0, 5.0).unwrap();
r.update(0.0, 5.0).unwrap();
assert_eq!(r.count(), 0);
}
#[test]
fn logarithmic_predict_negative_x_returns_none() {
let mut r = LogarithmicRegressionF64::new();
for x in 1..100 {
r.update(x as f64, (x as f64).ln()).unwrap();
}
assert!(r.predict(-1.0).is_none());
}
#[test]
fn power_exact_fit() {
let mut r = PowerRegressionF64::new();
for x in 1..200 {
let xf = x as f64;
r.update(xf, 4.0 * xf.powf(2.5)).unwrap();
}
let exp = r.exponent().unwrap();
assert!((exp - 2.5).abs() < 1e-4, "exponent = {exp}");
let scale = r.scale().unwrap();
assert!((scale - 4.0).abs() < 0.1, "scale = {scale}");
}
#[test]
fn power_skips_nonpositive() {
let mut r = PowerRegressionF64::new();
r.update(0.0, 5.0).unwrap();
r.update(1.0, -5.0).unwrap();
r.update(-1.0, 5.0);
assert_eq!(r.count(), 0);
}
#[test]
fn ew_exponential_basic() {
let mut r = EwExponentialRegressionF64::builder()
.alpha(0.05)
.build()
.unwrap();
for x in 0..300 {
let xf = x as f64;
r.update(xf, 2.0 * (0.01 * xf).exp()).unwrap();
}
assert!(r.is_primed());
assert!(r.growth_rate().is_some());
}
#[test]
fn ew_logarithmic_basic() {
let mut r = EwLogarithmicRegressionF64::builder()
.alpha(0.05)
.build()
.unwrap();
for x in 1..300 {
r.update(x as f64, 2.0 * (x as f64).ln() + 5.0).unwrap();
}
assert!(r.is_primed());
}
#[test]
fn ew_power_basic() {
let mut r = EwPowerRegressionF64::builder().alpha(0.05).build().unwrap();
for x in 1..300 {
r.update(x as f64, 3.0 * (x as f64).powf(1.5)).unwrap();
}
assert!(r.is_primed());
}
#[test]
fn f32_exponential() {
let mut r = ExponentialRegressionF32::new();
for x in 0..100u32 {
let xf = x as f32;
r.update(xf, 2.0 * (0.05 * xf).exp()).unwrap();
}
assert!(r.growth_rate().is_some());
}
#[test]
fn f32_logarithmic() {
let mut r = LogarithmicRegressionF32::new();
for x in 1..100u32 {
r.update(x as f32, 3.0 * (x as f32).ln() + 1.0).unwrap();
}
assert!(r.slope().is_some());
}
#[test]
fn f32_power() {
let mut r = PowerRegressionF32::new();
for x in 1..100u32 {
r.update(x as f32, 4.0 * (x as f32).powf(2.0)).unwrap();
}
assert!(r.exponent().is_some());
}
#[test]
fn reset_all_transforms() {
let mut exp = ExponentialRegressionF64::new();
let mut log = LogarithmicRegressionF64::new();
let mut pow = PowerRegressionF64::new();
for x in 1..100 {
exp.update(x as f64, (x as f64).exp()).unwrap();
log.update(x as f64, (x as f64).ln()).unwrap();
pow.update(x as f64, (x as f64).powi(2)).unwrap();
}
exp.reset();
log.reset();
pow.reset();
assert_eq!(exp.count(), 0);
assert_eq!(log.count(), 0);
assert_eq!(pow.count(), 0);
}
#[test]
fn defaults_are_empty() {
assert_eq!(ExponentialRegressionF64::default().count(), 0);
assert_eq!(LogarithmicRegressionF64::default().count(), 0);
assert_eq!(PowerRegressionF64::default().count(), 0);
}
}