Skip to content
Open
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
52fa102
feat: added common operations folding
shri-acha Jan 15, 2026
4efb87f
add: replaced parsed polynomial with folded polynomial
shri-acha Jan 15, 2026
628c0fa
refactor: reduced redundant code
shri-acha Jan 15, 2026
8a8cae5
Resolve requests for change
dawnandrew100 Jan 16, 2026
30da628
chore: merge fix
shri-acha Jan 16, 2026
26c5e89
feat: add passes for substitution of variables
shri-acha Jan 16, 2026
da0286b
chore: clippy fixes
shri-acha Jan 16, 2026
ad89c5c
chore: clippy fixes
shri-acha Jan 16, 2026
7d88344
refactor: Changed implementation for the evaluation function
shri-acha Jan 18, 2026
be760d9
chore: merge fix
shri-acha Jan 18, 2026
2ffc4b6
Merge branch 'main' into polynomial_traits
shri-acha Jan 18, 2026
141c784
feat: support for uni and multivariate evaluation
shri-acha Jan 22, 2026
0d56b05
add: additional test for missing variable mapping in case of multivar…
shri-acha Jan 22, 2026
890a7e0
Merge branch 'main' into polynomial_traits
dawnandrew100 Feb 3, 2026
0ca191d
chore: rename function and removed dependencies
shri-acha Feb 5, 2026
bc10e6c
Merge branch 'polynomial_traits' of github.com:shri-acha/spindalis in…
shri-acha Feb 5, 2026
11ee031
Merge branch 'main' into polynomial_traits
dawnandrew100 Mar 12, 2026
0955182
Add explicit match arms for advanced eval_univariate
dawnandrew100 Mar 13, 2026
a12f0f2
Merge branch 'polynomial_traits' of https://github.com/shri-acha/spin…
dawnandrew100 Mar 13, 2026
3c06902
Merge branch 'lignum-vitae:main' into main
shri-acha Mar 16, 2026
2fe6299
Merge branch 'main' of github.com:shri-acha/spindalis into polynomial…
shri-acha Apr 22, 2026
ef5ae3c
fix: added fix for unecessary clones
shri-acha Apr 28, 2026
89ed944
chore: lint
shri-acha Apr 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
272 changes: 262 additions & 10 deletions spindalis_core/src/polynomials/advanced.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use crate::polynomials::PolynomialError;
use crate::polynomials::structs::advanced::{Polynomial, TokenStream};
use std::collections::HashMap;
use std::collections::{BTreeSet, HashMap};
use std::f64;
use std::str::FromStr;
use std::sync::LazyLock;

Expand Down Expand Up @@ -106,7 +107,7 @@ macro_rules! token_from_char {

// declaring `Operators` with `token_from_char`
token_from_char! {
#[derive(Debug, PartialEq, Hash, Eq,Copy,Clone)]
#[derive(Debug, PartialEq,Eq,Copy,Clone,Hash)]
pub Operators {
Add => '+',
Sub => '-',
Expand Down Expand Up @@ -137,7 +138,7 @@ impl std::fmt::Display for Operators {

// declaring `Functions` with `token_from_str`
token_from_str! {
#[derive(Debug, PartialEq, Eq,Clone)]
#[derive(Debug, PartialEq,Clone,Copy)]
pub Functions {
Sin => "sin",
Cos => "cos",
Expand All @@ -164,7 +165,7 @@ impl std::fmt::Display for Functions {

// declaring `Constants` with `token_from_str`
token_from_str! {
#[derive(Debug, PartialEq, Eq,Clone)]
#[derive(Debug, PartialEq,Clone,Copy,)]
pub Constants {
Pi => "pi",
E => "e",
Expand Down Expand Up @@ -280,8 +281,7 @@ impl std::fmt::Display for Expr {
}
}

#[allow(dead_code)]
fn lexer<S>(input: S) -> Result<Vec<Token>, PolynomialError>
pub fn lexer<S>(input: S) -> Result<Vec<Token>, PolynomialError>
where
S: AsRef<str>,
{
Expand Down Expand Up @@ -422,7 +422,6 @@ fn implied_multiplication_pass(token_stream: &mut Vec<Token>) {
}
}

#[allow(dead_code)]
fn parse_expr(token_stream: &mut TokenStream, min_bind_pow: f64) -> Result<Expr, PolynomialError> {
let mut left = match token_stream.next() {
Some(Token::Number(n)) => Ok(Expr::Number(n)),
Expand Down Expand Up @@ -505,8 +504,7 @@ fn parse_expr(token_stream: &mut TokenStream, min_bind_pow: f64) -> Result<Expr,
Ok(left)
}

#[allow(dead_code)]
fn parser(token_stream: Vec<Token>) -> Result<Polynomial, PolynomialError> {
pub fn parser(token_stream: Vec<Token>) -> Result<Polynomial, PolynomialError> {
let mut tokens = token_stream;
implied_multiplication_pass(&mut tokens);
let mut token_stream = tokens.into_iter().peekable();
Expand Down Expand Up @@ -581,6 +579,169 @@ impl From<&'static str> for Expr {
}
}

pub fn eval_advanced_polynomial<V, S, F>(
poly: &Polynomial,
variables: &V,
) -> Result<f64, PolynomialError>
where
V: IntoIterator<Item = (S, F)> + std::fmt::Debug + Clone,
S: AsRef<str>,
F: Into<f64>,
{
let vars_map: HashMap<String, f64> = variables
.clone()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might be able to replace this clone too if we implement the visitor pattern mentioned in one of my other comments

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unsure of how to remove this clone and I'm not sure this is related to visitor pattern as well.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this shouldn't be much of a problem as considering there will be only a few variables, what do you think of it?

.into_iter()
.map(|(k, v)| (k.as_ref().to_string(), v.into()))
.collect();
let literal_expr = replace_variable_occurence(&poly.expr, &vars_map)?;
evaluate_numerical_expression(&literal_expr).ok_or(PolynomialError::MissingVariable) // TODO: Work on error messages
}

fn replace_variable_occurence(
expr: &Expr,
vars: &HashMap<String, f64>,
) -> Result<Expr, PolynomialError> {
expr.clone().map(&mut |e| match e {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another clone that might be removed

Expr::Variable(v) => {
vars.get(&v)
.copied()
.map(Expr::Number)
.ok_or(PolynomialError::VariableNotFound {
variable: v.to_string(),
})
}
x => Ok(x),
})
}

impl Expr {
pub fn map(
self,
f: &mut impl FnMut(Expr) -> Result<Expr, PolynomialError>,
) -> Result<Expr, PolynomialError> {
match self {
// recursively walks down the polynomial for operators
Expr::BinaryOp {
op,
lhs,
rhs,
paren,
} => Ok(Expr::BinaryOp {
op,
lhs: Box::new(lhs.map(f)?),
rhs: Box::new(rhs.map(f)?),
paren,
}),
Expr::UnaryOpPrefix { op, value } => Ok(Expr::UnaryOpPrefix {
op,
value: Box::new(value.map(f)?),
}),
Expr::UnaryOpPostfix { op, value } => Ok(Expr::UnaryOpPostfix {
op,
value: Box::new(value.map(f)?),
}),
Expr::Function { func, inner } => Ok(Expr::Function {
func,
inner: Box::new(inner.map(f)?),
}),
// This allows for extension of variants with f.
x => f(x),
}
}
}

pub fn extract_univariate_variable(expr: &Expr) -> Result<String, PolynomialError> {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should also probably take a look at the scoping of these various functions. Like this function might be good as pub(crate) instead of just pub, but this can be discussed whether that's a good idea or not

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah, I mean, are we expecting on introducing the api seperately? If so, then this would be fine, else I'll have to scope it down for pub (crate).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What specifically do you mean by introducing the API separately??

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nvm, I assumed if it we were to introduce an ability to provide function like 'extract_univariate_variable' as a part of the library.

let mut variables: BTreeSet<String> = Default::default();
let _ = expr.clone().map(&mut |e| match &e {
Expr::Variable(v) => {
variables.insert(v.to_string());
Ok(e)
}
expr => Ok(expr.clone()),
});
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of cloning the entire expression tree, is there another way to traverse the tree without needing to clone (like using the visitor pattern for example)

if variables.len() > 1 {
Err(PolynomialError::TooManyVariables {
variables: variables.into_iter().collect::<Vec<_>>(),
})
} else {
variables
.clone()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to remove this clone?

.into_iter()
.next()
.map_or_else(|| Err(PolynomialError::MissingVariable), Ok)
}
}

pub(crate) fn evaluate_numerical_expression(expr: &Expr) -> Option<f64> {
match expr {
Expr::Number(v) => Some(*v),
Expr::BinaryOp { op, lhs, rhs, .. } => handle_binary_operation(op, lhs, rhs),
Expr::UnaryOpPostfix { op, value } => handle_postfix_operation(op, value),
Expr::UnaryOpPrefix { op, value } => handle_prefix_operation(op, value),
Expr::Function { func, inner } => handle_function(func, inner),
Expr::Constant(v) => handle_constants(v),
_ => None,
}
}

fn handle_binary_operation(op: &Operators, lhs: &Expr, rhs: &Expr) -> Option<f64> {
let lhs = evaluate_numerical_expression(lhs)?;
let rhs = evaluate_numerical_expression(rhs)?;
match op {
Operators::Div => Some(lhs / rhs),
Operators::Mul | Operators::CDot => Some(lhs * rhs),
Operators::Add => Some(lhs + rhs),
Operators::Sub => Some(lhs - rhs),
Operators::Rem => Some(lhs % rhs),
Operators::Caret => Some(lhs.powf(rhs)),
_ => None,
}
}

fn factorial_f64(n: f64) -> f64 {
if n < 0.0 {
return f64::NAN;
}
let n_int = n.floor() as u64;
(1..=n_int).fold(1.0, |acc, x| acc * x as f64)
}

fn handle_postfix_operation(op: &Operators, value: &Expr) -> Option<f64> {
match op {
Operators::Fac => Some(factorial_f64(evaluate_numerical_expression(value)?)),
_ => None,
}
}

fn handle_prefix_operation(op: &Operators, value: &Expr) -> Option<f64> {
let value = evaluate_numerical_expression(value)?;
match op {
Operators::Add => Some(value),
Operators::Sub => Some(-value),
_ => None,
}
}
fn handle_function(func: &Functions, value: &Expr) -> Option<f64> {
let value = evaluate_numerical_expression(value)?;
Some(match func {
Functions::Sin => value.sin(),
Functions::Cos => value.cos(),
Functions::Tan => value.tan(),
Functions::Cot => 1.0 / value.tan(),
Functions::Ln => value.ln(),
Functions::Log => value.log10(),
})
}

fn handle_constants(cnst: &Constants) -> Option<f64> {
Some(match cnst {
Constants::Pi => f64::consts::PI,
Constants::E => f64::consts::E,
Constants::Tau => f64::consts::TAU,
Constants::Phi => 1.618_033_988_749_895_f64, // f64::consts::PHI
})
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -1498,6 +1659,97 @@ mod tests {
let expected = Polynomial::new(Expr::Number(0.));
assert_eq!(result, expected);
}
#[test]
fn test_failing_substitution_expression() {
let expr = "x^3-3xy+5";
let tok_str = lexer(expr).unwrap();
let parsed_result = parser(tok_str).unwrap();
let evaluated_result = eval_advanced_polynomial(&parsed_result, &[("x", 5)]);
assert!(evaluated_result.is_err());
}
#[test]
fn test_eval_expression() {
let expr = "x^3-3xy+5!";
let tok_str = lexer(expr).unwrap();
let parsed_result = parser(tok_str).unwrap();
let evaluated_result =
eval_advanced_polynomial(&parsed_result, &[("x", 5), ("y", 5)]).unwrap();
println!("{}", parsed_result);
println!("{}", evaluated_result);
assert_eq!(evaluated_result, 170.0);
}

#[test]
fn test_univariance_of_an_expression() {
let expr = "x^3-3x+5!";
let tok_str = lexer(expr).unwrap();
let parsed_result = parser(tok_str).unwrap();
let evaluated_result = extract_univariate_variable(&parsed_result.expr).unwrap();
println!("{}", parsed_result);
println!("{}", evaluated_result);
assert_eq!(evaluated_result, String::from("x"));
}

#[test]
fn test_too_many_variables_expression() {
let expr = "x^3-3y+5!";
let tok_str = lexer(expr).unwrap();
let parsed_result = parser(tok_str).unwrap();
let evaluated_result = extract_univariate_variable(&parsed_result.expr);
println!("{}", parsed_result);
println!("{:?}", evaluated_result);
assert!(evaluated_result.is_err());
}

#[test]
fn test_univariant_evaluation() {
let expr = "z^3-3z+5!";
let poly = Polynomial::parse(expr).unwrap();
let evaluated_result = poly.eval_univariate(10).unwrap();
println!("{}", poly);
println!("{:?}", evaluated_result);
assert_eq!(evaluated_result, 1090.);
}

#[test]
fn test_univariant_failure_evaluation() {
let expr = "-3x+5y!";
let poly = Polynomial::parse(expr).unwrap();
let evaluated_result = poly.eval_univariate(1);
println!("{}", poly);
println!("{:?}", evaluated_result);
assert!(evaluated_result.is_err());
}

#[test]
fn test_univariant_no_variables_evaluation() {
let expr = "-3+5!";
let poly = Polynomial::parse(expr).unwrap();
let evaluated_result = poly.eval_univariate(1).unwrap();
println!("{}", poly);
println!("{:?}", evaluated_result);
assert_eq!(evaluated_result, 117.0);
}

#[test]
fn test_multivariant_expression() {
let expr = "2x+3y!";
let poly = Polynomial::parse(expr).unwrap();
let evaluated_result = poly.eval_multivariate(&[("x", 2), ("y", 1)]).unwrap();
println!("{}", poly);
println!("{:?}", evaluated_result);
assert_eq!(evaluated_result, 7.0);
}

#[test]
fn test_multivariant_missing_mapping_expression() {
let expr = "2x+3y!";
let poly = Polynomial::parse(expr).unwrap();
let evaluated_result = poly.eval_multivariate(&[("x", 2)]);
println!("{}", poly);
println!("{:?}", evaluated_result);
assert!(evaluated_result.is_err());
}
}
// ---------------------------
// Test Display
Expand Down Expand Up @@ -1651,7 +1903,7 @@ mod tests {
use super::*;

token_from_str! {
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, PartialEq)]
pub TestEnum {
Alpha => "alpha",
Beta => "beta",
Expand Down
Loading