Finally disabled wasted area calculation for good in the 'cygon' packer because benchmarks show it as being counterproductive; implemented rectangle placement optimization as it is done in the original 'arevalo' packer - the C# version should now produce the exact identical results the C++ version does

git-svn-id: file:///srv/devel/repo-conversion/nusu@25 d2e56fa2-650e-0410-a79f-9358c0239efd
This commit is contained in:
Markus Ewald 2007-05-22 20:15:48 +00:00
parent dbc1da27a8
commit 9157bf8454
7 changed files with 194 additions and 27 deletions

View File

@ -31,7 +31,7 @@
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\x86\Release</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<DefineConstants>TRACE;UNITTEST</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<NoStdLib>true</NoStdLib>

View File

@ -35,11 +35,23 @@ namespace Nuclex.Support.Packing {
/// <summary>Tests the packer's efficiency using a deterministic benchmark</summary>
[Test]
public void TestSpaceEfficiency() {
float efficiency = calculateEfficiency(new ArevaloRectanglePacker(70, 70));
float efficiency = CalculateEfficiency(new ArevaloRectanglePacker(70, 70));
Assert.GreaterOrEqual(efficiency, 0.75, "Packer achieves 75% efficiency");
}
/// <summary>Tests the packer's stability by running a complete benchmark</summary>
[Test]
public void TestStability() {
float score = Benchmark(
delegate() { return new ArevaloRectanglePacker(1024, 1024); }
);
// This is mainly a stability and performance test. It fails when the
// packer crashes on its own and is otherwise only there to tell how long
// it takes to complete the benchmark.
}
}
} // namespace Nuclex.Support.Packing

View File

@ -137,12 +137,30 @@ namespace Nuclex.Support.Packing {
placement = this.anchors[anchorIndex];
// Move the rectangle either to the left or to the top until it collides with
// a neightbouring rectangle. This is done to combat the effect of lining up
// rectangles with gaps to the left or top of them because the anchor that
// would allow placement there has been blocked by another rectangle
optimizePlacement(ref placement, rectangleWidth, rectangleHeight);
// Remove the used anchor and add new anchors at the upper right and lower left
// positions of the new rectangle
{
// The anchor is only removed if the placement optimization didn't
// move the rectangle so far that the anchor isn't used at all
bool blocksAnchor =
((placement.X + rectangleWidth) > this.anchors[anchorIndex].X) &&
((placement.Y + rectangleHeight) > this.anchors[anchorIndex].Y);
if(blocksAnchor)
this.anchors.RemoveAt(anchorIndex);
// Add new anchors at the upper right and lower left coordinates of the rectangle
this.anchors.Add(new Point(placement.X + rectangleWidth, placement.Y));
this.anchors.Add(new Point(placement.X, placement.Y + rectangleHeight));
}
// Finally, we can add the rectangle to our packed rectangles list
this.packedRectangles.Add(
new Rectangle(placement.X, placement.Y, rectangleWidth, rectangleHeight)
);
@ -151,55 +169,98 @@ namespace Nuclex.Support.Packing {
}
/// <summary>
/// Optimizes the rectangle's placement by moving it either left or up to fill
/// any gaps resulting from rectangles blocking the anchors of the most optimal
/// placements.
/// </summary>
/// <param name="placement">Placement to be optimized</param>
/// <param name="rectangleWidth">Width of the rectangle to be optimized</param>
/// <param name="rectangleHeight">Height of the rectangle to be optimized</param>
private void optimizePlacement(
ref Point placement, int rectangleWidth, int rectangleHeight
) {
Rectangle rectangle = new Rectangle(
placement.X, placement.Y, rectangleWidth, rectangleHeight
);
// Try to move the rectangle to the left as far as possible
int leftMost = placement.X;
while(isFree(ref rectangle, PackingAreaWidth, PackingAreaHeight)) {
leftMost = rectangle.X;
--rectangle.X;
}
// Reset rectangle to original position
rectangle.X = placement.X;
// Try to move the rectangle upwards as far as possible
int topMost = placement.Y;
while(isFree(ref rectangle, PackingAreaWidth, PackingAreaHeight)) {
topMost = rectangle.Y;
--rectangle.Y;
}
// Use the dimension in which the rectangle could be moved farther
if((leftMost - placement.X) > (topMost - placement.Y))
placement.X = leftMost;
else
placement.Y = topMost;
}
/// <summary>
/// Searches for a free anchor and enlarges the packing area if none can be found
/// </summary>
/// <param name="rectangleWidth">Width of the rectangle to be placed</param>
/// <param name="rectangleHeight">Height of the rectangle to be placed</param>
/// <param name="packingAreaWidth">Total width of the packing area</param>
/// <param name="packingAreaHeight">Total height of the packing area</param>
/// <param name="testedPackingAreaWidth">Width of the tested packing area</param>
/// <param name="testedPackingAreaHeight">Height of the tested packing area</param>
/// <returns>
/// Index of the anchor the rectangle is to be placed at or -1 if the rectangle
/// does not fit in the packing area anymore
/// </returns>
private int selectAnchorRecursive(
int rectangleWidth, int rectangleHeight,
int packingAreaWidth, int packingAreaHeight
int testedPackingAreaWidth, int testedPackingAreaHeight
) {
// Try to locate an anchor point where the rectangle fits in
int freeAnchorIndex = findFirstFreeAnchor(
rectangleWidth, rectangleHeight, packingAreaWidth, packingAreaHeight
rectangleWidth, rectangleHeight, testedPackingAreaWidth, testedPackingAreaHeight
);
// If a the rectangle fits without resizing packing area (any further in case
// of a recursive call), take over the new packing area size and return the
// anchor at which the rectangle can be placed.
if(freeAnchorIndex != -1) {
this.actualPackingAreaWidth = packingAreaWidth;
this.actualPackingAreaHeight = packingAreaHeight;
this.actualPackingAreaWidth = testedPackingAreaWidth;
this.actualPackingAreaHeight = testedPackingAreaHeight;
return freeAnchorIndex;
}
//
// If we reach this point, the rectangle did not fit in the current packing
// area and our only choice is to try and enlarge the packing area.
//
bool canEnlargeWidth = (packingAreaWidth < PackingAreaWidth);
bool canEnlargeHeight = (packingAreaHeight < PackingAreaHeight);
// For readability, determine whether the packing area can be enlarged
// any further in its width and in its height
bool canEnlargeWidth = (testedPackingAreaWidth < PackingAreaWidth);
bool canEnlargeHeight = (testedPackingAreaHeight < PackingAreaHeight);
// Try to enlarge the smaller of the two dimensions first (unless the smaller
// dimension is already at its maximum size)
if(
(
(packingAreaHeight < packingAreaWidth) || !canEnlargeWidth
) && canEnlargeHeight
canEnlargeHeight && (
(testedPackingAreaHeight < testedPackingAreaWidth) || !canEnlargeWidth
)
) {
// Try to double the height of the packing area
return selectAnchorRecursive(
rectangleWidth, rectangleHeight,
packingAreaWidth, Math.Min(packingAreaHeight * 2, PackingAreaHeight)
testedPackingAreaWidth, Math.Min(testedPackingAreaHeight * 2, PackingAreaHeight)
);
} else if(canEnlargeWidth) {
@ -207,7 +268,7 @@ namespace Nuclex.Support.Packing {
// Try to double the width of the packing area
return selectAnchorRecursive(
rectangleWidth, rectangleHeight,
Math.Min(packingAreaWidth * 2, PackingAreaWidth), packingAreaHeight
Math.Min(testedPackingAreaWidth * 2, PackingAreaWidth), testedPackingAreaHeight
);
} else {
@ -262,14 +323,14 @@ namespace Nuclex.Support.Packing {
ref Rectangle rectangle, int packingAreaWidth, int packingAreaHeight
) {
// If the rectangle is partially or completely outside of the packing
// area, it can't be placed at its current location
bool leavesPackingArea =
(rectangle.X < 0) ||
(rectangle.Y < 0) ||
(rectangle.Right >= packingAreaWidth) ||
(rectangle.Bottom >= packingAreaHeight);
// If the rectangle is partially or completely outside of the packing
// area, it can't be placed at its current location
if(leavesPackingArea)
return false;

View File

@ -35,11 +35,23 @@ namespace Nuclex.Support.Packing {
/// <summary>Tests the packer's efficiency using a deterministic benchmark</summary>
[Test]
public void TestSpaceEfficiency() {
float efficiency = calculateEfficiency(new CygonRectanglePacker(70, 70));
float efficiency = CalculateEfficiency(new CygonRectanglePacker(70, 70));
Assert.GreaterOrEqual(efficiency, 0.75f, "Packer achieves 75% efficiency");
}
/// <summary>Tests the packer's stability by running a complete benchmark</summary>
[Test]
public void TestStability() {
float score = Benchmark(
delegate() { return new CygonRectanglePacker(1024, 1024); }
);
// This is mainly a stability and performance test. It fails when the
// packer crashes on its own and is otherwise only there to tell how long
// it takes to complete the benchmark.
}
}
} // namespace Nuclex.Support.Packing

View File

@ -41,6 +41,32 @@ namespace Nuclex.Support.Packing {
/// </remarks>
public class CygonRectanglePacker : RectanglePacker {
#if USE_WASTED_AREA
// An optimization idea of mine. With this, the packer not only tries to place
// rectangles as low in the packing area as possible, it also tried to choose
// locations where it doesn't block gaps where other rectangles might still fit
// in. This turned out to be counter-productive and a marginal improvement in
// space efficiency could be achieved by deliberately choosing positions where
// gaps where blocked for future rectangles.
//
// These are the results of a benchmark with different wastedAreaScoreWeights
//
// -10 579.315
// -5 582.140
// -4 582.886
// -3 583.166
// -2 583.792
// -1 583.975 (best)
// 0 583.791
// 1 583.960
// 2 583.469
// 3 582.444
// 4 580.259
// 5 578.400
// 10 570.467
//
// Needless to say, I chose to disable this splendid optimization.
/// <summary>By how much the wasted area influences a placement's score</summary>
/// <remarks>
/// <para>
@ -61,7 +87,9 @@ namespace Nuclex.Support.Packing {
/// packing problems is a matter of trial and error, as it seems :)
/// </para>
/// </remarks>
private const int WastedAreaScoreWeight = 3;
private const int WastedAreaScoreWeight = 10;
#endif // USE_WASTED_AREA
#region class SliceStartComparer
@ -161,7 +189,7 @@ namespace Nuclex.Support.Packing {
if((highest + rectangleHeight < PackingAreaHeight)) {
int score = highest;
// WASTED AREA CALCULATION --------------------------------------------------
#if USE_WASTED_AREA // --------------------------------------------------------
// Calculate the amount of space that would go to waste if the rectangle
// would be placed at this location
@ -185,7 +213,7 @@ namespace Nuclex.Support.Packing {
// Alter the score by the amount of wasted area in relation to
score += (wastedArea * WastedAreaScoreWeight / rectangleArea);
// WASTED AREA CALCULATION --------------------------------------------------
#endif // USE_WASTED_AREA -----------------------------------------------------
if(score < bestScore) {
bestSliceIndex = leftSliceIndex;

View File

@ -27,6 +27,10 @@ namespace Nuclex.Support.Packing {
/// <summary>Base class for unit testing the rectangle packers</summary>
public abstract class RectanglePackerTest {
/// <summary>Delegate for a Rectangle Packer factory method</summary>
/// <returns>A new rectangle packer</returns>
protected delegate RectanglePacker BuildRectanglePacker();
/// <summary>Determines the efficiency of a packer with a packing area of 70x70</summary>
/// <param name="packer">Packer with a packing area of 70x70 units</param>
/// <returns>The efficiency factor of the packer</returns>
@ -37,7 +41,7 @@ namespace Nuclex.Support.Packing {
/// the efficiency rating is to 1.0, the better, with 0.99 being the
/// mathematically best rating achievable.
/// </remarks>
public float calculateEfficiency(RectanglePacker packer) {
protected float CalculateEfficiency(RectanglePacker packer) {
// If we take a 1x1 square, a 2x2 square, etc. up to a 24x24 square,
// the sum of the areas of these squares is 4900, which is 70². This
// is the only nontrivial sum of consecutive squares starting with
@ -54,6 +58,44 @@ namespace Nuclex.Support.Packing {
return (float)areaCovered / 4900.0f;
}
/// <summary>Benchmarks the provided rectangle packer using random data</summary>
/// <param name="packerBuilder">
/// Rectangle packer builder returning new rectangle packers
/// with an area of 1024 x 1024
/// </param>
/// <returns>The achieved benchmark score</returns>
protected float Benchmark(BuildRectanglePacker packerBuilder) {
// How many runs to perform for getting a stable average
const int averagingRuns = 200;
// Generates the random number seeds. This is used so that each run produces
// the same number sequences and makes the comparison of different algorithms
// a little bit more stable.
Random seedGenerator = new Random(12345);
int rectanglesPacked = 0;
// Perform a number of runs to get a semi-stable average score
for(int averagingRun = 0; averagingRun < averagingRuns; ++averagingRun) {
Random dimensionGenerator = new Random(seedGenerator.Next());
RectanglePacker packer = packerBuilder();
// Try to cramp as many rectangles into the packing area as possible
for(;; ++rectanglesPacked) {
Point placement;
int width = dimensionGenerator.Next(16, 64);
int height = dimensionGenerator.Next(16, 64);
// As soon as the packer rejects the first rectangle, the run is over
if(!packer.TryPack(width, height, out placement))
break;
}
}
// Return the average score achieved by the packer
return (float)rectanglesPacked / (float)averagingRuns;
}
}
} // namespace Nuclex.Support.Packing

View File

@ -35,11 +35,23 @@ namespace Nuclex.Support.Packing {
/// <summary>Tests the packer's efficiency using a deterministic benchmark</summary>
[Test]
public void TestSpaceEfficiency() {
float efficiency = calculateEfficiency(new SimpleRectanglePacker(70, 70));
float efficiency = CalculateEfficiency(new SimpleRectanglePacker(70, 70));
Assert.GreaterOrEqual(efficiency, 0.75, "Packer achieves 75% efficiency");
}
/// <summary>Tests the packer's stability by running a complete benchmark</summary>
[Test]
public void TestStability() {
float score = Benchmark(
delegate() { return new SimpleRectanglePacker(1024, 1024); }
);
// This is mainly a stability and performance test. It fails when the
// packer crashes on its own and is otherwise only there to tell how long
// it takes to complete the benchmark.
}
}
} // namespace Nuclex.Support.Packing