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:
+becomesadd,*becomesmul,^2becomesn2, and the two derivative operatorsdandd^2render asdiff_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)).