mod boat;
mod boundary;
mod dynamics;
pub(crate) mod fit;
mod init;
mod mutation;
mod path_baseline;
mod range_cache;
mod route_bounds;
pub mod spherical;
mod spherical_pso;
mod time;
mod traits;
pub mod units;
#[cfg(feature = "profile-timers")]
pub mod profile_timers;
pub use boat::Boat;
pub use dynamics::{get_segment_fuel_and_time, get_segment_land_metres};
pub use fit::{SailboatFitCalc, weighted_fitness};
pub use init::{BaselineShares, InitShares, PathInit};
pub use path_baseline::PathBaseline;
pub use route_bounds::{DEFAULT_STEP_DISTANCE_FRACTION, RouteBounds};
pub use spherical::{LatLon, LatLonDelta, LonLatBbox, Segment, TangentMetres, Wind};
pub use traits::*;
pub use units::{Floats, Path, PathXY, Time};
#[cfg(feature = "probe-stats")]
pub use range_cache::{ProbeCounters, ProbeStats, SegmentRangeTables};
use boundary::SailingPathBoundary;
use fit::PathFitCalc;
use mutation::{CauchyKickMover, ShapeKickMover};
use rand::SeedableRng as _;
use rand::rngs::SmallRng;
use spherical::{haversine, initial_bearing};
use spherical_pso::SphericalPSOMover;
use swarmkit::{
Best, Evolution, FitCalc, IntoGBestSearcher as _, IntoLBestSearcher as _,
IntoNichedSearcher as _, LBestKind, PSOCoeffs, ParticleMover as _, Searcher,
};
use time::TimeNestedMover;
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Topology {
#[default]
GBest,
Niched,
Ring,
VonNeumann,
}
impl Topology {
pub fn as_str(&self) -> &'static str {
match self {
Self::GBest => "gbest",
Self::Niched => "niched",
Self::Ring => "ring",
Self::VonNeumann => "von_neumann",
}
}
pub const ALL: &'static [Self] = &[Self::GBest, Self::Niched, Self::Ring, Self::VonNeumann];
}
impl std::fmt::Display for Topology {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseTopologyError(pub String);
impl std::fmt::Display for ParseTopologyError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let valid = Topology::ALL
.iter()
.map(|t| t.as_str())
.collect::<Vec<_>>()
.join(", ");
write!(
f,
"unknown topology '{}' (expected one of: {valid})",
self.0
)
}
}
impl std::error::Error for ParseTopologyError {}
impl std::str::FromStr for Topology {
type Err = ParseTopologyError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
for &t in Self::ALL {
if t.as_str().eq_ignore_ascii_case(s) {
return Ok(t);
}
}
Err(ParseTopologyError(s.to_owned()))
}
}
#[derive(Clone, Copy, Debug)]
pub struct SearchSettings {
pub particle_count_space: usize,
pub particle_count_time: usize,
pub max_iteration_space: usize,
pub max_iteration_time: usize,
pub inertia: f64,
pub cognitive_coeff: f64,
pub social_coeff: f64,
pub init_shares: InitShares,
pub baseline_shares: BaselineShares,
pub mutation_gamma_0_fraction: f64,
pub mutation_gamma_min_fraction: f64,
pub path_kick_probability: f64,
pub path_kick_gamma_0_fraction: f64,
pub path_kick_gamma_min_fraction: f64,
pub seed: Option<u64>,
pub range_k: usize,
pub k_mcr: usize,
pub topology: Topology,
}
impl Default for SearchSettings {
fn default() -> Self {
Self {
particle_count_space: 40,
particle_count_time: 40,
max_iteration_space: 40,
max_iteration_time: 30,
inertia: 0.2,
cognitive_coeff: 1.6,
social_coeff: 0.85,
init_shares: InitShares::default(),
baseline_shares: BaselineShares::default(),
mutation_gamma_0_fraction: 0.0,
mutation_gamma_min_fraction: 0.0,
path_kick_probability: 0.1,
path_kick_gamma_0_fraction: 0.05,
path_kick_gamma_min_fraction: 0.005,
seed: None,
range_k: crate::range_cache::DEFAULT_RANGE_K,
k_mcr: crate::range_cache::DEFAULT_K_MCR,
topology: Topology::default(),
}
}
}
pub fn search<
const N: usize,
SB: Sailboat,
WS: WindSource,
LS: LandmassSource,
TFit: FitCalc<T = Path<N>> + SailboatFitData,
>(
boat: &SB,
wind_source: &WS,
landmass: &LS,
route_bounds: RouteBounds,
fit_calc: &TFit,
settings: SearchSettings,
) -> (Best<Path<N>>, Evolution<Path<N>>) {
#[cfg(feature = "profile-timers")]
profile_timers::reset_all();
let SearchSettings {
particle_count_space,
particle_count_time,
max_iteration_space,
max_iteration_time,
inertia,
cognitive_coeff,
social_coeff,
init_shares,
baseline_shares,
mutation_gamma_0_fraction,
mutation_gamma_min_fraction,
path_kick_probability,
path_kick_gamma_0_fraction,
path_kick_gamma_min_fraction,
seed,
range_k,
k_mcr,
topology,
} = settings;
let mut master_rng: SmallRng = match seed {
Some(s) => SmallRng::seed_from_u64(s),
None => rand::make_rng(),
};
let pso_coeffs = PSOCoeffs::new(inertia, cognitive_coeff, social_coeff);
let path_fit = PathFitCalc::new(fit_calc);
let (mut group, partition) = PathInit::new(
&route_bounds,
boat,
wind_source,
&path_fit,
particle_count_space,
init_shares,
baseline_shares,
)
.init_with_partition(&mut master_rng);
let line_length_m = haversine(route_bounds.origin, route_bounds.destination);
let gamma_0 = (mutation_gamma_0_fraction * line_length_m).max(0.0);
let gamma_min = (mutation_gamma_min_fraction * line_length_m).max(0.0);
let path_gamma_0 = (path_kick_gamma_0_fraction * line_length_m).max(0.0);
let path_gamma_min = (path_kick_gamma_min_fraction * line_length_m).max(0.0);
let route_bearing =
initial_bearing(route_bounds.origin, route_bounds.destination).unwrap_or(0.0);
let perp_bearing = route_bearing - std::f64::consts::FRAC_PI_2;
let space_mover = SphericalPSOMover::<N, Path<N>>::new(pso_coeffs)
.chain(CauchyKickMover::<N, Path<N>>::new(gamma_0, gamma_min))
.chain(ShapeKickMover::<N, Path<N>>::new(
path_kick_probability,
path_gamma_0,
path_gamma_min,
perp_bearing,
))
.adapt::<Best<Path<N>>>()
.bounded_by(SailingPathBoundary::new(
&route_bounds,
boat,
wind_source,
landmass,
));
let time_mover = TimeNestedMover::new(
&path_fit,
pso_coeffs,
max_iteration_time,
particle_count_time,
range_k,
k_mcr,
);
let mut evolution: Evolution<Path<N>> = Evolution::default();
let chained = space_mover.chain(time_mover);
let path_fit_for_searcher = PathFitCalc::new(fit_calc);
let last = match topology {
Topology::GBest => run_to_completion(
chained.into_gbest_searcher(path_fit_for_searcher),
&mut master_rng,
max_iteration_space,
&mut group,
&mut evolution,
),
Topology::Niched => run_to_completion(
chained.into_niched_searcher(path_fit_for_searcher, partition),
&mut master_rng,
max_iteration_space,
&mut group,
&mut evolution,
),
Topology::Ring => run_to_completion(
chained.into_lbest_searcher(path_fit_for_searcher, LBestKind::Ring { k: 1 }),
&mut master_rng,
max_iteration_space,
&mut group,
&mut evolution,
),
Topology::VonNeumann => run_to_completion(
chained.into_lbest_searcher(path_fit_for_searcher, LBestKind::VonNeumann),
&mut master_rng,
max_iteration_space,
&mut group,
&mut evolution,
),
};
#[cfg(feature = "profile-timers")]
profile_timers::dump_to_stderr();
(last, evolution)
}
fn run_to_completion<const N: usize, S>(
mut searcher: S,
master_rng: &mut SmallRng,
max_iter: usize,
group: &mut swarmkit::Group<Path<N>>,
evolution: &mut Evolution<Path<N>>,
) -> Best<Path<N>>
where
S: Searcher<TUnit = Path<N>>,
{
searcher.reseed(master_rng);
searcher
.iter(max_iter, group, Some(evolution))
.fold(Best::default(), |_acc, snapshot| snapshot)
}
pub fn reoptimize_times<const N: usize, TFit: FitCalc<T = Path<N>> + SailboatFitData>(
fit_calc: &TFit,
settings: SearchSettings,
fixed_path: Path<N>,
) -> Path<N> {
let SearchSettings {
particle_count_time,
max_iteration_time,
inertia,
cognitive_coeff,
social_coeff,
seed,
range_k,
k_mcr,
..
} = settings;
let mut master_rng: SmallRng = match seed {
Some(s) => SmallRng::seed_from_u64(s),
None => rand::make_rng(),
};
let pso_coeffs = PSOCoeffs::new(inertia, cognitive_coeff, social_coeff);
let path_fit = PathFitCalc::new(fit_calc);
let best_t = time::reoptimize_time(
&path_fit,
particle_count_time,
pso_coeffs,
max_iteration_time,
range_k,
k_mcr,
fixed_path,
&mut master_rng,
);
Path {
xy: fixed_path.xy,
t: best_t,
}
}