stepcas is a step-first symbolic algebra engine for Python.
It is not trying to be a tiny SymPy clone. The goal is different:
- compute exact symbolic results,
- preserve a human-readable chain of reasoning,
- expose intermediate transformations,
- stay small enough to audit,
- scale toward a serious computer algebra platform.
Many symbolic systems are powerful but opaque. Many AI math systems are fluent but not always grounded in real computation. stepcas aims to sit in the middle:
- exact symbolic core,
- explicit rewrite engine,
- step trace as a first-class artifact,
- Python library + CLI now,
- web API later.
This starter implements:
- immutable expression tree,
- parser for a Python-like math subset,
- simplification with step tracing,
- canonical ordering for addition and multiplication with explicit trace steps (
canonical-order-addandcanonical-order-mul), - additive-only like-term collection with explicit trace steps (
collect-like-terms-add) without expansion/factoring, plus mul-level repeated-base merging viamerge-repeated-bases-mul(for examplex*x**2 -> x**3), - one-variable polynomial degree utility (
polynomial_degree) for already-expanded forms, - one-variable polynomial coefficient extraction (
polynomial_coefficients) for already-expanded forms, - one-variable dense polynomial coefficient vectors (
polynomial_coeff_vector) for already-expanded forms, - one-variable polynomial evaluation (
polynomial_evaluate) for already-expanded forms, - one-variable polynomial leading-term extraction (
polynomial_leading_term) for already-expanded forms, - one-variable polynomial leading-coefficient extraction (
polynomial_leading_coefficient) for already-expanded forms, - one-variable polynomial trailing-coefficient extraction (
polynomial_trailing_coefficient) for already-expanded forms, - one-variable polynomial trailing-term extraction (
polynomial_trailing_term) for already-expanded forms, - symbolic differentiation for a useful subset,
- linear-form extraction for expressions in
a*x + bform, - core linear equation solving API (
solve_linear_equation) with step tracing, - schema-versioned JSON serializers for
Expr,Step, andTraceResult(expr_to_json,step_to_json,trace_result_to_json), - CLI commands for
simplify,diff, andsolvewith optional--jsonoutput and structured JSON errors (error.code,error.message), - reproducible benchmark harness for simplify/differentiate/solve workloads with runtime and step-count output,
- test suite,
- repo scaffolding for agentic development.
All polynomial and linear helpers validate the variable parameter strictly:
- It must be a single symbol name (for example,
"x") - The name must be a valid Python identifier (
str.isidentifier()returnsTrue) - Invalid inputs raise errors with domain-specific error codes
Valid: "x", "y", "foo", "var1"
Invalid: "", "x+y", "2x", "x y", None, 123
extract_linear_form and solve_linear_equation require the expression to be in canonical linear form (a*x + b where a and b are constants). They do not automatically expand or rearrange expressions.
from stepcas import extract_linear_form, parse_expr
# Works: canonical form a*x + b
extract_linear_form(parse_expr("3*x - 7"), "x") # coefficient=3, constant=-7
extract_linear_form(parse_expr("2*x"), "x") # coefficient=2, constant=0
# Fails: requires expansion first
extract_linear_form(parse_expr("(x + 1) * 2"), "x") # raises LinearFormError
extract_linear_form(parse_expr("x**2"), "x") # raises LinearFormErrorAll polynomial helpers require the expression to be already expanded (no unevaluated products or parentheses). They do not automatically expand or factor expressions.
from stepcas import polynomial_degree, parse_expr
# Works: expanded form
polynomial_degree(parse_expr("3*x**4 + 2*x - 1"), "x") # 4
# Fails: requires expansion first
polynomial_degree(parse_expr("(x + 1)**2"), "x") # raises PolynomialError
polynomial_degree(parse_expr("x*(x + 1)"), "x") # raises PolynomialErrorpolynomial_trailing_coefficient returns the degree-zero coefficient and yields 0 when no constant term is present.
polynomial_trailing_term returns the lowest-degree non-zero term as (degree, coefficient) and returns (0, 0) for the zero polynomial.
python -m venv .venv
source .venv/bin/activate
pip install -e .[dev]
pytestSimplify an expression:
stepcas simplify "x + 0 + 2 + 3" --stepsDifferentiate an expression:
stepcas diff "x**3 + 2*x + 5" x --stepsSolve a linear equation (lhs = rhs):
stepcas solve "2*x + 3" "11" xGet machine-readable JSON output from CLI commands:
stepcas simplify "x + 0 + 2 + 3" --steps --json
stepcas diff "x**3 + 2*x + 5" x --json
stepcas solve "2*x + 3" "11" x --steps --jsonWhen --json is enabled and an operation fails, the CLI emits a structured
error payload containing error.code and error.message.
Use as a library:
from stepcas import (
LinearSolveKind,
expr_to_json,
differentiate,
extract_linear_form,
parse_expr,
polynomial_coeff_vector,
polynomial_coefficients,
polynomial_degree,
polynomial_evaluate,
polynomial_leading_coefficient,
polynomial_leading_term,
polynomial_trailing_coefficient,
polynomial_trailing_term,
simplify,
solve_linear_equation,
)
expr = parse_expr("x**3 + 2*x + 5")
result = differentiate(expr, "x", trace=True)
print(result.expr)
for step in result.steps:
print(step.rule, step.before, "=>", step.after)
trace_payload = expr_to_json(expr)
print(trace_payload["schema_version"], trace_payload["object"])
linear = extract_linear_form(parse_expr("3*x - 7"), "x")
print(linear.coefficient, linear.constant) # 3 -7
degree = polynomial_degree(parse_expr("3*x**4 + 2*x - 1"), "x")
print(degree) # 4
coefficients = polynomial_coefficients(parse_expr("3*x**2 - 2*x + 5"), "x")
print(coefficients) # {2: 3, 1: -2, 0: 5}
vector = polynomial_coeff_vector(parse_expr("3*x**4 - 2*x + 5"), "x")
print(vector) # [3, 0, 0, -2, 5]
evaluated = polynomial_evaluate(parse_expr("3*x**2 - 2*x + 5"), "x", 2)
print(evaluated) # 13
leading = polynomial_leading_term(parse_expr("3*x**4 - 2*x + 5"), "x")
print(leading) # (4, 3)
leading_coefficient = polynomial_leading_coefficient(parse_expr("3*x**4 - 2*x + 5"), "x")
print(leading_coefficient) # 3
trailing_coefficient = polynomial_trailing_coefficient(parse_expr("3*x**4 - 2*x + 5"), "x")
print(trailing_coefficient) # 5
trailing = polynomial_trailing_term(parse_expr("3*x**4 - 2*x + 5"), "x")
print(trailing) # (0, 5)
solved = solve_linear_equation(parse_expr("2*x + 3"), parse_expr("11"), "x")
if solved.kind == LinearSolveKind.SOLVED:
print(solved.variable, solved.value) # x 4schema_version is included in every serializer payload and should be used as the client compatibility key.
Linear equation solving returns typed outcomes for solved, no-solution, and
infinite-solution cases (SolvedLinearEquation, NoLinearSolution,
InfiniteLinearSolutions) tagged by LinearSolveKind.
Error handling is exposed through a shared hierarchy rooted at StepcasError,
with domain-specific subclasses such as ParseError, DifferentiationError,
RewriteError, and LinearFormError. Each carries a stable machine-friendly
code and domain (for example: linear.nonlinear_form,
linear.unsupported_structure, and linear.unsupported_symbol).
This repository is set up to work well with OpenCode in two styles:
- Interactive TUI when you want to inspect and steer manually.
- Headless server plus one-shot
opencode run --attach ...calls when you want clean, task-scoped sessions.
Recommended for this project:
- keep one long-lived
opencode serveprocess running - use separate
opencode run --attach ...calls for planning, implementation, debugging, and review - avoid a never-ending optimization loop, this project is product engineering, not a single-metric ratchet
PowerShell helpers are included under scripts/.
The long-term design is four layers:
- Core expression system
- Rule and rewrite engine
- Step trace and explanation layer
- Frontends: Python API, CLI, web API, UI
See docs/architecture.md and program.md.
Run the benchmark harness:
stepcas-benchmarkRecord a baseline snapshot:
stepcas-benchmark --iterations 50 --warmups 10 --baseline-out benchmarks/baseline.jsonSee docs/benchmarks.md for usage and reproducibility guidance.
This repo also includes a lightweight local supervisor under company/ and helper scripts under scripts/. Use that mode if you want StepCAS work to proceed task by task with minimal intervention, while still being easy to pause and resume.