Skip to content

Commit

Permalink
feature: add timeout feature
Browse files Browse the repository at this point in the history
  • Loading branch information
Gemorroj committed Dec 23, 2024
1 parent f988d46 commit 841c8af
Show file tree
Hide file tree
Showing 6 changed files with 143 additions and 3 deletions.
35 changes: 35 additions & 0 deletions src/DefaultTimeoutChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

/**
* Box packing (3D bin packing, knapsack problem).
*
* @author Doug Wright
*/
declare(strict_types=1);

namespace DVDoug\BoxPacker;

use DVDoug\BoxPacker\Exception\TimeoutException;

class DefaultTimeoutChecker implements TimeoutChecker
{
private float $startTime;

public function __construct(readonly private float $timeout)
{
}

public function start(?float $startTime = null): void
{
$this->startTime = $startTime ?? \microtime(true);
}

public function throwOnTimeout(?float $currentTime = null, string $message = 'Exceeded the timeout'): void
{
$spentTime = ($currentTime ?? \microtime(true)) - $this->startTime;
$isTimeout = $spentTime >= $this->timeout;
if ($isTimeout) {
throw new TimeoutException($message, $spentTime, $this->timeout);
}
}
}
33 changes: 33 additions & 0 deletions src/Exception/TimeoutException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/**
* Box packing (3D bin packing, knapsack problem).
*
* @author Doug Wright
*/
declare(strict_types=1);

namespace DVDoug\BoxPacker\Exception;

use RuntimeException;

/**
* Exception used when the timeout occurred
*/
class TimeoutException extends RuntimeException
{
public function __construct(string $message, private readonly float $spentTime, private readonly float $timeout)
{
parent::__construct($message);
}

public function getTimeout(): float
{
return $this->timeout;
}

public function getSpentTime(): float
{
return $this->spentTime;
}
}
14 changes: 12 additions & 2 deletions src/Packer.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ class Packer implements LoggerAwareInterface

private bool $beStrictAboutItemOrdering = false;

protected ?TimeoutChecker $timeoutChecker = null;

public function __construct()
{
$this->items = new ItemList();
Expand Down Expand Up @@ -136,6 +138,11 @@ public function setPackedBoxSorter(PackedBoxSorter $packedBoxSorter): void
$this->packedBoxSorter = $packedBoxSorter;
}

public function setTimeoutChecker(TimeoutChecker $timeoutChecker): void
{
$this->timeoutChecker = $timeoutChecker;
}

public function throwOnUnpackableItem(bool $throwOnUnpackableItem): void
{
$this->throwOnUnpackableItem = $throwOnUnpackableItem;
Expand All @@ -160,12 +167,12 @@ public function getUnpackedItems(): ItemList
public function pack(): PackedBoxList
{
$this->logger->log(LogLevel::INFO, '[PACKING STARTED]');

$this->timeoutChecker?->start();
$packedBoxes = $this->doBasicPacking();

// 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->boxQuantitiesAvailable);
$redistributor = new WeightRedistributor($this->boxes, $this->packedBoxSorter, $this->boxQuantitiesAvailable, $this->timeoutChecker);
$redistributor->setLogger($this->logger);
$packedBoxes = $redistributor->redistributeWeight($packedBoxes);
}
Expand All @@ -188,6 +195,7 @@ public function doBasicPacking(bool $enforceSingleBox = false): PackedBoxList

// Loop through boxes starting with smallest, see what happens
foreach ($this->getBoxList($enforceSingleBox) as $box) {
$this->timeoutChecker?->throwOnTimeout();
$volumePacker = new VolumePacker($box, $this->items);
$volumePacker->setLogger($this->logger);
$volumePacker->beStrictAboutItemOrdering($this->beStrictAboutItemOrdering);
Expand Down Expand Up @@ -232,6 +240,7 @@ public function doBasicPacking(bool $enforceSingleBox = false): PackedBoxList
public function packAllPermutations(): array
{
$this->logger->log(LogLevel::INFO, '[PACKING STARTED (all permutations)]');
$this->timeoutChecker?->start();

$boxQuantitiesAvailable = clone $this->boxQuantitiesAvailable;

Expand All @@ -252,6 +261,7 @@ public function packAllPermutations(): array

$additionalPermutationsForThisPermutation = [];
foreach ($this->boxes as $box) {
$this->timeoutChecker?->throwOnTimeout();
if ($remainingBoxQuantities[$box] > 0) {
$volumePacker = new VolumePacker($box, $wipPermutation['itemsLeft']);
$volumePacker->setLogger($this->logger);
Expand Down
24 changes: 24 additions & 0 deletions src/TimeoutChecker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

/**
* Box packing (3D bin packing, knapsack problem).
*
* @author Doug Wright
*/
declare(strict_types=1);

namespace DVDoug\BoxPacker;

use DVDoug\BoxPacker\Exception\TimeoutException;

interface TimeoutChecker
{
public function __construct(float $timeout);

public function start(?float $startTime = null): void;

/**
* @throws TimeoutException
*/
public function throwOnTimeout(?float $currentTime = null, string $message = 'Exceeded the timeout'): void;
}
4 changes: 3 additions & 1 deletion src/WeightRedistributor.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ class WeightRedistributor implements LoggerAwareInterface
public function __construct(
private readonly BoxList $boxes,
private readonly PackedBoxSorter $packedBoxSorter,
private WeakMap $boxQuantitiesAvailable
private WeakMap $boxQuantitiesAvailable,
private readonly ?TimeoutChecker $timeoutChecker,
) {
$this->logger = new NullLogger();
}
Expand Down Expand Up @@ -106,6 +107,7 @@ private function equaliseWeight(PackedBox &$boxA, PackedBox &$boxB, float $targe
$underWeightBoxItems = $underWeightBox->items->asItemArray();

foreach ($overWeightBoxItems as $key => $overWeightItem) {
$this->timeoutChecker?->throwOnTimeout();
if (!self::wouldRepackActuallyHelp($overWeightBoxItems, $overWeightItem, $underWeightBoxItems, $targetWeight)) {
continue; // moving this item would harm more than help
}
Expand Down
36 changes: 36 additions & 0 deletions tests/PackerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace DVDoug\BoxPacker;

use DVDoug\BoxPacker\Exception\NoBoxesAvailableException;
use DVDoug\BoxPacker\Exception\TimeoutException;
use DVDoug\BoxPacker\Test\ConstrainedPlacementByCountTestItem;
use DVDoug\BoxPacker\Test\LimitedSupplyTestBox;
use DVDoug\BoxPacker\Test\PackedBoxByReferenceSorter;
Expand Down Expand Up @@ -1457,4 +1458,39 @@ public function testIssue620(): void

self::assertCount(6, $packedBoxes);
}

public function testTimeoutException(): void
{
$this->expectException(TimeoutException::class);
$packer = new Packer();
$packer->setTimeoutChecker(new DefaultTimeoutChecker(3.0));

for ($i = 0; $i < 100; ++$i) {
$box = new TestBox(
reference: 'box ' . $i,
outerWidth: $i * 10,
outerLength: $i * 10,
outerDepth: $i * 10,
emptyWeight: 1,
innerWidth: $i * 10,
innerLength: $i * 10,
innerDepth: $i * 10,
maxWeight: 10000
);
$packer->addBox($box);
}

$item = new TestItem(
description: 'item 1',
width: 100,
length: 100,
depth: 100,
weight: 100,
allowedRotation: Rotation::BestFit
);
$packer->addItem($item, 500);

/** @var PackedBox[] $packedBoxes */
$packedBoxes = iterator_to_array($packer->pack(), false);
}
}

0 comments on commit 841c8af

Please sign in to comment.