Skip to content

Commit

Permalink
Backport WeightRedistributor improvements
Browse files Browse the repository at this point in the history
  • Loading branch information
dvdoug committed Jul 26, 2023
1 parent c87c0f3 commit bf81ca8
Show file tree
Hide file tree
Showing 2 changed files with 26 additions and 30 deletions.
12 changes: 6 additions & 6 deletions src/Packer.php
Expand Up @@ -39,7 +39,7 @@ class Packer implements LoggerAwareInterface
/**
* @var SplObjectStorage<Box, int>
*/
protected SplObjectStorage $boxesQtyAvailable;
protected SplObjectStorage $boxQuantitiesAvailable;

protected PackedBoxSorter $packedBoxSorter;

Expand All @@ -49,7 +49,7 @@ public function __construct()
{
$this->items = new ItemList();
$this->boxes = new BoxList();
$this->boxesQtyAvailable = new SplObjectStorage();
$this->boxQuantitiesAvailable = new SplObjectStorage();
$this->packedBoxSorter = new DefaultPackedBoxSorter();

$this->logger = new NullLogger();
Expand Down Expand Up @@ -106,7 +106,7 @@ public function setBoxes(BoxList $boxList): void
*/
public function setBoxQuantity(Box $box, int $qty): void
{
$this->boxesQtyAvailable[$box] = $qty;
$this->boxQuantitiesAvailable[$box] = $qty;
}

/**
Expand Down Expand Up @@ -144,7 +144,7 @@ public function pack(): PackedBoxList

// If we have multiple boxes, try and optimise/even-out weight distribution
if (!$this->beStrictAboutItemOrdering && $packedBoxes->count() > 1 && $packedBoxes->count() <= $this->maxBoxesToBalanceWeight) {
$redistributor = new WeightRedistributor($this->boxes, $this->packedBoxSorter, $this->boxesQtyAvailable);
$redistributor = new WeightRedistributor($this->boxes, $this->packedBoxSorter, $this->boxQuantitiesAvailable);
$redistributor->setLogger($this->logger);
$packedBoxes = $redistributor->redistributeWeight($packedBoxes);
}
Expand Down Expand Up @@ -197,7 +197,7 @@ public function doVolumePacking(bool $singlePassMode = false, bool $enforceSingl
$this->items->removePackedItems($bestBox->getItems());

$packedBoxes->insert($bestBox);
$this->boxesQtyAvailable[$bestBox->getBox()] = $this->boxesQtyAvailable[$bestBox->getBox()] - 1;
$this->boxQuantitiesAvailable[$bestBox->getBox()] = $this->boxQuantitiesAvailable[$bestBox->getBox()] - 1;
}

return $packedBoxes;
Expand All @@ -218,7 +218,7 @@ protected function getBoxList(bool $enforceSingleBox = false): iterable
$preferredBoxes = [];
$otherBoxes = [];
foreach ($this->boxes as $box) {
if ($this->boxesQtyAvailable[$box] > 0) {
if ($this->boxQuantitiesAvailable[$box] > 0) {
if ($box->getInnerWidth() * $box->getInnerLength() * $box->getInnerDepth() >= $itemVolume) {
$preferredBoxes[] = $box;
} elseif (!$enforceSingleBox) {
Expand Down
44 changes: 20 additions & 24 deletions src/WeightRedistributor.php
Expand Up @@ -31,25 +31,20 @@ class WeightRedistributor implements LoggerAwareInterface
{
use LoggerAwareTrait;

/**
* List of box sizes available to pack items into.
*/
private BoxList $boxes;

/**
* Quantities available of each box type.
*
* @var SplObjectStorage|int[]
* @var SplObjectStorage<Box, int>
*/
private $boxesQtyAvailable;
private SplObjectStorage $boxQuantitiesAvailable;

private PackedBoxSorter $packedBoxSorter;

public function __construct(BoxList $boxList, PackedBoxSorter $packedBoxSorter, SplObjectStorage $boxQuantitiesAvailable)
{
$this->boxes = $boxList;
$this->packedBoxSorter = $packedBoxSorter;
$this->boxesQtyAvailable = $boxQuantitiesAvailable;
$this->boxQuantitiesAvailable = $boxQuantitiesAvailable;
$this->logger = new NullLogger();
}

Expand Down Expand Up @@ -77,17 +72,15 @@ public function redistributeWeight(PackedBoxList $originalBoxes): PackedBoxList

$iterationSuccessful = $this->equaliseWeight($boxA, $boxB, $targetWeight);
if ($iterationSuccessful) {
$boxes = array_filter($boxes, static function (?PackedBox $box) { // remove any now-empty boxes from the list
return $box instanceof PackedBox;
});
$boxes = array_filter($boxes, static fn (?PackedBox $box) => $box instanceof PackedBox); // remove any now-empty boxes from the list
break 2;
}
}
}
} while ($iterationSuccessful);

// Combine back into a single list
$packedBoxes = new PackedBoxList();
$packedBoxes = new PackedBoxList($this->packedBoxSorter);
$packedBoxes->insertFromArray($boxes);

return $packedBoxes;
Expand All @@ -114,7 +107,7 @@ private function equaliseWeight(PackedBox &$boxA, PackedBox &$boxB, float $targe
$underWeightBoxItems = $underWeightBox->getItems()->asItemArray();

foreach ($overWeightBoxItems as $key => $overWeightItem) {
if (!static::wouldRepackActuallyHelp($overWeightBoxItems, $overWeightItem, $underWeightBoxItems, $targetWeight)) {
if (!self::wouldRepackActuallyHelp($overWeightBoxItems, $overWeightItem, $underWeightBoxItems, $targetWeight)) {
continue; // moving this item would harm more than help
}

Expand All @@ -128,8 +121,8 @@ private function equaliseWeight(PackedBox &$boxA, PackedBox &$boxB, float $targe
if (count($overWeightBoxItems) === 1) { // sometimes a repack can be efficient enough to eliminate a box
$boxB = $newLighterBoxes->top();
$boxA = null;
$this->boxesQtyAvailable[$underWeightBox->getBox()] = $this->boxesQtyAvailable[$underWeightBox->getBox()] - 1;
$this->boxesQtyAvailable[$overWeightBox->getBox()] = $this->boxesQtyAvailable[$overWeightBox->getBox()] + 1;
$this->boxQuantitiesAvailable[$underWeightBox->getBox()] = $this->boxQuantitiesAvailable[$underWeightBox->getBox()] - 1;
$this->boxQuantitiesAvailable[$overWeightBox->getBox()] = $this->boxQuantitiesAvailable[$overWeightBox->getBox()] + 1;

return true;
}
Expand All @@ -140,10 +133,10 @@ private function equaliseWeight(PackedBox &$boxA, PackedBox &$boxB, float $targe
continue; // this should never happen, if we can pack n+1 into the box, we should be able to pack n
}

$this->boxesQtyAvailable[$overWeightBox->getBox()] = $this->boxesQtyAvailable[$overWeightBox->getBox()] + 1;
$this->boxesQtyAvailable[$underWeightBox->getBox()] = $this->boxesQtyAvailable[$underWeightBox->getBox()] + 1;
$this->boxesQtyAvailable[$newHeavierBoxes->top()->getBox()] = $this->boxesQtyAvailable[$newHeavierBoxes->top()->getBox()] - 1;
$this->boxesQtyAvailable[$newLighterBoxes->top()->getBox()] = $this->boxesQtyAvailable[$newLighterBoxes->top()->getBox()] - 1;
$this->boxQuantitiesAvailable[$overWeightBox->getBox()] = $this->boxQuantitiesAvailable[$overWeightBox->getBox()] + 1;
$this->boxQuantitiesAvailable[$underWeightBox->getBox()] = $this->boxQuantitiesAvailable[$underWeightBox->getBox()] + 1;
$this->boxQuantitiesAvailable[$newHeavierBoxes->top()->getBox()] = $this->boxQuantitiesAvailable[$newHeavierBoxes->top()->getBox()] - 1;
$this->boxQuantitiesAvailable[$newLighterBoxes->top()->getBox()] = $this->boxQuantitiesAvailable[$newLighterBoxes->top()->getBox()] - 1;
$underWeightBox = $boxB = $newLighterBoxes->top();
$overWeightBox = $boxA = $newHeavierBoxes->top();

Expand All @@ -155,16 +148,17 @@ private function equaliseWeight(PackedBox &$boxA, PackedBox &$boxB, float $targe

/**
* Do a volume repack of a set of items.
* @param iterable<Item> $items
*/
private function doVolumeRepack(iterable $items, Box $currentBox): PackedBoxList
{
$packer = new Packer();
$packer->setLogger($this->logger);
$packer->setBoxes($this->boxes); // use the full set of boxes to allow smaller/larger for full efficiency
foreach ($this->boxes as $box) {
$packer->setBoxQuantity($box, $this->boxesQtyAvailable[$box]);
$packer->setBoxQuantity($box, $this->boxQuantitiesAvailable[$box]);
}
$packer->setBoxQuantity($currentBox, $this->boxesQtyAvailable[$currentBox] + 1);
$packer->setBoxQuantity($currentBox, $this->boxQuantitiesAvailable[$currentBox] + 1);
$packer->setItems($items);

return $packer->doVolumePacking(true, true);
Expand All @@ -174,6 +168,8 @@ private function doVolumeRepack(iterable $items, Box $currentBox): PackedBoxList
* Not every attempted repack is actually helpful - sometimes moving an item between two otherwise identical
* boxes, or sometimes the box used for the now lighter set of items actually weighs more when empty causing
* an increase in total weight.
* @param array<Item> $overWeightBoxItems
* @param array<Item> $underWeightBoxItems
*/
private static function wouldRepackActuallyHelp(array $overWeightBoxItems, Item $overWeightItem, array $underWeightBoxItems, float $targetWeight): bool
{
Expand All @@ -184,13 +180,13 @@ private static function wouldRepackActuallyHelp(array $overWeightBoxItems, Item
return false;
}

$oldVariance = static::calculateVariance($overWeightItemsWeight, $underWeightItemsWeight);
$newVariance = static::calculateVariance($overWeightItemsWeight - $overWeightItem->getWeight(), $underWeightItemsWeight + $overWeightItem->getWeight());
$oldVariance = self::calculateVariance($overWeightItemsWeight, $underWeightItemsWeight);
$newVariance = self::calculateVariance($overWeightItemsWeight - $overWeightItem->getWeight(), $underWeightItemsWeight + $overWeightItem->getWeight());

return $newVariance < $oldVariance;
}

private static function calculateVariance(int $boxAWeight, int $boxBWeight)
private static function calculateVariance(int $boxAWeight, int $boxBWeight): float
{
return ($boxAWeight - (($boxAWeight + $boxBWeight) / 2)) ** 2; // don't need to calculate B and ÷ 2, for a 2-item population the difference from mean is the same for each box
}
Expand Down

0 comments on commit bf81ca8

Please sign in to comment.