alkahest
Python API · Submodule

alkahest.ansatz

Ansatz families — structured conjecture generation for search loops.

P2 item 1 — see docs/mdbook/src/ansatz.md.

Stage 1 of an autoresearch loop is generate: propose a structured family of candidate objects ("every polynomial of degree ≤ 3 in x and y", "a Padé approximant of type (2, 2)", "a quadratic Lyapunov candidate"), then either sweep it numerically or solve for the coefficients that make some residual vanish. Agents hand-roll this constantly, and the hand-rolled version is usually wrong in one of three specific ways:

  • it loses the distinction between an unknown coefficient and an independent variable, so the follow-up substitution solves for the wrong thing;
  • it assumes the first m sample points give m independent equations, which is false exactly when the family is degenerate — the interesting case;
  • it never substitutes the fitted answer back, so a fit that only satisfies the sampled constraints is reported as if it satisfied the identity.

This module is that plumbing, done once. Ansatz keeps the unknowns and the variables apart; fit() over-samples and reads the rank off the reduced row echelon form rather than assuming independence; and fit(..., certify="residual") — the default — substitutes the solution back and only reports "exactly_verified" when the residual provably normalises to zero.

Honesty invariants

Solving may be heuristic; checking is exact. The linear system is built by collocation (probing the residual at sample points), which is a sufficient condition for identical vanishing only for polynomial residuals of bounded degree. So the fit is never trusted on its own: it is substituted back and normalised. A solution that normalises to zero is verification["status"] == "exactly_verified"; one that does not is returned intact but labelled "numerically_checked", with the surviving residual stated in verification["residual"]. There is no status meaning "solved".

Inconsistent is a result, not a malfunction. When no member of the family can satisfy the constraints, fit() raises AnsatzError E-ANSATZ-003. For a loop that is a closed branch — a positive finding worth recording — in the same spirit as a non-elementarity verdict from integrate().

Underdetermined is also a result. When the rank is below the number of unknowns, the members that work form a positive-dimensional family. That is often the answer the loop wanted, so AnsatzSolution.free returns the free parameters rather than picking an arbitrary member of the space.

Quick start

Python
>>> import alkahest as ak
>>> from alkahest.ansatz import polynomial, fit
>>> pool = ak.ExprPool()
>>> x = pool.symbol("x")
>>> A = polynomial(pool, [x], degree=2)
>>> len(A.unknowns)
3
>>> target = x**2 - pool.integer(3) * x + pool.integer(2)
>>> sol = fit(A, A.expr - target)
>>> sol.status
'exactly_verified'
>>> sol.rank, sol.free
(3, ())

Everything here is pure Python composed from primitives that are already fast in Rust (Matrix.rref, simplify, subs), so it works in a build without the groebner feature — see fit() for the one path that needs it, and refuses rather than degrading when it is absent.

class
class ansatz.Ansatz

A parametric family of expressions with named unknown coefficients.

An ansatz is an object rather than a bare Expr because a bare expression loses the distinction between an unknown coefficient and an independent variable, and every downstream step needs it. Construct one with polynomial(), rational(), linear_combination(), exponential_polynomial(), or quadratic_form() rather than directly.

Attributes
exprExpr

The family member with symbolic coefficients, e.g. c_0 + c_1*x + c_2*x^2.

unknownstuple of Expr

The coefficient symbols, in a deterministic order (graded, then lexicographic by exponent).

basistuple of Expr

Parallel to unknowns: the expression each unknown multiplies. For rational() this is the numerator basis followed by the denominator basis, since the two blocks multiply into different places — read metadata["numerator_terms"] for the split.

varstuple of Expr

The independent variables. Disjoint from unknowns by construction.

familystr

Which constructor produced this, e.g. "polynomial".

namestr

Prefix used for the coefficient symbol names.

poolExprPool

Pool everything is interned in.

metadatadict

Family-specific detail (degree, rates, the numerator/denominator split).

Examples
Python
>>> import alkahest as ak
>>> from alkahest.ansatz import polynomial
>>> pool = ak.ExprPool()
>>> x, y = pool.symbol("x"), pool.symbol("y")
>>> A = polynomial(pool, [x, y], degree=1, name="c")
>>> A.names
('c_0_0', 'c_1_0', 'c_0_1')
>>> A.expr
(c_0_0 + (x * c_1_0) + (y * c_0_1))
Properties
expr: Any
unknowns: tuple[Any, ...]
basis: tuple[Any, ...]
vars: tuple[Any, ...]
family: str
name: str
pool: Any
metadata: dict[str, Any]
names: tuple[str, ...]

Coefficient symbol names, parallel to unknowns.

Names are predictable, never gensym-ed: an agent that cannot predict them cannot write the follow-up call.

symbols: dict[str, Any]

{name: Expr} for the unknowns, for keying an instantiate() mapping by name.

Methods
instantiate(
    assignment: Mapping[Any, Any],
    *,
    simplify: bool=True
) → Any

Substitute coefficient values, returning a concrete member.

Parameters
assignmentmapping

{Expr or str: Expr or int or float}. Unknowns left out stay symbolic, which is how an underdetermined fit is returned.

simplifybool

Fold the substituted expression (so a zeroed coefficient really disappears). Turn off for a hot enumeration whose consumer normalises anyway.

Returns
Expr
Examples
Python
>>> import alkahest as ak
>>> from alkahest.ansatz import polynomial
>>> pool = ak.ExprPool()
>>> x = pool.symbol("x")
>>> A = polynomial(pool, [x], degree=1)
>>> A.instantiate({"c_0": 2, "c_1": -1})
(2 + (x * -1))
residual(target: Any) → Any

The residual whose vanishing means "this member equals target".

For most families this is just self.expr - target. For rational() it is the denominator-cleared form numerator - target * denominator, which is affine in the unknowns where p/q - f is not. Passing the naive difference to fit() works too — it clears denominators itself — but this states the intent.

Examples
Python
>>> import alkahest as ak
>>> from alkahest.ansatz import rational
>>> pool = ak.ExprPool()
>>> x = pool.symbol("x")
>>> A = rational(pool, [x], num_degree=1, den_degree=1)
>>> A.residual(pool.integer(1))
(a_0 + (x * a_1) + ((1 + (x * b_1)) * -1))
with_prefix(name: str) → Ansatz

A copy of this family with the coefficient prefix name.

Useful when two ansätze are fitted jointly and their coefficient names would otherwise collide.

class
class ansatz.AnsatzSolution

The outcome of fitting an Ansatz.

Shaped so that research.ResearchSession.record() accepts it unchanged: it exposes .value, .steps, .verification, and .certificate exactly as a DerivedResult does, and verification["status"] is one of the existing research.STATUS_BADGES keys — no new vocabulary.

Attributes
exprExpr

The fitted member. When the system is underdetermined this still contains the free unknowns, symbolically.

assignmentdict

{unknown: Expr} for the determined coefficients. Values may mention the free unknowns.

freetuple of Expr

Unknowns the constraints do not pin down. Non-empty means the members that work form a positive-dimensional family — often the interesting answer, so no arbitrary member is picked for you.

rankint

Rank of the collocation system; rank == len(ansatz) iff the fit is unique.

statusstr

Mirrors verification["status"]: "exactly_verified", "numerically_checked", or "unverified".

verificationdict

{"status", "evidence", "method", "externally_verified", ...}. "residual" carries the surviving normal form when the exact check did not close, and "max_abs_residual" the worst numeric sample.

stepstuple of dict

Derivation log in the STEP_FIELDS schema.

residualExpr

The residual as fitted (after any denominator clearing).

checkdict

A re-verification recipe in the shape research.ClaimGraph.verify() understands ({"kind": "zero", "expr": ...} — the back-substituted residual, which must normalise to zero). Pass it through as session.record(solution, check=solution.check) so a graph loaded from disk months later can re-derive the check rather than trust it.

pointstuple

The rows' provenance, as nested rational strings: the sample points actually used for certify="residual", or the multi-indices α for certify="exact". Enough to reproduce the system.

ansatzAnsatz

The family this came from.

certificateNone

Always None: this module emits no Lean certificate. Withholding beats emitting one nothing checked.

Properties
expr: Any
assignment: dict[Any, Any]
free: tuple[Any, ...]
rank: int
status: str
verification: dict[str, Any]
steps: tuple[dict[str, Any], ...]
residual: Any
check: dict[str, Any]
points: tuple[tuple[str, ...], ...]
ansatz: Ansatz
certificate: None
value: Any

Alias of expr — the attribute ResearchSession reads.

determined: bool

True when the constraints pin down a unique member.

badge: str

Honest one-line rendering of status.

constant
ansatz.DEFAULT_MAX_MEMBERS
DEFAULT_MAX_MEMBERS = 100000

No docstring.

constant
ansatz.DEFAULT_MAX_TERMS
DEFAULT_MAX_TERMS = 256

No docstring.

constant
ansatz.DEFAULT_SEED
DEFAULT_SEED = 1592631573

No docstring.