Skip to content

Add GP_ELITE (pure-Python GP, LM constants, optional dimensional constraints) - #212

Open
ariel95500-create wants to merge 10 commits into
cavalab:masterfrom
ariel95500-create:master
Open

Add GP_ELITE (pure-Python GP, LM constants, optional dimensional constraints)#212
ariel95500-create wants to merge 10 commits into
cavalab:masterfrom
ariel95500-create:master

Conversation

@ariel95500-create

Copy link
Copy Markdown

This PR adds GP_ELITE, a pure-Python symbolic regression library
(pip install gp-elite, MIT).

Method: genetic programming with island parallelism, Levenberg-Marquardt
constant optimization, native multi-restart with merged archives, and a
complexity/accuracy Pareto front. The regressor passes scikit-learn's
check_estimator, implements max_time via time-boxed sequential restarts,
and model() returns a sympy-compatible string using the dataset column
names, with input normalization folded in (the string reproduces
est.predict on raw features to ~1e-12).

Internal run of the SRBench ground-truth protocol on the 119 Feynman
datasets (10k rows, 75/25 split, 3 seeds, R2_test>0.999): 60.2% of runs
solved, 69% of datasets solved by at least one seed, median R2 0.99973.
Raw jsonl logs and the runner are available in the project repo.

Repo: https://github.com/ariel95500-create/gp-elite
Happy to adjust anything to fit the harness.

@ariel95500-create ariel95500-create changed the title Add GP_ELITE (pure-Python GP with Levenberg-Marquardt constants) Add GP_ELITE (pure-Python GP, LM constants, optional dimensional constraints) Jul 27, 2026
@ariel95500-create

Copy link
Copy Markdown
Author

Bumped to gp-elite 0.4.1 (now on PyPI) and updated metadata.yml.

0.4.1 fixes a silent float64 overflow in the Levenberg-Marquardt constant
optimizer: unbounded sq/cube/* chains could reach ~1e198 and overflow during
the Jacobian products, degrading fits without raising. It also fixes two bugs
in the optional dimensionally-constrained search mode, which is not exercised
here since the harness does not pass units.

For reference, build-and-test (gp-elite) is green on this run (7m). The 10
failing checks are other algorithms (brush, bsr, e2et, eql, feat, nesymres,
operon, pysr, sklearn, tpsr); operon passed on the previous run and failed on
this one without any change to it, so these look unrelated to this submission.

Happy to adjust anything to fit the harness.

Add metadata for GP_ELITE symbolic regression package
Implement GPEliteSRBench class for symbolic regression with time constraints.
@ariel95500-create

Copy link
Copy Markdown
Author

Reopening — the branch was accidentally reset while syncing the fork,
which emptied this PR. Files restored, now targeting gp-elite 0.5.0.

Implemented GPEliteSRBench class for symbolic regression with time constraints and multiple restarts. Added methods for fitting the model, generating sympy expressions, and calculating model complexity.
@ariel95500-create

Copy link
Copy Markdown
Author

Restored after an accidental fork sync emptied this PR — the branch was
reset and the files were lost. Everything is back, now targeting gp-elite
0.5.0, which adds an optional mode that deduces the units and value of a
law's missing physical constant (not exercised here, as the harness does
not pass units).

build-and-test (gp-elite) is green on this run (8m). The remaining
failures are other algorithms and predate this PR.

Apologies for the churn.

lacava added a commit that referenced this pull request Aug 13, 2026
every push and PR rebuilds all 27 images, because the gate in build-and-test
is hardcoded to should_run=true (5b13029). check-changes already computes what
changed, but nothing consumes it. one method submission can cost a lot: #209
went through 16 full 27-image runs, #210 and #212 another 24 between them.

pull requests now build only the methods they touch. everything else - pushes
to master/dev, the new weekly schedule, manual dispatch - still rebuilds
everything, so a method that breaks from upstream drift without anyone
touching it still gets caught. that drift is calendar-driven, which is what
the schedule is for; during a quiet stretch there are no merges to catch it.

- a method rebuilds if either algorithms/<name>/ or experiment/methods/<name>/
  changed. the second one matters: a regressor.py edit has to retest the
  method even though the install is untouched.
- changes to shared build inputs (dockerfiles, base_environment, scripts,
  entry.sh, configure.sh, workflows) still rebuild everything.
- build-and-test always runs and always reports for every algorithm, so the
  check names stay present and can be marked required. only the docker build
  step is skipped.
- dropped always() from build-and-test. with the gate inside the job, a failed
  check-changes would have left an empty build list, skipped every build and
  reported green.
- check-changes no longer diffs against github.event.before, so a force-push
  to a CI branch no longer fails the job.

also fixes a long-standing bug: changed-experiments used awk field $2 on
experiment/methods/<name>/..., which is the literal string "methods", not the
method name. it needs $3. nothing consumed that output before, so it never
showed up.
@lacava

lacava commented Aug 13, 2026

Copy link
Copy Markdown
Member

Thanks — the only thing failing is a missing __init__.py. experiment/methods// needs an empty one so the harness can import your method. This used to be created automatically by a CI script that no longer exists, and our docs hadn't caught up — sorry about that. The layout is now documented in CONTRIBUTING.md, and validate-layout checks it.

One command from your branch:


touch experiment/methods/gp-elite/__init__.py
git add experiment/methods/gp-elite/__init__.py
git commit -m "add missing __init__.py for gp-elite"
git push

lacava and others added 2 commits August 13, 2026 12:44
@lacava

lacava commented Aug 14, 2026

Copy link
Copy Markdown
Member

thanks for your submission; sorry about the __init__.py runaround earlier, that was our docs being out of date.

the code looks good overall.

one thing i'd like changed before merging. fit() only sets self.model_ after the restart loop finishes:

for k in range(max(1, int(self.restarts))):
    r = symbolic_regression(...)
    ...
    if elapsed > 0.85 * budget or elapsed + elapsed / (k + 1) > budget:
        break
self.model_ = best

the budget is only checked between restarts, and a single symbolic_regression call isn't time-bounded. if one restart overruns, the harness's SIGALRM fires at max_time + 600, evaluate_model catches the timeout and continues, and then model(est) hits est.model_ and raises AttributeError — so the run produces no result at all instead of a worse result. one restart took ~181s on the 15-row test dataset, so on the larger pmlb problems this is a real possibility.

suggest moving the assignment inside the loop:

    if score > best_score:
        best, best_score = r, score
        self.model_ = best
        self.equation_ = best.expression
        self.is_fitted_ = True

if symbolic_regression accepts a time limit, passing the remaining budget per restart would close the gap on the first one too.

two other questions:

  1. in _to_sympy, leaves are split by isinstance(v, (int, float)). np.float64 subclasses float so it's fine, but np.int64 doesn't subclass int. an integer constant stored that way would fall through to the str(v) branch, get its digits extracted, and come out as cols[i], i.e. a feature name rather than a constant. can the engine ever hand back integer constants as numpy ints?

  2. the operator fallbacks assume every op name is a sympy function or a valid infix symbol. what's the full set under "physical" and "full"? something like atan2 or max would produce a string sympy can't parse, and the test only exercises whatever ops turned up in one run.

@ariel95500-create

Copy link
Copy Markdown
Author

Thanks for the careful review, all three are real and fixed in the latest commit.

On the model_ timing: I moved the assignment inside the loop so the champion is published as soon as it's found. You're right that symbolic_regression isn't itself time-bounded, so a single overrunning restart could hit SIGALRM before assignment and lose the whole run; now the harness will always find the best result so far. I don't currently expose a per-call time limit in the engine, so the first restart still isn't bounded, but it's no longer fatal. I'll add a real per-call budget upstream as a follow-up.

On np.int64: good catch. In practice the engine only ever emits native float leaves (I checked across many evolved models), so it doesn't trigger today, but the guard was fragile, so I now test np.integer and np.floating explicitly.

On the operators: you're right that the fallback was unsafe. Under physical and full the sets are {+, -, *, /, pow, max2, min2} and {abs, cos, cube, exp, is_even, log, neg, sin, sq, sqrt, step, tan, tanh}. max2 and min2 fail to parse, and cube, step and is_even silently parse as undefined sympy functions rather than the intended math, so I replaced the fallback with an explicit mapping for every operator (Max/Min, **3, Heaviside, and so on), with domain guards on sqrt and log to match the engine, and an explicit error for any unmapped name.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants