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
geatpy-rs overview: installation, supported platforms and parity with official Geatpy 2.7.0

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:

  1. 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.

  2. 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.

  3. 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:

DimensionOfficial Release (Geatpy)geatpy-rs
Core ImplementationLegacy C/C++ shared library, distributed only as a precompiled binaryRust implementation (crates/geatpy_core), source code public
Python SupportPython 3.5 – 3.10 onlyPython 3.8 – 3.15 full coverage, plus free-threaded (no-GIL) Python 3.14t wheels
ABI CompatibilityBuilt separately for each minor Python releasePyO3 abi3: single wheel runs across 3.8+; separate build for free-threaded 3.14t
Prebuilt WheelsWindows x86/x64, Linux x86_64, macOS (Apple Silicon Python 3.8–3.10 only), no Linux ARM64macOS Universal2 (Apple Silicon + Intel), Linux x86_64 / ARM64, Windows x64
Memory SafetyEdge-case input can segfault and kill the Python processSafe 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 & ConcurrencyC OpenMP2.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
ReproducibilityInherits numpy global random state, reproducible via np.random.seedIdentical to original: np.random.seed / ea.optimize(seed=…) fully reproduces any run
API CompatibilityBaselineSame 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:

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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

Published 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:

example_nsga3_dtlz1.py
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.rs

Population 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.rs

Tournament, 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.rs

Single-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.rs

Polynomial, 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.rs

Non-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.rs

Generational 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:

BibTeX
@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}}
}