use argmin::core::{CostFunction, Error, Executor, Gradient, Hessian, State};
use argmin::solver::conjugategradient::beta::PolakRibiere;
use argmin::solver::conjugategradient::NonlinearConjugateGradient;
use argmin::solver::linesearch::{
condition::ArmijoCondition, BacktrackingLineSearch, MoreThuenteLineSearch,
};
use argmin::solver::neldermead::NelderMead;
use argmin::solver::quasinewton::LBFGS;
use argmin::solver::simulatedannealing::{Anneal, SATempFunc, SimulatedAnnealing};
use argmin::solver::trustregion::{CauchyPoint, TrustRegion};
use finitediff::vec;
use nalgebra::DVector;
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
use std::cell::RefCell;
use crate::geometry::traits::DiagramShape;
use crate::spec::PreprocessedSpec;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Optimizer {
#[default]
NelderMead,
Lbfgs,
ConjugateGradient,
TrustRegion,
SimulatedAnnealing,
}
#[derive(Debug, Clone)]
pub(crate) struct FinalLayoutConfig {
pub max_iterations: usize,
pub loss_type: crate::loss::LossType,
pub optimizer: Optimizer,
#[allow(dead_code)]
pub tolerance: f64,
}
impl Default for FinalLayoutConfig {
fn default() -> Self {
Self {
max_iterations: 50000,
loss_type: crate::loss::LossType::sse(),
optimizer: Optimizer::NelderMead, tolerance: 1e-6,
}
}
}
pub(crate) fn optimize_layout<S: DiagramShape + Copy + 'static>(
spec: &PreprocessedSpec,
initial_positions: &[f64], initial_radii: &[f64], config: FinalLayoutConfig,
) -> Result<(Vec<f64>, f64), Error> {
let n_sets = spec.n_sets;
let params_per_shape = S::n_params();
let mut initial_params = Vec::with_capacity(n_sets * params_per_shape);
for i in 0..n_sets {
let x = initial_positions[i * 2];
let y = initial_positions[i * 2 + 1];
let r = initial_radii[i];
initial_params.extend(S::params_from_circle(x, y, r));
}
let initial_param = DVector::from_vec(initial_params);
let (final_params, loss) = match config.optimizer {
Optimizer::TrustRegion => {
struct VecCostFunction<'a, S: DiagramShape + Copy + 'static> {
inner: DiagramCost<'a, S>,
}
impl<'a, S: DiagramShape + Copy + 'static> CostFunction for VecCostFunction<'a, S> {
type Param = Vec<f64>;
type Output = f64;
fn cost(&self, param: &Self::Param) -> Result<Self::Output, Error> {
let dvec = DVector::from_vec(param.clone());
self.inner.cost(&dvec)
}
}
impl<'a, S: DiagramShape + Copy + 'static> Gradient for VecCostFunction<'a, S> {
type Param = Vec<f64>;
type Gradient = Vec<f64>;
fn gradient(&self, param: &Self::Param) -> Result<Self::Gradient, Error> {
let dvec = DVector::from_vec(param.clone());
let grad = self.inner.gradient(&dvec)?;
Ok(grad.as_slice().to_vec())
}
}
impl<'a, S: DiagramShape + Copy + 'static> Hessian for VecCostFunction<'a, S> {
type Param = Vec<f64>;
type Hessian = Vec<Vec<f64>>;
fn hessian(&self, param: &Self::Param) -> Result<Self::Hessian, Error> {
let dvec = DVector::from_vec(param.clone());
self.inner.hessian(&dvec)
}
}
let inner_cost = DiagramCost::<S> {
spec,
loss_type: config.loss_type,
params_per_shape,
_shape: std::marker::PhantomData,
};
let cost_function = VecCostFunction { inner: inner_cost };
let solver = TrustRegion::new(CauchyPoint::new());
let initial_param_vec = initial_param.as_slice().to_vec();
let result = Executor::new(cost_function, solver)
.configure(|state| {
state
.param(initial_param_vec)
.max_iters(config.max_iterations as u64)
})
.run()?;
(
DVector::from_vec(result.state().get_best_param().unwrap().clone()),
result.state().get_best_cost(),
)
}
Optimizer::NelderMead => {
let initial_simplex = {
let n_params = initial_param.len();
let mut simplex = Vec::with_capacity(n_params + 1);
simplex.push(initial_param.clone());
for i in 0..n_params {
let mut perturbed = initial_param.clone();
let x0_i = initial_param[i];
let param_idx = i % params_per_shape;
let is_rotation = params_per_shape == 5 && param_idx == 4;
let delta = if x0_i.abs() > 1e-10 {
0.05 * x0_i.abs()
} else if is_rotation {
0.2 } else {
0.00025
};
perturbed[i] += delta;
simplex.push(perturbed);
}
simplex
};
let cost_function = DiagramCost::<S> {
spec,
loss_type: config.loss_type,
params_per_shape,
_shape: std::marker::PhantomData,
};
let solver = NelderMead::new(initial_simplex);
let result = Executor::new(cost_function, solver)
.configure(|state| state.max_iters(config.max_iterations as u64))
.run()?;
(
result.state().get_best_param().unwrap().clone(),
result.state().get_cost(),
)
}
Optimizer::Lbfgs => {
let cost_function_lbfgs = DiagramCost::<S> {
spec,
loss_type: config.loss_type,
params_per_shape,
_shape: std::marker::PhantomData,
};
let line_search = MoreThuenteLineSearch::new();
let solver = LBFGS::new(line_search, 10);
let result = Executor::new(cost_function_lbfgs, solver)
.configure(|state| {
state
.param(initial_param.clone())
.max_iters(config.max_iterations as u64)
})
.run()?;
(
result.state().get_best_param().unwrap().clone(),
result.state().get_cost(),
)
}
Optimizer::ConjugateGradient => {
let cost_function_cg = DiagramCost::<S> {
spec,
loss_type: config.loss_type,
params_per_shape,
_shape: std::marker::PhantomData,
};
let line_search = BacktrackingLineSearch::new(ArmijoCondition::new(0.01)?);
let beta_update = PolakRibiere::new();
let solver = NonlinearConjugateGradient::new(line_search, beta_update);
let result = Executor::new(cost_function_cg, solver)
.configure(|state| {
state
.param(initial_param.clone())
.max_iters(config.max_iterations as u64)
})
.run()?;
(
result.state().get_best_param().unwrap().clone(),
result.state().get_cost(),
)
}
Optimizer::SimulatedAnnealing => {
let (lower, upper) = derive_sa_bounds(initial_param.as_slice(), params_per_shape);
let (best_params, best_loss) = run_simulated_annealing::<S>(
spec,
initial_param.as_slice(),
&lower,
&upper,
config.loss_type,
params_per_shape,
config.max_iterations,
0,
)?;
(DVector::from_vec(best_params), best_loss)
}
};
Ok((final_params.as_slice().to_vec(), loss))
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn run_simulated_annealing<S: DiagramShape + Copy + 'static>(
spec: &PreprocessedSpec,
start_params: &[f64],
lower: &[f64],
upper: &[f64],
loss_type: crate::loss::LossType,
params_per_shape: usize,
max_iters: usize,
seed: u64,
) -> Result<(Vec<f64>, f64), Error> {
let cost = BoundedDiagramCost::<S> {
inner: DiagramCost {
spec,
loss_type,
params_per_shape,
_shape: std::marker::PhantomData,
},
lower: lower.to_vec(),
upper: upper.to_vec(),
rng: RefCell::new(StdRng::seed_from_u64(seed ^ 0xA5A5_A5A5_A5A5_A5A5)),
};
let init_temp = 10.0_f64;
let solver = SimulatedAnnealing::new(init_temp)?
.with_temp_func(SATempFunc::Boltzmann)
.with_stall_best(200)
.with_stall_accepted(200);
let start_vec = start_params.to_vec();
let result = Executor::new(cost, solver)
.configure(|state| state.param(start_vec).max_iters(max_iters as u64))
.run()?;
Ok((
result.state().get_best_param().unwrap().clone(),
result.state().get_best_cost(),
))
}
pub(crate) fn derive_sa_bounds(params: &[f64], params_per_shape: usize) -> (Vec<f64>, Vec<f64>) {
let n_shapes = params.len() / params_per_shape;
let mut lower = vec![0.0; params.len()];
let mut upper = vec![0.0; params.len()];
let mut min_x = f64::INFINITY;
let mut min_y = f64::INFINITY;
let mut max_x = f64::NEG_INFINITY;
let mut max_y = f64::NEG_INFINITY;
for i in 0..n_shapes {
let off = i * params_per_shape;
let (h, k, xlim, ylim) = if params_per_shape == 5 {
let a = params[off + 2].abs();
let b = params[off + 3].abs();
let phi = params[off + 4];
let xlim = ((a * phi.cos()).powi(2) + (b * phi.sin()).powi(2)).sqrt();
let ylim = ((a * phi.sin()).powi(2) + (b * phi.cos()).powi(2)).sqrt();
(params[off], params[off + 1], xlim, ylim)
} else {
let r = params[off + 2].abs();
(params[off], params[off + 1], r, r)
};
min_x = min_x.min(h - xlim);
min_y = min_y.min(k - ylim);
max_x = max_x.max(h + xlim);
max_y = max_y.max(k + ylim);
}
if (max_x - min_x).abs() < 1e-6 {
min_x -= 1.0;
max_x += 1.0;
}
if (max_y - min_y).abs() < 1e-6 {
min_y -= 1.0;
max_y += 1.0;
}
for i in 0..n_shapes {
let off = i * params_per_shape;
lower[off] = min_x;
lower[off + 1] = min_y;
upper[off] = max_x;
upper[off + 1] = max_y;
if params_per_shape == 5 {
let a = params[off + 2].abs().max(1e-6);
let b = params[off + 3].abs().max(1e-6);
lower[off + 2] = a / 3.0;
upper[off + 2] = a * 3.0;
lower[off + 3] = b / 3.0;
upper[off + 3] = b * 3.0;
lower[off + 4] = 0.0;
upper[off + 4] = std::f64::consts::PI;
} else if params_per_shape == 3 {
let r = params[off + 2].abs().max(1e-6);
lower[off + 2] = r / 3.0;
upper[off + 2] = r * 3.0;
}
}
(lower, upper)
}
pub(crate) fn diag_error_from_params<S: DiagramShape + Copy + 'static>(
params: &[f64],
spec: &PreprocessedSpec,
) -> f64 {
let n_sets = spec.n_sets;
let params_per_shape = S::n_params();
let shapes: Vec<S> = (0..n_sets)
.map(|i| {
let start = i * params_per_shape;
let end = start + params_per_shape;
S::from_params(¶ms[start..end])
})
.collect();
let fitted = S::compute_exclusive_regions(&shapes);
crate::loss::LossType::DiagError.compute(&fitted, &spec.exclusive_areas)
}
struct DiagramCost<'a, S: DiagramShape + Copy + 'static> {
spec: &'a PreprocessedSpec,
loss_type: crate::loss::LossType,
params_per_shape: usize,
_shape: std::marker::PhantomData<S>,
}
impl<'a, S: DiagramShape + Copy + 'static> DiagramCost<'a, S> {
fn params_to_shapes(&self, params: &DVector<f64>) -> Vec<S> {
let n_sets = self.spec.n_sets;
(0..n_sets)
.map(|i| {
let start = i * self.params_per_shape;
let end = start + self.params_per_shape;
S::from_params(¶ms.as_slice()[start..end])
})
.collect()
}
}
impl<'a, S: DiagramShape + Copy + 'static> CostFunction for DiagramCost<'a, S> {
type Param = DVector<f64>;
type Output = f64;
fn cost(&self, param: &Self::Param) -> Result<Self::Output, Error> {
let shapes = self.params_to_shapes(param);
let exclusive_areas = S::compute_exclusive_regions(&shapes);
let error = self
.loss_type
.compute(&exclusive_areas, &self.spec.exclusive_areas);
Ok(error)
}
}
impl<'a, S: DiagramShape + Copy + 'static> Gradient for DiagramCost<'a, S> {
type Param = DVector<f64>;
type Gradient = DVector<f64>;
fn gradient(&self, param: &Self::Param) -> Result<Self::Gradient, Error> {
let param_vec = param.as_slice().to_vec();
let f = |x: &Vec<f64>| {
let p = DVector::from_vec(x.to_vec());
Ok(self.cost(&p).unwrap_or(f64::INFINITY))
};
let g_central = vec::central_diff(&f);
let grad_vec = g_central(¶m_vec)?;
Ok(DVector::from_vec(grad_vec))
}
}
struct BoundedDiagramCost<'a, S: DiagramShape + Copy + 'static> {
inner: DiagramCost<'a, S>,
lower: Vec<f64>,
upper: Vec<f64>,
rng: RefCell<StdRng>,
}
impl<'a, S: DiagramShape + Copy + 'static> CostFunction for BoundedDiagramCost<'a, S> {
type Param = Vec<f64>;
type Output = f64;
fn cost(&self, param: &Self::Param) -> Result<Self::Output, Error> {
let dvec = DVector::from_vec(param.clone());
self.inner.cost(&dvec)
}
}
impl<'a, S: DiagramShape + Copy + 'static> Anneal for BoundedDiagramCost<'a, S> {
type Param = Vec<f64>;
type Output = Vec<f64>;
type Float = f64;
fn anneal(&self, param: &Self::Param, extent: Self::Float) -> Result<Self::Output, Error> {
let mut rng = self.rng.borrow_mut();
let mut next = param.clone();
for (i, value) in next.iter_mut().enumerate() {
let range = (self.upper[i] - self.lower[i]).abs().max(1e-6);
let step = rng.random_range(-1.0..1.0) * extent * range / 20.0;
*value = (*value + step).clamp(self.lower[i], self.upper[i]);
}
Ok(next)
}
}
impl<'a, S: DiagramShape + Copy + 'static> Hessian for DiagramCost<'a, S> {
type Param = DVector<f64>;
type Hessian = Vec<Vec<f64>>;
fn hessian(&self, param: &Self::Param) -> Result<Self::Hessian, Error> {
let param_vec = param.as_slice().to_vec();
let grad_fn = |x: &Vec<f64>| -> Result<Vec<f64>, argmin::core::Error> {
let p = DVector::from_vec(x.to_vec());
let grad = self.gradient(&p)?;
Ok(grad.as_slice().to_vec())
};
let h_central = finitediff::vec::central_hessian(&grad_fn);
let hessian = h_central(¶m_vec)?;
Ok(hessian)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::geometry::diagram;
use crate::geometry::primitives::Point;
use crate::geometry::shapes::Circle;
use crate::spec::{DiagramSpec, DiagramSpecBuilder};
mod helpers {
use super::*;
use crate::Combination;
use std::collections::HashMap;
pub fn generate_random_diagram(n_sets: usize, seed: u64) -> (DiagramSpec, Vec<Circle>) {
let (circles, set_names) = random_circle_layout(n_sets, seed);
let exclusive_areas =
diagram::compute_exclusive_areas_from_layout(&circles, &set_names);
let spec = create_spec_from_exclusive(exclusive_areas);
(spec, circles)
}
pub fn random_circle_layout(n_sets: usize, seed: u64) -> (Vec<Circle>, Vec<String>) {
use rand::Rng;
use rand::SeedableRng;
let mut rng = rand::rngs::StdRng::seed_from_u64(seed);
let set_names: Vec<String> = (0..n_sets).map(|i| format!("Set{}", i)).collect();
let mut circles = Vec::new();
for _ in 0..n_sets {
let x = rng.random_range(-5.0..5.0);
let y = rng.random_range(-5.0..5.0);
let r = rng.random_range(0.5..2.0);
circles.push(Circle::new(Point::new(x, y), r));
}
(circles, set_names)
}
pub fn create_spec_from_exclusive(
exclusive_areas: HashMap<Combination, f64>,
) -> DiagramSpec {
let mut builder = DiagramSpecBuilder::new();
for (combo, &area) in &exclusive_areas {
if combo.sets().len() == 1 {
let set_name = &combo.sets()[0];
builder = builder.set(set_name, area);
}
}
for (combo, &area) in &exclusive_areas {
if combo.sets().len() > 1 {
let sets: Vec<&str> = combo.sets().iter().map(|s| s.as_str()).collect();
builder = builder.intersection(&sets, area);
}
}
builder.build().unwrap()
}
}
#[test]
fn test_optimize_layout_simple() {
let spec = DiagramSpecBuilder::new()
.set("A", 3.0)
.set("B", 4.0)
.intersection(&["A", "B"], 1.0)
.build()
.unwrap();
let preprocessed = spec.preprocess().unwrap();
let positions = vec![0.0, 0.0, 5.0, 0.0];
let radii = vec![
(3.0 / std::f64::consts::PI).sqrt(),
(4.0 / std::f64::consts::PI).sqrt(),
];
let config = FinalLayoutConfig {
optimizer: Optimizer::NelderMead,
loss_type: crate::loss::LossType::sse(),
max_iterations: 50,
tolerance: 1e-4,
};
let result = optimize_layout::<Circle>(&preprocessed, &positions, &radii, config);
assert!(result.is_ok());
let (final_params, loss) = result.unwrap();
assert_eq!(final_params.len(), 2 * Circle::n_params()); assert!(loss >= 0.0);
println!("Initial loss: compute initial loss");
println!("Final loss: {}", loss);
}
#[test]
fn test_cost_function_computes() {
let spec = DiagramSpecBuilder::new()
.set("A", 5.0)
.set("B", 5.0)
.intersection(&["A", "B"], 2.0)
.build()
.unwrap();
let preprocessed = spec.preprocess().unwrap();
let cost_fn = DiagramCost::<Circle> {
spec: &preprocessed,
loss_type: crate::loss::LossType::sse(),
params_per_shape: Circle::n_params(),
_shape: std::marker::PhantomData,
};
let positions = vec![0.0, 0.0, 2.0, 0.0];
let radii = vec![
(5.0 / std::f64::consts::PI).sqrt(),
(5.0 / std::f64::consts::PI).sqrt(),
];
let mut params = positions.clone();
params.extend_from_slice(&radii);
let param_vec = DVector::from_vec(params);
let result = cost_fn.cost(¶m_vec);
assert!(result.is_ok(), "Cost function should compute successfully");
let error = result.unwrap();
assert!(error >= 0.0, "Error should be non-negative");
println!("Initial error: {}", error);
}
#[test]
fn test_reproduce_simple_two_circle_layout() {
use helpers::*;
let c1 = Circle::new(Point::new(0.0, 0.0), 1.0);
let c2 = Circle::new(Point::new(1.5, 0.0), 1.0);
let circles = vec![c1, c2];
let set_names = vec!["A".to_string(), "B".to_string()];
let exclusive = diagram::compute_exclusive_areas_from_layout(&circles, &set_names);
println!("Exclusive areas from layout:");
for (combo, area) in &exclusive {
println!(" {:?}: {:.4}", combo.sets(), area);
}
let spec = create_spec_from_exclusive(exclusive);
let preprocessed = spec.preprocess().unwrap();
let positions = vec![0.0, 0.0, 1.5, 0.0];
let radii = vec![1.0, 1.0];
let config = FinalLayoutConfig {
optimizer: Optimizer::NelderMead,
loss_type: crate::loss::LossType::sse(),
max_iterations: 100,
tolerance: 1e-6,
};
let result = optimize_layout::<Circle>(&preprocessed, &positions, &radii, config);
assert!(result.is_ok());
let (_, loss) = result.unwrap();
println!("Reproduction loss: {}", loss);
assert!(
loss < 1e-2,
"Should reproduce layout with low error, got: {}",
loss
);
}
#[test]
fn test_reproduce_random_three_circle_layout() {
use helpers::*;
let (circles, set_names) = random_circle_layout(3, 42);
println!("Random circles:");
for (i, c) in circles.iter().enumerate() {
println!(
" {}: center=({:.2}, {:.2}), radius={:.2}",
set_names[i],
c.center().x(),
c.center().y(),
c.radius()
);
}
let exclusive_areas = diagram::compute_exclusive_areas_from_layout(&circles, &set_names);
println!("\nExclusive areas:");
for (combo, area) in &exclusive_areas {
println!(" {:?}: {:.4}", combo.sets(), area);
}
let spec = create_spec_from_exclusive(exclusive_areas);
let preprocessed = spec.preprocess().unwrap();
let mut positions = Vec::new();
let mut radii = Vec::new();
for c in &circles {
positions.push(c.center().x());
positions.push(c.center().y());
radii.push(c.radius());
}
let config = FinalLayoutConfig {
optimizer: Optimizer::NelderMead,
loss_type: crate::loss::LossType::sse(),
max_iterations: 200,
tolerance: 1e-6,
};
let result = optimize_layout::<Circle>(&preprocessed, &positions, &radii, config);
assert!(result.is_ok());
let (final_pos, loss) = result.unwrap();
println!("\nReproduction loss: {}", loss);
println!("Final params: {:?}", final_pos);
println!("Original radii: {:?}", radii);
assert!(
loss < 5.0,
"Should reproduce random layout reasonably, got: {}",
loss
);
}
#[test]
#[ignore]
fn test_reproduce_multiple_random_diagrams() {
use helpers::*;
let test_configs = [
(2, 100), (2, 200), (3, 300), (3, 400), (4, 501), ];
let mut results = Vec::new();
for (i, &(n_sets, seed)) in test_configs.iter().enumerate() {
println!(
"\n=== Test {} ({} circles, seed {}) ===",
i + 1,
n_sets,
seed
);
let (spec, original_circles) = generate_random_diagram(n_sets, seed);
let preprocessed = spec.preprocess().unwrap();
let mut positions = Vec::new();
let mut radii = Vec::new();
for c in &original_circles {
positions.push(c.center().x());
positions.push(c.center().y());
radii.push(c.radius());
}
let config = FinalLayoutConfig {
optimizer: Optimizer::NelderMead,
loss_type: crate::loss::LossType::sse(),
max_iterations: 200,
tolerance: 1e-6,
};
let result = optimize_layout::<Circle>(&preprocessed, &positions, &radii, config);
assert!(result.is_ok(), "Optimization failed for config {}", i);
let (_, loss) = result.unwrap();
println!("Loss: {:.6}", loss);
results.push((n_sets, seed, loss));
}
println!("\n=== Summary ===");
for (i, &(n_sets, seed, loss)) in results.iter().enumerate() {
let status = match n_sets {
2 if loss < 1.0 => "✅",
3 if loss < 2.0 => "✅",
4 if loss < 5.0 => "✅",
_ => "⚠️",
};
println!(
"{} Test {}: {} circles, seed {}, loss={:.6}",
status,
i + 1,
n_sets,
seed,
loss
);
}
for &(n_sets, seed, loss) in &results {
let tol: f64 = match n_sets {
2 => 2.0, 3 => 5.0, 4 => 10.0, _ => 20.0, };
assert!(
loss < tol,
"configuration n_sets={}, seed={} produced loss={:.6}, \
exceeds tolerance {:.1} — optimizer may have regressed, \
or the seed lands in a bad basin on this platform",
n_sets,
seed,
loss,
tol
);
}
}
#[test]
fn test_derive_sa_bounds_ellipse() {
let params = vec![
0.0, 0.0, 2.0, 1.0, 0.0, 5.0, 0.0, 2.0, 1.0, 0.0, ];
let (lower, upper) = derive_sa_bounds(¶ms, 5);
assert!((lower[0] - (-2.0)).abs() < 1e-9);
assert!((upper[0] - 7.0).abs() < 1e-9);
assert!((lower[1] - (-1.0)).abs() < 1e-9);
assert!((upper[1] - 1.0).abs() < 1e-9);
assert!((lower[2] - (2.0 / 3.0)).abs() < 1e-9);
assert!((upper[2] - 6.0).abs() < 1e-9);
assert!((lower[3] - (1.0 / 3.0)).abs() < 1e-9);
assert!((upper[3] - 3.0).abs() < 1e-9);
assert!((lower[4]).abs() < 1e-9);
assert!((upper[4] - std::f64::consts::PI).abs() < 1e-9);
}
#[test]
fn test_derive_sa_bounds_circle() {
let params = vec![0.0, 0.0, 2.0, 5.0, 0.0, 1.0];
let (lower, upper) = derive_sa_bounds(¶ms, 3);
assert!((lower[0] - (-2.0)).abs() < 1e-9);
assert!((upper[0] - 6.0).abs() < 1e-9);
assert!((lower[2] - (2.0 / 3.0)).abs() < 1e-9);
assert!((upper[2] - 6.0).abs() < 1e-9);
}
#[test]
fn test_sa_run_finds_improvement() {
use crate::geometry::shapes::Circle;
let spec = DiagramSpecBuilder::new()
.set("A", 10.0)
.set("B", 8.0)
.intersection(&["A", "B"], 2.0)
.build()
.unwrap();
let preprocessed = spec.preprocess().unwrap();
let start = vec![0.0, 0.0, 1.78, 3.0, 0.0, 1.6];
let (lower, upper) = derive_sa_bounds(&start, 3);
let (best, loss) = run_simulated_annealing::<Circle>(
&preprocessed,
&start,
&lower,
&upper,
crate::loss::LossType::sse(),
3,
500,
42,
)
.unwrap();
assert_eq!(best.len(), start.len());
assert!(loss.is_finite());
}
}