KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_pip_benchmark.cpp
Go to the documentation of this file.
1/*
2 * This program is part of KiCad, a free EDA CAD application.
3 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
17 */
18
20
24#include <core/profile.h>
25#include <nlohmann/json.hpp>
26
27#include <algorithm>
28#include <cmath>
29#include <filesystem>
30#include <fstream>
31#include <iomanip>
32#include <random>
33#include <sstream>
34#include <string>
35#include <vector>
36
37#if __has_include( <geometry/poly_ystripes_index.h> )
39#define HAS_YSTRIPES_INDEX 1
40#else
41#define HAS_YSTRIPES_INDEX 0
42#endif
43
44namespace fs = std::filesystem;
45
46namespace
47{
48
49// ---------------------------------------------------------------------------
50// Pluggable strategy interface
51// ---------------------------------------------------------------------------
52
53class PIP_STRATEGY
54{
55public:
56 virtual ~PIP_STRATEGY() = default;
57 virtual std::string Name() const = 0;
58 virtual void Build( const SHAPE_POLY_SET& aPolySet ) = 0;
59 virtual bool Contains( const VECTOR2I& aPt ) const = 0;
60};
61
62
63// ---------------------------------------------------------------------------
64// Concrete strategies
65// ---------------------------------------------------------------------------
66
67class RAYCASTING_STRATEGY : public PIP_STRATEGY
68{
69public:
70 std::string Name() const override { return "Raycasting"; }
71
72 void Build( const SHAPE_POLY_SET& aPolySet ) override
73 {
74 m_polySet = &aPolySet;
75 }
76
77 bool Contains( const VECTOR2I& aPt ) const override
78 {
79 return m_polySet->Contains( aPt );
80 }
81
82private:
83 const SHAPE_POLY_SET* m_polySet = nullptr;
84};
85
86
87class RTREE_EDGE_STRATEGY : public PIP_STRATEGY
88{
89public:
90 std::string Name() const override { return "RTreeEdge"; }
91
92 void Build( const SHAPE_POLY_SET& aPolySet ) override
93 {
94 m_index.Build( aPolySet );
95 }
96
97 bool Contains( const VECTOR2I& aPt ) const override
98 {
99 return m_index.Contains( aPt );
100 }
101
102private:
104};
105
106
107#if HAS_YSTRIPES_INDEX
108class YSTRIPES_STRATEGY : public PIP_STRATEGY
109{
110public:
111 std::string Name() const override { return "YStripes"; }
112
113 void Build( const SHAPE_POLY_SET& aPolySet ) override
114 {
115 m_index.Build( aPolySet );
116 }
117
118 bool Contains( const VECTOR2I& aPt ) const override
119 {
120 return m_index.Contains( aPt );
121 }
122
123private:
124 POLY_YSTRIPES_INDEX m_index;
125};
126#endif
127
128
129// ---------------------------------------------------------------------------
130// Synthetic polygon generators
131// ---------------------------------------------------------------------------
132
133SHAPE_POLY_SET makeSquare( int aSize )
134{
135 SHAPE_POLY_SET poly;
136 SHAPE_LINE_CHAIN outline;
137 outline.Append( 0, 0 );
138 outline.Append( aSize, 0 );
139 outline.Append( aSize, aSize );
140 outline.Append( 0, aSize );
141 outline.SetClosed( true );
142 poly.AddOutline( outline );
143 return poly;
144}
145
146
147SHAPE_POLY_SET makeRegularPolygon( int aRadius, int aVertexCount )
148{
149 SHAPE_POLY_SET poly;
150 SHAPE_LINE_CHAIN outline;
151
152 for( int i = 0; i < aVertexCount; i++ )
153 {
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 ) );
157 outline.Append( x, y );
158 }
159
160 outline.SetClosed( true );
161 poly.AddOutline( outline );
162 return poly;
163}
164
165
166SHAPE_POLY_SET makeStarPolygon( int aOuterRadius, int aInnerRadius, int aPoints )
167{
168 SHAPE_POLY_SET poly;
169 SHAPE_LINE_CHAIN outline;
170
171 for( int i = 0; i < aPoints * 2; i++ )
172 {
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 ) );
177 outline.Append( x, y );
178 }
179
180 outline.SetClosed( true );
181 poly.AddOutline( outline );
182 return poly;
183}
184
185
186SHAPE_POLY_SET makeSerpentine( int aStep, int aTeeth )
187{
188 SHAPE_POLY_SET poly;
189 SHAPE_LINE_CHAIN outline;
190 int width = aStep / 2;
191
192 // Bottom edge going right
193 for( int i = 0; i < aTeeth; i++ )
194 {
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 );
199 }
200
201 outline.Append( aTeeth * aStep, 0 );
202
203 // Top edge going left
204 outline.Append( aTeeth * aStep, aStep );
205
206 for( int i = aTeeth - 1; i >= 0; i-- )
207 {
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 );
212 }
213
214 outline.Append( 0, aStep );
215 outline.SetClosed( true );
216 poly.AddOutline( outline );
217 return poly;
218}
219
220
221SHAPE_POLY_SET makeSpiral( int aRadius, int aTurns, int aPointsPerTurn, int aWidth )
222{
223 SHAPE_POLY_SET poly;
224 SHAPE_LINE_CHAIN outline;
225 int totalPoints = aTurns * aPointsPerTurn;
226
227 // Outer trace going outward
228 for( int i = 0; i <= totalPoints; i++ )
229 {
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 ) );
234 outline.Append( x, y );
235 }
236
237 // Inner trace going back inward
238 for( int i = totalPoints; i >= 0; i-- )
239 {
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 ) );
244 outline.Append( x, y );
245 }
246
247 outline.SetClosed( true );
248 poly.AddOutline( outline );
249 return poly;
250}
251
252
253// ---------------------------------------------------------------------------
254// Point generators
255// ---------------------------------------------------------------------------
256
257std::vector<VECTOR2I> generateRandomPoints( const BOX2I& aBBox, int aCount, uint32_t aSeed )
258{
259 std::mt19937 gen( aSeed );
260 std::vector<VECTOR2I> points;
261 points.reserve( aCount );
262
263 std::uniform_int_distribution<int> distX( aBBox.GetLeft(), aBBox.GetRight() );
264 std::uniform_int_distribution<int> distY( aBBox.GetTop(), aBBox.GetBottom() );
265
266 for( int i = 0; i < aCount; i++ )
267 points.emplace_back( distX( gen ), distY( gen ) );
268
269 return points;
270}
271
272
273std::vector<VECTOR2I> generateGridPoints( const BOX2I& aBBox, int aGridSize )
274{
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 );
280
281 points.reserve( static_cast<size_t>( aGridSize ) * aGridSize );
282
283 for( int gy = 0; gy < aGridSize; gy++ )
284 {
285 for( int gx = 0; gx < aGridSize; gx++ )
286 points.emplace_back( aBBox.GetLeft() + gx * stepX, aBBox.GetTop() + gy * stepY );
287 }
288
289 return points;
290}
291
292
293// ---------------------------------------------------------------------------
294// Benchmark infrastructure
295// ---------------------------------------------------------------------------
296
297struct STRATEGY_RESULT
298{
299 std::string name;
300 int64_t buildTimeUs = 0;
301 int64_t queryTimeUs = 0;
302 int queryCount = 0;
303 double nsPerQuery = 0.0;
304 double queriesPerSec = 0.0;
305 int insideCount = 0;
306};
307
308
309STRATEGY_RESULT benchmarkStrategy( PIP_STRATEGY& aStrategy, const SHAPE_POLY_SET& aPolySet,
310 const std::vector<VECTOR2I>& aPoints )
311{
312 STRATEGY_RESULT result;
313 result.name = aStrategy.Name();
314
315 // Measure build time
316 {
317 PROF_TIMER timer;
318 aStrategy.Build( aPolySet );
319 timer.Stop();
320 result.buildTimeUs = static_cast<int64_t>( timer.msecs() * 1000.0 );
321 }
322
323 // Warmup pass
324 int warmupInside = 0;
325
326 for( const VECTOR2I& pt : aPoints )
327 {
328 if( aStrategy.Contains( pt ) )
329 warmupInside++;
330 }
331
332 result.insideCount = warmupInside;
333
334 // Self-calibrate iteration count to fill at least 100ms
335 int iterations = 1;
336 {
337 PROF_TIMER calibrate;
338 int calibrateInside = 0;
339
340 for( const VECTOR2I& pt : aPoints )
341 {
342 if( aStrategy.Contains( pt ) )
343 calibrateInside++;
344 }
345
346 calibrate.Stop();
347 double onePassMs = calibrate.msecs();
348
349 if( onePassMs > 0.0 )
350 iterations = std::max( 1, static_cast<int>( 100.0 / onePassMs ) );
351 }
352
353 // Timed benchmark loop
354 {
355 PROF_TIMER timer;
356
357 for( int iter = 0; iter < iterations; iter++ )
358 {
359 for( const VECTOR2I& pt : aPoints )
360 aStrategy.Contains( pt );
361 }
362
363 timer.Stop();
364 result.queryCount = static_cast<int>( aPoints.size() ) * iterations;
365 result.queryTimeUs = static_cast<int64_t>( timer.msecs() * 1000.0 );
366 }
367
368 if( result.queryCount > 0 )
369 {
370 result.nsPerQuery = static_cast<double>( result.queryTimeUs ) * 1000.0 / result.queryCount;
371 result.queriesPerSec = result.queryCount / ( static_cast<double>( result.queryTimeUs ) / 1e6 );
372 }
373
374 return result;
375}
376
377
378// ---------------------------------------------------------------------------
379// Real-world polygon loading
380// ---------------------------------------------------------------------------
381
382struct POLYGON_CASE
383{
384 std::string source;
385 std::string layer;
386 std::string net;
387 int vertexCount = 0;
388 SHAPE_POLY_SET polySet;
389};
390
391
392bool loadPolyFile( const fs::path& aPath, std::vector<POLYGON_CASE>& aCases )
393{
394 std::ifstream file( aPath );
395
396 if( !file.is_open() )
397 return false;
398
399 std::string content( ( std::istreambuf_iterator<char>( file ) ),
400 std::istreambuf_iterator<char>() );
401
402 std::string source;
403 size_t srcStart = content.find( "(source \"" );
404
405 if( srcStart != std::string::npos )
406 {
407 srcStart += 9;
408 size_t srcEnd = content.find( "\")", srcStart );
409
410 if( srcEnd != std::string::npos )
411 source = content.substr( srcStart, srcEnd - srcStart );
412 }
413
414 size_t zonePos = 0;
415
416 while( ( zonePos = content.find( "(zone (layer \"", zonePos ) ) != std::string::npos )
417 {
418 POLYGON_CASE entry;
419 entry.source = source;
420
421 size_t layerStart = zonePos + 14;
422 size_t layerEnd = content.find( "\")", layerStart );
423 entry.layer = content.substr( layerStart, layerEnd - layerStart );
424
425 size_t netStart = content.find( "(net \"", layerEnd );
426
427 if( netStart != std::string::npos )
428 {
429 netStart += 6;
430 size_t netEnd = content.find( "\")", netStart );
431 entry.net = content.substr( netStart, netEnd - netStart );
432 }
433
434 size_t vcStart = content.find( "(vertex_count ", layerEnd );
435
436 if( vcStart != std::string::npos )
437 {
438 vcStart += 14;
439 entry.vertexCount = std::stoi( content.substr( vcStart ) );
440 }
441
442 size_t polysetStart = content.find( "polyset ", zonePos );
443
444 if( polysetStart != std::string::npos )
445 {
446 std::string remainder = content.substr( polysetStart );
447 std::stringstream ss( remainder );
448
449 if( entry.polySet.Parse( ss ) )
450 aCases.push_back( std::move( entry ) );
451 }
452
453 zonePos = layerEnd + 1;
454 }
455
456 return !aCases.empty();
457}
458
459
460std::vector<std::unique_ptr<PIP_STRATEGY>> makeAllStrategies()
461{
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>() );
467#endif
468 return strategies;
469}
470
471
472std::string formatTable( const std::vector<std::vector<std::string>>& aRows )
473{
474 if( aRows.empty() )
475 return {};
476
477 size_t cols = aRows[0].size();
478 std::vector<size_t> widths( cols, 0 );
479
480 for( const auto& row : aRows )
481 {
482 for( size_t c = 0; c < std::min( cols, row.size() ); c++ )
483 widths[c] = std::max( widths[c], row[c].size() );
484 }
485
486 std::ostringstream out;
487
488 for( size_t r = 0; r < aRows.size(); r++ )
489 {
490 for( size_t c = 0; c < cols; c++ )
491 {
492 const std::string& cell = ( c < aRows[r].size() ) ? aRows[r][c] : std::string();
493
494 if( c == 0 )
495 out << std::left << std::setw( static_cast<int>( widths[c] ) ) << cell;
496 else
497 out << " " << std::right << std::setw( static_cast<int>( widths[c] ) ) << cell;
498 }
499
500 out << "\n";
501
502 if( r == 0 )
503 {
504 for( size_t c = 0; c < cols; c++ )
505 {
506 if( c > 0 )
507 out << " ";
508
509 out << std::string( widths[c], '-' );
510 }
511
512 out << "\n";
513 }
514 }
515
516 return out.str();
517}
518
519
520} // anonymous namespace
521
522
523BOOST_AUTO_TEST_SUITE( PIPBenchmark )
524
525
526BOOST_AUTO_TEST_CASE( CorrectnessAllStrategiesAgree )
527{
528 SHAPE_POLY_SET star = makeStarPolygon( 1000000, 400000, 6 );
529 BOX2I bbox = star.BBox();
530
531 std::vector<VECTOR2I> testPoints = generateRandomPoints( bbox, 10000, 42 );
532
533 auto strategies = makeAllStrategies();
534
535 for( auto& strategy : strategies )
536 strategy->Build( star );
537
538 int mismatches = 0;
539
540 for( const VECTOR2I& pt : testPoints )
541 {
542 bool refResult = strategies[0]->Contains( pt );
543
544 for( size_t s = 1; s < strategies.size(); s++ )
545 {
546 if( strategies[s]->Contains( pt ) != refResult )
547 {
548 mismatches++;
549 BOOST_TEST_MESSAGE( strategies[s]->Name() << " disagrees at ("
550 << pt.x << ", " << pt.y << ")"
551 << " ref=" << refResult );
552 }
553 }
554 }
555
556 BOOST_CHECK_MESSAGE( mismatches == 0,
557 std::to_string( mismatches ) + " mismatch(es) across strategies" );
558}
559
560
561BOOST_AUTO_TEST_CASE( CorrectnessEdgeCases )
562{
563 SHAPE_POLY_SET square = makeSquare( 1000000 );
564
565 auto strategies = makeAllStrategies();
566
567 for( auto& strategy : strategies )
568 strategy->Build( square );
569
570 struct TEST_POINT
571 {
572 VECTOR2I pt;
573 bool expectedInside;
574 std::string desc;
575 };
576
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" },
587 };
588
589 for( const auto& tc : cases )
590 {
591 for( const auto& strategy : strategies )
592 {
593 bool result = strategy->Contains( tc.pt );
594
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" ) );
601 }
602 }
603}
604
605
606BOOST_AUTO_TEST_CASE( CorrectnessPolygonWithHoles )
607{
608 SHAPE_POLY_SET poly;
609 SHAPE_LINE_CHAIN outline;
610 outline.Append( 0, 0 );
611 outline.Append( 1000, 0 );
612 outline.Append( 1000, 1000 );
613 outline.Append( 0, 1000 );
614 outline.SetClosed( true );
615 poly.AddOutline( outline );
616
617 SHAPE_LINE_CHAIN hole;
618 hole.Append( 400, 400 );
619 hole.Append( 600, 400 );
620 hole.Append( 600, 600 );
621 hole.Append( 400, 600 );
622 hole.SetClosed( true );
623 poly.AddHole( hole );
624
625 POLY_YSTRIPES_INDEX ystripes;
626 ystripes.Build( poly );
627
628 BOOST_CHECK_EQUAL( ystripes.Contains( VECTOR2I( 100, 100 ) ), true );
629 BOOST_CHECK_EQUAL( ystripes.Contains( VECTOR2I( 500, 500 ) ), false );
630 BOOST_CHECK_EQUAL( ystripes.Contains( VECTOR2I( 1500, 500 ) ), false );
631 BOOST_CHECK_EQUAL( ystripes.Contains( VECTOR2I( 800, 200 ) ), true );
632
633 // Point just outside the hole boundary must NOT be reported as inside
634 // when using accuracy-based proximity fallback (regression for isHole fix)
635 BOOST_CHECK( !ystripes.Contains( VECTOR2I( 500, 410 ), 20 ) );
636
637 BOX2I bbox = poly.BBox();
638 auto points = generateRandomPoints( bbox, 10000, 42 );
639 int mismatches = 0;
640
641 for( const VECTOR2I& pt : points )
642 {
643 bool yResult = ystripes.Contains( pt );
644 bool refResult = poly.Contains( pt );
645
646 if( yResult != refResult )
647 mismatches++;
648 }
649
650 BOOST_CHECK_EQUAL( mismatches, 0 );
651}
652
653
654BOOST_AUTO_TEST_CASE( BenchmarkSyntheticPolygons )
655{
656 struct POLY_CASE
657 {
658 std::string name;
659 SHAPE_POLY_SET poly;
660 };
661
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 ) } );
675
676 const int queryPointCount = 10000;
677 const uint32_t seed = 12345;
678
679 std::vector<std::vector<std::string>> table;
680 table.push_back( { "Polygon", "Vertices", "Strategy", "Build(us)", "ns/query", "Mquery/s",
681 "Inside" } );
682
683 for( auto& pc : polyCases )
684 {
685 BOX2I bbox = pc.poly.BBox();
686 std::vector<VECTOR2I> points = generateRandomPoints( bbox, queryPointCount, seed );
687 auto strategies = makeAllStrategies();
688
689 for( auto& strategy : strategies )
690 {
691 STRATEGY_RESULT r = benchmarkStrategy( *strategy, pc.poly, points );
692
693 int vertexCount = 0;
694
695 for( int i = 0; i < pc.poly.OutlineCount(); i++ )
696 vertexCount += pc.poly.COutline( i ).PointCount();
697
698 std::ostringstream nsStr, mqStr;
699 nsStr << std::fixed << std::setprecision( 1 ) << r.nsPerQuery;
700 mqStr << std::fixed << std::setprecision( 3 ) << ( r.queriesPerSec / 1e6 );
701
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 ) } );
705 }
706 }
707
708 BOOST_TEST_MESSAGE( "\n=== Synthetic Polygon PIP Benchmark ===\n" << formatTable( table ) );
709}
710
711
712BOOST_AUTO_TEST_CASE( BenchmarkRealWorldPolygons, * boost::unit_test::disabled() )
713{
714 std::string dataDir = KI_TEST::GetTestDataRootDir() + "triangulation/";
715
716 if( !fs::exists( dataDir ) || fs::is_empty( dataDir ) )
717 {
718 BOOST_TEST_MESSAGE( "No triangulation data in " << dataDir << ", skipping benchmark" );
719 return;
720 }
721
722 std::vector<fs::path> polyFiles;
723
724 for( const auto& entry : fs::directory_iterator( dataDir ) )
725 {
726 if( entry.path().extension() == ".kicad_polys" )
727 polyFiles.push_back( entry.path() );
728 }
729
730 std::sort( polyFiles.begin(), polyFiles.end() );
731
732 // Load all zones, pick the one with the most vertices per board
734 {
735 std::string source;
736 POLYGON_CASE bestZone;
737 };
738
739 std::vector<BOARD_BEST> boards;
740
741 for( const auto& polyFile : polyFiles )
742 {
743 std::vector<POLYGON_CASE> cases;
744
745 if( !loadPolyFile( polyFile, cases ) )
746 continue;
747
748 auto it = std::max_element( cases.begin(), cases.end(),
749 []( const POLYGON_CASE& a, const POLYGON_CASE& b )
750 {
751 return a.vertexCount < b.vertexCount;
752 } );
753
754 if( it != cases.end() )
755 boards.push_back( { it->source, std::move( *it ) } );
756 }
757
758 std::sort( boards.begin(), boards.end(),
759 []( const BOARD_BEST& a, const BOARD_BEST& b )
760 {
761 return a.bestZone.vertexCount < b.bestZone.vertexCount;
762 } );
763
764 const int queryPointCount = 5000;
765 const uint32_t seed = 54321;
766
767 std::vector<std::vector<std::string>> table;
768 table.push_back( { "Source", "Layer", "Vertices", "Strategy", "Build(us)", "ns/query",
769 "Mquery/s", "Inside" } );
770
771 nlohmann::json jsonResults = nlohmann::json::array();
773
774 for( auto& board : boards )
775 {
776 POLYGON_CASE& zone = board.bestZone;
777 BOX2I bbox = zone.polySet.BBox();
778 std::vector<VECTOR2I> points = generateRandomPoints( bbox, queryPointCount, seed );
779 auto strategies = makeAllStrategies();
780
781 std::vector<STRATEGY_RESULT> results;
782
783 for( auto& strategy : strategies )
784 results.push_back( benchmarkStrategy( *strategy, zone.polySet, points ) );
785
786 // Verify agreement
787 for( size_t s = 1; s < results.size(); s++ )
788 {
789 if( results[s].insideCount != results[0].insideCount )
790 {
792 BOOST_TEST_MESSAGE( "Mismatch on " << zone.source << " " << zone.layer
793 << ": " << results[0].name << "=" << results[0].insideCount
794 << " vs " << results[s].name << "=" << results[s].insideCount );
795 }
796 }
797
798 nlohmann::json boardJson;
799 boardJson["source"] = zone.source;
800 boardJson["layer"] = zone.layer;
801 boardJson["vertices"] = zone.vertexCount;
802
803 for( const auto& r : results )
804 {
805 std::ostringstream nsStr, mqStr;
806 nsStr << std::fixed << std::setprecision( 1 ) << r.nsPerQuery;
807 mqStr << std::fixed << std::setprecision( 3 ) << ( r.queriesPerSec / 1e6 );
808
809 std::string srcName = zone.source;
810
811 if( srcName.size() > 30 )
812 srcName = srcName.substr( 0, 27 ) + "...";
813
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 ) } );
817
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 );
825 }
826
827 jsonResults.push_back( boardJson );
828 }
829
830 BOOST_TEST_MESSAGE( "\n=== Real-World Polygon PIP Benchmark ===\n" << formatTable( table ) );
831
833 std::to_string( totalMismatches )
834 + " board(s) with strategy disagreements" );
835
836 // Write JSON results
837 nlohmann::json output;
838 output["boards"] = jsonResults;
839 output["metadata"] = { { "query_points", queryPointCount }, { "seed", seed },
840 { "board_count", static_cast<int>( boards.size() ) } };
841
842 std::ofstream jsonFile( "pip_benchmark_results.json" );
843
844 if( jsonFile.is_open() )
845 {
846 jsonFile << output.dump( 2 ) << "\n";
847 jsonFile.close();
848 BOOST_TEST_MESSAGE( "Wrote results to pip_benchmark_results.json" );
849 }
850}
851
852
853BOOST_AUTO_TEST_CASE( ScalingAnalysis, * boost::unit_test::disabled() )
854{
855 std::vector<int> vertexCounts = { 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096, 8192,
856 16384, 32768, 65536 };
857
858 const int queryPointCount = 5000;
859 const uint32_t seed = 99999;
860
861 auto strategyNames = makeAllStrategies();
863
864 std::vector<std::string> header = { "Vertices" };
865
866 for( const auto& s : strategyNames )
867 header.push_back( s->Name() + " ns/q" );
868
869 std::vector<std::vector<std::string>> table;
870 table.push_back( header );
871
872 for( int vc : vertexCounts )
873 {
874 SHAPE_POLY_SET poly = makeRegularPolygon( 10000000, vc );
875 BOX2I bbox = poly.BBox();
876 std::vector<VECTOR2I> points = generateRandomPoints( bbox, queryPointCount, seed );
877
878 auto strategies = makeAllStrategies();
879 std::vector<std::string> row;
880 row.push_back( std::to_string( vc ) );
881
882 for( size_t s = 0; s < numStrategies; s++ )
883 {
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() );
888 }
889
890 table.push_back( row );
891 }
892
893 BOOST_TEST_MESSAGE( "\n=== PIP Scaling Analysis (ns/query vs vertex count) ===\n"
894 << formatTable( table ) );
895}
896
897
const char * name
double square(double x)
BOX2< VECTOR2I > BOX2I
Definition box2.h:922
constexpr size_type GetWidth() const
Definition box2.h:214
constexpr size_type GetHeight() const
Definition box2.h:215
constexpr coord_type GetLeft() const
Definition box2.h:228
constexpr coord_type GetRight() const
Definition box2.h:217
constexpr coord_type GetTop() const
Definition box2.h:229
constexpr coord_type GetBottom() const
Definition box2.h:222
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.
Definition profile.h:49
void Stop()
Save the time when this function was called, and set the counter stane to stop.
Definition profile.h:88
double msecs(bool aSinceLast=false)
Definition profile.h:149
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()
POLYGON_CASE bestZone
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_SUITE_END()
int totalMismatches
std::vector< std::string > header
const int queryPointCount
nlohmann::json output
size_t numStrategies
nlohmann::json jsonResults
auto strategyNames
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))
const uint32_t seed
std::vector< std::vector< std::string > > table
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
#define M_PI
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:687