Bacovia: a symbolic math library
Named after the great symbolist poet, George Bacovia, I created this library to symbolically manipulate mathematical expressions in a simple and elegant way.
Before writing a symbolic math library, this was a somewhat mysterious subject to me, but in this post I would like to try to demystify and illustrate the beauty and satisfaction that comes from writing this simple, but powerful, symbolic math library.
The first thing in creating a new project, is the selection of the right programming language for the project. Just for fun, I decided to implement this library in the Sidef programming language, using recursion and multiple dispatch; two very powerful features that turned out to be just perfect for this task.
Represents a symbolic value. Optionally, it can have a numeric value.
Represents a symbolic fraction.
Represents a symbolic difference.
Represents a symbolic exponentiation in a symbolic base.
Represents the natural logarithm of a symbolic value.
Represents the natural exponentiation of a symbolic value.
Represents a summation of an arbitrary (finite) number of symbolic values.
Example:
Based on the alternatives() method, the simple() method returns the shortest expression from the list of all the possible alternative expressions.
In selecting the shortest expressions, it also calls the pretty() method and computes the length of the human-readable text of each alternative expression. Then, based on this lengths, it selects the shortest alternative expression and returns it.
Example:
The library also supports numerical evaluation, recursively evaluating each expression numerically.
Example:
Most of the other classes are implemented in a similar way, which for brevity, I will not include them in this post.
Here, in the alternatives() method, we use the Cartesian product, which generates all the possible combinations of alternative representations. In combination with simple(), it can find the shortest possible alternative representation, based on the identities specified in each class.
Bellow I included a few examples to give the reader an idea for what can be done with this library. First, the library can be included using the include() keyword and the path to the main file, "bacovia.sf":
As a first example, we implement a closed-form to the Riemann zeta function for positive even integers, as defined here:
Output:
The second example illustrates the Fraction class, which includes some interesting identities for manipulating fractions in a non-reducible way.
In this third example we take a look at some trigonometric functions applied on symbolic values. First, let's define a symbolic value with an associated numerical value.
Sidef: https://github.com/trizen/bacovia
Perl: https://github.com/trizen/Math-Bacovia
The first library requires the Sidef programming language.
Before writing a symbolic math library, this was a somewhat mysterious subject to me, but in this post I would like to try to demystify and illustrate the beauty and satisfaction that comes from writing this simple, but powerful, symbolic math library.
The first thing in creating a new project, is the selection of the right programming language for the project. Just for fun, I decided to implement this library in the Sidef programming language, using recursion and multiple dispatch; two very powerful features that turned out to be just perfect for this task.
# Design
The library has a simple class hierarchy, with one base class, and several other classes inheriting from it.class {} class < {} class < {} class < {} class < {} class < {} class < {} class < {} class < {}
# Classes
The classes provided by the Bacovia library, are described bellow:
Symbol(name, value=nil)
Represents a symbolic value. Optionally, it can have a numeric value.
Fraction(numerator, denominator)
Represents a symbolic fraction.
Difference(minuend, subtrahend)
Represents a symbolic difference.
Power(base, power)
Represents a symbolic exponentiation in a symbolic base.
Log(x)
Represents the natural logarithm of a symbolic value.
Exp(x)
Represents the natural exponentiation of a symbolic value.
Sum(a, b, c, ...)
Represents a summation of an arbitrary (finite) number of symbolic values.
Product(a, b, c, ...)
Represents a product of an arbitrary (finite) number of symbolic values.
# Special methods
To make the library more interesting, I included some special methods that do some interesting stuff with the self-expression. This methods are described bellow.
* alternatives()
The most exciting feature is the support for alternative representations, which uses common mathematical identities to create symbolically equivalent expressions from the self-expression, returning an array with the alternative representations, each as a distinct object.
Example:((3) * 2) -> .each { .say }Output:
(((3), 2)) (3, 2) 9
* pretty()
This method returns a human-readable representation for the self-expression, recursively calling the pretty() method on each object value.
Example:
Exp(4)**(((1, 2), 3)) -> .say
Output:
exp(log((1 / 2)^3) * 4)
* simple()
Based on the alternatives() method, the simple() method returns the shortest expression from the list of all the possible alternative expressions.In selecting the shortest expressions, it also calls the pretty() method and computes the length of the human-readable text of each alternative expression. Then, based on this lengths, it selects the shortest alternative expression and returns it.
((((((('x'))))))) -> .sayOutput:
('x')
* numeric()
The library also supports numerical evaluation, recursively evaluating each expression numerically.Example:
((3, 42), 5) -> .sayOutput:
21883797826302471841.8
# Bacovia
The Bacovia base class implements all the generic operations, which are the default operations for all the other classes that inherit from it.class { method +( ) { (self, ) } method -( ) { (self, ) } method *(Bacovia ) { (self, ) } method /(Bacovia ) { (self, ) } method **(Bacovia ) { (self, ) } method { (0, self) } method { (1, self) } method { [self] } method { self.. { .. } } }
# Power
The Power class takes two arguments: the base and the exponent, which can be implemented something like this:class (, ) { method **( ) { (, * ) } method ==( ) { ( == .) && ( == .) } method ==() { } method { (, .) } method () { gather { for , (. ~ .) { take((() * )) take((, )) take( ** ) # Identity: x^log(y) = y^log(x) if (.()) { take(.**()) } } }. { . } } method { . ** . } method () { "(#{v.pretty})^(#{n.pretty})" } method { "Power(#{v}, #{n})" } }
# Product
The Product and the Sum classes are slightly different that the other classes: they can take an arbitrary number of arguments which are internally stored inside an array. The interesting part comes in manipulating all of this values symbolically.class (*values) { method *( ) { (values..., .values...) } method /( ) { (values..., .values.map{.}...) } method /( ) { = [values...] if (.()) { (...) } else { (..., .) } } method *( ) { = [values...] if (.(.)) { (...) } else { (..., ) } } method **( ) { (values.map{ ** }...) } method { Product(-1, values...) } method () { { values.map{.}. { |*| (.('*')) } }. { . } } method ==( ) { values == } method ==() { } method { (values.map { . }...) } method { values.map { . }. } method () { '(' + values.map{.}.join(' * ') + ')' } method { "Product(#{values.join(', ')})" } }
Here, in the alternatives() method, we use the Cartesian product, which generates all the possible combinations of alternative representations. In combination with simple(), it can find the shortest possible alternative representation, based on the identities specified in each class.
# Examples
The library can be used for various things, such as finding alternative representations for a certain expression, or for symbolically simplifying expressions, making them shorter and easier to write them down.Bellow I included a few examples to give the reader an idea for what can be done with this library. First, the library can be included using the include() keyword and the path to the main file, "bacovia.sf":
('lib/bacovia.sf')
As a first example, we implement a closed-form to the Riemann zeta function for positive even integers, as defined here:
= ((-1) * -2) () { (-1)**(+1) * (2*) * **(2*) / ((2*)! * 2) } for (1..5) { say (). }
Output:
log(-1)^2 * -4 * (1 / 6) * (1 / 4) log(-1)^4 * 16 * (1 / 30) * (1 / 48) log(-1)^6 * -64 * (1 / 42) * (1 / 1440) log(-1)^8 * 256 * (1 / 30) * (1 / 80640) log(-1)^10 * -1024 * (5 / 66) * (1 / 7257600)
The second example illustrates the Fraction class, which includes some interesting identities for manipulating fractions in a non-reducible way.
= (0, 1) for (0..3) { += (1**, +1) -> .say }Output:
( 1 + 0) / 1 ( 2 + 1) / 2 ( 4 + 3) / 6 (16 + 6) / 24(The real part of the numerator is A281964, and the imaginary part is A282132)
In this third example we take a look at some trigonometric functions applied on symbolic values. First, let's define a symbolic value with an associated numerical value.
= ('n', 42)Using the symbolic value defined above, we can start investigating two well-known trigonometric functions.
cos(()) -> ..say sin(()) -> ..sayOutput:
The library also has built-in support for hyperbolic trigonometric functions (including an inverse to each trigonometric function).(^- + ^) / 2 (^ - ^-) / (2)
(()) -> ..say (()) -> ..sayOutput:
When a symbolic value has a numeric value associated with it, we have the possibility of evaluating the expression numerically.(1 + ^2) / (2 * ) (^2 - 1) / (2 * )
cos() -> .say #=> -0.3999853149883512939... sin() -> .say #=> -0.9165215479156337858...Inverses to hyperbolic trigonometric functions:
(()) -> .say #=> 42 (()) -> .say #=> 42
# Implementations
The code of the library is freely available on GitHub.Sidef: https://github.com/trizen/bacovia
Perl: https://github.com/trizen/Math-Bacovia
The first library requires the Sidef programming language.
Comments
Post a Comment