import sympy as sp
x = sp.Symbol("x")
p = x**2 - 1
(p, p.free_symbols, p.subs(x, 3))(x**2 - 1, {x}, 8)
After completing this unit, you will be able to:
Fraction and SymPy to preserve exact results;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.
The expression
contains the free symbol . It does not yet have a single numerical value. Substituting produces the exact value , 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))(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)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.
Without sign information, the formula is not true for every real number. If is real, the correct result is . If , the result is .
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)),
)(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
SymPy obtains after cancellation. Both formulas give the same value on their common domain, namely . However, they are not the same function when the original domain is included in the definition: the original formula is undefined at , whereas has the value there.
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 and 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)(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.
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)((x - 1)*(x + 1)*(x**2 + 1), True)
The expected factorization is
The expand check tests whether the result returns to the input polynomial. For the equation , 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)
solutionsThe exact real solution set is . Substituting each candidate into 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.
Computer algebra output can serve several different roles:
solve proposes values that can then be checked;The factorization of 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.
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.
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.
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.
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 QQThe 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.
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 . The prod() check multiplies the factors within the same parent and compares the result with the original polynomial.
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.
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.
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.
Construct the expression in SymPy. Identify its free symbol, then evaluate it exactly at .
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 . The expression q still contains t; -7/8 is its value at one input, not a replacement for the original expression.
A reader simplifies to for all real numbers. Give a counterexample and state the correct general result.
Try a negative real number.
Take . Then , whereas , so the simplification fails. For real , the correct general form is . The form is valid with the additional assumption ; a specifically positive SymPy symbol can be declared using positive=True.
Compare with . 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 , both formulas give the same value for every . The fractional formula is undefined at , while has the value . They are therefore equivalent on their common domain, but are not functions with the same domain unless the restriction is retained.
Factor , 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.
, so its real roots are . 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.
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.
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 , 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.
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() == pzfrom 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.
Solve 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.
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() == 53from 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"] == 53The 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.
ZZ, QQ, RR, SR, parents and coercion, polynomial rings, factorization, solving, residuals, and explicit approximate conversion; remote services are not a substitute.