Skip to content

Library usage

import mathema and call it directly, for interactive or programmatic use (a notebook, a script, an agent's own tool calls), as opposed to the CLI.

The core calls

mathema.claim("f(-x) == -f(x)")              # state a claim
mathema.check(fn, claims=[...])               # verify it, return a Record
mathema.write_spec(fn, claims=[...])                # verify + write the record
mathema.status()                              # fresh/stale sweep
mathema.track_claims                          # optional bare tag, zero overhead

mathema.claims.check(fn, [...])               # the conjecture pipeline directly
mathema.registry.load_specs(root)             # read the whole spec store
mathema.registry.load_claims(path)            # parse an authoring-shape claims file

check(fn, claims=None, domain=None, trials=None, trials_scale=1.0, extensive=False, declared=None, known_premises=None)

Verify a function's claims, each adjudicated against the real function: mathema's suggested standard claims when claims is omitted, otherwise the claims you pass in.

mathema.check(ema)                                    # suggested claims
mathema.check(ema, claims=["f(x, 1.0) == x[-1]"])      # add a claim
mathema.check(ema, domain={"alpha": (0, 1)})           # sample inside a declared domain
mathema.check(ema, claims=["excluding"], domain={"alpha": (0, 1)})

domain declares parameter limits, and the algebraic probes sample inside the declared domain rather than across the whole real line. Whether the code itself rejects an out-of-domain input is a separate question: opt into it with the excluding keyword, which adds an excluded_outside_domain[param] claim per declared parameter.

Domains are per claim. domain= and the signature's Annotated markers form the function-level parent domain, which every claim inherits; a claim's own for binding overrides the parent for that parameter. One claim's quantifier never reaches another claim, so f(x) >= 0 checked beside for x in [0, 1e6], f(x) >= 0 still means every x. Each record states what it was adjudicated over: the claim's own bindings in its domain and condition, the parent's share in meta["mathema.parent_domain"]. The excluding keyword reads the parent domain only: a parameter bounded only inside one claim has no function-level outside to exclude.

A parameter or return type hinted with a mathema type marker (Annotated[float, Probability], Annotated[list, Shape("m", "n")]) contributes its own claims automatically; see Authoring claims. A @claims_decorator(...)-tagged function, or a docstring Claims: block, also contributes its claims automatically. claims= passed here is unioned with those, winning per claim name on a collision.

With claims=None (the default), mathema also adjudicates and displays its own suggested standard claims (every row in the transcript below is one of these), candidates for adoption that never gate an exit code and are never written into a spec record (see mathema claims). claims=[] means declared surfaces only: type markers, decorator, and docstring claims still run, but no suggestions are added.

Returns a Record: the facts read off the function, and one Probe per claim adjudicated. For the exponential moving average of the first look:

import mathema

def ema(x: list, alpha: float) -> float:
    """Exponentially weighted moving average."""
    y = x[0]
    for v in x[1:]:
        y = alpha * v + (1 - alpha) * y
    return y
>>> mathema.check(ema, domain={"x": (-1e6, 1e6), "alpha": (-10, 10)})
mathema.Record(ema) · source, no side effects · form 5108dc8b5d5c
  FALSIFY monotonic_increasing[alpha]: d(f(x, alpha), alpha) >= 0
           counterexample alpha=-5.44324 -> -504886.9526187774, alpha=9.99998 -> -2491045.924006212 (not increasing)
  FALSIFY monotonic_decreasing[alpha]: d(f(x, alpha), alpha) <= 0
           counterexample alpha=3.09918 -> 43.62072599569275, alpha=10 -> 21771.614551164577 (not decreasing)
  FALSIFY affine[alpha]: d(f(x, alpha), alpha, alpha) = 0
           counterexample alpha=3.53765, h=0.02: curvature estimate 3.12726 does not settle affine
  FALSIFY convex[alpha]: d(f(x, alpha), alpha, alpha) >= 0
           counterexample alpha=8.52571, h=0.02: curvature estimate -221.981 does not settle convex
  FALSIFY concave[alpha]: d(f(x, alpha), alpha, alpha) <= 0
           counterexample alpha=-0.594668, h=0.02: curvature estimate 1222.99 does not settle concave
  proven  is_deterministic: f(x, alpha) = f(x, alpha)
           where y=alpha: ∀ x ∈ Seq(ℝ), y ∈ [-10, 10] ⊂ ℝ ∪ {∅}
  proven  is_state_safe: f(x, alpha) = f(x, alpha)
  holds   is_numerically_stable: let g = mathema.f.finite_no_error, g(f, x, alpha) = 1 (n=192)
  holds   is_representation_safe[alpha]: is_representation_safe(alpha) (n=20)
  FALSIFY bounded_lower: min(x) <= f(x, alpha)
           counterexample ([0, 902023, 21859.3, 865038, -999998, 740637, -77557.7, 946318], -8.89934): -999998.0 vs -7639061686900.594
  FALSIFY bounded_upper: f(x, alpha) <= max(x)
           counterexample ([103173, 1e+06, -993405, -514945, 606668, -999998], 6.56264): 6722876822.250346 vs 1000000.0
  FALSIFY permutation_invariant: let g = mathema.f.reverse_seq, f(x, alpha) = f(g(x), alpha)
           counterexample ([519138, 999998, 1e+06, 0, 917863, 745687, -818085], 10): -248319487748.5595 vs -814138493405.3986
  proven  scale_equivariant: let g = mathema.f.scale_seq, let c be [-5.0, 5.0]:float|missing, c*f(x, alpha) = f(g(x, c), alpha)
           where y=alpha: ∀ x ∈ Seq(ℝ), y ∈ [-10, 10] ⊂ ℝ ∪ {∅}
  holds   scale_equivariant[float]: let g = mathema.f.scale_seq, let c be [-5.0, 5.0]:float|missing, c*f(x, alpha) = f(g(x, c), alpha) (n=48)
  proven  translation_equivariant: let g = mathema.f.shift_seq, let c be [-5.0, 5.0]:float|missing, c + f(x, alpha) = f(g(x, c), alpha)
           where y=alpha: ∀ x ∈ Seq(ℝ), y ∈ [-10, 10] ⊂ ℝ ∪ {∅}
  holds   translation_equivariant[float]: let g = mathema.f.shift_seq, let c be [-5.0, 5.0]:float|missing, c + f(x, alpha) = f(g(x, c), alpha) (n=48)

Read the FALSIFY rows as facts about ema, not as bugs in it. The suggestions ask standard questions of any function, and a falsified suggestion is an answer, with the witness to prove it: an exponential average really is neither monotone nor order-independent in its inputs. Each [float] row is the float companion of the proof above it, the same law run through the real code in floating point inside the stated domain; both hold. Two of the answers change once the smoothing factor's real domain is stated. Checked with alpha in (0, 1), the bounds that failed for an alpha outside it are proven outright, and their float companions hold (an excerpt):

>>> mathema.check(ema, domain={"x": (-1e6, 1e6), "alpha": (0, 1)})
  proven  bounded_lower: min(x) ≤ f(x, alpha)
  holds   bounded_lower[float]: min(x) <= f(x, alpha) (n=44)
  proven  bounded_upper: f(x, alpha) ≤ max(x)
  holds   bounded_upper[float]: f(x, alpha) <= max(x) (n=44)

That is the loop in miniature: the suggestion found the assumption the code relies on, and stating it turned a counterexample into a proof.

write_spec(fn, claims=None, root=".", key=None, **kwargs)

The one-call IO workflow: retrieve the declared layer from the project store under root, check the function's claims against it, write the record into the store (.mathema/verified/), and return the Record. Everything check() accepts, write_spec() also accepts.

mathema.write_spec(ema, claims=[...])
mathema.write_spec(ema, domain={"a": (0, 1)})

The written key defaults to module.qualname (or bare qualname for a __main__-defined function), pass key= to override.

retrieve(fn_or_key, root=".", *, store=None)

The declared layer's one read entry point: the full declared entry for a function, with the hand-written claims-file store under root joined in at the documented precedence (file wins per claim name over decorator/docstring claims). Takes the live function or its dotted key. check() itself never reads the filesystem, pass the retrieved entry in explicitly:

rec = mathema.check(ema, declared=mathema.retrieve(ema, root="."))

write_spec(), the CLI, and the MCP tools do this join for you.

A sweep over many functions that has already loaded the whole declared store once passes it as store (the mapping spec.load_declared() returns) to skip the per-function re-read, which is the difference between one tree walk and one per target.

analyze(fn)

Machine-derived facts about a function: parameters, purity, guards, structural shape. Pure ast, no probing, no claim verification. Falls back to a documentation-only record for a callable with no retrievable Python source (builtins, C extensions), the docstring states the intent, and probing still runs against the live callable.

track_claims

An optional bare decorator tag: @mathema.track_claims. Runs once, at decoration time; the function object is returned unchanged (no wrapper, zero call overhead). Entirely optional, everything still works without it. What an untracked function loses is visibility in mathema.status(), since status can only report on functions it knows exist. See mathema verify --status in mathema verify.

%%mathema in IPython/Jupyter

%load_ext mathema
%%mathema
def my_fn(x: list, alpha: float) -> float:
    ...

Runs the cell, then for every function it defines: checks it, writes its record to .mathema/verified/, and displays the result inline.