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