#math #parser #expression #eval #differentiation

exmex

fast, simple, and extendable mathematical expression evaluator able to compute partial derivatives

25 releases (11 breaking)

0.16.0 May 1, 2022
0.15.0 Feb 26, 2022
0.14.0 Jan 9, 2022
0.13.0 Dec 31, 2021
0.8.0 Jul 30, 2021

#81 in Parser implementations

Download history 7/week @ 2022-01-29 8/week @ 2022-02-05 7/week @ 2022-02-12 34/week @ 2022-02-19 19/week @ 2022-02-26 7/week @ 2022-03-05 60/week @ 2022-03-12 110/week @ 2022-03-19 5/week @ 2022-03-26 4/week @ 2022-04-02 29/week @ 2022-04-09 3/week @ 2022-04-16 30/week @ 2022-04-23 146/week @ 2022-04-30 121/week @ 2022-05-07 430/week @ 2022-05-14

728 downloads per month

MIT/Apache

185KB
4K SLoC

Crate API example workflow license

Exmex

Exmex is an extendable mathematical expression parser and evaluator. Ease of use, flexibility, and efficient evaluations are its main design goals. Exmex can parse mathematical expressions possibly containing variables and operators. On the one hand, it comes with a list of default operators for floating point values. For differentiable default operators, Exmex can compute partial derivatives. On the other hand, users can define their own operators and work with different data types such as float, integer, bool, or other types that implement Clone, FromStr, and Debug.

Parts of Exmex' functionality are accessible from Python via Mexpress.

Installation

Add

[dependencies]
# ...
exmex = "0.16.0"

to your Cargo.toml for the latest relase. If you want to use the newest version of Exmex, add

[dependencies]
# ...
exmex = { git = "https://github.com/bertiqwerty/exmex.git", branch = "main" }

to your Cargo.toml.

Basic Usage

To simply evaluate a string there is

use exmex;
let result = eval_str::<f64>("e^(2*π-τ)")?;
assert!((result - 1.0).abs() < 1e-12);

where π/PI, τ/TAU, and Euler's number E/e are available as constants. To create an expression with variables that represents a mathematical function you can use any string that does not define an operator or constant and matches r"[a-zA-Zα-ωΑ-Ω_]+[a-zA-Zα-ωΑ-Ω_0-9]*" as in

use exmex::prelude::*;
let expr = exmex::parse::<f64>("2*x^3-4/y")?;

The wildcard-import from prelude makes only the expression-trait Express and its implementation FlatEx, a flattened expression, accessible. To use variables, you do not need to use a context or tell the parser explicitly what variables are. To evaluate the function at, e.g., x=2.0 and y=4.0 you can use

let result = expr.eval(&[2.0, 4.0])?;
assert!((result - 15.0).abs() < 1e-12);

The order of the variables' values passed for evaluation has to match the alphabetical order of the variable names.

Besides predefined operators for floats, you can implement custom operators and use their factory type as generic argument as shown in the following example.

use exmex::prelude::*;
use exmex::{BinOp, MakeOperators, Operator};
ops_factory!(
    BitwiseOpsFactory,
    u32,
    Operator::make_bin(
        "|",
        BinOp {
            apply: |a, b| a | b,
            prio: 0,
            is_commutative: true,
        }
    ),
    Operator::make_unary("!", |a| !a)
);
let expr = FlatEx::<_, BitwiseOpsFactory>::from_str("!(a|b)")?;
let result = expr.eval(&[0, 1])?;
assert_eq!(result, u32::MAX - 1);

More involved examples of data types are

Partial Differentiation

To compute partial derivatives of expressions with floating point numbers, you can use the method partial after activating the Exmex-feature partial in the Cargo.toml via

[dependencies]
exmex = { ..., features = ["partial"] }

The result of the method partial is again an expression.

use exmex::prelude::*;
let expr = exmex::parse::<f64>("y*x^2")?;

// d_x
let dexpr_dx = expr.partial(0)?;
assert_eq!(format!("{}", dexpr_dx), "({x}*2.0)*{y}");

// d_xy
let ddexpr_dxy = dexpr_dx.partial(1)?;
assert_eq!(format!("{}", ddexpr_dxy), "{x}*2.0");
let result = ddexpr_dxy.eval(&[2.0, f64::MAX])?;
assert!((result - 4.0).abs() < 1e-12);

// d_xyx
let dddexpr_dxyx = ddexpr_dxy.partial(0)?;
assert_eq!(format!("{}", dddexpr_dxyx), "2.0");
let result = dddexpr_dxyx.eval(&[f64::MAX, f64::MAX])?;
assert!((result - 2.0).abs() < 1e-12);

// all in one
let dddexpr_dxyx_iter = expr.partial_iter([0, 1, 0].iter())?;
assert_eq!(format!("{}", dddexpr_dxyx_iter), "2.0");
let result = dddexpr_dxyx_iter.eval(&[f64::MAX, f64::MAX])?;
assert!((result - 2.0).abs() < 1e-12);

Mixing Data Types in one Expression with the Feature value

After activating the Exmex-feature value one can use expressions with data of type Val, inspired by the type Value from the crate Evalexpr. An instance of Val can contain a boolean, an int, or a float. This way, it is possible to use booleans, ints, and floats in the same expression. Further, Exmex provides in terms of ValOpsFactory a pre-defined set of operators for Val. See the following example of a Python-like if-else-operator.

use exmex::{Express, Val};
let expr = exmex::parse_val::<i32, f64>("0 if b < c else 1.2")?;
let res = expr.eval(&[Val::Float(34.0), Val::Int(21)])?.to_float()?;
assert!((res - 1.2).abs() < 1e-12);

Serialization and Deserialization

To use serde activate the feature serde.

Documentation

More documentation and examples including integer data types and boolean literals can be found for the latest release under docs.rs/exmex/ or generated via

cargo doc --all-features

Benchmarks v0.13.0

Exmex was created with flexibility (e.g., use your own operators, literals, and types), ergonomics (e.g., just finds variables), and evaluation speed in mind. On the other hand, Exmex is slower than the other crates during parsing. However, evaluation might be more performance critical depending on the application.

The expressions used to compare Exmex with other creates are:

sin:     "sin(x)+sin(y)+sin(z)",
power:   "x^2+y*y+z^z",
nested:  "x*0.02*sin(-(3*(2*sin(x-1/(sin(y*5)+(5.0-1/z))))))",
compile: "x*0.2*5/4+x*2*4*1*1*1*1*1*1*1+7*sin(y)-z/sin(3.0/2/(1-x*4*1*1*1*1))",

The following table shows mean runtimes of 5-evaluation-runs with increasing x-values on a Win10 machine with an i7-10850H 2.7 GHz processor in micro-seconds, i.e., smaller means better. Criterion-based benchmarks can be executed via

cargo bench --bench benchmark -- --noplot --sample-size 10 --nresamples 10

to compute the results. Reported is the best result over multiple invocations. More about taking the minimum run-time for benchmarking can be found below.

sin power nested compile comment
Evalexpr 5.88 4.51 19.36 21.11 more than mathematical expressions
Exmex f64 0.27 0.5 0.57 0.53 can compute partial derivatives
Exmex uncompiled f64 0.27 0.5 0.57 1.17 can compute partial derivatives
Exmex Val 0.77 1.13 1.87 1.73 multiple data types in one expression possible
Fasteval 1.19 1.46 1.59 1.6 only f64, supports a faster, unsafe mode
Meval 0.65 0.66 0.82 1.01 only f64, no custom operators
Rsc 4.88 8.21 13.32 24.28

Note that we also tried the optimization flag --emit=asm which did not change the results qualitatively. Benchmarks for parsing all expressions again in μs on the aforementioned machine are shown in the following.

all expressions
Evalexpr 35.94
Exmex f64 24.83
Exmex uncompiled f64 21.56
Exmex Val 37.45
Fasteval 18.42
Meval 17.99
Rsc 20.50

Exmex parsing can be made faster by passing only the relevant operators.

The crates Mexprp and Asciimath did not run without errors on Win10. More details about the benchmarking can be found in the source file.

Note that Criterion does not provide the option to simply report the minimum runtime. A talk by Andrei Alexandrescu explains why I think taking the minimum is a good idea in many cases. See also https://github.com/bheisler/criterion.rs/issues/485.

License

You as library user can select between MIT and Apache 2.0.

Dependencies

~1.4–2MB
~50K SLoC