Symbolic Computing
The Compute Engine essentially performs computation by applying rewriting rules to a MathJSON expression.
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. Does not substitute assigned symbol values (that is evaluate()'s job). |
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 an integer 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 | |||
| Substitute assigned symbol values | |||
| Exact calculations | |||
| Floating-point approximations |
Other operations can be performed on an expression: comparing it to a pattern, replacing part of it, and applying conditional rewrite rules.
Comparing Expressions
There are three tiers of comparison, from the strictest and cheapest to the most powerful and most expensive:
| Tier | Method | Operator | Notation | Answers |
|---|---|---|---|---|
| Syntactic | isSame() | Same | ===, ≣ (Epsil) | Always True or False |
| Arithmetic | isEqual() | Equal | =, == (Epsil) | True, False, or undetermined |
| Identity | isIdenticallyEqual() | IdenticallyEqual | \equiv, ≡ | True, False, or undetermined |
Each tier answers a different question: "are these the same expression?", "do these have the same value?", and "are these the same function of their free variables?".
The operator, its LaTeX/Epsil notation and its JavaScript method share the same semantics at each tier.
Syntactic Equality: isSame()
Syntactic equality (or structural 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. The argument can be an Expression or a JavaScript primitive
(number, bigint, boolean, string).
This is a fast, exact check — no evaluation is performed, and the answer is
always true or false, never undetermined.
isSame() is strictly syntactic: it compares the expressions as written
(in canonical form) and never substitutes the value of a symbol. If x has
been assigned the value 5, ce.symbol('x').isSame(5) is false — one
expression is a symbol, the other a number. Use isEqual() to compare values.
- $ x + x $ and $ 2x $ are not structurally equal, one is a sum of two terms, the other a product
- $ (x + 1)^2 $ and $ x^2 + 2x + 1 $ are not structurally equal, one is a power of a sum, the other a sum of terms.
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 \( \frac110 \) and \( \frac220 \) 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.
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.expr(["CanonicalForm", ["Add", 1, "x"], "Order"]).isSame(
["CanonicalForm", ["Add", "x", 1], "Order"]
)
Arithmetic Equality: isEqual()
The lhs.isEqual(rhs) function answers "do these two expressions have the same
value?". It is the JavaScript counterpart of the = operator (== in Epsil).
The comparison is deliberately cheap and predictable:
- Both operands are evaluated (so assigned symbol values are substituted).
- If the results are structurally identical, they are equal.
- Otherwise, if the difference between them has no unknowns, it is evaluated
numerically: they are equal if the absolute value of the difference is less
than
ce.tolerance. - Otherwise the answer is
undefined.
No expansion, no simplification and no sampling is attempted. In particular,
isEqual() does not attempt to prove a symbolic identity: the expressions
$ (x+1)^2 $ and $ x^2 + 2x + 1 $ have the same value for every $ x
$, but isEqual() returns undefined for them. Use
isIdenticallyEqual() for that question.
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 have the same value.
The corresponding Equal expression is inert when the answer is
undefined: it stays unevaluated, since $ x = y $ is a condition, not a
claim that is false until proven. This is what makes an equation usable as an
argument to Solve.
Following IEEE 754, NaN is not equal to anything, including itself: NaN = NaN is False, and so is the comparison of two collections that contain it,
["Equal", ["List", "NaN"], ["List", "NaN"]]. Only the syntactic tier reports
those as identical (["Same", "NaN", "NaN"] is True).
Identity: isIdenticallyEqual()
Comparing two arbitrary symbolic expressions is a hard problem. In fact, Richardson's Theorem proves that it is impossible in general to determine if two symbolic expressions are identical.
However, there are many cases where it is possible to decide it in practice.
The lhs.isIdenticallyEqual(rhs) function asks whether lhs and rhs are the
same function of their free variables, that is whether they have the same value
for every value of those variables. It is the JavaScript counterpart of the
IdenticallyEqual operator, written $ \equiv $ in LaTeX.
Like isEqual(), it is three-valued: true, false or undefined when the
question could not be settled, in which case an IdenticallyEqual expression
stays unevaluated. A false requires a definite disagreement of values:
expressions that merely differ at the sampled points, such as $ x $ and
$ x + 1 $, are undefined, since an assumption could still constrain them
to be equal.
To settle an identity, this tier uses symbolic transformations (expansion and
simplification) and numerical sampling: the two expressions are evaluated
at a number of pseudo-random points. A true answer obtained by sampling is
therefore a very strong indication, not a formal proof. This is the only tier
that can answer from sampling; isSame() and isEqual() never do.
Smart Comparison: is()
The lhs.is(rhs) method provides a convenient middle ground between isSame()
and isEqual(). It first tries an exact structural check (like isSame()), and
if that fails and the expression is constant (no free variables), it
evaluates numerically and compares within engine.tolerance.
This is useful when checking whether an expression evaluates to a known value
without the overhead of a full isEqual() call:
For literal numbers (created with ce.number()), is() behaves identically
to isSame() — no tolerance is applied. Tolerance only kicks in for expressions
that require evaluation, such as \sin(\pi).
Other Comparisons
lhs === rhs | If true, same box expression instances |
lhs.isSame(rhs) | Syntactic equality (fast, exact, no evaluation). Accepts primitives. |
lhs.is(rhs) | Smart check: structural first, then numeric evaluation fallback for constant expressions (within engine.tolerance). For literal numbers, same as isSame(). |
lhs.isEqual(rhs) | Arithmetic equality (evaluation and tolerance compare). May return undefined. |
lhs.isIdenticallyEqual(rhs) | Identity in all free variables (expansion, simplification, sampling). May return undefined. |
lhs.match(rhs) !== null | Pattern match |
ce.expr(["Equal", lhs, rhs]).evaluate() | Synonym for lhs.isEqual(rhs) |
ce.expr(["IdenticallyEqual", lhs, rhs]).evaluate() | Synonym for lhs.isIdenticallyEqual(rhs) |
ce.expr(["IsSame", lhs, rhs]).evaluate() | Synonym for lhs.isSame(rhs), comparing the operands as written (they are not canonicalized) |
ce.expr(["Same", lhs, rhs]).evaluate() | Synonym for lhs.isSame(rhs) (Epsil ===). Always decides. |
- Use
isSame()when you know the exact expression you're comparing against (e.g., checking if an operand is0or1in a simplification rule). - Use
is()when the expression might need evaluation to reveal its value (e.g., checking user input like\cos(\pi/2)against0). - Use
isEqual()to compare values, including expressions that must be evaluated first. - Use
isIdenticallyEqual()to check a symbolic identity, i.e. an equality that must hold for every value of the free variables.
Replacing a Symbol in an Expression
To replace a symbol in an expression use the subs() function.
The argument of the subs() function is an object literal. Each key/value
pair is a symbol and the value to be substituted with. The value can be either
a number or a expression.
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.
To check if an expression matches a pattern, apply a substitution to some elements in an expression or apply conditional rewriting rules to an expression.