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, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may search the http://www.gnu.org website for the version 2 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
32
33#include "allegro_test_utils.h"
35
36#include <board.h>
38#include <footprint.h>
39#include <lset.h>
40#include <pad.h>
41#include <pcb_track.h>
42
43#include <algorithm>
44#include <cmath>
45#include <filesystem>
46#include <fstream>
47#include <map>
48#include <regex>
49#include <set>
50#include <string>
51#include <tuple>
52#include <vector>
53
54using namespace KI_TEST;
55
56
57namespace
58{
60using LAYER_SPAN = std::pair<int, int>;
61
63using SPAN_DRILL_TABLE = std::map<LAYER_SPAN, std::map<int, int>>;
64
65
67int toMicrons( int aNanometres )
68{
69 return static_cast<int>( std::llround( aNanometres / 1000.0 ) );
70}
71
72
78std::pair<LAYER_SPAN, std::map<int, int>> parseDrillFile( const std::filesystem::path& aPath )
79{
80 static const std::regex spanRe( R"(-(\d+)-(\d+)\.drl$)" );
81 static const std::regex toolDefRe( R"(^T(\d+)C([0-9.]+))" );
82 static const std::regex toolSelRe( R"(^T(\d+)\s*$)" );
83 static const std::regex coordRe( R"(^X.*Y)" );
84 static const std::regex repeatRe( R"(^R(\d+))" );
85
86 LAYER_SPAN span{ 0, 0 };
87 std::smatch m;
88 std::string name = aPath.filename().string();
89
90 if( std::regex_search( name, m, spanRe ) )
91 span = { std::stoi( m[1] ), std::stoi( m[2] ) };
92
93 std::map<int, int> toolDiaUm; // tool number -> diameter in µm
94 std::map<int, int> diaToCount; // diameter µm -> hit count
95 int activeTool = -1;
96
97 std::ifstream file( aPath );
98 std::string line;
99
100 while( std::getline( file, line ) )
101 {
102 // Excellon files use CRLF; strip the trailing CR so regexes anchor cleanly.
103 while( !line.empty() && ( line.back() == '\r' || line.back() == '\n' ) )
104 line.pop_back();
105
106 if( std::regex_search( line, m, toolDefRe ) )
107 {
108 int tool = std::stoi( m[1] );
109 double mm = std::stod( m[2] );
110 toolDiaUm[tool] = static_cast<int>( std::llround( mm * 1000.0 ) );
111 }
112 else if( std::regex_search( line, m, toolSelRe ) )
113 {
114 activeTool = std::stoi( m[1] );
115 }
116 else if( std::regex_search( line, m, coordRe ) )
117 {
118 if( activeTool >= 0 && toolDiaUm.count( activeTool ) )
119 diaToCount[toolDiaUm[activeTool]] += 1;
120 }
121 else if( std::regex_search( line, m, repeatRe ) )
122 {
123 if( activeTool >= 0 && toolDiaUm.count( activeTool ) )
124 diaToCount[toolDiaUm[activeTool]] += std::stoi( m[1] );
125 }
126 }
127
128 return { span, diaToCount };
129}
130
131
133SPAN_DRILL_TABLE loadDrillGroundTruth( const std::string& aBoardDir )
134{
135 SPAN_DRILL_TABLE table;
136
137 for( const auto& entry : std::filesystem::directory_iterator( aBoardDir ) )
138 {
139 if( entry.path().extension() != ".drl" )
140 continue;
141
142 auto [span, counts] = parseDrillFile( entry.path() );
143
144 for( const auto& [dia, count] : counts )
145 table[span][dia] += count;
146 }
147
148 return table;
149}
150
151
153std::map<PCB_LAYER_ID, int> buildCopperOrdinal( const BOARD& aBoard )
154{
155 std::map<PCB_LAYER_ID, int> ordinal;
156 int idx = 1;
157
158 for( PCB_LAYER_ID layer : aBoard.GetEnabledLayers().CuStack() )
159 ordinal[layer] = idx++;
160
161 return ordinal;
162}
163
164
166LAYER_SPAN viaLayerSpan( const PCB_VIA& aVia, const std::map<PCB_LAYER_ID, int>& aOrdinal, int aTotal )
167{
168 auto index = [&aOrdinal]( PCB_LAYER_ID aLayer, int aDefault )
169 {
170 auto it = aOrdinal.find( aLayer );
171 return it != aOrdinal.end() ? it->second : aDefault;
172 };
173
174 int top = index( aVia.TopLayer(), 1 );
175 int bot = index( aVia.BottomLayer(), aTotal );
176
177 if( top > bot )
178 std::swap( top, bot );
179
180 return { top, bot };
181}
182
183
185SPAN_DRILL_TABLE collectBoardDrillTable( const BOARD& aBoard )
186{
187 SPAN_DRILL_TABLE table;
188 std::map<PCB_LAYER_ID, int> ordinal = buildCopperOrdinal( aBoard );
189 const int total = aBoard.GetCopperLayerCount();
190
191 for( PCB_TRACK* track : aBoard.Tracks() )
192 {
193 if( track->Type() != PCB_VIA_T )
194 continue;
195
196 PCB_VIA* via = static_cast<PCB_VIA*>( track );
197 int drill = toMicrons( via->GetDrillValue() );
198
199 if( drill <= 0 )
200 continue;
201
202 table[viaLayerSpan( *via, ordinal, total )][drill] += 1;
203 }
204
205 for( FOOTPRINT* fp : aBoard.Footprints() )
206 {
207 for( PAD* pad : fp->Pads() )
208 {
209 if( pad->GetAttribute() != PAD_ATTRIB::PTH && pad->GetAttribute() != PAD_ATTRIB::NPTH )
210 continue;
211
212 // The Excellon .drl files describe round drills only; oblong slots are routed and
213 // live in the companion .rou file, so they are out of scope for this comparison.
214 if( pad->GetDrillShape() == PAD_DRILL_SHAPE::OBLONG )
215 continue;
216
217 int drill = toMicrons( pad->GetDrillSizeX() );
218
219 if( drill <= 0 )
220 continue;
221
222 // KiCad through-hole pads always span the full copper stack.
223 table[{ 1, total }][drill] += 1;
224 }
225 }
226
227 return table;
228}
229
230
232std::string describeTable( const SPAN_DRILL_TABLE& aTable )
233{
234 std::string out;
235
236 for( const auto& [span, sizes] : aTable )
237 {
238 for( const auto& [dia, count] : sizes )
239 {
240 out += " span " + std::to_string( span.first ) + "-" + std::to_string( span.second )
241 + " drill " + std::to_string( dia ) + "um x " + std::to_string( count ) + "\n";
242 }
243 }
244
245 return out;
246}
247
248
249struct DRILL_SPAN_FIXTURE
250{
251 DRILL_SPAN_FIXTURE()
252 {
253 m_boardDir = KI_TEST::AllegroBoardDataDir( "ABX00162_UNOQ" );
254 m_groundTruth = loadDrillGroundTruth( m_boardDir );
256 m_boardDir + "20250806_UNOQ.brd" );
257 }
258
259 std::string m_boardDir;
260 SPAN_DRILL_TABLE m_groundTruth;
261 BOARD* m_board = nullptr;
262};
263
264} // namespace
265
266
267BOOST_FIXTURE_TEST_SUITE( AllegroDrillSpans, DRILL_SPAN_FIXTURE )
268
269
270
275BOOST_AUTO_TEST_CASE( GroundTruthAndBoardLoad )
276{
277 BOOST_REQUIRE_MESSAGE( m_board != nullptr, "20250806_UNOQ.brd failed to import" );
278
279 // The .drl set describes six spans: 1-2, 2-3, 3-6, 6-7, 7-8 and the 1-8 through holes.
280 BOOST_REQUIRE_MESSAGE( m_groundTruth.size() >= 6,
281 "Expected at least 6 drill spans, parsed " << m_groundTruth.size() );
282
283 BOOST_TEST_MESSAGE( "Ground-truth drill table:\n" << describeTable( m_groundTruth ) );
284
285 BOOST_CHECK_EQUAL( m_board->GetCopperLayerCount(), 8 );
286}
287
288
293BOOST_AUTO_TEST_CASE( BlindAndBuriedViasPresent )
294{
295 BOOST_REQUIRE( m_board != nullptr );
296
297 std::map<VIATYPE, int> typeCounts;
298 std::set<LAYER_SPAN> viaSpans;
299 std::map<PCB_LAYER_ID, int> ordinal = buildCopperOrdinal( *m_board );
300 const int total = m_board->GetCopperLayerCount();
301
302 for( PCB_TRACK* track : m_board->Tracks() )
303 {
304 if( track->Type() != PCB_VIA_T )
305 continue;
306
307 PCB_VIA* via = static_cast<PCB_VIA*>( track );
308 typeCounts[via->GetViaType()]++;
309 viaSpans.insert( viaLayerSpan( *via, ordinal, total ) );
310 }
311
312 BOOST_TEST_MESSAGE( "Via type counts: THROUGH=" << typeCounts[VIATYPE::THROUGH]
313 << " BLIND=" << typeCounts[VIATYPE::BLIND]
314 << " BURIED=" << typeCounts[VIATYPE::BURIED]
315 << " MICROVIA=" << typeCounts[VIATYPE::MICROVIA] );
316
317 std::string spanList;
318
319 for( const LAYER_SPAN& s : viaSpans )
320 spanList += " " + std::to_string( s.first ) + "-" + std::to_string( s.second );
321
322 BOOST_TEST_MESSAGE( "Distinct via spans:" << spanList );
323
324 int nonThrough = typeCounts[VIATYPE::BLIND] + typeCounts[VIATYPE::BURIED]
325 + typeCounts[VIATYPE::MICROVIA];
326
327 BOOST_CHECK_MESSAGE( nonThrough > 0, "Importer produced no blind/buried/microvia vias" );
328
329 // The board has exactly six via spans: five blind/buried plus the 1-8 through span.
330 const std::set<LAYER_SPAN> expectedViaSpans = { { 1, 2 }, { 1, 8 }, { 2, 3 },
331 { 3, 6 }, { 6, 7 }, { 7, 8 } };
332
333 BOOST_CHECK_MESSAGE( viaSpans == expectedViaSpans, "Unexpected via span set:" << spanList );
334}
335
336
341BOOST_AUTO_TEST_CASE( DrillSpansMatchExcellon )
342{
343 BOOST_REQUIRE( m_board != nullptr );
344
345 SPAN_DRILL_TABLE boardTable = collectBoardDrillTable( *m_board );
346
347 BOOST_TEST_MESSAGE( "Imported board drill table:\n" << describeTable( boardTable ) );
348
349 // Reduce both tables to the set of (span, diameter) keys, ignoring counts.
350 auto keySet = []( const SPAN_DRILL_TABLE& aTable )
351 {
352 std::set<std::tuple<int, int, int>> keys;
353
354 for( const auto& [span, sizes] : aTable )
355 for( const auto& [dia, count] : sizes )
356 keys.insert( { span.first, span.second, dia } );
357
358 return keys;
359 };
360
361 std::set<std::tuple<int, int, int>> truthKeys = keySet( m_groundTruth );
362 std::set<std::tuple<int, int, int>> boardKeys = keySet( boardTable );
363
364 std::vector<std::tuple<int, int, int>> missing; // in .drl but not in board
365 std::vector<std::tuple<int, int, int>> extra; // in board but not in .drl
366
367 std::set_difference( truthKeys.begin(), truthKeys.end(), boardKeys.begin(), boardKeys.end(),
368 std::back_inserter( missing ) );
369 std::set_difference( boardKeys.begin(), boardKeys.end(), truthKeys.begin(), truthKeys.end(),
370 std::back_inserter( extra ) );
371
372 for( const auto& [t, b, d] : missing )
373 BOOST_TEST_MESSAGE( " MISSING from board: span " << t << "-" << b << " drill " << d << "um" );
374
375 for( const auto& [t, b, d] : extra )
376 BOOST_TEST_MESSAGE( " EXTRA in board: span " << t << "-" << b << " drill " << d << "um" );
377
378 BOOST_CHECK_MESSAGE( missing.empty(),
379 missing.size() << " (span, drill) combinations from the .drl files are "
380 "absent from the imported board" );
381 BOOST_CHECK_MESSAGE( extra.empty(),
382 extra.size() << " (span, drill) combinations in the imported board do not "
383 "exist in any .drl file" );
384
385 // The key set alone would miss holes shifted between two spans that both exist (e.g. some 1-2
386 // microvias landing on 2-3), so also compare counts. The tolerance absorbs the small drift from
387 // the board being a revision newer than the drill files while still catching gross shifts.
388 for( const auto& [span, sizes] : m_groundTruth )
389 {
390 for( const auto& [dia, truthCount] : sizes )
391 {
392 int boardCount = 0;
393
394 if( auto sIt = boardTable.find( span ); sIt != boardTable.end() )
395 {
396 if( auto dIt = sIt->second.find( dia ); dIt != sIt->second.end() )
397 boardCount = dIt->second;
398 }
399
400 const int tolerance = std::max( 5, truthCount / 100 );
401
402 BOOST_CHECK_MESSAGE( std::abs( boardCount - truthCount ) <= tolerance,
403 "span " << span.first << "-" << span.second << " drill " << dia
404 << "um: board has " << boardCount << " holes, .drl has "
405 << truthCount << " (tolerance " << tolerance << ")" );
406 }
407 }
408}
409
410
int index
const char * name
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:323
int GetCopperLayerCount() const
Definition board.cpp:937
const FOOTPRINTS & Footprints() const
Definition board.h:364
const TRACKS & Tracks() const
Definition board.h:362
const LSET & GetEnabledLayers() const
A proxy function that calls the corresponding function in m_BoardSettings.
Definition board.cpp:986
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:263
Definition pad.h:65
PCB_LAYER_ID BottomLayer() const
PCB_LAYER_ID TopLayer() const
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:60
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:94