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.
This commit is contained in:
@@ -483,4 +483,107 @@ mod nd_tests {
|
||||
let hv_with = hypervolume_nd(&with_dominated, &s, &[2.0, 2.0, 2.0]);
|
||||
assert!((hv_base - hv_with).abs() < 1e-12, "{hv_base} vs {hv_with}");
|
||||
}
|
||||
|
||||
// ---- Mutation-test pinned helpers --------------------------------------
|
||||
|
||||
/// `dominates(a, b)` is true iff `a` is ≤ `b` on every axis and strictly
|
||||
/// better on at least one. Pin all the boundary cases so the `<` / `>`
|
||||
/// comparison flips are caught.
|
||||
#[test]
|
||||
fn dominates_strict_and_boundary_cases() {
|
||||
// a strictly dominates b on both axes.
|
||||
assert!(dominates(&[1.0, 1.0], &[2.0, 2.0], 2));
|
||||
// b does not dominate a (reverse).
|
||||
assert!(!dominates(&[2.0, 2.0], &[1.0, 1.0], 2));
|
||||
// Equal points: neither dominates (no strict improvement).
|
||||
assert!(!dominates(&[1.0, 1.0], &[1.0, 1.0], 2));
|
||||
// a better on axis 0, equal on axis 1 → a dominates b.
|
||||
assert!(dominates(&[1.0, 2.0], &[2.0, 2.0], 2));
|
||||
// a better on axis 0 but worse on axis 1 → no domination.
|
||||
assert!(!dominates(&[1.0, 3.0], &[2.0, 2.0], 2));
|
||||
}
|
||||
|
||||
/// `non_dominated_projection` drops dominated members and keeps the
|
||||
/// rest. Pin the exact retained set.
|
||||
#[test]
|
||||
fn non_dominated_projection_drops_dominated() {
|
||||
let pts = vec![
|
||||
vec![1.0, 3.0], // non-dominated
|
||||
vec![3.0, 1.0], // non-dominated
|
||||
vec![2.0, 2.0], // non-dominated (trade-off)
|
||||
vec![4.0, 4.0], // dominated by all three
|
||||
];
|
||||
let nd = non_dominated_projection(&pts);
|
||||
assert_eq!(nd.len(), 3);
|
||||
assert!(!nd.contains(&vec![4.0, 4.0]));
|
||||
assert!(nd.contains(&vec![1.0, 3.0]));
|
||||
assert!(nd.contains(&vec![3.0, 1.0]));
|
||||
assert!(nd.contains(&vec![2.0, 2.0]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_dominated_projection_empty_input_is_empty() {
|
||||
let pts: Vec<Vec<f64>> = Vec::new();
|
||||
assert!(non_dominated_projection(&pts).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_dominated_projection_all_nondominated_keeps_all() {
|
||||
let pts = vec![vec![1.0, 3.0], vec![2.0, 2.0], vec![3.0, 1.0]];
|
||||
let nd = non_dominated_projection(&pts);
|
||||
assert_eq!(nd.len(), 3);
|
||||
}
|
||||
|
||||
/// `hso_recursive` 1-D base case: HV is `reference - min_point`,
|
||||
/// clamped at 0.
|
||||
#[test]
|
||||
fn hso_recursive_1d_base_case() {
|
||||
let pts = vec![vec![0.5], vec![1.5], vec![0.2]];
|
||||
// min is 0.2, reference is 2.0 → HV = 1.8
|
||||
assert!((hso_recursive(&pts, &[2.0]) - 1.8).abs() < 1e-12);
|
||||
// A point past the reference → clamped to 0 contribution; min still 0.2.
|
||||
let pts2 = vec![vec![3.0]];
|
||||
assert_eq!(hso_recursive(&pts2, &[2.0]), 0.0);
|
||||
}
|
||||
|
||||
/// `hso_recursive` 2-D base case: classic staircase area.
|
||||
#[test]
|
||||
fn hso_recursive_2d_staircase() {
|
||||
// Three points (1,3), (2,2), (3,1) against reference (4,4).
|
||||
// Dominated area = 6 (same as the hypervolume_2d doctest).
|
||||
let pts = vec![vec![1.0, 3.0], vec![2.0, 2.0], vec![3.0, 1.0]];
|
||||
let hv = hso_recursive(&pts, &[4.0, 4.0]);
|
||||
assert!((hv - 6.0).abs() < 1e-12, "hv = {hv}");
|
||||
}
|
||||
|
||||
/// `hypervolume_nd_from_evaluations` returns 0 for an empty slice and a
|
||||
/// positive value for a dominating point.
|
||||
#[test]
|
||||
fn hypervolume_nd_from_evaluations_empty_and_nonempty() {
|
||||
let s = ObjectiveSpace::new(vec![
|
||||
Objective::minimize("f1"),
|
||||
Objective::minimize("f2"),
|
||||
]);
|
||||
let empty: Vec<&Evaluation> = Vec::new();
|
||||
assert_eq!(hypervolume_nd_from_evaluations(&empty, &s, &[2.0, 2.0]), 0.0);
|
||||
|
||||
let e = Evaluation::new(vec![1.0, 1.0]);
|
||||
let evals = vec![&e];
|
||||
let hv = hypervolume_nd_from_evaluations(&evals, &s, &[2.0, 2.0]);
|
||||
// Single point (1,1) vs reference (2,2) → 1×1 = 1.
|
||||
assert!((hv - 1.0).abs() < 1e-12, "hv = {hv}");
|
||||
}
|
||||
|
||||
/// A point that does not strictly dominate the reference contributes 0.
|
||||
#[test]
|
||||
fn hypervolume_nd_from_evaluations_skips_non_dominating() {
|
||||
let s = ObjectiveSpace::new(vec![
|
||||
Objective::minimize("f1"),
|
||||
Objective::minimize("f2"),
|
||||
]);
|
||||
// (2, 1): axis 0 equals the reference → not strictly dominating.
|
||||
let e = Evaluation::new(vec![2.0, 1.0]);
|
||||
let evals = vec![&e];
|
||||
assert_eq!(hypervolume_nd_from_evaluations(&evals, &s, &[2.0, 2.0]), 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,4 +119,36 @@ mod tests {
|
||||
let s_val = spacing(&pts, &s);
|
||||
assert!(s_val > 0.0);
|
||||
}
|
||||
|
||||
/// Pins the exact spacing for a front with *varying* nearest-neighbor
|
||||
/// distances, exercising the `(a-b).abs()` sum, the `d < nearest`
|
||||
/// comparison, and both `/ n` divisions in the mean/variance.
|
||||
#[test]
|
||||
fn varying_nn_distances_pinned() {
|
||||
let s = space_min2();
|
||||
// (0,10), (1,9), (10,0): L1 nearest distances are 2, 2, 18.
|
||||
// mean = 22/3, variance = 1536/27, spacing = sqrt(1536/27).
|
||||
let front = [
|
||||
cand(vec![0.0, 10.0]),
|
||||
cand(vec![1.0, 9.0]),
|
||||
cand(vec![10.0, 0.0]),
|
||||
];
|
||||
let got = spacing(&front, &s);
|
||||
let expected = (1536.0_f64 / 27.0).sqrt();
|
||||
assert!((got - expected).abs() < 1e-9, "got {got}, expected {expected}");
|
||||
}
|
||||
|
||||
/// A perfectly even front has zero spacing — the variance term is 0.
|
||||
/// Distinct from the doctest case in that it uses three points whose
|
||||
/// nearest-neighbor L1 distances are all equal to 4.
|
||||
#[test]
|
||||
fn evenly_spaced_front_is_zero_spacing() {
|
||||
let s = space_min2();
|
||||
let front = [
|
||||
cand(vec![0.0, 4.0]),
|
||||
cand(vec![2.0, 2.0]),
|
||||
cand(vec![4.0, 0.0]),
|
||||
];
|
||||
assert!(spacing(&front, &s) < 1e-12);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,4 +265,62 @@ mod tests {
|
||||
a.extend(vec![cand(1, vec![1.0, 4.0]), cand(2, vec![3.0, 2.0])]);
|
||||
assert_eq!(a.members().len(), 2);
|
||||
}
|
||||
|
||||
/// `truncate` keeps the archive untouched when it is already at or
|
||||
/// below `max_size`, and trims it when over. Pins the `>` boundary.
|
||||
#[test]
|
||||
fn truncate_boundary_behavior() {
|
||||
let mut a = ParetoArchive::<u32>::new(space_min2());
|
||||
// Three mutually non-dominated members.
|
||||
a.insert(cand(1, vec![1.0, 3.0]));
|
||||
a.insert(cand(2, vec![2.0, 2.0]));
|
||||
a.insert(cand(3, vec![3.0, 1.0]));
|
||||
assert_eq!(a.members().len(), 3);
|
||||
// max_size == len → no-op (kills `>` → `>=`).
|
||||
a.truncate(3);
|
||||
assert_eq!(a.members().len(), 3);
|
||||
// max_size > len → no-op.
|
||||
a.truncate(10);
|
||||
assert_eq!(a.members().len(), 3);
|
||||
// max_size < len → trims.
|
||||
a.truncate(2);
|
||||
assert_eq!(a.members().len(), 2);
|
||||
}
|
||||
|
||||
/// A trade-off candidate (better on one axis, worse on the other) is
|
||||
/// neither dominated nor dominating — it must be *added* alongside the
|
||||
/// existing member. Pins the per-axis `<` / `>` scan in both
|
||||
/// `member_dominates_or_equals` and `candidate_dominates_member`.
|
||||
#[test]
|
||||
fn trade_off_candidate_is_kept_alongside() {
|
||||
let mut a = ParetoArchive::<u32>::new(space_min2());
|
||||
a.insert(cand(1, vec![1.0, 5.0]));
|
||||
a.insert(cand(2, vec![5.0, 1.0])); // trade-off — must be kept
|
||||
assert_eq!(a.members().len(), 2);
|
||||
}
|
||||
|
||||
/// An equal-objectives candidate is rejected (a member dominates-or-
|
||||
/// equals it). Pins the Equal branch — distinguishes `<=` from `<` in
|
||||
/// `candidate_dominates_member` and the `<=` in
|
||||
/// `member_dominates_or_equals`'s infeasible branch.
|
||||
#[test]
|
||||
fn equal_candidate_is_rejected() {
|
||||
let mut a = ParetoArchive::<u32>::new(space_min2());
|
||||
a.insert(cand(1, vec![2.0, 2.0]));
|
||||
a.insert(cand(2, vec![2.0, 2.0])); // identical objectives → rejected
|
||||
assert_eq!(a.members().len(), 1);
|
||||
assert_eq!(a.members()[0].decision, 1);
|
||||
}
|
||||
|
||||
/// Two infeasible candidates: the one with smaller constraint violation
|
||||
/// wins. Pins the `<` / `<=` in the infeasible branches.
|
||||
#[test]
|
||||
fn infeasible_candidate_with_smaller_violation_evicts_larger() {
|
||||
let mut a = ParetoArchive::<u32>::new(space_min2());
|
||||
a.insert(Candidate::new(1u32, Evaluation::constrained(vec![0.0, 0.0], 1.0)));
|
||||
// Smaller violation → dominates the existing infeasible member.
|
||||
a.insert(Candidate::new(2u32, Evaluation::constrained(vec![9.0, 9.0], 0.5)));
|
||||
assert_eq!(a.members().len(), 1);
|
||||
assert_eq!(a.members()[0].decision, 2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,4 +160,33 @@ mod tests {
|
||||
assert!(d[2].is_infinite());
|
||||
assert!(d[1].is_finite());
|
||||
}
|
||||
|
||||
/// Crowding distance pins the exact interior contribution: for a 3-point
|
||||
/// 2-objective front, the middle point's distance is the sum over both
|
||||
/// objectives of (next - prev) / span. With evenly-spaced points the
|
||||
/// value is exactly 2.0 (1.0 per objective).
|
||||
#[test]
|
||||
fn interior_point_distance_is_pinned() {
|
||||
let s = space_min2();
|
||||
// Front along the line f1 + f2 = 4: (0,4), (2,2), (4,0).
|
||||
let pop = [cand(vec![0.0, 4.0]), cand(vec![2.0, 2.0]), cand(vec![4.0, 0.0])];
|
||||
let d = crowding_distance(&pop, &[0, 1, 2], &s);
|
||||
// Boundary points are infinite; the middle point gets
|
||||
// (4-0)/4 + (4-0)/4 = 2.0 (objective 0 span 4, objective 1 span 4).
|
||||
assert!(d[0].is_infinite());
|
||||
assert!(d[2].is_infinite());
|
||||
assert!((d[1] - 2.0).abs() < 1e-12, "interior distance = {}", d[1]);
|
||||
}
|
||||
|
||||
/// An asymmetric front pins the per-objective `(next - prev) / span`
|
||||
/// arithmetic: catches the `-` ↔ `+`/`/` and `/` ↔ `*` mutants.
|
||||
#[test]
|
||||
fn asymmetric_interior_distance_is_pinned() {
|
||||
let s = space_min2();
|
||||
// (0,10), (1,2), (10,0): objective-0 span = 10, objective-1 span = 10.
|
||||
let pop = [cand(vec![0.0, 10.0]), cand(vec![1.0, 2.0]), cand(vec![10.0, 0.0])];
|
||||
let d = crowding_distance(&pop, &[0, 1, 2], &s);
|
||||
// middle point: obj0 (10-0)/10 = 1.0; obj1 (10-0)/10 = 1.0 → 2.0.
|
||||
assert!((d[1] - 2.0).abs() < 1e-12, "got {}", d[1]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,4 +152,35 @@ mod tests {
|
||||
let b = Evaluation::new(vec![2.0, 0.8]);
|
||||
assert_eq!(pareto_compare(&a, &b, &s), Dominance::Dominates);
|
||||
}
|
||||
|
||||
/// `a` better on one axis, worse on the other → NonDominated. Pins the
|
||||
/// `av < bv` / `av > bv` comparisons in the per-objective scan.
|
||||
#[test]
|
||||
fn trade_off_is_non_dominated() {
|
||||
let s = space_min2();
|
||||
let a = Evaluation::new(vec![1.0, 5.0]);
|
||||
let b = Evaluation::new(vec![5.0, 1.0]);
|
||||
assert_eq!(pareto_compare(&a, &b, &s), Dominance::NonDominated);
|
||||
assert_eq!(pareto_compare(&b, &a, &s), Dominance::NonDominated);
|
||||
}
|
||||
|
||||
/// `a` better on one axis, equal on the other → Dominates. This is the
|
||||
/// boundary case that distinguishes `<` from `<=` in the scan.
|
||||
#[test]
|
||||
fn better_on_one_equal_on_other_dominates() {
|
||||
let s = space_min2();
|
||||
let a = Evaluation::new(vec![1.0, 2.0]);
|
||||
let b = Evaluation::new(vec![2.0, 2.0]);
|
||||
assert_eq!(pareto_compare(&a, &b, &s), Dominance::Dominates);
|
||||
assert_eq!(pareto_compare(&b, &a, &s), Dominance::DominatedBy);
|
||||
}
|
||||
|
||||
/// Identical objectives → Equal (neither `<` nor `>` ever fires).
|
||||
#[test]
|
||||
fn identical_objectives_are_equal() {
|
||||
let s = space_min2();
|
||||
let a = Evaluation::new(vec![3.0, 3.0]);
|
||||
let b = Evaluation::new(vec![3.0, 3.0]);
|
||||
assert_eq!(pareto_compare(&a, &b, &s), Dominance::Equal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -180,4 +180,18 @@ mod tests {
|
||||
];
|
||||
assert!(best_candidate(&pop, &s).is_none());
|
||||
}
|
||||
|
||||
/// `best_candidate` keeps the *first* minimum on a tie — pins the strict
|
||||
/// `v < best_min` (a `<=` mutant would keep the last tied candidate).
|
||||
#[test]
|
||||
fn best_candidate_keeps_first_on_tie() {
|
||||
use crate::core::objective::Objective;
|
||||
let s = ObjectiveSpace::new(vec![Objective::minimize("f")]);
|
||||
let pop = [
|
||||
Candidate::new(1u32, Evaluation::new(vec![1.0])),
|
||||
Candidate::new(2u32, Evaluation::new(vec![1.0])),
|
||||
];
|
||||
let best = best_candidate(&pop, &s).unwrap();
|
||||
assert_eq!(best.decision, 1, "should keep the first of two tied minima");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,4 +227,39 @@ mod tests {
|
||||
assert_eq!(f1, vec![3]);
|
||||
assert_eq!(f2, vec![4]);
|
||||
}
|
||||
|
||||
/// Three mutually non-dominated points all land in front 0; a fourth
|
||||
/// point dominated by all three lands in front 1. Pins the `<` / `>`
|
||||
/// comparisons in the inline dominance check.
|
||||
#[test]
|
||||
fn three_nondominated_then_one_dominated() {
|
||||
let s = space_min2();
|
||||
let pop = [
|
||||
cand(vec![1.0, 3.0]),
|
||||
cand(vec![2.0, 2.0]),
|
||||
cand(vec![3.0, 1.0]),
|
||||
cand(vec![5.0, 5.0]), // dominated by all three
|
||||
];
|
||||
let fronts = non_dominated_sort(&pop, &s);
|
||||
assert_eq!(fronts.len(), 2);
|
||||
assert_eq!(fronts[0].len(), 3);
|
||||
assert_eq!(fronts[1], vec![3]);
|
||||
}
|
||||
|
||||
/// A strict chain a ▷ b ▷ c produces three singleton fronts. Pins the
|
||||
/// front-peeling `while` loop and the `&&` guard at line 127.
|
||||
#[test]
|
||||
fn strict_chain_produces_three_singleton_fronts() {
|
||||
let s = space_min2();
|
||||
let pop = [
|
||||
cand(vec![1.0, 1.0]), // dominates everything
|
||||
cand(vec![2.0, 2.0]),
|
||||
cand(vec![3.0, 3.0]),
|
||||
];
|
||||
let fronts = non_dominated_sort(&pop, &s);
|
||||
assert_eq!(fronts.len(), 3);
|
||||
assert_eq!(fronts[0], vec![0]);
|
||||
assert_eq!(fronts[1], vec![1]);
|
||||
assert_eq!(fronts[2], vec![2]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,4 +269,105 @@ mod tests {
|
||||
let mut rng = rng_from_seed(0);
|
||||
let _ = stochastic_ranking_select(&pop, &s, 1.5, 1, &mut rng);
|
||||
}
|
||||
|
||||
// ---- Mutation-test pinned helpers --------------------------------------
|
||||
|
||||
fn constrained(d: u32, obj: f64, cv: f64) -> Candidate<u32> {
|
||||
Candidate::new(d, Evaluation::constrained(vec![obj], cv))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn challenger_wins_feasibility_first() {
|
||||
// Feasible challenger beats infeasible best, regardless of objective.
|
||||
let feasible = cand_min(1, 100.0);
|
||||
let infeasible = constrained(2, 0.0, 1.0);
|
||||
assert!(challenger_wins(&feasible, &infeasible, Direction::Minimize));
|
||||
assert!(!challenger_wins(&infeasible, &feasible, Direction::Minimize));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn challenger_wins_two_infeasible_compares_violation() {
|
||||
let less_violating = constrained(1, 0.0, 0.5);
|
||||
let more_violating = constrained(2, 0.0, 1.0);
|
||||
assert!(challenger_wins(&less_violating, &more_violating, Direction::Minimize));
|
||||
assert!(!challenger_wins(&more_violating, &less_violating, Direction::Minimize));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn challenger_wins_two_feasible_under_min_and_max() {
|
||||
let lower = cand_min(1, 1.0);
|
||||
let higher = cand_min(2, 2.0);
|
||||
assert!(challenger_wins(&lower, &higher, Direction::Minimize));
|
||||
assert!(!challenger_wins(&higher, &lower, Direction::Minimize));
|
||||
assert!(challenger_wins(&higher, &lower, Direction::Maximize));
|
||||
assert!(!challenger_wins(&lower, &higher, Direction::Maximize));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn challenger_wins_equal_objectives_does_not_win() {
|
||||
// Strict comparison: equal objectives → challenger does NOT win.
|
||||
let a = cand_min(1, 1.0);
|
||||
let b = cand_min(2, 1.0);
|
||||
assert!(!challenger_wins(&a, &b, Direction::Minimize));
|
||||
assert!(!challenger_wins(&a, &b, Direction::Maximize));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn better_by_objective_min_and_max() {
|
||||
let a = Evaluation::new(vec![1.0]);
|
||||
let b = Evaluation::new(vec![2.0]);
|
||||
assert!(better_by_objective(&a, &b, Direction::Minimize));
|
||||
assert!(!better_by_objective(&b, &a, Direction::Minimize));
|
||||
assert!(better_by_objective(&b, &a, Direction::Maximize));
|
||||
assert!(!better_by_objective(&a, &b, Direction::Maximize));
|
||||
// Equal → not strictly better.
|
||||
let c = Evaluation::new(vec![1.0]);
|
||||
assert!(!better_by_objective(&a, &c, Direction::Minimize));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn better_by_feasibility_all_four_branches() {
|
||||
let feasible_a = Evaluation::new(vec![10.0]);
|
||||
let infeasible_b = Evaluation::constrained(vec![0.0], 1.0);
|
||||
// feasible vs infeasible
|
||||
assert!(better_by_feasibility(&feasible_a, &infeasible_b, Direction::Minimize));
|
||||
assert!(!better_by_feasibility(&infeasible_b, &feasible_a, Direction::Minimize));
|
||||
// two infeasible: smaller violation wins
|
||||
let low_cv = Evaluation::constrained(vec![0.0], 0.3);
|
||||
let high_cv = Evaluation::constrained(vec![0.0], 0.9);
|
||||
assert!(better_by_feasibility(&low_cv, &high_cv, Direction::Minimize));
|
||||
assert!(!better_by_feasibility(&high_cv, &low_cv, Direction::Minimize));
|
||||
// two feasible: delegates to better_by_objective
|
||||
let feasible_lower = Evaluation::new(vec![1.0]);
|
||||
let feasible_higher = Evaluation::new(vec![2.0]);
|
||||
assert!(better_by_feasibility(&feasible_lower, &feasible_higher, Direction::Minimize));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stochastic_ranking_select_pf_zero_is_pure_feasibility_order() {
|
||||
// pf = 0 → always compare by feasibility. The feasible candidate
|
||||
// must rank first regardless of objective value.
|
||||
let s = ObjectiveSpace::new(vec![Objective::minimize("f")]);
|
||||
let pop = [
|
||||
constrained(1, 0.0, 2.0), // infeasible, great objective
|
||||
cand_min(2, 100.0), // feasible, terrible objective
|
||||
];
|
||||
let mut rng = rng_from_seed(7);
|
||||
let picks = stochastic_ranking_select(&pop, &s, 0.0, 1, &mut rng);
|
||||
// With pf=0, feasibility dominates → candidate 2 ranked first.
|
||||
assert_eq!(picks, vec![2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stochastic_ranking_select_count_wraps_modulo_population() {
|
||||
// count > population size wraps around via `order[k % n]`.
|
||||
let s = ObjectiveSpace::new(vec![Objective::minimize("f")]);
|
||||
let pop = [cand_min(1, 1.0), cand_min(2, 2.0)];
|
||||
let mut rng = rng_from_seed(0);
|
||||
let picks = stochastic_ranking_select(&pop, &s, 0.0, 5, &mut rng);
|
||||
assert_eq!(picks.len(), 5);
|
||||
// Best (candidate 1) is at index 0; index 2 wraps to it again.
|
||||
assert_eq!(picks[0], 1);
|
||||
assert_eq!(picks[2], 1);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user