#![cfg(test)]
use super::fixtures::{Bounds, RandomInit, RectBoundary, Vector2f};
use crate::{
Best, Contextful, FitCalc, GBestMover, IntoGBestSearcher as _, PSOCoeffs, ParticleInit as _,
ParticleMover as _, Searcher,
};
use rand::rngs::ThreadRng;
struct SphereFitCalc;
impl Contextful for SphereFitCalc {
type TContext = ();
}
impl FitCalc for SphereFitCalc {
type T = Vector2f;
fn calculate_fit(&self, v: Vector2f) -> f64 {
-(v.x * v.x + v.y * v.y)
}
}
#[test]
fn gbest_searcher_monotonically_improves_on_sphere() {
let bounds = Bounds {
min: Vector2f::new(-10.0, -10.0),
max: Vector2f::new(10.0, 10.0),
};
let boundary = RectBoundary { bounds };
let fit_calc = SphereFitCalc;
let init = RandomInit {
particle_count: 30,
bounds,
};
let config = PSOCoeffs {
inertia: 0.4,
cognitive_coeff: 1.5,
social_coeff: 1.5,
};
let mut group = init.init(&mut ThreadRng::default());
let mut searcher = GBestMover::<Vector2f>::new(config)
.bounded_by(boundary)
.into_gbest_searcher(fit_calc);
let history: Vec<Best<Vector2f>> = searcher.iter(50, &mut group, None).collect();
assert!(
!history.is_empty(),
"searcher should yield at least one iteration"
);
for window in history.windows(2) {
assert!(
window[1].best_fit >= window[0].best_fit,
"gbest regressed: {} -> {}",
window[0].best_fit,
window[1].best_fit,
);
}
let final_fit = history.last().unwrap().best_fit;
assert!(
final_fit > -0.1,
"expected convergence near origin (best_fit > -0.1), got {final_fit}",
);
}
use crate::{
Group, NestedMover, Particle, ParticleInitDependent, ParticleRefFrom, ParticleRefMut, SetTo,
};
use rand::{Rng, SeedableRng as _};
#[derive(Copy, Clone, Default, Debug, PartialEq)]
struct Compound {
outer: Vector2f,
inner: Vector2f,
}
#[derive(Copy, Clone, Default, Debug, PartialEq)]
#[repr(transparent)]
struct InnerView(Vector2f);
#[expect(
unsafe_code,
clippy::multiple_unsafe_ops_per_block,
reason = "deliberate transparent-repr pointer cast for ParticleRefFrom test fixture; the three derefs share one SAFETY argument"
)]
impl ParticleRefFrom for InnerView {
type TSource = Compound;
fn divide_from<'a>(source: &'a mut ParticleRefMut<'_, Compound>) -> ParticleRefMut<'a, Self>
where
Self: Copy,
{
unsafe {
ParticleRefMut {
pos: &mut *std::ptr::from_mut::<Vector2f>(&mut source.pos.inner).cast::<Self>(),
vel: &mut *std::ptr::from_mut::<Vector2f>(&mut source.vel.inner).cast::<Self>(),
fit: source.fit,
best_pos: &mut *std::ptr::from_mut::<Vector2f>(&mut source.best_pos.inner)
.cast::<Self>(),
best_fit: source.best_fit,
}
}
}
}
impl SetTo for InnerView {
type TTarget = Compound;
fn set_to_ref_mut(&self, target: &mut ParticleRefMut<'_, Compound>) {
target.pos.inner = self.0;
}
}
struct StubInit;
impl ParticleInitDependent for StubInit {
type TIn = Compound;
type TOut = InnerView;
fn init_pos<R: Rng>(&self, _: &mut R, _: Compound) -> Vec<InnerView> {
vec![InnerView::default()]
}
fn init_vel<R: Rng>(&self, _: &mut R, _: Compound) -> Vec<InnerView> {
vec![InnerView::default()]
}
fn init_dep<R: Rng>(&self, _: &mut R, _: Compound) -> Group<InnerView> {
Group::from(vec![Particle {
pos: InnerView::default(),
vel: InnerView::default(),
fit: 0.0,
best_pos: InnerView::default(),
best_fit: f64::MIN,
}])
}
}
#[derive(Clone)]
struct StubSearcher {
best: Best<InnerView>,
}
impl Contextful for StubSearcher {
type TContext = Compound;
}
impl Searcher for StubSearcher {
type TUnit = InnerView;
fn init(&mut self, _: &mut Group<InnerView>) {}
fn next(&mut self, _: &mut Group<InnerView>) -> &Best<InnerView> {
&self.best
}
fn reseed<R: Rng>(&mut self, _: &mut R) {}
}
#[test]
fn nested_mover_writes_inner_best_and_leaves_outer_untouched() {
let outer_initial = Vector2f::new(1.0, 2.0);
let inner_initial = Vector2f::new(3.0, 4.0);
let inner_target = Vector2f::new(99.0, -99.0);
let stub_searcher = StubSearcher {
best: Best {
best_fit: 0.0,
best_pos: InnerView(inner_target),
},
};
let nested = NestedMover::new(1, stub_searcher, StubInit);
let mut outer_group = Group::<Compound>::from(vec![Particle {
pos: Compound {
outer: outer_initial,
inner: inner_initial,
},
vel: Compound::default(),
fit: 0.0,
best_pos: Compound::default(),
best_fit: f64::MIN,
}]);
let mut rng = rand::rngs::SmallRng::from_seed([0u8; 32]);
let common = Best::<Compound>::new();
nested.update(&common, &mut rng, 0, &mut outer_group[0].as_ref_mut());
let after_pos = outer_group[0].pos;
assert_eq!(
after_pos.outer, outer_initial,
"outer field must not be modified by the inner search",
);
assert_eq!(
after_pos.inner, inner_target,
"inner field must be overwritten with the inner search's best",
);
}