23#include <nlohmann/json.hpp>
35namespace fs = std::filesystem;
46 SHAPE_POLY_SET polySet;
53 std::vector<ZONE_ENTRY> zones;
63 int triangleCount = 0;
65 double originalArea = 0.0;
66 double triangulatedArea = 0.0;
67 double areaCoverage = 0.0;
68 double meanTriArea = 0.0;
69 double stddevTriArea = 0.0;
70 int spikeyTriangles = 0;
71 double spikeyRatio = 0.0;
79 int triangleCount = 0;
80 double areaCoverage = 0.0;
81 double spikeyRatio = 0.0;
82 double stddevTriArea = 0.0;
83 int spikeyTriangles = 0;
84 double originalArea = 0.0;
90 std::vector<BASELINE_ZONE> zones;
96 std::map<std::string, BASELINE_BOARD> boards;
97 int totalTriangles = 0;
98 int totalSpikeyTri = 0;
99 double spikeyRatio = 0.0;
115struct ZONE_COMPARISON
122 int baseTriangles = 0;
123 int curTriangles = 0;
124 double baseSpikeyRatio = 0.0;
125 double curSpikeyRatio = 0.0;
126 double baseStddev = 0.0;
127 double curStddev = 0.0;
128 double baseCoverage = 0.0;
129 double curCoverage = 0.0;
131 double spikeyDeltaPp()
const {
return ( curSpikeyRatio - baseSpikeyRatio ) * 100.0; }
133 double triangleDeltaPct()
const
135 if( baseTriangles == 0 )
136 return curTriangles == 0 ? 0.0 : 100.0;
138 return ( curTriangles - baseTriangles ) /
static_cast<double>( baseTriangles ) * 100.0;
141 double stddevDeltaPct()
const
143 if( baseStddev == 0.0 )
144 return curStddev == 0.0 ? 0.0 : 100.0;
146 return ( curStddev - baseStddev ) / baseStddev * 100.0;
151BASELINE_DATA LoadBaseline(
const fs::path& aJsonPath )
153 BASELINE_DATA baseline;
155 if( !fs::exists( aJsonPath ) )
158 std::ifstream file( aJsonPath );
160 if( !file.is_open() )
167 j = nlohmann::json::parse( file );
169 catch(
const nlohmann::json::exception& )
174 if( j.contains(
"metadata" ) )
176 baseline.boardCount = j[
"metadata"].value(
"board_count", 0 );
177 baseline.zoneCount = j[
"metadata"].value(
"zone_count", 0 );
180 if( j.contains(
"global" ) )
182 baseline.totalTriangles = j[
"global"].value(
"total_triangles", 0 );
183 baseline.totalSpikeyTri = j[
"global"].value(
"total_spikey_triangles", 0 );
184 baseline.spikeyRatio = j[
"global"].value(
"spikey_ratio", 0.0 );
187 if( j.contains(
"boards" ) )
189 for(
const auto& boardJson : j[
"boards"] )
191 std::string source = boardJson.value(
"source",
"" );
192 BASELINE_BOARD board;
194 if( boardJson.contains(
"zones" ) )
196 for(
const auto& zoneJson : boardJson[
"zones"] )
199 zone.layer = zoneJson.value(
"layer",
"" );
200 zone.net = zoneJson.value(
"net",
"" );
201 zone.triangleCount = zoneJson.value(
"triangle_count", 0 );
202 zone.areaCoverage = zoneJson.value(
"area_coverage", 0.0 );
203 zone.spikeyRatio = zoneJson.value(
"spikey_ratio", 0.0 );
204 zone.stddevTriArea = zoneJson.value(
"stddev_triangle_area_nm2", 0.0 );
205 zone.spikeyTriangles = zoneJson.value(
"spikey_triangles", 0 );
206 zone.originalArea = zoneJson.value(
"original_area_nm2", 0.0 );
207 board.zones.push_back( zone );
211 baseline.boards[source] = std::move( board );
215 baseline.valid =
true;
220bool ParsePolyFile(
const fs::path& aPath, BOARD_ENTRY& aBoard )
222 std::ifstream file( aPath );
224 if( !file.is_open() )
227 std::string content( ( std::istreambuf_iterator<char>( file ) ),
228 std::istreambuf_iterator<char>() );
230 size_t srcStart = content.find(
"(source \"" );
232 if( srcStart != std::string::npos )
235 size_t srcEnd = content.find(
"\")", srcStart );
237 if( srcEnd != std::string::npos )
238 aBoard.source = content.substr( srcStart, srcEnd - srcStart );
243 while( ( zonePos = content.find(
"(zone (layer \"", zonePos ) ) != std::string::npos )
247 size_t layerStart = zonePos + 14;
248 size_t layerEnd = content.find(
"\")", layerStart );
249 entry.layer = content.substr( layerStart, layerEnd - layerStart );
251 size_t netStart = content.find(
"(net \"", layerEnd );
253 if( netStart != std::string::npos )
256 size_t netEnd = content.find(
"\")", netStart );
257 entry.net = content.substr( netStart, netEnd - netStart );
260 size_t ocStart = content.find(
"(outline_count ", layerEnd );
262 if( ocStart != std::string::npos )
265 entry.outlineCount = std::stoi( content.substr( ocStart ) );
268 size_t vcStart = content.find(
"(vertex_count ", layerEnd );
270 if( vcStart != std::string::npos )
273 entry.vertexCount = std::stoi( content.substr( vcStart ) );
276 size_t polysetStart = content.find(
"polyset ", zonePos );
278 if( polysetStart != std::string::npos )
280 std::string remainder = content.substr( polysetStart );
281 std::stringstream ss( remainder );
283 if( entry.polySet.
Parse( ss ) )
284 aBoard.zones.push_back( std::move( entry ) );
287 zonePos = layerEnd + 1;
290 return !aBoard.zones.empty();
294ZONE_STATS ComputeZoneStats( ZONE_ENTRY& aZone )
297 stats.layer = aZone.layer;
298 stats.net = aZone.net;
299 stats.outlineCount = aZone.outlineCount;
300 stats.vertexCount = aZone.vertexCount;
301 stats.originalArea = aZone.polySet.
Area();
306 stats.timeUs =
static_cast<int64_t
>( timer.
msecs() * 1000.0 );
308 std::vector<double> triAreas;
314 for(
const auto& tri : triPoly->Triangles() )
315 triAreas.push_back( tri.Area() );
318 stats.triangleCount =
static_cast<int>( triAreas.size() );
319 stats.triangulatedArea = std::accumulate( triAreas.begin(), triAreas.end(), 0.0 );
321 if( stats.originalArea > 0.0 )
322 stats.areaCoverage = stats.triangulatedArea / stats.originalArea;
324 if( !triAreas.empty() )
326 stats.meanTriArea = stats.triangulatedArea /
static_cast<double>( triAreas.size() );
328 double sumSqDiff = 0.0;
330 for(
double a : triAreas )
332 double diff = a - stats.meanTriArea;
333 sumSqDiff += diff * diff;
336 stats.stddevTriArea = std::sqrt( sumSqDiff /
static_cast<double>( triAreas.size() ) );
343 for(
const auto& tri : triPoly->Triangles() )
353 double longest = std::max( { ab, bc, ca } );
354 double shortest = std::min( { ab, bc, ca } );
356 if( shortest > 0.0 && longest / shortest > 10.0 )
357 stats.spikeyTriangles++;
361 if( stats.triangleCount > 0 )
362 stats.spikeyRatio =
static_cast<double>( stats.spikeyTriangles ) / stats.triangleCount;
368nlohmann::json ZoneStatsToJson(
const ZONE_STATS& aStats )
371 j[
"layer"] = aStats.layer;
372 j[
"net"] = aStats.net;
373 j[
"outline_count"] = aStats.outlineCount;
374 j[
"vertex_count"] = aStats.vertexCount;
375 j[
"triangle_count"] = aStats.triangleCount;
376 j[
"time_us"] = aStats.timeUs;
377 j[
"original_area_nm2"] = aStats.originalArea;
378 j[
"triangulated_area_nm2"] = aStats.triangulatedArea;
379 j[
"area_coverage"] = aStats.areaCoverage;
380 j[
"mean_triangle_area_nm2"] = aStats.meanTriArea;
381 j[
"stddev_triangle_area_nm2"] = aStats.stddevTriArea;
382 j[
"spikey_triangles"] = aStats.spikeyTriangles;
383 j[
"spikey_ratio"] = aStats.spikeyRatio;
388ZONE_COMPARISON CompareZone(
const std::string& aSource,
const ZONE_STATS& aCurrent,
389 const BASELINE_ZONE* aBaseline )
392 cmp.source = aSource;
393 cmp.layer = aCurrent.layer;
394 cmp.net = aCurrent.net;
395 cmp.curTriangles = aCurrent.triangleCount;
396 cmp.curSpikeyRatio = aCurrent.spikeyRatio;
397 cmp.curStddev = aCurrent.stddevTriArea;
398 cmp.curCoverage = aCurrent.areaCoverage;
402 cmp.type = CHANGE_TYPE::UNCHANGED;
406 cmp.baseTriangles = aBaseline->triangleCount;
407 cmp.baseSpikeyRatio = aBaseline->spikeyRatio;
408 cmp.baseStddev = aBaseline->stddevTriArea;
409 cmp.baseCoverage = aBaseline->areaCoverage;
411 bool coverageBroke = aCurrent.originalArea > 0.0
412 && ( aCurrent.areaCoverage < 0.99 || aCurrent.areaCoverage > 1.01 );
413 bool newFailure = aCurrent.triangleCount == 0 && aBaseline->triangleCount > 0
414 && aCurrent.originalArea > 0.0;
416 if( coverageBroke || newFailure )
418 cmp.type = CHANGE_TYPE::BREAKING;
422 double spikeyDeltaPp = cmp.spikeyDeltaPp();
423 double triangleDeltaPct = cmp.triangleDeltaPct();
424 double stddevDeltaPct = cmp.stddevDeltaPct();
426 bool hasRegression = spikeyDeltaPp > 1.0 || triangleDeltaPct > 5.0 || stddevDeltaPct > 10.0;
427 bool hasImprovement = spikeyDeltaPp < -1.0 || triangleDeltaPct < -5.0 || stddevDeltaPct < -10.0;
429 if( hasRegression && !hasImprovement )
430 cmp.type = CHANGE_TYPE::REGRESSION;
431 else if( hasImprovement && !hasRegression )
432 cmp.type = CHANGE_TYPE::IMPROVEMENT;
433 else if( hasRegression && hasImprovement )
434 cmp.type = spikeyDeltaPp > 0.0 ? CHANGE_TYPE::REGRESSION : CHANGE_TYPE::IMPROVEMENT;
436 cmp.type = CHANGE_TYPE::UNCHANGED;
442std::string FormatSign(
double aValue,
const std::string& aSuffix )
444 std::ostringstream ss;
445 ss << std::fixed << std::setprecision( 1 );
450 ss << aValue << aSuffix;
455std::string FormatZoneDetail(
const ZONE_COMPARISON& aCmp )
457 std::ostringstream ss;
458 ss <<
" " << aCmp.source <<
" " << aCmp.layer <<
" \"" << aCmp.net <<
"\"" <<
"\n";
459 ss << std::fixed << std::setprecision( 1 );
460 ss <<
" spikey: " << ( aCmp.baseSpikeyRatio * 100.0 ) <<
"% -> "
461 << ( aCmp.curSpikeyRatio * 100.0 ) <<
"% (" << FormatSign( aCmp.spikeyDeltaPp(),
"pp" )
463 ss <<
" triangles: " << aCmp.baseTriangles <<
" -> " << aCmp.curTriangles
464 <<
" (" << FormatSign( aCmp.triangleDeltaPct(),
"%" ) <<
")";
466 if( aCmp.baseStddev > 0.0 || aCmp.curStddev > 0.0 )
468 ss <<
" stddev: " << FormatSign( aCmp.stddevDeltaPct(),
"%" );
475void OutputComparisonReport(
const BASELINE_DATA& aBaseline,
476 const std::vector<ZONE_COMPARISON>& aComparisons,
477 int aTotalTriangles,
int aTotalSpikeyTri,
int aTotalZones )
479 std::vector<ZONE_COMPARISON> breaking;
480 std::vector<ZONE_COMPARISON> regressions;
481 std::vector<ZONE_COMPARISON> improvements;
484 for(
const auto& cmp : aComparisons )
488 case CHANGE_TYPE::BREAKING: breaking.push_back( cmp );
break;
489 case CHANGE_TYPE::REGRESSION: regressions.push_back( cmp );
break;
490 case CHANGE_TYPE::IMPROVEMENT: improvements.push_back( cmp );
break;
491 case CHANGE_TYPE::UNCHANGED: unchanged++;
break;
495 std::sort( improvements.begin(), improvements.end(),
496 [](
const ZONE_COMPARISON& a,
const ZONE_COMPARISON& b )
498 return a.spikeyDeltaPp() < b.spikeyDeltaPp();
501 std::sort( regressions.begin(), regressions.end(),
502 [](
const ZONE_COMPARISON& a,
const ZONE_COMPARISON& b )
504 return a.spikeyDeltaPp() > b.spikeyDeltaPp();
507 std::ostringstream report;
508 report << std::fixed << std::setprecision( 1 );
510 report <<
"\n=== Triangulation Comparison vs Baseline ===\n\n";
512 report <<
"Baseline: " << aBaseline.boardCount <<
" boards, "
513 << aBaseline.zoneCount <<
" zones\n";
514 report <<
"Current: " << aTotalZones <<
" zones\n\n";
516 double baseSpikey = aBaseline.spikeyRatio * 100.0;
517 double curSpikey = aTotalTriangles > 0
518 ?
static_cast<double>( aTotalSpikeyTri ) / aTotalTriangles * 100.0
521 report <<
"Global:\n";
522 report <<
" Triangles: " << aBaseline.totalTriangles <<
" -> " << aTotalTriangles
523 <<
" (" << FormatSign(
524 aTotalTriangles - aBaseline.totalTriangles == 0
526 : ( aTotalTriangles - aBaseline.totalTriangles )
527 /
static_cast<double>( aBaseline.totalTriangles )
531 report <<
" Spikey: " << baseSpikey <<
"% -> " << curSpikey <<
"% ("
532 << FormatSign( curSpikey - baseSpikey,
"pp" ) <<
")\n";
533 report <<
" Spikey ct: " << aBaseline.totalSpikeyTri <<
" -> " << aTotalSpikeyTri
536 report <<
"BREAKING: " << breaking.size() <<
" zones\n";
538 for(
const auto& cmp : breaking )
539 report << FormatZoneDetail( cmp ) <<
"\n";
541 if( breaking.empty() )
542 report <<
" (none)\n";
544 report <<
"\nREGRESSIONS: " << regressions.size() <<
" zones"
545 <<
" (spikey >+1pp, triangles >+5%, or stddev >+10%)\n";
549 for(
const auto& cmp : regressions )
553 report <<
" ... and " << ( regressions.size() - 20 ) <<
" more\n";
557 report << FormatZoneDetail( cmp ) <<
"\n";
561 if( regressions.empty() )
562 report <<
" (none)\n";
564 report <<
"\nIMPROVEMENTS: " << improvements.size() <<
" zones\n";
568 for(
const auto& cmp : improvements )
572 report <<
" ... and " << ( improvements.size() - 20 ) <<
" more\n";
576 report << FormatZoneDetail( cmp ) <<
"\n";
580 if( improvements.empty() )
581 report <<
" (none)\n";
583 report <<
"\nSummary: " << improvements.size() <<
" improved, "
584 << regressions.size() <<
" regressed, "
585 << breaking.size() <<
" breaking, "
586 << unchanged <<
" unchanged\n";
591 std::to_string( breaking.size() )
592 +
" zone(s) have breaking triangulation changes" );
596std::string GetTriangulationDataDir()
609 std::string dataDir = GetTriangulationDataDir();
611 if( !fs::exists( dataDir ) || fs::is_empty( dataDir ) )
613 BOOST_TEST_MESSAGE(
"No triangulation data in " << dataDir <<
", skipping benchmark" );
617 fs::path
jsonPath = fs::path( dataDir ) /
"triangulation_status.json";
618 BASELINE_DATA baseline = LoadBaseline(
jsonPath );
623 << baseline.zoneCount <<
" zones, "
624 << baseline.totalTriangles <<
" triangles" );
633 for(
const auto& entry : fs::directory_iterator( dataDir ) )
635 if( entry.path().extension() ==
".kicad_polys" )
648 std::vector<ZONE_COMPARISON> comparisons;
654 if( !ParsePolyFile( polyFile, board ) )
660 int boardTriangles = 0;
662 double boardTimeUs = 0.0;
664 const BASELINE_BOARD* baseBoard =
nullptr;
665 auto it = baseline.boards.find( board.source );
667 if( it != baseline.boards.end() )
668 baseBoard = &it->second;
670 for(
size_t zi = 0; zi < board.zones.size(); zi++ )
672 ZONE_STATS stats = ComputeZoneStats( board.zones[zi] );
675 stats.triangleCount > 0 || stats.originalArea == 0.0,
676 board.source +
" " + stats.layer +
" " + stats.net
677 +
" produced 0 triangles with non-zero area" );
679 if( stats.originalArea > 0.0 )
682 stats.areaCoverage > 0.999 && stats.areaCoverage < 1.001,
683 board.source +
" " + stats.layer +
" " + stats.net
684 +
" area coverage: " + std::to_string( stats.areaCoverage ) );
689 const BASELINE_ZONE* baseZone =
nullptr;
691 if( baseBoard && zi < baseBoard->zones.size() )
692 baseZone = &baseBoard->zones[zi];
694 comparisons.push_back( CompareZone( board.source, stats, baseZone ) );
697 boardTriangles += stats.triangleCount;
698 boardSpikey += stats.spikeyTriangles;
699 boardTimeUs +=
static_cast<double>( stats.timeUs );
723 std::string dataDir = GetTriangulationDataDir();
725 if( !fs::exists( dataDir ) || fs::is_empty( dataDir ) )
733 for(
const auto& entry : fs::directory_iterator( dataDir ) )
735 if( entry.path().extension() ==
".kicad_polys" )
753 if( !ParsePolyFile( polyFile, board ) )
756 nlohmann::json boardJson;
757 boardJson[
"source"] = board.source;
758 nlohmann::json zonesJson = nlohmann::json::array();
760 int boardTriangles = 0;
762 double boardTimeUs = 0.0;
764 for(
size_t zi = 0; zi < board.zones.size(); zi++ )
766 ZONE_STATS stats = ComputeZoneStats( board.zones[zi] );
767 zonesJson.push_back( ZoneStatsToJson( stats ) );
769 boardTriangles += stats.triangleCount;
770 boardSpikey += stats.spikeyTriangles;
771 boardTimeUs +=
static_cast<double>( stats.timeUs );
776 boardJson[
"zones"] = zonesJson;
778 nlohmann::json boardTotals;
779 boardTotals[
"triangle_count"] = boardTriangles;
780 boardTotals[
"time_us"] =
static_cast<int64_t
>( boardTimeUs );
781 boardTotals[
"spikey_ratio"] = boardTriangles > 0
782 ?
static_cast<double>( boardSpikey ) / boardTriangles
784 boardJson[
"board_totals"] = boardTotals;
810 fs::path
jsonPath = fs::path( dataDir ) /
"triangulation_status.json";
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)
double Area()
Return the area of this poly set.
bool Parse(std::stringstream &aStream) override
virtual void CacheTriangulation(bool aSimplify=false, const TASK_SUBMITTER &aSubmitter={})
Build a polygon triangulation, needed to draw a polygon on OpenGL and in some other calculations.
const TRIANGULATED_POLYGON * TriangulatedPolygon(int aIndex) const
unsigned int TriangulatedPolyCount() const
Return the number of triangulated polygons.
double Distance(const VECTOR2< extended_type > &aVector) const
Compute the distance between two vectors.
CHANGE_TYPE
Types of changes.
std::string GetTestDataRootDir()
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_SUITE_END()
std::ofstream jsonFile("pip_benchmark_results.json")
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
std::vector< fs::path > polyFiles
BOOST_TEST_MESSAGE("\n=== Real-World Polygon PIP Benchmark ===\n"<< formatTable(table))
nlohmann::json metadataJson
nlohmann::json boardsJson
nlohmann::json globalJson
BOOST_AUTO_TEST_CASE(BenchmarkAllExtractedPolygons)
std::ofstream jsonFile(jsonPath)
nlohmann::json jsonOutput
VECTOR2< int32_t > VECTOR2I