Commit Graph
153 Commits
Author SHA1 Message Date
swaitsandClaude Opus 4.7 3f104a4395 perf(operators): make ERX neighbor removal O(degree) per step (397K -> 140K instr)
Edge Recombination Crossover scrubbed `current` from every one of the n
adjacency lists on each step of the walk -- an O(n^2) pass. The parent-tour
adjacency relation is symmetric (b in adj[a] iff a in adj[b]), so `current`
only ever appears in the lists of its own neighbors. Taking adj[current]
out with mem::take and retaining only over those lists is O(degree).

edge_recombination_crossover_vary n=100: 397_214 -> 140_403 (-65%, 2.83x);
n=30: 62_708 -> 39_987 (-36%). Output bit-identical -- all 606 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 06:29:23 -06:00
swaitsandClaude Opus 4.7 eb4c8a6a9f perf(pareto): halve non_dominated_sort dominance comparisons (2.46M -> 1.27M instr)
The dominance relation is antisymmetric, so the outcome of compare(i, j)
fully determines compare(j, i). Iterating only j > i and applying the
result in both directions does identical work in half the pair scans.

non_dominated_sort_2d n=200: 2_461_178 -> 1_268_372 (-48%); n=50 -1.65x.
Ripples into dependents: nsga2 one-generation -20%, nsga3 / sms_emoa ~-9%.
Output is bit-identical (dominates[] still ascending, first_front order
unchanged) -- all 606 tests including run() snapshots pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 06:26:08 -06:00
swaitsandClaude Opus 4.7 b1d339a869 test(bench): cover permutation operators, combinatorial problems, and remaining algorithms
Expand benches/hot_paths.rs so the instruction-count harness exercises the
whole library. Adds four groups — permutation_ops_group (all 10 permutation
operators at n=30/100), variation_ops_group (BitFlip, Levy, BoundedGaussian,
ClampToBounds, ProjectToSimplex), combinatorial_group (TSP/JSS/knapsack
end-to-end plus AntColonyTsp), and multi_fidelity_group (Hyperband) — and
folds tabu_search_short and umda_short into single_objective_group. All 33
algorithms and 20 operators are now on the benchmarking surface (69 benches).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 06:21:59 -06:00
swaits 54f9db5dc9 docs(mutants): document the mutation-testing campaign in mutants.toml
Records the outcome of the 2026-05 mutation-testing campaign and the
gotchas for future runs:
- always pass --all-features so the async runners and explorer module
  are compiled (otherwise their mutants are unviable/missed noise);
- --test-tool nextest needs --no-config because the libtest-style
  --test-threads=1 arg isn't accepted there;
- where the new invariant tests live, and what the residual MISSED /
  TIMEOUT categories actually represent.
2026-05-14 03:47:22 -06:00
swaits 6aee6d6318 style: rustfmt the Phase 1 test additions
The per-file Phase 1 test commits were written without running rustfmt
as I went; this pass formats the new test code (long assert_eq! lines
wrapped, etc.). Formatting-only — no behavioural change.
2026-05-14 03:41:38 -06:00
swaits 2ca8d71b94 test(snapshot): pin exact run() output for every algorithm
The full-codebase mutants run showed ~600 of the 902 surviving mutants
are arithmetic / comparison flips inside algorithm run() bodies — the
per-helper Phase 1 tests don't reach the optimization loop itself, and
the existing deterministic-with-same-seed tests can't catch them (both
the clean and mutated runs use the same seed, so they still match).

This extends the async-parity sweep: each of the 33 parity tests now
also asserts the sync run's result against an exact captured snapshot
(best objectives for single-objective algorithms; sorted pareto-front
objective tuples for multi-objective ones). Any arithmetic flip
anywhere in run() perturbs at least one f64 and breaks the snapshot.

Fixtures are deliberately multi-dimensional — SnapSphere (3-D sum of
squares), SnapMo (3-variable / 2-objective), a 6-city TinyTsp, a 3-D
SnapSpherePartial for Hyperband. A 1-D problem leaves the per-axis /
covariance-matrix / simplex machinery degenerate, so arithmetic
mutations there wouldn't change the result; 3-D exercises the full
loop body.

Snapshots captured from the un-mutated implementation; an intentional
algorithm change requires regenerating them, by design. The assertions
live in the async-gated module because they reuse its per-algorithm
constructions — active during the mutation campaign
(--features async,serde) and under cargo test --features async.
2026-05-14 03:41:38 -06:00
swaits 7aa9e627f0 test(sms_emoa,pesa2,paes,random_search): pin remaining algorithm helpers
Phase 1, final algorithm batch:
- sms_emoa: pick_drop_index returns the singleton worst front, and
  finds the least-HV-contributor at a non-zero index.
- pesa2: build_grid empty/corner-point boxing; region_tournament
  prefers the less-crowded grid box (statistical majority).
- paes: deterministic non-empty front + archive cap.
- random_search: evaluation count = iterations*batch; best is no
  worse than any sampled candidate.
2026-05-13 22:58:17 -06:00
swaits c5b003a9c9 test(tlbo,umda,simulated_annealing,tabu_search,tpe,snes,rvea,spea2): pin comparison and geometry helpers
Phase 1 tests for eight more algorithms — the feasibility-first
comparison helpers (better / better_than / compare_so / worse_than),
plus algorithm-specific pure functions:
- tlbo: best_index min/max/tie.
- tpe: oriented_target sign-flip + penalty; split_good_bad partition
  and clamp-to-at-least-one-each.
- snes: nes_utilities sum-to-zero + descending + positive-best.
- rvea: unit_normalize (3-4-5 → 0.6/0.8), zero-vector passthrough;
  closest_reference smallest-angle; smallest_neighbor_angle = π/2 for
  orthogonal refs.
- spea2: euclidean distance basics; binary_tournament prefers lower
  fitness.
2026-05-13 22:58:17 -06:00
swaits 6819ce4091 test(nelder_mead,nsga2,nsga3,one_plus_one_es,particle_swarm): pin selection/geometry helpers
Phase 1 tests:
- nelder_mead: compare / better feasibility-first + direction.
- nsga2: binary_tournament prefers lower rank, then higher crowding
  distance at equal rank (statistical majority over 200 seeds).
- nsga3: solve_intercepts on axis-aligned extremes / singular / empty;
  associate picks the closest reference direction with correct
  perpendicular distance.
- one_plus_one_es: worse_than across feasibility + direction + equal.
- particle_swarm: best_index min/max/tie/single-element.
2026-05-13 22:58:17 -06:00
swaits c2319116b8 test(hyperband,moead,knea,ibea,ipop_cma_es,mopso): pin helper functions
Phase 1 tests:
- hyperband: compare / better feasibility-first + direction branches.
- moead: tchebycheff (max weighted deviation from ideal) and
  weight_distance (Euclidean) pins.
- knea: perpendicular_distance to the simplex hyperplane, zero-on-plane,
  and the too-few-extremes degenerate fallback.
- ibea: compute_fitness empty/dominating/symmetric-tradeoff cases and
  binary_tournament fitness preference.
- ipop_cma_es: better feasibility-first + direction + equal-not-better.
- mopso: population/front sizing and determinism cross-check.
2026-05-13 22:58:17 -06:00
swaits 952d93ac85 test(grea,hill_climber,hype): pin selection sizing and tournament logic
Phase 1 tests:
- grea: environmental_selection truncates the 2N pool to exactly N
  across three population sizes.
- hill_climber: full-run never-worsens and decreases-sphere pins.
- hype: binary_tournament picks the higher-fitness index (statistical
  majority + valid-index invariant).
2026-05-13 22:58:17 -06:00
swaits 8ff43d0240 test(epsilon_moea): pin box_coords, corner_distance, box_dominates
Phase 1 tests for the ε-MOEA box-archive helpers: per-axis floor in
box_coords, Euclidean corner_distance (incl. zero at exact corner),
and box_dominates across the strict/boundary cross-product.
2026-05-13 22:58:17 -06:00
swaits 03b450f050 test(genetic_algorithm): pin compare_for_fitness and survival_selection
Phase 1 tests for GA — feasibility-first fitness comparison across all
branches, and survival_selection's exact elite + best-offspring
composition (including the zero-elitism case).
2026-05-13 22:58:17 -06:00
swaits 4569244a68 test(pareto,metrics,selection): pin shared-utility comparisons and arithmetic
Phase 1, tier 3 of the mutation-testing campaign — the shared Pareto /
metric / selection utilities used by every multi-objective algorithm.
A scoped cargo-mutants run found 75 survivors across these files; the
tests below target them.

- metrics/hypervolume.rs: dominates() boundary cases, non_dominated_
  projection retained-set pins, hso_recursive 1-D/2-D base cases,
  hypervolume_nd_from_evaluations empty/non-dominating skips.
- selection/tournament.rs: challenger_wins across the full feasibility
  cross-product + equal-objective tie; better_by_objective and
  better_by_feasibility branch pins; stochastic_ranking_select pf=0
  feasibility ordering and count-wraps-modulo-population.
- pareto/crowding.rs: exact interior crowding distance on symmetric
  and asymmetric fronts (pins the (next-prev)/span arithmetic).
- pareto/sort.rs: three-non-dominated-then-one-dominated and a strict
  3-chain producing three singleton fronts.
- pareto/dominance.rs: trade-off → NonDominated, better-on-one-equal-
  on-other → Dominates, identical → Equal.
- pareto/archive.rs: truncate boundary, trade-off kept alongside,
  equal candidate rejected, smaller-violation infeasible eviction.
- pareto/front.rs: best_candidate keeps the first of tied minima.
- metrics/spacing.rs: exact spacing for a varying-NN-distance front.

src/core/problem.rs's lone survivor (decision_schema default body
'replace with vec![]') is an equivalent mutant — Vec::new() and vec![]
are identical — and is left in the residue.
2026-05-13 22:58:17 -06:00
swaits 7b8b7170f4 test(differential_evolution): pin pick_three_distinct and convergence
Phase 1 tests for DE — distinct-index helper and a convergence sanity test.
2026-05-13 22:58:17 -06:00
swaits 44b70c34d5 test(cma_es): pin compare_so / better_than_so and exercise full convergence
Phase 1 tests for src/algorithms/cma_es.rs.

- compare_so: feasibility-first ordering, minimize/maximize inversion,
  infeasibility-violation comparison.
- better_than_so: matches compare_so == Less; equal evaluations are
  not strictly better.
- A 30-generation Sphere1D run pins that CMA-ES actually decreases
  the objective (catches mutants that collapse the update rules).
2026-05-13 22:58:17 -06:00
swaits 8003a97dfa test(bayesian_opt): pin GP / EI / erf helpers
Phase 1 tests for src/algorithms/bayesian_opt.rs. Adds 15 tests
pinning the GP regression and EI acquisition machinery:

- rbf_kernel: signal-variance return at zero distance, exp(-0.5) at
  unit distance, monotone in length scale, decays to 0 for far points.
- normal_pdf: symmetric about zero, value at zero equals 1/sqrt(2π).
- normal_cdf: 0.5 at z=0, symmetric tail sums to 1.
- erf: odd function and erf(0) ≈ 0 within the rational approximation's
  ~1e-7 accuracy.
- expected_improvement: zero at sigma=0, monotone in sigma, positive
  when mu < f_best.
- oriented_target: sign flips under direction, infeasible adds 1e6
  penalty.
- better: feasibility-first then objective ordering under both
  directions.
2026-05-13 22:58:16 -06:00
swaits 3456db6cd8 test(ant_colony_tsp): pin better_than_so branches and build_tour invariants
Phase 1 for src/algorithms/ant_colony_tsp.rs. Targets the ~30 remaining
mutants after Phase 0 sweeps — mostly arithmetic flips in build_tour
(pheromone × heuristic weighting) and the feasibility-comparison logic
in better_than_so.

Added:
- Four branch tests for better_than_so covering the full feasibility
  cross-product: feasible-vs-infeasible (both orders), two-infeasible
  (smaller violation wins), and two-feasible under both directions.
  Plus an equal-objectives test pinning the strict-less-than semantics.
- build_tour-is-a-permutation invariant across 20 seeds × 6 start cities.
- A strong-heuristic test: with eta favoring the next-city by 1000x and
  beta=5, build_tour walks the preferred path. Pins the .powf(beta)
  arithmetic.
- Zero-alpha/zero-beta degenerate-case test: uniform random fallback
  still returns a permutation.
2026-05-13 22:58:16 -06:00
swaits e5e979f02a test(age_moea): pin L_p helpers and add full-run snapshots
Phase 1 for src/algorithms/age_moea.rs. Targets the ~50 algorithm-internal
mutants surviving after the Phase 0 sweeps:

Pure helper-fn pins (lp_norm / lp_distance / nearest_neighbor_distance /
estimate_p):
- L_p norm at p ∈ {1, 2} on canonical vectors (unit, all-ones,
  Pythagorean 3-4-5, signed-via-abs).
- L_p distance: zero-to-itself = 0, symmetry, L_1/L_2 sanity values.
- nearest_neighbor_distance: empty selected → ∞, self-only → ∞, picks
  the closest of mixed-distance candidates.
- estimate_p: empty-front fallback to p=2; axis-aligned extremes (CV=0
  for all p, returns first candidate 0.25); corner-vs-diagonal extremes
  (CV minimized at large p).

Full-run pins:
- A 10-generation Schaffer-N1 run with seed 7 verifies the pareto front
  is non-empty and finite/nonneg (catches  body collapse mutants).
- Population size after run matches config across three pop sizes
  (catches  size mutants).
- Evaluation count falls in [pop, pop*(gens+1)] (catches comparison
  flips in the offspring-collection loop).
2026-05-13 22:58:16 -06:00
swaits f63b85900c test(real): pin exact outputs and algebraic invariants for every real-valued operator
Phase 1 of the mutation-testing campaign for src/operators/real.rs (the
file with the largest mutant surface — 128 missed mutants spread across
GaussianMutation, BoundedGaussianMutation, SBX, PolynomialMutation,
LevyMutation, and the Mantegna gamma/sigma helpers).

Added:
- Seed-pinned numerical snapshots for each operator's vary() output on
  a fixed parent and seed. Any arithmetic flip in the operator's math
  changes one of the snapshot values and fails the assertion. The
  snapshot tolerance is 1e-12 so even subtle FP drift is caught.
- An algebraic-identity test for SBX: c1 + c2 = p1 + p2 per dimension
  before clamping. This identity holds for any β and pins the
  (1+β)·p1 + (1-β)·p2 formula cleanly across 20 seeds.
- A scale-coupling test for PolynomialMutation: a 10× wider bound range
  produces a 10× larger perturbation step at the same seed. Catches any
  mutation that breaks the δ·(hi-lo) coupling.
- Three direct pin tests for mantegna_sigma_u (alpha = 1.5, 1.0, 2.0)
  exercising the gamma() Lanczos series and the formula's edge cases
  (alpha = 1.0 → Cauchy, alpha = 2.0 → Normal-limit where sin(π) ≈ 0).
- A monotonicity property test (sigma_u changes with alpha) to catch
  structural mutants that collapse the formula to a constant.
2026-05-13 22:58:16 -06:00
swaits aa8b6e46e6 test(permutation): pin operator outputs and prove they actually mutate
Phase 1 of the mutation-testing campaign for src/operators/permutation.rs.
Adds 13 tests targeting the 30 surviving mutants in the new permutation
toolkit:

Mutation operators (Inversion / Insertion / Scramble):
- Previously only checked that the output was a valid permutation, which
  passes trivially when the mutant 'replace >= 2 with < 2' skips the
  guard entirely (no mutation = identity output = still a permutation).
  New tests run 30 seeds on an 8-element parent and assert at least one
  seed produces a non-identity output. Kills the >= ↔ < flips.

ShuffledMultisetPermutation::initialize:
- Tightened to assert pop.len() == size up-front, killing the 'replace
  with vec![]' mutant.

Crossover operators (OX / PMX / CX / ERX):
- 'Recombines for n >= 3' tests: with 5-element distinct parents, some
  seed must produce a child differing from both parents. Kills the
  < ↔ > / == / <= guard flips that would early-return parents at n >= 3.
- CX-specific pinned tests: the single-cycle case (children = parents)
  and the two-cycle case (exactly known output). Pins the cycle-detection
  arithmetic and the parent-alternation logic — kills the ==↔!= and
  += ↔ *= mutants inside cx_child.
- ERX: 'distinct starts can yield distinct children' across 30 seeds —
  kills the prev/next-index arithmetic mutants in the adjacency table.

Some residual mutants in this file are equivalent (e.g., < ↔ <= when
n=2 still produces the same OX result because for length-2 inputs the
segment-and-fill recombination converges to the parents anyway).
Documented in test comments.
2026-05-13 20:25:43 -06:00
swaits 47cb1d5f79 test(repair): pin ProjectToSimplex argmax tie-breaking and position
The degenerate-magnitude shortcut in ProjectToSimplex::repair scans
`decision` for the argmax and concentrates all mass there. cargo
mutants found that the strict-greater scan was unpinned: replacing
`>` with `>=` (which would shift the argmax to the last tied
index) and `>` with `==` (which would silently skip larger
values further along) both survived.

Two tests:
- A 3-element vector with two tied maxima at the front pins that the
  scan keeps the first index on a tie.
- A 3-element vector whose argmax is at index 1 pins that the scan
  actually walks past the start when later values are larger.

Other mutants in this file (the `>` ↔ `>=` threshold check at line
106, the `*` ↔ `+` in the threshold constant, the `-` ↔ `+` /
`/` in the tau-fallback initializer, and the `>` ↔ `>=` in the
projection loop's rho update) are equivalent mutants for non-pathological
inputs: the normal-path and shortcut-path math converge to the same
projection result for any input the operator is documented to handle.
Leaving them in the residue.
2026-05-13 20:22:40 -06:00
swaits c94357abe3 test(explorer): pin ExplorerExport, builders, ToDecisionValues outputs
Phase 0.3 of the mutation-testing campaign. Extends the inline tests in
src/explorer/mod.rs with 24 new tests covering the gaps cargo-mutants
identified — about 30 surviving mutants in this one file.

Coverage added:
- Exact-output tests for ToDecisionValues impls on Vec<f64>, Vec<i64>,
  Vec<bool>, Vec<usize> (the previous tests only asserted lengths or
  spot-checked individual entries).
- from_result propagates evaluations / generations from the
  OptimizationResult into RunMeta.
- with_problem_name / with_wall_clock / with_timestamp each set their
  field and preserve the rest of the export.
- to_json emits a JSON containing schema_version, candidates, problem
  name, and algorithm name strings.
- to_writer and to_file round-trip the same bytes.
- Free top-level to_json / to_writer / to_file convenience functions
  exercised end-to-end (round-trip through tmp file).
- pad_decision_schema at all three boundaries (< target / == target /
  > target) to pin the < comparison.
- candidate_to_export's in_pareto_front toggles at front_rank == 0.
- candidate_to_export's feasible toggles at constraint_violation <= 0.
- candidate_to_export pads short objective vectors and truncates long
  ones (the defensive branch).
2026-05-13 20:22:40 -06:00
swaits 3085359d01 test(async): parity sweep — run_async must match run with same seed
Phase 0.2 of the mutation-testing campaign. Adds a module gated on
#[cfg(feature = "async")] that, for every algorithm with a run_async,
asserts that the async runner produces the same result as the sync
runner given the same Config + seed + problem.

Before: nothing exercised run_async, so cargo mutants survived
'replace run_async body with OptimizationResult::new()' and every
comparison/arithmetic mutant inside the async loop for every
async-capable algorithm — about 25-30 algorithms * 5-10 mutants each.
After: every such mutant is killed because the parity test detects
any divergence in best.evaluation.objectives or pareto-front
objective tuples.

Coverage:
- Single-objective real (Sphere1D fixture): RandomSearch,
  HillClimber, OnePlusOneEs, SimulatedAnnealing, GA, PSO, DE, CmaEs,
  IpopCmaEs, sNES, TLBO, NelderMead, BayesianOpt, TPE.
- Multi-objective real (SchafferN1 fixture): NSGA-II/III, SPEA2,
  MOEA/D, MOPSO, IBEA, SMS-EMOA, HypE, PESA-II, ε-MOEA, AGE-MOEA,
  GrEA, KnEA, RVEA, PAES.
- Binary (OneMax): UMDA.
- Permutation (TinyTsp fixture): AntColonyTsp.
- Integer (AbsInt fixture): TabuSearch.
- Multi-fidelity (Sphere1DPartial fixture): Hyperband.

Run with: cargo test --features async --test algorithm_properties async_parity
2026-05-13 19:47:33 -06:00
swaits a773a1eaf6 test(algorithm_info): pin name/full_name/seed for every algorithm
Phase 0.1 of the mutation-testing campaign: a sweep test per algorithm
(33 total) asserting the exact strings returned by AlgorithmInfo::name()
and AlgorithmInfo::full_name() plus the seed propagated through
AlgorithmInfo::seed().

Before: cargo mutants survived dozens of mutants per algorithm replacing
the name/full_name return values with "" or "xyzzy", and the seed
return with None/Some(0)/Some(1). After: every such mutant is caught
by an exact-equality assertion.

NelderMead is deterministic and has no seed override (intentionally);
its test asserts seed() == None to pin the default-trait-impl behavior.
2026-05-13 19:43:17 -06:00
swaits 6368db74bb docs(book): permutation toolkit and multi-objective combinatorial cookbook
- Rewrites cookbook/permutation.md to cover the new operator toolkit:
  initializers, crossovers (OX/PMX/CX/ERX), mutations, a 'what should I
  use' picker, and a worked GA-on-TSP example. JSS multiset section
  explains why the strict-permutation crossovers don't compose with
  operation-string encodings and shows the local POX pattern.

- Adds cookbook/multi-objective-combinatorial.md: bi-objective TSP via
  NSGA-II, bi-objective knapsack (binary encoding), 3-objective JSS
  via NSGA-III, and a hypervolume-based operator comparison.

- Updates choosing-an-algorithm.md to reference the new operators in
  the single- and multi-objective decision tables, plus a noting
  NSGA-II/III's genericity over Vec<usize> and Vec<bool> decisions.

- SUMMARY.md and cookbook.md updated to list the new recipe.
2026-05-13 19:43:17 -06:00
swaits ab545e268a docs(examples): add bi-objective TSP crossover-comparison demo
tsp_operators_compare.rs runs NSGA-II four times on the KroAB-25
bi-objective TSP, holding everything constant except the crossover
operator. Ranks OX, PMX, CX, ERX by hypervolume against a fixed
reference point, plus front size, unique-point count, and runtime.

Pedagogical demonstration that the right comparison metric for a
Pareto search is hypervolume, not single-objective fitness.
2026-05-13 19:33:51 -06:00
swaits de463c2214 docs(examples): add bi-objective TSP, 3-objective JSS, and bi-objective knapsack benchmarks
Three harder Pareto-front demos:

- btsp_kroab.rs — Lust-Teghem bi-objective TSP (KroAB-25 subset of
  TSPLIB KroA100/KroB100). NSGA-II with EdgeRecombinationCrossover +
  InversionMutation. Reports hypervolume vs a fixed reference.

- mo_jss_la01.rs — 3-objective JSS on Lawrence LA01 (10x5 instance).
  Objectives: makespan, total flow time, total tardiness (with
  synthetic due dates dj = 1.3 * sum_processing_times(j)). NSGA-III
  with reference_divisions = 12 (91 Das-Dennis points).

- mo_knapsack.rs — bi-objective 0/1 knapsack a la Zitzler-Thiele.
  30 items, two profit vectors, one capacity. NSGA-II with a local
  one-point binary crossover + BitFlipMutation; weight overruns
  penalized in both objectives.
2026-05-13 19:33:45 -06:00
swaits fa499bdb6b docs(examples): add Ulysses16 TSP and FT06 bi-objective JSS benchmarks
Two canonical combinatorial optimization benchmarks demonstrating the
new permutation toolkit:

- tsp_ulysses16.rs — single-objective TSP via GeneticAlgorithm using
  OrderCrossover + InversionMutation. Reaches the known TSPLIB
  optimum (6859) for the 16-city Ulysses GEO-distance instance.

- jss_ft06_bi.rs — bi-objective JSS (makespan + total flow time) on
  the Fisher-Thompson 6x6 benchmark via NSGA-II using a local POX
  crossover + SwapMutation. Makespan corner reaches the known
  single-objective optimum (55).
2026-05-13 19:33:37 -06:00
swaits 1d1187f20b feat(operators): expand permutation toolkit
Adds eight operators for permutation (Vec<usize>) decisions:

Initializers
- ShuffledPermutation { n } — random shuffles of [0..n)
- ShuffledMultisetPermutation { repeats_per_id } — random shuffles
  of an arbitrary multiset (e.g., JSS operation strings)

Crossovers (strict permutations only)
- OrderCrossover (OX)
- PartiallyMappedCrossover (PMX)
- CycleCrossover (CX)
- EdgeRecombinationCrossover (ERX)

Mutations (preserve both strict permutations and multisets)
- InversionMutation
- InsertionMutation
- ScrambleMutation

All are re-exported from the prelude. Comprehensive unit tests + doctests
included; the pre-existing SwapMutation is untouched.
2026-05-13 19:33:29 -06:00
swaits 31e3757ad0 chore: gitignore cargo-mutants output dir 2026-05-13 19:33:22 -06:00
swaits 6371d82f40 docs(0.9): release notes, cookbook recipe, README polish
Companion to the feat(explorer) commit. Bumps the version and
brings every cross-referencing doc up to v0.9 currency.

- Cargo.toml: version 0.8.0 -> 0.9.0.
- CHANGELOG: 0.9.0 entry covering the explorer export, the
  Problem-side metadata additions, the AlgorithmInfo trait, the
  pick_a_car example, and the new cookbook recipe.
- README: closing paragraph of the PickACar example points users
  at the explorer with a one-call snippet
  (`ExplorerExport::from_result(...).with_algorithm_info(...)
  .to_file(...)?`). Version snippets bumped 0.8 -> 0.9.
- New cookbook recipe at docs/book/src/cookbook/explorer.md
  covering: enabling the serde feature, enriching Problem with
  labels/units/decision-schema, the export call, the JSON schema,
  and custom decision-type handling.
- SUMMARY.md and cookbook.md link the new recipe.
- migration.md: new "To 0.9" section documenting the additive
  changes (purely backwards-compatible upgrade from 0.8.x).
- introduction.md, comparison.md, choosing-an-algorithm.md,
  stability.md: version refs bumped 0.8 -> 0.9.
- cookbook/parallel.md, cookbook/async.md: version refs bumped
  0.8 -> 0.9.
- getting-started.md: version refs bumped, serde feature
  description expanded to mention the explorer module.
- SECURITY.md: supported-versions table moves to 0.9.x.
2026-05-06 22:45:59 -06:00
swaits 729842c260 feat(explorer): JSON export module + supporting metadata + example
Adds a tiny additive surface that turns any OptimizationResult into
a self-describing JSON file the heuropt-explorer webapp can load.
Real Pareto fronts have 50–200+ candidates spanning 2–7+ objectives;
reading them as numbers in a terminal scales badly. This commit
ships the heuropt-side of the explorer — the schema and the export
API. The webapp itself lives in a separate repo on its own cadence.

Three trait/type extensions, all with working defaults so existing
impls compile untouched:

- Objective gains optional `label: Option<String>` and
  `unit: Option<String>` fields, plus fluent builders
  `.with_label("Price").with_unit(\"\$k\")`. Existing
  `Objective::minimize(name)` / `Objective::maximize(name)` are
  unchanged. Both fields are #[serde(default,
  skip_serializing_if = \"Option::is_none\")] so existing JSON
  round-trips cleanly.
- Problem trait gains an optional
  `fn decision_schema(&self) -> Vec<DecisionVariable>` with default
  empty impl. Override it to provide pretty names / labels / units /
  bounds for the explorer; the default produces fallback x[0],
  x[1], … names. New DecisionVariable type at
  `heuropt::core::DecisionVariable` with builder methods.
- New `heuropt::traits::AlgorithmInfo` trait with `name()`
  (required) and `seed()` (default None). Every built-in algorithm
  — all 33 — implements it. Separate from Optimizer<P> so
  multi-fidelity Hyperband (which uses PartialProblem) implements
  it uniformly.

The new explorer module:

- `heuropt::explorer::ExplorerExport` envelope with versioned
  schema (SCHEMA_VERSION = 1).
- ExplorerCandidate per row, with front_rank from
  non_dominated_sort attached at export time so downstream tools
  don't re-derive it.
- ToDecisionValues adapter trait with provided impls for Vec<f64>,
  Vec<bool>, Vec<usize>, Vec<i64>; custom decision types implement
  one method.
- Free functions to_json / to_writer / to_file plus a builder API
  (with_algorithm_info, with_problem_name, with_wall_clock,
  with_timestamp).
- Gated on the existing `serde` feature, which now also pulls in
  `serde_json` as a dep.

The example:

- `examples/pick_a_car.rs` — promotes the README's PickACar to a
  real example, fully enriched with Objective labels/units and a
  decision_schema. Runs NSGA-III for 200 generations, prints a
  sample slice, writes pick_a_car.json. Gated on `serde`.

10 new explorer unit tests cover round-trip serde, fallback
decision-variable names, enriched export, AlgorithmInfo flow,
front-rank correctness, and the ToDecisionValues impls. Lib test
count went from 229 to 242.
2026-05-06 12:48:01 -06:00
swaits 57a43c260e docs: 0.8.0 release polish — README, guide, changelog
Companion to the feat(async) commit. Brings every cross-referencing
doc up to v0.8 currency, replaces marketing-flavored copy with plain
prose, and replaces toy benchmark problems with relatable ones that
include actual run output and interpretive narrative.

- README: collapses the four-bullet "Read the user guide / API
  reference / Tested with N tests / Hot paths optimized" list into
  a single Docs links line.
- README: replaces the Schaffer-N1 toy problem with a PickACar
  multi-objective design problem — three decision variables
  (displacement, weight, drag), four objectives (price, 0-60,
  fuel, noise), and *nonlinear* cost relationships so the Pareto
  front is a real surface, not a 1D sweep. Includes actual NSGA-III
  run output (representative slice across the 100-car front) and
  a narrative explaining what each row tells you and why hand-
  picking would miss the interesting tradeoffs.
- README: removes rustdoc-style hidden `#` setup lines from code
  blocks. The README is rendered as plain markdown on GitHub /
  crates.io, where those lines are visible garbage instead of
  hidden setup. Code blocks are now self-contained.
- Guide quickstart (getting-started.md): replaces Sphere ( Σ x² )
  with a least-squares LineFit example. Same shape (single-
  objective continuous), but recognizable framing. Includes
  actual CMA-ES output, residual table, and narrative comparing
  the answer to standard regression.
- Algorithm count audit: stale "35 algorithms" claim corrected to
  the actual 33 across README, src/lib.rs, introduction.md, and
  the comparison.md table cell.
- Async feature flag listed in the optional-features sections of
  README, src/lib.rs, getting-started.md.
- introduction.md, choosing-an-algorithm.md, comparison.md,
  stability.md, migration.md, cookbook/parallel.md,
  cookbook/custom-optimizer.md: cross-references updated to
  describe full async coverage and link the new cookbook recipe.
- stability.md: removes the speculative "Observer / Snapshot /
  Checkpoint planned" bullet (those didn't ship); documents the
  AsyncProblem / AsyncPartialProblem trait stability.
- migration.md: new "To 0.8" section with paths from 0.5.x and 0.7.x.
- CHANGELOG: 0.8.0 entry capturing the async feature plus the
  documentation / governance / CI catch-up.
- SECURITY.md: supported versions table reflects 0.8.x.
v0.8.0
2026-05-06 11:51:13 -06:00
swaits cbfedd85fa feat(async): add run_async to every algorithm in the catalog
Async coverage was incomplete in 0.7 (only RandomSearch and
DifferentialEvolution had run_async). 0.8 closes the gap: every one
of the 33 algorithms now exposes
run_async(&problem, concurrency).await, gated on the async feature.

- Population-based algorithms fan out per-generation evaluations
  through evaluate_batch_async with concurrency-bounded
  FuturesOrdered chunks.
- Steady-state algorithms (HillClimber, SimulatedAnnealing,
  OnePlusOneEs, Paes, NelderMead) await each step sequentially;
  they accept the concurrency parameter for API uniformity.
- TabuSearch fans out the K-neighbor batch each step.
- Surrogate algorithms (BayesianOpt, Tpe) batch the initial design
  and await per-iteration acquisitions sequentially so the surrogate
  can update between picks.
- Hyperband uses a new AsyncPartialProblem trait (mirroring
  PartialProblem for multi-fidelity workloads) and a parallel
  evaluate_batch_at_budget_async helper; each Successive-Halving
  rung fans out its budgeted evaluations.

All paths preserve seeded determinism: RNG draws happen on the main
task in the same order as the sync path, and only the evaluations
are concurrent.

Adds a dedicated cookbook recipe at docs/book/src/cookbook/async.md
with a worked example (DifferentialEvolution under tokio) and
guidance on picking concurrency. Cross-references in SUMMARY.md
and cookbook.md are updated to surface the new recipe.

The follow-up docs commit reconciles the rest of the user guide
and README to describe the new feature; this commit is the bare
async surface.
2026-05-06 11:51:13 -06:00
swaits d1288aa623 ci(docs): re-enable GitHub Pages deploy
Pages is now enabled on the repo (Settings → Pages → 'Build and
deployment: GitHub Actions'), so the workflow can use the standard
configure-pages → upload-pages-artifact → deploy-pages chain
without needing the GITHUB_TOKEN to enable Pages itself.

PR builds run the build job (catches mdbook breakage) but skip the
deploy job, so PRs don't republish the live site.
2026-05-06 09:04:13 -06:00
swaits af226e3d3b feat: drop heuropt-plot companion crate
Removes the heuropt-plot subcrate, the visualize example that used
it, and the related workspace plumbing (root [workspace] table, the
[workspace] override added to fuzz/Cargo.toml to detach from it,
heuropt-plot dev-dep, CHANGELOG mention).

The visualization concern is better served as an independent third-
party project than as a companion crate in this repo. No effect on
heuropt's public API or the async work in 0.8.0.
2026-05-06 09:04:13 -06:00
swaits cfd5207fb6 ci: drop Pages deploy + loosen simplex-projection fuzz tolerance
Two CI fixes; the previous `enablement: true` attempt didn't work
because the default GITHUB_TOKEN can write to Pages but can't enable
it on a repo that doesn't yet have it configured.

1. .github/workflows/docs.yml: drop the Pages deploy job entirely.
   Build mdbook on every push and upload it as a CI artifact. When
   Pages is enabled manually (Settings → Pages → 'Build and
   deployment: GitHub Actions'), this file can grow back a deploy
   job using actions/configure-pages + actions/deploy-pages.

2. fuzz/fuzz_targets/clamp_to_bounds.rs: the simplex projection's τ
   computation operates on values up to `simplex_total · 1e6` per
   the input filter, so its FP precision floor is ~1e-4 of the
   input scale. Outputs near the `max(x_i − τ, 0)` clamp boundary
   can flip between 0 and a small positive value across
   re-applications without that being a correctness bug. The fuzz
   target is meant to catch *gross* non-idempotence (the all-zeros
   bug that the v0.4 cleanup fixed), not ULP-level slop. Loosen the
   per-element tolerance to `1e-4 · max(simplex_total, max|x_i|, 1)`.
   Verified clean over a 10 M-run soak.
2026-05-06 08:32:22 -06:00
swaits ae1daf687d ci(docs): auto-enable GitHub Pages on first run
The Docs workflow was failing on `actions/configure-pages@v5` with
"Get Pages site failed" because Pages isn't enabled on the repo
yet. Setting `enablement: true` lets the action auto-enable it so
the deploy can proceed without a manual Settings → Pages click.
2026-05-06 08:18:23 -06:00
swaits c1bc3b0528 docs(rustdoc): add runnable examples across operators, metrics, and Pareto utilities
Completes the rustdoc audit — every public item now has at least one
```rust example block in its docstring, exercised by
`cargo test --doc` (55 doctests, all passing).

- Operators: BitFlipMutation, SwapMutation, RealBounds,
  GaussianMutation, BoundedGaussianMutation,
  SimulatedBinaryCrossover, PolynomialMutation, LevyMutation,
  ClampToBounds, ProjectToSimplex.
- Metrics: hypervolume_2d, hypervolume_nd, spacing.
- Pareto utilities: pareto_compare, pareto_front, best_candidate,
  non_dominated_sort, crowding_distance, das_dennis,
  ParetoArchive.

Each example is short (5-15 lines) and self-contained — copy-paste
into a fresh project and it runs.
2026-05-06 08:16:04 -06:00
swaits d564f862d7 ci: fix mdbook edition + isolate fuzz crate from workspace
mdbook 0.4.40 (the version pinned in .github/workflows/docs.yml)
doesn't recognize edition = '2024' under [rust], failing the docs
build. Drop to '2021' for the in-book code blocks; the heuropt
crate itself stays on Rust 2024.

Adding [workspace] to the root Cargo.toml made fuzz/Cargo.toml
inherit it, but fuzz isn't in the members list — every fuzz-smoke
job failed with 'current package believes it's in a workspace when
it's not'. Add an empty [workspace] table at the top of
fuzz/Cargo.toml so cargo treats fuzz as the root of its own
workspace and stops walking up.
2026-05-06 08:15:55 -06:00
swaits 5b5fe50df3 feat(heuropt-plot): v0.1.0 — SVG visualization companion crate
Adds heuropt-plot, a tiny SVG-only plotter that takes heuropt
results and emits scatter plots (pareto_front_svg) and line plots
(convergence_svg). No heavy 'plotters' or 'tiny-skia' dep — hand-
rolled SVG so the crate adds <100 KB to a build.

Workspace setup: root Cargo.toml gains [workspace] with members =
['.', 'heuropt-plot']. heuropt-plot has its own version (0.1.0) and
publishes independently against heuropt 0.8+.

Adds examples/visualize.rs that wires it up: NSGA-II on Schaffer
N.1, plain run() (no observer plumbing), final-front SVG written to
disk.
2026-05-06 07:58:08 -06:00
swaits 6368ca5f3d feat(async): AsyncProblem trait + run_async on RandomSearch and DifferentialEvolution
Adds the headline async/await capability for IO-bound evaluations
(HTTP services, RPC clients, spawned subprocesses) — the
differentiator vs pymoo / hyperopt / MOEA Framework.

No public-API breaks for synchronous users. The new surface is
gated behind a new `async` feature flag.

- core::async_problem::AsyncProblem trait (async fn evaluate_async).
- algorithms::parallel_eval_async::evaluate_batch_async helper using
  futures::stream::FuturesOrdered with concurrency-bounded chunks;
  preserves input order so seeded determinism holds when evaluations
  are themselves deterministic.
- run_async on RandomSearch and DifferentialEvolution.
- examples/async_eval.rs: simulated 20 ms remote service. concurrency=1
  → 4.2 s, concurrency=4 → 2.1 s (2× speedup).

Bumps Cargo.toml to 0.8.0; CHANGELOG entry covers the above plus a
note that 0.6.0/0.7.0 on crates.io are yanked experimentals and 0.8
picks up cleanly from 0.5.
2026-05-06 07:55:56 -06:00
swaits fa3f2e8fb0 feat: v0.5.0 — comprehensive documentation release
Theme: documentation and project polish. No public-API changes; this
is the v0.5 release that elevates heuropt's docs/onboarding/governance
to bar-setting status.

Adds:
- mdbook user guide at docs/book/ with intro, getting-started,
  defining-problems, choosing-an-algorithm, cookbook (7 recipes),
  comparison vs other libraries, stability/SemVer, migration guides.
  Deploys to https://swaits.github.io/heuropt/ via .github/workflows/
  docs.yml.
- Runnable rustdoc examples on every algorithm (35 of them), all
  exercised by cargo test --doc.
- Three real-world examples: portfolio.rs (multi-obj with budget
  constraint), hyperparam_tuning.rs (BO + TPE), scheduling.rs
  (permutation via SA + SwapMutation against Smith's-rule oracle).
- Governance: CONTRIBUTING.md, SECURITY.md, CODE_OF_CONDUCT.md
  (adopting builderscode.org's Builder's Code of Conduct), GitHub
  issue templates, PR template.

Polishes:
- README hero with badges + user-guide link.
- lib.rs crate-level docs.
- CHANGELOG entry for 0.5.0.

Bumps Cargo.toml to 0.5.0.
v0.5.0
2026-05-05 14:33:12 -06:00
swaits a9edb0916f ci(fuzz): drop --locked on cargo install cargo-fuzz
cargo-fuzz's bundled Cargo.lock pinned rustix=0.36.5, which used the
now-removed `rustc_attrs` cfg name and broke the install step on
current nightly toolchain (the only toolchain that can build the
fuzzers via libfuzzer-sys). Letting cargo resolve fresh picks a
recent rustix that builds cleanly.

Fixes the fuzz-smoke matrix on the v0.4.0 push CI run.
2026-05-05 13:35:58 -06:00
swaits 232dbc0172 chore(release): bump to v0.4.0
CHANGELOG entry consolidates the unreleased work since v0.3.0:
testing-infrastructure expansion (proptest suites, cargo-fuzz
harness, stability tests, gungraun benches, CI), two real bug
fixes the testing surfaced (NaN-cycle non_dominated_sort, simplex
projection magnitude precision), the README decision-tree update
against the v0.3.0 comparison data, and the v0.4.0 perf pass
(cumulative compare harness 18.6 s → 5.7 s, 3.27×).

`examples/compare-results.md` refreshed with the post-perf-pass
ms numbers; quality metrics are bit-identical to the v0.3.0
snapshot (the perf pass was strictly CPU time, never algorithmic).
v0.4.0
2026-05-05 13:28:47 -06:00
swaits d60a3c3fe8 perf(pareto_archive): cache oriented + inline dominance checks in insert
`ParetoArchive::insert` calls `pareto_compare` twice per existing
member (once per pass), and each call re-allocates two Vec<f64>s
via `as_minimization` — 4N allocations per insert. Cache the
candidate's oriented + feasibility/violation once, build each
member's oriented vector once for the call, then inline the
dominance test against those cached arrays.

Used by PESA-II (per offspring per generation), PAES (per child),
ε-MOEA, and any user code working through the archive directly.

Wall-clock (compare harness, 10-seed mean):
- PESA-II / DTLZ2: 498 → 426 ms (-14 %)
- PESA-II / ZDT1:   87 →  75 ms (-14 %)

Smaller wins on PAES / MOPSO / IBEA / HypE / ε-MOEA where the
archive isn't the dominant per-generation cost.

Bit-identical via the compare harness.
2026-05-05 13:28:38 -06:00
swaits 060841d011 build(release): enable thin LTO + codegen-units=1 in release profile
Cuts ~150 ms (-2.5 %) off the compare harness via better cross-crate
inlining of small Pareto/HV helpers. Costs ~20 s extra on a from-
scratch `cargo build --release`, but is essentially free on
incremental rebuilds.

Only applies when this crate is the workspace root (i.e. when
developing heuropt or running its own examples). Downstream users
who consume heuropt as a dependency see whatever profile their own
Cargo.toml configures.
2026-05-05 13:28:18 -06:00
swaits f01089e9d0 perf(hypervolume): index-sort instead of cloning point vectors
The M≥3 branch of `hso_recursive` cloned every input point into
`sorted: Vec<Vec<f64>>` solely so it could sort. Each clone is M
f64s allocated; with N points per call and ~30 HV calls per SMS-EMOA
generation × 30 k generations, that's millions of small Vec<f64>
allocations.

Sort indices into a `Vec<usize>` instead, then iterate the original
points by index. The pre-projection step still produces a
Vec<Vec<f64>> (which the active-prefix slicing requires), but we
save the outer N inner-Vec clones per call.

gungraun (instructions):
- hypervolume_nd_3d n=30:    87 969 →  70 334  (-20 %, 1.25×)
- hypervolume_nd_3d n=100:  422 767 → 367 767  (-13 %, 1.15×)

Cumulative vs the v0.3.0 baseline:
- hypervolume_nd_3d n=30:    676 902 →  70 334  (9.6×)
- hypervolume_nd_3d n=100: 13 523 760 → 367 767 (37×)

Wall-clock impact is in the noise on the compare harness because the
SMS-EMOA worst-front HV calls operate on small fronts (5–10 points
once converged). The win is most visible in synthetic dense-front
HV benchmarks.
2026-05-05 13:28:18 -06:00
swaits adf18950dc perf(spea2): incremental truncation sort + cache compute_fitness inputs
Two independent wins in SPEA2's per-generation hot path. Both
bit-identical against the compare harness.

# 1. compute_fitness — cache oriented + distance matrix

`compute_fitness` is called twice per generation. The strength-graph
loop calls `pareto_compare` in an N² loop, allocating two Vec<f64>s
per call via `as_minimization`. Inline the dominance test against
cached oriented arrays. The density loop's per-row euclidean recompute
is replaced by a symmetric N×N distance matrix built once.

# 2. build_archive — incremental sort maintenance in truncation

The archive-truncation loop was O(K³ log K) — each pruning iteration
recomputed every alive member's pairwise distances and re-sorted them,
when the only change since the prior iteration was that one specific
neighbor (the just-removed victim) became dead. Compute the distance
matrix and sorted neighbor vectors once, then on victim removal use
binary-search-remove on every survivor's still-sorted vector. Total
truncation cost drops from O(K³ log K) to O(K² log K). Victim choice
is bit-identical.

gungraun (instructions):
- spea2_short: 179 113 → 133 783 (-25 %, 1.34×)

Wall-clock (compare harness, 10-seed mean):
- SPEA2 / ZDT1:   458 → 241 ms (1.9×, cumulative)
- SPEA2 / DTLZ2: 4304 → 513 ms (8.4×, cumulative)
2026-05-05 13:28:18 -06:00