feat(selection): add stochastic_ranking_select for constrained problems
Runarsson & Yao 2000 stochastic ranking: a probabilistic alternative to feasibility-first tournament selection. Each pairwise comparison during a bubble-sort pass uses the *objective* value with probability `pf` even when one or both candidates are infeasible. The classic recommendation `pf = 0.45` reliably outperforms strict feasibility-first on heavily-constrained problems where occasionally exploring the infeasible region helps cross narrow feasible corridors. New helper: `stochastic_ranking_select` lives next to `tournament_select_single_objective` in `selection::tournament`. Single-objective only; same signature pattern (population, objectives, count, rng, plus the new `pf` knob).
This commit is contained in:
@@ -73,6 +73,105 @@ fn challenger_wins<D>(c: &Candidate<D>, b: &Candidate<D>, dir: Direction) -> boo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stochastic-ranking selection (Runarsson & Yao 2000) for single-objective
|
||||||
|
/// constrained problems.
|
||||||
|
///
|
||||||
|
/// Performs a probabilistic bubble-sort pass on the population — each
|
||||||
|
/// pairwise comparison uses the *objective* value with probability `pf`,
|
||||||
|
/// otherwise it uses the standard feasibility-then-violation-then-objective
|
||||||
|
/// rule. The classic value is `pf = 0.45`; values close to `0.5` weight
|
||||||
|
/// objective improvement against constraint satisfaction.
|
||||||
|
///
|
||||||
|
/// Returns `count` decisions cloned from the top of the ranked
|
||||||
|
/// population. Useful when constraint satisfaction is hard and strict
|
||||||
|
/// feasibility-first selection traps the search outside the feasible
|
||||||
|
/// region.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// If `objectives` does not contain exactly one objective, if `pf` is
|
||||||
|
/// outside `[0.0, 1.0]`, or if the population is empty when `count > 0`.
|
||||||
|
pub fn stochastic_ranking_select<D: Clone>(
|
||||||
|
population: &[Candidate<D>],
|
||||||
|
objectives: &ObjectiveSpace,
|
||||||
|
pf: f64,
|
||||||
|
count: usize,
|
||||||
|
rng: &mut Rng,
|
||||||
|
) -> Vec<D> {
|
||||||
|
assert!(
|
||||||
|
objectives.is_single_objective(),
|
||||||
|
"stochastic_ranking_select requires exactly one objective",
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
(0.0..=1.0).contains(&pf),
|
||||||
|
"stochastic_ranking_select pf must be in [0.0, 1.0]",
|
||||||
|
);
|
||||||
|
if count == 0 {
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
assert!(
|
||||||
|
!population.is_empty(),
|
||||||
|
"stochastic_ranking_select called on empty population with count > 0",
|
||||||
|
);
|
||||||
|
|
||||||
|
let direction = objectives.objectives[0].direction;
|
||||||
|
let n = population.len();
|
||||||
|
let mut order: Vec<usize> = (0..n).collect();
|
||||||
|
|
||||||
|
// Bubble-sort with at most n full sweeps (Runarsson & Yao §3).
|
||||||
|
for _ in 0..n {
|
||||||
|
let mut swapped = false;
|
||||||
|
for i in 0..n - 1 {
|
||||||
|
let a = &population[order[i]].evaluation;
|
||||||
|
let b = &population[order[i + 1]].evaluation;
|
||||||
|
let use_objective = rng.random::<f64>() < pf;
|
||||||
|
let a_first = if use_objective || (a.is_feasible() && b.is_feasible()) {
|
||||||
|
better_by_objective(a, b, direction)
|
||||||
|
} else {
|
||||||
|
better_by_feasibility(a, b, direction)
|
||||||
|
};
|
||||||
|
if !a_first {
|
||||||
|
order.swap(i, i + 1);
|
||||||
|
swapped = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !swapped {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut out = Vec::with_capacity(count);
|
||||||
|
for k in 0..count {
|
||||||
|
out.push(population[order[k % n]].decision.clone());
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn better_by_objective(
|
||||||
|
a: &crate::core::evaluation::Evaluation,
|
||||||
|
b: &crate::core::evaluation::Evaluation,
|
||||||
|
direction: Direction,
|
||||||
|
) -> bool {
|
||||||
|
let av = a.objectives.first().copied().unwrap_or(f64::INFINITY);
|
||||||
|
let bv = b.objectives.first().copied().unwrap_or(f64::INFINITY);
|
||||||
|
match direction {
|
||||||
|
Direction::Minimize => av < bv,
|
||||||
|
Direction::Maximize => av > bv,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn better_by_feasibility(
|
||||||
|
a: &crate::core::evaluation::Evaluation,
|
||||||
|
b: &crate::core::evaluation::Evaluation,
|
||||||
|
direction: Direction,
|
||||||
|
) -> bool {
|
||||||
|
match (a.is_feasible(), b.is_feasible()) {
|
||||||
|
(true, false) => true,
|
||||||
|
(false, true) => false,
|
||||||
|
(false, false) => a.constraint_violation < b.constraint_violation,
|
||||||
|
(true, true) => better_by_objective(a, b, direction),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -127,4 +226,40 @@ mod tests {
|
|||||||
let mut rng = rng_from_seed(0);
|
let mut rng = rng_from_seed(0);
|
||||||
let _ = tournament_select_single_objective(&pop, &s, 2, 1, &mut rng);
|
let _ = tournament_select_single_objective(&pop, &s, 2, 1, &mut rng);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stochastic_ranking_returns_count_decisions() {
|
||||||
|
let s = ObjectiveSpace::new(vec![Objective::minimize("f")]);
|
||||||
|
let pop = [cand_min(1, 5.0), cand_min(2, 1.0), cand_min(3, 9.0)];
|
||||||
|
let mut rng = rng_from_seed(7);
|
||||||
|
let picks = stochastic_ranking_select(&pop, &s, 0.45, 4, &mut rng);
|
||||||
|
assert_eq!(picks.len(), 4);
|
||||||
|
for p in &picks {
|
||||||
|
assert!([1, 2, 3].contains(p));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn stochastic_ranking_pf_zero_is_feasibility_first() {
|
||||||
|
// With pf = 0, the algorithm reduces to strict feasibility-first
|
||||||
|
// ordering, so the best feasible candidate should top the rank.
|
||||||
|
let s = ObjectiveSpace::new(vec![Objective::minimize("f")]);
|
||||||
|
let pop = [
|
||||||
|
Candidate::new(1u32, Evaluation::constrained(vec![0.0], 5.0)), // infeasible
|
||||||
|
Candidate::new(2u32, Evaluation::new(vec![10.0])), // feasible, big f
|
||||||
|
Candidate::new(3u32, Evaluation::new(vec![3.0])), // feasible, small f
|
||||||
|
];
|
||||||
|
let mut rng = rng_from_seed(0);
|
||||||
|
let picks = stochastic_ranking_select(&pop, &s, 0.0, 3, &mut rng);
|
||||||
|
assert_eq!(picks[0], 3); // best feasible first
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[should_panic(expected = "pf must be in [0.0, 1.0]")]
|
||||||
|
fn stochastic_ranking_pf_out_of_range_panics() {
|
||||||
|
let s = ObjectiveSpace::new(vec![Objective::minimize("f")]);
|
||||||
|
let pop = [cand_min(1, 1.0)];
|
||||||
|
let mut rng = rng_from_seed(0);
|
||||||
|
let _ = stochastic_ranking_select(&pop, &s, 1.5, 1, &mut rng);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user