1use serde::{Deserialize, Serialize};
4
5use crate::{BinPackingError, Result};
6
7const MAX_DIMENSION: u32 = 1 << 30;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
14#[serde(rename_all = "snake_case")]
15pub enum OneDAlgorithm {
16 #[default]
18 Auto,
19 FirstFitDecreasing,
21 BestFitDecreasing,
23 LocalSearch,
25 ColumnGeneration,
27}
28
29#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
31pub struct Stock1D {
32 pub name: String,
34 pub length: u32,
36 #[serde(default)]
38 pub kerf: u32,
39 #[serde(default)]
41 pub trim: u32,
42 #[serde(default = "default_stock_cost")]
44 pub cost: f64,
45 #[serde(default)]
47 pub available: Option<usize>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct CutDemand1D {
53 pub name: String,
55 pub length: u32,
57 pub quantity: usize,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct CutAssignment1D {
64 pub name: String,
66 pub length: u32,
68}
69
70#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
72pub struct StockLayout1D {
73 pub stock_name: String,
75 pub stock_length: u32,
77 pub used_length: u32,
79 pub remaining_length: u32,
81 pub waste: u32,
83 pub cost: f64,
85 pub cuts: Vec<CutAssignment1D>,
87}
88
89#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
91pub struct StockRequirement1D {
92 pub stock_name: String,
94 pub stock_length: u32,
96 pub usable_length: u32,
98 pub cost: f64,
100 pub available_quantity: Option<usize>,
102 pub used_quantity: usize,
104 pub required_quantity: usize,
106 pub additional_quantity_needed: usize,
108}
109
110#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112pub struct SolverMetrics1D {
113 pub iterations: usize,
115 pub generated_patterns: usize,
117 pub enumerated_patterns: usize,
119 pub explored_states: usize,
121 pub notes: Vec<String>,
123}
124
125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
127pub struct OneDSolution {
128 pub algorithm: String,
130 pub exact: bool,
132 pub lower_bound: Option<f64>,
134 pub stock_count: usize,
136 pub total_waste: u64,
138 pub total_cost: f64,
140 pub layouts: Vec<StockLayout1D>,
142 #[serde(default)]
144 pub stock_requirements: Vec<StockRequirement1D>,
145 pub unplaced: Vec<CutAssignment1D>,
147 pub metrics: SolverMetrics1D,
149}
150
151#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
153pub struct OneDProblem {
154 pub stock: Vec<Stock1D>,
156 pub demands: Vec<CutDemand1D>,
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
162pub struct OneDOptions {
163 #[serde(default)]
165 pub algorithm: OneDAlgorithm,
166 #[serde(default = "default_multistart_runs")]
168 pub multistart_runs: usize,
169 #[serde(default = "default_improvement_rounds")]
171 pub improvement_rounds: usize,
172 #[serde(default = "default_column_generation_rounds")]
174 pub column_generation_rounds: usize,
175 #[serde(default = "default_exact_pattern_limit")]
177 pub exact_pattern_limit: usize,
178 #[serde(default = "default_auto_exact_max_types")]
180 pub auto_exact_max_types: usize,
181 #[serde(default = "default_auto_exact_max_quantity")]
183 pub auto_exact_max_quantity: usize,
184 #[serde(default)]
186 pub seed: Option<u64>,
187}
188
189impl Default for OneDOptions {
190 fn default() -> Self {
191 Self {
192 algorithm: OneDAlgorithm::Auto,
193 multistart_runs: default_multistart_runs(),
194 improvement_rounds: default_improvement_rounds(),
195 column_generation_rounds: default_column_generation_rounds(),
196 exact_pattern_limit: default_exact_pattern_limit(),
197 auto_exact_max_types: default_auto_exact_max_types(),
198 auto_exact_max_quantity: default_auto_exact_max_quantity(),
199 seed: None,
200 }
201 }
202}
203
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub(crate) struct PieceInstance {
206 pub(crate) demand_index: usize,
207 pub(crate) name: String,
208 pub(crate) length: u32,
209}
210
211#[derive(Debug, Clone, PartialEq, Eq)]
212pub(crate) struct PackedBin {
213 pub(crate) stock_index: usize,
214 pub(crate) pieces: Vec<PieceInstance>,
215 occupied_length: u32,
216}
217
218impl Stock1D {
219 pub(crate) fn usable_length(&self) -> u32 {
220 self.length.saturating_sub(self.trim)
221 }
222
223 pub(crate) fn adjusted_capacity(&self) -> u32 {
224 self.usable_length().saturating_add(self.kerf)
225 }
226}
227
228impl OneDProblem {
229 pub(crate) fn validate(&self) -> Result<()> {
230 if self.stock.is_empty() {
231 return Err(BinPackingError::InvalidInput(
232 "at least one stock entry is required".to_string(),
233 ));
234 }
235
236 if self.demands.is_empty() {
237 return Err(BinPackingError::InvalidInput(
238 "at least one demand entry is required".to_string(),
239 ));
240 }
241
242 for stock in &self.stock {
243 if stock.length == 0 {
244 return Err(BinPackingError::InvalidInput(format!(
245 "stock `{}` must have a positive length",
246 stock.name
247 )));
248 }
249
250 if stock.length > MAX_DIMENSION {
251 return Err(BinPackingError::InvalidInput(format!(
252 "stock `{}` length {} exceeds the supported maximum of {MAX_DIMENSION}",
253 stock.name, stock.length
254 )));
255 }
256
257 if stock.kerf > MAX_DIMENSION {
258 return Err(BinPackingError::InvalidInput(format!(
259 "stock `{}` kerf {} exceeds the supported maximum of {MAX_DIMENSION}",
260 stock.name, stock.kerf
261 )));
262 }
263
264 if stock.trim > MAX_DIMENSION {
265 return Err(BinPackingError::InvalidInput(format!(
266 "stock `{}` trim {} exceeds the supported maximum of {MAX_DIMENSION}",
267 stock.name, stock.trim
268 )));
269 }
270
271 if stock.usable_length() == 0 {
272 return Err(BinPackingError::InvalidInput(format!(
273 "stock `{}` has no usable length after trim",
274 stock.name
275 )));
276 }
277
278 if !stock.cost.is_finite() || stock.cost < 0.0 {
279 return Err(BinPackingError::InvalidInput(format!(
280 "stock `{}` must have a finite non-negative cost",
281 stock.name
282 )));
283 }
284 }
285
286 for demand in &self.demands {
287 if demand.length == 0 {
288 return Err(BinPackingError::InvalidInput(format!(
289 "demand `{}` must have a positive length",
290 demand.name
291 )));
292 }
293
294 if demand.length > MAX_DIMENSION {
295 return Err(BinPackingError::InvalidInput(format!(
296 "demand `{}` length {} exceeds the supported maximum of {MAX_DIMENSION}",
297 demand.name, demand.length
298 )));
299 }
300
301 if demand.quantity == 0 {
302 return Err(BinPackingError::InvalidInput(format!(
303 "demand `{}` must have a positive quantity",
304 demand.name
305 )));
306 }
307 }
308
309 Ok(())
310 }
311
312 pub(crate) fn ensure_feasible_demands(&self) -> Result<()> {
313 for demand in &self.demands {
314 let feasible = self.stock.iter().any(|stock| stock.usable_length() >= demand.length);
315 if !feasible {
316 return Err(BinPackingError::Infeasible1D {
317 item: demand.name.clone(),
318 length: demand.length,
319 });
320 }
321 }
322
323 Ok(())
324 }
325
326 pub(crate) fn total_quantity(&self) -> usize {
327 self.demands.iter().map(|demand| demand.quantity).sum()
328 }
329
330 pub(crate) fn expanded_pieces(&self) -> Vec<PieceInstance> {
331 let mut pieces = Vec::with_capacity(self.total_quantity());
332
333 for (index, demand) in self.demands.iter().enumerate() {
334 for _ in 0..demand.quantity {
335 pieces.push(PieceInstance {
336 demand_index: index,
337 name: demand.name.clone(),
338 length: demand.length,
339 });
340 }
341 }
342
343 pieces
344 }
345}
346
347impl PackedBin {
348 pub(crate) fn new(stock_index: usize) -> Self {
349 Self { stock_index, pieces: Vec::new(), occupied_length: 0 }
350 }
351
352 pub(crate) fn delta_for_piece(&self, piece: &PieceInstance, stock: &Stock1D) -> Option<u32> {
353 let delta = if self.pieces.is_empty() {
354 piece.length
355 } else {
356 piece.length.saturating_add(stock.kerf)
357 };
358
359 (self.occupied_length.saturating_add(delta) <= stock.usable_length()).then_some(delta)
360 }
361
362 pub(crate) fn can_fit_piece(&self, piece: &PieceInstance, stock: &Stock1D) -> bool {
363 self.delta_for_piece(piece, stock).is_some()
364 }
365
366 pub(crate) fn add_piece(&mut self, piece: PieceInstance, stock: &Stock1D) -> bool {
367 if let Some(delta) = self.delta_for_piece(&piece, stock) {
368 self.occupied_length = self.occupied_length.saturating_add(delta);
369 self.pieces.push(piece);
370 true
371 } else {
372 false
373 }
374 }
375
376 pub(crate) fn used_length(&self) -> u32 {
377 self.occupied_length
378 }
379
380 pub(crate) fn remaining_length(&self, stock: &Stock1D) -> u32 {
381 stock.usable_length().saturating_sub(self.occupied_length)
382 }
383}
384
385impl OneDSolution {
386 pub(crate) fn from_bins(
387 algorithm: impl Into<String>,
388 exact: bool,
389 lower_bound: Option<f64>,
390 stock: &[Stock1D],
391 bins: &[PackedBin],
392 unplaced: &[PieceInstance],
393 metrics: SolverMetrics1D,
394 ) -> Self {
395 let used_counts = count_stock_usage(stock.len(), bins);
396 let mut layouts = bins
397 .iter()
398 .map(|bin| {
399 let stock_entry = &stock[bin.stock_index];
400 let used_length = bin.used_length();
401 let remaining_length = bin.remaining_length(stock_entry);
402
403 StockLayout1D {
404 stock_name: stock_entry.name.clone(),
405 stock_length: stock_entry.length,
406 used_length,
407 remaining_length,
408 waste: remaining_length,
409 cost: stock_entry.cost,
410 cuts: bin
411 .pieces
412 .iter()
413 .map(|piece| CutAssignment1D {
414 name: piece.name.clone(),
415 length: piece.length,
416 })
417 .collect(),
418 }
419 })
420 .collect::<Vec<_>>();
421
422 layouts.sort_by(|left, right| {
423 right
424 .used_length
425 .cmp(&left.used_length)
426 .then_with(|| left.stock_name.cmp(&right.stock_name))
427 });
428
429 let total_waste = layouts.iter().map(|layout| u64::from(layout.waste)).sum();
430 let total_cost = layouts.iter().map(|layout| layout.cost).sum();
431
432 let mut unplaced_cuts = unplaced
433 .iter()
434 .map(|piece| CutAssignment1D { name: piece.name.clone(), length: piece.length })
435 .collect::<Vec<_>>();
436 unplaced_cuts.sort_by(|left, right| right.length.cmp(&left.length));
437
438 Self {
439 algorithm: algorithm.into(),
440 exact,
441 lower_bound,
442 stock_count: layouts.len(),
443 total_waste,
444 total_cost,
445 layouts,
446 stock_requirements: build_stock_requirements(stock, &used_counts, &used_counts),
447 unplaced: unplaced_cuts,
448 metrics,
449 }
450 }
451
452 pub(crate) fn set_required_stock_counts(
453 &mut self,
454 stock: &[Stock1D],
455 required_counts: &[usize],
456 ) {
457 let used_counts = self
458 .stock_requirements
459 .iter()
460 .map(|requirement| requirement.used_quantity)
461 .collect::<Vec<_>>();
462 self.stock_requirements = build_stock_requirements(stock, &used_counts, required_counts);
463 }
464
465 pub(crate) fn is_better_than(&self, other: &Self) -> bool {
466 (
467 self.unplaced.len(),
468 self.stock_count,
469 self.total_waste,
470 OrderedFloat(self.total_cost),
471 !self.exact,
472 ) < (
473 other.unplaced.len(),
474 other.stock_count,
475 other.total_waste,
476 OrderedFloat(other.total_cost),
477 !other.exact,
478 )
479 }
480}
481
482#[derive(Debug, Clone, Copy, PartialEq)]
483struct OrderedFloat(pub f64);
484
485impl Eq for OrderedFloat {}
486
487impl PartialOrd for OrderedFloat {
488 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
489 Some(self.cmp(other))
490 }
491}
492
493impl Ord for OrderedFloat {
494 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
495 self.0.total_cmp(&other.0)
496 }
497}
498
499fn default_stock_cost() -> f64 {
500 1.0
501}
502
503fn count_stock_usage(stock_count: usize, bins: &[PackedBin]) -> Vec<usize> {
504 let mut counts = vec![0_usize; stock_count];
505 for bin in bins {
506 counts[bin.stock_index] = counts[bin.stock_index].saturating_add(1);
507 }
508 counts
509}
510
511fn build_stock_requirements(
512 stock: &[Stock1D],
513 used_counts: &[usize],
514 required_counts: &[usize],
515) -> Vec<StockRequirement1D> {
516 stock
517 .iter()
518 .enumerate()
519 .map(|(index, stock_entry)| {
520 let used_quantity = *used_counts.get(index).unwrap_or(&0);
521 let required_quantity = *required_counts.get(index).unwrap_or(&used_quantity);
522 let additional_quantity_needed = stock_entry
523 .available
524 .map(|available| required_quantity.saturating_sub(available))
525 .unwrap_or(0);
526
527 StockRequirement1D {
528 stock_name: stock_entry.name.clone(),
529 stock_length: stock_entry.length,
530 usable_length: stock_entry.usable_length(),
531 cost: stock_entry.cost,
532 available_quantity: stock_entry.available,
533 used_quantity,
534 required_quantity,
535 additional_quantity_needed,
536 }
537 })
538 .collect()
539}
540
541fn default_multistart_runs() -> usize {
542 16
543}
544
545fn default_improvement_rounds() -> usize {
546 24
547}
548
549fn default_column_generation_rounds() -> usize {
550 32
551}
552
553fn default_exact_pattern_limit() -> usize {
554 25_000
555}
556
557fn default_auto_exact_max_types() -> usize {
558 14
559}
560
561fn default_auto_exact_max_quantity() -> usize {
562 96
563}
564
565#[cfg(test)]
566mod tests {
567 use serde_json::json;
568
569 use super::*;
570
571 fn sample_problem() -> OneDProblem {
572 OneDProblem {
573 stock: vec![Stock1D {
574 name: "bar".to_string(),
575 length: 10,
576 kerf: 1,
577 trim: 2,
578 cost: 1.0,
579 available: None,
580 }],
581 demands: vec![
582 CutDemand1D { name: "leg".to_string(), length: 4, quantity: 2 },
583 CutDemand1D { name: "brace".to_string(), length: 3, quantity: 1 },
584 ],
585 }
586 }
587
588 #[test]
589 fn serde_defaults_fill_in_optional_stock_and_option_fields() {
590 let stock: Stock1D =
591 serde_json::from_value(json!({ "name": "bar", "length": 12 })).expect("stock");
592 assert_eq!(stock.cost, 1.0);
593 assert_eq!(stock.kerf, 0);
594 assert_eq!(stock.trim, 0);
595 assert_eq!(stock.available, None);
596
597 let options: OneDOptions = serde_json::from_value(json!({})).expect("options");
598 assert_eq!(options, OneDOptions::default());
599 }
600
601 #[test]
602 fn validation_rejects_missing_or_invalid_one_d_inputs() {
603 let missing_stock = OneDProblem { stock: Vec::new(), demands: sample_problem().demands };
604 assert!(matches!(
605 missing_stock.validate(),
606 Err(BinPackingError::InvalidInput(message))
607 if message == "at least one stock entry is required"
608 ));
609
610 let missing_demands = OneDProblem { stock: sample_problem().stock, demands: Vec::new() };
611 assert!(matches!(
612 missing_demands.validate(),
613 Err(BinPackingError::InvalidInput(message))
614 if message == "at least one demand entry is required"
615 ));
616
617 let zero_length_stock = OneDProblem {
618 stock: vec![Stock1D {
619 name: "bar".to_string(),
620 length: 0,
621 kerf: 0,
622 trim: 0,
623 cost: 1.0,
624 available: None,
625 }],
626 demands: vec![CutDemand1D { name: "cut".to_string(), length: 1, quantity: 1 }],
627 };
628 assert!(matches!(
629 zero_length_stock.validate(),
630 Err(BinPackingError::InvalidInput(message))
631 if message == "stock `bar` must have a positive length"
632 ));
633
634 let trimmed_away_stock = OneDProblem {
635 stock: vec![Stock1D {
636 name: "bar".to_string(),
637 length: 5,
638 kerf: 0,
639 trim: 5,
640 cost: 1.0,
641 available: None,
642 }],
643 demands: vec![CutDemand1D { name: "cut".to_string(), length: 1, quantity: 1 }],
644 };
645 assert!(matches!(
646 trimmed_away_stock.validate(),
647 Err(BinPackingError::InvalidInput(message))
648 if message == "stock `bar` has no usable length after trim"
649 ));
650
651 let zero_length_demand = OneDProblem {
652 stock: vec![Stock1D {
653 name: "bar".to_string(),
654 length: 5,
655 kerf: 0,
656 trim: 0,
657 cost: 1.0,
658 available: None,
659 }],
660 demands: vec![CutDemand1D { name: "cut".to_string(), length: 0, quantity: 1 }],
661 };
662 assert!(matches!(
663 zero_length_demand.validate(),
664 Err(BinPackingError::InvalidInput(message))
665 if message == "demand `cut` must have a positive length"
666 ));
667
668 let zero_quantity_demand = OneDProblem {
669 stock: vec![Stock1D {
670 name: "bar".to_string(),
671 length: 5,
672 kerf: 0,
673 trim: 0,
674 cost: 1.0,
675 available: None,
676 }],
677 demands: vec![CutDemand1D { name: "cut".to_string(), length: 1, quantity: 0 }],
678 };
679 assert!(matches!(
680 zero_quantity_demand.validate(),
681 Err(BinPackingError::InvalidInput(message))
682 if message == "demand `cut` must have a positive quantity"
683 ));
684 }
685
686 #[test]
689 fn validation_rejects_lengths_above_max_dimension() {
690 let oversized_stock = OneDProblem {
691 stock: vec![Stock1D {
692 name: "huge".to_string(),
693 length: MAX_DIMENSION + 1,
694 kerf: 0,
695 trim: 0,
696 cost: 1.0,
697 available: None,
698 }],
699 demands: vec![CutDemand1D { name: "cut".to_string(), length: 1, quantity: 1 }],
700 };
701 assert!(matches!(
702 oversized_stock.validate(),
703 Err(BinPackingError::InvalidInput(message))
704 if message.starts_with("stock `huge` length")
705 ));
706
707 let oversized_kerf = OneDProblem {
708 stock: vec![Stock1D {
709 name: "bar".to_string(),
710 length: 100,
711 kerf: MAX_DIMENSION + 1,
712 trim: 0,
713 cost: 1.0,
714 available: None,
715 }],
716 demands: vec![CutDemand1D { name: "cut".to_string(), length: 1, quantity: 1 }],
717 };
718 assert!(matches!(
719 oversized_kerf.validate(),
720 Err(BinPackingError::InvalidInput(message))
721 if message.starts_with("stock `bar` kerf")
722 ));
723
724 let oversized_trim = OneDProblem {
725 stock: vec![Stock1D {
726 name: "bar".to_string(),
727 length: 100,
728 kerf: 0,
729 trim: MAX_DIMENSION + 1,
730 cost: 1.0,
731 available: None,
732 }],
733 demands: vec![CutDemand1D { name: "cut".to_string(), length: 1, quantity: 1 }],
734 };
735 assert!(matches!(
736 oversized_trim.validate(),
737 Err(BinPackingError::InvalidInput(message))
738 if message.starts_with("stock `bar` trim")
739 ));
740
741 let oversized_demand = OneDProblem {
742 stock: vec![Stock1D {
743 name: "bar".to_string(),
744 length: 100,
745 kerf: 0,
746 trim: 0,
747 cost: 1.0,
748 available: None,
749 }],
750 demands: vec![CutDemand1D {
751 name: "cut".to_string(),
752 length: MAX_DIMENSION + 1,
753 quantity: 1,
754 }],
755 };
756 assert!(matches!(
757 oversized_demand.validate(),
758 Err(BinPackingError::InvalidInput(message))
759 if message.starts_with("demand `cut` length")
760 ));
761 }
762
763 #[test]
764 fn feasibility_and_piece_expansion_follow_declared_demands() {
765 let feasible = sample_problem();
766 feasible.validate().expect("sample input should validate");
767 feasible.ensure_feasible_demands().expect("sample input should be feasible");
768 assert_eq!(feasible.total_quantity(), 3);
769
770 let pieces = feasible.expanded_pieces();
771 assert_eq!(pieces.len(), 3);
772 assert_eq!(pieces[0].demand_index, 0);
773 assert_eq!(pieces[0].name, "leg");
774 assert_eq!(pieces[2].demand_index, 1);
775 assert_eq!(pieces[2].name, "brace");
776
777 let infeasible = OneDProblem {
778 stock: feasible.stock.clone(),
779 demands: vec![CutDemand1D { name: "oversized".to_string(), length: 9, quantity: 1 }],
780 };
781 assert!(matches!(
782 infeasible.ensure_feasible_demands(),
783 Err(BinPackingError::Infeasible1D { item, length })
784 if item == "oversized" && length == 9
785 ));
786 }
787
788 #[test]
789 fn packed_bin_accounts_for_kerf_and_rejects_overflow() {
790 let stock = Stock1D {
791 name: "bar".to_string(),
792 length: 12,
793 kerf: 1,
794 trim: 2,
795 cost: 1.0,
796 available: None,
797 };
798 assert_eq!(stock.usable_length(), 10);
799 assert_eq!(stock.adjusted_capacity(), 11);
800
801 let first = PieceInstance { demand_index: 0, name: "A".to_string(), length: 6 };
802 let second = PieceInstance { demand_index: 1, name: "B".to_string(), length: 3 };
803 let oversized = PieceInstance { demand_index: 2, name: "C".to_string(), length: 4 };
804
805 let mut bin = PackedBin::new(0);
806 assert!(bin.can_fit_piece(&first, &stock));
807 assert_eq!(bin.delta_for_piece(&first, &stock), Some(6));
808 assert!(bin.add_piece(first.clone(), &stock));
809 assert_eq!(bin.used_length(), 6);
810 assert_eq!(bin.remaining_length(&stock), 4);
811
812 assert_eq!(bin.delta_for_piece(&second, &stock), Some(4));
813 assert!(bin.add_piece(second, &stock));
814 assert_eq!(bin.used_length(), 10);
815 assert_eq!(bin.remaining_length(&stock), 0);
816
817 assert!(!bin.can_fit_piece(&oversized, &stock));
818 assert_eq!(bin.delta_for_piece(&oversized, &stock), None);
819 assert!(!bin.add_piece(oversized, &stock));
820 }
821
822 #[test]
823 fn solution_helpers_sort_layouts_and_prefer_exact_ties() {
824 let stock = vec![
825 Stock1D {
826 name: "slow".to_string(),
827 length: 10,
828 kerf: 0,
829 trim: 0,
830 cost: 2.0,
831 available: None,
832 },
833 Stock1D {
834 name: "fast".to_string(),
835 length: 10,
836 kerf: 0,
837 trim: 0,
838 cost: 1.0,
839 available: None,
840 },
841 ];
842
843 let bins = vec![
844 PackedBin {
845 stock_index: 0,
846 pieces: vec![PieceInstance {
847 demand_index: 0,
848 name: "small".to_string(),
849 length: 3,
850 }],
851 occupied_length: 3,
852 },
853 PackedBin {
854 stock_index: 1,
855 pieces: vec![
856 PieceInstance { demand_index: 1, name: "large".to_string(), length: 6 },
857 PieceInstance { demand_index: 2, name: "medium".to_string(), length: 2 },
858 ],
859 occupied_length: 8,
860 },
861 ];
862 let unplaced = vec![
863 PieceInstance { demand_index: 0, name: "tiny".to_string(), length: 1 },
864 PieceInstance { demand_index: 1, name: "big".to_string(), length: 7 },
865 ];
866 let metrics = SolverMetrics1D {
867 iterations: 2,
868 generated_patterns: 0,
869 enumerated_patterns: 0,
870 explored_states: 0,
871 notes: vec!["test".to_string()],
872 };
873
874 let exact = OneDSolution::from_bins(
875 "column_generation",
876 true,
877 Some(2.0),
878 &stock,
879 &bins,
880 &unplaced,
881 metrics.clone(),
882 );
883 assert_eq!(exact.layouts[0].stock_name, "fast");
884 assert_eq!(exact.layouts[1].stock_name, "slow");
885 assert_eq!(exact.unplaced[0].name, "big");
886 assert_eq!(exact.total_cost, 3.0);
887 assert_eq!(exact.total_waste, 9);
888 assert_eq!(exact.stock_requirements.len(), 2);
889 assert_eq!(exact.stock_requirements[0].stock_name, "slow");
890 assert_eq!(exact.stock_requirements[0].used_quantity, 1);
891 assert_eq!(exact.stock_requirements[0].required_quantity, 1);
892 assert_eq!(exact.stock_requirements[1].stock_name, "fast");
893 assert_eq!(exact.stock_requirements[1].used_quantity, 1);
894 assert_eq!(exact.stock_requirements[1].additional_quantity_needed, 0);
895
896 let heuristic = OneDSolution { exact: false, ..exact.clone() };
897 assert!(exact.is_better_than(&heuristic));
898 }
899
900 #[test]
901 fn stock_requirements_can_record_shortfalls_against_inventory() {
902 let stock = vec![Stock1D {
903 name: "bar".to_string(),
904 length: 10,
905 kerf: 0,
906 trim: 0,
907 cost: 1.0,
908 available: Some(1),
909 }];
910 let bins = vec![PackedBin {
911 stock_index: 0,
912 pieces: vec![
913 PieceInstance { demand_index: 0, name: "A".to_string(), length: 5 },
914 PieceInstance { demand_index: 1, name: "B".to_string(), length: 5 },
915 ],
916 occupied_length: 10,
917 }];
918 let mut solution = OneDSolution::from_bins(
919 "first_fit_decreasing",
920 false,
921 None,
922 &stock,
923 &bins,
924 &[],
925 SolverMetrics1D {
926 iterations: 1,
927 generated_patterns: 0,
928 enumerated_patterns: 0,
929 explored_states: 0,
930 notes: Vec::new(),
931 },
932 );
933
934 solution.set_required_stock_counts(&stock, &[2]);
935
936 assert_eq!(solution.stock_requirements.len(), 1);
937 assert_eq!(solution.stock_requirements[0].available_quantity, Some(1));
938 assert_eq!(solution.stock_requirements[0].used_quantity, 1);
939 assert_eq!(solution.stock_requirements[0].required_quantity, 2);
940 assert_eq!(solution.stock_requirements[0].additional_quantity_needed, 1);
941 }
942
943 #[test]
948 fn one_d_is_better_than_tie_breaks_on_each_key() {
949 let stock = vec![Stock1D {
950 name: "bar".to_string(),
951 length: 10,
952 kerf: 0,
953 trim: 0,
954 cost: 1.0,
955 available: None,
956 }];
957 let bins = vec![PackedBin {
958 stock_index: 0,
959 pieces: vec![PieceInstance { demand_index: 0, name: "cut".to_string(), length: 5 }],
960 occupied_length: 5,
961 }];
962 let base = OneDSolution::from_bins(
963 "test",
964 false,
965 None,
966 &stock,
967 &bins,
968 &[],
969 SolverMetrics1D {
970 iterations: 0,
971 generated_patterns: 0,
972 enumerated_patterns: 0,
973 explored_states: 0,
974 notes: Vec::new(),
975 },
976 );
977
978 let more_unplaced = OneDSolution {
980 unplaced: vec![CutAssignment1D { name: "u".to_string(), length: 1 }],
981 ..base.clone()
982 };
983 assert!(base.is_better_than(&more_unplaced));
984 assert!(!more_unplaced.is_better_than(&base));
985
986 let more_stock = OneDSolution { stock_count: base.stock_count + 1, ..base.clone() };
988 assert!(base.is_better_than(&more_stock));
989
990 let more_waste = OneDSolution { total_waste: base.total_waste + 10, ..base.clone() };
992 assert!(base.is_better_than(&more_waste));
993
994 let more_cost = OneDSolution { total_cost: base.total_cost + 1.0, ..base.clone() };
996 assert!(base.is_better_than(&more_cost));
997
998 assert!(!base.is_better_than(&base));
1000 }
1001}