Skip to content

Commit

Permalink
Backport speed improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
dvdoug committed Sep 14, 2019
1 parent deaf8a0 commit bc850d6
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 83 deletions.
38 changes: 16 additions & 22 deletions src/OrientatedItem.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
*
* @author Doug Wright
*/

namespace DVDoug\BoxPacker;

use JsonSerializable;
Expand Down Expand Up @@ -37,9 +36,14 @@ class OrientatedItem implements JsonSerializable
protected $depth;

/**
* @var float[]
* @var int
*/
protected $surfaceFootprint;

/**
* @var bool[]
*/
protected static $tippingPointCache = [];
protected static $stabilityCache = [];

/**
* Constructor.
Expand All @@ -55,6 +59,7 @@ public function __construct(Item $item, $width, $length, $depth)
$this->width = $width;
$this->length = $length;
$this->depth = $depth;
$this->surfaceFootprint = $width * $length;
}

/**
Expand Down Expand Up @@ -104,24 +109,7 @@ public function getDepth()
*/
public function getSurfaceFootprint()
{
return $this->width * $this->length;
}

/**
* @return float
*/
public function getTippingPoint()
{
$cacheKey = $this->width . '|' . $this->length . '|' . $this->depth;

if (isset(static::$tippingPointCache[$cacheKey])) {
$tippingPoint = static::$tippingPointCache[$cacheKey];
} else {
$tippingPoint = atan(min($this->length, $this->width) / ($this->depth ?: 1));
static::$tippingPointCache[$cacheKey] = $tippingPoint;
}

return $tippingPoint;
return $this->surfaceFootprint;
}

/**
Expand All @@ -133,7 +121,13 @@ public function getTippingPoint()
*/
public function isStable()
{
return $this->getTippingPoint() > 0.261;
$cacheKey = $this->width . '|' . $this->length . '|' . $this->depth;

if (!isset(static::$stabilityCache[$cacheKey])) {
static::$stabilityCache[$cacheKey] = (atan(min($this->length, $this->width) / ($this->depth ?: 1)) > 0.261);
}

return static::$stabilityCache[$cacheKey];
}

/**
Expand Down
44 changes: 29 additions & 15 deletions src/OrientatedItemFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\NullLogger;

/**
* Figure out orientations for an item and a given set of dimensions.
Expand All @@ -30,6 +31,7 @@ class OrientatedItemFactory implements LoggerAwareInterface
public function __construct(Box $box)
{
$this->box = $box;
$this->logger = new NullLogger();
}

/**
Expand Down Expand Up @@ -149,35 +151,47 @@ public function getPossibleOrientations(
$z,
PackedItemList $prevPackedItemList
) {
$orientations = [];
$orientations = $orientationsDimensions = [];

$isSame = false;
if ($prevItem) {
$itemADimensions = [$item->getWidth(), $item->getLength(), $item->getDepth()];
$itemBDimensions = [$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()];
sort($itemADimensions);
sort($itemBDimensions);
$isSame = ($itemADimensions === $itemBDimensions);
}

//Special case items that are the same as what we just packed - keep orientation
if ($prevItem && $this->isSameDimensions($prevItem->getItem(), $item)) {
$orientations[] = new OrientatedItem($item, $prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth());
if ($isSame && $prevItem) {
$orientationsDimensions[] = [$prevItem->getWidth(), $prevItem->getLength(), $prevItem->getDepth()];
} else {
//simple 2D rotation
$orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getLength(), $item->getDepth());
$orientations[] = new OrientatedItem($item, $item->getLength(), $item->getWidth(), $item->getDepth());
$orientationsDimensions[] = [$item->getWidth(), $item->getLength(), $item->getDepth()];
$orientationsDimensions[] = [$item->getLength(), $item->getWidth(), $item->getDepth()];

//add 3D rotation if we're allowed
if (!$item->getKeepFlat()) {
$orientations[] = new OrientatedItem($item, $item->getWidth(), $item->getDepth(), $item->getLength());
$orientations[] = new OrientatedItem($item, $item->getLength(), $item->getDepth(), $item->getWidth());
$orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getWidth(), $item->getLength());
$orientations[] = new OrientatedItem($item, $item->getDepth(), $item->getLength(), $item->getWidth());
$orientationsDimensions[] = [$item->getWidth(), $item->getDepth(), $item->getLength()];
$orientationsDimensions[] = [$item->getLength(), $item->getDepth(), $item->getWidth()];
$orientationsDimensions[] = [$item->getDepth(), $item->getWidth(), $item->getLength()];
$orientationsDimensions[] = [$item->getDepth(), $item->getLength(), $item->getWidth()];
}
}

$orientations = array_unique($orientations);

//remove any that simply don't fit
$orientations = array_filter($orientations, function (OrientatedItem $i) use ($widthLeft, $lengthLeft, $depthLeft) {
return $i->getWidth() <= $widthLeft && $i->getLength() <= $lengthLeft && $i->getDepth() <= $depthLeft;
$orientationsDimensions = array_unique($orientationsDimensions, SORT_REGULAR);
$orientationsDimensions = array_filter($orientationsDimensions, static function (array $i) use ($widthLeft, $lengthLeft, $depthLeft) {
return $i[0] <= $widthLeft && $i[1] <= $lengthLeft && $i[2] <= $depthLeft;
});

foreach ($orientationsDimensions as $dimensions) {
$orientations[] = new OrientatedItem($item, $dimensions[0], $dimensions[1], $dimensions[2]);
}

if ($item instanceof ConstrainedPlacementItem) {
$box = $this->box;
$orientations = array_filter($orientations, function (OrientatedItem $i) use ($box, $x, $y, $z, $prevPackedItemList) {
$orientations = array_filter($orientations, static function (OrientatedItem $i) use ($box, $x, $y, $z, $prevPackedItemList) {
/** @var ConstrainedPlacementItem $constrainedItem */
$constrainedItem = $i->getItem();

Expand Down Expand Up @@ -295,7 +309,7 @@ function (OrientatedItem $orientation) {
*
* @return bool
*/
protected function isSameDimensions(Item $itemA, Item $itemB)
public function isSameDimensions(Item $itemA, Item $itemB)
{
$itemADimensions = [$itemA->getWidth(), $itemA->getLength(), $itemA->getDepth()];
$itemBDimensions = [$itemB->getWidth(), $itemB->getLength(), $itemB->getDepth()];
Expand Down
95 changes: 50 additions & 45 deletions src/VolumePacker.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
namespace DVDoug\BoxPacker;

use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

/**
Expand All @@ -17,7 +17,12 @@
*/
class VolumePacker implements LoggerAwareInterface
{
use LoggerAwareTrait;
/**
* The logger instance.
*
* @var LoggerInterface
*/
protected $logger;

/**
* Box to pack items into.
Expand Down Expand Up @@ -46,9 +51,9 @@ class VolumePacker implements LoggerAwareInterface
/**
* List of items temporarily skipped to be packed.
*
* @var ItemList
* @var array
*/
protected $skippedItems;
protected $skippedItems = [];

/**
* Remaining weight capacity of the box.
Expand Down Expand Up @@ -76,6 +81,11 @@ class VolumePacker implements LoggerAwareInterface
*/
protected $lookAheadMode = false;

/**
* @var OrientatedItemFactory
*/
private $orientatedItemFactory;

/**
* Constructor.
*
Expand All @@ -90,13 +100,25 @@ public function __construct(Box $box, ItemList $items)
$this->boxWidth = max($this->box->getInnerWidth(), $this->box->getInnerLength());
$this->boxLength = min($this->box->getInnerWidth(), $this->box->getInnerLength());
$this->remainingWeight = $this->box->getMaxWeight() - $this->box->getEmptyWeight();
$this->skippedItems = new ItemList();
$this->logger = new NullLogger();

// we may have just rotated the box for packing purposes, record if we did
if ($this->box->getInnerWidth() !== $this->boxWidth || $this->box->getInnerLength() !== $this->boxLength) {
$this->boxRotated = true;
}

$this->orientatedItemFactory = new OrientatedItemFactory($this->box);
}

/**
* Sets a logger.
*
* @param LoggerInterface $logger
*/
public function setLogger(LoggerInterface $logger)
{
$this->logger = $logger;
$this->orientatedItemFactory->setLogger($logger);
}

/**
Expand All @@ -117,7 +139,7 @@ public function pack()
{
$this->logger->debug("[EVALUATING BOX] {$this->box->getReference()}", ['box' => $this->box]);

while (count($this->items) > 0) {
while ($this->items->count() > 0) {
$layerStartDepth = $this->getCurrentPackedDepth();
$this->packLayer($layerStartDepth, $this->boxWidth, $this->boxLength, $this->box->getInnerDepth() - $layerStartDepth);
}
Expand Down Expand Up @@ -149,16 +171,15 @@ protected function packLayer($startDepth, $widthLeft, $lengthLeft, $depthLeft)
$prevItem = null;
$x = $y = $rowWidth = $rowLength = $layerDepth = 0;

while (count($this->items) > 0) {
while ($this->items->count() > 0) {
$itemToPack = $this->items->extract();

//skip items that are simply too heavy or too large
if (!$this->checkConstraints($itemToPack)) {
$this->rebuildItemList();
if (!$this->checkNonDimensionalConstraints($itemToPack)) {
continue;
}

$orientatedItem = $this->getOrientationForItem($itemToPack, $prevItem, $this->items, $this->hasItemsLeftToPack(), $widthLeft, $lengthLeft, $depthLeft, $rowLength, $x, $y, $startDepth);
$orientatedItem = $this->getOrientationForItem($itemToPack, $prevItem, $this->items, !$this->hasItemsLeftToPack(), $widthLeft, $lengthLeft, $depthLeft, $rowLength, $x, $y, $startDepth);

if ($orientatedItem instanceof OrientatedItem) {
$packedItem = PackedItem::fromOrientatedItem($orientatedItem, $x, $y, $startDepth);
Expand All @@ -176,25 +197,33 @@ protected function packLayer($startDepth, $widthLeft, $lengthLeft, $depthLeft)
$x += $orientatedItem->getWidth();

$prevItem = $packedItem;
$this->rebuildItemList();
if ($this->items->count() === 0) {
$this->rebuildItemList();
}
} elseif (count($layer->getItems()) === 0) { // zero items on layer
$this->logger->debug("doesn't fit on layer even when empty, skipping for good");
continue;
} elseif ($widthLeft > 0 && count($this->items) > 0) { // skip for now, move on to the next item
} elseif ($widthLeft > 0 && $this->items->count() > 0) { // skip for now, move on to the next item
$this->logger->debug("doesn't fit, skipping for now");
$this->skippedItems->insert($itemToPack);
$this->skippedItems[] = $itemToPack;
// abandon here if next item is the same, no point trying to keep going. Last time is not skipped, need that to trigger appropriate reset logic
while ($this->items->count() > 2 && $this->orientatedItemFactory->isSameDimensions($itemToPack, $this->items->top())) {
$this->skippedItems[] = $this->items->extract();
}
} elseif ($x > 0 && $lengthLeft >= min($itemToPack->getWidth(), $itemToPack->getLength(), $itemToPack->getDepth())) {
$this->logger->debug('No more fit in width wise, resetting for new row');
$widthLeft += $rowWidth;
$lengthLeft -= $rowLength;
$y += $rowLength;
$x = $rowWidth = $rowLength = 0;
$this->rebuildItemList($itemToPack);
$this->skippedItems[] = $itemToPack;
$this->rebuildItemList();
$prevItem = null;
continue;
} else {
$this->logger->debug('no items fit, so starting next vertical layer');
$this->rebuildItemList($itemToPack);
$this->skippedItems[] = $itemToPack;
$this->rebuildItemList();

return;
}
Expand Down Expand Up @@ -256,9 +285,7 @@ protected function getOrientationForItem(
$prevOrientatedItem = $prevItem ? $prevItem->toOrientatedItem() : null;
$prevPackedItemList = $itemToPack instanceof ConstrainedPlacementItem ? $this->getPackedItemList() : new PackedItemList(); // don't calculate it if not going to be used

$orientatedItemFactory = new OrientatedItemFactory($this->box);
$orientatedItemFactory->setLogger($this->logger);
$orientatedItemDecision = $orientatedItemFactory->getBestOrientation($itemToPack, $prevOrientatedItem, $nextItems, $isLastItem, $maxWidth, $maxLength, $maxDepth, $rowLength, $x, $y, $z, $prevPackedItemList);
$orientatedItemDecision = $this->orientatedItemFactory->getBestOrientation($itemToPack, $prevOrientatedItem, $nextItems, $isLastItem, $maxWidth, $maxLength, $maxDepth, $rowLength, $x, $y, $z, $prevPackedItemList);

return $orientatedItemDecision;
}
Expand Down Expand Up @@ -289,7 +316,7 @@ protected function tryAndStackItemsIntoSpace(
$z,
$rowLength
) {
while (count($this->items) > 0 && $this->checkNonDimensionalConstraints($this->items->top())) {
while ($this->items->count() > 0 && $this->checkNonDimensionalConstraints($this->items->top())) {
$stackedItem = $this->getOrientationForItem(
$this->items->top(),
$prevItem,
Expand All @@ -315,20 +342,6 @@ protected function tryAndStackItemsIntoSpace(
}
}

/**
* Check item generally fits into box.
*
* @param Item $itemToPack
*
* @return bool
*/
protected function checkConstraints(
Item $itemToPack
) {
return $this->checkNonDimensionalConstraints($itemToPack) &&
$this->checkDimensionalConstraints($itemToPack);
}

/**
* As well as purely dimensional constraints, there are other constraints that need to be met
* e.g. weight limits or item-specific restrictions (e.g. max <x> batteries per box).
Expand Down Expand Up @@ -365,19 +378,11 @@ protected function checkDimensionalConstraints(Item $itemToPack)

/**
* Reintegrate skipped items into main list.
*
* @param Item|null $currentItem item from current iteration
*/
protected function rebuildItemList(Item $currentItem = null)
protected function rebuildItemList()
{
if (count($this->items) === 0) {
$this->items = $this->skippedItems;
$this->skippedItems = new ItemList();
}

if ($currentItem instanceof Item) {
$this->items->insert($currentItem);
}
$this->items = ItemList::fromArray(array_merge($this->skippedItems, iterator_to_array($this->items)));
$this->skippedItems = [];
}

/**
Expand All @@ -402,7 +407,7 @@ protected function rotateLayersNinetyDegrees()
*/
protected function hasItemsLeftToPack()
{
return count($this->skippedItems) + count($this->items) === 0;
return count($this->skippedItems) + $this->items->count() > 0;
}

/**
Expand Down
2 changes: 1 addition & 1 deletion tests/data/expected.csv
Original file line number Diff line number Diff line change
Expand Up @@ -1785,7 +1785,7 @@
66cfc755b3bf57a31443b880a13b8329,1,0,19.9,1,0,19.9
66d19a0cf23fd8bdc01c8ce6331b005e,1,0,26.4,1,0,26.4
66dc2656db4413ae3e09c518ed7bb556,1,0,23.6,1,0,23.6
66dc2e0b086507a10167fbafabee6ac2,3,12799138.7,36.2,3,4769.6,23.6
66dc2e0b086507a10167fbafabee6ac2,3,12597768.0,36.2,3,4264.2,23.6
66e7fc5f91b1a25a68ecbe3db9b7742e,1,0,82.7,1,0,82.7
66ff7bfe4da0b72b1fe35f5e211915f2,1,0,19.7,1,0,19.7
673678add1d458f12ec22ab7030a368b,1,0,18.5,1,0,18.5
Expand Down

0 comments on commit bc850d6

Please sign in to comment.