Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make packing algorithm cancellable #38

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 22 additions & 24 deletions src/CromulentBisgetti.ContainerPacking/Algorithms/EB_AFIT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

namespace CromulentBisgetti.ContainerPacking.Algorithms
{
Expand All @@ -20,9 +21,9 @@ public class EB_AFIT : IPackingAlgorithm
/// <param name="container">The container to pack items into.</param>
/// <param name="items">The items to pack.</param>
/// <returns>The bin packing result.</returns>
public AlgorithmPackingResult Run(Container container, List<Item> items)
public AlgorithmPackingResult Run(Container container, List<Item> items, CancellationToken cancellationToken)
{
Initialize(container, items);
Initialize(container, items, cancellationToken);
ExecuteIterations(container);
Report(container);

Expand All @@ -41,8 +42,6 @@ public AlgorithmPackingResult Run(Container container, List<Item> items)
}

result.PackedItems = itemsPackedInOrder;



if (result.UnpackedItems.Count == 0)
{
Expand All @@ -52,13 +51,14 @@ public AlgorithmPackingResult Run(Container container, List<Item> items)
return result;
}

#endregion Public Methods
#endregion Public Methods

#region Private Variables
#region Private Variables

private List<Item> itemsToPack;
private List<Item> itemsToPack;
private List<Item> itemsPackedInOrder;
private List<Layer> layers;
private CancellationToken algorithmCancellationToken;
private ContainerPackingResult result;

private ScrapPad scrapfirst;
Expand All @@ -70,7 +70,6 @@ public AlgorithmPackingResult Run(Container container, List<Item> items)
private bool layerDone;
private bool packing;
private bool packingBest = false;
private bool quit = false;

private int bboxi;
private int bestIteration;
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -525,10 +524,12 @@ private void FindSmallestZ()
/// <summary>
/// Initializes everything.
/// </summary>
private void Initialize(Container container, List<Item> items)
private void Initialize(Container container, List<Item> items, CancellationToken cancellationToken)
{
itemsToPack = new List<Item>();
itemsPackedInOrder = new List<Item>();
algorithmCancellationToken = cancellationToken;

result = new ContainerPackingResult();

// The original code uses 1-based indexing everywhere. This fake entry is added to the beginning
Expand Down Expand Up @@ -565,13 +566,12 @@ private void Initialize(Container container, List<Item> items)
scrapfirst.Post = null;
packingBest = false;
hundredPercentPacked = false;
quit = false;
}

/// <summary>
/// Lists all possible layer heights by giving a weight value to each of them.
/// </summary>
private void ListCanditLayers()
/// <summary>
/// Lists all possible layer heights by giving a weight value to each of them.
/// </summary>
private void ListCanditLayers()
{
bool same;
decimal exdim = 0;
Expand Down Expand Up @@ -752,7 +752,7 @@ private void PackLayer()
scrapfirst.CumX = px;
scrapfirst.CumZ = 0;

for (; !quit;)
for (; !algorithmCancellationToken.IsCancellationRequested;)
{
FindSmallestZ();

Expand Down Expand Up @@ -1037,8 +1037,6 @@ private void PackLayer()
/// </summary>
private void Report(Container container)
{
quit = false;

switch (bestVariant)
{
case 1:
Expand Down Expand Up @@ -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);
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using CromulentBisgetti.ContainerPacking.Entities;
using System.Collections.Generic;
using System.Threading;

namespace CromulentBisgetti.ContainerPacking.Algorithms
{
Expand All @@ -13,7 +14,8 @@ public interface IPackingAlgorithm
/// </summary>
/// <param name="container">The container.</param>
/// <param name="items">The items to pack.</param>
/// <param name="cancellationToken">Algorith will check this token to detect if the current packing attempt should be cancelled.</param>
/// <returns>The algorithm packing result.</returns>
AlgorithmPackingResult Run(Container container, List<Item> items);
AlgorithmPackingResult Run(Container container, List<Item> items, CancellationToken cancellationToken);
}
}
12 changes: 10 additions & 2 deletions src/CromulentBisgetti.ContainerPacking/PackingService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;

namespace CromulentBisgetti.ContainerPacking
Expand All @@ -20,7 +21,14 @@ public static class PackingService
/// <param name="itemsToPack">The items to pack.</param>
/// <param name="algorithmTypeIDs">The list of algorithm type IDs to use for packing.</param>
/// <returns>A container packing result with lists of the packed and unpacked items.</returns>
public static List<ContainerPackingResult> Pack(List<Container> containers, List<Item> itemsToPack, List<int> algorithmTypeIDs)
public static List<ContainerPackingResult> Pack(List<Container> containers, List<Item> itemsToPack, List<int> algorithmTypeIDs)
{
var source = new CancellationTokenSource();

return Pack(containers, itemsToPack, algorithmTypeIDs, source.Token);
}

public static List<ContainerPackingResult> Pack(List<Container> containers, List<Item> itemsToPack, List<int> algorithmTypeIDs, CancellationToken cancellationToken)
{
Object sync = new Object { };
List<ContainerPackingResult> result = new List<ContainerPackingResult>();
Expand All @@ -45,7 +53,7 @@ public static List<ContainerPackingResult> Pack(List<Container> 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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Item>
{
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<Container>
{
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> { (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");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
using CromulentBisgetti.ContainerPacking;
using CromulentBisgetti.ContainerPacking.Entities;
using CromulentBisgetti.ContainerPacking.Algorithms;
using System.Globalization;
using System.Diagnostics;

namespace CromulentBisgetti.ContainerPackingTests
{
Expand All @@ -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))
Expand Down Expand Up @@ -53,21 +57,25 @@ 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<ContainerPackingResult> result = PackingService.Pack(containers, itemsToPack, new List<int> { (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]));

// Assert that the number of items successfully packed equals the number stated in the published reference.
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++;
}
Expand Down