Skip to content

Fix NSGA-II survivor truncation: crowding order and index mapping#141

Merged
ChrisRackauckas merged 1 commit into
SciML:masterfrom
JohnCobbler:nsga2-crowding-order
Jun 5, 2026
Merged

Fix NSGA-II survivor truncation: crowding order and index mapping#141
ChrisRackauckas merged 1 commit into
SciML:masterfrom
JohnCobbler:nsga2-crowding-order

Conversation

@JohnCobbler

Copy link
Copy Markdown
Contributor

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, so idxs[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 with rev = true.

Bug 2 — front-local positions used as population indices

The same block appended idxs[1:k] — positions within the front (1:length(f)) — into fitidx, which otherwise holds global population indices (append!(fitidx, f) for fronts that fit entirely). The survivors were then materialized as state.population[fitidx], so whenever the truncated front's indices differed from 1: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) exceeds populationSize = 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).

…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 ChrisRackauckas merged commit a2fbf0c into SciML:master Jun 5, 2026
8 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

NSGA-II: crowding distance sort selects least diverse individuals (ascending instead of descending)

2 participants