Fix NSGA-II survivor truncation: crowding order and index mapping#141
Merged
Merged
Conversation
…ing) Two bugs in the survivor-truncation block of update_state! (src/nsga2.jl): 1. The crowding-distance sortperm was ascending, so the truncated front kept the individuals with the SMALLEST crowding distance. Per Deb et al. 2002, among equal-rank individuals those with LARGER crowding distance must be preferred; boundary members (infinite distance) must always survive. Fixed with rev = true. 2. The sorted positions (1:length(front)) were appended to fitidx directly, mixing front-local positions with the global population indices appended for non-truncated fronts. The selected survivors were then read as state.population[fitidx], picking unrelated individuals. Fixed by mapping back through the front: f[idxs[1:k]]. The regression test runs one deterministic update_state! step (identity operators, fixed selection, parents crafted so the first front exceeds populationSize with front indices differing from front positions) and compares survivors against an independently recomputed crowded-comparison oracle. Verified to fail under each bug separately and under both. Fixes SciML#132
ChrisRackauckas
approved these changes
Jun 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #132 — and a second, adjacent bug found while writing the regression test.
Bug 1 — crowding distance sort direction (#132)
In
update_state!, the truncated front was sorted by crowding distance ascending, soidxs[1:n]kept the individuals with the smallest crowding distance — the least diverse ones. Deb et al. 2002's crowded-comparison operator requires the opposite: among equal-rank individuals, larger crowding distance wins, and boundary members (infinite distance) must always survive. Fixed withrev = true.Bug 2 — front-local positions used as population indices
The same block appended
idxs[1:k]— positions within the front (1:length(f)) — intofitidx, which otherwise holds global population indices (append!(fitidx, f)for fronts that fit entirely). The survivors were then materialized asstate.population[fitidx], so whenever the truncated front's indices differed from1:length(f)(i.e. whenever any earlier front existed or the sort reordered), unrelated individuals were selected. Fixed by mapping back through the front:f[idxs[1:k]].Note the fixes are only meaningful together — correcting the sort direction alone just changes which wrong individuals get picked.
Regression test
E2E spread assertions turned out too flaky to discriminate (NSGA-II runs on Schaffer F2 vary widely by seed), so the test instead runs one deterministic
update_state!step — identity operators (crossoverRate = mutationRate = 0), a fixed selection, and parents crafted so that the first front (6 members after offspring duplication) exceedspopulationSize = 5, forcing the truncation branch with front indices that differ from front positions. Survivors are compared as fitness multisets against an independently recomputed crowded-comparison oracle.The test was verified to fail under each bug separately and under both, and to pass with the fixes (also: full existing test suite passes).