use crate::geometry::{Boundary, Geometry};
use crate::result::SolveResult;
use crate::Result;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Strategy {
#[default]
BottomLeftFill,
NfpGuided,
GeneticAlgorithm,
Brkga,
SimulatedAnnealing,
ExtremePoint,
Gdrr,
Alns,
MilpExact,
HybridExact,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum Dimension {
#[cfg_attr(feature = "serde", serde(rename = "2d"))]
TwoD,
#[cfg_attr(feature = "serde", serde(rename = "3d"))]
ThreeD,
}
impl Dimension {
pub fn key(self) -> &'static str {
match self {
Self::TwoD => "2d",
Self::ThreeD => "3d",
}
}
}
pub const SETTINGS: [&str; 10] = [
"spacing",
"margin",
"time_limit_ms",
"target_utilization",
"population_size",
"max_generations",
"crossover_rate",
"mutation_rate",
"seed",
"multi_sheet",
];
pub fn offered_names(dimension: Dimension) -> String {
Strategy::offered(dimension)
.into_iter()
.map(Strategy::name)
.collect::<Vec<_>>()
.join(", ")
}
impl Strategy {
pub const ALL: [Self; 10] = [
Self::BottomLeftFill,
Self::NfpGuided,
Self::GeneticAlgorithm,
Self::Brkga,
Self::SimulatedAnnealing,
Self::ExtremePoint,
Self::Gdrr,
Self::Alns,
Self::MilpExact,
Self::HybridExact,
];
pub fn name(self) -> &'static str {
let index = Self::ALL
.iter()
.position(|&s| s == self)
.expect("ALL lists every strategy");
Self::NAMES[index]
}
pub fn is_offered(self, dimension: Dimension) -> bool {
use Strategy::*;
match dimension {
Dimension::TwoD => {
self != ExtremePoint
&& (cfg!(feature = "milp") || !matches!(self, MilpExact | HybridExact))
}
Dimension::ThreeD => matches!(
self,
BottomLeftFill | ExtremePoint | GeneticAlgorithm | Brkga | SimulatedAnnealing
),
}
}
pub fn offered(dimension: Dimension) -> Vec<Self> {
Self::ALL
.into_iter()
.filter(|s| s.is_offered(dimension))
.collect()
}
pub fn reads_setting(self, dimension: Dimension, setting: &str) -> bool {
use Strategy::*;
match setting {
"spacing" | "margin" | "time_limit_ms" => true,
"multi_sheet" => dimension == Dimension::TwoD,
"crossover_rate" | "mutation_rate" => matches!(self, GeneticAlgorithm),
"population_size" | "max_generations" => matches!(self, GeneticAlgorithm | Brkga),
"seed" => matches!(
self,
GeneticAlgorithm | Brkga | SimulatedAnnealing | Gdrr | Alns | HybridExact
),
"target_utilization" => matches!(self, GeneticAlgorithm | Brkga | SimulatedAnnealing),
_ => false,
}
}
pub fn reads(self, dimension: Dimension) -> Vec<&'static str> {
SETTINGS
.into_iter()
.filter(|s| self.reads_setting(dimension, s))
.collect()
}
pub fn readers_of(dimension: Dimension, setting: &str) -> Vec<&'static str> {
Self::offered(dimension)
.into_iter()
.filter(|s| s.reads_setting(dimension, setting))
.map(Self::name)
.collect()
}
pub const NAMES: [&'static str; 10] = [
"blf",
"nfp",
"ga",
"brkga",
"sa",
"ep",
"gdrr",
"alns",
"milpexact",
"hybridexact",
];
pub const SPELLINGS: [(&'static str, Self); 18] = [
("blf", Self::BottomLeftFill),
("bottomleftfill", Self::BottomLeftFill),
("nfp", Self::NfpGuided),
("nfpguided", Self::NfpGuided),
("ga", Self::GeneticAlgorithm),
("genetic", Self::GeneticAlgorithm),
("geneticalgorithm", Self::GeneticAlgorithm),
("brkga", Self::Brkga),
("sa", Self::SimulatedAnnealing),
("simulatedannealing", Self::SimulatedAnnealing),
("ep", Self::ExtremePoint),
("extremepoint", Self::ExtremePoint),
("gdrr", Self::Gdrr),
("alns", Self::Alns),
("milpexact", Self::MilpExact),
("exact", Self::MilpExact),
("hybridexact", Self::HybridExact),
("hybrid", Self::HybridExact),
];
pub fn parse(name: &str) -> Option<Self> {
let name = name.trim().to_lowercase();
Self::SPELLINGS
.iter()
.find(|(spelling, _)| *spelling == name)
.map(|&(_, strategy)| strategy)
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct Config {
pub strategy: Strategy,
pub spacing: f64,
pub margin: f64,
pub time_limit_ms: u64,
pub target_utilization: Option<f64>,
pub population_size: Option<usize>,
pub max_generations: Option<u32>,
pub crossover_rate: f64,
pub mutation_rate: f64,
pub elite_count: usize,
pub seed: Option<u64>,
}
impl Default for Config {
fn default() -> Self {
Self {
strategy: Strategy::default(),
spacing: 0.0,
margin: 0.0,
time_limit_ms: 30000,
target_utilization: None,
population_size: None,
max_generations: None,
crossover_rate: 0.85,
mutation_rate: 0.05,
elite_count: 5,
seed: None,
}
}
}
impl Config {
pub fn new() -> Self {
Self::default()
}
pub fn validate(&self) -> Result<()> {
use crate::error::{check_at_least, check_range};
check_at_least("spacing", self.spacing, 0.0)?;
check_at_least("margin", self.margin, 0.0)?;
if let Some(size) = self.population_size {
Self::check_population_size(size as i128)?;
}
check_range("crossover_rate", self.crossover_rate, 0.0, 1.0)?;
check_range("mutation_rate", self.mutation_rate, 0.0, 1.0)?;
if let Some(util) = self.target_utilization {
check_range("target_utilization", util, 0.0, 1.0)?;
}
Ok(())
}
pub(crate) fn check_population_size(value: i128) -> Result<usize> {
crate::error::whole_at_least("population_size", value, 2)
}
pub fn with_strategy(mut self, strategy: Strategy) -> Self {
self.strategy = strategy;
self
}
pub fn with_spacing(mut self, spacing: f64) -> Self {
self.spacing = spacing;
self
}
pub fn with_margin(mut self, margin: f64) -> Self {
self.margin = margin;
self
}
pub fn with_time_limit(mut self, ms: u64) -> Self {
self.time_limit_ms = ms;
self
}
pub fn with_target_utilization(mut self, util: f64) -> Self {
self.target_utilization = Some(util);
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.seed = Some(seed);
self
}
pub fn with_population_size(mut self, size: usize) -> Self {
self.population_size = Some(size);
self
}
pub fn with_max_generations(mut self, generations: u32) -> Self {
self.max_generations = Some(generations);
self
}
}
pub type ProgressCallback = Box<dyn Fn(ProgressInfo) + Send + Sync>;
#[derive(Debug, Clone, Default)]
pub struct ProgressInfo {
pub iteration: u32,
pub total_iterations: u32,
pub utilization: f64,
pub best_fitness: f64,
pub items_placed: usize,
pub total_items: usize,
pub elapsed_ms: u64,
pub phase: String,
pub running: bool,
}
impl ProgressInfo {
pub fn new() -> Self {
Self {
running: true,
..Default::default()
}
}
pub fn with_iteration(mut self, current: u32, total: u32) -> Self {
self.iteration = current;
self.total_iterations = total;
self
}
pub fn with_utilization(mut self, utilization: f64) -> Self {
self.utilization = utilization;
self
}
pub fn with_fitness(mut self, fitness: f64) -> Self {
self.best_fitness = fitness;
self
}
pub fn with_items(mut self, placed: usize, total: usize) -> Self {
self.items_placed = placed;
self.total_items = total;
self
}
pub fn with_elapsed(mut self, elapsed_ms: u64) -> Self {
self.elapsed_ms = elapsed_ms;
self
}
pub fn with_phase(mut self, phase: impl Into<String>) -> Self {
self.phase = phase.into();
self
}
pub fn finished(mut self) -> Self {
self.running = false;
self
}
pub fn progress_percent(&self) -> f64 {
if self.total_iterations > 0 {
self.iteration as f64 / self.total_iterations as f64
} else {
0.0
}
}
}
pub trait Solver {
type Geometry: Geometry;
type Boundary: Boundary;
type Scalar;
fn solve(
&self,
geometries: &[Self::Geometry],
boundary: &Self::Boundary,
) -> Result<SolveResult<Self::Scalar>>;
fn solve_with_progress(
&self,
geometries: &[Self::Geometry],
boundary: &Self::Boundary,
callback: ProgressCallback,
) -> Result<SolveResult<Self::Scalar>>;
fn cancel(&self);
}
#[cfg(test)]
mod config_tests {
use super::Config;
#[test]
fn a_target_utilization_above_one_is_refused_not_clamped() {
let config = Config::default().with_target_utilization(1.5);
assert_eq!(config.target_utilization, Some(1.5), "stored as given");
let err = config.validate().expect_err("1.5 is not a utilization");
assert!(err.to_string().contains("target_utilization"), "{err}");
assert!(Config::default()
.with_target_utilization(0.9)
.validate()
.is_ok());
let nan_rate = Config {
mutation_rate: f64::NAN,
..Config::default()
};
assert!(nan_rate.validate().is_err());
}
#[test]
fn a_population_is_stored_as_given_and_judged_by_validate() {
let unset = Config::default();
assert_eq!(unset.population_size, None);
assert_eq!(unset.max_generations, None);
assert!(unset.validate().is_ok());
let set = Config::default()
.with_population_size(100)
.with_max_generations(0);
assert_eq!(set.population_size, Some(100));
assert_eq!(set.max_generations, Some(0), "0 generations is a budget");
assert!(set.validate().is_ok());
let one = Config::default().with_population_size(1);
assert_eq!(one.population_size, Some(1), "stored as given");
let err = one.validate().expect_err("a population of 1 cannot cross");
assert_eq!(err.code(), "parameter_out_of_range");
assert_eq!(
err.to_string(),
"population_size must be a whole number >= 2, got 1"
);
}
}