Repository navigation
Crossover operators - #86
light-weaver wants to merge 30 commits into
Conversation
|
I think it would be interesting to know how these compare with the operators provided by pymoo and DEAP in terms of speed and in terms of return value. |
|
Yes, please send a pull request to the bib repo. References should have a DOI so the CI can validate it. |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #86 +/- ##
==========================================
+ Coverage 77.86% 80.02% +2.15%
==========================================
Files 70 73 +3
Lines 7590 8200 +610
Branches 1477 1549 +72
==========================================
+ Hits 5910 6562 +652
+ Misses 1312 1262 -50
- Partials 368 376 +8
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
I guess all these are being used and tested in DESDEO. Are they used in any other package like pymoo, jMetal, DEAP, etc. ?
That sounds fine as long as the differences are not arbitrary (e.g., similar arguments should have similar names, documentation and positions). But the real test for this concept is to actually compare these implementations with the ones in other packages. Are they faster/slower? Do the results differ significantly?
That's perfectly fine. I'd rather add operators incrementally. It is a lot to review at once.
Don't worry about R, we can ask copilot to do that once the Python part is completed. The shared library built for C, Python and R is not the same one (and it is compiled with different flags). Do not worry about
Please do so. |
It should be fairly easy to convert this to a benchmark that tests each crossover operator over increasing number of parents and increasing number of variables (two separate plots per operator). See https://github.com/multi-objective/moocore/tree/main/python/benchmarks |
Added a new benchmark page for this. Originally benchmarks were on the homepage which blew up the length of that page.
will do.
Added comparisons.
The only major difference is that some crossover calls the array of decision variables "parents", some call it "population". That's because they treat these arrays very differently. I feel like anyone using moocore should know the difference and then act accordingly, but we can unify that terminology. |
|
A nice to have feature that I didn't end up implementing yet: Many of these crossover operators do not respect decision variable bounds. There are many strategies to deal with this: clipping at the bounds, resampling using the operator, uniformly resampling within the bounds, or just not caring about the bounds at all. Different packages use different strategies, DESDEO and platemo use clipping, so I ended up using that here too. However, we could make choosing the strategy an input argument too. I just didn't want to complicate things too much in this initial PR. |
aac4cd8 to
200ff8a
Compare
|
The documentation is failing to build. I'd prefer to keep the current benchmarks on the main page. You can add a link from it to the crossover benchmarks like #82 |
|
@light-weaver I think I fixed the documentation problem. Before you work more on this, let me have a chat with Giovanni. I don't see anything moo-specific here, so we are opening the door to adding stuff to Perhaps this would fit better in a new package called |
It doesn't work. Something is different from my local environment. |
* python/pyproject.toml (docs): require sphinx>=9 for on-disk intersphinx inventory cache. * .github/workflows/python.yml (build-doc): Cache only python/doc/_build/doctrees/__intersphinx_cache__/ Co-authored-by: MLopez-Ibanez <2620021+MLopez-Ibanez@users.noreply.github.com>
* c/crossover.c, c/crossover.h: New. * c/rng.h (rng_integer): New. * c/Makefile: Add crossover.c and crossover.h. * c/NEWS.md: Document.
* python/src/moocore/_crossover.py: New. * python/src/moocore/__init__.py: Export the crossover functions. * python/src/moocore/_docsubstitute.py (COMMON_PARAMS): Add parents, pair_offspring, population, targets, lower_bound and upper_bound. * python/src/moocore/_ffi_build.py: Compile crossover.c. * python/src/moocore/libmoocore.h: Declare the crossover functions. * python/tests/test_crossover.py: New.
* bibkeys.txt (BezLopStu2015emode, EibSmi2003, EshSch1992, Goldberg89, Hol75, KukLam2005:gde3, LiZha2009:moead-de, RobFil05demo, StoPri1997:de, Syswerda89): New. * python/doc/source/reference/functions.variation.rst: New. * python/doc/source/reference/index.rst: Add it. * python/doc/source/index.rst: Mention crossover operators. * python/doc/source/whatsnew/index.rst: Document. * python/examples/plot_crossover.py: New. * python/doc/source/REFERENCES.bib: Regenerate. * r/inst/REFERENCES.bib: Regenerate.
…dren * python/doc/source/reference/functions.variation.rst: List all crossover functions together and add a table of children and parents per operation. * python/examples/plot_crossover.py: Do not use the group names.
* c/crossover.c (bex_child): Rename to bex_offspring. (get_pair): Rename c1 and c2 to off1 and off2. Update comments. * c/crossover.h: Update comment.
* python/src/moocore/_crossover.py: Update docstrings. * python/src/moocore/_docsubstitute.py (COMMON_PARAMS): Likewise. * python/tests/test_crossover.py: Rename tests and variables.
* python/doc/source/reference/functions.variation.rst: Update. * python/examples/plot_crossover.py (mate): Rename to cross_over. Update comments.
* python/src/moocore/_crossover.py (de_current_1_crossover): Cite Tanabe and Ishibuchi (2019) and Gong et al. (2017) for the name current/1. * python/doc/source/reference/functions.variation.rst: Likewise.
* python/src/moocore/_crossover.py (de_rand_1_bin_crossover): Say that Storn and Price recommend F = 0.5 as a first choice. (de_current_1_crossover): Reword the crossover rate. * python/tests/test_crossover.py (test_de_current_1_crossover_two_rows): Pass scaling_factor explicitly.
* python/doc/source/reference/functions.variation.rst: Introduce mutation and crossover. Explain the order of the offspring. * python/examples/plot_crossover.py: Add figures of every crossover operator applied to a round and to a diagonal population. Remove the inheritance profile and the separate DE and PCX figure. Reword.
…eed the operators in an evolutionary algorithm.
* python/doc/source/reference/functions.variation.rst: Say that variation operators are used in evolutionary algorithms. * python/examples/plot_crossover.py: Note that local crossover stays in the box spanned by the parents. Explain why uniform and single-point crossover look alike in two dimensions. Reword the captions.
* c/crossover.c (copy_parents): New. (single_point_crossover, blend_alpha_crossover, local_crossover): Add pair_prob. (bounded_exponential_crossover): Take swap_prob before pair_prob. Use copy_parents. * c/crossover.h: Likewise.
* python/src/moocore/_crossover.py (single_point_crossover) (blend_alpha_crossover, local_crossover): Add pair_prob. (bounded_exponential_crossover): Take swap_prob before pair_prob. (de_current_1_crossover): Call the step vector the mutant. (parent_centric_crossover): Say how the orthogonal step differs from equation (2) of Deb et al. (2002). * python/src/moocore/_docsubstitute.py (COMMON_PARAMS): Add swap_prob, pair_prob, scaling_factor and crossover_rate. * python/src/moocore/libmoocore.h: Update the crossover prototypes. * python/tests/test_crossover.py: Test pair_prob for every operator with two parents.
* python/benchmarks/bench_crossover.py: New. Time each crossover operator against pymoo, DEAP, jMetalPy and DESDEO for an increasing number of parents and of variables. * python/benchmarks/compare_crossover.py: New. Compare the offspring of each operator with those of the other packages. * python/pyproject.toml (benchmarks): Add deap.
* python/doc/source/benchmarks.inc.rst: Add the crossover operators. * python/doc/source/_static/bench/crossover_bench-*-time.png: New.
* python/benchmarks/bench.py (Bench.plots): New argument add_name. * python/benchmarks/bench_crossover.py: Title each plot with the operator and the quantity that varies.
…n Thakur et al. (2014).
* python/src/moocore/_crossover.py (_check_within_bounds): New. Also reject NaN. (blend_alpha_crossover): Require the parents to be within the bounds. Say what Eshelman and Schaffer recommend and do differently. (single_arithmetic_crossover): Say that this is the case alpha = 1/2 of the operator of Eiben and Smith. (bounded_exponential_crossover): Describe the two sides of the distribution. Say which settings are not from the paper. (de_rand_1_bin_crossover): Quote Storn and Price on CR in full. (de_current_1_crossover): Say where the distinct donors come from. (parent_centric_crossover): Correct the comparison with equation (2) of Deb et al. (2002). * python/tests/test_crossover.py (test_errors_operator_specific): Test parents outside the bounds in blend_alpha_crossover and NaN parents.
Print and save to a CSV file one matrix of KS p-values per operator, and use 100 000 offspring per package.
* python/doc/source/benchmarks.inc.rst: Split into ... * python/doc/source/benchmarks/index.rst: ... this, * python/doc/source/benchmarks/dominance.rst: ... this, * python/doc/source/benchmarks/indicators.rst: ... this, * python/doc/source/benchmarks/crossover/*.rst: ... and one page per crossover operator, with a matrix of KS test results. * python/doc/source/benchmarks-links.inc.rst: New. * python/doc/source/_static/bench/crossover_ks-*.csv: New. * python/doc/source/index.rst: Link to the benchmarks instead of including them. * python/doc/source/my_unused_images.py (check_unused_images): Also resolve image paths relative to the source directory. * python/benchmarks/README.md: Update the link to the benchmarks.
Added type ignore comment for unresolved import.
for more information, see https://pre-commit.ci
0543b10 to
091913c
Compare
This PR adds nine crossover operators for evolutionary algorithms. They are implemented in C (
c/crossover.c) and exposed in Python. They are ported from DESDEO'sdesdeo.emo, rewritten as plain functions that take and return numpy arrays.I am not very familiar with C these days, so Claude was used for porting. I did check the C code for the actual operators themselves to verify their correctness. Moreover, I added some documentation pages inspired from DESDEO's to again verify operator behaviour.
New functions
uniform_crossoversingle_point_crossoverblend_alpha_crossoversingle_arithmetic_crossoverlocal_crossoverbounded_exponential_crossoverde_rand_1_bin_crossoverde_current_1_crossoverparent_centric_crossoverOperators with 2 parents per operation take a matrix
parentsand cross over rows2iand2i+1. With an odd number of rows, the last row is crossed over with the first one, and the extra offspring is dropped, so the output has as many rows as the input. Operators with 1 parent per operation takepopulationand optional row indicestargets, and return one offspring per target. Except forblend_alpha_crossover, offspringkcomes from parentk(ortargets[k]), which matters for parent-centric operators.Bounds are optional (
lower,upper;Nonemeans unbounded).seedfollows the existing convention (_get_seed_for_c), and the C code draws fromrng.h. The docs recommend passing onenumpy.random.Generatorto every call inside an evolutionary algorithm.Points for review
de_rand_1_bin_crossoveris DE/rand/1/bin (Storn & Price 1997), the variant used by GDE3 and DEMO.de_current_1_crossoveris the operator of MOEA/D-DE (Li & Zhang 2009). The name current/1 comes from Tanabe & Ishibuchi (2019), who credit Gong et al. (2017). Both variants are implemented in many MOEA frameworks.libmoocore.soand not wrapped in R.c/Makefilelistscrossover.c, butmake alldoes not compile. I am unfamiliar with this part of the library so it would be nice if you can help with this part.c/rng.hgets a smallrng_integer()helper.bibkeys.txt, and bothREFERENCES.bibfiles are regenerated withupdate_bib.sh. I could send another pull request to the bib repo.Documentation
A new reference page, "Variation operators", and a gallery example,
plot_crossover.py. The example shows where each operator places the offspring of two parents, sweeps the parameters of BEX and BLX-α, and applies every operator to three population distributions: a round cloud, a diagonal band and three disjoint clusters. I also compared the implementation against DESDEO and pymoo implementations in terms of computation time. This implementation is unsurprisingly faster, though a majority of the cost is not due to the operators themselves, but because of the cost of converting between datastructures (e.g., between polars and numpy in case of DESDEO). I chose not to include these results in the docs as we will probably move to moocore anyway, even though we probably won't see much performance benefits.Testing
python/tests/test_crossover.pyhas 148 test cases: output shapes, odd numbers of parents, seeding, input immutability, operator-specific properties and error messages.The tests pass on Python 3.10 (release build) and 3.12 (debug build with
-Werror), and under ASan and UBSan.crossover.ccompiles cleanly with gcc and clang using the CI warning flags.I compared the outputs with DESDEO's implementations using two-sample KS tests on 20 000 offspring per operator, and the distributions agree. The exact values differ because DESDEO draws from numpy's PCG64 while moocore uses MT19937.
Speed of 1000 calls of
parent_centric_crossover:The issue with SBX
SBX is by far the most popular crossover operator. It is also by the the operator with most distinct variants floating out in the wild. @gialmisi can elaborate more on this matter. In DESDEO, we are currently trying to provide all reasonable (and many unreasonable) variants of SBX. However, if
moocoreis supposed to provide a "canonical" implementation, I think it should only have one implementation called "SBX". I don't think I should unilaterally decide which SBX is canonical.