Commit Graph
59 Commits
Author SHA1 Message Date
swaits 16032bf28b feat(algorithms): add MOEA/D with Tchebycheff decomposition
Implementation of Zhang & Li 2007 MOEA/D — the canonical
decomposition-based MOEA. Different paradigm from Pareto-dominance
algorithms: each subproblem is a scalarized single-objective problem
defined by a Das–Dennis weight vector, and subproblems with similar
weight vectors form neighborhoods that share genetic material.

Each generation iterates over every weight vector `i`:
1. Pick two parents uniformly from the T-nearest neighbors of weight i
   (T = neighborhood_size).
2. Apply variation, evaluate the child.
3. Update the ideal point z* with the child's objectives.
4. Walk the entire neighborhood: for each j, if the child's
   Tchebycheff value g(child | w_j, z*) <= g(current[j] | w_j, z*),
   replace current[j] with the child.

Tchebycheff scalarization:

  g(f | w, z*) = max_k w_k · |f_k - z*_k|

(With the standard `w_k = 1e-6` floor when a weight is zero, so the
max well-defined.)

Public API:

  MoeadConfig {
      generations,
      reference_divisions,  // Das-Dennis H, also fixes population size
      neighborhood_size,    // T
      seed,
  }
  Moead { config, initializer, variation }
  impl<P, I, V> Optimizer<P> for Moead<I, V>

Population size equals the number of weight vectors generated by
das_dennis(num_objectives, reference_divisions). Re-exported from the
prelude. Tests cover non-empty Pareto front, deterministic reruns,
and panic on `reference_divisions` that would yield zero weights.
2026-05-04 20:02:54 -06:00
swaits ac1a5856cf chore: fix clippy warnings in NSGA-III, SPEA2, and compare example
- nsga3: drop redundant `.into_iter()` in extend call; use
  `#[allow(clippy::needless_range_loop)]` on the back-substitution
  loop where `j` indexes into the matrix; remove an unneeded
  `return` keyword in a closure.
- spea2: switch `pool.extend(x.drain(..))` to `pool.append(&mut x)`.
- examples/compare.rs DTLZ2 evaluator: same `needless_range_loop`
  silencer on the inner cosine product loop.
2026-05-04 19:59:18 -06:00
swaits 4b7c5825e8 feat(algorithms): add NSGA-III
Implementation of Deb & Jain 2014 NSGA-III — the canonical
many-objective MOEA. Replaces NSGA-II's crowding-distance niching
with a structured reference-point niching procedure that scales to
3+ objectives where crowding distance loses its diversity signal.

Each generation:
1. Random parent selection + variation + offspring evaluation, same as
   NSGA-II.
2. Combine + non_dominated_sort, fill the next population front-by-
   front until the next front would overflow (the splitting front F_l).
3. Survival on F_l uses reference-point niching:
   - Translate by the ideal point z* (per-axis min in oriented space).
   - Compute extreme points by ASF and intercepts; normalize by
     intercepts (with a robust fallback to per-axis range if extreme
     points are degenerate).
   - Associate every member of the working pool with the closest
     reference direction by perpendicular distance.
   - Iteratively pick from F_l: prefer the niche with the smallest
     count among references that have F_l candidates; if the niche is
     empty in the already-selected set, take the closest associated
     member by perpendicular distance, otherwise pick uniformly from
     the niche.

Public API:

  Nsga3Config { population_size, generations, reference_divisions, seed }
  Nsga3 { config, initializer, variation }
  impl<P, I, V> Optimizer<P> for Nsga3<I, V>

Re-exported from the prelude. Tests cover non-empty Pareto front,
exact final population size, deterministic reruns, and panic on
`population_size == 0`. Uses the existing tests_support problems.
2026-05-04 19:56:03 -06:00
swaits 9d46cf9d65 feat(algorithms): add SPEA2 (Strength Pareto Evolutionary Algorithm 2)
Implementation of Zitzler, Laumanns, Thiele 2001 SPEA2 — the classic
Pareto MOEA built around an explicit external archive of fixed size.

Each generation:
1. Combine the current population and the archive into one pool.
2. For every member, compute strength S(i) = number of others that
   member dominates, then raw fitness R(i) = sum of S(j) over members
   j that dominate i.
3. Add a density estimator D(i) = 1/(σ_k + 2) where σ_k is the distance
   to the k-th nearest neighbor (k = floor(sqrt(|pool|))) in
   minimization-oriented objective space.
4. Final fitness F(i) = R(i) + D(i); lower is better.
5. Build the next archive by taking every non-dominated member
   (R(i) == 0). If too many, prune by repeatedly removing the member
   with the smallest k-th-nearest-neighbor distance. If too few, fill
   from the rest sorted by F ascending.
6. Generate the next population by binary tournament on F (lower wins),
   then variation, then evaluation.

Public API mirrors the other algorithms:

  Spea2Config { population_size, archive_size, generations, seed }
  Spea2 { config, initializer, variation }
  impl<P, I, V> Optimizer<P> for Spea2<I, V>

Re-exported from the prelude. Tests cover archive size invariants,
non-empty Pareto front on Schaffer N.1, deterministic reruns under
the same seed, and panic on population_size == 0.
2026-05-04 19:52:47 -06:00
swaits 9aaa4402a8 feat: add optional parallel feature for population-evaluation parallelism
Adds a `parallel` Cargo feature that pulls in rayon and parallelizes
the only step that's actually expensive in practice — calls to
`Problem::evaluate` — across the population. RNG-driven steps (parent
and donor selection, variation, replacement decisions) stay serial, so
seeded runs remain deterministic regardless of feature state, and the
default and `--features parallel` builds produce bit-identical
results.

Wiring:
- New `algorithms::parallel_eval::evaluate_batch` helper with two
  cfg-gated implementations (rayon's `into_par_iter` when the feature
  is on, plain `into_iter` otherwise). Both preserve input order, so
  pareto_front and crowding-distance decisions remain reproducible.
- `RandomSearch`, `Nsga2`, and `DifferentialEvolution` now route
  population/offspring evaluation through the helper. NSGA-II's main
  loop is restructured into a serial selection-and-variation phase
  followed by a parallel-friendly batch evaluation phase.
- DE's per-target loop is restructured into three phases (serial trial
  construction → batch evaluation → serial replacement). Side effect
  of the restructuring: DE is now the canonical synchronous DE/rand/1/bin
  rather than the asynchronous variant where target `i+1` sees `i`'s
  in-flight update. Synchronous is the textbook formulation, so this
  is a small correctness improvement on top of the parallelism enable.
- PAES stays serial — its main loop has a sequential dependency on the
  current candidate and would gain nothing from rayon.

Cost: algorithm impls now require `P: Sync` and `P::Decision: Send`
unconditionally so a single impl serves both feature modes. This is a
small bound tightening that any plain-data Problem already satisfies; in
return the public `Problem` trait itself stays unchanged and the
default build picks up no new dependencies.

Verified:
- `cargo test` and `cargo test --features parallel` both pass; the
  Nsga2 `deterministic_with_same_seed` test confirms reproducibility.
- `cargo run --release --example benchmarks` and the same with
  `--features parallel` produce bit-identical ZDT1 / Rastrigin
  results.
2026-05-04 19:39:43 -06:00
swaits a1bb49d74e feat(algorithms): add DifferentialEvolution (DE/rand/1/bin)
Optional v1 algorithm requested by the user (spec §12.4):

- Vec<f64> decisions only.
- Single-objective only — panics with a clear message otherwise.
- Standard DE/rand/1/bin: for each target i, sample distinct r1, r2, r3;
  mutant = x[r1] + F * (x[r2] - x[r3]); apply binomial crossover with at
  least one forced index; greedy replacement on direction-correct
  comparison.
- Bounds taken from the embedded RealBounds (mutants are clamped to the
  per-variable range so the trial vector stays feasible).
- Seed-deterministic; tests verify reproducibility, that DE improves on
  the initial random population for a sphere problem, and that
  multi-objective use panics.
2026-05-04 19:25:29 -06:00
swaits 33a927d86d feat(algorithms): add NSGA-II
Standard (μ+λ) NSGA-II with binary tournament parent selection on
(rank, crowding distance) and elitist survival selection on the combined
parent + offspring population (spec §12.3):

1. Initialize population_size random decisions.
2. Each generation: select parents by binary tournament (rank ↑ then
   crowding ↓ then random), apply variation, evaluate offspring,
   combine, non_dominated_sort, fill the next population front-by-front
   trimming the partial last front by crowding distance descending.
3. Return final population, Pareto front, best (None for >1 objective),
   evaluation count, and generation count.

Internal Nsga2Entry { candidate, rank, crowding_distance } stays
private. Panics with clear messages on `population_size == 0` or
empty `vary` output. Tests cover population length, evaluation count,
non-empty front, and full determinism with the same seed (spec §18.4).
2026-05-04 19:24:41 -06:00
swaits bb3a01f90e feat(algorithms): add Paes (Pareto Archived Evolution Strategy)
A readable v1 PAES (spec §12.2):

- Single starting decision from the initializer.
- Each iteration mutates the current decision via the Variation operator,
  evaluates the child, and pareto_compares to the current.
- Dominating children become current; for non-dominated comparisons we
  move to the child (acceptable v1 behavior per spec).
- Both current and child are inserted into a ParetoArchive truncated
  to `archive_size` (simple tail-truncation in v1).

The final result returns the archive as both `population` and
`pareto_front`. Tests verify the archive never exceeds
`archive_size`.
2026-05-04 19:23:53 -06:00
swaits f17c960ec7 feat(algorithms): add RandomSearch baseline optimizer
The reference baseline and the spec's recommended starting example. Per
iteration it asks the initializer for `batch_size` decisions, evaluates
each, and accumulates them. At the end it returns the full population
plus the Pareto front and (if single-objective) the best feasible
candidate. `generations` equals `iterations`; `evaluations` equals
`iterations * batch_size` (spec §12.1).

Includes a tiny single-objective sphere test problem under
`tests_support` that later algorithm tests will reuse.
2026-05-04 19:23:20 -06:00