Symbolic Computing

The CortexJS Compute Engine essentially performs computation by applying rewriting rules to a MathJSON expression.

Note: To use the Compute Engine you must write JavaScript or TypeScript code. This guide assumes you are familiar with one of these programming languages.

Note: In this guide, functions such as ce.box() and ce.parse() require a ComputeEngine instance which is denoted by a ce. prefix.
Functions that apply to a boxed expression, such as expr.simplify() are denoted with a expr. prefix.

There are three common transformations that can be applied to an expression:

Transformation
expr.simplify() Eliminate constants and common sub-expressions. Use available assumptions to determine which rules are applicable. Limit calculations to exact results.
expr.evaluate() Calculate the exact value of an expression. Replace symbols with their value.
expr.N() Calculate a numeric approximation of an expression using floating point numbers.

A key difference between expr.evaluate() and expr.N() is that the former will use the exact value of symbols, while the latter will use their numeric approximation. An exact value is a rational number, an integer, the square root of a rational and some constants such as \(\pi\) or \(e\). A numeric approximation is a floating point number.

expr.simplify() expr.evaluate() expr.N()
Use assumptions on symbols
Exact calculations
Floating-point approximations

For example:

const f = ce.parse('2 + (\\sqrt{4x} + 1)');
ce.assign('x', 'Pi');
console.log(f.simplify().latex); // 2\sqrt{x}+3
console.log(f.evaluate().latex); // 2\sqrt{\pi}+3
console.log(f.N().latex); // 9.283\,185\,307\ldots
f.simplify() \[ \sqrt{x}+3 \] Exact calculations, simplification
f.evaluate() \[ \sqrt{\pi}+3 \] Evaluation of symbols
f.N() \[ 9.283,185,307 \ldots \] Numerical approximation

Other operations can be performed on an expression: comparing it to a pattern, replacing part of it, and applying conditional rewrite rules.

const expr = ce.parse('3x^2 + 2x^2 + x + 5');
console.log(expr.latex, '=', expr.simplify().latex);

Comparing Expressions

There are two useful ways to compare symbolic expressions:

  • structural equality
  • mathematical equality

Structural Equality: isSame()

Structural equality (or syntactic equality) considers the symbolic structure used to represent an expression.

The symbolic structure of an expression is the tree of symbols and functions that make up the expression.

For example, the symbolic structure of \(2 + 1\) is a sum of two terms, the first term is the number 2 and the second term is the number 1.

The symbolic structure of \(3\) is a number 3.

The symbolic structure of \(2 + 1\) and \(3\) are different, even though they represent the same mathematical object.

The lhs.isSame(rhs) function returns true if lhs and rhs are structurally exactly identical, that is each sub-expression is recursively identical in lhs and rhs.

  • \(1 + 1 \) and \( 2 \) are not structurally equal, one is a sum of two integers, the other is an integer
  • \( (x + 1)^2 \) and \( x^2 + 2x + 1 \) are not structural equal, one is a power of a sum, the other a sum of terms.
const a = ce.parse('2 + 1');
const b = ce.parse('3');
console.log('isSame?', a.isSame(b));

By default, when parsing or boxing an expression, they are put in canonical form. For example, fractions are automatically reduced to their simplest form, and arguments are sorted in a standard way.

The expressions \( \frac{1}{10} \) and \( \frac{2}{20} \) are structurally equal because they get put into a canonical form when parsed, in which the fractions are reduced.

Similarly, \( x^2 - 3x + 4 \) and \( 4 - 3x + x^2 \) are structurally equal (isSame returns true) because the arguments of the sum are sorted in a standard way.

To compare two expressions without canonicalizing them, parse or box them with the canonical option set to false.

const a = ce.parse('\\frac{1}{10}');
const b = ce.parse('\\frac{2}{20}');
console.log('Canonical isSame?', a.isSame(b));
//
const aPrime = ce.parse('\\frac{1}{10}', {canonical: false});
const bPrime = ce.parse('\\frac{2}{20}', {canonical: false});
console.log('Non-canonical isSame?', aPrime.isSame(bPrime));

In some cases you may want to compare two expressions with a weak form of canonicalization, for example to ignore the order of the arguments of a sum.

You can achieve this by comparing the expressions in their canonical order:

ce.box(["CanonicalForm", ["Add", 1, "x"], "Order"]).isSame(
  ["CanonicalForm", ["Add", "x", 1], "Order"]
)

Mathematical Equality: isEqual()

It turns out that comparing two arbitrary mathematical expressions is a complex problem.

In fact, Richardson’s Theorem proves that it is impossible to determine if two symbolic expressions are identical in general.

However, there are many cases where it is possible to make a comparison between two expressions to check if they represent the same mathematical object.

The lhs.isEqual(rhs) function return true if lhs and rhs represent the same mathematical object.

If lhs and rhs are numeric expressions, they are evaluated before being compared. They are considered equal if the absolute value of the difference between them is less than ce.tolerance.

The expressions \( x^2 - 3x + 4 \) and \( 4 - 3x + x^2 \) will be considered equal (isEqual returns true) because the difference between them is zero, i.e. \( (x^2 - 3x + 4) - (4 - 3x + x^2) \) is zero once the expression has been simplified.

Note that unlike expr.isSame(), expr.isEqual() can return true, false or undefined. The latter value indicates that there is not enough information to determine if the two expressions are mathematically equal.

const a = ce.parse('1 + 2');
const b = ce.parse('3');
console.log('isEqual?', a.isEqual(b));

Other Comparisons

lhs === rhs If true, same box expression instances
lhs.value === rhs.value Equivalent to lhs.N().isEqual(rhs.N())
lhs.isSame(rhs) Structural equality
lhs.isEqual(rhs) Mathematical equality
lhs.match(rhs) !== null Pattern match
lhs.is(rhs) Synonym for isSame()
ce.box(["Equal", lhs, rhs]).evaluate() Synonym for isEqual()
ce.box(["Same", lhs, rhs]).evaluate() Synonym for isSame()

Replacing a Symbol in an Expresssion

To replace a symbol in an expression use the subs() function.

The argument of the subs() function is an object literal. Each key value pairs is an identifier and the value to be substituted with. The value can be either a number or a boxed expression.

let expr = ce.parse('\\sqrt{\\frac{1}{x+1}}');
console.log(expr.json);
//
expr = expr.subs({x: 3});
//
console.log("Substitute x -> 3\n", expr.json);
console.log("Numerical Evaluation:", expr.N().latex);

Other Symbolic Manipulation

There are a number of operations that can be performed on an expression:

  • creating an expression from a raw MathJSON expression or from a LaTeX string
  • simplifying an expression
  • evaluating an expression
  • applying a substitution to an expression
  • applying conditional rewrite rules to an expression
  • checking if an expression matches a pattern
  • checking if an expression is a number, a symbol, a function, etc…
  • checking if an expression is zero, positive, negative, etc…
  • checking if an expression is an integer, a rational, etc…
  • and more…

We’ve introduced some of these operations in this guide, but there are many more that are available.

You can check if an expression match a pattern, apply a substitution to some elements in an expression or apply conditional rewriting rules to an expression.