cut_optimizer_1d/
lib.rs

1//! cut-optimizer-1d is an optimizer library that attempts to figure out an optimal cutting of
2//! linear cut lengths from linear stock lengths with the least amount of waste.
3//! It uses genetic algorithms and multiple heuristics to solve the problem.
4
5#![deny(missing_docs)]
6
7mod basic;
8mod genetic;
9
10#[cfg(test)]
11mod tests;
12
13use basic::BasicBin;
14use fnv::FnvHashSet;
15use genetic::population::Population;
16use genetic::unit::Unit;
17use rand::prelude::*;
18use rand::seq::SliceRandom;
19use std::borrow::Borrow;
20use std::cmp;
21use std::hash::{Hash, Hasher};
22
23#[cfg(feature = "serialize")]
24use serde::{Deserialize, Serialize};
25
26/// A rectangular piece that needs to be cut from a stock piece.
27#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
28#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
29#[derive(Clone, Debug)]
30pub struct CutPiece {
31    /// Quantity of this cut piece.
32    pub quantity: usize,
33
34    /// ID to be used by the caller to match up result cut pieces
35    /// with the original cut piece. This ID has no meaning to the
36    /// optimizer so it can be set to `None` if not needed.
37    pub external_id: Option<usize>,
38
39    /// Length of this cut piece.
40    pub length: usize,
41}
42
43#[derive(Clone, Debug)]
44pub(crate) struct CutPieceWithId {
45    pub(crate) id: usize,
46    pub(crate) external_id: Option<usize>,
47    pub(crate) length: usize,
48}
49
50impl Hash for CutPieceWithId {
51    fn hash<H: Hasher>(&self, state: &mut H) {
52        self.id.hash(state);
53    }
54}
55impl PartialEq for CutPieceWithId {
56    fn eq(&self, other: &CutPieceWithId) -> bool {
57        self.id == other.id
58    }
59}
60impl Eq for CutPieceWithId {}
61
62#[derive(Clone, Debug)]
63pub(crate) struct UsedCutPiece {
64    pub(crate) id: usize,
65    pub(crate) external_id: Option<usize>,
66    pub(crate) start: usize,
67    pub(crate) end: usize,
68}
69
70impl UsedCutPiece {
71    pub(crate) fn length(&self) -> usize {
72        self.end - self.start
73    }
74}
75
76impl PartialEq for UsedCutPiece {
77    fn eq(&self, other: &UsedCutPiece) -> bool {
78        self.id == other.id
79    }
80}
81impl Eq for UsedCutPiece {}
82
83impl From<&UsedCutPiece> for CutPieceWithId {
84    fn from(used_cut_piece: &UsedCutPiece) -> Self {
85        Self {
86            id: used_cut_piece.id,
87            external_id: used_cut_piece.external_id,
88            length: used_cut_piece.end - used_cut_piece.start,
89        }
90    }
91}
92
93impl From<&UsedCutPiece> for ResultCutPiece {
94    fn from(used_cut_piece: &UsedCutPiece) -> Self {
95        Self {
96            external_id: used_cut_piece.external_id,
97            start: used_cut_piece.start,
98            end: used_cut_piece.end,
99        }
100    }
101}
102
103/// A cut piece that has been placed in a solution by the optimizer.
104#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
105#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
106#[derive(Clone, Debug, PartialEq, Eq)]
107pub struct ResultCutPiece {
108    /// ID that matches the one on the cut piece that was passed to the optimizer.
109    pub external_id: Option<usize>,
110
111    /// Start location of this cut piece within the stock piece.
112    pub start: usize,
113
114    /// End location of this cut piece within the stock piece.
115    pub end: usize,
116}
117
118/// A rectangular stock piece that is available to cut one or more
119/// cut pieces from.
120#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
121#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
122#[derive(Hash, Copy, Clone, Debug, Eq, PartialEq)]
123pub struct StockPiece {
124    /// Length of rectangular stock piece.
125    pub length: usize,
126
127    /// Price to use to optimize for price when not all stock pieces are the same price per unit
128    /// length. If optimizing for less waste instead, price can be set to 0 for all stock pieces.
129    pub price: usize,
130
131    /// Quantity of this stock piece available for optimization. `None` means infinite quantity.
132    pub quantity: Option<usize>,
133}
134
135impl StockPiece {
136    /// Decrement the quantity of this stock piece. If quantity is `None` it will remain `None`.
137    fn dec_quantity(&mut self) {
138        if let Some(ref mut quantity) = self.quantity {
139            *quantity -= 1;
140        }
141    }
142
143    /// Increment the quantity of this stock piece. If quantity is `None` it will remain `None`.
144    fn inc_quantity(&mut self) {
145        if let Some(ref mut quantity) = self.quantity {
146            *quantity += 1;
147        }
148    }
149}
150
151/// Stock piece that was used by the optimizer to get one or more cut pieces.
152#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
153#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
154#[derive(Clone, Debug)]
155pub struct ResultStockPiece {
156    /// Length of this stock piece.
157    pub length: usize,
158
159    /// Cut pieces to cut from this stock piece.
160    pub cut_pieces: Vec<ResultCutPiece>,
161
162    /// Price of stock piece.
163    pub price: usize,
164}
165
166/// Represents a bin used for bin-packing.
167trait Bin {
168    /// Creates a new `Bin`.
169    fn new(length: usize, blade_width: usize, price: usize) -> Self;
170
171    /// Computes the fitness of this `Bin` on a scale of 0.0 to 1.0, with 1.0 being the most fit.
172    fn fitness(&self) -> f64;
173
174    fn price(&self) -> usize;
175
176    /// Removes `UsedCutPiece`s from this `Bin` and returns how many were removed.
177    fn remove_cut_pieces<I>(&mut self, cut_pieces: I) -> usize
178    where
179        I: Iterator,
180        I::Item: Borrow<UsedCutPiece>;
181
182    /// Returns an iterator over the `UsedCutPiece`s in this `Bin`.
183    fn cut_pieces(&self) -> std::slice::Iter<'_, UsedCutPiece>;
184
185    /// Inserts the `CutPieceWithId` into this `Bin`. Returns whether the insert succeeded.
186    fn insert_cut_piece(&mut self, cut_piece: &CutPieceWithId) -> bool;
187
188    /// Returns whether the `StockPiece` is equivalent to this `Bin`.
189    fn matches_stock_piece(&self, stock_piece: &StockPiece) -> bool;
190}
191
192struct OptimizerUnit<'a, B>
193where
194    B: Bin,
195{
196    bins: Vec<B>,
197
198    // All of the possible stock pieces. It remains constant.
199    possible_stock_pieces: &'a [StockPiece],
200
201    // Stock pieces that are currently available to use for new bins.
202    available_stock_pieces: Vec<StockPiece>,
203
204    // Cut pieces that couldn't be added to bins.
205    unused_cut_pieces: FnvHashSet<CutPieceWithId>,
206
207    blade_width: usize,
208}
209
210impl<'a, B> Clone for OptimizerUnit<'a, B>
211where
212    B: Bin + Clone,
213{
214    fn clone(&self) -> Self {
215        Self {
216            bins: self.bins.clone(),
217            possible_stock_pieces: self.possible_stock_pieces,
218            available_stock_pieces: self.available_stock_pieces.to_vec(),
219            unused_cut_pieces: self.unused_cut_pieces.clone(),
220            blade_width: self.blade_width,
221        }
222    }
223}
224
225impl<'a, B> OptimizerUnit<'a, B>
226where
227    B: Bin,
228{
229    fn new<R>(
230        possible_stock_pieces: &'a [StockPiece],
231        cut_pieces: &[&CutPieceWithId],
232        blade_width: usize,
233        rng: &mut R,
234    ) -> Result<OptimizerUnit<'a, B>>
235    where
236        R: Rng + ?Sized,
237    {
238        let mut unit = OptimizerUnit {
239            bins: Vec::new(),
240            possible_stock_pieces,
241            available_stock_pieces: possible_stock_pieces.to_vec(),
242            unused_cut_pieces: Default::default(),
243            blade_width,
244        };
245
246        for cut_piece in cut_pieces {
247            if !unit.insert_cut_piece(cut_piece, rng) {
248                unit.unused_cut_pieces.insert((*cut_piece).clone());
249            }
250        }
251
252        Ok(unit)
253    }
254
255    pub(crate) fn generate_initial_units(
256        possible_stock_pieces: &'a [StockPiece],
257        mut cut_pieces: Vec<&CutPieceWithId>,
258        blade_width: usize,
259        random_seed: u64,
260    ) -> Result<Vec<OptimizerUnit<'a, B>>> {
261        let length_set: FnvHashSet<usize> = cut_pieces.iter().map(|p| p.length).collect();
262        let unique_cut_lengths = length_set.len();
263
264        let num_units = {
265            let denom = if cut_pieces.len() > 1 {
266                (cut_pieces.len() as f64).log10()
267            } else {
268                1.0
269            };
270
271            cmp::max(
272                10,
273                (cut_pieces.len() as f64 / denom + ((unique_cut_lengths - 1) * 10) as f64) as usize,
274            )
275        };
276        let mut units = Vec::with_capacity(num_units);
277        let mut rng: StdRng = SeedableRng::seed_from_u64(random_seed);
278
279        // Sorted by length ascending
280        cut_pieces.sort_by_key(|p| p.length);
281        units.push(OptimizerUnit::new(
282            possible_stock_pieces,
283            &cut_pieces,
284            blade_width,
285            &mut rng,
286        )?);
287
288        // Sorted by length descending
289        cut_pieces.sort_by_key(|p| cmp::Reverse(p.length));
290        units.push(OptimizerUnit::new(
291            possible_stock_pieces,
292            &cut_pieces,
293            blade_width,
294            &mut rng,
295        )?);
296
297        // The rest randomly ordered
298        for _ in 0..num_units - units.len() {
299            cut_pieces.shuffle(&mut rng);
300            units.push(OptimizerUnit::new(
301                possible_stock_pieces,
302                &cut_pieces,
303                blade_width,
304                &mut rng,
305            )?);
306        }
307
308        Ok(units)
309    }
310
311    fn insert_cut_piece<R>(&mut self, cut_piece: &CutPieceWithId, rng: &mut R) -> bool
312    where
313        R: Rng + ?Sized,
314    {
315        for bin in self.bins.iter_mut() {
316            if bin.insert_cut_piece(cut_piece) {
317                return true;
318            }
319        }
320
321        self.add_to_new_bin(cut_piece, rng)
322    }
323
324    fn add_to_new_bin<R>(&mut self, cut_piece: &CutPieceWithId, rng: &mut R) -> bool
325    where
326        R: Rng + ?Sized,
327    {
328        let stock_pieces = self
329            .available_stock_pieces
330            .iter_mut()
331            .filter(|stock_piece| {
332                stock_piece.quantity != Some(0) && cut_piece.length <= stock_piece.length
333            });
334
335        match stock_pieces.choose(rng) {
336            Some(stock_piece) => {
337                stock_piece.dec_quantity();
338
339                let mut bin = B::new(stock_piece.length, self.blade_width, stock_piece.price);
340                if !bin.insert_cut_piece(cut_piece) {
341                    return false;
342                }
343                self.bins.push(bin);
344                true
345            }
346            None => false,
347        }
348    }
349
350    fn crossover<R>(&self, other: &OptimizerUnit<'a, B>, rng: &mut R) -> OptimizerUnit<'a, B>
351    where
352        R: Rng + ?Sized,
353        B: Clone,
354    {
355        // If there aren't multiple bins we can't do a crossover,
356        // so just return a clone of this unit.
357        if self.bins.len() < 2 && other.bins.len() < 2 {
358            return self.clone();
359        }
360
361        // Randomly select insertion point and range of bins to inject into the new unit.
362        let cross_dest = rng.gen_range(0..=self.bins.len());
363        let cross_src_start = rng.gen_range(0..other.bins.len());
364        let cross_src_end = rng.gen_range(cross_src_start + 1..=other.bins.len());
365
366        // Create a new unit with the bins of `self` and some of the bins of `other` injected in.
367        let mut new_unit = OptimizerUnit {
368            bins: (&self.bins[..cross_dest])
369                .iter()
370                .chain((&other.bins[cross_src_start..cross_src_end]).iter())
371                .chain((&self.bins[cross_dest..]).iter())
372                .cloned()
373                .collect(),
374            possible_stock_pieces: self.possible_stock_pieces,
375            // Make all possible stock pieces available initially. Quantities will be updated below.
376            available_stock_pieces: self.possible_stock_pieces.to_vec(),
377            // Start with no unused cut pieces, and update below.
378            unused_cut_pieces: Default::default(),
379            blade_width: self.blade_width,
380        };
381
382        let mut unused_cut_pieces = self.unused_cut_pieces.clone();
383
384        // Update available stock piece quantities based on the injected bins.
385        other.bins[cross_src_start..cross_src_end]
386            .iter()
387            .for_each(|bin| {
388                if let Some(ref mut stock_piece) = new_unit
389                    .available_stock_pieces
390                    .iter_mut()
391                    .find(|sp| bin.matches_stock_piece(sp))
392                {
393                    // Remove the injected cut pieces from the unused set.
394                    for cut_piece in bin.cut_pieces() {
395                        unused_cut_pieces.remove(&cut_piece.into());
396                    }
397                    stock_piece.dec_quantity();
398                } else {
399                    panic!("Attempt to inject invalid bin in crossover operation. This shouldn't happen, and means there is a bug in the code.");
400                }
401            });
402
403        // Remove injected cut pieces from all non-injected bins,
404        // and update the available stock piece quantities.
405        for i in (0..cross_dest)
406            .chain((cross_dest + cross_src_end - cross_src_start)..new_unit.bins.len())
407            .rev()
408        {
409            let bin = &mut new_unit.bins[i];
410            let stock_piece = new_unit
411                .available_stock_pieces
412                .iter_mut()
413                .find(|sp| sp.quantity != Some(0) && bin.matches_stock_piece(sp));
414            let injected_cut_pieces = (&other.bins[cross_src_start..cross_src_end])
415                .iter()
416                .flat_map(Bin::cut_pieces);
417            let num_removed_cut_pieces = bin.remove_cut_pieces(injected_cut_pieces);
418
419            if let (0, Some(stock_piece)) = (num_removed_cut_pieces, stock_piece) {
420                // No cut pieces were removed from this bin, so we'll keep it
421                // and decrement the quantity of the available stock piece.
422                stock_piece.dec_quantity();
423            } else {
424                // Either there were no available stock pieces, or there
425                // were cut pieces removed from this bin so we'll remove it
426                // and add its cut pieces to the unused set.
427                for cut_piece in bin.cut_pieces() {
428                    unused_cut_pieces.insert(cut_piece.into());
429                }
430                new_unit.bins.remove(i);
431            }
432        }
433
434        // Try to add all unused cut pieces.
435        unused_cut_pieces.retain(|cut_piece| !new_unit.insert_cut_piece(cut_piece, rng));
436        new_unit.unused_cut_pieces = unused_cut_pieces;
437
438        // Remove bins that don't have cut pieces.
439        for i in (0..new_unit.bins.len()).rev() {
440            if new_unit.bins[i].cut_pieces().next().is_none() {
441                let bin = &mut new_unit.bins[i];
442                if let Some(ref mut stock_piece) = new_unit
443                    .available_stock_pieces
444                    .iter_mut()
445                    .find(|sp| sp.quantity != Some(0) && bin.matches_stock_piece(sp))
446                {
447                    // Make this stock piece available to use again.
448                    stock_piece.inc_quantity();
449                }
450                new_unit.bins.remove(i);
451            }
452        }
453
454        new_unit
455    }
456
457    // Randomly apply a mutation to this unit.
458    fn mutate<R>(&mut self, rng: &mut R)
459    where
460        R: Rng + ?Sized,
461    {
462        if !self.bins.is_empty() && rng.gen_range(0..20) == 1 {
463            self.inversion(rng)
464        }
465    }
466
467    // Reverse order of a random range of bins.
468    fn inversion<R>(&mut self, rng: &mut R)
469    where
470        R: Rng + ?Sized,
471    {
472        let start = rng.gen_range(0..self.bins.len());
473        let end = rng.gen_range(start..self.bins.len());
474        self.bins[start..end].reverse();
475    }
476}
477
478impl<'a, B> Unit for OptimizerUnit<'a, B>
479where
480    B: Bin + Send + Clone,
481{
482    fn fitness(&self) -> f64 {
483        let fitness = if self.bins.is_empty() {
484            0.0
485        } else {
486            self.bins.iter().fold(0.0, |acc, b| acc + b.fitness()) / self.bins.len() as f64
487        };
488
489        if self.unused_cut_pieces.is_empty() {
490            fitness
491        } else {
492            // If there are unused cut pieces, the fitness is below 0 because it's not a valid
493            // solution.
494            fitness - 1.0
495        }
496    }
497
498    fn breed_with<R>(&self, other: &OptimizerUnit<'a, B>, rng: &mut R) -> OptimizerUnit<'a, B>
499    where
500        R: Rng + ?Sized,
501    {
502        let mut new_unit = self.crossover(other, rng);
503        new_unit.mutate(rng);
504        new_unit
505    }
506}
507
508/// Error while optimizing.
509#[derive(Debug)]
510pub enum Error {
511    /// There was no stock piece that could contain this demand piece.
512    NoFitForCutPiece(CutPiece),
513}
514fn no_fit_for_cut_piece_error(cut_piece: &CutPieceWithId) -> Error {
515    Error::NoFitForCutPiece(CutPiece {
516        quantity: 1,
517        external_id: cut_piece.external_id,
518        length: cut_piece.length,
519    })
520}
521type Result<T> = std::result::Result<T, Error>;
522
523/// A valid solution to an optimization.
524#[cfg_attr(feature = "serialize", derive(Deserialize, Serialize))]
525#[cfg_attr(feature = "serialize", serde(rename_all = "camelCase"))]
526pub struct Solution {
527    /// Fitness score for this solution.
528    /// Ranges between 0.0 and 1.0 inclusive, with 1.0 being a perfect solution with no waste.
529    pub fitness: f64,
530
531    /// The stock pieces that were used for this solution, each containing the demand piece layout.
532    pub stock_pieces: Vec<ResultStockPiece>,
533
534    #[cfg_attr(feature = "serialize", serde(skip))]
535    price: usize,
536}
537
538/// Optimizer for optimizing rectangular cut pieces from rectangular
539/// stock pieces.
540pub struct Optimizer {
541    stock_pieces: Vec<StockPiece>,
542    cut_pieces: Vec<CutPieceWithId>,
543    cut_width: usize,
544    random_seed: u64,
545    allow_mixed_stock_sizes: bool,
546}
547
548impl Default for Optimizer {
549    fn default() -> Self {
550        Self {
551            stock_pieces: Default::default(),
552            cut_pieces: Default::default(),
553            cut_width: Default::default(),
554            random_seed: Default::default(),
555            allow_mixed_stock_sizes: true,
556        }
557    }
558}
559
560impl Optimizer {
561    /// Create a new optimizer.
562    pub fn new() -> Self {
563        Default::default()
564    }
565
566    /// Add a stock piece that the optimizer can use to optimize cut pieces.
567    /// If the same stock piece is added multiple times, the quantities will be
568    /// summed up. If any have a `None` quantity, the quantity on other equivalent
569    /// pieces will be ignored.
570    pub fn add_stock_piece(&mut self, stock_piece: StockPiece) -> &mut Self {
571        let mut existing_stock_piece = self
572            .stock_pieces
573            .iter_mut()
574            .find(|sp| sp.length == stock_piece.length && sp.price == stock_piece.price);
575
576        if let Some(ref mut existing_stock_piece) = existing_stock_piece {
577            match (&mut existing_stock_piece.quantity, stock_piece.quantity) {
578                (Some(ref mut existing_quantity), Some(quantity)) => {
579                    // If there is already a stock piece that is the same except the quantity, add
580                    // to the quantity.
581                    *existing_quantity += quantity;
582                }
583                _ => {
584                    // `None` quantity means infinite, so if either is `None` we want it to be
585                    // `None`.
586                    existing_stock_piece.quantity = None;
587                }
588            }
589        } else {
590            // A stock piece like this hasn't yet been added so let's do it.
591            self.stock_pieces.push(stock_piece);
592        }
593
594        self
595    }
596
597    /// Add a stock pieces that the optimizer can use to optimize cut pieces.
598    /// If the same stock piece is added multiple times, the quantities will be
599    /// summed up. If any have a `None` quantity, the quantity on other equivalent
600    /// pieces will be ignored.
601    pub fn add_stock_pieces<I>(&mut self, stock_pieces: I) -> &mut Self
602    where
603        I: IntoIterator<Item = StockPiece>,
604    {
605        stock_pieces.into_iter().for_each(|sp| {
606            self.add_stock_piece(sp);
607        });
608        self
609    }
610
611    /// Add a desired cut piece that you need cut from a stock piece.
612    pub fn add_cut_piece(&mut self, cut_piece: CutPiece) -> &mut Self {
613        for _ in 0..cut_piece.quantity {
614            let cut_piece = CutPieceWithId {
615                id: self.cut_pieces.len(),
616                external_id: cut_piece.external_id,
617                length: cut_piece.length,
618            };
619
620            self.cut_pieces.push(cut_piece);
621        }
622
623        self
624    }
625
626    /// Add desired cut pieces that you need cut from a stock piece.
627    pub fn add_cut_pieces<I>(&mut self, cut_pieces: I) -> &mut Self
628    where
629        I: IntoIterator<Item = CutPiece>,
630    {
631        cut_pieces.into_iter().for_each(|dp| {
632            self.add_cut_piece(dp);
633        });
634        self
635    }
636
637    /// Set the width of the cut to use between cut pieces. This could
638    /// represent blade or kerf thickness.
639    pub fn set_cut_width(&mut self, cut_width: usize) -> &mut Self {
640        self.cut_width = cut_width;
641        self
642    }
643
644    /// Set the random seed used by the genetic algorithms in the optimizer. Using
645    /// the same random seed will give you the same result for the same input.
646    pub fn set_random_seed(&mut self, seed: u64) -> &mut Self {
647        self.random_seed = seed;
648        self
649    }
650
651    /// Set whether the optimizer should allow mixed sized stock pieces in the results.
652    /// If set to false, and multiple stock sizes are given, only one stock size will be used in
653    /// the results.
654    pub fn allow_mixed_stock_sizes(&mut self, allow: bool) -> &mut Self {
655        self.allow_mixed_stock_sizes = allow;
656        self
657    }
658
659    /// Perform optimization
660    pub fn optimize<F>(&self, progress_callback: F) -> Result<Solution>
661    where
662        F: Fn(f64),
663    {
664        // If there are no cut pieces, there's nothing to optimize.
665        if self.cut_pieces.is_empty() {
666            return Ok(Solution {
667                fitness: 1.0,
668                stock_pieces: Vec::new(),
669                price: 0,
670            });
671        }
672
673        let size_set: FnvHashSet<usize> = self.stock_pieces.iter().map(|sp| sp.length).collect();
674
675        let num_runs = size_set.len() + if self.allow_mixed_stock_sizes { 1 } else { 0 };
676        let callback = |progress| {
677            progress_callback(progress / num_runs as f64);
678        };
679
680        let mut best_result = if self.allow_mixed_stock_sizes {
681            // Optimize with all stock sizes
682            self.optimize_with_stock_pieces::<BasicBin, _>(&self.stock_pieces.clone(), &callback)
683        } else {
684            // We're not allowing mixed sizes so just give an error result
685            // here. Each stock size will be optimized separately below.
686            // Note: it's safe to assume `self.cut_pieces` isn't empty because
687            // that's checked at the beginning of this function.
688            Err(no_fit_for_cut_piece_error(&self.cut_pieces[0]))
689        };
690
691        // Optimize each stock size separately and see if any have better result than
692        // when optimizing with all stock sizes.
693        for (i, length) in size_set.iter().enumerate() {
694            let stock_pieces: Vec<StockPiece> = self
695                .stock_pieces
696                .iter()
697                .filter(|sp| sp.length == *length)
698                .cloned()
699                .collect();
700
701            let completed_runs = i + 1;
702            if let Ok(solution) =
703                self.optimize_with_stock_pieces::<BasicBin, _>(&stock_pieces, &|progress| {
704                    progress_callback((completed_runs as f64 + progress) / num_runs as f64);
705                })
706            {
707                match best_result {
708                    Ok(ref best_solution) => {
709                        // Use the lower-priced solution, but if the prices are the same, use the
710                        // solution with the higher fitness score.
711                        if solution.fitness < 0.0 || best_solution.fitness < 0.0 {
712                            if solution.fitness > best_solution.fitness {
713                                best_result = Ok(solution);
714                            }
715                        } else if solution.price < best_solution.price
716                            || (solution.price == best_solution.price
717                                && solution.fitness > best_solution.fitness)
718                        {
719                            best_result = Ok(solution);
720                        }
721                    }
722                    Err(_) => best_result = Ok(solution),
723                }
724            }
725        }
726
727        if let Ok(ref mut solution) = &mut best_result {
728            solution
729                .stock_pieces
730                .sort_by_key(|p| cmp::Reverse(p.length));
731        };
732
733        best_result
734    }
735
736    fn optimize_with_stock_pieces<B, F>(
737        &self,
738        stock_pieces: &[StockPiece],
739        progress_callback: &F,
740    ) -> Result<Solution>
741    where
742        B: Bin + Clone + Send + Into<ResultStockPiece>,
743        F: Fn(f64),
744    {
745        let cut_pieces: Vec<&CutPieceWithId> = self.cut_pieces.iter().collect();
746
747        let units: Vec<OptimizerUnit<B>> = OptimizerUnit::generate_initial_units(
748            stock_pieces,
749            cut_pieces,
750            self.cut_width,
751            self.random_seed,
752        )?;
753
754        let population_size = units.len();
755        let mut result_units = Population::new(units)
756            .set_size(population_size)
757            .set_rand_seed(self.random_seed)
758            .set_breed_factor(0.5)
759            .set_survival_factor(0.6)
760            .epochs(100, progress_callback)
761            .finish();
762
763        let best_unit = &mut result_units[0];
764        if !best_unit.unused_cut_pieces.is_empty() {
765            return Err(no_fit_for_cut_piece_error(
766                best_unit.unused_cut_pieces.iter().next().unwrap(),
767            ));
768        }
769
770        let fitness = best_unit.fitness();
771        let price = best_unit.bins.iter().map(|bin| bin.price()).sum();
772
773        let used_stock_pieces: Vec<ResultStockPiece> =
774            best_unit.bins.drain(..).map(Into::into).collect();
775
776        Ok(Solution {
777            fitness,
778            stock_pieces: used_stock_pieces,
779            price,
780        })
781    }
782}