/** * Given a solution set of packed boxes, repack them to achieve optimum weight distribution * * @param PackedBoxList $originalBoxes * @return PackedBoxList */ public function redistributeWeight(PackedBoxList $originalBoxes) { $targetWeight = $originalBoxes->getMeanWeight(); $this->logger->log(LogLevel::DEBUG, "repacking for weight distribution, weight variance {$originalBoxes->getWeightVariance()}, target weight {$targetWeight}"); $packedBoxes = new PackedBoxList(); $overWeightBoxes = []; $underWeightBoxes = []; foreach (clone $originalBoxes as $packedBox) { $boxWeight = $packedBox->getWeight(); if ($boxWeight > $targetWeight) { $overWeightBoxes[] = $packedBox; } elseif ($boxWeight < $targetWeight) { $underWeightBoxes[] = $packedBox; } else { $packedBoxes->insert($packedBox); //target weight, so we'll keep these } } do { //Keep moving items from most overweight box to most underweight box $tryRepack = false; $this->logger->log(LogLevel::DEBUG, 'boxes under/over target: ' . count($underWeightBoxes) . '/' . count($overWeightBoxes)); foreach ($underWeightBoxes as $u => $underWeightBox) { $this->logger->log(LogLevel::DEBUG, 'Underweight Box ' . $u); foreach ($overWeightBoxes as $o => $overWeightBox) { $this->logger->log(LogLevel::DEBUG, 'Overweight Box ' . $o); $overWeightBoxItems = $overWeightBox->getItems()->asArray(); //For each item in the heavier box, try and move it to the lighter one foreach ($overWeightBoxItems as $oi => $overWeightBoxItem) { $this->logger->log(LogLevel::DEBUG, 'Overweight Item ' . $oi); if ($underWeightBox->getWeight() + $overWeightBoxItem->getWeight() > $targetWeight) { $this->logger->log(LogLevel::DEBUG, 'Skipping item for hindering weight distribution'); continue; //skip if moving this item would hinder rather than help weight distribution } $newItemsForLighterBox = clone $underWeightBox->getItems(); $newItemsForLighterBox->insert($overWeightBoxItem); $newLighterBoxPacker = new Packer(); //we may need a bigger box $newLighterBoxPacker->setBoxes($this->boxes); $newLighterBoxPacker->setItems($newItemsForLighterBox); $this->logger->log(LogLevel::INFO, "[ATTEMPTING TO PACK LIGHTER BOX]"); $newLighterBox = $newLighterBoxPacker->doVolumePacking()->extract(); if ($newLighterBox->getItems()->count() === $newItemsForLighterBox->count()) { //new item fits $this->logger->log(LogLevel::DEBUG, 'New item fits'); unset($overWeightBoxItems[$oi]); //now packed in different box $newHeavierBoxPacker = new Packer(); //we may be able to use a smaller box $newHeavierBoxPacker->setBoxes($this->boxes); $newHeavierBoxPacker->setItems($overWeightBoxItems); $this->logger->log(LogLevel::INFO, "[ATTEMPTING TO PACK HEAVIER BOX]"); $newHeavierBoxes = $newHeavierBoxPacker->doVolumePacking(); if (count($newHeavierBoxes) > 1) { //found an edge case in packing algorithm that *increased* box count $this->logger->log(LogLevel::INFO, "[REDISTRIBUTING WEIGHT] Abandoning redistribution, because new packing is less efficient than original"); return $originalBoxes; } $overWeightBoxes[$o] = $newHeavierBoxes->extract(); $underWeightBoxes[$u] = $newLighterBox; $tryRepack = true; //we did some work, so see if we can do even better usort($overWeightBoxes, [$packedBoxes, 'reverseCompare']); usort($underWeightBoxes, [$packedBoxes, 'reverseCompare']); break 3; } } } } } while ($tryRepack); //Combine back into a single list $packedBoxes->insertFromArray($overWeightBoxes); $packedBoxes->insertFromArray($underWeightBoxes); return $packedBoxes; }
/** * Given a solution set of packed boxes, repack them to achieve optimum weight distribution * * @param PackedBoxList $aPackedBoxes * @return PackedBoxList */ public function redistributeWeight(PackedBoxList $aPackedBoxes) { $targetWeight = $aPackedBoxes->getMeanWeight(); $this->logger->log(LogLevel::DEBUG, "repacking for weight distribution, weight variance {$aPackedBoxes->getWeightVariance()}, target weight {$targetWeight}"); $packedBoxes = new PackedBoxList(); $overWeightBoxes = []; $underWeightBoxes = []; foreach ($aPackedBoxes as $packedBox) { $boxWeight = $packedBox->getWeight(); if ($boxWeight > $targetWeight) { $overWeightBoxes[] = $packedBox; } else { if ($boxWeight < $targetWeight) { $underWeightBoxes[] = $packedBox; } else { $packedBoxes->insert($packedBox); //target weight, so we'll keep these } } } do { //Keep moving items from most overweight box to most underweight box $tryRepack = false; $this->logger->log(LogLevel::DEBUG, 'boxes under/over target: ' . count($underWeightBoxes) . '/' . count($overWeightBoxes)); foreach ($underWeightBoxes as $u => $underWeightBox) { foreach ($overWeightBoxes as $o => $overWeightBox) { $overWeightBoxItems = $overWeightBox->getItems()->asArray(); //For each item in the heavier box, try and move it to the lighter one foreach ($overWeightBoxItems as $oi => $overWeightBoxItem) { if ($underWeightBox->getWeight() + $overWeightBoxItem->getWeight() > $targetWeight) { continue; //skip if moving this item would hinder rather than help weight distribution } $newItemsForLighterBox = clone $underWeightBox->getItems(); $newItemsForLighterBox->insert($overWeightBoxItem); $newLighterBoxPacker = new Packer(); //we may need a bigger box $newLighterBoxPacker->setBoxes($this->boxes); $newLighterBoxPacker->setItems($newItemsForLighterBox); $newLighterBox = $newLighterBoxPacker->doVolumePacking()->extract(); if ($newLighterBox->getItems()->count() === $newItemsForLighterBox->count()) { //new item fits unset($overWeightBoxItems[$oi]); //now packed in different box $newHeavierBoxPacker = new Packer(); //we may be able to use a smaller box $newHeavierBoxPacker->setBoxes($this->boxes); $newHeavierBoxPacker->setItems($overWeightBoxItems); $overWeightBoxes[$o] = $newHeavierBoxPacker->doVolumePacking()->extract(); $underWeightBoxes[$u] = $newLighterBox; $tryRepack = true; //we did some work, so see if we can do even better usort($overWeightBoxes, [$packedBoxes, 'reverseCompare']); usort($underWeightBoxes, [$packedBoxes, 'reverseCompare']); break 3; } } } } } while ($tryRepack); //Combine back into a single list $packedBoxes->insertFromArray($overWeightBoxes); $packedBoxes->insertFromArray($underWeightBoxes); return $packedBoxes; }
/** * Pack items into boxes using the principle of largest volume item first * * @throws \RuntimeException * @return PackedBoxList */ public function doVolumePacking() { $packedBoxes = new PackedBoxList(); //Keep going until everything packed while ($this->items->count()) { $boxesToEvaluate = clone $this->boxes; $packedBoxesIteration = new PackedBoxList(); //Loop through boxes starting with smallest, see what happens while (!$boxesToEvaluate->isEmpty()) { $box = $boxesToEvaluate->extract(); $volumePacker = new VolumePacker($box, clone $this->items); $volumePacker->setLogger($this->logger); $packedBox = $volumePacker->pack(); if ($packedBox->getItems()->count()) { $packedBoxesIteration->insert($packedBox); //Have we found a single box that contains everything? if ($packedBox->getItems()->count() === $this->items->count()) { break; } } } //Check iteration was productive if ($packedBoxesIteration->isEmpty()) { throw new \RuntimeException('Item ' . $this->items->top()->getDescription() . ' is too large to fit into any box'); } //Find best box of iteration, and remove packed items from unpacked list $bestBox = $packedBoxesIteration->top(); $unPackedItems = $this->items->asArray(); foreach (clone $bestBox->getItems() as $packedItem) { foreach ($unPackedItems as $unpackedKey => $unpackedItem) { if ($packedItem === $unpackedItem) { unset($unPackedItems[$unpackedKey]); break; } } } $unpackedItemList = new ItemList(); foreach ($unPackedItems as $unpackedItem) { $unpackedItemList->insert($unpackedItem); } $this->items = $unpackedItemList; $packedBoxes->insert($bestBox); } return $packedBoxes; }