1mod boat;
19mod boundary;
20mod dynamics;
21pub(crate) mod fit;
22mod init;
23mod mutation;
24mod path_baseline;
25mod range_cache;
26mod route_bounds;
27pub mod spherical;
28mod spherical_pso;
29mod time;
30mod traits;
31pub mod units;
32
33#[cfg(feature = "profile-timers")]
37pub mod profile_timers;
38
39pub use boat::Boat;
40pub use dynamics::{get_segment_fuel_and_time, get_segment_land_metres};
41pub use fit::{SailboatFitCalc, weighted_fitness};
42pub use init::{BaselineShares, InitShares, PathInit};
43pub use path_baseline::PathBaseline;
44pub use route_bounds::{DEFAULT_STEP_DISTANCE_FRACTION, RouteBounds};
45pub use spherical::{LatLon, LatLonDelta, LonLatBbox, Segment, TangentMetres, Wind};
46pub use traits::*;
47pub use units::{Floats, Path, PathXY, Time};
48
49#[cfg(feature = "probe-stats")]
50pub use range_cache::{ProbeCounters, ProbeStats, SegmentRangeTables};
51
52use boundary::SailingPathBoundary;
53use fit::PathFitCalc;
54use mutation::{CauchyKickMover, ShapeKickMover};
55use rand::SeedableRng as _;
56use rand::rngs::SmallRng;
57use spherical::{haversine, initial_bearing};
58use spherical_pso::SphericalPSOMover;
59use swarmkit::{
60 Best, Evolution, FitCalc, IntoGBestSearcher as _, IntoLBestSearcher as _,
61 IntoNichedSearcher as _, LBestKind, PSOCoeffs, ParticleMover as _, Searcher,
62};
63use time::TimeNestedMover;
64
65#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Hash)]
69#[non_exhaustive]
70pub enum Topology {
71 #[default]
74 GBest,
75 Niched,
80 Ring,
84 VonNeumann,
88}
89
90impl Topology {
91 pub fn as_str(&self) -> &'static str {
93 match self {
94 Self::GBest => "gbest",
95 Self::Niched => "niched",
96 Self::Ring => "ring",
97 Self::VonNeumann => "von_neumann",
98 }
99 }
100
101 pub const ALL: &'static [Self] = &[Self::GBest, Self::Niched, Self::Ring, Self::VonNeumann];
103}
104
105impl std::fmt::Display for Topology {
106 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107 f.write_str(self.as_str())
108 }
109}
110
111#[derive(Debug, Clone, PartialEq, Eq)]
114pub struct ParseTopologyError(pub String);
115
116impl std::fmt::Display for ParseTopologyError {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 let valid = Topology::ALL
119 .iter()
120 .map(|t| t.as_str())
121 .collect::<Vec<_>>()
122 .join(", ");
123 write!(
124 f,
125 "unknown topology '{}' (expected one of: {valid})",
126 self.0
127 )
128 }
129}
130
131impl std::error::Error for ParseTopologyError {}
132
133impl std::str::FromStr for Topology {
134 type Err = ParseTopologyError;
135
136 fn from_str(s: &str) -> Result<Self, Self::Err> {
137 for &t in Self::ALL {
138 if t.as_str().eq_ignore_ascii_case(s) {
139 return Ok(t);
140 }
141 }
142 Err(ParseTopologyError(s.to_owned()))
143 }
144}
145
146#[derive(Clone, Copy, Debug)]
159pub struct SearchSettings {
160 pub particle_count_space: usize,
161 pub particle_count_time: usize,
162 pub max_iteration_space: usize,
163 pub max_iteration_time: usize,
164 pub inertia: f64,
165 pub cognitive_coeff: f64,
166 pub social_coeff: f64,
167 pub init_shares: InitShares,
168 pub baseline_shares: BaselineShares,
171 pub mutation_gamma_0_fraction: f64,
172 pub mutation_gamma_min_fraction: f64,
173 pub path_kick_probability: f64,
174 pub path_kick_gamma_0_fraction: f64,
175 pub path_kick_gamma_min_fraction: f64,
176 pub seed: Option<u64>,
178 pub range_k: usize,
180 pub k_mcr: usize,
182 pub topology: Topology,
183}
184
185impl Default for SearchSettings {
186 fn default() -> Self {
187 Self {
188 particle_count_space: 40,
189 particle_count_time: 40,
190 max_iteration_space: 40,
191 max_iteration_time: 30,
192 inertia: 0.2,
193 cognitive_coeff: 1.6,
194 social_coeff: 0.85,
195 init_shares: InitShares::default(),
196 baseline_shares: BaselineShares::default(),
197 mutation_gamma_0_fraction: 0.0,
198 mutation_gamma_min_fraction: 0.0,
199 path_kick_probability: 0.1,
200 path_kick_gamma_0_fraction: 0.05,
201 path_kick_gamma_min_fraction: 0.005,
202 seed: None,
203 range_k: crate::range_cache::DEFAULT_RANGE_K,
204 k_mcr: crate::range_cache::DEFAULT_K_MCR,
205 topology: Topology::default(),
206 }
207 }
208}
209
210pub fn search<
211 const N: usize,
212 SB: Sailboat,
213 WS: WindSource,
214 LS: LandmassSource,
215 TFit: FitCalc<T = Path<N>> + SailboatFitData,
216>(
217 boat: &SB,
218 wind_source: &WS,
219 landmass: &LS,
220 route_bounds: RouteBounds,
221 fit_calc: &TFit,
222 settings: SearchSettings,
223) -> (Best<Path<N>>, Evolution<Path<N>>) {
224 #[cfg(feature = "profile-timers")]
227 profile_timers::reset_all();
228
229 let SearchSettings {
230 particle_count_space,
231 particle_count_time,
232 max_iteration_space,
233 max_iteration_time,
234 inertia,
235 cognitive_coeff,
236 social_coeff,
237 init_shares,
238 baseline_shares,
239 mutation_gamma_0_fraction,
240 mutation_gamma_min_fraction,
241 path_kick_probability,
242 path_kick_gamma_0_fraction,
243 path_kick_gamma_min_fraction,
244 seed,
245 range_k,
246 k_mcr,
247 topology,
248 } = settings;
249
250 let mut master_rng: SmallRng = match seed {
254 Some(s) => SmallRng::seed_from_u64(s),
255 None => rand::make_rng(),
256 };
257
258 let pso_coeffs = PSOCoeffs::new(inertia, cognitive_coeff, social_coeff);
259
260 let path_fit = PathFitCalc::new(fit_calc);
261
262 let (mut group, partition) = PathInit::new(
266 &route_bounds,
267 boat,
268 wind_source,
269 &path_fit,
270 particle_count_space,
271 init_shares,
272 baseline_shares,
273 )
274 .init_with_partition(&mut master_rng);
275
276 let line_length_m = haversine(route_bounds.origin, route_bounds.destination);
282 let gamma_0 = (mutation_gamma_0_fraction * line_length_m).max(0.0);
283 let gamma_min = (mutation_gamma_min_fraction * line_length_m).max(0.0);
284 let path_gamma_0 = (path_kick_gamma_0_fraction * line_length_m).max(0.0);
285 let path_gamma_min = (path_kick_gamma_min_fraction * line_length_m).max(0.0);
286
287 let route_bearing =
292 initial_bearing(route_bounds.origin, route_bounds.destination).unwrap_or(0.0);
293 let perp_bearing = route_bearing - std::f64::consts::FRAC_PI_2;
294
295 let space_mover = SphericalPSOMover::<N, Path<N>>::new(pso_coeffs)
300 .chain(CauchyKickMover::<N, Path<N>>::new(gamma_0, gamma_min))
301 .chain(ShapeKickMover::<N, Path<N>>::new(
302 path_kick_probability,
303 path_gamma_0,
304 path_gamma_min,
305 perp_bearing,
306 ))
307 .adapt::<Best<Path<N>>>()
308 .bounded_by(SailingPathBoundary::new(
309 &route_bounds,
310 boat,
311 wind_source,
312 landmass,
313 ));
314
315 let time_mover = TimeNestedMover::new(
316 &path_fit,
317 pso_coeffs,
318 max_iteration_time,
319 particle_count_time,
320 range_k,
321 k_mcr,
322 );
323
324 let mut evolution: Evolution<Path<N>> = Evolution::default();
330 let chained = space_mover.chain(time_mover);
331 let path_fit_for_searcher = PathFitCalc::new(fit_calc);
332 let last = match topology {
333 Topology::GBest => run_to_completion(
334 chained.into_gbest_searcher(path_fit_for_searcher),
335 &mut master_rng,
336 max_iteration_space,
337 &mut group,
338 &mut evolution,
339 ),
340 Topology::Niched => run_to_completion(
341 chained.into_niched_searcher(path_fit_for_searcher, partition),
342 &mut master_rng,
343 max_iteration_space,
344 &mut group,
345 &mut evolution,
346 ),
347 Topology::Ring => run_to_completion(
348 chained.into_lbest_searcher(path_fit_for_searcher, LBestKind::Ring { k: 1 }),
349 &mut master_rng,
350 max_iteration_space,
351 &mut group,
352 &mut evolution,
353 ),
354 Topology::VonNeumann => run_to_completion(
355 chained.into_lbest_searcher(path_fit_for_searcher, LBestKind::VonNeumann),
356 &mut master_rng,
357 max_iteration_space,
358 &mut group,
359 &mut evolution,
360 ),
361 };
362
363 #[cfg(feature = "profile-timers")]
364 profile_timers::dump_to_stderr();
365
366 (last, evolution)
367}
368
369fn run_to_completion<const N: usize, S>(
385 mut searcher: S,
386 master_rng: &mut SmallRng,
387 max_iter: usize,
388 group: &mut swarmkit::Group<Path<N>>,
389 evolution: &mut Evolution<Path<N>>,
390) -> Best<Path<N>>
391where
392 S: Searcher<TUnit = Path<N>>,
393{
394 searcher.reseed(master_rng);
395 searcher
399 .iter(max_iter, group, Some(evolution))
400 .fold(Best::default(), |_acc, snapshot| snapshot)
401}
402
403pub fn reoptimize_times<const N: usize, TFit: FitCalc<T = Path<N>> + SailboatFitData>(
414 fit_calc: &TFit,
415 settings: SearchSettings,
416 fixed_path: Path<N>,
417) -> Path<N> {
418 let SearchSettings {
419 particle_count_time,
420 max_iteration_time,
421 inertia,
422 cognitive_coeff,
423 social_coeff,
424 seed,
425 range_k,
426 k_mcr,
427 ..
428 } = settings;
429
430 let mut master_rng: SmallRng = match seed {
431 Some(s) => SmallRng::seed_from_u64(s),
432 None => rand::make_rng(),
433 };
434
435 let pso_coeffs = PSOCoeffs::new(inertia, cognitive_coeff, social_coeff);
436
437 let path_fit = PathFitCalc::new(fit_calc);
438 let best_t = time::reoptimize_time(
439 &path_fit,
440 particle_count_time,
441 pso_coeffs,
442 max_iteration_time,
443 range_k,
444 k_mcr,
445 fixed_path,
446 &mut master_rng,
447 );
448
449 Path {
450 xy: fixed_path.xy,
451 t: best_t,
452 }
453}