8  Exact and Symbolic Computation

8.1 Learning objectives

After completing this unit, you will be able to:

  • distinguish a symbolic expression from a value obtained by substitution;
  • use Fraction and SymPy to preserve exact results;
  • state assumptions and domains before simplifying expressions;
  • check algebraic equivalence without removing domain restrictions;
  • factor and solve simple equations exactly;
  • explain the boundary between computer algebra results and mathematical proof;
  • produce canonical symbolic records that can be compared byte for byte; and
  • use local SageMath for ZZ, QQ, RR, SR, parents and coercion, polynomial rings, factorization, exact solving, and explicitly stated approximate conversions.

Local prerequisites: Units 1 and 2, polynomial operations, quadratic equations, and the concept of a function’s domain. The first route uses the Python standard library and SymPy; the local SageMath 9.5 lab is a required part of the unit. No calculus or linear algebra is required.

8.2 An expression is not a value

The expression

p(x)=x21 p(x)=x^2-1

contains the free symbol xx. It does not yet have a single numerical value. Substituting x=3x=3 produces the exact value 88, but does not turn the original expression into a number.

import sympy as sp

x = sp.Symbol("x")
p = x**2 - 1
(p, p.free_symbols, p.subs(x, 3))
Listing 8.1
(x**2 - 1, {x}, 8)

This distinction matters for notebooks run out of order. The name x in code may refer to a Python object, whereas a SymPy symbol named x is part of an expression tree. Display and save the tree actually used, not just the last result on screen.

For small rational numbers, the standard library is sufficient:

from fractions import Fraction

Fraction(1, 10) + Fraction(2, 10)
Listing 8.2
Fraction(3, 10)

Fraction(3, 10) and sp.Rational(3, 10) both preserve the exact rational value. Choose boundaries between libraries explicitly; do not convert a value to float merely to obtain shorter-looking output.

8.3 Assumptions and domains are part of the problem

Without sign information, the formula x2=x\sqrt{x^2}=x is not true for every real number. If xx is real, the correct result is |x||x|. If x>0x>0, the result is xx.

x_real = sp.Symbol("x", real=True)
x_positive = sp.Symbol("x", positive=True)

(
    sp.simplify(sp.sqrt(x_real**2)),
    sp.simplify(sp.sqrt(x_positive**2)),
)
Listing 8.3
(Abs(x), x)

The two symbols have the same printed name, but their internal assumptions differ. A symbolic computation record must therefore preserve the symbol definitions or their accompanying domain statements.

Domains must also be preserved when factors are canceled. For

r(x)=x21x1, r(x)=\frac{x^2-1}{x-1},

SymPy obtains x+1x+1 after cancellation. Both formulas give the same value on their common domain, namely x1x\ne1. However, they are not the same function when the original domain is included in the definition: the original formula is undefined at x=1x=1, whereas x+1x+1 has the value 22 there.

8.4 Structural equality and equivalence

The == operator on two SymPy expressions primarily compares their canonical structures. It does not universally answer the question, “Do these formulas have the same value for every valid input?” For example, the expressions (x+1)2(x+1)^2 and x2+2x+1x^2+2x+1 have different structures but represent the same polynomial.

left = (x + 1) ** 2
right = x**2 + 2*x + 1

structurally_equal = left == right
polynomial_identity = sp.expand(left - right) == 0
(structurally_equal, polynomial_identity)
Listing 8.4
(False, True)

The checking procedure must fit the expression class. Expanding the difference is appropriate for polynomial identities. simplify(left-right) == 0 is useful as another check, but it does not replace recording domains, root branches, assumptions, or singular points.

8.5 Exact factorization and solving

Symbolic computation preserves roots and factors without decimal approximation.

x = sp.Symbol("x")
polynomial = x**4 - 1
factored = sp.factor(polynomial)
(factored, sp.expand(factored) == polynomial)
Listing 8.5
((x - 1)*(x + 1)*(x**2 + 1), True)

The expected factorization is

(x1)(x+1)(x2+1). (x-1)(x+1)(x^2+1).

The expand check tests whether the result returns to the input polynomial. For the equation x2=2x^2=2, the solution domain must be stated:

x = sp.Symbol("x", real=True)
solutions = sp.solveset(sp.Eq(x**2, 2), x, domain=sp.S.Reals)
solutions
Listing 8.6

{2,2}\displaystyle \left\{- \sqrt{2}, \sqrt{2}\right\}

The exact real solution set is {2,2}\{-\sqrt{2},\sqrt{2}\}. Substituting each candidate into x22x^2-2 must give an exactly zero residual. Changing the domain may change the form or number of solutions, so do not save a list of roots without its equation and domain.

8.6 Symbolic results and proof

Computer algebra output can serve several different roles:

  • candidate: solve proposes values that can then be checked;
  • local certificate: re-expansion or residual substitution checks a specific relation;
  • experiment: many examples can test a conjecture; and
  • proof: an argument explains why a claim holds throughout the domain.

The factorization of x41x^4-1 can be checked directly using distributivity. However, one successful command does not prove that every output of the algorithm is correct for every input. A broader claim also depends on the algorithm’s specification and the correctness of its implementation. In mathematical writing, state exactly which relation the computer checked and which general step was proved mathematically.

8.7 Canonical output

Readable two-dimensional output can change with terminal width, printer settings, or library versions. This unit uses sympy.srepr to record expression trees and JSON with sorted keys, fixed indentation, UTF-8, and LF line endings. The SymPy version is also saved because the library’s canonical forms may change between releases.

Run the script from the project root:

python source/code/unit05_symbolic.py --output output/unit05-results.json

The core_sha256 field binds the core record before the hash field is added. Running the same command in the same environment must produce identical bytes. A hash is not proof of mathematical correctness; it establishes that two byte sequences are the same.

8.8 Required local SageMath lab

SageMath brings many mathematical structures together in a system that uses Python syntax. In this unit, Sage is neither a web service nor an optional snippet. The lab runs locally with SageMath 9.5, frozen in WSL Ubuntu 22.04. SageCell or a remote service does not meet the requirement because its runtime, network availability, and result bytes are not under the control of the local experiment.

If that profile is not yet available, follow the local setup guide and verify the lock before the lab. Version differences must be recorded; do not describe a different profile as identical to the release environment.

From the project root in PowerShell, run:

wsl.exe -d Ubuntu-22.04 -- /usr/bin/sage -python source/code/unit05_sage_lab.py --output output/unit05-sage-results.json
wsl.exe -d Ubuntu-22.04 -- /usr/bin/sage -python tests/test_unit05_sage.py

The first command writes deterministic JSON to the specified relative path. The second runs all lab tests in the same runtime. Sage blocks in this reader deliberately use ordinary python fences rather than Quarto {python} blocks. The Windows build therefore does not attempt to import Sage through the ordinary Python kernel.

8.8.1 Parents determine where objects live

Sage asks for an object’s parent(), not just its Python implementation type. The four core parents in this lab are:

Name Mathematical parent Role
ZZ integer ring exact integer arithmetic
QQ rational field exact rational division
RR 53-bit real field floating-point approximation
SR symbolic ring symbolic expressions and equations
from sage.all import QQ, RR, SR, ZZ

integer_value = ZZ(6)
rational_value = QQ(1) / 3
approximation = RR(rational_value)
symbolic_value = SR(rational_value)

print(integer_value.parent())
print(rational_value.parent())
print(approximation.parent())
print(symbolic_value.parent())

Printed values alone are not enough. 1/3 in QQ is an exact value, whereas a value in RR is an approximation at its parent’s precision.

8.8.2 Coercion must be explainable

Coercion is the canonical conversion that Sage chooses when an operation mixes parents. For example, ZZ(6) + QQ(1)/3 lives in QQ because there is a canonical map from ZZ to QQ. The lab explicitly checks the maps from ZZ to QQ, QQ to RR, and QQ to SR.

from sage.all import QQ, RR, SR, ZZ

assert QQ.has_coerce_map_from(ZZ)
assert RR.has_coerce_map_from(QQ)
assert SR.has_coerce_map_from(QQ)
mixed_sum = ZZ(6) + QQ(1) / 3
assert mixed_sum.parent() is QQ

The availability of a coercion does not mean that every change of representation is lossless. Mapping QQ to RR turns an exact rational value into an approximation. Scientific code should therefore make conversion points explicit.

8.8.3 Polynomial rings and exact factorization

A polynomial’s parent records its coefficient ring. PolynomialRing(QQ, "x") specifies univariate polynomials with rational coefficients. This is more specific than a symbolic expression that happens to look like a polynomial.

from sage.all import PolynomialRing, QQ

R = PolynomialRing(QQ, "x")
x = R.gen()
p = x**4 - 1
factorization = p.factor()

assert p.parent() is R
assert R.base_ring() is QQ
assert factorization.prod() == p
print(factorization)

The exact factorization is (x1)(x+1)(x2+1)(x-1)(x+1)(x^2+1). The prod() check multiplies the factors within the same parent and compares the result with the original polynomial.

8.8.4 Solving in SR and exact residuals

For symbolic equations, the domain and parent must still be recorded. The lab uses SR, requests solutions as dictionaries, then substitutes each candidate into the residual.

from sage.all import SR, solve

y = SR.var("y")
solutions = solve(y**2 == 2, y, solution_dict=True)
roots = [solution[y] for solution in solutions]
assert all((root**2 - 2).simplify_full() == 0 for root in roots)

Zero residuals check the two returned candidates. They do not by themselves prove that no other solutions exist on a different domain.

8.8.5 Approximate conversions must be explicit

Do not convert exact objects to decimals just to shorten their display. Preserve the original object and specify the target parent:

from sage.all import QQ, RR

exact_value = QQ(1) / 3
approximation = RR(exact_value)
assert exact_value.parent() is QQ
assert approximation.parent() is RR
print(exact_value, approximation, RR.precision())

RR(exact_value) is a representation decision that can be located in the source. The lab record preserves the exact value, conversion expression, approximate value, parent, and 53-bit precision.

8.8.6 An auditable Sage record

unit05-sage-results.json records SageMath 9.5, the local-execution requirement, parents, coercion maps, polynomial rings, factors, solutions, residuals, conversions to RR, and core_sha256. Every object is converted to a specified JSON-compatible form before serialization. Two runs using the same runtime must produce the same bytes.

The script rejects versions other than 9.5. This restriction does not claim that other versions are wrong; it binds the execution evidence to the runtime actually tested. SageMath results are still no substitute for general proof. Parents and residuals explain what the computation checked; mathematical arguments explain why a claim holds on its domain.

8.9 Exercises

8.9.1 Exercise 1 - expressions and values

Construct the expression q(t)=t32tq(t)=t^3-2t in SymPy. Identify its free symbol, then evaluate it exactly at t=1/2t=1/2.

Use Symbol, Rational, the free_symbols attribute, and subs.

The code t=sp.Symbol("t"); q=t**3-2*t gives the free-symbol set {t}. The substitution q.subs(t, sp.Rational(1,2)) gives 1/81=7/81/8-1=-7/8. The expression q still contains t; -7/8 is its value at one input, not a replacement for the original expression.

8.9.2 Exercise 2 - a missing assumption

A reader simplifies z2\sqrt{z^2} to zz for all real numbers. Give a counterexample and state the correct general result.

Try a negative real number.

Take z=3z=-3. Then z2=9=3\sqrt{z^2}=\sqrt{9}=3, whereas z=3z=-3, so the simplification fails. For real zz, the correct general form is z2=|z|\sqrt{z^2}=|z|. The form zz is valid with the additional assumption z0z\ge0; a specifically positive SymPy symbol can be declared using positive=True.

8.9.3 Exercise 3 - equivalence and domains

Compare (u24)/(u2)(u^2-4)/(u-2) with u+2u+2. State the set on which they have equal values and explain why the two functions do not automatically have the same domain.

Factor the numerator and examine the denominator before canceling factors.

Since u24=(u2)(u+2)u^2-4=(u-2)(u+2), both formulas give the same value for every u2u\ne2. The fractional formula is undefined at u=2u=2, while u+2u+2 has the value 44. They are therefore equivalent on their common domain, but are not functions with the same domain unless the restriction u2u\ne2 is retained.

8.9.4 Exercise 4 - factors, roots, and checks

Factor v3vv^3-v, find all its real roots exactly, then describe two checks that can be made without trusting the displayed output.

Take out a factor of v, then factor the difference of squares.

v3v=v(v21)=v(v1)(v+1)v^3-v=v(v^2-1)=v(v-1)(v+1), so its real roots are (1,0,1)(-1,0,1). First check: expanding v*(v-1)*(v+1) must recover v**3-v. Second check: substituting each root into the polynomial must give an exactly zero residual. These checks support clearly defined local claims.

8.9.5 Exercise 5 - stable symbolic artifacts

Why is saving only a screenshot of pretty print output insufficient for a reproducible symbolic experiment? Name at least four elements of a better record.

Consider expression trees, assumptions, domains, versions, and output bytes.

A screenshot may lose structure, changes easily with the layout, and cannot be evaluated again. A better record includes (1) the canonical expression tree, (2) symbol assumptions, (3) the problem domain, (4) the SymPy or SageMath version, (5) inputs and methods, and (6) JSON or a text format with fixed ordering and line-ending rules. Hashes can then compare the bytes, while residuals or expansion check specific mathematical claims.

8.9.6 Sage exercise 1 - parents, coercion, and factors

In local SageMath 9.5, construct ZZ(5), QQ(2)/3, and their sum. Record each object’s parent and explain the coercion that occurs. Then construct the ring QQ[z], factor z41z^4-1, and check that multiplying the factors recovers the original polynomial.

Use parent(), QQ.has_coerce_map_from(ZZ), PolynomialRing(QQ, "z"), and the factor() and prod() methods.

ImportantSelf-check

Run this block only through /usr/bin/sage -python, not the Windows Quarto kernel.

from sage.all import PolynomialRing, QQ, ZZ

a = ZZ(5)
b = QQ(2) / 3
c = a + b
Rz = PolynomialRing(QQ, "z")
z = Rz.gen()
pz = z**4 - 1
fz = pz.factor()

assert a.parent() is ZZ
assert b.parent() is QQ
assert c.parent() is QQ
assert QQ.has_coerce_map_from(ZZ)
assert Rz.base_ring() is QQ
assert fz.prod() == pz
from sage.all import PolynomialRing, QQ, ZZ

integer_value = ZZ(5)
rational_value = QQ(2) / 3
mixed_sum = integer_value + rational_value

assert str(integer_value.parent()) == "Integer Ring"
assert str(rational_value.parent()) == "Rational Field"
assert mixed_sum == QQ(17) / 3
assert mixed_sum.parent() is QQ

ring = PolynomialRing(QQ, "z")
z = ring.gen()
polynomial = z**4 - 1
factorization = polynomial.factor()
assert factorization.prod() == polynomial
assert {str(factor) for factor, _ in factorization} == {
    "z - 1",
    "z + 1",
    "z^2 + 1",
}

Sage uses the canonical map from ZZ to QQ, so the sum lives in QQ and remains exact. The polynomial lives in QQ[z]; multiplying its factors within that same parent provides a local certificate for the factorization.

8.9.7 Sage exercise 2 - exact solutions and approximations

Solve w2=5w^2=5 in SR, check each solution’s residual exactly, then explicitly convert the positive root to RR. Save the exact value, original parent, approximate value, target parent, and precision. Explain what the residuals establish and what they do not.

Use solve(..., solution_dict=True), simplify_full(), and RR(root). Do not replace the SR object before checking the exact residual.

ImportantSelf-check
from sage.all import RR, SR, solve

w = SR.var("w")
solutions = solve(w**2 == 5, w, solution_dict=True)
roots = [solution[w] for solution in solutions]

assert {str(root) for root in roots} == {"-sqrt(5)", "sqrt(5)"}
assert all((root**2 - 5).simplify_full() == 0 for root in roots)

positive_root = next(root for root in roots if root > 0)
approximation = RR(positive_root)
assert positive_root.parent() is SR
assert approximation.parent() is RR
assert RR.precision() == 53
from sage.all import RR, SR, solve

w = SR.var("w")
solutions = solve(w**2 == 5, w, solution_dict=True)
exact_roots = sorted((solution[w] for solution in solutions), key=str)
zero_residuals = [
    bool((root**2 - 5).simplify_full() == 0)
    for root in exact_roots
]

positive_root = next(root for root in exact_roots if root > 0)
approximate_value = RR(positive_root)
record = {
    "exact": str(positive_root),
    "exact_parent": str(positive_root.parent()),
    "approximate": str(approximate_value),
    "approximate_parent": str(approximate_value.parent()),
    "precision_bits": RR.precision(),
}

assert zero_residuals == [True, True]
assert record["exact"] == "sqrt(5)"
assert record["exact_parent"] == "Symbolic Ring"
assert record["approximate_parent"].startswith("Real Field")
assert record["precision_bits"] == 53

The exact residuals check that the two candidates satisfy the equation. The conversion RR(positive_root) produces a 53-bit approximation without erasing the record of the exact object. This check does not prove that the solve algorithm is complete for every equation or every domain.

8.10 Summary

  • Symbolic expressions describe structure; substitution produces values at specific inputs.
  • Exact arithmetic preserves rational and algebraic relationships without decimal rounding.
  • Assumptions and domains determine which simplifications are valid.
  • Structural equality, equivalence on a common domain, and equality of functions are three different questions.
  • Factorization, re-expansion, exact solving, and residuals complement one another as local checks.
  • Computer algebra output is execution evidence whose claims must be bounded, not an automatic substitute for general proof.
  • Canonical representations, library versions, and hashes make artifacts comparable.
  • The required local SageMath 9.5 lab uses ZZ, QQ, RR, SR, parents and coercion, polynomial rings, factorization, solving, residuals, and explicit approximate conversion; remote services are not a substitute.