KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_triangulation_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
22#include <core/profile.h>
23#include <nlohmann/json.hpp>
24
25#include <algorithm>
26#include <cmath>
27#include <filesystem>
28#include <fstream>
29#include <map>
30#include <numeric>
31#include <sstream>
32#include <string>
33#include <vector>
34
35namespace fs = std::filesystem;
36
37namespace
38{
39
40struct ZONE_ENTRY
41{
42 std::string layer;
43 std::string net;
44 int outlineCount = 0;
45 int vertexCount = 0;
46 SHAPE_POLY_SET polySet;
47};
48
49
50struct BOARD_ENTRY
51{
52 std::string source;
53 std::vector<ZONE_ENTRY> zones;
54};
55
56
57struct ZONE_STATS
58{
59 std::string layer;
60 std::string net;
61 int outlineCount = 0;
62 int vertexCount = 0;
63 int triangleCount = 0;
64 int64_t timeUs = 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;
72};
73
74
75struct BASELINE_ZONE
76{
77 std::string layer;
78 std::string net;
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;
85};
86
87
88struct BASELINE_BOARD
89{
90 std::vector<BASELINE_ZONE> zones;
91};
92
93
94struct BASELINE_DATA
95{
96 std::map<std::string, BASELINE_BOARD> boards;
97 int totalTriangles = 0;
98 int totalSpikeyTri = 0;
99 double spikeyRatio = 0.0;
100 int zoneCount = 0;
101 int boardCount = 0;
102 bool valid = false;
103};
104
105
106enum class CHANGE_TYPE
107{
108 BREAKING,
109 REGRESSION,
110 IMPROVEMENT,
111 UNCHANGED
112};
113
114
115struct ZONE_COMPARISON
116{
117 CHANGE_TYPE type = CHANGE_TYPE::UNCHANGED;
118 std::string source;
119 std::string layer;
120 std::string net;
121
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;
130
131 double spikeyDeltaPp() const { return ( curSpikeyRatio - baseSpikeyRatio ) * 100.0; }
132
133 double triangleDeltaPct() const
134 {
135 if( baseTriangles == 0 )
136 return curTriangles == 0 ? 0.0 : 100.0;
137
138 return ( curTriangles - baseTriangles ) / static_cast<double>( baseTriangles ) * 100.0;
139 }
140
141 double stddevDeltaPct() const
142 {
143 if( baseStddev == 0.0 )
144 return curStddev == 0.0 ? 0.0 : 100.0;
145
146 return ( curStddev - baseStddev ) / baseStddev * 100.0;
147 }
148};
149
150
151BASELINE_DATA LoadBaseline( const fs::path& aJsonPath )
152{
153 BASELINE_DATA baseline;
154
155 if( !fs::exists( aJsonPath ) )
156 return baseline;
157
158 std::ifstream file( aJsonPath );
159
160 if( !file.is_open() )
161 return baseline;
162
163 nlohmann::json j;
164
165 try
166 {
167 j = nlohmann::json::parse( file );
168 }
169 catch( const nlohmann::json::exception& )
170 {
171 return baseline;
172 }
173
174 if( j.contains( "metadata" ) )
175 {
176 baseline.boardCount = j["metadata"].value( "board_count", 0 );
177 baseline.zoneCount = j["metadata"].value( "zone_count", 0 );
178 }
179
180 if( j.contains( "global" ) )
181 {
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 );
185 }
186
187 if( j.contains( "boards" ) )
188 {
189 for( const auto& boardJson : j["boards"] )
190 {
191 std::string source = boardJson.value( "source", "" );
192 BASELINE_BOARD board;
193
194 if( boardJson.contains( "zones" ) )
195 {
196 for( const auto& zoneJson : boardJson["zones"] )
197 {
198 BASELINE_ZONE zone;
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 );
208 }
209 }
210
211 baseline.boards[source] = std::move( board );
212 }
213 }
214
215 baseline.valid = true;
216 return baseline;
217}
218
219
220bool ParsePolyFile( const fs::path& aPath, BOARD_ENTRY& aBoard )
221{
222 std::ifstream file( aPath );
223
224 if( !file.is_open() )
225 return false;
226
227 std::string content( ( std::istreambuf_iterator<char>( file ) ),
228 std::istreambuf_iterator<char>() );
229
230 size_t srcStart = content.find( "(source \"" );
231
232 if( srcStart != std::string::npos )
233 {
234 srcStart += 9;
235 size_t srcEnd = content.find( "\")", srcStart );
236
237 if( srcEnd != std::string::npos )
238 aBoard.source = content.substr( srcStart, srcEnd - srcStart );
239 }
240
241 size_t zonePos = 0;
242
243 while( ( zonePos = content.find( "(zone (layer \"", zonePos ) ) != std::string::npos )
244 {
245 ZONE_ENTRY entry;
246
247 size_t layerStart = zonePos + 14;
248 size_t layerEnd = content.find( "\")", layerStart );
249 entry.layer = content.substr( layerStart, layerEnd - layerStart );
250
251 size_t netStart = content.find( "(net \"", layerEnd );
252
253 if( netStart != std::string::npos )
254 {
255 netStart += 6;
256 size_t netEnd = content.find( "\")", netStart );
257 entry.net = content.substr( netStart, netEnd - netStart );
258 }
259
260 size_t ocStart = content.find( "(outline_count ", layerEnd );
261
262 if( ocStart != std::string::npos )
263 {
264 ocStart += 15;
265 entry.outlineCount = std::stoi( content.substr( ocStart ) );
266 }
267
268 size_t vcStart = content.find( "(vertex_count ", layerEnd );
269
270 if( vcStart != std::string::npos )
271 {
272 vcStart += 14;
273 entry.vertexCount = std::stoi( content.substr( vcStart ) );
274 }
275
276 size_t polysetStart = content.find( "polyset ", zonePos );
277
278 if( polysetStart != std::string::npos )
279 {
280 std::string remainder = content.substr( polysetStart );
281 std::stringstream ss( remainder );
282
283 if( entry.polySet.Parse( ss ) )
284 aBoard.zones.push_back( std::move( entry ) );
285 }
286
287 zonePos = layerEnd + 1;
288 }
289
290 return !aBoard.zones.empty();
291}
292
293
294ZONE_STATS ComputeZoneStats( ZONE_ENTRY& aZone )
295{
296 ZONE_STATS stats;
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();
302
303 PROF_TIMER timer;
304 aZone.polySet.CacheTriangulation();
305 timer.Stop();
306 stats.timeUs = static_cast<int64_t>( timer.msecs() * 1000.0 );
307
308 std::vector<double> triAreas;
309
310 for( unsigned int i = 0; i < aZone.polySet.TriangulatedPolyCount(); i++ )
311 {
312 const auto* triPoly = aZone.polySet.TriangulatedPolygon( static_cast<int>( i ) );
313
314 for( const auto& tri : triPoly->Triangles() )
315 triAreas.push_back( tri.Area() );
316 }
317
318 stats.triangleCount = static_cast<int>( triAreas.size() );
319 stats.triangulatedArea = std::accumulate( triAreas.begin(), triAreas.end(), 0.0 );
320
321 if( stats.originalArea > 0.0 )
322 stats.areaCoverage = stats.triangulatedArea / stats.originalArea;
323
324 if( !triAreas.empty() )
325 {
326 stats.meanTriArea = stats.triangulatedArea / static_cast<double>( triAreas.size() );
327
328 double sumSqDiff = 0.0;
329
330 for( double a : triAreas )
331 {
332 double diff = a - stats.meanTriArea;
333 sumSqDiff += diff * diff;
334 }
335
336 stats.stddevTriArea = std::sqrt( sumSqDiff / static_cast<double>( triAreas.size() ) );
337 }
338
339 for( unsigned int i = 0; i < aZone.polySet.TriangulatedPolyCount(); i++ )
340 {
341 const auto* triPoly = aZone.polySet.TriangulatedPolygon( static_cast<int>( i ) );
342
343 for( const auto& tri : triPoly->Triangles() )
344 {
345 VECTOR2I pa = tri.GetPoint( 0 );
346 VECTOR2I pb = tri.GetPoint( 1 );
347 VECTOR2I pc = tri.GetPoint( 2 );
348
349 double ab = pa.Distance( pb );
350 double bc = pb.Distance( pc );
351 double ca = pc.Distance( pa );
352
353 double longest = std::max( { ab, bc, ca } );
354 double shortest = std::min( { ab, bc, ca } );
355
356 if( shortest > 0.0 && longest / shortest > 10.0 )
357 stats.spikeyTriangles++;
358 }
359 }
360
361 if( stats.triangleCount > 0 )
362 stats.spikeyRatio = static_cast<double>( stats.spikeyTriangles ) / stats.triangleCount;
363
364 return stats;
365}
366
367
368nlohmann::json ZoneStatsToJson( const ZONE_STATS& aStats )
369{
370 nlohmann::json j;
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;
384 return j;
385}
386
387
388ZONE_COMPARISON CompareZone( const std::string& aSource, const ZONE_STATS& aCurrent,
389 const BASELINE_ZONE* aBaseline )
390{
391 ZONE_COMPARISON cmp;
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;
399
400 if( !aBaseline )
401 {
402 cmp.type = CHANGE_TYPE::UNCHANGED;
403 return cmp;
404 }
405
406 cmp.baseTriangles = aBaseline->triangleCount;
407 cmp.baseSpikeyRatio = aBaseline->spikeyRatio;
408 cmp.baseStddev = aBaseline->stddevTriArea;
409 cmp.baseCoverage = aBaseline->areaCoverage;
410
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;
415
416 if( coverageBroke || newFailure )
417 {
418 cmp.type = CHANGE_TYPE::BREAKING;
419 return cmp;
420 }
421
422 double spikeyDeltaPp = cmp.spikeyDeltaPp();
423 double triangleDeltaPct = cmp.triangleDeltaPct();
424 double stddevDeltaPct = cmp.stddevDeltaPct();
425
426 bool hasRegression = spikeyDeltaPp > 1.0 || triangleDeltaPct > 5.0 || stddevDeltaPct > 10.0;
427 bool hasImprovement = spikeyDeltaPp < -1.0 || triangleDeltaPct < -5.0 || stddevDeltaPct < -10.0;
428
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;
435 else
436 cmp.type = CHANGE_TYPE::UNCHANGED;
437
438 return cmp;
439}
440
441
442std::string FormatSign( double aValue, const std::string& aSuffix )
443{
444 std::ostringstream ss;
445 ss << std::fixed << std::setprecision( 1 );
446
447 if( aValue > 0.0 )
448 ss << "+";
449
450 ss << aValue << aSuffix;
451 return ss.str();
452}
453
454
455std::string FormatZoneDetail( const ZONE_COMPARISON& aCmp )
456{
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" )
462 << ")";
463 ss << " triangles: " << aCmp.baseTriangles << " -> " << aCmp.curTriangles
464 << " (" << FormatSign( aCmp.triangleDeltaPct(), "%" ) << ")";
465
466 if( aCmp.baseStddev > 0.0 || aCmp.curStddev > 0.0 )
467 {
468 ss << " stddev: " << FormatSign( aCmp.stddevDeltaPct(), "%" );
469 }
470
471 return ss.str();
472}
473
474
475void OutputComparisonReport( const BASELINE_DATA& aBaseline,
476 const std::vector<ZONE_COMPARISON>& aComparisons,
477 int aTotalTriangles, int aTotalSpikeyTri, int aTotalZones )
478{
479 std::vector<ZONE_COMPARISON> breaking;
480 std::vector<ZONE_COMPARISON> regressions;
481 std::vector<ZONE_COMPARISON> improvements;
482 int unchanged = 0;
483
484 for( const auto& cmp : aComparisons )
485 {
486 switch( cmp.type )
487 {
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;
492 }
493 }
494
495 std::sort( improvements.begin(), improvements.end(),
496 []( const ZONE_COMPARISON& a, const ZONE_COMPARISON& b )
497 {
498 return a.spikeyDeltaPp() < b.spikeyDeltaPp();
499 } );
500
501 std::sort( regressions.begin(), regressions.end(),
502 []( const ZONE_COMPARISON& a, const ZONE_COMPARISON& b )
503 {
504 return a.spikeyDeltaPp() > b.spikeyDeltaPp();
505 } );
506
507 std::ostringstream report;
508 report << std::fixed << std::setprecision( 1 );
509
510 report << "\n=== Triangulation Comparison vs Baseline ===\n\n";
511
512 report << "Baseline: " << aBaseline.boardCount << " boards, "
513 << aBaseline.zoneCount << " zones\n";
514 report << "Current: " << aTotalZones << " zones\n\n";
515
516 double baseSpikey = aBaseline.spikeyRatio * 100.0;
517 double curSpikey = aTotalTriangles > 0
518 ? static_cast<double>( aTotalSpikeyTri ) / aTotalTriangles * 100.0
519 : 0.0;
520
521 report << "Global:\n";
522 report << " Triangles: " << aBaseline.totalTriangles << " -> " << aTotalTriangles
523 << " (" << FormatSign(
524 aTotalTriangles - aBaseline.totalTriangles == 0
525 ? 0.0
526 : ( aTotalTriangles - aBaseline.totalTriangles )
527 / static_cast<double>( aBaseline.totalTriangles )
528 * 100.0,
529 "%" )
530 << ")\n";
531 report << " Spikey: " << baseSpikey << "% -> " << curSpikey << "% ("
532 << FormatSign( curSpikey - baseSpikey, "pp" ) << ")\n";
533 report << " Spikey ct: " << aBaseline.totalSpikeyTri << " -> " << aTotalSpikeyTri
534 << "\n\n";
535
536 report << "BREAKING: " << breaking.size() << " zones\n";
537
538 for( const auto& cmp : breaking )
539 report << FormatZoneDetail( cmp ) << "\n";
540
541 if( breaking.empty() )
542 report << " (none)\n";
543
544 report << "\nREGRESSIONS: " << regressions.size() << " zones"
545 << " (spikey >+1pp, triangles >+5%, or stddev >+10%)\n";
546
547 int shown = 0;
548
549 for( const auto& cmp : regressions )
550 {
551 if( shown >= 20 )
552 {
553 report << " ... and " << ( regressions.size() - 20 ) << " more\n";
554 break;
555 }
556
557 report << FormatZoneDetail( cmp ) << "\n";
558 shown++;
559 }
560
561 if( regressions.empty() )
562 report << " (none)\n";
563
564 report << "\nIMPROVEMENTS: " << improvements.size() << " zones\n";
565
566 shown = 0;
567
568 for( const auto& cmp : improvements )
569 {
570 if( shown >= 20 )
571 {
572 report << " ... and " << ( improvements.size() - 20 ) << " more\n";
573 break;
574 }
575
576 report << FormatZoneDetail( cmp ) << "\n";
577 shown++;
578 }
579
580 if( improvements.empty() )
581 report << " (none)\n";
582
583 report << "\nSummary: " << improvements.size() << " improved, "
584 << regressions.size() << " regressed, "
585 << breaking.size() << " breaking, "
586 << unchanged << " unchanged\n";
587
588 BOOST_TEST_MESSAGE( report.str() );
589
590 BOOST_CHECK_MESSAGE( breaking.empty(),
591 std::to_string( breaking.size() )
592 + " zone(s) have breaking triangulation changes" );
593}
594
595
596std::string GetTriangulationDataDir()
597{
598 return KI_TEST::GetTestDataRootDir() + "triangulation/";
599}
600
601}; // anonymous namespace
602
603
604BOOST_AUTO_TEST_SUITE( TriangulationBenchmark )
605
606
607BOOST_AUTO_TEST_CASE( BenchmarkAllExtractedPolygons )
608{
609 std::string dataDir = GetTriangulationDataDir();
610
611 if( !fs::exists( dataDir ) || fs::is_empty( dataDir ) )
612 {
613 BOOST_TEST_MESSAGE( "No triangulation data in " << dataDir << ", skipping benchmark" );
614 return;
615 }
616
617 fs::path jsonPath = fs::path( dataDir ) / "triangulation_status.json";
618 BASELINE_DATA baseline = LoadBaseline( jsonPath );
619
620 if( baseline.valid )
621 {
622 BOOST_TEST_MESSAGE( "Loaded baseline: " << baseline.boardCount << " boards, "
623 << baseline.zoneCount << " zones, "
624 << baseline.totalTriangles << " triangles" );
625 }
626 else
627 {
628 BOOST_TEST_MESSAGE( "No baseline found, running without comparison" );
629 }
630
631 std::vector<fs::path> polyFiles;
632
633 for( const auto& entry : fs::directory_iterator( dataDir ) )
634 {
635 if( entry.path().extension() == ".kicad_polys" )
636 polyFiles.push_back( entry.path() );
637 }
638
639 std::sort( polyFiles.begin(), polyFiles.end() );
640
641 BOOST_TEST_MESSAGE( "Found " << polyFiles.size() << " polygon files" );
642
643 int totalTriangles = 0;
644 int totalSpikeyTri = 0;
645 int totalZones = 0;
646 double totalTimeUs = 0.0;
647
648 std::vector<ZONE_COMPARISON> comparisons;
649
650 for( const auto& polyFile : polyFiles )
651 {
652 BOARD_ENTRY board;
653
654 if( !ParsePolyFile( polyFile, board ) )
655 {
656 BOOST_TEST_MESSAGE( "Failed to parse: " << polyFile.filename() );
657 continue;
658 }
659
660 int boardTriangles = 0;
661 int boardSpikey = 0;
662 double boardTimeUs = 0.0;
663
664 const BASELINE_BOARD* baseBoard = nullptr;
665 auto it = baseline.boards.find( board.source );
666
667 if( it != baseline.boards.end() )
668 baseBoard = &it->second;
669
670 for( size_t zi = 0; zi < board.zones.size(); zi++ )
671 {
672 ZONE_STATS stats = ComputeZoneStats( board.zones[zi] );
673
675 stats.triangleCount > 0 || stats.originalArea == 0.0,
676 board.source + " " + stats.layer + " " + stats.net
677 + " produced 0 triangles with non-zero area" );
678
679 if( stats.originalArea > 0.0 )
680 {
682 stats.areaCoverage > 0.999 && stats.areaCoverage < 1.001,
683 board.source + " " + stats.layer + " " + stats.net
684 + " area coverage: " + std::to_string( stats.areaCoverage ) );
685 }
686
687 if( baseline.valid )
688 {
689 const BASELINE_ZONE* baseZone = nullptr;
690
691 if( baseBoard && zi < baseBoard->zones.size() )
692 baseZone = &baseBoard->zones[zi];
693
694 comparisons.push_back( CompareZone( board.source, stats, baseZone ) );
695 }
696
697 boardTriangles += stats.triangleCount;
698 boardSpikey += stats.spikeyTriangles;
699 boardTimeUs += static_cast<double>( stats.timeUs );
700 totalZones++;
701 }
702
703 totalTriangles += boardTriangles;
704 totalSpikeyTri += boardSpikey;
705 totalTimeUs += boardTimeUs;
706 }
707
708 BOOST_TEST_MESSAGE( "Total triangles: " << totalTriangles
709 << " Spikey: " << totalSpikeyTri
710 << " (" << ( totalTriangles > 0
712 : 0.0 )
713 << "%)" );
714 BOOST_TEST_MESSAGE( "Total time: " << totalTimeUs / 1000.0 << " ms" );
715
716 if( baseline.valid )
717 OutputComparisonReport( baseline, comparisons, totalTriangles, totalSpikeyTri, totalZones );
718}
719
720
721BOOST_AUTO_TEST_CASE( UpdateTriangulationStatus, * boost::unit_test::disabled() )
722{
723 std::string dataDir = GetTriangulationDataDir();
724
725 if( !fs::exists( dataDir ) || fs::is_empty( dataDir ) )
726 {
727 BOOST_TEST_MESSAGE( "No triangulation data in " << dataDir << ", skipping" );
728 return;
729 }
730
731 std::vector<fs::path> polyFiles;
732
733 for( const auto& entry : fs::directory_iterator( dataDir ) )
734 {
735 if( entry.path().extension() == ".kicad_polys" )
736 polyFiles.push_back( entry.path() );
737 }
738
739 std::sort( polyFiles.begin(), polyFiles.end() );
740
743 int totalZones = 0;
744 double totalTimeUs = 0.0;
745 double totalArea = 0.0;
746
747 nlohmann::json boardsJson = nlohmann::json::array();
748
749 for( const auto& polyFile : polyFiles )
750 {
751 BOARD_ENTRY board;
752
753 if( !ParsePolyFile( polyFile, board ) )
754 continue;
755
756 nlohmann::json boardJson;
757 boardJson["source"] = board.source;
758 nlohmann::json zonesJson = nlohmann::json::array();
759
760 int boardTriangles = 0;
761 int boardSpikey = 0;
762 double boardTimeUs = 0.0;
763
764 for( size_t zi = 0; zi < board.zones.size(); zi++ )
765 {
766 ZONE_STATS stats = ComputeZoneStats( board.zones[zi] );
767 zonesJson.push_back( ZoneStatsToJson( stats ) );
768
769 boardTriangles += stats.triangleCount;
770 boardSpikey += stats.spikeyTriangles;
771 boardTimeUs += static_cast<double>( stats.timeUs );
772 totalArea += stats.triangulatedArea;
773 totalZones++;
774 }
775
776 boardJson["zones"] = zonesJson;
777
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
783 : 0.0;
784 boardJson["board_totals"] = boardTotals;
785 boardsJson.push_back( boardJson );
786
787 totalTriangles += boardTriangles;
788 totalSpikeyTri += boardSpikey;
789 totalTimeUs += boardTimeUs;
790 }
791
792 nlohmann::json globalJson;
793 globalJson["total_triangles"] = totalTriangles;
794 globalJson["total_time_us"] = static_cast<int64_t>( totalTimeUs );
795 globalJson["total_area_nm2"] = totalArea;
796 globalJson["total_spikey_triangles"] = totalSpikeyTri;
797 globalJson["spikey_ratio"] = totalTriangles > 0
798 ? static_cast<double>( totalSpikeyTri ) / totalTriangles
799 : 0.0;
800
801 nlohmann::json metadataJson;
802 metadataJson["board_count"] = static_cast<int>( polyFiles.size() );
803 metadataJson["zone_count"] = totalZones;
804
805 nlohmann::json jsonOutput;
806 jsonOutput["metadata"] = metadataJson;
807 jsonOutput["global"] = globalJson;
808 jsonOutput["boards"] = boardsJson;
809
810 fs::path jsonPath = fs::path( dataDir ) / "triangulation_status.json";
811
812 std::ofstream jsonFile( jsonPath );
813 jsonFile << jsonOutput.dump( 2 ) << "\n";
814 BOOST_CHECK( jsonFile.good() );
815 jsonFile.close();
816
817 BOOST_TEST_MESSAGE( "Wrote triangulation status to " << jsonPath );
818 BOOST_TEST_MESSAGE( "Boards: " << polyFiles.size() << " Zones: " << totalZones
819 << " Triangles: " << totalTriangles );
820}
821
822
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
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.
Definition vector2d.h:553
CHANGE_TYPE
Types of changes.
Definition commit.h:41
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
Definition vector2d.h:687