Skip to main content
The Math Engine is QWED’s core verification engine. It uses SymPy for symbolic computation to provide exact verification of mathematical claims.

Capabilities


Quick start


Core operations

1. Expression evaluation

Verify that an expression equals a value:

2. Identity verification

Check if two expressions are mathematically equivalent:
Identity verification uses symbolic simplification first. If SymPy proves the identity algebraically, the result is VERIFIED. When symbolic simplification is inconclusive, the engine samples five test points as a fallback. Only points that the engine can successfully evaluate count toward agreement — the engine skips domain-restricted expressions (e.g., log(x) at x = -1) rather than producing a false negative.
Numerical sampling cannot prove equivalence. If all sample points agree but no formal proof was established, the engine now fails closed — returning BLOCKED with is_equivalent: false, method: "numerical_sampling_rejected", and confidence: 0.0. Two expressions can match at fixed points without being algebraically identical, so sampling-only agreement is rejected outright. Treat BLOCKED results as unverified.

Domain-restricted identities

An identity that holds only after SymPy cancels a removable singularity is not proven. For example, x/x = 1 is false at x = 0, and (x**2-1)/(x-1) = x+1 is false at x = 1. The engine reads the denominators from the expression before SymPy simplifies it. It returns VERIFIED only when both sides have the same singular factors.
  • POST /verify/math returns UNVERIFIABLE with constraint_id: "math.domain_restricted_identity" and admission BLOCKED.
  • verify_identity returns status: "INCONCLUSIVE" with reason: "domain_mismatch" and the same constraint_id.
The same rule covers floor division and modulo (x//x = 1, x % x = 0). An identity with a free symbol that uses a function with poles or singular points (tan, sec, log, factorial) is never verified this way. Closed constants such as factorial(5) = 120 still verify.

3. Derivatives

Verify calculus derivatives:

Derivatives in /verify/math identities

Changed in v9.0.0
POST /verify/math and /verify/batch math items evaluate diff and Derivative inside an identity, so you can state a derivative claim directly. == reads as =.
  • Order limit. The total derivative order in one expression is at most 3, nested calls included. Each order multiplies the work that simplification has to do.
  • No derivative inside a power, factorial, or binomial. diff(x**2, x)**2 is refused. A reciprocal or a root, such as 2*x/diff(x**2, x) or sqrt(diff(x**2, x)), is allowed.
  • Domain check. The domain check reads each evaluated derivative as well as the text. A derivative can be undefined where the expression it differentiates is defined. diff(sqrt(x), x) = 1/(2*sqrt(x)) is VERIFIED, but diff(sqrt(x), x)*2*sqrt(x) = 1 holds only for x other than 0, so it is UNVERIFIABLE with math.domain_restricted_identity.
Leibniz notation is refused, not read as symbols. d/dx(x^2) = 2x, dy/dx, d(x^2)/dx, dy/dtheta, and d^2y/dx^2 return BLOCKED with constraint_id: "math.parse_error" and the message to write diff(f, x). Write diff(x^2, x) = 2*x instead.The same refusal covers a name starting with d that SymPy splits into letters, because it would be read as d times the rest: dx, dtheta, d2y, and also data (read as a**2*d*t). A name the parser already knows, such as delta, diff, or a symbol you declare, is never refused. To use a d-prefixed name as a symbol, pass it in extra_symbols. extra_symbols is an argument of safe_parse_expr() in the Python package only; /verify/math and batch items don’t take it, so there rename the variable. A name with _, such as distance_m, is a symbol unless the rest is a variable used beside it (dx_1 beside x_1).

Parse errors

Changed in v9.0.0
Input the parser refuses is BLOCKED with constraint_id: "math.parse_error" and the message Expression could not be parsed, as SQL and schema parse errors are. Leibniz notation gets the diff(f, x) message instead. The parser’s own error text is not returned. Before v9.0.0, such input returned BLOCKED with api.math.execution_error and Internal verification error. A /verify/batch math item the parser refuses gets the same BLOCKED result instead of a failed item.

4. Integrals

Verify indefinite and definite integrals:

5. Limits


Financial calculations

Compound interest

Net present value (NPV)

Internal rate of return (IRR)

Cash flows with more than one sign change, or that fail to converge, are now rejected with BLOCKED rather than returning a best-effort iterate. See verify_irr — convergence proof for the full state table.

Verdicts and admission

The math engine separates the verdict (did the engine decide the claim?) from admission (may you use the result?). A claim the engine proves false is a decided verdict. It is VERIFIED with is_valid: false and admission DENY, never UNVERIFIABLE. Gate on admission, not on the verdict alone. The legacy engine status is the status field of the dictionary that VerificationEngine methods return. HTTP responses report admission DENY as "admission": "BLOCKED". POST /verify/math and /verify/batch math items build the identity verdict with the same function, so both routes return the same verdict for the same claim. A claim with a side that has an undefined part is not disproven. SymPy evaluates some undefined parts away: log(0)**0 becomes 1, so log(0)**0 = 2 looks like 1 = 2. A part is undefined when it evaluates to zoo or nan. A symbol-free part is also undefined when it is not finite. Such a claim is UNVERIFIABLE with constraint_id: "math.undefined_part" and no attestation. log(0)**0 = 1, where the sides agree, is UNVERIFIABLE with math.domain_restricted_identity. x/x = 2 and 2x^2 = 5 are still disproven.
VerificationEngine no longer emits CORRECTION_NEEDED. A mismatch decided by a concrete computation now returns DISPROVEN. If your code branches on status == "CORRECTION_NEEDED", switch to DISPROVEN for engine dictionaries, or to admission and is_valid for API responses.
The engine returns DISPROVEN only when the mismatch is decided:
  • Symbolic checks. Derivatives, indefinite integrals, limits, and /verify/math identities are disproven only when SymPy decides the difference is non-zero. The difference can be decided outright, through the coefficients of a bounded polynomial, or at an exact witness point inside each symbol’s declared domain. Otherwise the result is INCONCLUSIVE.
  • Float residue. A non-zero difference that comes from a float literal (0.1 + 0.2 = 0.3 leaves 5.55e-17) is INCONCLUSIVE, not DISPROVEN.
  • Non-finite values. A mismatch involving an overflowed value or a NaN claim is INCONCLUSIVE.
  • Exact matrices and scalars. Matrix add, multiply, inverse, transpose, and determinant read each float input as the decimal you wrote (0.1 is 1/10), so [[0.1]] + [[0.2]] vs [[0.3]] is VERIFIED. A claim inside the float rounding window is INCONCLUSIVE. A claimed matrix of the wrong shape is DISPROVEN.
  • Unit conversion. verify_unit_conversion uses exact unit definitions (1 lb = 0.45359237 kg, 1 US gal = 3.785411784 L). Converting between units of different dimensions, such as m to kg, returns BLOCKED instead of comparing the numbers.

Fail-closed semantics

Three verification methods require an additional proof step before returning VERIFIED. When the underlying claim is ambiguous, incomplete, or numerically unproven, the engine returns BLOCKED or DISPROVEN with structured diagnostics rather than a best-effort answer.

verify_statistics(statistic="mode") — unique mode required

mode verification returns VERIFIED only when a single value has the maximum frequency. When two or more values tie for the maximum frequency, the engine returns BLOCKED with an ambiguous_modes list — it will not heuristically pick one. Response fields on the BLOCKED case:
The Stats Engine’s compute_statistics method returns an equivalent multimodal error on its own surface — see Stats engine — Error handling.

verify_matrix_operation(operation="eigenvalues") — cardinality match required

Eigenvalue verification requires the claimed list to have the same length as the calculated eigenvalue set, counting algebraic multiplicity. Previously the value comparison used zip, which silently truncated to the shorter list — so a claim of [2] for a matrix with eigenvalues [2, 3] could pass. The cardinality check now runs before the value comparison. Response fields on the cardinality-mismatch case:

verify_irr — convergence proof

IRR verification now requires proof that Newton-Raphson converged before returning VERIFIED. Successful results include converged: true and iterations_used for auditability. The engine blocks inputs whose IRR is mathematically ambiguous or numerically unreachable rather than returning the current iterate. Response fields:
These three methods are behavior changes. Inputs that previously received VERIFIED — an ambiguous mode dataset, an under-specified eigenvalue list, or a cash-flow series with multiple sign changes — now return BLOCKED or DISPROVEN. Treat both statuses as not admitted and consume the structured diagnostic fields rather than relying on a numeric result.

Trust boundary

When you verify a natural language math query through the /verify/natural_language endpoint, the engine only decides the expression the LLM translated your question into. The response carries the enforced verdict for that expression, the scope it covers, and a separate admission decision.
Admission is always BLOCKED (DENY) for natural language queries, even when the translated claim holds, because nothing proves the translation means what you asked. A translated claim proven false is VERIFIED with is_valid: false. The control plane also records a trust_boundary object that describes exactly what the pipeline proved and what it did not:
QWED evaluates the translated expression deterministically, but it cannot verify that the LLM correctly interpreted your intent. That is why admission is always BLOCKED for natural language queries. Use POST /verify/math directly when you need an admissible result.

Error handling

When verification fails, QWED provides detailed error information:

Exact SymPy arithmetic

When SymPy is available, the math engine evaluates expressions using SymPy-native types (sympy.Integer, sympy.Float) instead of Python built-in int and float. This prevents floating-point drift during intermediate computation and ensures that comparisons between LLM answers and verified results use symbolic simplification rather than string matching alone.

Decimal precision

The math engine accepts Decimal values for exact arithmetic, which is especially useful for financial calculations:
When use_decimal=True (the default), the engine uses Decimal internally regardless of whether you pass a float or Decimal.

Float precision advisory

When an expression submitted to POST /verify/math contains binary floating-point constants (0.1, 1e9, 1.0j), the response carries a precision advisory in developer_fields.advisory_checks. Binary floats can be inexact relative to decimal arithmetic, so the advisory lists the offending constants and suggests decimal.Decimal or exact SymPy rationals (sympy.Rational) where exactness matters. The advisory is informational only. It is an AdvisoryCheck, which enforces advisory_only=True at construction, so it structurally cannot change the verification status or proof_ref. Expressions with floats still verify normally.
When the advisory appears: The engine checks the expression exactly as you submitted it, before symbolic simplification. Implicit multiplication (0.5x) is normalized for parsing but does not hide the float literal.

Tolerance settings

For floating-point comparisons, you can specify a tolerance:

Tolerance bounding

To prevent inflated tolerances from masking incorrect results, the math engine enforces a deterministic upper bound on the tolerance parameter. QWED computes the maximum allowed tolerance as a function of the result’s magnitude:
If the requested tolerance exceeds this bound, QWED rejects the verification with a BLOCKED status instead of returning a potentially misleading VERIFIED result. This applies to both decimal and float precision modes.
The engine also rejects invalid tolerance values (negative numbers, NaN, Infinity, or non-numeric strings) with a BLOCKED status and an "Invalid tolerance" error message.

Trust boundary

When math verification runs through the natural language pipeline (POST /verify/natural_language), the response includes a trust_boundary object. This object describes exactly what the pipeline proved and what it did not, separating deterministic expression evaluation from the non-deterministic LLM translation step — and records the mandatory attestation admission decision.
Because the LLM translation step is non-deterministic, the natural language pipeline never admits a result: admission is always BLOCKED, even when the translated expression is VERIFIED. The verdict carries verification_scope: "translated_expression_only" and query_semantics_verified: false, so it can’t be mistaken for proof of your question. Use the direct POST /verify/math endpoint if you need a fully deterministic result without the LLM translation layer.

Mandatory attestation admission

The control plane runs every math response through enforce_trust_decision(..., require_attestation=True). Attestation is now an admission gate — a VERIFIED result is only returned once the signing step succeeds:
  • On success: the verifier evidence (translated expression, calculated value, claimed value, diff, precision mode) is passed to create_verification_attestation(). If the returned AttestationResult has status == ISSUED, enforce_trust_decision accepts the signed token and sets overall_status = "VERIFIED".
  • On signing failure: the response is downgraded to BLOCKED and trust_boundary.attestation_error records the error code from the AttestationResult (for example SIGNING_FAILURE or CRYPTO_UNAVAILABLE). No token is returned. This is a fail-closed guarantee: signing outages never surface as VERIFIED.
  • overall_status is post-attestation. It is always sourced from the enforced decision, never from the raw verifier verdict. If you need to reason about the raw verifier output separately, inspect verification.is_correct and deterministic_expression_evaluation.

Attestation scope: translated expression, not natural-language query

The attestation qwed.query_hash binds to the translated expression that the deterministic engine actually evaluated (task.expression), not to the user’s natural-language query. This matches the disclosed verification_scope: "translated_expression_only" and prevents the attestation from over-claiming that the user’s prose was verified.
  • response.user_query — the original natural-language query, kept for display and audit correlation.
  • Attestation qwed.query_hash — SHA-256 of the translated deterministic expression (e.g. 0.15 * 200), which is the exact input the engine executed.
  • Downstream verifiers should hash the translated expression when re-checking qwed.query_hash. Hashing user_query will not match — that is intentional, because QWED does not attest to the LLM translation step.

Ambiguous expressions

Expressions with implicit multiplication after division are ambiguous — for example, 1/2(3+1) could mean (1/2)*(3+1) or 1/(2*(3+1)). Rather than guessing, the math engine fails closed and returns BLOCKED:
To resolve this, rewrite the expression with explicit parentheses or a * operator:

Expression input rules

Every math expression passes through a layered structural validator before SymPy evaluates it. The validator runs in order: NFKC Unicode normalization, an ASCII character-set gate, a construct denylist, and an AST node-type allowlist. An expression that fails any layer is rejected before evaluation. These are the rules your expressions must follow: Additional details:
  • Greek letters: the parser provides ASCII names for the full Greek alphabet (alpha, beta, gamma, … omega). Write alpha * x, not α * x. Non-ASCII codepoints never reach the parser, including Unicode look-alikes that NFKC-normalize to ASCII identifiers.
  • Allowed characters: letters, digits, underscore, whitespace, and the operator set + - * / ( ) . , ^ %.
  • Allowed syntax: arithmetic operators, function calls from the safe namespace (sin, cos, log, sqrt, factorial, …), symbol names, and numeric constants. Everything else is rejected by the AST allowlist.
  • Caret exponentiation: ^ is converted to ** by the default transformation pipeline, so x^2 and 2^3 parse correctly.
  • Depth and length limits: expressions are capped at 5,000 characters, an AST depth of 30, and a SymPy tree depth of 40.
  • Implicit multiplication (2x, sin x, 2(x+1)) still parses. These forms are covered by the character-set gate instead of the AST check.
These rules are structural security guarantees, not style preferences. Attribute access, string constants, and non-ASCII identifiers are the building blocks of sandbox-escape payloads, so the parser rejects them by construction rather than pattern-matching known attacks. Legitimate arithmetic never needs them.

Compute-cost bounds

SymPy expands integer powers eagerly and exactly. An expression like 9^9^9^9 demands a result with billions of digits, so evaluating it would hang the engine. The parser now estimates the exact-expansion cost of every expression statically and rejects expressions that exceed the budget before evaluation starts. The bounds are: Additional details:
  • Power towers and caret chains are folded right-associatively and checked as a whole, so ((9^9)^9999)^9999 and 9^9^9^9 are both caught even though each individual exponent is small.
  • Concrete-valued calls are evaluated statically. Wrapping a large value in abs(), factorial(), binomial(), or a numeric constructor does not hide it from the gate: 2^abs(-100000) and 2^factorial(10000) are both rejected.
  • All cost comparisons use exact arithmetic, never binary floats, so the estimates are deterministic.
A rejected expression fails validation before evaluation, with an error message naming the exceeded bound:
These bounds are far above anything legitimate verification needs — 2^100, factorial(500), and 300-digit results all pass. They only reject expressions whose exact expansion is computationally unbounded.

Edge cases


Performance


Next steps