25#include <nlohmann/json.hpp>
37#if __has_include( <geometry/poly_ystripes_index.h> )
39#define HAS_YSTRIPES_INDEX 1
41#define HAS_YSTRIPES_INDEX 0
44namespace fs = std::filesystem;
56 virtual ~PIP_STRATEGY() =
default;
57 virtual std::string Name()
const = 0;
59 virtual bool Contains(
const VECTOR2I& aPt )
const = 0;
67class RAYCASTING_STRATEGY :
public PIP_STRATEGY
70 std::string Name()
const override {
return "Raycasting"; }
74 m_polySet = &aPolySet;
77 bool Contains(
const VECTOR2I& aPt )
const override
87class RTREE_EDGE_STRATEGY :
public PIP_STRATEGY
90 std::string Name()
const override {
return "RTreeEdge"; }
94 m_index.Build( aPolySet );
97 bool Contains(
const VECTOR2I& aPt )
const override
107#if HAS_YSTRIPES_INDEX
108class YSTRIPES_STRATEGY :
public PIP_STRATEGY
111 std::string Name()
const override {
return "YStripes"; }
115 m_index.
Build( aPolySet );
118 bool Contains(
const VECTOR2I& aPt )
const override
120 return m_index.Contains( aPt );
138 outline.
Append( aSize, 0 );
139 outline.
Append( aSize, aSize );
140 outline.
Append( 0, aSize );
152 for(
int i = 0; i < aVertexCount; i++ )
154 double angle = 2.0 *
M_PI * i / aVertexCount;
155 int x =
static_cast<int>( aRadius * std::cos( angle ) );
156 int y =
static_cast<int>( aRadius * std::sin( angle ) );
166SHAPE_POLY_SET makeStarPolygon(
int aOuterRadius,
int aInnerRadius,
int aPoints )
171 for(
int i = 0; i < aPoints * 2; i++ )
173 double angle =
M_PI * i / aPoints;
174 int r = ( i % 2 == 0 ) ? aOuterRadius : aInnerRadius;
175 int x =
static_cast<int>( r * std::cos( angle ) );
176 int y =
static_cast<int>( r * std::sin( angle ) );
190 int width = aStep / 2;
193 for(
int i = 0; i < aTeeth; i++ )
195 outline.
Append( i * aStep, 0 );
196 outline.
Append( i * aStep, -width );
197 outline.
Append( i * aStep + width, -width );
198 outline.
Append( i * aStep + width, 0 );
201 outline.
Append( aTeeth * aStep, 0 );
204 outline.
Append( aTeeth * aStep, aStep );
206 for(
int i = aTeeth - 1; i >= 0; i-- )
208 outline.
Append( i * aStep + width, aStep );
209 outline.
Append( i * aStep + width, aStep + width );
210 outline.
Append( i * aStep, aStep + width );
211 outline.
Append( i * aStep, aStep );
214 outline.
Append( 0, aStep );
221SHAPE_POLY_SET makeSpiral(
int aRadius,
int aTurns,
int aPointsPerTurn,
int aWidth )
225 int totalPoints = aTurns * aPointsPerTurn;
228 for(
int i = 0; i <= totalPoints; i++ )
230 double angle = 2.0 *
M_PI * i / aPointsPerTurn;
231 double r = aWidth + (
static_cast<double>( aRadius ) * i / totalPoints );
232 int x =
static_cast<int>( r * std::cos( angle ) );
233 int y =
static_cast<int>( r * std::sin( angle ) );
238 for(
int i = totalPoints; i >= 0; i-- )
240 double angle = 2.0 *
M_PI * i / aPointsPerTurn;
241 double r = (
static_cast<double>( aRadius ) * i / totalPoints );
242 int x =
static_cast<int>( r * std::cos( angle ) );
243 int y =
static_cast<int>( r * std::sin( angle ) );
257std::vector<VECTOR2I> generateRandomPoints(
const BOX2I& aBBox,
int aCount, uint32_t aSeed )
259 std::mt19937 gen( aSeed );
260 std::vector<VECTOR2I> points;
261 points.reserve( aCount );
263 std::uniform_int_distribution<int> distX( aBBox.
GetLeft(), aBBox.
GetRight() );
264 std::uniform_int_distribution<int> distY( aBBox.
GetTop(), aBBox.
GetBottom() );
266 for(
int i = 0; i < aCount; i++ )
267 points.emplace_back( distX( gen ), distY( gen ) );
273std::vector<VECTOR2I> generateGridPoints(
const BOX2I& aBBox,
int aGridSize )
275 std::vector<VECTOR2I> points;
276 int64_t rawStepX = aBBox.
GetWidth() / aGridSize;
277 int64_t rawStepY = aBBox.
GetHeight() / aGridSize;
278 int stepX =
static_cast<int>( rawStepX > 0 ? rawStepX : 1 );
279 int stepY =
static_cast<int>( rawStepY > 0 ? rawStepY : 1 );
281 points.reserve(
static_cast<size_t>( aGridSize ) * aGridSize );
283 for(
int gy = 0; gy < aGridSize; gy++ )
285 for(
int gx = 0; gx < aGridSize; gx++ )
286 points.emplace_back( aBBox.
GetLeft() + gx * stepX, aBBox.
GetTop() + gy * stepY );
297struct STRATEGY_RESULT
300 int64_t buildTimeUs = 0;
301 int64_t queryTimeUs = 0;
303 double nsPerQuery = 0.0;
304 double queriesPerSec = 0.0;
309STRATEGY_RESULT benchmarkStrategy( PIP_STRATEGY& aStrategy,
const SHAPE_POLY_SET& aPolySet,
310 const std::vector<VECTOR2I>& aPoints )
313 result.name = aStrategy.Name();
318 aStrategy.Build( aPolySet );
320 result.buildTimeUs =
static_cast<int64_t
>( timer.
msecs() * 1000.0 );
324 int warmupInside = 0;
328 if( aStrategy.Contains( pt ) )
332 result.insideCount = warmupInside;
338 int calibrateInside = 0;
342 if( aStrategy.Contains( pt ) )
347 double onePassMs = calibrate.
msecs();
349 if( onePassMs > 0.0 )
350 iterations = std::max( 1,
static_cast<int>( 100.0 / onePassMs ) );
357 for(
int iter = 0; iter < iterations; iter++ )
360 aStrategy.Contains( pt );
364 result.queryCount =
static_cast<int>( aPoints.size() ) * iterations;
365 result.queryTimeUs =
static_cast<int64_t
>( timer.
msecs() * 1000.0 );
368 if(
result.queryCount > 0 )
370 result.nsPerQuery =
static_cast<double>(
result.queryTimeUs ) * 1000.0 /
result.queryCount;
371 result.queriesPerSec =
result.queryCount / (
static_cast<double>(
result.queryTimeUs ) / 1e6 );
392bool loadPolyFile(
const fs::path& aPath, std::vector<POLYGON_CASE>& aCases )
394 std::ifstream file( aPath );
396 if( !file.is_open() )
399 std::string content( ( std::istreambuf_iterator<char>( file ) ),
400 std::istreambuf_iterator<char>() );
403 size_t srcStart = content.find(
"(source \"" );
405 if( srcStart != std::string::npos )
408 size_t srcEnd = content.find(
"\")", srcStart );
410 if( srcEnd != std::string::npos )
411 source = content.substr( srcStart, srcEnd - srcStart );
416 while( ( zonePos = content.find(
"(zone (layer \"", zonePos ) ) != std::string::npos )
419 entry.source = source;
421 size_t layerStart = zonePos + 14;
422 size_t layerEnd = content.find(
"\")", layerStart );
423 entry.layer = content.substr( layerStart, layerEnd - layerStart );
425 size_t netStart = content.find(
"(net \"", layerEnd );
427 if( netStart != std::string::npos )
430 size_t netEnd = content.find(
"\")", netStart );
431 entry.net = content.substr( netStart, netEnd - netStart );
434 size_t vcStart = content.find(
"(vertex_count ", layerEnd );
436 if( vcStart != std::string::npos )
439 entry.vertexCount = std::stoi( content.substr( vcStart ) );
442 size_t polysetStart = content.find(
"polyset ", zonePos );
444 if( polysetStart != std::string::npos )
446 std::string remainder = content.substr( polysetStart );
447 std::stringstream ss( remainder );
449 if( entry.polySet.
Parse( ss ) )
450 aCases.push_back( std::move( entry ) );
453 zonePos = layerEnd + 1;
456 return !aCases.empty();
460std::vector<std::unique_ptr<PIP_STRATEGY>> makeAllStrategies()
462 std::vector<std::unique_ptr<PIP_STRATEGY>> strategies;
463 strategies.push_back( std::make_unique<RAYCASTING_STRATEGY>() );
464 strategies.push_back( std::make_unique<RTREE_EDGE_STRATEGY>() );
465#if HAS_YSTRIPES_INDEX
466 strategies.push_back( std::make_unique<YSTRIPES_STRATEGY>() );
472std::string formatTable(
const std::vector<std::vector<std::string>>& aRows )
477 size_t cols = aRows[0].size();
478 std::vector<size_t> widths( cols, 0 );
480 for(
const auto& row : aRows )
482 for(
size_t c = 0; c < std::min( cols, row.size() ); c++ )
483 widths[c] = std::max( widths[c], row[c].size() );
486 std::ostringstream out;
488 for(
size_t r = 0; r < aRows.size(); r++ )
490 for(
size_t c = 0; c < cols; c++ )
492 const std::string& cell = ( c < aRows[r].size() ) ? aRows[r][c] : std::string();
495 out << std::left << std::setw( static_cast<int>( widths[c] ) ) << cell;
497 out <<
" " << std::right << std::setw( static_cast<int>( widths[c] ) ) << cell;
504 for(
size_t c = 0; c < cols; c++ )
509 out << std::string( widths[c],
'-' );
529 BOX2I bbox = star.BBox();
531 std::vector<VECTOR2I> testPoints = generateRandomPoints( bbox, 10000, 42 );
533 auto strategies = makeAllStrategies();
535 for(
auto& strategy : strategies )
536 strategy->Build( star );
540 for(
const VECTOR2I& pt : testPoints )
542 bool refResult = strategies[0]->Contains( pt );
544 for(
size_t s = 1; s < strategies.size(); s++ )
546 if( strategies[s]->Contains( pt ) != refResult )
550 << pt.x <<
", " << pt.y <<
")"
551 <<
" ref=" << refResult );
557 std::to_string( mismatches ) +
" mismatch(es) across strategies" );
565 auto strategies = makeAllStrategies();
567 for(
auto& strategy : strategies )
568 strategy->Build(
square );
577 std::vector<TEST_POINT> cases = {
578 { { 500000, 500000 },
true,
"center" },
579 { { 100000, 100000 },
true,
"inside near corner" },
580 { { 900000, 900000 },
true,
"inside far corner" },
581 { { -100000, 500000 },
false,
"outside left" },
582 { { 500000, -100000 },
false,
"outside above" },
583 { { 1500000, 500000 },
false,
"outside right" },
584 { { 500000, 1500000 },
false,
"outside below" },
585 { { 500000, -1000000 },
false,
"outside Y range above" },
586 { { 500000, 3000000 },
false,
"outside Y range below" },
589 for(
const auto& tc : cases )
591 for(
const auto& strategy : strategies )
593 bool result = strategy->Contains( tc.pt );
596 result == tc.expectedInside,
597 strategy->Name() +
" at " + tc.desc +
" (" + std::to_string( tc.pt.x ) +
", "
598 + std::to_string( tc.pt.y ) +
") expected "
599 + ( tc.expectedInside ?
"inside" :
"outside" ) +
" got "
600 + (
result ?
"inside" :
"outside" ) );
611 outline.
Append( 1000, 0 );
612 outline.
Append( 1000, 1000 );
613 outline.
Append( 0, 1000 );
626 ystripes.
Build( poly );
638 auto points = generateRandomPoints( bbox, 10000, 42 );
643 bool yResult = ystripes.
Contains( pt );
644 bool refResult = poly.
Contains( pt );
646 if( yResult != refResult )
662 std::vector<POLY_CASE> polyCases;
663 polyCases.push_back( {
"square_4v", makeSquare( 1000000 ) } );
664 polyCases.push_back( {
"hex_6v", makeRegularPolygon( 1000000, 6 ) } );
665 polyCases.push_back( {
"ngon_12v", makeRegularPolygon( 1000000, 12 ) } );
666 polyCases.push_back( {
"ngon_32v", makeRegularPolygon( 1000000, 32 ) } );
667 polyCases.push_back( {
"ngon_64v", makeRegularPolygon( 1000000, 64 ) } );
668 polyCases.push_back( {
"ngon_256v", makeRegularPolygon( 1000000, 256 ) } );
669 polyCases.push_back( {
"star_12v", makeStarPolygon( 1000000, 400000, 6 ) } );
670 polyCases.push_back( {
"star_24v", makeStarPolygon( 1000000, 400000, 12 ) } );
671 polyCases.push_back( {
"star_100v", makeStarPolygon( 1000000, 400000, 50 ) } );
672 polyCases.push_back( {
"serpentine_200v", makeSerpentine( 100000, 20 ) } );
673 polyCases.push_back( {
"serpentine_1000v", makeSerpentine( 100000, 100 ) } );
674 polyCases.push_back( {
"spiral_2000v", makeSpiral( 1000000, 10, 100, 50000 ) } );
677 const uint32_t
seed = 12345;
679 std::vector<std::vector<std::string>>
table;
680 table.push_back( {
"Polygon",
"Vertices",
"Strategy",
"Build(us)",
"ns/query",
"Mquery/s",
683 for(
auto& pc : polyCases )
685 BOX2I bbox = pc.poly.BBox();
687 auto strategies = makeAllStrategies();
689 for(
auto& strategy : strategies )
691 STRATEGY_RESULT r = benchmarkStrategy( *strategy, pc.poly, points );
695 for(
int i = 0; i < pc.poly.OutlineCount(); i++ )
696 vertexCount += pc.poly.COutline( i ).PointCount();
698 std::ostringstream nsStr, mqStr;
699 nsStr << std::fixed << std::setprecision( 1 ) << r.nsPerQuery;
700 mqStr << std::fixed << std::setprecision( 3 ) << ( r.queriesPerSec / 1e6 );
702 table.push_back( { pc.name, std::to_string( vertexCount ), r.name,
703 std::to_string( r.buildTimeUs ), nsStr.str(), mqStr.str(),
704 std::to_string( r.insideCount ) } );
716 if( !fs::exists( dataDir ) || fs::is_empty( dataDir ) )
718 BOOST_TEST_MESSAGE(
"No triangulation data in " << dataDir <<
", skipping benchmark" );
724 for(
const auto& entry : fs::directory_iterator( dataDir ) )
726 if( entry.path().extension() ==
".kicad_polys" )
743 std::vector<POLYGON_CASE> cases;
745 if( !loadPolyFile( polyFile, cases ) )
748 auto it = std::max_element( cases.begin(), cases.end(),
749 [](
const POLYGON_CASE& a,
const POLYGON_CASE& b )
751 return a.vertexCount < b.vertexCount;
754 if( it != cases.end() )
755 boards.push_back( { it->source, std::move( *it ) } );
761 return a.bestZone.vertexCount < b.bestZone.vertexCount;
767 std::vector<std::vector<std::string>>
table;
768 table.push_back( {
"Source",
"Layer",
"Vertices",
"Strategy",
"Build(us)",
"ns/query",
769 "Mquery/s",
"Inside" } );
776 POLYGON_CASE& zone = board.bestZone;
779 auto strategies = makeAllStrategies();
781 std::vector<STRATEGY_RESULT> results;
783 for(
auto& strategy : strategies )
784 results.push_back( benchmarkStrategy( *strategy, zone.polySet, points ) );
787 for(
size_t s = 1; s < results.size(); s++ )
789 if( results[s].insideCount != results[0].insideCount )
793 <<
": " << results[0].name <<
"=" << results[0].insideCount
794 <<
" vs " << results[s].name <<
"=" << results[s].insideCount );
798 nlohmann::json boardJson;
799 boardJson[
"source"] = zone.source;
800 boardJson[
"layer"] = zone.layer;
801 boardJson[
"vertices"] = zone.vertexCount;
803 for(
const auto& r : results )
805 std::ostringstream nsStr, mqStr;
806 nsStr << std::fixed << std::setprecision( 1 ) << r.nsPerQuery;
807 mqStr << std::fixed << std::setprecision( 3 ) << ( r.queriesPerSec / 1e6 );
809 std::string srcName = zone.source;
811 if( srcName.size() > 30 )
812 srcName = srcName.substr( 0, 27 ) +
"...";
814 table.push_back( { srcName, zone.layer, std::to_string( zone.vertexCount ), r.name,
815 std::to_string( r.buildTimeUs ), nsStr.str(), mqStr.str(),
816 std::to_string( r.insideCount ) } );
818 nlohmann::json stratJson;
819 stratJson[
"strategy"] = r.name;
820 stratJson[
"build_time_us"] = r.buildTimeUs;
821 stratJson[
"ns_per_query"] = r.nsPerQuery;
822 stratJson[
"queries_per_sec"] = r.queriesPerSec;
823 stratJson[
"inside_count"] = r.insideCount;
824 boardJson[
"strategies"].push_back( stratJson );
834 +
" board(s) with strategy disagreements" );
840 {
"board_count",
static_cast<int>(
boards.size() ) } };
842 std::ofstream
jsonFile(
"pip_benchmark_results.json" );
855 std::vector<int> vertexCounts = { 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192,
856 16384, 32768, 65536 };
859 const uint32_t
seed = 99999;
864 std::vector<std::string>
header = {
"Vertices" };
867 header.push_back( s->Name() +
" ns/q" );
869 std::vector<std::vector<std::string>>
table;
872 for(
int vc : vertexCounts )
878 auto strategies = makeAllStrategies();
879 std::vector<std::string> row;
880 row.push_back( std::to_string( vc ) );
884 STRATEGY_RESULT r = benchmarkStrategy( *strategies[s], poly, points );
885 std::ostringstream nsStr;
886 nsStr << std::fixed << std::setprecision( 1 ) << r.nsPerQuery;
887 row.push_back( nsStr.str() );
890 table.push_back( row );
894 << formatTable(
table ) );
constexpr size_type GetWidth() const
constexpr size_type GetHeight() const
constexpr coord_type GetLeft() const
constexpr coord_type GetRight() const
constexpr coord_type GetTop() const
constexpr coord_type GetBottom() const
Spatial index for efficient point-in-polygon containment testing.
void Build(const SHAPE_POLY_SET &aPolySet)
Build the spatial index from a SHAPE_POLY_SET's outlines.
Y-stripe spatial index for efficient point-in-polygon containment testing.
bool Contains(const VECTOR2I &aPt, int aAccuracy=0) const
Test whether a point is inside the indexed polygon set.
void Build(const SHAPE_POLY_SET &aPolySet)
Build the spatial index from a SHAPE_POLY_SET's outlines and holes.
A small class to help profiling.
void Stop()
Save the time when this function was called, and set the counter stane to stop.
double msecs(bool aSinceLast=false)
Represent a polyline containing arcs as well as line segments: A chain of connected line and/or arc s...
void SetClosed(bool aClosed)
Mark the line chain as closed (i.e.
void Append(int aX, int aY, bool aAllowDuplication=false)
Append a new point at the end of the line chain.
Represent a set of closed polygons.
int AddOutline(const SHAPE_LINE_CHAIN &aOutline)
Adds a new outline to the set and returns its index.
bool Parse(std::stringstream &aStream) override
int AddHole(const SHAPE_LINE_CHAIN &aHole, int aOutline=-1)
Adds a new hole to the given outline (default: last) and returns its index.
bool Contains(const VECTOR2I &aP, int aSubpolyIndex=-1, int aAccuracy=0, bool aUseBBoxCaches=false) const
Return true if a given subpolygon contains the point aP.
const BOX2I BBox(int aClearance=0) const override
Compute a bounding box of the shape, with a margin of aClearance a collision.
std::string GetTestDataRootDir()
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_SUITE_END()
std::vector< std::string > header
const int queryPointCount
nlohmann::json jsonResults
BOOST_AUTO_TEST_CASE(CorrectnessAllStrategiesAgree)
std::ofstream jsonFile("pip_benchmark_results.json")
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
std::vector< BOARD_BEST > boards
std::vector< fs::path > polyFiles
BOOST_TEST_MESSAGE("\n=== Real-World Polygon PIP Benchmark ===\n"<< formatTable(table))
std::vector< std::vector< std::string > > table
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I