KiCad PCB EDA Suite
Loading...
Searching...
No Matches
topo_match.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
20#include <cstdio>
21#include <cstdlib>
22#include <cmath>
23#include <string>
24#include <vector>
25#include <algorithm>
26#include <cassert>
27#include <future>
28#include <map>
29#include <set>
30#include <unordered_map>
31#include <unordered_set>
32
33#include <thread_pool.h>
34#include <cctype>
35
36#include <core/profile.h>
37#include <pad.h>
38#include <footprint.h>
39#include <refdes_utils.h>
40#include <board.h>
41#include <wx/string.h>
42#include <wx/log.h>
43
44#include "topo_match.h"
45
46
47static const wxString traceTopoMatch = wxT( "TOPO_MATCH" );
48static const wxString traceTopoMatchDetail = wxT( "TOPO_MATCH_DETAIL" );
49
50
51namespace TMATCH
52{
53
54bool PIN::IsIsomorphic( const PIN& b, TOPOLOGY_MISMATCH_REASON& aReason ) const
55{
56 if( m_conns.size() != b.m_conns.size() )
57 {
58 wxLogTrace( traceTopoMatch,
59 wxT( "[conns mismatch n1 %d n2 %d c-ref %d c-other %d thispin %s-%s "
60 "otherpin %s-%s" ),
62 b.m_netcode,
63 (int) m_conns.size(),
64 (int) b.m_conns.size(),
65 m_parent->m_reference,
66 m_ref,
68 b.m_ref );
69
70 aReason.m_reference = m_parent->GetParent()->GetReferenceAsString();
72 aReason.m_reason = wxString::Format(
73 _( "Pad %s of %s connects to %lu pads, but candidate pad %s of %s connects to %lu." ), m_ref,
74 aReason.m_reference, static_cast<unsigned long>( m_conns.size() ), b.m_ref, aReason.m_candidate,
75 static_cast<unsigned long>( b.m_conns.size() ) );
76
77 for( auto c : m_conns )
78 {
79 wxLogTrace( traceTopoMatch, wxT( "%s-%s " ), c->m_parent->m_reference, c->m_ref );
80 }
81
82 wxLogTrace( traceTopoMatch, wxT( "||" ) );
83
84 for( auto c : b.m_conns )
85 {
86 wxLogTrace( traceTopoMatch, wxT( "%s-%s " ), c->m_parent->m_reference, c->m_ref );
87 }
88
89
90 wxLogTrace( traceTopoMatch, wxT( "] " ) );
91 return false;
92 }
93
94 if( m_conns.empty() )
95 {
96 wxLogTrace( traceTopoMatch, wxT( "[conns empty]" ) );
97 return true;
98 }
99
100 std::vector<bool> matches( m_conns.size() );
101
102 for( size_t i = 0; i < m_conns.size(); i++ )
103 matches[i] = false;
104
105 size_t nref = 0;
106
107 for( auto& cref : m_conns )
108 {
109 for( size_t i = 0; i < m_conns.size(); i++ )
110 {
111 if( b.m_conns[i]->IsTopologicallySimilar( *cref ) )
112 {
113 matches[nref] = true;
114 break;
115 }
116 }
117
118 nref++;
119 }
120
121 for( size_t i = 0; i < m_conns.size(); i++ )
122 {
123 if( !matches[i] )
124 {
125 aReason.m_reference = m_parent->GetParent()->GetReferenceAsString();
127 aReason.m_reason = wxString::Format(
128 _( "Pad %s of %s cannot match candidate pad %s of %s due to differing connectivity." ), m_ref,
129 aReason.m_reference, b.m_ref, aReason.m_candidate );
130
131 return false;
132 }
133 }
134
135 return true;
136}
137
138
139std::unordered_map<int, int> buildBaseNetMapping( const BACKTRACK_STAGE& aMatches )
140{
141 std::unordered_map<int, int> mapping;
142 mapping.reserve( aMatches.GetMatchingComponentPairs().size() * 4 );
143
144 for( const auto& [tgtCmp, refCmp] : aMatches.GetMatchingComponentPairs() )
145 {
146 auto& refPins = refCmp->Pins();
147 auto& tgtPins = tgtCmp->Pins();
148
149 for( size_t i = 0; i < refPins.size() && i < tgtPins.size(); i++ )
150 mapping[refPins[i]->GetNetCode()] = tgtPins[i]->GetNetCode();
151 }
152
153 return mapping;
154}
155
156
157bool checkCandidateNetConsistency( const std::unordered_map<int, int>& aBaseMapping,
158 COMPONENT* aRef, COMPONENT* aTgt,
160 const std::unordered_set<int>& aExternalNets )
161{
162 if( aRef->Pins().size() != aTgt->Pins().size() )
163 {
164 aReason.m_reference = aRef->GetParent()->GetReferenceAsString();
165 aReason.m_candidate = aTgt->GetParent()->GetReferenceAsString();
166 aReason.m_reason =
167 wxString::Format( _( "Component %s expects %lu matching pads but candidate %s provides %lu." ),
168 aReason.m_reference, static_cast<unsigned long>( aRef->Pins().size() ),
169 aReason.m_candidate, static_cast<unsigned long>( aTgt->Pins().size() ) );
170 return false;
171 }
172
173 // Track net mappings introduced by this candidate's pins that aren't yet in the
174 // base mapping. Two pins sharing the same ref net must map to the same target net.
175 std::unordered_map<int, int> candidateAdditions;
176
177 for( size_t i = 0; i < aRef->Pins().size(); i++ )
178 {
179 int refNet = aRef->Pins()[i]->GetNetCode();
180 int tgtNet = aTgt->Pins()[i]->GetNetCode();
181
182 // Pads on external (global/power) nets are shared with the outside world and may be
183 // tied to different global nets in different channels (e.g. an address-select pin tied
184 // to +3V3 in one channel and GND in another). Skip them in the net-consistency check
185 // so such legitimate differences do not cause false topology-mismatch errors.
186 if( aExternalNets.count( refNet ) || aExternalNets.count( tgtNet ) )
187 continue;
188
189 auto baseIt = aBaseMapping.find( refNet );
190
191 if( baseIt != aBaseMapping.end() )
192 {
193 if( baseIt->second != tgtNet )
194 {
195 wxLogTrace( traceTopoMatch, wxT( "nets inconsistent\n" ) );
196
197 aReason.m_reference = aRef->GetParent()->GetReferenceAsString();
198 aReason.m_candidate = aTgt->GetParent()->GetReferenceAsString();
199
200 wxString refNetName;
201 wxString tgtNetName;
202
203 if( const BOARD* board = aRef->GetParent()->GetBoard() )
204 {
205 if( const NETINFO_ITEM* net = board->FindNet( refNet ) )
206 refNetName = net->GetNetname();
207 }
208
209 if( const BOARD* board = aTgt->GetParent()->GetBoard() )
210 {
211 if( const NETINFO_ITEM* net = board->FindNet( tgtNet ) )
212 tgtNetName = net->GetNetname();
213 }
214
215 if( refNetName.IsEmpty() )
216 refNetName = wxString::Format( _( "net %d" ), refNet );
217
218 if( tgtNetName.IsEmpty() )
219 tgtNetName = wxString::Format( _( "net %d" ), tgtNet );
220
221 aReason.m_reason = wxString::Format(
222 _( "Pad %s of %s is on net %s but its match in candidate %s is on net %s." ),
223 aRef->Pins()[i]->GetReference(), aReason.m_reference, refNetName,
224 aReason.m_candidate, tgtNetName );
225
226 return false;
227 }
228
229 continue;
230 }
231
232 auto localIt = candidateAdditions.find( refNet );
233
234 if( localIt != candidateAdditions.end() )
235 {
236 if( localIt->second != tgtNet )
237 {
238 wxLogTrace( traceTopoMatch, wxT( "nets inconsistent (candidate internal)\n" ) );
239
240 aReason.m_reference = aRef->GetParent()->GetReferenceAsString();
241 aReason.m_candidate = aTgt->GetParent()->GetReferenceAsString();
242 aReason.m_reason = wxString::Format(
243 _( "Pad %s of %s has inconsistent net mapping in candidate %s." ),
244 aRef->Pins()[i]->GetReference(), aReason.m_reference,
245 aReason.m_candidate );
246
247 return false;
248 }
249
250 continue;
251 }
252
253 candidateAdditions[refNet] = tgtNet;
254 }
255
256 return true;
257}
258
259
260std::vector<COMPONENT*>
262 const std::vector<COMPONENT*>& aStructuralMatches,
263 const TOPOLOGY_MISMATCH_REASON& aStructuralReason,
264 const BACKTRACK_STAGE& partialMatches,
265 std::vector<TOPOLOGY_MISMATCH_REASON>& aMismatchReasons,
266 const std::atomic<bool>* aCancelled )
267{
268 if( aCancelled && aCancelled->load( std::memory_order_relaxed ) )
269 return {};
270
271 PROF_TIMER timerFmc;
272
273 aMismatchReasons.clear();
274 std::vector<COMPONENT*> matches;
275 int candidatesChecked = 0;
276
277 // Build the net consistency map from locked pairs once for this entire evaluation
278 // pass, rather than rebuilding it from scratch for every candidate.
279 std::unordered_map<int, int> baseNetMapping = buildBaseNetMapping( partialMatches );
280
281 double netCheckMs = 0.0;
282
283 for( COMPONENT* cmpTarget : aStructuralMatches )
284 {
285 if( partialMatches.m_locked.find( cmpTarget ) != partialMatches.m_locked.end() )
286 continue;
287
288 candidatesChecked++;
289
290 wxLogTrace( traceTopoMatch, wxT( "Check '%s'/'%s' " ), aRef->m_reference,
291 cmpTarget->m_reference );
292
293 TOPOLOGY_MISMATCH_REASON localReason;
294 localReason.m_reference = aRef->GetParent()->GetReferenceAsString();
295 localReason.m_candidate = cmpTarget->GetParent()->GetReferenceAsString();
296
297 PROF_TIMER timerNet;
298 bool netResult = checkCandidateNetConsistency( baseNetMapping, aRef, cmpTarget, localReason,
300 timerNet.Stop();
301 netCheckMs += timerNet.msecs();
302
303 if( netResult )
304 {
305 wxLogTrace( traceTopoMatch, wxT( "match!\n" ) );
306 matches.push_back( cmpTarget );
307 }
308 else
309 {
310 wxLogTrace( traceTopoMatch, wxT( "Reject [net topo mismatch]\n" ) );
311 aMismatchReasons.push_back( localReason );
312 }
313 }
314
315 PROF_TIMER timerScore;
316
317 std::unordered_map<COMPONENT*, double> simScores;
318 simScores.reserve( matches.size() );
319
320 for( COMPONENT* match : matches )
321 {
322 int n = 0;
323
324 for( size_t i = 0; i < aRef->m_pins.size(); i++ )
325 {
326 if( aRef->m_pins[i]->GetNetCode() == match->m_pins[i]->GetNetCode() )
327 n++;
328 }
329
330 simScores[match] = static_cast<double>( n ) / static_cast<double>( aRef->m_pins.size() );
331 }
332
333 std::sort( matches.begin(), matches.end(),
334 [&]( COMPONENT* a, COMPONENT* b ) -> bool
335 {
336 double simA = simScores[a];
337 double simB = simScores[b];
338
339 if( simA != simB )
340 return simA > simB;
341
342 return a->GetParent()->GetReferenceAsString()
343 < b->GetParent()->GetReferenceAsString();
344 } );
345
346 timerScore.Stop();
347
348 if( matches.empty() && aMismatchReasons.empty() )
349 {
350 // No net-consistency reasons were recorded above, which means there were no structural
351 // candidates to test in the first place. Surface the structural reason captured during
352 // precomputation so the user sees the actual connectivity difference (e.g. a pad that
353 // connects to a different number of pads because of an external loop between channels)
354 // rather than a generic "no compatible component" message.
355 if( !aStructuralReason.m_reason.IsEmpty() )
356 {
357 aMismatchReasons.push_back( aStructuralReason );
358 }
359 else
360 {
362 reason.m_reference = aRef->GetParent()->GetReferenceAsString();
363 reason.m_reason = _( "No compatible component found in the target area." );
364 aMismatchReasons.push_back( reason );
365 }
366 }
367
368 timerFmc.Stop();
369
370 wxLogTrace( traceTopoMatchDetail,
371 wxT( " findMatch '%s' (%d pins): %s total, checked %d/%d structural, "
372 "netCheck %0.3f ms, score %0.3f ms, %d matches" ),
373 aRef->m_reference, aRef->GetPinCount(), timerFmc.to_string(),
374 candidatesChecked, (int) aStructuralMatches.size(),
375 netCheckMs, timerScore.msecs(),
376 (int) matches.size() );
377
378 return matches;
379}
380
381
382void CONNECTION_GRAPH::breakTie( COMPONENT* aRef, std::vector<COMPONENT*>& aMatches ) const
383{
384 if( aMatches.size() <= 1 )
385 return;
386
387 wxString candidateRefs;
388
389 for( size_t i = 0; i < aMatches.size(); i++ )
390 {
391 if( i > 0 )
392 candidateRefs += wxT( ", " );
393
394 candidateRefs += aMatches[i]->GetParent()->GetReferenceAsString();
395 }
396
397 wxLogTrace( traceTopoMatch, wxT( "Topology tie for %s: %s" ),
398 aRef->GetParent()->GetReferenceAsString(), candidateRefs );
399
400 if( breakTieBySymbolUuid( aRef, aMatches ) )
401 {
402 wxLogTrace( traceTopoMatchDetail, wxT( "Broke tie with symbol UUID match for %s" ),
403 aRef->GetParent()->GetReferenceAsString() );
404 }
405 // TODO: other tie breakers can be added, e.g. based on position or reference designators,
406 // just waiting for actual user test cases
407 else
408 {
409 wxLogTrace( traceTopoMatchDetail, wxT( "No tie breakers worked for %s, leaving match order alone." ),
410 aRef->GetParent()->GetReferenceAsString() );
411 }
412}
413
414
415bool CONNECTION_GRAPH::breakTieBySymbolUuid( COMPONENT* aRef, std::vector<COMPONENT*>& aMatches ) const
416{
417 auto getSymbolInstanceUuid =
418 []( const FOOTPRINT* aFootprint ) -> KIID
419 {
420 if( !aFootprint )
421 return niluuid;
422
423 const KIID_PATH& path = aFootprint->GetPath();
424
425 if( path.empty() )
426 return niluuid;
427
428 const KIID& symbolUuid = path.back();
429
430 return symbolUuid;
431 };
432
433 FOOTPRINT* refFp = aRef ? aRef->GetParent() : nullptr;
434 const KIID refSymbolUuid = getSymbolInstanceUuid( refFp );
435 wxString candidateSymbolUuids;
436 wxString matchingSymbolCandidates;
437 int symbolUuidHitCount = 0;
438 int uniqueMatchIdx = -1;
439
440 if( refSymbolUuid == niluuid )
441 {
442 wxLogTrace( traceTopoMatchDetail, wxT( "Tie symbol UUID unavailable for %s" ),
443 refFp ? refFp->GetReferenceAsString() : wxString( wxT( "<null>" ) ) );
444 return false;
445 }
446
447 // Inspect every tied candidate and collect:
448 // 1) a detailed ref->symbol UUID mapping string for traces, and
449 // 2) the subset of candidates whose symbol-path tail UUID matches the reference.
450 for( size_t i = 0; i < aMatches.size(); i++ )
451 {
452 FOOTPRINT* candidateFp = aMatches[i]->GetParent();
453 const wxString candidateRef = candidateFp->GetReferenceAsString();
454 const KIID candidateSymbolUuid = getSymbolInstanceUuid( candidateFp );
455
456 if( i > 0 )
457 candidateSymbolUuids += wxT( ", " );
458
459 if( candidateSymbolUuid == niluuid )
460 candidateSymbolUuids += candidateRef + wxT( "=<none>" );
461 else
462 candidateSymbolUuids += candidateRef + wxT( "=" ) + candidateSymbolUuid.AsString();
463
464 if( candidateSymbolUuid == refSymbolUuid )
465 {
466 if( uniqueMatchIdx < 0 )
467 uniqueMatchIdx = static_cast<int>( i );
468
469 symbolUuidHitCount++;
470
471 if( !matchingSymbolCandidates.IsEmpty() )
472 matchingSymbolCandidates += wxT( ", " );
473
474 matchingSymbolCandidates += candidateRef;
475 }
476 }
477
478 wxLogTrace( traceTopoMatchDetail, wxT( "Tie reference symbol UUID for %s: %s (hits=%d)" ),
479 refFp->GetReferenceAsString(), refSymbolUuid.AsString(), symbolUuidHitCount );
480
481 wxLogTrace( traceTopoMatchDetail, wxT( "Tie candidate symbol UUIDs: %s" ), candidateSymbolUuids );
482
483 // One match is what we want, we should have one match between the source symbol instance
484 // and the destination only since in theory we are repeating across two instances of the same sheet
485 if( symbolUuidHitCount == 1 )
486 {
487 wxLogTrace( traceTopoMatchDetail, wxT( "Symbol UUID unique match (usable) for %s: %s" ),
488 refFp->GetReferenceAsString(), matchingSymbolCandidates );
489
490 std::rotate( aMatches.begin(), aMatches.begin() + uniqueMatchIdx, aMatches.begin() + uniqueMatchIdx + 1 );
491
492 wxLogTrace( traceTopoMatchDetail, wxT( "Applied symbol UUID tie-break for %s: selected %s" ),
493 refFp->GetReferenceAsString(), aMatches.front()->GetParent()->GetReferenceAsString() );
494
495 return true;
496 }
497 // Copy and pasting footprints can result in multiple matches
498 else if( symbolUuidHitCount > 1 )
499 {
500 wxLogTrace( traceTopoMatchDetail, wxT( "Symbol UUID multiple matches (not usable) for %s: %s" ),
501 refFp->GetReferenceAsString(), matchingSymbolCandidates );
502 return false;
503 }
504 // Probably not sheet instances, break the tie some other way
505 else
506 {
507 wxLogTrace( traceTopoMatchDetail, wxT( "No symbol UUID candidate match (not usable) for %s" ),
508 refFp->GetReferenceAsString() );
509 return false;
510 }
511}
512
513
515{
516 std::sort( m_pins.begin(), m_pins.end(),
517 []( PIN* a, PIN* b )
518 {
519 return a->GetReference() < b->GetReference();
520 } );
521}
522
523
525{
526 std::sort( m_components.begin(), m_components.end(),
527 []( COMPONENT* a, COMPONENT* b )
528 {
529 if( a->GetPinCount() != b->GetPinCount() )
530 return a->GetPinCount() > b->GetPinCount();
531
532 return a->GetParent()->GetReferenceAsString() < b->GetParent()->GetReferenceAsString();
533 } );
534}
535
536
537void CONNECTION_GRAPH::BuildConnectivity( const std::unordered_set<int>& aExternalNets )
538{
539 m_externalNets = aExternalNets;
540
541 std::map<int, std::vector<PIN*>> nets;
542
544
545 for( auto c : m_components )
546 {
547 c->sortPinsByName();
548
549 for( auto p : c->Pins() )
550 {
551 if( p->GetNetCode() > 0 )
552 nets[p->GetNetCode()].push_back( p );
553 }
554 }
555
556 for( auto& [netcode, pins] : nets )
557 {
558 // Skip nets that extend beyond this channel's footprint set. Global power nets
559 // (GND, VCC, etc.) are shared across channels and can create spurious intra-channel
560 // connections that cause false topology mismatches when hierarchical pins are tied
561 // directly to those nets.
562 if( aExternalNets.count( netcode ) )
563 continue;
564
565 wxLogTrace( traceTopoMatch, wxT( "net %d: %d connections\n" ), netcode,
566 (int) pins.size() );
567
568 for( PIN* p : pins )
569 {
570 p->m_conns.reserve( pins.size() - 1 );
571
572 for( PIN* p2 : pins )
573 {
574 if( p != p2 )
575 p->m_conns.push_back( p2 );
576 }
577 }
578 }
579
580/* for( auto c : m_components )
581 for( auto p : c->Pins() )
582 {
583 printf("pin %s: \n", p->m_ref.c_str().AsChar() );
584
585 for( auto c : p->m_conns )
586 printf( "%s ", c->m_ref.c_str().AsChar() );
587 printf("\n");
588 }
589 */
590}
591
592
594 std::vector<TOPOLOGY_MISMATCH_REASON>& aMismatchReasons,
595 const ISOMORPHISM_PARAMS& aParams )
596{
597 std::vector<BACKTRACK_STAGE> stack;
599
600 aMismatchReasons.clear();
601
602 if( aParams.m_totalComponents )
603 aParams.m_totalComponents->store( (int) m_components.size(), std::memory_order_relaxed );
604
605 PROF_TIMER timerTotal;
606 int backtrackCount = 0;
607 double mrvTotalMs = 0.0;
608
609 std::vector<TOPOLOGY_MISMATCH_REASON> localReasons;
610
611 if( m_components.empty()|| aTarget->m_components.empty() )
612 {
614 reason.m_reason = _( "One or both of the areas has no components assigned." );
615 aMismatchReasons.push_back( reason );
616 return false;
617 }
618
619 if( m_components.size() != aTarget->m_components.size() )
620 {
622 reason.m_reason = _( "Component count mismatch" );
623 aMismatchReasons.push_back( reason );
624 return false;
625 }
626
627 // Structural compatibility (MatchesWith) depends only on pin count, footprint ID, and
628 // pin connection topology -- all immutable graph properties. Precompute it once per
629 // source component so the backtracking loop never repeats these comparisons.
630 size_t numRef = m_components.size();
631 std::vector<std::vector<COMPONENT*>> structuralMatches( numRef );
632
633 // When a source component has no structural match at all, keep a representative reason so we
634 // can explain the actual connectivity difference rather than a generic "no compatible
635 // component" message.
636 std::vector<TOPOLOGY_MISMATCH_REASON> structuralReasons( numRef );
637
638 PROF_TIMER timerPrecompute;
639 {
641 std::vector<std::future<void>> futures;
642 futures.reserve( numRef );
643
644 const std::atomic<bool>* cancelled = aParams.m_cancelled;
645
646 for( size_t i = 0; i < numRef; i++ )
647 {
648 futures.emplace_back( tp.submit_task(
649 [this, i, aTarget, &structuralMatches, &structuralReasons, cancelled]()
650 {
651 if( cancelled && cancelled->load( std::memory_order_relaxed ) )
652 return;
653
654 COMPONENT* ref = m_components[i];
655 TOPOLOGY_MISMATCH_REASON reason;
656 TOPOLOGY_MISMATCH_REASON bestReason;
657
658 for( COMPONENT* tgt : aTarget->m_components )
659 {
660 if( ref->MatchesWith( tgt, reason ) )
661 {
662 structuralMatches[i].push_back( tgt );
663 }
664 else if( bestReason.m_reason.IsEmpty() || ref->IsSameKind( *tgt ) )
665 {
666 // Prefer the reason from a same-kind counterpart (same prefix and
667 // footprint) because that is the candidate the user actually expects
668 // to match; a connectivity difference there is the meaningful failure.
669 bestReason = reason;
670 }
671 }
672
673 if( structuralMatches[i].empty() )
674 structuralReasons[i] = bestReason;
675 } ) );
676 }
677
678 for( auto& f : futures )
679 f.wait();
680 }
681 timerPrecompute.Stop();
682
683 wxLogTrace( traceTopoMatchDetail,
684 wxT( "Structural precomputation: %s (%d source x %d target)" ),
685 timerPrecompute.to_string(), (int) numRef,
686 (int) aTarget->m_components.size() );
687
688 if( aParams.m_cancelled && aParams.m_cancelled->load( std::memory_order_relaxed ) )
689 return false;
690
691 top.m_ref = m_components.front();
692 top.m_refIndex = 0;
693
694 stack.push_back( top );
695
696 int nloops = 0;
697
698 while( !stack.empty() )
699 {
700 if( aParams.m_cancelled && aParams.m_cancelled->load( std::memory_order_relaxed ) )
701 return false;
702
703 nloops++;
704 auto& current = stack.back();
705
706 for( auto it = current.m_locked.begin(); it != current.m_locked.end(); it++ )
707 {
708 if (it->second == current.m_ref)
709 {
710 wxLogTrace( traceTopoMatch, wxT( "stk: Remove %s from locked\n" ),
711 current.m_ref->m_reference );
712 current.m_locked.erase( it );
713 break;
714 }
715 }
716
717 if( nloops >= c_ITER_LIMIT )
718 {
719 wxLogTrace( traceTopoMatch, wxT( "stk: Iter cnt exceeded\n" ) );
720
722 reason.m_reason = _( "Iteration count exceeded (timeout)" );
723
724 if( aMismatchReasons.empty() )
725 aMismatchReasons.push_back( reason );
726 else
727 aMismatchReasons.insert( aMismatchReasons.begin(), reason );
728
729 return false;
730 }
731
732 if( current.m_currentMatch < 0 )
733 {
734 PROF_TIMER timerInitMatch;
735
736 localReasons.clear();
737 current.m_matches = aTarget->findMatchingComponents(
738 current.m_ref, structuralMatches[current.m_refIndex],
739 structuralReasons[current.m_refIndex], current, localReasons,
740 aParams.m_cancelled );
741
742 timerInitMatch.Stop();
743
744 wxLogTrace( traceTopoMatchDetail,
745 wxT( "iter %d: initial match for '%s' (%d pins): %s, %d candidates" ),
746 nloops, current.m_ref->m_reference, current.m_ref->GetPinCount(),
747 timerInitMatch.to_string(), (int) current.m_matches.size() );
748
749 if( current.m_matches.empty() && aMismatchReasons.empty() && !localReasons.empty() )
750 aMismatchReasons = localReasons;
751
752 current.m_currentMatch = 0;
753 }
754
755 wxLogTrace( traceTopoMatch, wxT( "stk: Current '%s' stack %d cm %d/%d locked %d/%d\n" ),
756 current.m_ref->m_reference, (int) stack.size(), current.m_currentMatch,
757 (int) current.m_matches.size(), (int) current.m_locked.size(),
758 (int) m_components.size() );
759
760 if( current.m_currentMatch == 0 && current.m_matches.size() > 1 )
761 breakTie( current.m_ref, current.m_matches );
762
763 if ( current.m_matches.empty() )
764 {
765 wxLogTrace( traceTopoMatch, wxT( "stk: No matches at all, going up [level=%d]\n" ),
766 (int) stack.size() );
767 stack.pop_back();
768 backtrackCount++;
769 continue;
770 }
771
772 if( current.m_currentMatch >= 0
773 && static_cast<size_t>( current.m_currentMatch ) >= current.m_matches.size() )
774 {
775 wxLogTrace( traceTopoMatch, wxT( "stk: No more matches, going up [level=%d]\n" ),
776 (int) stack.size() );
777 stack.pop_back();
778 backtrackCount++;
779 continue;
780 }
781
782 auto& match = current.m_matches[current.m_currentMatch];
783
784 wxLogTrace( traceTopoMatch, wxT( "stk: candidate '%s', match list : ( " ),
785 current.m_matches[current.m_currentMatch]->m_reference, current.m_refIndex );
786
787 for( auto m : current.m_matches )
788 wxLogTrace( traceTopoMatch, wxT( "%s " ), m->GetParent()->GetReferenceAsString() );
789
790 wxLogTrace( traceTopoMatch, wxT( "\n" ) );
791
792 current.m_currentMatch++;
793 current.m_locked[match] = current.m_ref;
794
795 if( aParams.m_matchedComponents )
796 {
797 aParams.m_matchedComponents->store( (int) current.m_locked.size(),
798 std::memory_order_relaxed );
799 }
800
801 if( current.m_locked.size() == m_components.size() )
802 {
803 current.m_nloops = nloops;
804
805 aResult.clear();
806 aMismatchReasons.clear();
807
808 for( auto iter : current.m_locked )
809 aResult[ iter.second->GetParent() ] = iter.first->GetParent();
810
811 timerTotal.Stop();
812 wxLogTrace( traceTopoMatch,
813 wxT( "Isomorphism: %s, %d iterations, %d backtracks, "
814 "MRV total %0.1f ms (%d candidates)" ),
815 timerTotal.to_string(), nloops, backtrackCount, mrvTotalMs,
816 (int) m_components.size() );
817
818 return true;
819 }
820
821
822 // MRV heuristic: find the unlocked component with the fewest candidate matches.
823 // Collect unlocked components, then evaluate them in parallel since each
824 // findMatchingComponents call is independent (read-only on graphs and current stage).
825 struct MRV_CANDIDATE
826 {
827 COMPONENT* m_cmp;
828 size_t m_index;
829 std::vector<COMPONENT*> m_matches;
830 std::vector<TOPOLOGY_MISMATCH_REASON> m_reasons;
831 };
832
833 std::vector<MRV_CANDIDATE> mrvCandidates;
834
835 // Build a set of all ref-components already locked so we can skip them in O(1)
836 // instead of scanning m_locked values for each component.
837 std::unordered_set<COMPONENT*> lockedRefs;
838 lockedRefs.reserve( current.m_locked.size() );
839
840 for( const auto& [tgt, ref] : current.m_locked )
841 lockedRefs.insert( ref );
842
843 for( size_t i = 0; i < m_components.size(); i++ )
844 {
845 COMPONENT* cmp = m_components[i];
846
847 if( cmp != current.m_ref && lockedRefs.find( cmp ) == lockedRefs.end() )
848 mrvCandidates.push_back( { cmp, i, {}, {} } );
849 }
850
851 static const size_t MRV_PARALLEL_THRESHOLD = 4;
852
853 PROF_TIMER timerMrv;
854
855 if( mrvCandidates.size() >= MRV_PARALLEL_THRESHOLD )
856 {
858 std::vector<std::future<void>> futures;
859 futures.reserve( mrvCandidates.size() );
860
861 const std::atomic<bool>* cancelled = aParams.m_cancelled;
862
863 for( MRV_CANDIDATE& c : mrvCandidates )
864 {
865 futures.emplace_back( tp.submit_task(
866 [&c, aTarget, &current, &structuralMatches, &structuralReasons, cancelled]()
867 {
868 c.m_matches = aTarget->findMatchingComponents(
869 c.m_cmp, structuralMatches[c.m_index],
870 structuralReasons[c.m_index], current, c.m_reasons,
871 cancelled );
872 } ) );
873 }
874
875 for( auto& f : futures )
876 f.wait();
877 }
878 else
879 {
880 for( MRV_CANDIDATE& c : mrvCandidates )
881 {
882 c.m_matches = aTarget->findMatchingComponents(
883 c.m_cmp, structuralMatches[c.m_index],
884 structuralReasons[c.m_index], current, c.m_reasons,
885 aParams.m_cancelled );
886 }
887 }
888
889 timerMrv.Stop();
890 double mrvMs = timerMrv.msecs();
891 mrvTotalMs += mrvMs;
892
893 wxLogTrace( traceTopoMatchDetail,
894 wxT( "iter %d: MRV scan %0.3f ms, %d unlocked candidates" ),
895 nloops, mrvMs, (int) mrvCandidates.size() );
896
897 if( aParams.m_cancelled && aParams.m_cancelled->load( std::memory_order_relaxed ) )
898 return false;
899
900 int minMatches = std::numeric_limits<int>::max();
901 COMPONENT* altNextRef = nullptr;
902 COMPONENT* bestNextRef = nullptr;
903 int bestRefIndex = 0;
904 int altRefIndex = 0;
905 std::vector<COMPONENT*> bestMatches;
906
907 for( MRV_CANDIDATE& c : mrvCandidates )
908 {
909 int nMatches = static_cast<int>( c.m_matches.size() );
910
911 if( nMatches == 1 )
912 {
913 bestNextRef = c.m_cmp;
914 bestRefIndex = static_cast<int>( c.m_index );
915 bestMatches = std::move( c.m_matches );
916 break;
917 }
918 else if( nMatches == 0 )
919 {
920 altNextRef = c.m_cmp;
921 altRefIndex = static_cast<int>( c.m_index );
922
923 if( aMismatchReasons.empty() && !c.m_reasons.empty() )
924 aMismatchReasons = c.m_reasons;
925 }
926 else if( nMatches < minMatches )
927 {
928 minMatches = nMatches;
929 bestNextRef = c.m_cmp;
930 bestRefIndex = static_cast<int>( c.m_index );
931 bestMatches = std::move( c.m_matches );
932 }
933 }
934
935 BACKTRACK_STAGE next( current );
936
937 if( bestNextRef )
938 {
939 wxLogTrace( traceTopoMatchDetail,
940 wxT( "iter %d: MRV picked '%s' (%d matches, best of %d)" ),
941 nloops, bestNextRef->m_reference,
942 (int) bestMatches.size(), (int) mrvCandidates.size() );
943
944 next.m_ref = bestNextRef;
945 next.m_refIndex = bestRefIndex;
946 next.m_matches = std::move( bestMatches );
947 next.m_currentMatch = 0;
948 }
949 else
950 {
951 wxLogTrace( traceTopoMatchDetail,
952 wxT( "iter %d: MRV dead end, alt='%s'" ),
953 nloops, altNextRef ? altNextRef->m_reference : wxString( "(none)" ) );
954
955 next.m_ref = altNextRef;
956 next.m_refIndex = altRefIndex;
957 next.m_currentMatch = -1;
958 }
959
960 stack.push_back( next );
961 };
962
963 timerTotal.Stop();
964 wxLogTrace( traceTopoMatch,
965 wxT( "Isomorphism: %s, %d iterations, %d backtracks, "
966 "MRV total %0.1f ms (%d candidates)" ),
967 timerTotal.to_string(), nloops, backtrackCount, mrvTotalMs,
968 (int) m_components.size() );
969
970 return false;
971}
972
973
974#if 0
975int main()
976{
977 FILE * f = fopen("connectivity.dump","rb" );
978 auto cgRef = loadCGraph(f);
979 auto cgTarget = loadCGraph(f);
980
981 cgRef->buildConnectivity();
982 cgTarget->buildConnectivity();
983
984 int attempts = 0;
985 int max_loops = 0;
986
987 for( ;; )
988 {
989 cgRef->shuffle();
990 cgTarget->shuffle();
991
992 const BacktrackStage latest = cgRef->matchCGraphs( cgTarget );
993
994 if( !latest.locked.size() )
995 {
996 printf("MATCH FAIL\n");
997 break;
998 }
999
1000 //printf("loops: %d\n", latest.nloops );
1001 //printf("Locked: %d\n", latest.locked.size() );
1002
1003 //if (matchFound)
1004 //{
1005 // for( auto& iter : latest.locked )
1006 //{
1007 // printf("%-10s : %-10s\n", iter.first->reference.c_str(), iter.second->reference.c_str() );
1008 //}
1009
1010 //}
1011
1012 if( latest.nloops > max_loops )
1013 {
1014 max_loops = latest.nloops;
1015 }
1016
1017 if (attempts % 10000 == 0)
1018 {
1019 printf("attempts: %d maxloops: %d\n", attempts, max_loops );
1020 }
1021
1022 attempts++;
1023
1024 }
1025
1026 fclose(f);
1027
1028 return 0;
1029}
1030
1031#endif
1032
1033
1034COMPONENT::COMPONENT( const wxString& aRef, FOOTPRINT* aParentFp,
1035 std::optional<VECTOR2I> aRaOffset ) :
1036 m_raOffset( aRaOffset ),
1037 m_reference( aRef ),
1038 m_parentFootprint( aParentFp )
1039{
1041}
1042
1043
1044bool COMPONENT::isChannelSuffix( const wxString& aSuffix )
1045{
1046 if( aSuffix.IsEmpty() )
1047 return true;
1048
1049 for( wxUniChar ch : aSuffix )
1050 {
1051 if( std::isalpha( static_cast<int>( ch ) ) )
1052 return false;
1053 }
1054
1055 return true;
1056}
1057
1058
1059bool COMPONENT::prefixesShareCommonBase( const wxString& aPrefixA, const wxString& aPrefixB )
1060{
1061 if( aPrefixA == aPrefixB )
1062 return true;
1063
1064 size_t commonLen = 0;
1065 size_t minLen = std::min( aPrefixA.length(), aPrefixB.length() );
1066
1067 while( commonLen < minLen && aPrefixA[commonLen] == aPrefixB[commonLen] )
1068 commonLen++;
1069
1070 if( commonLen == 0 )
1071 return false;
1072
1073 wxString suffixA = aPrefixA.Mid( commonLen );
1074 wxString suffixB = aPrefixB.Mid( commonLen );
1075
1076 return isChannelSuffix( suffixA ) && isChannelSuffix( suffixB );
1077}
1078
1079
1080bool COMPONENT::IsSameKind( const COMPONENT& b ) const
1081{
1083 return false;
1084
1085 return ( m_parentFootprint->GetFPID() == b.m_parentFootprint->GetFPID() )
1086 || ( m_parentFootprint->GetFPID().empty() && b.m_parentFootprint->GetFPID().empty() );
1087}
1088
1089
1091{
1092 m_pins.push_back( aPin );
1093 aPin->SetParent( this );
1094}
1095
1096
1098{
1099 if( GetPinCount() != b->GetPinCount() )
1100 {
1102 aReason.m_candidate = b->GetParent()->GetReferenceAsString();
1103 aReason.m_reason =
1104 wxString::Format( _( "Component %s has %d pads but candidate %s has %d." ), aReason.m_reference,
1105 GetPinCount(), aReason.m_candidate, b->GetPinCount() );
1106 return false;
1107 }
1108
1109 if( !IsSameKind( *b ) )
1110 {
1112 aReason.m_candidate = b->GetParent()->GetReferenceAsString();
1113
1115 {
1116 aReason.m_reason = wxString::Format(
1117 _( "Reference prefix mismatch: %s uses prefix '%s' but candidate %s uses '%s'." ),
1118 aReason.m_reference, m_prefix, aReason.m_candidate, b->m_prefix );
1119 }
1120 else
1121 {
1122 wxString refFootprint = GetParent()->GetFPIDAsString();
1123 wxString candFootprint = b->GetParent()->GetFPIDAsString();
1124
1125 if( refFootprint.IsEmpty() )
1126 refFootprint = _( "(no library ID)" );
1127
1128 if( candFootprint.IsEmpty() )
1129 candFootprint = _( "(no library ID)" );
1130
1131 aReason.m_reason =
1132 wxString::Format( _( "Library link mismatch: %s expects '%s' but candidate %s is '%s'." ),
1133 aReason.m_reference, refFootprint, aReason.m_candidate, candFootprint );
1134 }
1135
1136 return false;
1137 }
1138
1139 for( int pin = 0; pin < b->GetPinCount(); pin++ )
1140 {
1141 // Call with the reference pin as the subject so the reason's reference/candidate match
1142 // MatchesWith's own orientation (this == reference, b == candidate).
1143 if( !m_pins[pin]->IsIsomorphic( *b->m_pins[pin], aReason ) )
1144 {
1145 if( aReason.m_reason.IsEmpty() )
1146 {
1148 aReason.m_candidate = b->GetParent()->GetReferenceAsString();
1149 aReason.m_reason = wxString::Format( _( "Component pads differ between %s and %s." ),
1150 aReason.m_reference, aReason.m_candidate );
1151 }
1152
1153 return false;
1154 }
1155
1156 }
1157
1158 return true;
1159}
1160
1161
1163{
1164 auto cmp = new COMPONENT( aFp->GetReference(), aFp );
1165
1166 for( auto pad : aFp->Pads() )
1167 {
1168 auto pin = new PIN( );
1169 pin->m_netcode = pad->GetNetCode();
1170 pin->m_ref = pad->GetNumber();
1171 cmp->AddPin( pin );
1172 }
1173
1174 m_components.push_back( cmp );
1175}
1176
1177
1178std::unique_ptr<CONNECTION_GRAPH>
1179CONNECTION_GRAPH::BuildFromFootprintSet( const std::set<FOOTPRINT*>& aFps,
1180 const std::set<FOOTPRINT*>& aOtherChannelFps )
1181{
1182 auto cgraph = std::make_unique<CONNECTION_GRAPH>();
1183 VECTOR2I ref(0, 0);
1184
1185 if( aFps.size() > 0 )
1186 ref = (*aFps.begin())->GetPosition();
1187
1188 for( auto fp : aFps )
1189 cgraph->AddFootprint( fp, fp->GetPosition() - ref );
1190
1191 std::unordered_map<int, int> localNetPadCounts;
1192
1193 for( const FOOTPRINT* fp : aFps )
1194 {
1195 for( const PAD* pad : fp->Pads() )
1196 {
1197 if( pad->GetNetCode() > 0 )
1198 localNetPadCounts[pad->GetNetCode()]++;
1199 }
1200 }
1201
1202 std::unordered_map<int, int> otherChannelNetPadCounts;
1203
1204 for( const FOOTPRINT* fp : aOtherChannelFps )
1205 {
1206 for( const PAD* pad : fp->Pads() )
1207 {
1208 if( pad->GetNetCode() > 0 )
1209 otherChannelNetPadCounts[pad->GetNetCode()]++;
1210 }
1211 }
1212
1213 // Exclude a net from topology comparison only when it forms a real internal connection (two
1214 // or more pads) in BOTH sets. This drops global rails (GND, VCC) while keeping single-pad
1215 // boundary nets, such as an applied design block's daisy-chain output feeding the target
1216 // instance's inputs, whose asymmetric exclusion would otherwise break the isomorphism.
1217 std::unordered_set<int> externalNets;
1218
1219 for( const auto& [netCode, localCount] : localNetPadCounts )
1220 {
1221 auto otherIt = otherChannelNetPadCounts.find( netCode );
1222
1223 if( localCount >= 2 && otherIt != otherChannelNetPadCounts.end() && otherIt->second >= 2 )
1224 externalNets.insert( netCode );
1225 }
1226
1227 cgraph->BuildConnectivity( externalNets );
1228
1229 return cgraph;
1230}
1231
1232
1237
1238
1240{
1241 for( COMPONENT* fp : m_components )
1242 {
1243 delete fp;
1244 }
1245}
1246
1247
1249{
1250 for( PIN* p : m_pins )
1251 {
1252 delete p;
1253 }
1254}
1255
1256
1257}; // namespace TMATCH
virtual const BOARD * GetBoard() const
Return the BOARD in which this BOARD_ITEM resides, or NULL if none.
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:372
COMPONENT(const LIB_ID &aFPID, const wxString &aReference, const wxString &aValue, const KIID_PATH &aPath, const std::vector< KIID > &aKiids)
std::deque< PAD * > & Pads()
Definition footprint.h:375
wxString GetFPIDAsString() const
Definition footprint.h:447
const LIB_ID & GetFPID() const
Definition footprint.h:441
wxString GetReferenceAsString() const
Definition footprint.h:850
const wxString & GetReference() const
Definition footprint.h:841
Definition kiid.h:44
wxString AsString() const
Definition kiid.cpp:242
bool empty() const
Definition lib_id.h:189
Handle the data for a net.
Definition netinfo.h:46
Definition pad.h:61
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
std::string to_string()
Definition profile.h:153
double msecs(bool aSinceLast=false)
Definition profile.h:147
const std::unordered_map< COMPONENT *, COMPONENT * > & GetMatchingComponentPairs() const
Definition topo_match.h:166
std::unordered_map< COMPONENT *, COMPONENT * > m_locked
Definition topo_match.h:176
static bool prefixesShareCommonBase(const wxString &aPrefixA, const wxString &aPrefixB)
Check if two prefixes share a common starting sequence.
bool IsSameKind(const COMPONENT &b) const
int GetPinCount() const
Definition topo_match.h:68
COMPONENT(const wxString &aRef, FOOTPRINT *aParentFp, std::optional< VECTOR2I > aRaOffset=std::optional< VECTOR2I >())
FOOTPRINT * m_parentFootprint
Definition topo_match.h:99
std::optional< VECTOR2I > m_raOffset
Definition topo_match.h:96
bool MatchesWith(COMPONENT *b, TOPOLOGY_MISMATCH_REASON &aDetail)
wxString m_prefix
Definition topo_match.h:98
void AddPin(PIN *p)
std::vector< PIN * > & Pins()
Definition topo_match.h:70
friend class PIN
Definition topo_match.h:59
wxString m_reference
Definition topo_match.h:97
static bool isChannelSuffix(const wxString &aSuffix)
Check if a suffix looks like a channel identifier.
std::vector< PIN * > m_pins
Definition topo_match.h:100
FOOTPRINT * GetParent() const
Definition topo_match.h:71
std::vector< COMPONENT * > findMatchingComponents(COMPONENT *ref, const std::vector< COMPONENT * > &aStructuralMatches, const TOPOLOGY_MISMATCH_REASON &aStructuralReason, const BACKTRACK_STAGE &partialMatches, std::vector< TOPOLOGY_MISMATCH_REASON > &aFailureDetails, const std::atomic< bool > *aCancelled=nullptr)
bool FindIsomorphism(CONNECTION_GRAPH *target, COMPONENT_MATCHES &result, std::vector< TOPOLOGY_MISMATCH_REASON > &aFailureDetails, const ISOMORPHISM_PARAMS &aParams={})
void BuildConnectivity(const std::unordered_set< int > &aExternalNets={})
std::vector< COMPONENT * > m_components
Definition topo_match.h:222
void AddFootprint(FOOTPRINT *aFp, const VECTOR2I &aOffset)
bool breakTieBySymbolUuid(COMPONENT *aRef, std::vector< COMPONENT * > &aMatches) const
The most useful tie breaker is based on symbol/sheet instances, since multiple channels in a design a...
void breakTie(COMPONENT *aRef, std::vector< COMPONENT * > &aMatches) const
Many times components are electrically/topologically identical, e.g.
std::unordered_set< int > m_externalNets
Definition topo_match.h:223
static std::unique_ptr< CONNECTION_GRAPH > BuildFromFootprintSet(const std::set< FOOTPRINT * > &aFps, const std::set< FOOTPRINT * > &aOtherChannelFps={})
std::vector< PIN * > m_conns
Definition topo_match.h:140
void SetParent(COMPONENT *parent)
Definition topo_match.h:111
COMPONENT * m_parent
Definition topo_match.h:139
bool IsIsomorphic(const PIN &b, TOPOLOGY_MISMATCH_REASON &aDetail) const
wxString m_ref
Definition topo_match.h:137
static bool empty(const wxTextEntryBase *aCtrl)
#define _(s)
KIID niluuid(0)
std::unordered_map< int, int > buildBaseNetMapping(const BACKTRACK_STAGE &aMatches)
std::map< FOOTPRINT *, FOOTPRINT * > COMPONENT_MATCHES
Definition topo_match.h:180
bool checkCandidateNetConsistency(const std::unordered_map< int, int > &aBaseMapping, COMPONENT *aRef, COMPONENT *aTgt, TOPOLOGY_MISMATCH_REASON &aReason, const std::unordered_set< int > &aExternalNets)
wxString GetRefDesPrefix(const wxString &aRefDes)
Get the (non-numeric) prefix from a refdes - e.g.
CITER next(CITER it)
Definition ptree.cpp:120
Collection of utility functions for component reference designators (refdes)
std::atomic< bool > * m_cancelled
Definition topo_match.h:45
std::atomic< int > * m_totalComponents
Definition topo_match.h:47
std::string path
KIBIS top(path, &reporter)
KIBIS_PIN * pin
thread_pool & GetKiCadThreadPool()
Get a reference to the current thread pool.
static thread_pool * tp
BS::priority_thread_pool thread_pool
Definition thread_pool.h:27
static const wxString traceTopoMatchDetail
static const wxString traceTopoMatch
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683