“Know how to solve every problem that has been solved.” “What I cannot create, I do not understand.” — Richard Feynman

Symbolic Regression: Rediscovering Physical Laws from Data

Machine Learning

Ordinary regression fits the parameters of a form you already chose: you decide the model is a line, or a Gaussian, or a power law, and least squares hands you the coefficients. Symbolic regression asks the harder question — what is the form? Given a table of numbers, it searches the space of algebraic expressions themselves, built from and the input variables, and returns a formula. Kepler spent years staring at Tycho Brahe's orbital tables before he saw that the period squared goes as the radius cubed. This page builds a symbolic-regression engine from scratch — a real genetic-programming search, no libraries — hands it the same eight planets, and watches it recover Kepler's third law in a few seconds of evolution.

Searching the space of formulas

Represent every candidate law as an expression tree: internal nodes are operators, leaves are variables or constants. The space of such trees is the space of all formulas, and it is astronomically large — which is exactly why you search it the way evolution searches genotypes. Start with a population of random trees. Score each by how well it fits the data. Breed the good ones: crossover splices a subtree from one formula into another; mutation perturbs a node. Repeat. Good structure — a here, a product there — survives and spreads because the trees that carry it fit better. It is curve-fitting where the curve's shape is the thing under selection.

One decision makes symbolic regression work instead of embarrassing itself. Left to minimize error alone, the search will grow a monster: a tree with forty nodes can thread every data point including the noise, and it will report a "law" that is really a memorized table. The cure is to refuse to optimize accuracy in isolation and instead track the Pareto front — the best-fitting expression at each level of complexity — and then read off the one at the elbow, where accuracy stops improving and complexity keeps climbing. That elbow is where a law lives: the simplest expression that explains the data, and not one node more.

# Expression trees:  ['op', child, ...]  or  ('var', i)  or  ('const', c)
# Protected operators so a bad subtree just scores poorly instead of crashing.
OPS = {'add': (2, add), 'sub': (2, sub), 'mul': (2, mul), 'div': (2, pdiv),
       'sqrt': (1, psqrt), 'sq': (1, square), 'log': (1, plog)}

def evaluate(tree, x):                       # x = the input variables of one sample
    if tree[0] == 'var':   return x[tree[1]]
    if tree[0] == 'const': return tree[1]
    return OPS[tree[0]][1](*[evaluate(c, x) for c in tree[1:]])

def fit_and_score(tree, X, y):               # fit  a*expr + b  by least squares
    p = predict(tree, X)                     # -> the shape is discovered, the
    A = np.vstack([p, np.ones_like(p)]).T    #    two constants are fit linearly
    (scale, off), *_ = np.linalg.lstsq(A, y, rcond=None)
    return np.sqrt(np.mean((y - (scale * p + off)) ** 2))     # RMSE

# Evolve, and keep the BEST expression seen at each complexity -> the Pareto front
for g in range(generations):
    scored.sort(key=lambda s: s.rmse)
    newpop = scored[:elite]                                   # carry the best forward
    while len(newpop) < pop:
        a = tournament(scored)                               # pick a fit parent
        child = (crossover(a, tournament(scored))            # splice two subtrees...
                 if random() < 0.75 else mutate(a))          # ...or perturb one
        newpop.append(child)
    for t in newpop:
        c = complexity(t)                                    # node count
        if fit_and_score(t, X, y) < pareto[c].rmse:
            pareto[c] = t                                    # record on the front

Two details matter. The fitness fits a linear scale by least squares before scoring, so the search only has to discover the shape — and the two calibration constants come for free; it never has to evolve the number node by node. And the operators are protected — division by near-zero returns one, of a negative returns zero — so a nonsensical subtree simply scores badly instead of throwing, and evolution routes around it.

Kepler's third law, from eight numbers

Log-log plot of orbital period versus semi-major axis for the eight planets, from Mercury at lower left to Neptune at upper right. The blue data points fall exactly on the red line T equals a to the three-halves.

Feed the engine the semi-major axes and orbital periods of the eight planets — sixteen numbers, no units, no hints — and let it evolve. Read the result off the Pareto front:

Rediscover T(a) from 8 planets   (operators: + - * / sqrt sq log)

  Pareto front  (accuracy vs complexity)
    complexity  1:   RMSE = 8.495 yr   R^2 = 0.9764    ~  5.385*a - 8.79
    complexity  2:   RMSE = 6.171 yr   R^2 = 0.9875    ~  0.183*a^2 + 4.80
    complexity  4:   RMSE = 0.008 yr   R^2 = 1.000000  ~  0.999*(a*sqrt(a))   <-- !
    complexity  7:   RMSE = 0.007 yr   R^2 = 1.000000  ~  ... + O(1e-3) noise terms
  chosen law (the elbow):   T = a*sqrt(a) = a^(3/2),   i.e.   T^2 = a^3
  log-log exponent fit:  1.4997      (Kepler's exponent = 3/2)

Noisy law   y = 2*x1*x2 + 0.5   (15% noise on 60 points)
    complexity  1:   RMSE = 2.577    ~  3.26*x2
    complexity  3:   RMSE = 0.467    ~  2.010*(x1*x2) + 0.537       <-- hits noise floor
    complexity  9:   RMSE = 0.458    ~  x2*(x1 + x1/log|x1+x2|)     <-- fitting noise
  noise floor RMSE = 0.472;   parsimony stops at complexity 3 = the true law

At complexity 1 the best it can do is a straight line, — decent, and completely wrong. At complexity 4 it finds and the error drops by three orders of magnitude to . Every more complicated expression on the front is that same law plus corrections at the level — fitting the last digits of the tabulated radii, not physics. So the elbow is unmistakable, and it reads , which is Kepler's third law, . As a check, the raw log-log slope of the data is against Kepler's exact . The machine did in seconds what took Kepler the better part of a decade — not because it is smarter, but because it can try a million formulas without getting tired.

Why parsimony decides everything

The Pareto front: RMSE versus expression complexity. The error drops steeply from complexity 1 to 3, hits a dashed horizontal line marking the noise floor at complexity 3, and then stays flat as complexity increases further.

Real data is noisy, and this is where a naive fit-the-error search destroys itself. Take and corrupt it with 15% noise. The Pareto front finds the true law — — at complexity 3, and that expression is the first to reach the noise floor (RMSE 0.467 against a floor of 0.472): there is nothing left to explain but the noise itself. Everything past the elbow proves the point by failing it — the search starts bolting on terms like that shave the RMSE by a percent while fitting pure randomness. The Pareto front is the guardrail: it makes the difference between a discovered law and an overfit one visible as a corner in a plot, rather than something you have to intuit.

What makes it hard, and what real engines add

The point, for a computational scientist

Kepler is a demonstration with a known answer; the reason to own this tool is the case where the answer is not known. Run a simulation — DFT adsorption energies across a catalyst series, excitation energies across a family of dyes, a transport coefficient across a phase diagram — and you have a table exactly like the planetary one, except no one has found the law yet. Symbolic regression is how you get an interpretable scaling relation out of your own output instead of a black-box fit: the superexchange , a Sabatier volcano, a structure–property trend. Every DFT study that follows on this site is going to end by handing its numbers to this engine and asking what law they obey.

Try First

Each prompt asks a checkable question about the working code or math above — predict an output, derive a sign, state an invariant, find a bug. Commit to an answer before clicking "reveal." That commitment is the whole point: if your answer matched, you understand the piece you were looking at; if it didn't, that's the part worth re-reading.

predict
Kepler's law was found because was in the operator set (the engine built ). Suppose you remove and (power) but keep . Can the search still recover the law, and what will the Pareto front look like?
why does this work
A neural network fit to the eight planets would also predict from with tiny error. Why is the symbolic answer worth more than an equally accurate network?

Reproduce it

scripts/gen_symbolic_regression.py is the whole engine — expression trees, tournament selection, subtree crossover and mutation, and the complexity-indexed Pareto front — in a few hundred lines with no dependencies beyond NumPy and SymPy (used only to pretty-print the winning tree). It rediscovers from the real planetary data (, exponent ) and recovers from noisy samples without overfitting past the noise floor. Change the operator set or feed it your own table and watch the front move.