KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_allegro_drill_spans.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
28
29#include "allegro_test_utils.h"
31
32#include <board.h>
34#include <footprint.h>
35#include <lset.h>
36#include <pad.h>
37#include <pcb_track.h>
38
39#include <algorithm>
40#include <cmath>
41#include <filesystem>
42#include <fstream>
43#include <map>
44#include <regex>
45#include <set>
46#include <string>
47#include <tuple>
48#include <vector>
49
50using namespace KI_TEST;
51
52
53namespace
54{
56using LAYER_SPAN = std::pair<int, int>;
57
59using SPAN_DRILL_TABLE = std::map<LAYER_SPAN, std::map<int, int>>;
60
61
63int toMicrons( int aNanometres )
64{
65 return static_cast<int>( std::llround( aNanometres / 1000.0 ) );
66}
67
68
74std::pair<LAYER_SPAN, std::map<int, int>> parseDrillFile( const std::filesystem::path& aPath )
75{
76 static const std::regex spanRe( R"(-(\d+)-(\d+)\.drl$)" );
77 static const std::regex toolDefRe( R"(^T(\d+)C([0-9.]+))" );
78 static const std::regex toolSelRe( R"(^T(\d+)\s*$)" );
79 static const std::regex coordRe( R"(^X.*Y)" );
80 static const std::regex repeatRe( R"(^R(\d+))" );
81
82 LAYER_SPAN span{ 0, 0 };
83 std::smatch m;
84 std::string name = aPath.filename().string();
85
86 if( std::regex_search( name, m, spanRe ) )
87 span = { std::stoi( m[1] ), std::stoi( m[2] ) };
88
89 std::map<int, int> toolDiaUm; // tool number -> diameter in µm
90 std::map<int, int> diaToCount; // diameter µm -> hit count
91 int activeTool = -1;
92
93 std::ifstream file( aPath );
94 std::string line;
95
96 while( std::getline( file, line ) )
97 {
98 // Excellon files use CRLF; strip the trailing CR so regexes anchor cleanly.
99 while( !line.empty() && ( line.back() == '\r' || line.back() == '\n' ) )
100 line.pop_back();
101
102 if( std::regex_search( line, m, toolDefRe ) )
103 {
104 int tool = std::stoi( m[1] );
105 double mm = std::stod( m[2] );
106 toolDiaUm[tool] = static_cast<int>( std::llround( mm * 1000.0 ) );
107 }
108 else if( std::regex_search( line, m, toolSelRe ) )
109 {
110 activeTool = std::stoi( m[1] );
111 }
112 else if( std::regex_search( line, m, coordRe ) )
113 {
114 if( activeTool >= 0 && toolDiaUm.count( activeTool ) )
115 diaToCount[toolDiaUm[activeTool]] += 1;
116 }
117 else if( std::regex_search( line, m, repeatRe ) )
118 {
119 if( activeTool >= 0 && toolDiaUm.count( activeTool ) )
120 diaToCount[toolDiaUm[activeTool]] += std::stoi( m[1] );
121 }
122 }
123
124 return { span, diaToCount };
125}
126
127
129SPAN_DRILL_TABLE loadDrillGroundTruth( const std::string& aBoardDir )
130{
131 SPAN_DRILL_TABLE table;
132
133 for( const auto& entry : std::filesystem::directory_iterator( aBoardDir ) )
134 {
135 if( entry.path().extension() != ".drl" )
136 continue;
137
138 auto [span, counts] = parseDrillFile( entry.path() );
139
140 for( const auto& [dia, count] : counts )
141 table[span][dia] += count;
142 }
143
144 return table;
145}
146
147
149std::map<PCB_LAYER_ID, int> buildCopperOrdinal( const BOARD& aBoard )
150{
151 std::map<PCB_LAYER_ID, int> ordinal;
152 int idx = 1;
153
154 for( PCB_LAYER_ID layer : aBoard.GetEnabledLayers().CuStack() )
155 ordinal[layer] = idx++;
156
157 return ordinal;
158}
159
160
162LAYER_SPAN viaLayerSpan( const PCB_VIA& aVia, const std::map<PCB_LAYER_ID, int>& aOrdinal, int aTotal )
163{
164 auto index = [&aOrdinal]( PCB_LAYER_ID aLayer, int aDefault )
165 {
166 auto it = aOrdinal.find( aLayer );
167 return it != aOrdinal.end() ? it->second : aDefault;
168 };
169
170 int top = index( aVia.TopLayer(), 1 );
171 int bot = index( aVia.BottomLayer(), aTotal );
172
173 if( top > bot )
174 std::swap( top, bot );
175
176 return { top, bot };
177}
178
179
181SPAN_DRILL_TABLE collectBoardDrillTable( const BOARD& aBoard )
182{
183 SPAN_DRILL_TABLE table;
184 std::map<PCB_LAYER_ID, int> ordinal = buildCopperOrdinal( aBoard );
185 const int total = aBoard.GetCopperLayerCount();
186
187 for( PCB_TRACK* track : aBoard.Tracks() )
188 {
189 if( track->Type() != PCB_VIA_T )
190 continue;
191
192 PCB_VIA* via = static_cast<PCB_VIA*>( track );
193 int drill = toMicrons( via->GetDrillValue() );
194
195 if( drill <= 0 )
196 continue;
197
198 table[viaLayerSpan( *via, ordinal, total )][drill] += 1;
199 }
200
201 for( FOOTPRINT* fp : aBoard.Footprints() )
202 {
203 for( PAD* pad : fp->Pads() )
204 {
205 if( pad->GetAttribute() != PAD_ATTRIB::PTH && pad->GetAttribute() != PAD_ATTRIB::NPTH )
206 continue;
207
208 // The Excellon .drl files describe round drills only; oblong slots are routed and
209 // live in the companion .rou file, so they are out of scope for this comparison.
210 if( pad->GetDrillShape() == PAD_DRILL_SHAPE::OBLONG )
211 continue;
212
213 int drill = toMicrons( pad->GetDrillSizeX() );
214
215 if( drill <= 0 )
216 continue;
217
218 // KiCad through-hole pads always span the full copper stack.
219 table[{ 1, total }][drill] += 1;
220 }
221 }
222
223 return table;
224}
225
226
228std::string describeTable( const SPAN_DRILL_TABLE& aTable )
229{
230 std::string out;
231
232 for( const auto& [span, sizes] : aTable )
233 {
234 for( const auto& [dia, count] : sizes )
235 {
236 out += " span " + std::to_string( span.first ) + "-" + std::to_string( span.second )
237 + " drill " + std::to_string( dia ) + "um x " + std::to_string( count ) + "\n";
238 }
239 }
240
241 return out;
242}
243
244
245struct DRILL_SPAN_FIXTURE
246{
247 DRILL_SPAN_FIXTURE()
248 {
249 m_boardDir = KI_TEST::AllegroBoardDataDir( "ABX00162_UNOQ" );
250 m_groundTruth = loadDrillGroundTruth( m_boardDir );
252 m_boardDir + "20250806_UNOQ.brd" );
253 }
254
255 std::string m_boardDir;
256 SPAN_DRILL_TABLE m_groundTruth;
257 BOARD* m_board = nullptr;
258};
259
260} // namespace
261
262
263BOOST_FIXTURE_TEST_SUITE( AllegroDrillSpans, DRILL_SPAN_FIXTURE )
264
265
266
271BOOST_AUTO_TEST_CASE( GroundTruthAndBoardLoad )
272{
273 BOOST_REQUIRE_MESSAGE( m_board != nullptr, "20250806_UNOQ.brd failed to import" );
274
275 // The .drl set describes six spans: 1-2, 2-3, 3-6, 6-7, 7-8 and the 1-8 through holes.
276 BOOST_REQUIRE_MESSAGE( m_groundTruth.size() >= 6,
277 "Expected at least 6 drill spans, parsed " << m_groundTruth.size() );
278
279 BOOST_TEST_MESSAGE( "Ground-truth drill table:\n" << describeTable( m_groundTruth ) );
280
281 BOOST_CHECK_EQUAL( m_board->GetCopperLayerCount(), 8 );
282}
283
284
289BOOST_AUTO_TEST_CASE( BlindAndBuriedViasPresent )
290{
291 BOOST_REQUIRE( m_board != nullptr );
292
293 std::map<VIATYPE, int> typeCounts;
294 std::set<LAYER_SPAN> viaSpans;
295 std::map<PCB_LAYER_ID, int> ordinal = buildCopperOrdinal( *m_board );
296 const int total = m_board->GetCopperLayerCount();
297
298 for( PCB_TRACK* track : m_board->Tracks() )
299 {
300 if( track->Type() != PCB_VIA_T )
301 continue;
302
303 PCB_VIA* via = static_cast<PCB_VIA*>( track );
304 typeCounts[via->GetViaType()]++;
305 viaSpans.insert( viaLayerSpan( *via, ordinal, total ) );
306 }
307
308 BOOST_TEST_MESSAGE( "Via type counts: THROUGH=" << typeCounts[VIATYPE::THROUGH]
309 << " BLIND=" << typeCounts[VIATYPE::BLIND]
310 << " BURIED=" << typeCounts[VIATYPE::BURIED]
311 << " MICROVIA=" << typeCounts[VIATYPE::MICROVIA] );
312
313 std::string spanList;
314
315 for( const LAYER_SPAN& s : viaSpans )
316 spanList += " " + std::to_string( s.first ) + "-" + std::to_string( s.second );
317
318 BOOST_TEST_MESSAGE( "Distinct via spans:" << spanList );
319
320 int nonThrough = typeCounts[VIATYPE::BLIND] + typeCounts[VIATYPE::BURIED]
321 + typeCounts[VIATYPE::MICROVIA];
322
323 BOOST_CHECK_MESSAGE( nonThrough > 0, "Importer produced no blind/buried/microvia vias" );
324
325 // The board has exactly six via spans: five blind/buried plus the 1-8 through span.
326 const std::set<LAYER_SPAN> expectedViaSpans = { { 1, 2 }, { 1, 8 }, { 2, 3 },
327 { 3, 6 }, { 6, 7 }, { 7, 8 } };
328
329 BOOST_CHECK_MESSAGE( viaSpans == expectedViaSpans, "Unexpected via span set:" << spanList );
330}
331
332
337BOOST_AUTO_TEST_CASE( DrillSpansMatchExcellon )
338{
339 BOOST_REQUIRE( m_board != nullptr );
340
341 SPAN_DRILL_TABLE boardTable = collectBoardDrillTable( *m_board );
342
343 BOOST_TEST_MESSAGE( "Imported board drill table:\n" << describeTable( boardTable ) );
344
345 // Reduce both tables to the set of (span, diameter) keys, ignoring counts.
346 auto keySet = []( const SPAN_DRILL_TABLE& aTable )
347 {
348 std::set<std::tuple<int, int, int>> keys;
349
350 for( const auto& [span, sizes] : aTable )
351 for( const auto& [dia, count] : sizes )
352 keys.insert( { span.first, span.second, dia } );
353
354 return keys;
355 };
356
357 std::set<std::tuple<int, int, int>> truthKeys = keySet( m_groundTruth );
358 std::set<std::tuple<int, int, int>> boardKeys = keySet( boardTable );
359
360 std::vector<std::tuple<int, int, int>> missing; // in .drl but not in board
361 std::vector<std::tuple<int, int, int>> extra; // in board but not in .drl
362
363 std::set_difference( truthKeys.begin(), truthKeys.end(), boardKeys.begin(), boardKeys.end(),
364 std::back_inserter( missing ) );
365 std::set_difference( boardKeys.begin(), boardKeys.end(), truthKeys.begin(), truthKeys.end(),
366 std::back_inserter( extra ) );
367
368 for( const auto& [t, b, d] : missing )
369 BOOST_TEST_MESSAGE( " MISSING from board: span " << t << "-" << b << " drill " << d << "um" );
370
371 for( const auto& [t, b, d] : extra )
372 BOOST_TEST_MESSAGE( " EXTRA in board: span " << t << "-" << b << " drill " << d << "um" );
373
374 BOOST_CHECK_MESSAGE( missing.empty(),
375 missing.size() << " (span, drill) combinations from the .drl files are "
376 "absent from the imported board" );
377 BOOST_CHECK_MESSAGE( extra.empty(),
378 extra.size() << " (span, drill) combinations in the imported board do not "
379 "exist in any .drl file" );
380
381 // The key set alone would miss holes shifted between two spans that both exist (e.g. some 1-2
382 // microvias landing on 2-3), so also compare counts. The tolerance absorbs the small drift from
383 // the board being a revision newer than the drill files while still catching gross shifts.
384 for( const auto& [span, sizes] : m_groundTruth )
385 {
386 for( const auto& [dia, truthCount] : sizes )
387 {
388 int boardCount = 0;
389
390 if( auto sIt = boardTable.find( span ); sIt != boardTable.end() )
391 {
392 if( auto dIt = sIt->second.find( dia ); dIt != sIt->second.end() )
393 boardCount = dIt->second;
394 }
395
396 const int tolerance = std::max( 5, truthCount / 100 );
397
398 BOOST_CHECK_MESSAGE( std::abs( boardCount - truthCount ) <= tolerance,
399 "span " << span.first << "-" << span.second << " drill " << dia
400 << "um: board has " << boardCount << " holes, .drl has "
401 << truthCount << " (tolerance " << tolerance << ")" );
402 }
403 }
404}
405
406
int index
const char * name
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:372
int GetCopperLayerCount() const
Definition board.cpp:985
const FOOTPRINTS & Footprints() const
Definition board.h:420
const TRACKS & Tracks() const
Definition board.h:418
const LSET & GetEnabledLayers() const
A proxy function that calls the corresponding function in m_BoardSettings.
Definition board.cpp:1034
static ALLEGRO_CACHED_LOADER & GetInstance()
Get the singleton instance of the Allegro board cache loader.
BOARD * GetCachedBoard(const std::string &aFilePath)
Get a cached board for the given file path, or load it if not already cached, without forcing a reloa...
LSEQ CuStack() const
Return a sequence of copper layers in starting from the front/top and extending to the back/bottom.
Definition lset.cpp:259
Definition pad.h:61
PCB_LAYER_ID BottomLayer() const
PCB_LAYER_ID TopLayer() const
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
std::string AllegroBoardDataDir(const std::string &aBoardName)
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:400
@ NPTH
like PAD_PTH, but not plated mechanical use only, no connection allowed
Definition padstack.h:103
@ PTH
Plated through hole pad.
Definition padstack.h:98
BOOST_AUTO_TEST_CASE(GroundTruthAndBoardLoad)
The fixture and its ground truth must be well-formed before the cross-validation tests have any meani...
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
KIBIS top(path, &reporter)
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
BOOST_TEST_MESSAGE("\n=== Real-World Polygon PIP Benchmark ===\n"<< formatTable(table))
std::vector< std::vector< std::string > > table
BOOST_CHECK_EQUAL(result, "25.4")
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:90