use crate::boundary::Boundary2D;
use crate::geometry::Geometry2D;
use u_nesting_core::exact::{ExactConfig, ExactResult};
use u_nesting_core::geometry::{Boundary, Geometry};
use u_nesting_core::solver::Config;
use u_nesting_core::{Placement, SolveResult};
#[cfg(feature = "milp")]
use good_lp::{
constraint, default_solver, variable, Expression, ProblemVariables, Solution, SolverModel,
Variable,
};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::Instant;
#[derive(Debug, Clone)]
struct PieceInstance {
geometry_idx: usize,
instance_num: usize,
widths: Vec<f64>,
heights: Vec<f64>,
area: f64,
id: String,
}
#[derive(Debug, Clone)]
struct MilpSolution {
placements: Vec<(f64, f64, usize)>, objective: f64,
exact_result: ExactResult,
}
#[cfg(feature = "milp")]
pub fn run_milp_nesting(
geometries: &[Geometry2D],
boundary: &Boundary2D,
config: &Config,
exact_config: &ExactConfig,
cancelled: Arc<AtomicBool>,
) -> SolveResult<f64> {
let start = Instant::now();
let mut result = SolveResult::new();
let total_instances: usize = geometries.iter().map(|g| g.quantity()).sum();
if !exact_config.is_within_limit(total_instances) {
log::warn!(
"Instance count {} exceeds exact limit {}, returning empty result",
total_instances,
exact_config.max_items
);
result.computation_time_ms = start.elapsed().as_millis() as u64;
return result;
}
let (b_min, b_max) = boundary.aabb();
let margin = config.margin;
let bound_width = b_max[0] - b_min[0] - 2.0 * margin;
let bound_height = b_max[1] - b_min[1] - 2.0 * margin;
if bound_width <= 0.0 || bound_height <= 0.0 {
log::error!("Invalid boundary dimensions after margin");
result.computation_time_ms = start.elapsed().as_millis() as u64;
return result;
}
let rotation_angles = exact_config.rotation_angles();
let num_rotations = rotation_angles.len();
let mut pieces: Vec<PieceInstance> = Vec::new();
for (geom_idx, geom) in geometries.iter().enumerate() {
let mut widths = Vec::with_capacity(num_rotations);
let mut heights = Vec::with_capacity(num_rotations);
for &angle in &rotation_angles {
let (g_min, g_max) = geom.aabb_at_rotation(angle);
widths.push(g_max[0] - g_min[0]);
heights.push(g_max[1] - g_min[1]);
}
for instance in 0..geom.quantity() {
pieces.push(PieceInstance {
geometry_idx: geom_idx,
instance_num: instance,
widths: widths.clone(),
heights: heights.clone(),
area: geom.measure(),
id: geom.id().to_string(),
});
}
}
let n = pieces.len();
if n == 0 {
result.computation_time_ms = start.elapsed().as_millis() as u64;
return result;
}
match solve_milp(
&pieces,
bound_width,
bound_height,
b_min[0] + margin,
b_min[1] + margin,
config.spacing,
exact_config,
&cancelled,
start,
) {
Some(solution) => {
for (i, piece) in pieces.iter().enumerate() {
if i < solution.placements.len() {
let (x, y, rot_idx) = solution.placements[i];
let rotation = rotation_angles.get(rot_idx).copied().unwrap_or(0.0);
result.placements.push(Placement::new_2d(
piece.id.clone(),
piece.instance_num,
x,
y,
rotation,
));
}
}
result.boundaries_used = if result.placements.is_empty() { 0 } else { 1 };
result.utilization =
pieces.iter().map(|p| p.area).sum::<f64>() / (bound_width * bound_height);
result.best_fitness = Some(solution.objective);
result.strategy = Some("MilpExact".to_string());
result.iterations = Some(solution.exact_result.iterations);
if solution.exact_result.is_optimal {
log::info!("MILP found optimal solution");
} else {
log::info!(
"MILP found feasible solution (gap: {:.2}%)",
solution.exact_result.gap * 100.0
);
}
}
None => {
log::warn!("MILP solver failed to find a solution");
for piece in &pieces {
result.unplaced.push(piece.id.clone());
}
}
}
result.computation_time_ms = start.elapsed().as_millis() as u64;
result
}
#[cfg(feature = "milp")]
fn solve_milp(
pieces: &[PieceInstance],
width: f64,
height: f64,
origin_x: f64,
origin_y: f64,
spacing: f64,
config: &ExactConfig,
cancelled: &Arc<AtomicBool>,
start: Instant,
) -> Option<MilpSolution> {
let n = pieces.len();
let r = config.rotation_steps;
let big_m = width.max(height) * 2.0;
let mut vars = ProblemVariables::new();
let x: Vec<Variable> = (0..n)
.map(|i| vars.add(variable().min(0.0).max(width).name(format!("x_{}", i))))
.collect();
let y: Vec<Variable> = (0..n)
.map(|i| vars.add(variable().min(0.0).max(height).name(format!("y_{}", i))))
.collect();
let rot: Vec<Vec<Variable>> = (0..n)
.map(|i| {
(0..r)
.map(|k| vars.add(variable().binary().name(format!("rot_{}_{}", i, k))))
.collect()
})
.collect();
let mut left: Vec<Vec<Variable>> = Vec::new();
let mut right: Vec<Vec<Variable>> = Vec::new();
let mut below: Vec<Vec<Variable>> = Vec::new();
let mut above: Vec<Vec<Variable>> = Vec::new();
for i in 0..n {
let mut left_row = Vec::new();
let mut right_row = Vec::new();
let mut below_row = Vec::new();
let mut above_row = Vec::new();
for j in 0..n {
if i < j {
left_row.push(vars.add(variable().binary().name(format!("left_{}_{}", i, j))));
right_row.push(vars.add(variable().binary().name(format!("right_{}_{}", i, j))));
below_row.push(vars.add(variable().binary().name(format!("below_{}_{}", i, j))));
above_row.push(vars.add(variable().binary().name(format!("above_{}_{}", i, j))));
} else {
left_row.push(vars.add(variable().binary()));
right_row.push(vars.add(variable().binary()));
below_row.push(vars.add(variable().binary()));
above_row.push(vars.add(variable().binary()));
}
}
left.push(left_row);
right.push(right_row);
below.push(below_row);
above.push(above_row);
}
let strip_length = vars.add(variable().min(0.0).max(width).name("strip_length"));
let mut problem = vars.minimise(strip_length).using(default_solver);
for rot_row in rot.iter().take(n) {
let sum: Expression = rot_row.iter().map(|&v| Expression::from(v)).sum();
problem = problem.with(constraint!(sum == 1.0));
}
for i in 0..n {
let width_expr: Expression = (0..r)
.map(|k| pieces[i].widths[k] * rot[i][k])
.fold(Expression::from(0.0), |acc, term| acc + term);
problem = problem.with(constraint!(x[i] + width_expr.clone() <= width));
problem = problem.with(constraint!(x[i] + width_expr <= strip_length));
let height_expr: Expression = (0..r)
.map(|k| pieces[i].heights[k] * rot[i][k])
.fold(Expression::from(0.0), |acc, term| acc + term);
problem = problem.with(constraint!(y[i] + height_expr <= height));
}
for i in 0..n {
for j in (i + 1)..n {
if cancelled.load(Ordering::Relaxed) {
return None;
}
if start.elapsed().as_millis() as u64 > config.time_limit_ms / 2 {
log::warn!("Constraint building taking too long, using simplified model");
}
let w_i = pieces[i].widths.iter().cloned().fold(0.0_f64, f64::max);
let w_j = pieces[j].widths.iter().cloned().fold(0.0_f64, f64::max);
let h_i = pieces[i].heights.iter().cloned().fold(0.0_f64, f64::max);
let h_j = pieces[j].heights.iter().cloned().fold(0.0_f64, f64::max);
problem = problem.with(constraint!(
left[i][j] + right[i][j] + below[i][j] + above[i][j] >= 1.0
));
problem = problem.with(constraint!(
x[i] - x[j] <= big_m - big_m * left[i][j] - w_i - spacing
));
problem = problem.with(constraint!(
x[j] - x[i] <= big_m - big_m * right[i][j] - w_j - spacing
));
problem = problem.with(constraint!(
y[i] - y[j] <= big_m - big_m * below[i][j] - h_i - spacing
));
problem = problem.with(constraint!(
y[j] - y[i] <= big_m - big_m * above[i][j] - h_j - spacing
));
}
}
if config.use_symmetry_breaking {
for i in 0..(n - 1) {
if pieces[i].geometry_idx == pieces[i + 1].geometry_idx {
problem = problem.with(constraint!(x[i] <= x[i + 1]));
}
}
}
log::info!("Solving MILP with {} pieces, {} rotations", n, r);
let solution_result = problem.solve();
match solution_result {
Ok(solution) => {
let obj_value = solution.value(strip_length);
let mut placements = Vec::with_capacity(n);
for i in 0..n {
let px = solution.value(x[i]) + origin_x;
let py = solution.value(y[i]) + origin_y;
let mut selected_rot = 0;
for (k, rot_ik) in rot[i].iter().enumerate().take(r) {
if solution.value(*rot_ik) > 0.5 {
selected_rot = k;
break;
}
}
placements.push((px, py, selected_rot));
}
let exact_result = ExactResult::optimal(obj_value);
Some(MilpSolution {
placements,
objective: obj_value,
exact_result,
})
}
Err(e) => {
log::error!("MILP solver error: {:?}", e);
None
}
}
}
#[cfg(not(feature = "milp"))]
pub fn run_milp_nesting(
geometries: &[Geometry2D],
boundary: &Boundary2D,
_config: &Config,
_exact_config: &ExactConfig,
_cancelled: Arc<AtomicBool>,
) -> SolveResult<f64> {
log::warn!("MILP solver not available (compile with 'milp' feature)");
let mut result = SolveResult::new();
for geom in geometries {
for _ in 0..geom.quantity() {
result.unplaced.push(geom.id().to_string());
}
}
result.strategy = Some("MilpExact (disabled)".to_string());
result
}
pub fn is_milp_available() -> bool {
cfg!(feature = "milp")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::boundary::Boundary2D;
use crate::geometry::Geometry2D;
#[test]
fn test_is_milp_available() {
let _available = is_milp_available();
}
#[test]
#[cfg(feature = "milp")]
fn test_milp_simple_rectangles() {
let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(2)];
let boundary = Boundary2D::rectangle(50.0, 50.0);
let config = Config::default();
let exact_config = ExactConfig::default()
.with_time_limit_ms(10000)
.with_rotation_steps(1);
let cancelled = Arc::new(AtomicBool::new(false));
let result = run_milp_nesting(&geometries, &boundary, &config, &exact_config, cancelled);
assert!(!result.placements.is_empty(), "Should place some pieces");
assert_eq!(result.placements.len(), 2, "Should place all 2 pieces");
}
#[test]
#[cfg(feature = "milp")]
fn test_milp_exceeds_limit() {
let geometries = vec![Geometry2D::rectangle("R1", 5.0, 5.0).with_quantity(20)];
let boundary = Boundary2D::rectangle(100.0, 100.0);
let config = Config::default();
let exact_config = ExactConfig::default().with_max_items(10);
let cancelled = Arc::new(AtomicBool::new(false));
let result = run_milp_nesting(&geometries, &boundary, &config, &exact_config, cancelled);
assert!(
result.placements.is_empty(),
"Should not solve when exceeding limit"
);
}
#[test]
#[cfg(not(feature = "milp"))]
fn test_milp_stub() {
let geometries = vec![Geometry2D::rectangle("R1", 10.0, 10.0).with_quantity(2)];
let boundary = Boundary2D::rectangle(50.0, 50.0);
let config = Config::default();
let exact_config = ExactConfig::default();
let cancelled = Arc::new(AtomicBool::new(false));
let result = run_milp_nesting(&geometries, &boundary, &config, &exact_config, cancelled);
assert!(result.placements.is_empty());
assert!(!result.unplaced.is_empty());
}
}