Skip to content

Equation representation

Equations are represented the same way everywhere in KD, as term strings. This applies to an algorithm's input configuration, the answer it returns, and the argument to kd.evaluate_terms. Each algorithm holds candidates in its own data structure internally and converts them on the way out.

The notation describes structure rather than display. It is not intended to resemble the notation used in a paper. Its purpose is to ensure that one law is written as one string regardless of which algorithm produced it.

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. A constant written into a term is accepted (mul(2, u) is a valid term), but the fitted coefficient absorbs it, so the constant has no effect.

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 assembled for one algorithm can go straight to another algorithm, or to kd.evaluate_terms for a refit. 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.

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)).