From 2868456dcc578907b3f4ccc1b6eb621bc6293610 Mon Sep 17 00:00:00 2001 From: Leo Sluis Date: Mon, 22 Nov 2021 15:04:50 +0100 Subject: [PATCH] Make packing algorithm cancellable --- .../Algorithms/EB_AFIT.cs | 46 ++++++++-------- .../Algorithms/IPackingAlgorithm.cs | 4 +- .../PackingService.cs | 12 ++++- .../ContainerPackingCancelTests.cs | 54 +++++++++++++++++++ .../ContainerPackingTests.cs | 20 ++++--- 5 files changed, 103 insertions(+), 33 deletions(-) create mode 100644 src/CromulentBisgetti.ContainerPackingTests/ContainerPackingCancelTests.cs diff --git a/src/CromulentBisgetti.ContainerPacking/Algorithms/EB_AFIT.cs b/src/CromulentBisgetti.ContainerPacking/Algorithms/EB_AFIT.cs index c10292c..05ef057 100644 --- a/src/CromulentBisgetti.ContainerPacking/Algorithms/EB_AFIT.cs +++ b/src/CromulentBisgetti.ContainerPacking/Algorithms/EB_AFIT.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; namespace CromulentBisgetti.ContainerPacking.Algorithms { @@ -20,9 +21,9 @@ public class EB_AFIT : IPackingAlgorithm /// The container to pack items into. /// The items to pack. /// The bin packing result. - public AlgorithmPackingResult Run(Container container, List items) + public AlgorithmPackingResult Run(Container container, List items, CancellationToken cancellationToken) { - Initialize(container, items); + Initialize(container, items, cancellationToken); ExecuteIterations(container); Report(container); @@ -41,8 +42,6 @@ public AlgorithmPackingResult Run(Container container, List items) } result.PackedItems = itemsPackedInOrder; - - if (result.UnpackedItems.Count == 0) { @@ -52,13 +51,14 @@ public AlgorithmPackingResult Run(Container container, List items) return result; } - #endregion Public Methods + #endregion Public Methods - #region Private Variables + #region Private Variables - private List itemsToPack; + private List itemsToPack; private List itemsPackedInOrder; private List layers; + private CancellationToken algorithmCancellationToken; private ContainerPackingResult result; private ScrapPad scrapfirst; @@ -70,7 +70,6 @@ public AlgorithmPackingResult Run(Container container, List items) private bool layerDone; private bool packing; private bool packingBest = false; - private bool quit = false; private int bboxi; private int bestIteration; @@ -288,7 +287,7 @@ private void ExecuteIterations(Container container) int layersIndex; decimal bestVolume = 0.0M; - for (int containerOrientationVariant = 1; (containerOrientationVariant <= 6) && !quit; containerOrientationVariant++) + for (int containerOrientationVariant = 1; (containerOrientationVariant <= 6) && !algorithmCancellationToken.IsCancellationRequested; containerOrientationVariant++) { switch (containerOrientationVariant) { @@ -321,7 +320,7 @@ private void ExecuteIterations(Container container) ListCanditLayers(); layers = layers.OrderBy(l => l.LayerEval).ToList(); - for (layersIndex = 1; (layersIndex <= layerListLen) && !quit; layersIndex++) + for (layersIndex = 1; (layersIndex <= layerListLen) && !algorithmCancellationToken.IsCancellationRequested; layersIndex++) { packedVolume = 0.0M; packedy = 0; @@ -347,7 +346,7 @@ private void ExecuteIterations(Container container) packedy = packedy + layerThickness; remainpy = py - packedy; - if (layerinlayer != 0 && !quit) + if (layerinlayer != 0 && !algorithmCancellationToken.IsCancellationRequested) { prepackedy = packedy; preremainpy = remainpy; @@ -365,9 +364,9 @@ private void ExecuteIterations(Container container) } FindLayer(remainpy); - } while (packing && !quit); + } while (packing && !algorithmCancellationToken.IsCancellationRequested); - if ((packedVolume > bestVolume) && !quit) + if ((packedVolume > bestVolume) && !algorithmCancellationToken.IsCancellationRequested) { bestVolume = packedVolume; bestVariant = containerOrientationVariant; @@ -525,10 +524,12 @@ private void FindSmallestZ() /// /// Initializes everything. /// - private void Initialize(Container container, List items) + private void Initialize(Container container, List items, CancellationToken cancellationToken) { itemsToPack = new List(); itemsPackedInOrder = new List(); + algorithmCancellationToken = cancellationToken; + result = new ContainerPackingResult(); // The original code uses 1-based indexing everywhere. This fake entry is added to the beginning @@ -565,13 +566,12 @@ private void Initialize(Container container, List items) scrapfirst.Post = null; packingBest = false; hundredPercentPacked = false; - quit = false; } - /// - /// Lists all possible layer heights by giving a weight value to each of them. - /// - private void ListCanditLayers() + /// + /// Lists all possible layer heights by giving a weight value to each of them. + /// + private void ListCanditLayers() { bool same; decimal exdim = 0; @@ -752,7 +752,7 @@ private void PackLayer() scrapfirst.CumX = px; scrapfirst.CumZ = 0; - for (; !quit;) + for (; !algorithmCancellationToken.IsCancellationRequested;) { FindSmallestZ(); @@ -1037,8 +1037,6 @@ private void PackLayer() /// private void Report(Container container) { - quit = false; - switch (bestVariant) { case 1: @@ -1112,11 +1110,11 @@ private void Report(Container container) remainpz = pz; } - if (!quit) + if (!algorithmCancellationToken.IsCancellationRequested) { FindLayer(remainpy); } - } while (packing && !quit); + } while (packing && !algorithmCancellationToken.IsCancellationRequested); } /// diff --git a/src/CromulentBisgetti.ContainerPacking/Algorithms/IPackingAlgorithm.cs b/src/CromulentBisgetti.ContainerPacking/Algorithms/IPackingAlgorithm.cs index 4213bee..eff5bbc 100644 --- a/src/CromulentBisgetti.ContainerPacking/Algorithms/IPackingAlgorithm.cs +++ b/src/CromulentBisgetti.ContainerPacking/Algorithms/IPackingAlgorithm.cs @@ -1,5 +1,6 @@ using CromulentBisgetti.ContainerPacking.Entities; using System.Collections.Generic; +using System.Threading; namespace CromulentBisgetti.ContainerPacking.Algorithms { @@ -13,7 +14,8 @@ public interface IPackingAlgorithm /// /// The container. /// The items to pack. + /// Algorith will check this token to detect if the current packing attempt should be cancelled. /// The algorithm packing result. - AlgorithmPackingResult Run(Container container, List items); + AlgorithmPackingResult Run(Container container, List items, CancellationToken cancellationToken); } } \ No newline at end of file diff --git a/src/CromulentBisgetti.ContainerPacking/PackingService.cs b/src/CromulentBisgetti.ContainerPacking/PackingService.cs index e7fae8b..1c6a73c 100644 --- a/src/CromulentBisgetti.ContainerPacking/PackingService.cs +++ b/src/CromulentBisgetti.ContainerPacking/PackingService.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; namespace CromulentBisgetti.ContainerPacking @@ -20,7 +21,14 @@ public static class PackingService /// The items to pack. /// The list of algorithm type IDs to use for packing. /// A container packing result with lists of the packed and unpacked items. - public static List Pack(List containers, List itemsToPack, List algorithmTypeIDs) + public static List Pack(List containers, List itemsToPack, List algorithmTypeIDs) + { + var source = new CancellationTokenSource(); + + return Pack(containers, itemsToPack, algorithmTypeIDs, source.Token); + } + + public static List Pack(List containers, List itemsToPack, List algorithmTypeIDs, CancellationToken cancellationToken) { Object sync = new Object { }; List result = new List(); @@ -45,7 +53,7 @@ public static List Pack(List containers, List Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); - AlgorithmPackingResult algorithmResult = algorithm.Run(container, items); + AlgorithmPackingResult algorithmResult = algorithm.Run(container, items, cancellationToken); stopwatch.Stop(); algorithmResult.PackTimeInMilliseconds = stopwatch.ElapsedMilliseconds; diff --git a/src/CromulentBisgetti.ContainerPackingTests/ContainerPackingCancelTests.cs b/src/CromulentBisgetti.ContainerPackingTests/ContainerPackingCancelTests.cs new file mode 100644 index 0000000..1d3445b --- /dev/null +++ b/src/CromulentBisgetti.ContainerPackingTests/ContainerPackingCancelTests.cs @@ -0,0 +1,54 @@ +using CromulentBisgetti.ContainerPacking; +using CromulentBisgetti.ContainerPacking.Algorithms; +using CromulentBisgetti.ContainerPacking.Entities; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +namespace CromulentBisgetti.ContainerPackingTests +{ + [TestClass] + public class ContainerPackingCancelTests + { + [TestMethod] + public async Task LongRunningTest_CanBeCancelled() + { + // One of the longer-running 700 tests, #479 + var itemsToPack = new List + { + new Item(1, 64, 48, 64, 14), + new Item(2, 79, 25, 79, 23), + new Item(3, 89, 85, 89, 19), + new Item(4, 79, 66, 79, 17), + new Item(5, 79, 54, 79, 16), + new Item(6, 115, 95, 115, 11), + new Item(7, 76, 54, 76, 20), + new Item(8, 80, 44, 80, 10), + new Item(9, 66, 33, 66, 15), + new Item(10, 50, 32, 50, 15), + new Item(11, 116, 93, 116, 19), + new Item(12, 113, 64, 113, 11), + }; + var containers = new List + { + new Container(1, 587, 233, 220), + }; + + var source = new CancellationTokenSource(); + + // start the packing task on another thread and give it a bit of time to start + var packingTask = Task.Run(() => + PackingService.Pack(containers, itemsToPack, new List { (int)AlgorithmType.EB_AFIT }, source.Token)); + await Task.Delay(50); + + // then cancel it. Packing should return quickly + source.Cancel(); + + var result = await packingTask; + + var elapsedMilliSec = result[0].AlgorithmPackingResults[0].PackTimeInMilliseconds; + Assert.IsTrue(elapsedMilliSec < 100, $"Expected elapsed time to be less than 100 but found {elapsedMilliSec} msec"); + } + } +} diff --git a/src/CromulentBisgetti.ContainerPackingTests/ContainerPackingTests.cs b/src/CromulentBisgetti.ContainerPackingTests/ContainerPackingTests.cs index 897479c..d9f6f10 100644 --- a/src/CromulentBisgetti.ContainerPackingTests/ContainerPackingTests.cs +++ b/src/CromulentBisgetti.ContainerPackingTests/ContainerPackingTests.cs @@ -6,6 +6,8 @@ using CromulentBisgetti.ContainerPacking; using CromulentBisgetti.ContainerPacking.Entities; using CromulentBisgetti.ContainerPacking.Algorithms; +using System.Globalization; +using System.Diagnostics; namespace CromulentBisgetti.ContainerPackingTests { @@ -19,6 +21,8 @@ public void EB_AFIT_Passes_700_Standard_Reference_Tests() string resourceName = "CromulentBisgetti.ContainerPackingTests.DataFiles.ORLibrary.txt"; Assembly assembly = Assembly.GetExecutingAssembly(); + var decimalPointCulture = CultureInfo.GetCultureInfo("en-us"); + using (Stream stream = assembly.GetManifestResourceStream(resourceName)) { using (StreamReader reader = new StreamReader(stream)) @@ -53,7 +57,9 @@ public void EB_AFIT_Passes_700_Standard_Reference_Tests() containers.Add(new Container(0, Convert.ToDecimal(containerDims[0]), Convert.ToDecimal(containerDims[1]), Convert.ToDecimal(containerDims[2]))); List result = PackingService.Pack(containers, itemsToPack, new List { (int)AlgorithmType.EB_AFIT }); - + + Debug.WriteLine($"Test #{counter} took {result[0].AlgorithmPackingResults[0].PackTimeInMilliseconds}msec"); + // Assert that the number of items we tried to pack equals the number stated in the published reference. Assert.AreEqual(result[0].AlgorithmPackingResults[0].PackedItems.Count + result[0].AlgorithmPackingResults[0].UnpackedItems.Count, Convert.ToDecimal(testResults[1])); @@ -61,13 +67,15 @@ public void EB_AFIT_Passes_700_Standard_Reference_Tests() Assert.AreEqual(result[0].AlgorithmPackingResults[0].PackedItems.Count, Convert.ToDecimal(testResults[2])); // Assert that the packed container volume percentage is equal to the published reference result. - // Make an exception for a couple of tests where this algorithm yields 87.20% and the published result - // was 87.21% (acceptable rounding error). - Assert.IsTrue(result[0].AlgorithmPackingResults[0].PercentContainerVolumePacked == Convert.ToDecimal(testResults[3]) || - (result[0].AlgorithmPackingResults[0].PercentContainerVolumePacked == 87.20M && Convert.ToDecimal(testResults[3]) == 87.21M)); + var actualPercentage = result[0].AlgorithmPackingResults[0].PercentContainerVolumePacked; + var expectedPrecentage = Convert.ToDecimal(testResults[3], decimalPointCulture); + + Assert.IsTrue( + Math.Abs(actualPercentage - expectedPrecentage) < 0.02M, + $"Test #{counter} failed: expected%={expectedPrecentage}; actual%={actualPercentage};"); // Assert that the packed item volume percentage is equal to the published reference result. - Assert.AreEqual(result[0].AlgorithmPackingResults[0].PercentItemVolumePacked, Convert.ToDecimal(testResults[4])); + Assert.AreEqual(result[0].AlgorithmPackingResults[0].PercentItemVolumePacked, Convert.ToDecimal(testResults[4], decimalPointCulture)); counter++; }