From d0dc36921662ef21c45d840058692fc01e1e63e8 Mon Sep 17 00:00:00 2001 From: Matt Robinson Date: Fri, 27 Mar 2026 14:53:27 -0400 Subject: [PATCH 1/6] first pass on convolution --- rust/src/algorithms/algebra.rs | 83 ++++++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/rust/src/algorithms/algebra.rs b/rust/src/algorithms/algebra.rs index e5c4505..ed5fcbb 100644 --- a/rust/src/algorithms/algebra.rs +++ b/rust/src/algorithms/algebra.rs @@ -129,6 +129,89 @@ pub fn product_discrete( Ok(product_rv) } +/// Computes the product of two independent discrete random variables +/// +/// # Arguments +/// * `random_variable_1` - the first random variable +/// * `random_variable_2` - the second random variable +/// +/// # Returns +/// * `product_rv` - the product of the two random variables +/// +/// # Examples +pub fn convolution_discrete( + random_variable_1: &RandomVariable, + random_variable_2: &RandomVariable, +) -> Result { + let pdf_random_variable_1 = random_variable_1.to_pdf()?; + let function_1 = pdf_random_variable_1.function; + let support_1 = pdf_random_variable_1.support; + + let pdf_random_variable_2 = random_variable_2.to_pdf()?; + let function_2 = pdf_random_variable_2.function; + let support_2 = pdf_random_variable_2.support; + + // Find the values and probabilities for support_1 + support_2 + // for all combinations of support values + let mut raw_conv_support = Vec::new(); + for &s1 in support_1.iter() { + for &s2 in support_2.iter() { + let support_sum = s1 + s2; + raw_conv_support.push(support_sum); + } + } + + let mut raw_conv_function = Vec::new(); + for &f1 in function_1.iter() { + for &f2 in function_2.iter() { + let probability = f1 + f2; + raw_conv_function.push(probability); + } + } + + // Sorts the results by the support values + let mut raw_conv_pairs: Vec<_> = raw_conv_support + .into_iter() + .zip(raw_conv_function) + .collect(); + + raw_conv_pairs.sort_by(|a, b| { + let first_value = a.0.to_f64(); + let second_value = b.0.to_f64(); + first_value.total_cmp(&second_value) + }); + + let (sorted_support, sorted_function): (Vec, Vec) = + raw_conv_pairs.into_iter().unzip(); + + // Remove redundant elements from the support + let mut conv_support = Vec::new(); + let mut conv_function = Vec::new(); + + for (&s, &f) in sorted_support.iter().zip(sorted_function.iter()) { + let support_index = conv_support.iter().position(|&x| x == s); + + match support_index { + Some(index) => { + conv_function[index] += f; + } + None => { + conv_support.push(s); + conv_function.push(f); + } + } + } + + let conv_random_variable = RandomVariable { + function: conv_function, + support: conv_support, + functional_form: FunctionalForm::Pdf, + domain_type: DomainType::Discrete, + }; + + Ok(conv_random_variable) +} + #[cfg(test)] mod tests { use super::*; From d0e293bf106dce0ab7f10cda734427e546923278 Mon Sep 17 00:00:00 2001 From: Matt Robinson Date: Fri, 27 Mar 2026 14:56:21 -0400 Subject: [PATCH 2/6] corrections for conv --- rust/src/algorithms/algebra.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/rust/src/algorithms/algebra.rs b/rust/src/algorithms/algebra.rs index ed5fcbb..5f4baf2 100644 --- a/rust/src/algorithms/algebra.rs +++ b/rust/src/algorithms/algebra.rs @@ -129,14 +129,14 @@ pub fn product_discrete( Ok(product_rv) } -/// Computes the product of two independent discrete random variables +/// Computes the sum of two independent discrete random variables /// /// # Arguments /// * `random_variable_1` - the first random variable /// * `random_variable_2` - the second random variable /// /// # Returns -/// * `product_rv` - the product of the two random variables +/// * `sum_rv` - the product of the two random variables /// /// # Examples pub fn convolution_discrete( @@ -164,7 +164,7 @@ pub fn convolution_discrete( let mut raw_conv_function = Vec::new(); for &f1 in function_1.iter() { for &f2 in function_2.iter() { - let probability = f1 + f2; + let probability = f1 * f2; raw_conv_function.push(probability); } } @@ -202,14 +202,14 @@ pub fn convolution_discrete( } } - let conv_random_variable = RandomVariable { + let sum_rv = RandomVariable { function: conv_function, support: conv_support, functional_form: FunctionalForm::Pdf, domain_type: DomainType::Discrete, }; - Ok(conv_random_variable) + Ok(sum_rv) } #[cfg(test)] From 759e78de4c1b16f393e1dd838e37e8568153d871 Mon Sep 17 00:00:00 2001 From: Matt Robinson Date: Fri, 27 Mar 2026 15:00:07 -0400 Subject: [PATCH 3/6] add tests for convolution --- rust/src/algorithms/algebra.rs | 128 +++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) diff --git a/rust/src/algorithms/algebra.rs b/rust/src/algorithms/algebra.rs index 5f4baf2..e2edf94 100644 --- a/rust/src/algorithms/algebra.rs +++ b/rust/src/algorithms/algebra.rs @@ -139,6 +139,48 @@ pub fn product_discrete( /// * `sum_rv` - the product of the two random variables /// /// # Examples +/// ``` +/// use applpy_rust::algorithms::algebra::convolution_discrete; +/// use applpy_rust::algorithms::number::Number; +/// use applpy_rust::algorithms::rv::{DomainType, FunctionalForm, RandomVariable}; +/// use num_rational::Rational64; +/// +/// let rv1 = RandomVariable { +/// function: vec![ +/// Number::Rational(Rational64::new(1, 2)), +/// Number::Rational(Rational64::new(1, 2)), +/// ], +/// support: vec![Number::Integer(1), Number::Integer(2)], +/// functional_form: FunctionalForm::Pdf, +/// domain_type: DomainType::Discrete, +/// }; +/// +/// let rv2 = RandomVariable { +/// function: vec![ +/// Number::Rational(Rational64::new(1, 2)), +/// Number::Rational(Rational64::new(1, 2)), +/// ], +/// support: vec![Number::Integer(2), Number::Integer(3)], +/// functional_form: FunctionalForm::Pdf, +/// domain_type: DomainType::Discrete, +/// }; +/// +/// let sum = convolution_discrete(&rv1, &rv2).unwrap(); +/// +/// assert_eq!( +/// sum.support, +/// vec![Number::Integer(3), Number::Integer(4), Number::Integer(5)] +/// ); +/// assert_eq!( +/// sum.function, +/// vec![ +/// Number::Rational(Rational64::new(1, 4)), +/// Number::Rational(Rational64::new(1, 2)), +/// Number::Rational(Rational64::new(1, 4)), +/// ] +/// ); +/// assert!(sum.verify_pdf(None).unwrap()); +/// ``` pub fn convolution_discrete( random_variable_1: &RandomVariable, random_variable_2: &RandomVariable, @@ -318,4 +360,90 @@ mod tests { assert_eq!(product.domain_type, DomainType::Discrete); assert!(product.verify_pdf(None).unwrap()); } + + #[test] + fn convolution_discrete_combines_duplicate_support_values() { + let rv1 = two_point_pdf( + [0, 1], + [Rational64::new(1, 2), Rational64::new(1, 2)], + FunctionalForm::Pdf, + ); + let rv2 = two_point_pdf( + [2, 3], + [Rational64::new(1, 5), Rational64::new(4, 5)], + FunctionalForm::Pdf, + ); + + let sum = convolution_discrete(&rv1, &rv2).unwrap(); + + assert_eq!( + sum.support, + vec![Number::Integer(2), Number::Integer(3), Number::Integer(4)] + ); + assert_eq!( + sum.function, + vec![ + Number::Rational(Rational64::new(1, 10)), + Number::Rational(Rational64::new(1, 2)), + Number::Rational(Rational64::new(2, 5)) + ] + ); + assert!(sum.verify_pdf(None).unwrap()); + } + + #[test] + fn convolution_discrete_accepts_cdf_inputs_by_converting_to_pdf() { + let rv1 = two_point_pdf( + [1, 2], + [Rational64::new(1, 4), Rational64::new(1, 1)], + FunctionalForm::Cdf, + ); + let rv2 = two_point_pdf( + [3, 5], + [Rational64::new(1, 2), Rational64::new(1, 1)], + FunctionalForm::Cdf, + ); + + let sum = convolution_discrete(&rv1, &rv2).unwrap(); + + assert_eq!( + sum.support, + vec![ + Number::Integer(4), + Number::Integer(5), + Number::Integer(6), + Number::Integer(7) + ] + ); + assert_eq!( + sum.function, + vec![ + Number::Rational(Rational64::new(1, 8)), + Number::Rational(Rational64::new(3, 8)), + Number::Rational(Rational64::new(1, 8)), + Number::Rational(Rational64::new(3, 8)) + ] + ); + assert!(sum.verify_pdf(None).unwrap()); + } + + #[test] + fn convolution_discrete_preserves_pdf_and_discrete_domain() { + let rv1 = two_point_pdf( + [2, 4], + [Rational64::new(1, 3), Rational64::new(2, 3)], + FunctionalForm::Pdf, + ); + let rv2 = two_point_pdf( + [1, 3], + [Rational64::new(3, 10), Rational64::new(7, 10)], + FunctionalForm::Pdf, + ); + + let sum = convolution_discrete(&rv1, &rv2).unwrap(); + + assert_eq!(sum.functional_form, FunctionalForm::Pdf); + assert_eq!(sum.domain_type, DomainType::Discrete); + assert!(sum.verify_pdf(None).unwrap()); + } } From 68f705fd557d01874dee6b54aabc7cfa00439c64 Mon Sep 17 00:00:00 2001 From: Matt Robinson Date: Fri, 27 Mar 2026 15:06:34 -0400 Subject: [PATCH 4/6] add and addassign for rv --- rust/src/algorithms/rv.rs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/rust/src/algorithms/rv.rs b/rust/src/algorithms/rv.rs index 271a4f5..f5a1f31 100644 --- a/rust/src/algorithms/rv.rs +++ b/rust/src/algorithms/rv.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use std::fmt; -use std::ops::Mul; +use std::ops::{Add, AddAssign, Mul}; use num_rational::Rational64; use num_traits::cast::ToPrimitive; @@ -63,6 +63,22 @@ pub struct RandomVariable { pub domain_type: DomainType, } +impl Add for RandomVariable { + type Output = Result; + + fn add(self, rhs: Self) -> Self::Output { + let sum_rv = algebra::convolution_discrete(&self, &rhs)?; + Ok(sum_rv) + } +} + +impl AddAssign for RandomVariable { + fn add_assign(&mut self, rhs: Self) { + *self = + algebra::convolution_discrete(self, &rhs).expect("failed to sum the random variables"); + } +} + impl Mul for RandomVariable { type Output = Result; From 1b0ecab4aa4b644a60cf92eb0f2a35ff6b112f35 Mon Sep 17 00:00:00 2001 From: Matt Robinson Date: Fri, 27 Mar 2026 15:10:12 -0400 Subject: [PATCH 5/6] add convolution into the python api --- rust/src/lib.rs | 4 ++++ rust/src/python/api.rs | 52 ++++++++++++++++++++++++++++-------------- 2 files changed, 39 insertions(+), 17 deletions(-) diff --git a/rust/src/lib.rs b/rust/src/lib.rs index d53ee91..9b1628c 100644 --- a/rust/src/lib.rs +++ b/rust/src/lib.rs @@ -36,6 +36,10 @@ fn applpy_rust(_py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> { module.add_function(wrap_pyfunction!(python::api::bootstrap_rv_py, module)?)?; // random variable algebra functions + module.add_function(wrap_pyfunction!( + python::api::convolution_discrete_py, + module + )?)?; module.add_function(wrap_pyfunction!(python::api::product_discrete_py, module)?)?; // transformation functions diff --git a/rust/src/python/api.rs b/rust/src/python/api.rs index 7b1effa..bf298da 100644 --- a/rust/src/python/api.rs +++ b/rust/src/python/api.rs @@ -1,8 +1,6 @@ #![allow(clippy::useless_conversion)] #![allow(dead_code)] -use std::ops::Mul; - use pyo3::exceptions::PyValueError; use pyo3::prelude::*; use pyo3::types::PyAny; @@ -134,6 +132,25 @@ pub fn product_discrete_py( )) } +#[pyfunction(name = "convolution_discrete", signature = (random_variable_1, random_variable_2))] +pub fn convolution_discrete_py( + random_variable_1: &Bound<'_, PyAny>, + random_variable_2: &Bound<'_, PyAny>, +) -> PyResult { + let random_variable_1: FastRV = random_variable_1.extract()?; + let random_variable_2: FastRV = random_variable_2.extract()?; + + let sum_rv = algebra::convolution_discrete(&random_variable_1.inner, &random_variable_2.inner) + .map_err(PyValueError::new_err)?; + + Ok(FastRV::new( + sum_rv.function, + sum_rv.support, + sum_rv.functional_form, + sum_rv.domain_type, + )) +} + #[pyfunction(name = "next_combination", signature = (previous, n))] pub fn next_combination_py(previous: Vec, n: usize) -> PyResult>> { if previous.is_empty() { @@ -221,21 +238,6 @@ pub struct FastRV { inner: RandomVariable, } -impl Mul for FastRV { - type Output = Result; - - fn mul(self, rhs: FastRV) -> Self::Output { - let product_rv = algebra::product_discrete(&self.inner, &rhs.inner)?; - let fast_rv = Self::new( - product_rv.function, - product_rv.support, - product_rv.functional_form, - product_rv.domain_type, - ); - Ok(fast_rv) - } -} - fn format_number_list(values: &[Number]) -> String { values .iter() @@ -273,6 +275,22 @@ impl FastRV { ) } + pub fn __add__(&self, rhs: FastRV) -> PyResult { + let self_rv = self.inner.clone(); + let rhs_rv = rhs.inner.clone(); + + let sum_rv = self_rv * rhs_rv; + + match sum_rv { + Ok(rv) => { + let fast_rv = + FastRV::new(rv.function, rv.support, rv.functional_form, rv.domain_type); + Ok(fast_rv) + } + Err(s) => Err(PyErr::new::(s)), + } + } + pub fn __mul__(&self, rhs: FastRV) -> PyResult { let self_rv = self.inner.clone(); let rhs_rv = rhs.inner.clone(); From 1cf28d0f39f40c530efc1d55f27522ceff63fe42 Mon Sep 17 00:00:00 2001 From: Matt Robinson Date: Fri, 27 Mar 2026 15:20:33 -0400 Subject: [PATCH 6/6] patch new discrete functions into python --- applpy/algebra.py | 42 +++--------------------------------------- rust/src/python/api.rs | 2 +- 2 files changed, 4 insertions(+), 40 deletions(-) diff --git a/applpy/algebra.py b/applpy/algebra.py index 2958121..1ff80fd 100644 --- a/applpy/algebra.py +++ b/applpy/algebra.py @@ -58,8 +58,6 @@ def convolution(random_variable_1, random_variable_2): 2. random_variable_2: A random variable Output: 1. The convolution of random_variable_1 and random_variable_2 """ - # If the two random variables are not both continuous or - # both discrete, return an error if random_variable_1.domain_type != random_variable_2.domain_type: discrete_domain_types = ["discrete", "discrete_functional"] if (random_variable_1.domain_type not in discrete_domain_types) and ( @@ -140,44 +138,10 @@ def convolution(random_variable_1, random_variable_2): raise RVError(err_string) random_variable_2 = Convert(random_variable_2) - # If the distributions are discrete, find and return the convolution - # of the two random variables. if random_variable_1.is_discrete(): - # Convert each random variable to its pdf form - left_pdf_rv = pdf(random_variable_1) - right_pdf_rv = pdf(random_variable_2) - # Create function and support lists for the convolution of the - # two random variables - convolution_support_candidates = [] - convolution_function_candidates = [] - for i in range(len(left_pdf_rv.support)): - for j in range(len(right_pdf_rv.support)): - convolution_support_candidates.append( - left_pdf_rv.support[i] + right_pdf_rv.support[j] - ) - convolution_function_candidates.append(left_pdf_rv.func[i] * right_pdf_rv.func[j]) - # Sort the function and support lists for the convolution - sorted_convolution_terms = list( - zip(convolution_support_candidates, convolution_function_candidates) - ) - sorted_convolution_terms.sort() - sorted_convolution_support = [] - sorted_convolution_functions = [] - for i in range(len(sorted_convolution_terms)): - sorted_convolution_support.append(sorted_convolution_terms[i][0]) - sorted_convolution_functions.append(sorted_convolution_terms[i][1]) - # Remove redundant elements in the support list - unique_convolution_support = [] - unique_convolution_functions = [] - for i in range(len(sorted_convolution_support)): - if sorted_convolution_support[i] not in unique_convolution_support: - unique_convolution_support.append(sorted_convolution_support[i]) - unique_convolution_functions.append(sorted_convolution_functions[i]) - else: - support_index = unique_convolution_support.index(sorted_convolution_support[i]) - unique_convolution_functions[support_index] += sorted_convolution_functions[i] - # Create and return the new random variable - return RV(unique_convolution_functions, unique_convolution_support, ["discrete", "pdf"]) + fast_rv_1 = random_variable_1.to_fast_rv() + fast_rv_2 = random_variable_2.to_fast_rv() + return RV.from_fast_rv(fast_rv_1 + fast_rv_2) def product(random_variable_1, random_variable_2): diff --git a/rust/src/python/api.rs b/rust/src/python/api.rs index bf298da..77b050d 100644 --- a/rust/src/python/api.rs +++ b/rust/src/python/api.rs @@ -279,7 +279,7 @@ impl FastRV { let self_rv = self.inner.clone(); let rhs_rv = rhs.inner.clone(); - let sum_rv = self_rv * rhs_rv; + let sum_rv = self_rv + rhs_rv; match sum_rv { Ok(rv) => {