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 <https://www.gnu.org/licenses/>.
17 */
18
20
23#include <core/profile.h>
24
25#include "geom_test_utils.h"
26#include <nlohmann/json.hpp>
27
28#include <algorithm>
29#include <cmath>
30#include <cstdlib>
31#include <filesystem>
32#include <fstream>
33#include <map>
34#include <numeric>
35#include <sstream>
36#include <string>
37#include <vector>
38
39namespace fs = std::filesystem;
40
41namespace
42{
43
44struct ZONE_ENTRY
45{
46 std::string layer;
47 std::string net;
48 int outlineCount = 0;
49 int vertexCount = 0;
50 SHAPE_POLY_SET polySet;
51};
52
53
54struct BOARD_ENTRY
55{
56 std::string source;
57 std::vector<ZONE_ENTRY> zones;
58};
59
60
61struct ZONE_STATS
62{
63 std::string layer;
64 std::string net;
65 int outlineCount = 0;
66 int vertexCount = 0;
67 int triangleCount = 0;
68 int64_t timeUs = 0;
69 double originalArea = 0.0;
70 double triangulatedArea = 0.0;
71 double areaCoverage = 0.0;
72 double meanTriArea = 0.0;
73 double stddevTriArea = 0.0;
74 int spikeyTriangles = 0;
75 double spikeyRatio = 0.0;
76
77 // Regularity metrics. minAngle percentiles describe the tail of thin triangles; the
78 // worst 1% (p1) is the sliver signal, p50 the typical shape. Angles are degrees.
79 double minAnglePctl1 = 0.0;
80 double minAnglePctl5 = 0.0;
81 double minAnglePctl50 = 0.0;
82 int trisBelow5Deg = 0;
83 int trisBelow10Deg = 0;
84 int trisBelow15Deg = 0;
85 double meanRadiusRatio = 0.0;
86
87 // Per-triangle minimum interior angles, retained so the global run can aggregate
88 // corpus-wide percentiles rather than averaging per-zone summaries.
89 std::vector<double> minAngles;
90};
91
92
93struct BASELINE_ZONE
94{
95 std::string layer;
96 std::string net;
97 int triangleCount = 0;
98 double areaCoverage = 0.0;
99 double spikeyRatio = 0.0;
100 double stddevTriArea = 0.0;
101 int spikeyTriangles = 0;
102 double originalArea = 0.0;
103 double minAnglePctl1 = 0.0;
104 double minAnglePctl5 = 0.0;
105 int trisBelow10Deg = 0;
106 int64_t timeUs = 0;
107};
108
109
110struct BASELINE_BOARD
111{
112 std::vector<BASELINE_ZONE> zones;
113};
114
115
116struct BASELINE_DATA
117{
118 std::map<std::string, BASELINE_BOARD> boards;
119 int totalTriangles = 0;
120 int totalSpikeyTri = 0;
121 int64_t totalTimeUs = 0;
122 double spikeyRatio = 0.0;
123 int zoneCount = 0;
124 int boardCount = 0;
125 bool valid = false;
126};
127
128
129enum class CHANGE_TYPE
130{
131 BREAKING,
132 REGRESSION,
133 IMPROVEMENT,
135};
136
137
138struct ZONE_COMPARISON
139{
140 CHANGE_TYPE type = CHANGE_TYPE::UNCHANGED;
141 std::string source;
142 std::string layer;
143 std::string net;
144
145 int baseTriangles = 0;
146 int curTriangles = 0;
147 double baseSpikeyRatio = 0.0;
148 double curSpikeyRatio = 0.0;
149 double baseStddev = 0.0;
150 double curStddev = 0.0;
151 double baseCoverage = 0.0;
152 double curCoverage = 0.0;
153 double baseMinAngleP5 = 0.0;
154 double curMinAngleP5 = 0.0;
155 int baseBelow10Deg = 0;
156 int curBelow10Deg = 0;
157 int64_t baseTimeUs = 0;
158 int64_t curTimeUs = 0;
159
160 double spikeyDeltaPp() const { return ( curSpikeyRatio - baseSpikeyRatio ) * 100.0; }
161
162 // Positive means the current run's worst-tail triangles opened up (more regular).
163 double minAngleP5DeltaDeg() const { return curMinAngleP5 - baseMinAngleP5; }
164
165 double triangleDeltaPct() const
166 {
167 if( baseTriangles == 0 )
168 return curTriangles == 0 ? 0.0 : 100.0;
169
170 return ( curTriangles - baseTriangles ) / static_cast<double>( baseTriangles ) * 100.0;
171 }
172
173 double stddevDeltaPct() const
174 {
175 if( baseStddev == 0.0 )
176 return curStddev == 0.0 ? 0.0 : 100.0;
177
178 return ( curStddev - baseStddev ) / baseStddev * 100.0;
179 }
180
181 double timeDeltaPct() const
182 {
183 if( baseTimeUs == 0 )
184 return curTimeUs == 0 ? 0.0 : 100.0;
185
186 return static_cast<double>( curTimeUs - baseTimeUs ) / static_cast<double>( baseTimeUs )
187 * 100.0;
188 }
189};
190
191
192BASELINE_DATA LoadBaseline( const fs::path& aJsonPath )
193{
194 BASELINE_DATA baseline;
195
196 if( !fs::exists( aJsonPath ) )
197 return baseline;
198
199 std::ifstream file( aJsonPath );
200
201 if( !file.is_open() )
202 return baseline;
203
204 nlohmann::json j;
205
206 try
207 {
208 j = nlohmann::json::parse( file );
209 }
210 catch( const nlohmann::json::exception& )
211 {
212 return baseline;
213 }
214
215 if( j.contains( "metadata" ) )
216 {
217 baseline.boardCount = j["metadata"].value( "board_count", 0 );
218 baseline.zoneCount = j["metadata"].value( "zone_count", 0 );
219 }
220
221 if( j.contains( "global" ) )
222 {
223 baseline.totalTriangles = j["global"].value( "total_triangles", 0 );
224 baseline.totalSpikeyTri = j["global"].value( "total_spikey_triangles", 0 );
225 baseline.totalTimeUs = j["global"].value( "total_time_us", (int64_t) 0 );
226 baseline.spikeyRatio = j["global"].value( "spikey_ratio", 0.0 );
227 }
228
229 if( j.contains( "boards" ) )
230 {
231 for( const auto& boardJson : j["boards"] )
232 {
233 std::string source = boardJson.value( "source", "" );
234 BASELINE_BOARD board;
235
236 if( boardJson.contains( "zones" ) )
237 {
238 for( const auto& zoneJson : boardJson["zones"] )
239 {
240 BASELINE_ZONE zone;
241 zone.layer = zoneJson.value( "layer", "" );
242 zone.net = zoneJson.value( "net", "" );
243 zone.triangleCount = zoneJson.value( "triangle_count", 0 );
244 zone.areaCoverage = zoneJson.value( "area_coverage", 0.0 );
245 zone.spikeyRatio = zoneJson.value( "spikey_ratio", 0.0 );
246 zone.stddevTriArea = zoneJson.value( "stddev_triangle_area_nm2", 0.0 );
247 zone.spikeyTriangles = zoneJson.value( "spikey_triangles", 0 );
248 zone.originalArea = zoneJson.value( "original_area_nm2", 0.0 );
249 zone.minAnglePctl1 = zoneJson.value( "min_angle_p1_deg", 0.0 );
250 zone.minAnglePctl5 = zoneJson.value( "min_angle_p5_deg", 0.0 );
251 zone.trisBelow10Deg = zoneJson.value( "tris_below_10deg", 0 );
252 zone.timeUs = zoneJson.value( "time_us", (int64_t) 0 );
253 board.zones.push_back( zone );
254 }
255 }
256
257 baseline.boards[source] = std::move( board );
258 }
259 }
260
261 baseline.valid = true;
262 return baseline;
263}
264
265
266bool ParsePolyFile( const fs::path& aPath, BOARD_ENTRY& aBoard )
267{
268 std::ifstream file( aPath );
269
270 if( !file.is_open() )
271 return false;
272
273 std::string content( ( std::istreambuf_iterator<char>( file ) ),
274 std::istreambuf_iterator<char>() );
275
276 size_t srcStart = content.find( "(source \"" );
277
278 if( srcStart != std::string::npos )
279 {
280 srcStart += 9;
281 size_t srcEnd = content.find( "\")", srcStart );
282
283 if( srcEnd != std::string::npos )
284 aBoard.source = content.substr( srcStart, srcEnd - srcStart );
285 }
286
287 size_t zonePos = 0;
288
289 while( ( zonePos = content.find( "(zone (layer \"", zonePos ) ) != std::string::npos )
290 {
291 ZONE_ENTRY entry;
292
293 size_t layerStart = zonePos + 14;
294 size_t layerEnd = content.find( "\")", layerStart );
295 entry.layer = content.substr( layerStart, layerEnd - layerStart );
296
297 size_t netStart = content.find( "(net \"", layerEnd );
298
299 if( netStart != std::string::npos )
300 {
301 netStart += 6;
302 size_t netEnd = content.find( "\")", netStart );
303 entry.net = content.substr( netStart, netEnd - netStart );
304 }
305
306 size_t ocStart = content.find( "(outline_count ", layerEnd );
307
308 if( ocStart != std::string::npos )
309 {
310 ocStart += 15;
311 entry.outlineCount = std::stoi( content.substr( ocStart ) );
312 }
313
314 size_t vcStart = content.find( "(vertex_count ", layerEnd );
315
316 if( vcStart != std::string::npos )
317 {
318 vcStart += 14;
319 entry.vertexCount = std::stoi( content.substr( vcStart ) );
320 }
321
322 size_t polysetStart = content.find( "polyset ", zonePos );
323
324 if( polysetStart != std::string::npos )
325 {
326 std::string remainder = content.substr( polysetStart );
327 std::stringstream ss( remainder );
328
329 if( entry.polySet.Parse( ss ) )
330 aBoard.zones.push_back( std::move( entry ) );
331 }
332
333 zonePos = layerEnd + 1;
334 }
335
336 return !aBoard.zones.empty();
337}
338
339
340// Radius ratio 2r/R normalized to [0,1]; 1.0 is equilateral, 0.0 is degenerate.
341double TriangleRadiusRatio( const VECTOR2I& a, const VECTOR2I& b, const VECTOR2I& c )
342{
343 double ab = a.Distance( b );
344 double bc = b.Distance( c );
345 double ca = c.Distance( a );
346
347 double s = ( ab + bc + ca ) / 2.0;
348
349 if( s <= 0.0 || ab <= 0.0 || bc <= 0.0 || ca <= 0.0 )
350 return 0.0;
351
352 double area = std::sqrt( std::max( 0.0, s * ( s - ab ) * ( s - bc ) * ( s - ca ) ) );
353
354 return 8.0 * area * area / ( s * ab * bc * ca );
355}
356
357
358// Linear-interpolated percentile over a pre-sorted ascending vector.
359double Percentile( const std::vector<double>& aSorted, double aPct )
360{
361 if( aSorted.empty() )
362 return 0.0;
363
364 if( aSorted.size() == 1 )
365 return aSorted.front();
366
367 double rank = aPct / 100.0 * static_cast<double>( aSorted.size() - 1 );
368 size_t lo = static_cast<size_t>( std::floor( rank ) );
369 size_t hi = static_cast<size_t>( std::ceil( rank ) );
370 double frac = rank - static_cast<double>( lo );
371
372 return aSorted[lo] + frac * ( aSorted[hi] - aSorted[lo] );
373}
374
375
376ZONE_STATS ComputeZoneStats( ZONE_ENTRY& aZone )
377{
378 ZONE_STATS stats;
379 stats.layer = aZone.layer;
380 stats.net = aZone.net;
381 stats.outlineCount = aZone.outlineCount;
382 stats.vertexCount = aZone.vertexCount;
383 stats.originalArea = aZone.polySet.Area();
384
385 // The geometry walk below needs a triangulation anyway, and aZone.polySet has never been
386 // triangulated, so timing it here is a cold measurement that costs no extra passes
387 PROF_TIMER timer;
388 aZone.polySet.CacheTriangulation();
389 timer.Stop();
390 stats.timeUs = static_cast<int64_t>( timer.msecs() * 1000.0 );
391
392 std::vector<double> triAreas;
393
394 for( unsigned int i = 0; i < aZone.polySet.TriangulatedPolyCount(); i++ )
395 {
396 const auto* triPoly = aZone.polySet.TriangulatedPolygon( static_cast<int>( i ) );
397
398 for( const auto& tri : triPoly->Triangles() )
399 triAreas.push_back( tri.Area() );
400 }
401
402 stats.triangleCount = static_cast<int>( triAreas.size() );
403 stats.triangulatedArea = std::accumulate( triAreas.begin(), triAreas.end(), 0.0 );
404
405 if( stats.originalArea > 0.0 )
406 stats.areaCoverage = stats.triangulatedArea / stats.originalArea;
407
408 if( !triAreas.empty() )
409 {
410 stats.meanTriArea = stats.triangulatedArea / static_cast<double>( triAreas.size() );
411
412 double sumSqDiff = 0.0;
413
414 for( double a : triAreas )
415 {
416 double diff = a - stats.meanTriArea;
417 sumSqDiff += diff * diff;
418 }
419
420 stats.stddevTriArea = std::sqrt( sumSqDiff / static_cast<double>( triAreas.size() ) );
421 }
422
423 double radiusRatioSum = 0.0;
424
425 for( unsigned int i = 0; i < aZone.polySet.TriangulatedPolyCount(); i++ )
426 {
427 const auto* triPoly = aZone.polySet.TriangulatedPolygon( static_cast<int>( i ) );
428
429 for( const auto& tri : triPoly->Triangles() )
430 {
431 VECTOR2I pa = tri.GetPoint( 0 );
432 VECTOR2I pb = tri.GetPoint( 1 );
433 VECTOR2I pc = tri.GetPoint( 2 );
434
435 if( KIGEOM::IsSliverTriangle( pa, pb, pc ) )
436 stats.spikeyTriangles++;
437
438 double minAngle = GEOM_TEST::TriangleMinAngleDeg( pa, pb, pc );
439 stats.minAngles.push_back( minAngle );
440
441 if( minAngle < 5.0 )
442 stats.trisBelow5Deg++;
443
444 if( minAngle < 10.0 )
445 stats.trisBelow10Deg++;
446
447 if( minAngle < 15.0 )
448 stats.trisBelow15Deg++;
449
450 radiusRatioSum += TriangleRadiusRatio( pa, pb, pc );
451 }
452 }
453
454 if( stats.triangleCount > 0 )
455 {
456 stats.spikeyRatio = static_cast<double>( stats.spikeyTriangles ) / stats.triangleCount;
457 stats.meanRadiusRatio = radiusRatioSum / stats.triangleCount;
458
459 std::vector<double> sorted = stats.minAngles;
460 std::sort( sorted.begin(), sorted.end() );
461 stats.minAnglePctl1 = Percentile( sorted, 1.0 );
462 stats.minAnglePctl5 = Percentile( sorted, 5.0 );
463 stats.minAnglePctl50 = Percentile( sorted, 50.0 );
464 }
465
466 return stats;
467}
468
469
470nlohmann::json ZoneStatsToJson( const ZONE_STATS& aStats )
471{
472 nlohmann::json j;
473 j["layer"] = aStats.layer;
474 j["net"] = aStats.net;
475 j["outline_count"] = aStats.outlineCount;
476 j["vertex_count"] = aStats.vertexCount;
477 j["triangle_count"] = aStats.triangleCount;
478 j["time_us"] = aStats.timeUs;
479 j["original_area_nm2"] = aStats.originalArea;
480 j["triangulated_area_nm2"] = aStats.triangulatedArea;
481 j["area_coverage"] = aStats.areaCoverage;
482 j["mean_triangle_area_nm2"] = aStats.meanTriArea;
483 j["stddev_triangle_area_nm2"] = aStats.stddevTriArea;
484 j["spikey_triangles"] = aStats.spikeyTriangles;
485 j["spikey_ratio"] = aStats.spikeyRatio;
486 j["min_angle_p1_deg"] = aStats.minAnglePctl1;
487 j["min_angle_p5_deg"] = aStats.minAnglePctl5;
488 j["min_angle_p50_deg"] = aStats.minAnglePctl50;
489 j["tris_below_5deg"] = aStats.trisBelow5Deg;
490 j["tris_below_10deg"] = aStats.trisBelow10Deg;
491 j["tris_below_15deg"] = aStats.trisBelow15Deg;
492 j["mean_radius_ratio"] = aStats.meanRadiusRatio;
493 return j;
494}
495
496
497ZONE_COMPARISON CompareZone( const std::string& aSource, const ZONE_STATS& aCurrent,
498 const BASELINE_ZONE* aBaseline )
499{
500 ZONE_COMPARISON cmp;
501 cmp.source = aSource;
502 cmp.layer = aCurrent.layer;
503 cmp.net = aCurrent.net;
504 cmp.curTriangles = aCurrent.triangleCount;
505 cmp.curSpikeyRatio = aCurrent.spikeyRatio;
506 cmp.curStddev = aCurrent.stddevTriArea;
507 cmp.curCoverage = aCurrent.areaCoverage;
508 cmp.curMinAngleP5 = aCurrent.minAnglePctl5;
509 cmp.curBelow10Deg = aCurrent.trisBelow10Deg;
510 cmp.curTimeUs = aCurrent.timeUs;
511
512 if( !aBaseline )
513 {
514 cmp.type = CHANGE_TYPE::UNCHANGED;
515 return cmp;
516 }
517
518 cmp.baseTriangles = aBaseline->triangleCount;
519 cmp.baseSpikeyRatio = aBaseline->spikeyRatio;
520 cmp.baseStddev = aBaseline->stddevTriArea;
521 cmp.baseCoverage = aBaseline->areaCoverage;
522 cmp.baseMinAngleP5 = aBaseline->minAnglePctl5;
523 cmp.baseBelow10Deg = aBaseline->trisBelow10Deg;
524 cmp.baseTimeUs = aBaseline->timeUs;
525
526 bool coverageBroke = aCurrent.originalArea > 0.0
527 && ( aCurrent.areaCoverage < 0.99 || aCurrent.areaCoverage > 1.01 );
528 bool newFailure = aCurrent.triangleCount == 0 && aBaseline->triangleCount > 0
529 && aCurrent.originalArea > 0.0;
530
531 if( coverageBroke || newFailure )
532 {
533 cmp.type = CHANGE_TYPE::BREAKING;
534 return cmp;
535 }
536
537 // Lexicographic classification honoring the project priority regularity > count. The first
538 // axis that moves beyond its noise threshold decides the verdict; lower-priority axes are
539 // only consulted when every higher-priority axis is unchanged.
540 //
541 // Speed is deliberately not an axis. The baseline records absolute microseconds with no note
542 // of the build or machine that produced them, and optimization does not scale zones evenly,
543 // so comparing against it detects the build type rather than the algorithm. Measured against
544 // a debug-recorded baseline, a release run reported 273 of 273 measurable zones as improved,
545 // and normalizing each zone to its share of the corpus total still reported 352. Judge speed
546 // from the corpus total, comparing two builds on one machine.
547 struct AXIS
548 {
549 double delta; // signed change, current minus baseline
550 double threshold; // magnitude below which the axis counts as unchanged
551 bool lowerBetter;
552 };
553
554 // Regularity blends p5 min-angle (want higher) and sub-10 degree count (want lower) into a
555 // single signed score where positive is worse, matching the lowerBetter axes.
556 double regressScore = ( cmp.baseMinAngleP5 - cmp.curMinAngleP5 )
557 + ( cmp.curBelow10Deg - cmp.baseBelow10Deg )
558 / std::max( 1.0, static_cast<double>( cmp.baseTriangles ) )
559 * 100.0;
560
561 std::vector<AXIS> axes = {
562 { regressScore, 1.0, true }, // regularity (positive worse)
563 { cmp.triangleDeltaPct(), 5.0, true }, // count (more tris is worse)
564 };
565
566 cmp.type = CHANGE_TYPE::UNCHANGED;
567
568 for( const AXIS& axis : axes )
569 {
570 double signedWorse = axis.lowerBetter ? axis.delta : -axis.delta;
571
572 if( signedWorse > axis.threshold )
573 {
574 cmp.type = CHANGE_TYPE::REGRESSION;
575 break;
576 }
577
578 if( signedWorse < -axis.threshold )
579 {
580 cmp.type = CHANGE_TYPE::IMPROVEMENT;
581 break;
582 }
583 }
584
585 return cmp;
586}
587
588
589std::string FormatSign( double aValue, const std::string& aSuffix )
590{
591 std::ostringstream ss;
592 ss << std::fixed << std::setprecision( 1 );
593
594 if( aValue > 0.0 )
595 ss << "+";
596
597 ss << aValue << aSuffix;
598 return ss.str();
599}
600
601
602std::string FormatZoneDetail( const ZONE_COMPARISON& aCmp )
603{
604 std::ostringstream ss;
605 ss << " " << aCmp.source << " " << aCmp.layer << " \"" << aCmp.net << "\"" << "\n";
606 ss << std::fixed << std::setprecision( 1 );
607 ss << " time: " << FormatSign( aCmp.timeDeltaPct(), "%" );
608 ss << " minAngleP5: " << aCmp.baseMinAngleP5 << " -> " << aCmp.curMinAngleP5
609 << " deg (" << FormatSign( aCmp.minAngleP5DeltaDeg(), "deg" ) << ")";
610 ss << " <10deg: " << aCmp.baseBelow10Deg << " -> " << aCmp.curBelow10Deg;
611 ss << "\n spikey: " << ( aCmp.baseSpikeyRatio * 100.0 ) << "% -> "
612 << ( aCmp.curSpikeyRatio * 100.0 ) << "% (" << FormatSign( aCmp.spikeyDeltaPp(), "pp" )
613 << ")";
614 ss << " triangles: " << aCmp.baseTriangles << " -> " << aCmp.curTriangles
615 << " (" << FormatSign( aCmp.triangleDeltaPct(), "%" ) << ")";
616
617 if( aCmp.baseStddev > 0.0 || aCmp.curStddev > 0.0 )
618 {
619 ss << " stddev: " << FormatSign( aCmp.stddevDeltaPct(), "%" );
620 }
621
622 return ss.str();
623}
624
625
626void OutputComparisonReport( const BASELINE_DATA& aBaseline,
627 const std::vector<ZONE_COMPARISON>& aComparisons,
628 int aTotalTriangles, int aTotalSpikeyTri, int aTotalZones )
629{
630 std::vector<ZONE_COMPARISON> breaking;
631 std::vector<ZONE_COMPARISON> regressions;
632 std::vector<ZONE_COMPARISON> improvements;
633 int unchanged = 0;
634
635 for( const auto& cmp : aComparisons )
636 {
637 switch( cmp.type )
638 {
639 case CHANGE_TYPE::BREAKING: breaking.push_back( cmp ); break;
640 case CHANGE_TYPE::REGRESSION: regressions.push_back( cmp ); break;
641 case CHANGE_TYPE::IMPROVEMENT: improvements.push_back( cmp ); break;
642 case CHANGE_TYPE::UNCHANGED: unchanged++; break;
643 }
644 }
645
646 std::sort( improvements.begin(), improvements.end(),
647 []( const ZONE_COMPARISON& a, const ZONE_COMPARISON& b )
648 {
649 return a.spikeyDeltaPp() < b.spikeyDeltaPp();
650 } );
651
652 std::sort( regressions.begin(), regressions.end(),
653 []( const ZONE_COMPARISON& a, const ZONE_COMPARISON& b )
654 {
655 return a.spikeyDeltaPp() > b.spikeyDeltaPp();
656 } );
657
658 std::ostringstream report;
659 report << std::fixed << std::setprecision( 1 );
660
661 report << "\n=== Triangulation Comparison vs Baseline ===\n\n";
662
663 report << "Baseline: " << aBaseline.boardCount << " boards, "
664 << aBaseline.zoneCount << " zones\n";
665 report << "Current: " << aTotalZones << " zones\n\n";
666
667 double baseSpikey = aBaseline.spikeyRatio * 100.0;
668 double curSpikey = aTotalTriangles > 0
669 ? static_cast<double>( aTotalSpikeyTri ) / aTotalTriangles * 100.0
670 : 0.0;
671
672 report << "Global:\n";
673 report << " Triangles: " << aBaseline.totalTriangles << " -> " << aTotalTriangles
674 << " (" << FormatSign(
675 aTotalTriangles - aBaseline.totalTriangles == 0
676 ? 0.0
677 : ( aTotalTriangles - aBaseline.totalTriangles )
678 / static_cast<double>( aBaseline.totalTriangles )
679 * 100.0,
680 "%" )
681 << ")\n";
682 report << " Spikey: " << baseSpikey << "% -> " << curSpikey << "% ("
683 << FormatSign( curSpikey - baseSpikey, "pp" ) << ")\n";
684 report << " Spikey ct: " << aBaseline.totalSpikeyTri << " -> " << aTotalSpikeyTri
685 << "\n\n";
686
687 report << "BREAKING: " << breaking.size() << " zones\n";
688
689 for( const auto& cmp : breaking )
690 report << FormatZoneDetail( cmp ) << "\n";
691
692 if( breaking.empty() )
693 report << " (none)\n";
694
695 report << "\nREGRESSIONS: " << regressions.size() << " zones"
696 << " (lexicographic: regularity worse, else triangles >+5%)\n";
697
698 int shown = 0;
699
700 for( const auto& cmp : regressions )
701 {
702 if( shown >= 20 )
703 {
704 report << " ... and " << ( regressions.size() - 20 ) << " more\n";
705 break;
706 }
707
708 report << FormatZoneDetail( cmp ) << "\n";
709 shown++;
710 }
711
712 if( regressions.empty() )
713 report << " (none)\n";
714
715 report << "\nIMPROVEMENTS: " << improvements.size() << " zones\n";
716
717 shown = 0;
718
719 for( const auto& cmp : improvements )
720 {
721 if( shown >= 20 )
722 {
723 report << " ... and " << ( improvements.size() - 20 ) << " more\n";
724 break;
725 }
726
727 report << FormatZoneDetail( cmp ) << "\n";
728 shown++;
729 }
730
731 if( improvements.empty() )
732 report << " (none)\n";
733
734 report << "\nSummary: " << improvements.size() << " improved, "
735 << regressions.size() << " regressed, "
736 << breaking.size() << " breaking, "
737 << unchanged << " unchanged\n";
738
739 BOOST_TEST_MESSAGE( report.str() );
740
741 BOOST_CHECK_MESSAGE( breaking.empty(),
742 std::to_string( breaking.size() )
743 + " zone(s) have breaking triangulation changes" );
744}
745
746
747std::string GetTriangulationDataDir()
748{
749 return KI_TEST::GetTestDataRootDir() + "triangulation/";
750}
751
752}; // anonymous namespace
753
754
755BOOST_AUTO_TEST_SUITE( TriangulationBenchmark )
756
757
758BOOST_AUTO_TEST_CASE( BenchmarkAllExtractedPolygons )
759{
760 std::string dataDir = GetTriangulationDataDir();
761
762 if( !fs::exists( dataDir ) || fs::is_empty( dataDir ) )
763 {
764 BOOST_TEST_MESSAGE( "No triangulation data in " << dataDir << ", skipping benchmark" );
765 return;
766 }
767
768 fs::path jsonPath = fs::path( dataDir ) / "triangulation_status.json";
769 BASELINE_DATA baseline = LoadBaseline( jsonPath );
770
771 if( baseline.valid )
772 {
773 BOOST_TEST_MESSAGE( "Loaded baseline: " << baseline.boardCount << " boards, "
774 << baseline.zoneCount << " zones, "
775 << baseline.totalTriangles << " triangles" );
776 }
777 else
778 {
779 BOOST_TEST_MESSAGE( "No baseline found, running without comparison" );
780 }
781
782 std::vector<fs::path> polyFiles;
783
784 for( const auto& entry : fs::directory_iterator( dataDir ) )
785 {
786 if( entry.path().extension() == ".kicad_polys" )
787 polyFiles.push_back( entry.path() );
788 }
789
790 std::sort( polyFiles.begin(), polyFiles.end() );
791
792 BOOST_TEST_MESSAGE( "Found " << polyFiles.size() << " polygon files" );
793
794 // Stamp the report with the experimental variant under test (set by the harness / an env
795 // var when A/B testing an algorithm change against the recorded baseline).
796 if( const char* variant = std::getenv( "KICAD_TRI_VARIANT" ) )
797 BOOST_TEST_MESSAGE( "Variant: " << variant );
798
799 int totalTriangles = 0;
800 int totalSpikeyTri = 0;
801 int totalZones = 0;
802 int totalBelow10 = 0;
803 int64_t totalTimeUs = 0;
804
805 // Corpus-wide min-angle percentiles are computed from every triangle rather than by
806 // averaging per-zone summaries, so one huge zone cannot dominate the tail statistics.
807 std::vector<double> globalMinAngles;
808
809 std::vector<ZONE_COMPARISON> comparisons;
810
811 for( const auto& polyFile : polyFiles )
812 {
813 BOARD_ENTRY board;
814
815 if( !ParsePolyFile( polyFile, board ) )
816 {
817 BOOST_TEST_MESSAGE( "Failed to parse: " << polyFile.filename() );
818 continue;
819 }
820
821 int boardTriangles = 0;
822 int boardSpikey = 0;
823 int64_t boardTimeUs = 0;
824
825 const BASELINE_BOARD* baseBoard = nullptr;
826 auto it = baseline.boards.find( board.source );
827
828 if( it != baseline.boards.end() )
829 baseBoard = &it->second;
830
831 for( size_t zi = 0; zi < board.zones.size(); zi++ )
832 {
833 ZONE_STATS stats = ComputeZoneStats( board.zones[zi] );
834
835 BOOST_CHECK_MESSAGE(
836 stats.triangleCount > 0 || stats.originalArea == 0.0,
837 board.source + " " + stats.layer + " " + stats.net
838 + " produced 0 triangles with non-zero area" );
839
840 if( stats.originalArea > 0.0 )
841 {
842 BOOST_CHECK_MESSAGE(
843 stats.areaCoverage > 0.999 && stats.areaCoverage < 1.001,
844 board.source + " " + stats.layer + " " + stats.net
845 + " area coverage: " + std::to_string( stats.areaCoverage ) );
846 }
847
848 boardTriangles += stats.triangleCount;
849 boardSpikey += stats.spikeyTriangles;
850 boardTimeUs += stats.timeUs;
851 totalBelow10 += stats.trisBelow10Deg;
852 globalMinAngles.insert( globalMinAngles.end(), stats.minAngles.begin(),
853 stats.minAngles.end() );
854 totalZones++;
855
856 if( baseline.valid )
857 {
858 const BASELINE_ZONE* baseZone = nullptr;
859
860 if( baseBoard && zi < baseBoard->zones.size() )
861 baseZone = &baseBoard->zones[zi];
862
863 comparisons.push_back( CompareZone( board.source, stats, baseZone ) );
864 }
865 }
866
867 totalTriangles += boardTriangles;
868 totalSpikeyTri += boardSpikey;
869 totalTimeUs += boardTimeUs;
870 }
871
872 BOOST_TEST_MESSAGE( "Total triangles: " << totalTriangles
873 << " Spikey: " << totalSpikeyTri
874 << " (" << ( totalTriangles > 0
876 : 0.0 )
877 << "%)" );
878
879 std::sort( globalMinAngles.begin(), globalMinAngles.end() );
880 BOOST_TEST_MESSAGE( "Global min-angle deg p1: " << Percentile( globalMinAngles, 1.0 )
881 << " p5: " << Percentile( globalMinAngles, 5.0 )
882 << " p50: " << Percentile( globalMinAngles, 50.0 )
883 << " (<10deg: " << totalBelow10 << " = "
884 << ( totalTriangles > 0 ? 100.0 * totalBelow10 / totalTriangles : 0.0 )
885 << "%)" );
886 // Only comparable against a total from the same machine and build
887 BOOST_TEST_MESSAGE( "Total time: " << totalTimeUs / 1000.0 << " ms (baseline "
888 << baseline.totalTimeUs / 1000.0 << " ms)" );
889
890 if( baseline.valid )
891 OutputComparisonReport( baseline, comparisons, totalTriangles, totalSpikeyTri, totalZones );
892}
893
894
895BOOST_AUTO_TEST_CASE( UpdateTriangulationStatus, * boost::unit_test::disabled() )
896{
897 // Naming the enclosing suite with --run_test overrides disabled(), which would otherwise
898 // rewrite the tracked baseline with whatever timings the local machine produced
899 if( !std::getenv( "KICAD_TRI_UPDATE_BASELINE" ) )
900 {
901 BOOST_TEST_MESSAGE( "Set KICAD_TRI_UPDATE_BASELINE=1 to rewrite the baseline" );
902 return;
903 }
904
905 std::string dataDir = GetTriangulationDataDir();
906
907 if( !fs::exists( dataDir ) || fs::is_empty( dataDir ) )
908 {
909 BOOST_TEST_MESSAGE( "No triangulation data in " << dataDir << ", skipping" );
910 return;
911 }
912
913 std::vector<fs::path> polyFiles;
914
915 for( const auto& entry : fs::directory_iterator( dataDir ) )
916 {
917 if( entry.path().extension() == ".kicad_polys" )
918 polyFiles.push_back( entry.path() );
919 }
920
921 std::sort( polyFiles.begin(), polyFiles.end() );
922
925 int totalZones = 0;
926 double totalTimeUs = 0.0;
927 double totalArea = 0.0;
928
929 nlohmann::json boardsJson = nlohmann::json::array();
930
931 for( const auto& polyFile : polyFiles )
932 {
933 BOARD_ENTRY board;
934
935 if( !ParsePolyFile( polyFile, board ) )
936 continue;
937
938 nlohmann::json boardJson;
939 boardJson["source"] = board.source;
940 nlohmann::json zonesJson = nlohmann::json::array();
941
942 int boardTriangles = 0;
943 int boardSpikey = 0;
944 double boardTimeUs = 0.0;
945
946 for( ZONE_ENTRY& zone : board.zones )
947 {
948 ZONE_STATS stats = ComputeZoneStats( zone );
949 zonesJson.push_back( ZoneStatsToJson( stats ) );
950
951 boardTriangles += stats.triangleCount;
952 boardSpikey += stats.spikeyTriangles;
953 boardTimeUs += static_cast<double>( stats.timeUs );
954 totalArea += stats.triangulatedArea;
955 totalZones++;
956 }
957
958 boardJson["zones"] = zonesJson;
959
960 nlohmann::json boardTotals;
961 boardTotals["triangle_count"] = boardTriangles;
962 boardTotals["time_us"] = static_cast<int64_t>( boardTimeUs );
963 boardTotals["spikey_ratio"] = boardTriangles > 0
964 ? static_cast<double>( boardSpikey ) / boardTriangles
965 : 0.0;
966 boardJson["board_totals"] = boardTotals;
967 boardsJson.push_back( boardJson );
968
969 totalTriangles += boardTriangles;
970 totalSpikeyTri += boardSpikey;
971 totalTimeUs += boardTimeUs;
972 }
973
974 nlohmann::json globalJson;
975 globalJson["total_triangles"] = totalTriangles;
976 globalJson["total_time_us"] = static_cast<int64_t>( totalTimeUs );
977 globalJson["total_area_nm2"] = totalArea;
978 globalJson["total_spikey_triangles"] = totalSpikeyTri;
979 globalJson["spikey_ratio"] = totalTriangles > 0
980 ? static_cast<double>( totalSpikeyTri ) / totalTriangles
981 : 0.0;
982
983 nlohmann::json metadataJson;
984 metadataJson["board_count"] = static_cast<int>( polyFiles.size() );
985 metadataJson["zone_count"] = totalZones;
986
987 nlohmann::json jsonOutput;
988 jsonOutput["metadata"] = metadataJson;
989 jsonOutput["global"] = globalJson;
990 jsonOutput["boards"] = boardsJson;
991
992 fs::path jsonPath = fs::path( dataDir ) / "triangulation_status.json";
993
994 std::ofstream jsonFile( jsonPath );
995 jsonFile << jsonOutput.dump( 2 ) << "\n";
996 BOOST_CHECK( jsonFile.good() );
997 jsonFile.close();
998
999 BOOST_TEST_MESSAGE( "Wrote triangulation status to " << jsonPath );
1000 BOOST_TEST_MESSAGE( "Boards: " << polyFiles.size() << " Zones: " << totalZones
1001 << " Triangles: " << totalTriangles );
1002}
1003
1004
A small class to help profiling.
Definition profile.h:46
void Stop()
Save the time when this function was called, and set the counter stane to stop.
Definition profile.h:86
double msecs(bool aSinceLast=false)
Definition profile.h:147
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:549
CHANGE_TYPE
Types of changes.
Definition commit.h:37
Exact orientation and in-circle predicates over integer coordinates.
double TriangleMinAngleDeg(const VECTOR2I &a, const VECTOR2I &b, const VECTOR2I &c)
The smallest interior angle of a triangle, in degrees; near zero for a sliver.
bool IsSliverTriangle(const VECTOR2I &a, const VECTOR2I &b, const VECTOR2I &c)
A triangle is a sliver when its longest edge exceeds ten times its shortest.
std::string GetTestDataRootDir()
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_SUITE_END()
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
nlohmann::json metadataJson
nlohmann::json boardsJson
nlohmann::json globalJson
BOOST_AUTO_TEST_CASE(BenchmarkAllExtractedPolygons)
std::vector< fs::path > polyFiles
std::ofstream jsonFile(jsonPath)
nlohmann::json jsonOutput
int delta
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683