Rust Core Edition · v2.7.6 · Updated
geatpy-rsHigh-Performance Evolutionary Algorithm Toolbox
geatpy-rs is a pure Rust (PyO3 + abi3) rewrite of the core of Geatpy, the classic Python toolbox for evolutionary and genetic algorithms. pip install geatpy-rs installs prebuilt wheels for Python 3.8 – 3.15 (plus free-threaded 3.14t) on Linux x86_64 / ARM64, macOS (Apple Silicon and Intel) and Windows x64, and code written with import geatpy as ea keeps working unchanged. The core’s behaviour has been checked case by case against the official 2.7.0 binaries, and classic benchmark cases generally run faster than the original.
pip install geatpy-rs- Python Versions Supported
- 3.8 – 3.15
- Rust Core Operators
- 74
- Algorithm Templates Intact
- 42
- Differential Test Cases Verified
- ≈1500

Why Rust
Why Rebuild in Rust?
The original Geatpy depended on specific C/C++ dynamic libraries, facing three major challenges in modern development and production environments:
- 01
Install Fails on Modern Python
The original Geatpy only published prebuilt wheels up to Python 3.10. On Python 3.11 – 3.15 pip finds no matching wheel and falls back to building from source, which fails because the original core is not released as source.
- 02
Incomplete ARM Platform Coverage
The original Apple Silicon wheels only cover Python 3.8 – 3.10, and there are no Linux ARM64 (aarch64) wheels at all, so it cannot be installed on ARM cloud servers, Raspberry Pi or in Docker containers.
- 03
Opaque Binary Core
The original core is a precompiled C/C++ binary: edge cases such as length-1 chromosomes or one-dimensional input segfault and take down the whole Python process, and several operators behave differently from their own documentation.
Comparison
Core Feature Comparison
Technical comparison between geatpy-rs and the original official C/C++ version:
| Dimension | Official Release (Geatpy) | geatpy-rs |
|---|---|---|
| Core Implementation | Legacy C/C++ shared library, distributed only as a precompiled binary | Rust implementation (crates/geatpy_core), source code public |
| Python Support | Python 3.5 – 3.10 only | Python 3.8 – 3.15 full coverage, plus free-threaded (no-GIL) Python 3.14t wheels |
| ABI Compatibility | Built separately for each minor Python release | PyO3 abi3: single wheel runs across 3.8+; separate build for free-threaded 3.14t |
| Prebuilt Wheels | Windows x86/x64, Linux x86_64, macOS (Apple Silicon Python 3.8–3.10 only), no Linux ARM64 | macOS Universal2 (Apple Silicon + Intel), Linux x86_64 / ARM64, Windows x64 |
| Memory Safety | Edge-case input can segfault and kill the Python process | Safe Rust (the one unsafe call runs the FMA-instruction dot product after checking that the CPU supports FMA); empty, reversed, oversized and other edge-case input raises a Python exception |
| Speed & Concurrency | C OpenMP | 2.7.6 is single-threaded (0.05–0.73x runtime on 36 classic cases; 0.27–0.46x generation time with 100k variables); free-threaded 3.14t runs multiple templates in parallel threads; 2.8.0b2 preview adds automatic multithreading for large inputs, bit-identical to single thread |
| Reproducibility | Inherits numpy global random state, reproducible via np.random.seed | Identical to original: np.random.seed / ea.optimize(seed=…) fully reproduces any run |
| API Compatibility | Baseline | Same Python API, data structures and 42 algorithm templates; core behaviour checked case by case against the official 2.7.0 binaries |
Parity
Parity with Official 2.7.0 Kernel
The Rust kernel treats the official geatpy 2.7.0 binaries as behavioral ground truth, validated continuously through black-box differential tests:
Deterministic Operators Compared Value by Value
Non-dominated sorting, crowding distance, encoding and decoding, fitness assignment, reference points, performance indicators and NSGA-III / RVEA environmental selection are compared value by value against the actual output of the official 2.7.0 binaries.
Random Operators Compared by Distribution
Crossover, mutation and selection operators are called hundreds to thousands of times and their element-wise means are compared, confirming they follow the same distribution as the official implementation.
Templates and Classic Benchmarks Compared by Solution Quality
39 algorithm templates are compared on identical problems by final solution quality. In addition, 36 classic cases such as Ackley, Rastrigin, att48 TSP, ZDT and DTLZ, covering maximisation, mixed-integer, constrained, permutation, multi-population and multi-chromosome templates, run 30 times on each side to compare the distribution of the best objective or of IGD / HV. There is no significant difference, except that multi-population templates find clearly better solutions because a migration bug in the original was fixed.
About 1500 Randomised Differential Cases
Random shapes, parameter combinations and boundary values run on both the official binary and the Rust core, comparing values, dtypes, return structures and errors one by one; all-integer variables still come back as int32, and argument checks and error messages match the original.
Edge-Case Input Checked One by One
Empty populations, empty reference sets, reversed or three-dimensional arrays, oversized input, NaN, and wrong argument types or keywords are each checked against the official binary, with the same error types and messages; where the original crashes or reads out of bounds, geatpy-rs raises a Python exception instead.
24 known defects in official binaries (fixed per documentation in geatpy-rs)
- ndsortDED assigning wrong fronts
- mergecv accumulating across rows
- tcheby / pbi returning the ideal point when idealPoint=None
- refselect misordering rows of the candidate matrix
- migrate worst-replacement actually discarding the best individuals
- migrate random-replacement always discarding the last individual, and the fully connected topology migrating from a population into itself
- mutgau / mutuni with Middle=True centring on (ub-lb)/2 instead of the midpoint of the interval
- crtip generating out-of-bounds integers when the bounds are fractional
- pbi returning NaN for individuals exactly on the ideal point
- cdist reading the wrong data from non-contiguous arrays
- refselect computing meaningless intercepts due to rounding when the extreme-point matrix is singular (e.g. repeated extreme points)
- crtri, like crtip, generating out-of-bounds integers when the bounds are fractional
- mutde treating None in the F list as another fixed factor (the documentation says None means random), and not excluding the individual itself when Mask_N is an N-row matrix
- refgselect not checking the number of CV rows and modifying the CV passed in
- refselect / refgselect still selecting when the reference-point set is empty
- mselecting only topping up populations with nothing selected, so the documented per-population minimum is not guaranteed
- Templates with a global archive (NSGA2_archive, awGA, MOEAD_archive) returning duplicate solutions
- xovsp / xovsh and others segfaulting on length-1 chromosomes or one-dimensional input
- mutde crashing when drawing donors from an empty population; refgselect with too short a Gamma and GD / IGD / HV on empty input reading out-of-bounds memory
- rws / sus returning uninitialised indices on an empty population, and tour / dup crashing
- migrate reading out of bounds when FitnVs has too few entries or rows while fitness is used
- Nonlinear ranking overflowing to NaN with many individuals or a large SP
- recint / reclin reading memory as float64 when Alpha is an integer, float32 or bool array
- Population slices pop[:] / pop[:-1] raising errors or miscounting individuals; PsyPopulation accumulating Linds on re-initialisation, and derived populations sharing the decoding matrix
Installation
Installation
Supports PyPI one-command install, prebuilt wheels from GitHub Releases, and building from source:
pip install geatpy-rs# Linux (x86_64)
pip install https://github.com/yangyachao/geatpy-rs/releases/download/v2.7.6/geatpy_rs-2.7.6-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
# Linux (ARM64 / aarch64)
pip install https://github.com/yangyachao/geatpy-rs/releases/download/v2.7.6/geatpy_rs-2.7.6-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
# macOS (Universal2 - Apple Silicon M1-M4 & Intel)
pip install https://github.com/yangyachao/geatpy-rs/releases/download/v2.7.6/geatpy_rs-2.7.6-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl
# Windows (x64)
pip install https://github.com/yangyachao/geatpy-rs/releases/download/v2.7.6/geatpy_rs-2.7.6-cp38-abi3-win_amd64.whl
# free-threaded (no-GIL) Python 3.14t: replace cp38-abi3 with cp314-cp314t, e.g.
pip install https://github.com/yangyachao/geatpy-rs/releases/download/v2.7.6/geatpy_rs-2.7.6-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whlgit clone https://github.com/yangyachao/geatpy-rs.git
cd geatpy-rs
# Cross-platform build script (requires Rust toolchain)
python scripts/build.py --install
# Or install with maturin into current environment
pip install maturin
maturin develop --releasePublished as geatpy-rs. After installation, existing Python code continues using import geatpy as ea with zero changes.
Quick Start
Code Example
Solving the classic DTLZ1 3-objective optimization problem with NSGA-III without changing any algorithm logic:
import numpy as np
import geatpy as ea # Identical API, zero code modifications needed
class DTLZ1(ea.Problem):
def __init__(self, M=3):
name = 'DTLZ1'
maxormins = [1] * M
Dim = M + 4
varTypes = [0] * Dim
lb = [0] * Dim
ub = [1] * Dim
lbin = [1] * Dim
ubin = [1] * Dim
ea.Problem.__init__(self, name, M, maxormins, Dim, varTypes, lb, ub, lbin, ubin)
def aimFunc(self, pop):
Vars = pop.Phen
XM = Vars[:, (self.M - 1):]
g = np.array([100 * (self.Dim - self.M + 1 + np.sum(((XM - 0.5)**2 - np.cos(20 * np.pi * (XM - 0.5))), 1))]).T
ones_metrix = np.ones((Vars.shape[0], 1))
pop.ObjV = 0.5 * np.fliplr(np.cumprod(np.hstack([ones_metrix, Vars[:, :self.M - 1]]), 1)) * \
np.hstack([ones_metrix, 1 - Vars[:, range(self.M - 2, -1, -1)]]) * np.tile(1 + g, (1, self.M))
if __name__ == '__main__':
problem = DTLZ1(M=3)
algorithm = ea.moea_NSGA3_templet(
problem,
ea.Population(Encoding='RI', NIND=100),
MAXGEN=500,
logTras=1
)
res = ea.optimize(algorithm, verbose=True, drawing=1, outputMsg=True, drawLog=True)Architecture
All 74 Kernel Operators Rebuilt in Rust
The Rust core lives in crates/geatpy_core/. Below are all the operators it exports, with the same names as the original:
Encoding & Initialization
encoding.rsPopulation generation, binary / Gray code / real conversions, boundary repair
- crtfld
- crtbp
- crtip
- crtrp
- crtri
- crtpp
- crtpc
- crtup
- crtgp
- bs2ri
- bs2real
- bs2int
- ri2bs
- boundfix
Selection & Fitness
selection.rsTournament, roulette wheel, stochastic universal sampling, fitness ranking and scaling
- tour
- etour
- rws
- sus
- urs
- rcs
- rps
- ecs
- otos
- dup
- selecting
- mselecting
- ranking
- scaling
- powing
- indexing
Crossover & Recombination
recombination.rsSingle-point, two-point, uniform, shuffle crossover; PMX/OX permutation crossover; SBX
- xovsp
- xovdp
- xovud
- xovsh
- xovbd
- xovexp
- xovsec
- xovpmx
- xovox
- recsbx
- recdis
- recint
- reclin
- recndx
- recombin
Mutation
mutation.rsPolynomial, Gaussian, uniform, DE mutations; swap, move, and inversion for permutations
- mutpolyn
- mutgau
- mutbga
- mutuni
- mutbin
- mutde
- mutinv
- mutmove
- mutswap
- mutpp
- mutate
Multi-Objective & Constraints
multiobjective.rsNon-dominated sorting, crowding distance, decomposition, reference point selection, constraint merging
- ndsortESS
- ndsortTNS
- ndsortDED
- crowdis
- cdist
- mergecv
- tcheby
- pbi
- crtidp
- awGA
- rwGA
- refselect
- refgselect
- migrate
Performance Metrics
indicator.rsGenerational distance, inverted generational distance, spacing, and hypervolume
- GD
- IGD
- Spacing
- HV
FAQ
Frequently Asked Questions
Python 3.11+ installation errors, ARM platform support, numerical consistency, and migration:
Q1What is geatpy-rs, and how does it differ from the original Geatpy?
geatpy-rs is a Rust rewrite of the core of Geatpy, the classic Python toolbox for evolutionary computation. The original Geatpy ships its C/C++ core only as a binary, its prebuilt wheels stop at Python 3.10, and there is no Linux ARM64 build. geatpy-rs rewrites all 74 core operators in Rust (PyO3 + abi3), provides prebuilt wheels for Python 3.8 – 3.15, and keeps the original Python interface and algorithm templates, so existing code runs without changes.
Q2Is there a Rust version of Geatpy? Can I call it from a Rust program?
geatpy-rs is the Rust version of Geatpy: all 74 operators of the original core are rewritten in Rust (source in crates/geatpy_core, built on PyO3 and rust-numpy) and published as a Python extension for Python users; after pip install geatpy-rs you import geatpy as ea as usual. The Rust core is not published on crates.io, and its operators take and return Python / numpy objects, so a Rust program cannot depend on it as an ordinary Rust library; to implement the same algorithms in Rust, use the source in the repository as a reference.
Q3Why does pip install geatpy fail on Python 3.11 or later, and how do I fix it?
The original Geatpy only publishes prebuilt wheels for Python 3.5 – 3.10 on PyPI. On Python 3.11, 3.12, 3.13, 3.14 or 3.15, pip finds no matching wheel and tries to build from source, which fails because the original core is not released as source. Install geatpy-rs instead (pip install geatpy-rs): it ships prebuilt abi3 wheels, so no compiler is needed.
Q4Does geatpy-rs support Apple Silicon (M1 – M4) and Linux ARM64?
Yes. macOS gets Universal2 wheels containing both Apple Silicon and Intel builds, so no Rosetta 2 is needed; Linux gets x86_64 and ARM64 (aarch64) manylinux2014 wheels that work on ARM cloud servers such as AWS Graviton and in Docker containers; Windows gets x64 wheels.
Q5Do I need to change code written for Geatpy?
No. geatpy-rs keeps the Python interface of the original: ea.Problem, ea.Population, ea.optimize, the 42 algorithm templates (moea_NSGA3_templet, moea_NSGA2_templet, soea_SEGA_templet and others) and core operators such as ndsortESS, crowdis, mutbga and xovdp keep their names and signatures. After installing geatpy-rs you still import geatpy as ea. Where the original binary contradicts its own documentation, geatpy-rs follows the documentation (see the parity section above).
Q6Are the results of geatpy-rs consistent with the original?
This is verified systematically. Deterministic operators are compared value by value against the actual output of the official 2.7.0 binaries, random operators by their statistical distributions, and 39 algorithm templates by final solution quality; about 1500 randomised differential cases run on both sides, comparing values, dtypes, return structures and errors; and 36 classic benchmark cases run 30 times on each side show no significant difference in solution quality. The official binary has several defects where it contradicts its own documentation (such as wrong fronts in ndsortDED, cross-row accumulation in mergecv, multi-population migration discarding the best individuals, and segfaults on edge-case input); geatpy-rs follows the documentation and the tests note each case, which is also why multi-population templates find clearly better solutions.
Q7Which evolutionary algorithms and operators are included?
42 single- and multi-objective algorithm templates: multi-objective algorithms such as NSGA-III, NSGA-II, RVEA, MOEA/D and awGA, and single-objective algorithms such as SEGA, SGA, EGA, differential evolution (DE) and evolution strategies (ES). The Rust core contains 74 operators: population initialisation and encoding conversion; tournament, roulette-wheel and other selection; single-point, two-point, uniform, PMX, SBX and other crossover; polynomial, Gaussian, differential-evolution and other mutation; ndsortESS non-dominated sorting, crowdis crowding distance, refselect reference-point selection; and the GD, IGD, Spacing and HV indicators.
Q8Does geatpy-rs use multiple cores?
The Rust core of the stable 2.7.6 release is single-threaded; the Parallel argument of the original operators is accepted for compatibility but has no effect. Even so, the 36 classic cases run in 0.05 – 0.73 times the original runtime. On free-threaded (no-GIL) Python 3.14t the core does not need the GIL, so threads can run several templates at once (8 threads running 8 DE runs are 4.4 times faster). The multithreading preview 2.8.0b2 is installed with pip install --pre geatpy-rs: non-dominated sorting, reference-point selection, distances and the GD / IGD / Spacing indicators run on several threads for large inputs, bit-identical to one thread; set the thread count with ea.set_num_threads() or the GEATPY_NUM_THREADS environment variable. The gain is small for typical populations under a thousand, and when running several processes, use one thread per process. Objective functions still need to be vectorised or parallelised in your problem class.
Q9Why does soea_DE_currentToRand_1_templet converge more slowly than the original?
The template passes F=[0.5, None]. The documentation says None means F2 is generated randomly during mutation, but the original binary actually fixes F2 at 0.5; geatpy-rs follows the documentation and draws it randomly, so on problems like Rastrigin it converges more slowly than 2.7.0 (30 dimensions, 50 individuals × 1000 generations: median best value about 93 → 156). For the original behaviour, set algorithm.mutOper.F = [0.5, 0.5] before running.
Q10It reports “Could not find any feasible solution” although feasible solutions exist. What should I check?
Constraints go into the CV matrix returned by evalVars, one column per constraint, and a constraint is satisfied when CV <= 0. Common mistakes are a flipped sign (x1 + x2 >= 1 should be written as 1 - x1 - x2) and equality constraints without a tolerance (write |h(x)| - 1e-6); for an “or” of two constraints, take the smaller of the two. When solving fails, geatpy-rs prints how many individuals of the last generation violate each constraint and the smallest violation, to help locate the problem. See the FAQ in the README for more.
Q11Are large constrained multi-objective problems slow?
In the original, infeasible individuals are ranked into one long chain by their degree of violation, so non-dominated sorting degrades to quadratic time and sorting 50,000 individuals once takes tens of minutes (upstream issue #319). From 2.7.5 on, geatpy-rs compares each individual only with the nearest front members: 10,000 individuals with 3 objectives and 1 constraint drop from 6.4 s in the original to 0.05 s, with fronts bit-identical to the original. Pass progress=True to show a progress bar on long runs.
Q12Does geatpy-rs support free-threaded (no-GIL) Python 3.14t?
Yes. From 2.7.6 on there are cp314t wheels for Python 3.14t (Linux x86_64 / ARM64, macOS Universal2, Windows x64); run pip install geatpy-rs on 3.14t as usual, and the GIL stays disabled after import. Several threads can run their own algorithm templates, or compute the objective function in chunks with a thread pool inside evalVars: 8 threads running 8 DE runs are 4.4 times faster, and 3.8 times faster when the objective is written in pure Python; chunking a pure-Python objective within a single run (400 individuals × 1000 variables) is 1.9 times faster overall. Threads share numpy’s global random state, so seeded runs are not reproducible when several run at the same time; calling numpy functions such as np.cos on single numbers makes threads contend and slows them down, so use the math module or call numpy on whole arrays.
Q13Does it still work with a very large number of decision variables (around 100,000)?
Yes; speed and memory are not the bottleneck. With 100,000 variables and 50 individuals, DE, SEGA and EGA take 23 – 30 ms per generation, 0.33 – 0.39 times the official 2.7.0; a binary-encoded 0-1 knapsack takes 25 ms per generation (original 95 ms) and 125 ms at 500,000 variables (original 442 ms), with 10 % – 40 % less peak memory than the original. Evolutionary algorithms themselves hardly converge at this scale (upstream issues #281 and #295), so exploit the structure of the problem (variable grouping, dimension reduction, problem-specific operators) or use a classical optimisation method. The benchmark script is scripts/parity/large_scale.py in the repository.
Q14Are runs reproducible?
Yes. As in the original, the core draws random numbers from numpy’s global random state: call np.random.seed, or pass seed to ea.optimize, and the same code gives exactly the same result.
Open Source & Citation
License & Academic Citation
This project is open source under LGPL-3.0. If you use it in research or academic work, please cite the original project:
@misc{geatpy2020,
author = {Jazzbin, J.},
title = {Geatpy: The Genetic and Evolutionary Algorithm Toolbox with High Performance in Python},
year = {2020},
publisher = {GitHub},
journal = {GitHub repository},
howpublished = {\url{https://github.com/geatpy-dev/geatpy}}
}