Skip to content

Equation representation

KD uses function-call term strings to exchange equation structure between algorithms and platform services. Each algorithm holds candidates in its own data structure internally and converts them on the way out. Algorithm term libraries and kd.evaluate_terms use this notation, with different requirements for accepted terms.

The notation describes structure rather than display. It is not intended to resemble the notation used in a paper. It separates equation structure from the way an algorithm displays its answer.

Public results use shared equation and result objects. A plugin's checkpoint state is an exception: it keeps the native representation needed by that same plugin to restore its search.

Term syntax

A term is a prefix function call, or a terminal token.

Written Meaning
u the field itself
u_x, u_xx first and second spatial derivatives, as terminal tokens
diff_x(u), diff2_x(u) the same derivatives, as calls
diff_x(diff2_x(u)) third derivative
mul(u, u_x) u times u_x
add(u_xx, mul(u, u_x)) a sum of two terms

The built-in operators are add, sub, mul, div, neg, n2 (square), n3 (cube), recip (reciprocal), sin, cos, exp, log, and lap, the spatial Laplacian: it sums the second derivative over the dataset's spatial axes, so on two spatial axes lap(u) is u_xx + u_yy, and lap(lap(u)) gives the biharmonic term. Six of them route through guarded implementations: div and recip floor a near-zero denominator, log floors a near-zero argument, and exp, n2 and n3 clamp their input magnitude, so none of these six produces an inf or a nan from a zero denominator or an overflowing exponent.

Coefficients are not part of a term. An algorithm returns terms, coefficients and a left-hand side separately, and the term string describes structure only. Two terms are therefore identical regardless of the coefficient fitted for them: mul(u, u_x) has the same term string whether the fit returned -1 or -0.5. Canonical term libraries, including those used by PySINDy and PySR for PDE data, require terms without numeric literals: write u rather than mul(2, u) and let the fit determine its coefficient.

The evaluation entry points kd.validate_terms and kd.evaluate_terms also accept numeric literals such as mul(2, u). A nonzero factor multiplying a whole term can be absorbed into its fitted coefficient. Acceptance by these entry points does not imply acceptance by an algorithm's canonical term library.

max_order sets the highest derivative order available as a terminal token. It is a parameter of kd.evaluate_terms and kd.validate_terms and defaults to 2, so u_xx can be written directly. Derivatives written as calls are not bounded by max_order, so diff_x(diff2_x(u)) resolves under the default settings, because the on-the-fly finite-difference scheme reaches third order.

Notation on the input side

Candidate-term libraries and operator sets are written in the same IR as the terms that come back out:

kd.PySINDyConfig().terms          # ('u', 'u_x', 'u_xx', 'mul(u, u_x)')
kd.PySRConfig().terms             # ('u', 'u_x', 'u_xx', 'mul(u, u_x)')
kd.DLGAConfig().library           # ['u', 'u_x', 'u_xx', 'u_xxx']
kd.DiscoverConfig().library.operators
# ['add', 'mul', 'sub', 'div', 'sin', 'cos', 'diff_x', 'diff2_x']

A candidate library can be reused where its terms meet the receiving algorithm's requirements for operators, derivatives and data. kd.evaluate_terms can refit the terms under its own validation contract. When a library carries a terminal token above the default max_order (u_xxx in the DLGAConfig list above), raise max_order to that order for the refit:

kd.evaluate_terms(dataset, kd.DLGAConfig().library, max_order=3)

Conversion per algorithm

  • SGA-PDE searches expression trees and maps the node names on the way out: + becomes add, * becomes mul, ^2 becomes n2, and the two derivative operators d and d^2 render as diff_x(...) / diff2_x(...) against the differentiation axis.
  • PySR returns a SymPy expression. It is expanded, split at the top-level sum, and each part is stripped of its numeric coefficient and serialized to IR; the platform refits the coefficients.
  • DISCOVER builds its trees from the IR operator names directly; the operator set above is its own.
  • PySINDy and DLGA-PDE select columns from a candidate library that is already IR.

The equation in a result

model.best_expr_ is a display string and each algorithm renders it its own way (SGA-PDE bakes the fitted coefficients into the text), so it is not the thing to align two algorithms on.

The equation-level object is model.result_.equation: terms paired with their coefficients, plus a left-hand side LhsSpec(field='u', axis='t', order=1), that is, u_t.

Structural comparison

kd.law_signature(equation) reduces an equation to a signature: the left-hand side joins the term set, terms are sorted in canonical form, and the coefficient vector is normalized with a fixed sign anchor. Two equations with the same structure get the same structure_key, whatever string each of them prints.

For example, the catalog sums add(diff_x(diff2_x(u)), mul(u,u_x)) and add(mul(u,u_x), diff_x(diff2_x(u))) differ in order. A comparison uses the terms and equation structure rather than that display order.

Canonical form reorders the arguments of add and mul (they commute) and performs no algebraic simplification, so mul(u, u_x) and mul(u_x, u) are one term. Stopping at commutativity is deliberate: simplification means a body of algebraic rules, and the more rules there are, the less obvious the reason two equations were called the same one.

The two runs below differ only in how the PySINDy candidate library is spelled and ordered:

import kd

dataset = kd.load_kdv()
for terms in [
    ("u_x", "u_xx", "diff_x(diff2_x(u))", "mul(u, u_x)"),
    ("mul(u_x, u)", "diff_x(diff2_x(u))", "u_x", "u_xx"),
]:
    model = kd.Model(
        algorithm="pysindy",
        config=kd.PySINDyConfig(terms=terms, threshold=1e-3, seed=42),
        verbose=False,
    )
    model.fit(dataset)
    signature = kd.law_signature(model.result_.equation)
    print(model.best_expr_)
    print(signature.structure_key, signature.terms)
add(diff_x(diff2_x(u)), mul(u,u_x))
1f62a06976f85133 ('diff_x(diff2_x(u))', 'mul(u,u_x)', 'u_t')
add(mul(u,u_x), diff_x(diff2_x(u)))
1f62a06976f85133 ('diff_x(diff2_x(u))', 'mul(u,u_x)', 'u_t')

The display strings differ while the structure_key is the same. That key is used to align results across algorithms.

Settle on one spelling for the derivatives before you align on it. The canonical form does no algebraic rewriting, so the terminal token u_xx and the call diff2_x(u) are two different terms, and one equation written both ways yields two different structure_key values. Give the algorithms you are comparing libraries in the same spelling: either both u_xxx or both diff_x(diff2_x(u)).