-
New Simplification Engine
The way expressions are simplified has been completely rewritten. The new
engine is more powerful and more flexible.
The core API remains the same: to simplify an expression, use
expr.simplify()
.
To use a custom set of rules, pass the rules as an argument to simplify()
:
expr.simplify({rules: [
"|x:<0| -> -x",
"|x:>=0| -> x",
]});
There are a few changes to the way rules are represented. The priority
property has been removed. Instead, rules are applied in the order in which
they are defined.
A rule can also now be a function that takes an expression and returns a new
expression. For example:
expr.simplify({rules: [
(expr) => {
if (expr.operator !== 'Abs') return undefined;
const x = expr.args[0];
return x.isNegative ? x.negate() : expr;
}
]});
This can be used to perform more complex transformations at the cost of more
verbose JavaScript code.
The algorithm for simplification has been simplified. It attempts to apply
each rule in the rule set in turn, then restarts the process until no more
rules can be applied or the result of applying a rule returns a previously
seen expression.
Function definitions previously included a simplify
handler that could be
used to perform simplifications specific to this function. This has been
removed. Instead, use a rule that matches the function and returns the
simplified expression.
-
Types
Previously, an expression was associated with a domain such as RealNumbers
or ComplexNumbers
. This has been replaced with a more flexible system of
types.
A type is a set of values that an expression can take. For example, the type
real
is the set of real numbers, the type integer
is the set of integers,
The type of an expression can be set with the type
property. For example:
const expr = ce.parse('\\sqrt{-1}');
console.info(expr.type); // -> imaginary
The type of a symbol can be set when declaring the symbol. For example:
ce.declare('x', 'imaginary');
In addition to primitive types, the type system supports more complex types
such union types, intersection types, and function types.
For example, the type real|imaginary
is the union of the real and imaginary
numbers.
When declaring a function, the type of the arguments and the return value can
be specified. For example, to declare a function f
that takes two integers
and returns a real number:
ce.declare('f', '(integer, integer) -> real');
The sets of numbers are defined as follows:
number
- any number, real or complex, including NaN and infinity
non_finite_number
- NaN or infinity
real
finite_real
- finite real numbers (exclude NaN and infinity)
imaginary
- imaginary numbers (complex numbers with a real part of 0)
finite_imaginary
complex
- complex numbers with a real and imaginary part not equal to 0
finite_complex
rational
finite_rational
integer
finite_integer
To check the type of an expression, use the isSubtypeOf()
method. For
example:
let expr = ce.parse('5');
console.info(expr.type.isSubtypeOf('rational')); // -> true
console.info(expr.type.isSubtypeOf('integer')); // -> true
expr = ce.parse('\\frac{1}{2}');
console.info(expr.type.isSubtypeOf('rational')); // -> true
console.info(expr.type.isSubtypeOf('integer')); // -> false
As a shortcut, the properties isReal
, isRational
, isInteger
are
available on boxed expressions. For example:
let expr = ce.parse('5');
console.info(expr.isInteger); // -> true
console.info(expr.isRational); // -> true
They are equivalent to expr.type.isSubtypeOf('integer')
and
expr.type.isSubtypeOf('rational')
respectively.
To check if a number has a non-zero imaginary part, use:
let expr = ce.parse('5i');
console.info(expr.isNumber && expr.isReal === false); // -> true
-
Collections
Support for collections has been improved. Collections include List
, Set
,
Tuple
, Range
, Interval
, Linspace
and Dictionary
.
It is now possible to check if an element is contained in a collection using
an Element
expression. For example:
let expr = ce.parse('[1, 2, 3]');
ce.box(['Element', 3, expr]).print(); // -> True
ce.box(['Element', 5, expr]).print(); // -> False
To check if a collection is a subset of another collection, use the Subset
expression. For example:
ce.box(['Subset', 'Integers', 'RealNumbers']).print(); // -> True
Collections can also be compared for equality. For example:
let set1 = ce.parse('\\lbrace 1, 2, 3 \\rbrace');
let set2 = ce.parse('\\lbrace 3, 2, 1 \\rbrace');
console.info(set1.isEqual(set2)); // -> true
There are also additional convenience methods on boxed expressions:
expr.isCollection
expr.contains(element)
expr.size
expr.isSubsetOf(other)
expr.indexOf(element)
expr.at(index)
expr.each()
expr.get(key)
-
Exact calculations
The Compute Engine has a new backed for numerical calculations. The new backed
can handle arbitrary precision calculations, including real and complex
numbers. It can also handle exact calculations, preserving calculations with
rationals and radicals (square root of integers). For example 1/2 + 1/3
is
evaluated to 5/6
instead of 0.8(3)
.
To get an approximate result, use the N()
method, for example
ce.parse("\\frac12 + \\frac13").N()
.
Previously the result of calculations was not always an exact number but
returned a numerical approximation instead.
This has now been improved by introducing a NumericValue
type that
encapsulates exact numbers and by doing all calculations in this type.
Previously the calculations were handled manually in the various evaluation
functions. This made the code complicated and error prone.
A NumericValue
is made of:
- an imaginary part, represented as a fixed-precision number
- a real part, represented either as a fixed or arbitrary precision number or
as the product of a rational number and the square root of an integer.
For example:
- 234.567
- 1/2
- 3√5
- √7/3
- 4-3i
While this is a significant change internally, the external API remains the
same. The result of calculations should be more predictable and more accurate.
One change to the public API is that the expr.numericValue
property is now
either a machine precision number or a NumericValue
object.
-
Rule Wildcards
When defining a rule as a LaTeX expression, single character identifiers are
interpreted as wildcards. For example, the rule x + x -> 2x
will match any
expression with two identical terms. The wildcard corresponding to x
is
_x
.
It is now possible to define sequence wildcards and optional sequence
wildcards. Sequence wildcards match 1 or more expressions, while optional
sequence wildcards match 0 or more expressions.
They are indicated in LaTeX as ...x
and ...x?
respectively. For example:
expr.simplify("x + ...y -> 2x");
If expr
is a + b + c
the rule will match and return 2a
expr.simplify("x + ...y? -> 3x");
If expr
is a + b + c
the rule will match and return 3a
. If expr
is a
the rule will match and return 3a
.
-
Conditional Rules
Rules can now include conditions that are evaluated at runtime. If the
condition is not satisfied, the rules does not apply.
For example, to simplify the expression |x|
:
expr.simplify({rules: [
"|x_{>=0}| -> x",
"|x_{<0}| -> -x",
]});
The condition is indicated as a subscript of the wildcard. The condition can
be one of:
-
boolean
- a boolean value, True or False
-
string
- a string of characters
-
number
- a number literal
-
symbol
-
expression
-
numeric
- an expression that has a numeric value, i.e. 2√3, 1/2, 3.14
-
integer
- an integer value, -2, -1, 0, 1, 2, 3, ...
-
natural
- a natural number, 0, 1, 2, 3, ...
-
real
- real numbers, including integers
-
imaginary
- imaginary numbers, i.e. 2i, 3√-1 (not including real numbers)
-
complex
- complex numbers, including real and imaginary
-
rational
- rational numbers, 1/2, 3/4, 5/6, ...
-
irrational
- irrational numbers, √2, √3, π, ...
-
algebraic
- algebraic numbers, rational and irrational
-
transcendental
- transcendental numbers, π, e, ...
-
positive
- positive real numbers, > 0
-
negative
- negative real numbers, < 0
-
nonnegative
- nonnegative real numbers, >= 0
-
nonpositive
- nonpositive real numbers, <= 0
-
even
- even integers, 0, 2, 4, 6, ...
-
odd
- odd integers, 1, 3, 5, 7, ...
-
prime
:A000040 - prime numbers, 2, 3, 5, 7, 11, ...
-
composite
:A002808 - composite numbers, 4, 6, 8, 9, 10, ...
-
notzero
- a value that is not zero
-
notone
- a value that is not one
-
finite
- a finite value, not infinite
-
infinite
-
constant
-
variable
-
function
-
operator
-
relation
- an equation or inequality
-
equation
-
inequality
-
vector
- a tensor of rank 1
-
matrix
- a tensor of rank 2
-
list
- a collection of values
-
set
- a collection of unique values
-
tuple
- a fixed length list
-
single
- a tuple of length 1
-
pair
- a tuple of length 2
-
triple
- a tuple of length 3
-
collection
- a list, set, or tuple
-
tensor
- a nested list of values of the same type
-
scalar
- not a tensor or list
or one of the following expressions:
>0'
-> positive
,
\gt0'
-> positive
,
<0'
-> negative
,
\lt0'
-> negative
,
>=0'
-> nonnegative
,
\geq0'
-> nonnegative
,
<=0'
-> nonpositive
,
\leq0'
-> nonpositive
,
!=0'
-> notzero
,
\neq0'
-> notzero
,
!=1'
-> notone
,
\neq1'
-> notone
,
\in\Z'
-> integer
,
\in\mathbb{Z}'
-> integer
,
\in\N'
-> natural
,
\in\mathbb{N}'
-> natural
,
\in\R'
-> real
,
\in\mathbb{R}'
-> real
,
\in\C'
-> complex
,
\in\mathbb{C}'
-> complex
,
\in\Q'
-> rational
,
\in\mathbb{Q}'
-> rational
,
\in\Z^+'
-> integer,positive
,
\in\Z^-'
-> intger,negative
,
\in\Z^*'
-> nonzero
,
\in\R^+'
-> positive
,
\in\R^-'
-> negative
,
\in\R^*'
-> real,nonzero
,
\in\N^*'
-> integer,positive
,
\in\N_0'
-> integer,nonnegative
,
\in\R\backslash\Q'
-> irrational
,
More complex conditions can be specified following a semi-colon, for example:
expr.simplify({x -> 2x; x < 10});
Note that this syntax complements the existing rule syntax, and can be used
together with the existing, more verbose, rule syntax.
expr.simplify({rules: [
{match: "x + x", replace: "2x", condition: "x < 10"}
]});
This advanced syntax can specify more complex conditions, for example above
the rule will only apply if x
is less than 10.
-
Improved results for Expand
. In some cases the expression was not fully
expanded. For example, 4x(3x+2)-5(5x-4)
now returns 12x^2 - 17x + 20
.
Previously it returned 4x(3x+2)+25x-20
.
-
AsciiMath serialization The expr.toString()
method now returns a
serialization of the expression using the AsciiMath
format.
The serialization to AsciiMath can be customized using the toAsciiMath()
method. For example:
console.log(ce.box(['Sigma', 2]).toAsciiMath({functions: {Sigma: 'sigma'}}));
// -> sigma(2)
-
The tolerance can now be specified with a value of "auto"
which will use the
precision to determine a reasonable tolerance. The tolerance is used when
comparing two numbers for equality. The tolerance can be specified with the
ce.tolerance
property or in the Compute Engine constructor.
-
Boxed expressions have some additional properties:
expr.isNumberLiteral
- true if the expression is a number literal.This is
equivalent to checking if expr.numericValue
is not null
.
expr.re
- the real part of the expression, if it is a number literal,
undefined
if not a number literal.
expr.im
- the imaginary part of the expression, if it is a number literal,
undefined
if not a number literal.
expr.bignumRe
- the real part of the expression as a bignum, if it is a
number literal, undefined
if not a number literal or a bignum
representation is not available.
expr.bignumIm
- the imaginary part of the expression as a bignum, if it is
a number literal, undefined
if not a number literal or if a bignum
representation is not available.
expr.root()
to get the root of the expression. For example, expr.root(3)
will return the cube root of the expression.
- Additionally, the relational operators (
expr.isLess(), expr.isEqual()
,
etc...) now accept a number argument. For example, expr.isGreater(1)
will
return true if the expression is greater than 1.
-
Added LaTeX syntax to index collections. If a
is a collection:
a[i]
is parsed as ["At", "a", "i"]
.
a[i,j]
is parsed as ["At", "a", "i", "j"]
.
a_i
is parsed as ["At", "a", "i"]
.
a_{i,j}
is parsed as ["At", "a", "i", "j"]
.
-
Added support for Kronecker delta notation, i.e. \delta_{ij}
, which is
parsed as ["KroneckerDelta", "i", "j"]
and is equal to 1 if i = j
and 0
otherwise.
When a single index is provided the value of the function is 1 if the index is
0 and 0 otherwise
When multiple index are provided, the value of the function is 1 if all the
indexes are equal and 0 otherwise.
-
Added support for Iverson Bracket notation, i.e. [a = b]
, which is parsed as
["Boole", ["Equal", "a", "b"]]
and is equal to 1 if its argument is true and
0 otherwise. The argument is expected to be a relational expression.
-
Implemented Unique
and Tally
on collections. Unique
returns a collection
with only the unique elements of the input collection, and Tally
returns a
collection with the count of each unique element.
console.log(ce.box(['Unique', ['List', 1, 2, 3, 1, 2, 3, 4, 5]]).value);
// -> [1, 2, 3, 4, 5]
console.log(ce.box(['Tally', ['List', 1, 2, 3, 1, 2, 3, 4, 5]]).value);
// -> [['List', 1, 2, 3, 4, 5], ['List', 2, 2, 2, 1, 1]]
-
Implemented the Map
, Filter
and Tabulate
functions. These functions can
be used to transform collections, for example:
// Using LaTeX
console.log(ce.parse('\\mathrm{Map}([3, 5, 7], x \\mapsto x^2)').toString());
// -> [9, 25, 49]
// Using boxed expressions
console.log(
ce.box(['Map', ['List', 3, 5, 7], ['Square', '_']]).value
);
// -> [9, 25, 49]
console.log(ce.box(['Tabulate',['Square', '_'], 5]).value);
// -> [1, 4, 9, 16, 25]
Tabulate
can be used with multiple indexes. For example, to generate a 4x4
unit matrix:
console.log(ce.box(['Tabulate', ['If', ['Equal', '_1', '_2'], 1, 0]], 4, 4).value);
// -> [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
// Using the Kronecker delta notation:
console.log(ce.parse('\\mathrm{Tabulate}(i, j \\mapsto \\delta_{ij}, 4, 4)').value);
// -> [[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, 1]]
-
Added Random
function. ["Random"]
returns a real pseudo-random number
betwen 0 and 1. ["Random", 10]
returns an integer between 0 and 9,
["Random", 5, 10]
returns an integer between 5 and 10.
-
Extended the definition of expr.isConstant
. Previously, it only applied to
symbols, e.g. Pi
. Now it apply to all expressions. expr.isConstant
is true
if the expression is a number literal, a symbol with a constant value, or a
pure function with constant arguments.
-
The boxed expression properties isPositive
, isNegative
, isNonNegative
,
isNonPositive
, isZero
, isNotZero
now return a useful value for most
function expressions. For example, ce.parse('|x + 1|').isPositive
is true.
If the value cannot be determined, the property will return undefined
. For
example, ce.parse('|x + 1|').isZero
is undefined
.
If the expression is not a real number, the property will return NaN
. For
example, ce.parse('i').isPositive
is NaN
.
-
Added Choose
function to compute binomial coefficients, i.e. Choose(5, 2)
is equal to 10.
-
The fallback for non-constructible complex values of trigonometric functions
is now implemented via rules.
-
The canonical order of the arguments has changed and should be more consistent
and predictable. In particular, for polynomials, the
monomial order is now
degrevlex.
-
Canonical expressions can now include a Root
expression. For example, the
canonical form of \\sqrt[3]{5}
is ["Root", 5, 3]
. Previously, these were
represented as ["Power", 5, ["Divide", 1, 3]]
.
-
The function definitions no longer have a N
handler. Instead the evaluate
handler has an optional {numericApproximation}
argument.