https://github.com/sha0coder/eqhunt
Genetic Algorithm to learn ecuations from data
https://github.com/sha0coder/eqhunt
Last synced: 4 days ago
JSON representation
Genetic Algorithm to learn ecuations from data
- Host: GitHub
- URL: https://github.com/sha0coder/eqhunt
- Owner: sha0coder
- Created: 2026-05-19T09:38:19.000Z (2 months ago)
- Default Branch: main
- Last Pushed: 2026-05-19T09:42:36.000Z (2 months ago)
- Last Synced: 2026-05-19T11:33:24.799Z (2 months ago)
- Language: C++
- Size: 14.6 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
# eqhunt
pip install eqhunt
Symbolic regression by genetic programming. C++ engine, Python bindings via [nanobind](https://github.com/wjakob/nanobind).
Give it a table of `(inputs, target)` pairs; it returns a human-readable formula
that approximates the relationship. No neural network, no black box — just an
algebraic expression you can read, paste into a calculator, or hand-tune.
```python
import eqhunt
X = [[1, 1], [2, 3], [4, 5], [7, 2], [9, 9]]
y = [2, 5, 9, 9, 18]
model = eqhunt.fit(X, y)
print(model.formula) # e.g. f(x,y) = (x+y)
print(model.error) # e.g 0.0
print(model.predict([6, 7])) # -> 13.0
```
## Install
```bash
pip install eqhunt
```
Prebuilt wheels are published for Linux, macOS and Windows on common Python
versions. If pip falls back to building from source you'll need a C++17 compiler.
## Two ways to use it
### Ultra-simple
```python
import eqhunt
model = eqhunt.fit(X, y, generations=5000)
print(model.formula)
model.predict([1, 2]) # single row
model.predict([[1, 2], [3, 4]]) # batch
```
`fit()` accepts any `Config` field as a keyword argument:
```python
eqhunt.fit(X, y, pop=800, trig_penalty=2.0, bloat_penalty=0.3)
```
### Fully configurable
```python
import eqhunt
cfg = eqhunt.Config()
cfg.pop = 800
cfg.gen = 50000
cfg.tournament_size = 5
cfg.initial_depth = 5
cfg.bloat_penalty = 0.3
cfg.trig_penalty = 1.5
cfg.accepted_error = 0.01
# Re-weight individual operators (higher = more likely to appear)
cfg.op_weights.sin = 1.0 # boost sine
cfg.op_weights.cos = 1.0
cfg.op_weights.exp = 0.0 # disable exp entirely
cfg.pi_prob = 0.10 # 'pi' more frequent in terminals
model = eqhunt.Model(cfg).fit(X, y)
print(model.formula)
```
You can also train from a CSV file (one row per sample, last column = target,
lines starting with `#` are comments):
```python
eqhunt.Model().fit_csv("nivel_embase.csv")
```
## Operators available
| Category | Operators |
|------------|----------------------------------|
| Arithmetic | `+ - * / -x` |
| Powers | `sqrt **` |
| Conditional| `if(cond, then, else)` (cond > 0)|
| Trig | `sin cos tan` |
| Exp / log | `exp log` |
| Constants | numeric literals, `pi` |
Trigonometric, log and exp nodes have **low default weights** so they only
appear after enough mutation pressure — useful for cyclic / physical data,
ignored otherwise. Adjust via `Config.op_weights`.
## How error and validity are handled
* Per-sample error is `|prediction - target|`; total error is the sum.
* Invalid evaluations (`/0`, `sqrt(<0)`, `log(<=0)`, `exp(huge)`) get a soft
per-sample penalty rather than killing the whole formula — a single
out-of-domain sample no longer disqualifies an otherwise good candidate.
If more than 25% of samples fail, the formula is rejected.
## Stopping early
`Config.accepted_error` stops the search as soon as total error drops below
the threshold. You can also call `model.stop()` from another thread (or a
signal handler) to ask the loop to wrap up after the current generation.
## Saving and reloading a formula
A trained model is just a string — you can persist it, ship it, paste it,
diff it. To reuse a formula in a new process without retraining, parse it
back into a `Model`:
```python
import eqhunt
# train and save
m = eqhunt.fit(X, y)
print(m.formula) # e.g. f(x,y) = ((x*x) - (y*y))
m.save("model.txt") # one-liner persisted
# later, in a fresh process — no training needed
m2 = eqhunt.Model.load_file("model.txt")
m2.predict([6, 7]) # -13.0
m2.predict([[1, 2], [3, 4]])
```
You can also go through strings directly:
```python
formula_str = m.formula # or any equivalent expression
m3 = eqhunt.Model.from_formula(formula_str)
m3.predict([12, 5])
```
Or mutate an existing model in place:
```python
m.load_formula("(x*x + y*y)") # replaces the current tree
```
Accepted syntax: anything the engine itself emits via `get_formula()` —
arithmetic (`+ - * / **`), unary minus, `sqrt sin cos tan log exp if`,
variables `x y z w v u x6 x7 …`, numeric literals (int / float / `1e5`),
and `pi`. Both the bare expression (`"(x+y)"`) and the full prefixed form
(`"f(x,y) = (x+y)"`) are accepted; the parser strips everything up to and
including the first `=`. Parse errors raise `RuntimeError`.
The number of input variables is inferred from the highest variable index
in the formula, so `m2.num_vars` is set correctly without needing to know
it in advance.
## Config reference
| Field | Default | Meaning |
|----------------------|---------|------------------------------------------------------|
| `pop` | 400 | Population size |
| `gen` | 15000 | Max generations |
| `tournament_size` | 4 | Tournament selection pool |
| `crossover_prob` | 0.7 | Crossover probability per pair |
| `mutation_prob` | 0.25 | Mutation probability per offspring |
| `initial_depth` | 4 | Depth used to seed the initial population |
| `mutation_depth` | 3 | Depth for mutation-generated subtrees |
| `const_min/max` | -9, 9 | Range for random numeric terminals |
| `pi_prob` | 0.01 | Probability a terminal is `pi` |
| `bloat_penalty` | 0.1 | Per-node penalty (favours smaller trees) |
| `trig_penalty` | 0.5 | Extra penalty per `sin/cos/tan/log/exp` node |
| `immigrant_rate` | 0.05 | Fraction of population replaced by random each gen |
| `weak_parent_rate` | 0.2 | Prob. 2nd parent is random (not tournament) |
| `accepted_error` | 0.5 | Stop training once total error < this value |
| `verbose` | False* | Print best-so-far per improvement |
| `simplify` | True | Run algebraic simplification on the final tree |
| `simplify_interval` | 500 | Periodically simplify top-N members during training |
| `simplify_top_n` | 10 | How many to simplify periodically |
*C++ default is `True`; the Python `fit()` helper defaults to `False`.
## Building from source
```bash
git clone https://github.com/sha0coder/eqhunt
cd eqhunt
pip install -e .
pytest
```
Requires Python 3.8+, a C++17 compiler, CMake 3.15+.
## License
MIT.