KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_netlist_exporter_spice.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
21#include <boost/test/results_collector.hpp> // To check if the current test failed (to be moved?).
24#include <mock_pgm_base.h>
25#include <locale_io.h>
26
27#include <schematic.h>
28#include <sch_symbol.h>
29#include <sch_screen.h>
30#include <sch_sheet_path.h>
31
32#include <wx/tokenzr.h>
33#include <wx/filename.h>
34#include <wx/utils.h>
35
36
37// Test-only access to the exporter's private multi-unit helpers.
39{
40public:
41 static std::vector<UNIT_PIN_MAP> CollectUnitPinMaps( NETLIST_EXPORTER_SPICE& aExporter,
42 SCH_SYMBOL& aSymbol,
43 const SCH_SHEET_PATH& aSheet,
44 const wxString& aVariant )
45 {
46 return aExporter.collectUnitPinMaps( aSymbol, aSheet, aVariant );
47 }
48
50 SCH_SYMBOL& aSymbol, const SCH_SHEET_PATH& aSheet,
51 const wxString& aVariant )
52 {
53 return aExporter.getDecomposition( aSymbol, aSheet, aVariant );
54 }
55};
56
57
58// Normalizes volatile parts of a SPICE netlist so a checked-in golden file stays
59// portable across machines and checkouts. Only `.include` lines carry absolute
60// filesystem paths; everything else (nets, model names, NC counters, pin order)
61// is already deterministic.
62static wxString normalizeSpiceNetlist( const wxString& aNetlist )
63{
64 wxString out;
65 wxStringTokenizer lines( aNetlist, wxS( "\n" ), wxTOKEN_RET_EMPTY_ALL );
66
67 while( lines.HasMoreTokens() )
68 {
69 wxString line = lines.GetNextToken();
70
71 if( line.StartsWith( wxS( ".include " ) ) )
72 {
73 wxFileName fn( line.AfterFirst( '"' ).BeforeLast( '"' ) );
74 line = wxS( ".include \"" ) + fn.GetFullName() + wxS( "\"" );
75 }
76
77 out << line << wxS( "\n" );
78 }
79
80 return out;
81}
82
83
84// Splits a netlist line into whitespace-separated tokens.
85static std::vector<wxString> splitNetlistLine( const wxString& aLine )
86{
87 std::vector<wxString> tokens;
88 wxStringTokenizer tokenizer( aLine, wxS( " \t" ), wxTOKEN_STRTOK );
89
90 while( tokenizer.HasMoreTokens() )
91 tokens.push_back( tokenizer.GetNextToken() );
92
93 return tokens;
94}
95
96
97// Returns the first unit (in hierarchy traversal order) carrying the given reference, i.e. the
98// primary unit the exporter processes. Mirrors the test's need to drive the private helpers.
99static SCH_SYMBOL* findPrimaryUnit( SCHEMATIC* aSchematic, const wxString& aRef,
100 SCH_SHEET_PATH& aSheetOut )
101{
102 for( const SCH_SHEET_PATH& sheet : aSchematic->Hierarchy() )
103 {
104 for( SCH_ITEM* item : sheet.LastScreen()->Items().OfType( SCH_SYMBOL_T ) )
105 {
106 SCH_SYMBOL* symbol = static_cast<SCH_SYMBOL*>( item );
107
108 if( symbol->GetRef( &sheet ) == aRef )
109 {
110 aSheetOut = sheet;
111 return symbol;
112 }
113 }
114 }
115
116 return nullptr;
117}
118
119
120BOOST_FIXTURE_TEST_SUITE( NetlistExporterSpice, TEST_NETLIST_EXPORTER_SPICE_FIXTURE )
121
122
124{
126
127 // const MOCK_PGM_BASE& program = static_cast<MOCK_PGM_BASE&>( Pgm() );
128 // MOCK_EXPECT( program.GetLocalEnvVariables ).returns( ENV_VAR_MAP() );
129
130 TestNetlist( "rectifier" );
131 TestTranPoint( 0, { { "V(/in)", 0 }, { "V(/out)", 0 } } );
132 TestTranPoint( 9250e-6, { { "V(/in)", 5 }, { "V(/out)", 4.26 } } );
133 TestTranPoint( 10e-3, { { "V(/in)", 0 }, { "V(/out)", 4.24 } } );
134}
135
136// FIXME: Fails due to some nondeterminism, seems related to convergence problems.
137
138/*BOOST_AUTO_TEST_CASE( Chirp )
139{
140 LOCALE_IO dummy;
141 TestNetlist( "chirp", { "V(/out)", "I(R1)" } );
142}*/
143
144
146{
148 // Instead of Simulation_SPICE:OPAMP, we use Amplifier_Operational:MCP6001-OT because its pins
149 // are not ordered by pin numbers, which is a possible failure condition.
150
151 // const MOCK_PGM_BASE& program = static_cast<MOCK_PGM_BASE&>( Pgm() );
152 // MOCK_EXPECT( program.GetLocalEnvVariables ).returns( ENV_VAR_MAP() );
153
154 TestNetlist( "opamp" );
155 TestTranPoint( 0, { { "V(/in)", 0 }, { "V(/out)", 0 } } );
156 TestTranPoint( 250e-6, { { "V(/in)", 500e-3 }, { "V(/out)", 1 } } );
157 TestTranPoint( 500e-6, { { "V(/in)", 0 }, { "V(/out)", 0 } } );
158 TestTranPoint( 750e-6, { { "V(/in)", -500e-3 }, { "V(/out)", -1 } } );
159 TestTranPoint( 1e-3, { { "V(/in)", 0 }, { "V(/out)", 0 } } );
160}
161
162
164{
166 // This test intentionally uses non-inferred voltage sources to test them.
167
168 TestNetlist( "npn_ce_amp" );
169 TestTranPoint( 900e-6, { { "V(/in)", 0 }, { "V(/out)", 5.32 } } );
170 TestTranPoint( 925e-6, { { "V(/in)", 10e-3 }, { "V(/out)", 5.30 } } );
171 TestTranPoint( 950e-6, { { "V(/in)", 0 }, { "V(/out)", 5.88 } } );
172 TestTranPoint( 975e-6, { { "V(/in)", -10e-3 }, { "V(/out)", 5.91 } } );
173 TestTranPoint( 1e-3, { { "V(/in)", 0 }, { "V(/out)", 5.32 } } );
174}
175
176
178{
180 TestNetlist( "rlc" );
181 TestTranPoint( 9.43e-3, { { "V(/Vp)", -19e-3 }, { "I(Rs1)", 19e-3 } } );
182 TestTranPoint( 9.74e-3, { { "V(/Vp)", 19e-3 }, { "I(Rs1)", -19e-3 } } );
183}
184
185
186BOOST_AUTO_TEST_CASE( Potentiometers )
187{
188 TestNetlist( "potentiometers" );
189 TestOpPoint( 0.5, "V(/out1)" );
190 TestOpPoint( 0.7, "V(/out2)" );
191 TestOpPoint( 0.9, "V(/out3)" );
192}
193
194
196{
198 TestNetlist( "tlines" );
199 TestTranPoint( 910e-6, { { "V(/z0_in)", 1 }, { "V(/z0_out)", 0 },
200 { "V(/rlgc_in)", 1 }, { "V(/rlgc_out)", 0 } } );
201 TestTranPoint( 970e-6, { { "V(/z0_in)", 0 }, { "V(/z0_out)", 1 },
202 { "V(/rlgc_in)", 0 }, { "V(/rlgc_out)", 1 } } );
203}
204
205
206/*BOOST_AUTO_TEST_CASE( Sources )
207{
208 LOCALE_IO dummy;
209 TestNetlist( "sources", { "V(/vdc)", "V(/idc)",
210 "V(/vsin)", "V(/isin)",
211 "V(/vpulse)", "V(/ipulse)",
212 "V(/vexp)", "V(/iexp)",
213 "V(/vpwl)", "V(/ipwl)",
214 "V(/vbehavioral)", "V(/ibehavioral)" } );
215
216 // TODO: Make some tests for random and noise sources, e.g. check their RMS or spectra.
217 //"V(/vwhitenoise)", "V(/iwhitenoise)",
218 //"V(/vpinknoise)", "V(/ipinknoise)",
219 //"V(/vburstnoise)", "V(/iburstnoise)",
220 //"V(/vranduniform)", "V(/iranduniform)",
221 //"V(/vrandnormal)", "V(/iranduniform)",
222 //"V(/vrandexp)", "V(/irandexp)",
223}*/
224
225
227{
229 TestNetlist( "cmos_not" );
230 TestTranPoint( 0, { { "V(/in)", 2.5 }, { "V(/out)", 2.64 } } );
231 TestTranPoint( 250e-6, { { "V(/in)", 5 }, { "V(/out)", 0.013 } } );
232 TestTranPoint( 500e-6, { { "V(/in)", 2.5 }, { "V(/out)", 2.64 } } );
233 TestTranPoint( 750e-6, { { "V(/in)", 0 }, { "V(/out)", 5 } } );
234 TestTranPoint( 1e-3, { { "V(/in)", 2.5 }, { "V(/out)", 2.64 } } );
235}
236
237
238/*BOOST_AUTO_TEST_CASE( InstanceParams )
239{
240 // TODO.
241 //TestNetlist( "instance_params", {} );
242}*/
243
244
245BOOST_AUTO_TEST_CASE( FliegeFilter )
246{
248 // We test a multi-unit part here, as Fliege topology uses two op amps (power supply pins are a
249 // third part).
250
251 TestNetlist( "fliege_filter" );
252 TestACPoint( 0.8e3, { { "V(/in)", 1 }, { "V(/out)", 1 } } );
253 TestACPoint( 1.061e3, { { "V(/in)", 1 }, { "V(/out)", 0 } } );
254 TestACPoint( 1.2e3, { { "V(/in)", 1 }, { "V(/out)", 1 } } );
255}
256
257
259{
261 TestNetlist( "switches" );
262 TestTranPoint( 0.5e-3, { { "V(/inswv)", 0 }, { "V(/outswv)", 0 } } );
263 TestTranPoint( 1.5e-3, { { "V(/inswv)", 1 }, { "V(/outswv)", 5 } } );
264 TestTranPoint( 2.5e-3, { { "V(/inswv)", 0 }, { "V(/outswv)", 0 } } );
265
266 // TODO: Current switch, when it's fixed in Ngspice.
267}
268
269
270// This test is sometimes failing on certain platforms for unknown reasons
271// Disabling it for now so that it doesn't prevent packages from building
272#if 0
273BOOST_AUTO_TEST_CASE( Directives )
274{
276 TestNetlist( "directives" );
277 TestTranPoint( 9.25e-3, { { "V(/in)", 1 }, { "V(/out)", -900e-3 }, { "I(XR1)", 1e-3 } } );
278 TestTranPoint( 9.50e-3, { { "V(/in)", 0 }, { "V(/out)", 0 }, { "I(XR1)", 1e-3 } } );
279 TestTranPoint( 9.75e-3, { { "V(/in)", -1 }, { "V(/out)", 900e-3 }, { "I(XR1)", 1e-3 } } );
280 TestTranPoint( 10e-3, { { "V(/in)", 0 }, { "V(/out)", 0 }, { "I(XR1)", 1e-3 } } );
281}
282#endif
283
284
285// This test is flaky and fails on ngspice-42 / Linux.
286// Please replace it with something that is more stable
287#if 0
288BOOST_AUTO_TEST_CASE( LegacyLaserDriver )
289{
291
292 TestNetlist( "legacy_laser_driver" );
293
294 if( m_abort )
295 return;
296
297 // Test D1 current before the pulse
298 TestTranPoint( 95e-9, { { "I(D1)", 0 } } );
299 // Test D1 current during the pulse
300 TestTranPoint( 110e-9, { { "I(D1)", 0.770 } }, 0.1 );
301 // Test D1 current after the pulse
302 TestTranPoint( 150e-9, { { "I(D1)", 0 } } );
303}
304#endif
305
306BOOST_AUTO_TEST_CASE( LegacyPotentiometer )
307{
308 TestNetlist( "legacy_pot" );
309 TestOpPoint( 0.5, "V(/out1)" );
310 TestOpPoint( 0.7, "V(/out2)" );
311 TestOpPoint( 0.9, "V(/out3)" );
312}
313
314BOOST_AUTO_TEST_CASE( LegacyPspice )
315{
317 TestNetlist( "legacy_pspice" );
318 TestACPoint( 190, { { "V(/VIN)", pow( 10, -186e-3 / 20 ) },
319 { "V(VOUT)", pow( 10, 87e-3 / 20 ) } } );
320}
321
322
323BOOST_AUTO_TEST_CASE( LegacyRectifier )
324{
326 TestNetlist( "legacy_rectifier" );
327 TestTranPoint( 0, { { "V(/signal_in)", 0 },
328 { "V(/rect_out)", 0 } } );
329 TestTranPoint( 9.75e-3, { { "V(/signal_in)", 1.5 },
330 { "V(/rect_out)", 823e-3 } } );
331}
332
333
334BOOST_AUTO_TEST_CASE( LegacySallenKey )
335{
337 TestNetlist( "legacy_sallen_key" );
338
339 if( m_abort )
340 return;
341
342 TestACPoint( 1, { { "V(/lowpass)", pow( 10, 0.0 / 20 ) } } );
343 TestACPoint( 1e3, { { "V(/lowpass)", pow( 10, -2.9 / 20 ) } } );
344}
345
346
347/*BOOST_AUTO_TEST_CASE( LegacySources )
348{
349 LOCALE_IO dummy;
350 TestNetlist( "legacy_sources", { "V(/vam)", "V(/iam)",
351 "V(/vdc)", "V(/idc)",
352 "V(/vexp)", "V(/iexp)",
353 "V(/vpulse)", "V(/ipulse)",
354 "V(/vpwl)", "V(/ipwl)",
355 "V(/vsffm)", "V(/isffm)",
356 "V(/vsin)", "V(/isin)" } );
357 //"V(/vtrnoise)", "V(/itrnoise)",
358 //"V(/vtrrandom)", "V(/itrrandom)" } );
359}*/
360
361
363{
365 // Amplifier_Operational:AD797 model is used to test symbols that have more pins than the model.
366
367 TestNetlist( "legacy_opamp" );
368 TestTranPoint( 0, { { "V(/in)", 0 }, { "V(/out)", 0 } } );
369 TestTranPoint( 250e-6, { { "V(/in)", 500e-3 }, { "V(/out)", 1 } } );
370 TestTranPoint( 500e-6, { { "V(/in)", 0 }, { "V(/out)", 0 } } );
371 TestTranPoint( 750e-6, { { "V(/in)", -500e-3 }, { "V(/out)", -1 } } );
372 TestTranPoint( 1e-3, { { "V(/in)", 0 }, { "V(/out)", 0 } } );
373}
374
375
376// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/1779
377// Multi-unit symbols should emit a single subcircuit instance whose pin map
378// is the union of the Sim.Pins fields collected from every unit, even when
379// each individual unit only carries a partial Sim.Pins mapping.
380BOOST_AUTO_TEST_CASE( MultiUnitSplitPinMap )
381{
383
384 LoadSchematic( SchematicQAPath( "multiunit_pinmap_split" ) );
385 WriteNetlist();
386
387 wxString netlistPath = GetNetlistPath( true );
388 wxFFile netlistFile( netlistPath, "rt" );
389 BOOST_REQUIRE( netlistFile.IsOpened() );
390
391 wxString netlist;
392 netlistFile.ReadAll( &netlist );
393 netlistFile.Close();
394
395 BOOST_TEST_INFO( "Netlist:\n" << netlist );
396
397 wxString xu1Line;
398 wxStringTokenizer lines( netlist, wxS( "\n" ) );
399
400 while( lines.HasMoreTokens() )
401 {
402 wxString line = lines.GetNextToken();
403
404 if( line.StartsWith( wxS( "XU1 " ) ) )
405 {
406 BOOST_REQUIRE_MESSAGE( xu1Line.IsEmpty(),
407 "Multiple XU1 lines found; multi-unit symbols must emit a "
408 "single subcircuit instance, not one per unit" );
409 xu1Line = line;
410 }
411 }
412
413 BOOST_REQUIRE_MESSAGE( !xu1Line.IsEmpty(),
414 "XU1 subcircuit instance was not generated" );
415
416 wxStringTokenizer tokens( xu1Line, wxS( " \t" ), wxTOKEN_STRTOK );
417 std::vector<wxString> parts;
418
419 while( tokens.HasMoreTokens() )
420 parts.push_back( tokens.GetNextToken() );
421
422 // The uopamp_lvl2_2x subckt declares its nodes in the order
423 // VCC VEE +IN1 -IN1 OUT1 +IN2 -IN2 OUT2. The merged Sim.Pins maps every model
424 // node to a specific symbol pin number, so each position in the instance line is
425 // determined by the schematic's connection on that symbol pin. Validating the
426 // full token list catches regressions in any unit's merge behavior independently.
427 const std::vector<wxString> expectedParts = {
428 wxS( "XU1" ),
429 wxS( "Net-_U1C-V+_" ), // VCC <- symbol pin 8 (unit 3 V+)
430 wxS( "Net-_U1C-V-_" ), // VEE <- symbol pin 4 (unit 3 V-)
431 wxS( "Net-_U1A-+_" ), // +IN1 <- symbol pin 3 (unit 1 +IN)
432 wxS( "Net-_U1A--_" ), // -IN1 <- symbol pin 2 (unit 1 -IN)
433 wxS( "/out" ), // OUT1 <- symbol pin 1 (unit 1 OUT, labeled /out)
434 wxS( "Net-_U1B-+_" ), // +IN2 <- symbol pin 5 (unit 2 +IN)
435 wxS( "Net-_U1A--_" ), // -IN2 <- symbol pin 6 (unit 2 -IN, tied to unit 1 -IN)
436 wxS( "Net-_C2-Pad1_" ), // OUT2 <- symbol pin 7 (unit 2 OUT)
437 wxS( "uopamp_lvl2_2x" )
438 };
439
440 BOOST_REQUIRE_EQUAL( parts.size(), expectedParts.size() );
441
442 for( size_t ii = 0; ii < expectedParts.size(); ++ii )
443 {
444 BOOST_CHECK_MESSAGE( parts[ii] == expectedParts[ii],
445 "Expected XU1 token " << ii << " to be '" << expectedParts[ii]
446 << "', got '" << parts[ii]
447 << "'. Per-unit Sim.Pins merge is missing or incorrect" );
448 }
449
450 Cleanup();
451}
452
453
454// Repeat-per-unit decomposition (issue #1779): a multi-unit symbol whose model is a single-unit
455// vendor model is netlisted as one outer X<refdes> referencing a generated wrapper .subckt that
456// instantiates the base model once per functional unit, with the supply pins shared.
457BOOST_AUTO_TEST_CASE( MultiUnitRepeatOpamp )
458{
460
461 LoadSchematic( SchematicQAPath( wxS( "multiunit_repeat_opamp" ) ) );
462 WriteNetlist();
463
464 wxString netlistPath = GetNetlistPath( true );
465 wxFFile netlistFile( netlistPath, "rt" );
466 BOOST_REQUIRE( netlistFile.IsOpened() );
467
468 wxString netlist;
469 netlistFile.ReadAll( &netlist );
470 netlistFile.Close();
471
472 BOOST_TEST_INFO( "Netlist:\n" << netlist );
473
474 wxString xu1Line;
475 wxString subcktLine;
476 std::vector<wxString> innerInstanceLines;
477 bool hasBaseInclude = false;
478
479 wxStringTokenizer lines( netlist, wxS( "\n" ) );
480
481 while( lines.HasMoreTokens() )
482 {
483 wxString line = lines.GetNextToken();
484
485 if( line.StartsWith( wxS( "XU1 " ) ) )
486 {
487 BOOST_REQUIRE_MESSAGE( xu1Line.IsEmpty(), "multiple XU1 lines; expected exactly one" );
488 xu1Line = line;
489 }
490 else if( line.StartsWith( wxS( ".subckt kicad_mu_" ) ) )
491 {
492 BOOST_REQUIRE_MESSAGE( subcktLine.IsEmpty(),
493 "wrapper .subckt emitted more than once" );
494 subcktLine = line;
495 }
496 else if( line.StartsWith( wxS( "X" ) ) && line.EndsWith( wxS( "uopamp_single" ) ) )
497 {
498 innerInstanceLines.push_back( line );
499 }
500 else if( line.Contains( wxS( "uopamp_single.lib.spice" ) )
501 && line.StartsWith( wxS( ".include" ) ) )
502 {
503 hasBaseInclude = true;
504 }
505 }
506
507 BOOST_REQUIRE_MESSAGE( !xu1Line.IsEmpty(), "outer XU1 line was not generated" );
508 BOOST_REQUIRE_MESSAGE( !subcktLine.IsEmpty(), "wrapper .subckt was not generated" );
509 BOOST_CHECK_MESSAGE( hasBaseInclude, "base model library was not included" );
510
511 std::vector<wxString> subcktTokens = splitNetlistLine( subcktLine );
512 BOOST_REQUIRE_GE( subcktTokens.size(), 2u );
513 wxString wrapperName = subcktTokens[1];
514
515 // The outer instance references exactly the generated wrapper.
516 std::vector<wxString> xu1Tokens = splitNetlistLine( xu1Line );
517 BOOST_REQUIRE_GE( xu1Tokens.size(), 2u );
518 BOOST_CHECK_EQUAL( xu1Tokens.back(), wrapperName );
519
520 // Two functional units -> two inner instances, both of the single-unit base model.
521 BOOST_REQUIRE_EQUAL( innerInstanceLines.size(), 2u );
522
523 std::vector<wxString> x1 = splitNetlistLine( innerInstanceLines[0] );
524 std::vector<wxString> x2 = splitNetlistLine( innerInstanceLines[1] );
525
526 // Inner instance node order is +IN -IN VCC VEE OUT; the shared VCC (index 3) and VEE (index 4)
527 // nodes must be identical across both instances.
528 BOOST_REQUIRE_EQUAL( x1.size(), 7u );
529 BOOST_REQUIRE_EQUAL( x2.size(), 7u );
530 BOOST_CHECK_EQUAL( x1[3], x2[3] );
531 BOOST_CHECK_EQUAL( x1[4], x2[4] );
532
533 // The outer nets follow the synthetic pin order (instance-1 signal pins, instance-2 signal
534 // pins, then shared), each connected to the schematic net on that symbol pin.
535 const std::vector<wxString> expectedOuterNets = {
536 wxS( "Net-_U1A-+_" ), // +IN1 (symbol pin 3)
537 wxS( "Net-_U1A--_" ), // -IN1 (symbol pin 2)
538 wxS( "/out" ), // OUT1 (symbol pin 1)
539 wxS( "Net-_U1B-+_" ), // +IN2 (symbol pin 5)
540 wxS( "Net-_U1A--_" ), // -IN2 (symbol pin 6, tied to -IN1)
541 wxS( "Net-_C2-Pad1_" ), // OUT2 (symbol pin 7)
542 wxS( "Net-_U1C-V+_" ), // VCC (symbol pin 8, shared)
543 wxS( "Net-_U1C-V-_" ) // VEE (symbol pin 4, shared)
544 };
545
546 std::vector<wxString> outerNets( xu1Tokens.begin() + 1, xu1Tokens.end() - 1 );
547 BOOST_CHECK( outerNets == expectedOuterNets );
548
549 Cleanup();
550}
551
552
553// The synthesized wrapper subcircuit must be valid, simulatable SPICE: ngspice has to parse and
554// run the generated netlist (a malformed .subckt would fail to parse). This is the automated
555// equivalent of opening the project and running a simulation.
556BOOST_AUTO_TEST_CASE( MultiUnitRepeatOpampSimulates )
557{
559
560 LoadSchematic( SchematicQAPath( wxS( "multiunit_repeat_opamp" ) ) );
561 WriteNetlist();
562
563 // CompareNetlists() loads the generated netlist into ngspice, runs it, and asserts there is no
564 // parse error ("circuit not parsed") or missing-code-model error.
565 CompareNetlists();
566
567 Cleanup();
568}
569
570
571// The per-unit Sim.Pins of a multi-unit symbol are gathered into one map per unit, ordered by
572// unit number, with each unit's written token order preserved and repeats across units kept.
573BOOST_AUTO_TEST_CASE( CollectUnitPinMaps )
574{
576
577 LoadSchematic( SchematicQAPath( wxS( "multiunit_repeat_opamp" ) ) );
578
579 SCH_SHEET_PATH primarySheet;
580 SCH_SYMBOL* primary = findPrimaryUnit( m_schematic.get(), wxS( "U1" ), primarySheet );
581 BOOST_REQUIRE( primary );
582
583 NETLIST_EXPORTER_SPICE exporter( m_schematic.get() );
584 std::vector<UNIT_PIN_MAP> maps = NETLIST_EXPORTER_SPICE_PROBE::CollectUnitPinMaps(
585 exporter, *primary, primarySheet, wxEmptyString );
586
587 BOOST_REQUIRE_EQUAL( maps.size(), 3 );
588
589 BOOST_CHECK_EQUAL( maps[0].unit, 1 );
590 BOOST_CHECK_EQUAL( maps[1].unit, 2 );
591 BOOST_CHECK_EQUAL( maps[2].unit, 3 );
592
593 const std::vector<std::pair<wxString, wxString>> unit1 = {
594 { wxS( "1" ), wxS( "OUT" ) }, { wxS( "2" ), wxS( "-IN" ) }, { wxS( "3" ), wxS( "+IN" ) } };
595 const std::vector<std::pair<wxString, wxString>> unit2 = {
596 { wxS( "5" ), wxS( "+IN" ) }, { wxS( "6" ), wxS( "-IN" ) }, { wxS( "7" ), wxS( "OUT" ) } };
597 const std::vector<std::pair<wxString, wxString>> unit3 = {
598 { wxS( "4" ), wxS( "VEE" ) }, { wxS( "8" ), wxS( "VCC" ) } };
599
600 BOOST_CHECK( maps[0].pins == unit1 );
601 BOOST_CHECK( maps[1].pins == unit2 );
602 BOOST_CHECK( maps[2].pins == unit3 );
603}
604
605
606// With no Sim.Decomposition field, a symbol resolves to the safe whole-device default.
607// The component-level Sim.Decomposition is read for the symbol regardless of which unit carries
608// it (the fixture places it on a single unit only).
609BOOST_AUTO_TEST_CASE( DecompositionReadsRepeat )
610{
612
613 LoadSchematic( SchematicQAPath( wxS( "multiunit_repeat_opamp" ) ) );
614
615 SCH_SHEET_PATH primarySheet;
616 SCH_SYMBOL* primary = findPrimaryUnit( m_schematic.get(), wxS( "U1" ), primarySheet );
617 BOOST_REQUIRE( primary );
618
619 NETLIST_EXPORTER_SPICE exporter( m_schematic.get() );
621 exporter, *primary, primarySheet, wxEmptyString );
622
624 BOOST_REQUIRE_EQUAL( dec.sharedModelPins.size(), 2 );
625 BOOST_CHECK_EQUAL( dec.sharedModelPins[0], wxS( "VCC" ) );
626 BOOST_CHECK_EQUAL( dec.sharedModelPins[1], wxS( "VEE" ) );
627}
628
629
630// Byte-exact safety rail for the default (whole-device) netlist path. The
631// multi-unit decomposition work (issue #1779) must never perturb the output of
632// schematics that use today's behavior; a diff here is a backward-compatibility
633// regression and a hard stop. Regenerate the .golden files (after an
634// intentional, reviewed format change) by setting KICAD_RECORD_GOLDEN=1.
635BOOST_AUTO_TEST_CASE( WholeDeviceGoldenNetlist )
636{
638
639 const std::vector<wxString> fixtures = { wxS( "multiunit_pinmap_split" ),
640 wxS( "fliege_filter" ) };
641
642 for( const wxString& fixture : fixtures )
643 {
644 BOOST_TEST_CONTEXT( "Fixture: " << fixture )
645 {
646 LoadSchematic( SchematicQAPath( fixture ) );
647 WriteNetlist();
648
649 wxString netlistPath = GetNetlistPath( true );
650 wxFFile netlistFile( netlistPath, "rt" );
651 BOOST_REQUIRE( netlistFile.IsOpened() );
652
653 wxString netlist;
654 netlistFile.ReadAll( &netlist );
655 netlistFile.Close();
656
657 wxString normalized = normalizeSpiceNetlist( netlist );
658
659 wxFileName goldenFn = SchematicQAPath( fixture );
660 goldenFn.SetExt( "golden" );
661 wxString goldenPath = goldenFn.GetFullPath();
662
663 if( wxGetEnv( wxS( "KICAD_RECORD_GOLDEN" ), nullptr ) )
664 {
665 wxFFile out( goldenPath, "wt" );
666 BOOST_REQUIRE( out.IsOpened() );
667 out.Write( normalized );
668 out.Close();
669 BOOST_TEST_MESSAGE( "Recorded golden netlist: " << goldenPath );
670 }
671 else
672 {
673 wxFFile goldenFile( goldenPath, "rt" );
674 BOOST_REQUIRE_MESSAGE( goldenFile.IsOpened(),
675 "Missing golden netlist; regenerate with "
676 "KICAD_RECORD_GOLDEN=1" );
677
678 wxString golden;
679 goldenFile.ReadAll( &golden );
680 goldenFile.Close();
681
682 BOOST_TEST_INFO( "Normalized netlist:\n" << normalized );
683 BOOST_CHECK_EQUAL( normalized, golden );
684 }
685
686 Cleanup();
687 }
688 }
689}
690
691
692// The simulator builds one SPICE_CIRCUIT_MODEL and exports from it repeatedly, so the model-name
693// generator must start each netlist clean. An externally defined subcircuit name (opamp) must be
694// emitted verbatim every time, and a KiCad-defined .model name (the Gummel-Poon NPN) must not drift
695// run to run. Either failure means a reused exporter leaks model-name state across exports.
696BOOST_AUTO_TEST_CASE( RepeatedExportIsDeterministic )
697{
699
700 const std::vector<wxString> fixtures = { wxS( "opamp" ), wxS( "npn_ce_amp" ) };
701
702 for( const wxString& fixture : fixtures )
703 {
704 BOOST_TEST_CONTEXT( "Fixture: " << fixture )
705 {
706 LoadSchematic( SchematicQAPath( fixture ) );
707
708 NETLIST_EXPORTER_SPICE exporter( m_schematic.get() );
709
710 auto exportOnce =
711 [&]() -> wxString
712 {
713 wxString path = GetNetlistPath( true );
715
716 BOOST_REQUIRE( exporter.WriteNetlist( path, GetNetlistOptions(), reporter ) );
717
718 wxFFile file( path, "rt" );
719 BOOST_REQUIRE( file.IsOpened() );
720
721 wxString netlist;
722 file.ReadAll( &netlist );
723 file.Close();
724
725 return netlist;
726 };
727
728 wxString first = exportOnce();
729 wxString second = exportOnce();
730
731 BOOST_TEST_INFO( "First export:\n" << first );
732 BOOST_TEST_INFO( "Second export:\n" << second );
733 BOOST_CHECK_EQUAL( first, second );
734
735 Cleanup();
736 }
737 }
738}
739
740
Instantiate the current locale within a scope in which you are expecting exceptions to be thrown.
Definition locale_io.h:37
static SIM_DECOMPOSITION GetDecomposition(NETLIST_EXPORTER_SPICE &aExporter, SCH_SYMBOL &aSymbol, const SCH_SHEET_PATH &aSheet, const wxString &aVariant)
static std::vector< UNIT_PIN_MAP > CollectUnitPinMaps(NETLIST_EXPORTER_SPICE &aExporter, SCH_SYMBOL &aSymbol, const SCH_SHEET_PATH &aSheet, const wxString &aVariant)
SIM_DECOMPOSITION getDecomposition(SCH_SYMBOL &aSymbol, const SCH_SHEET_PATH &aSheet, const wxString &aVariantName) const
Read and parse the Sim.Decomposition field from the primary unit.
bool WriteNetlist(const wxString &aOutFileName, unsigned aNetlistOptions, REPORTER &aReporter) override
Write to specified output file.
std::vector< UNIT_PIN_MAP > collectUnitPinMaps(SCH_SYMBOL &aSymbol, const SCH_SHEET_PATH &aSheet, const wxString &aVariantName)
Gather every unit's Sim.Pins mapping for a multi-unit symbol, one entry per unit.
Holds all the data relating to one schematic.
Definition schematic.h:90
SCH_SHEET_LIST Hierarchy() const
Return the full schematic flattened hierarchical sheet list.
Base class for any item which can be embedded within the SCHEMATIC container class,...
Definition sch_item.h:162
Handle access to a stack of flattened SCH_SHEET objects by way of a path for creating a flattened sch...
Schematic symbol object.
Definition sch_symbol.h:69
const wxString GetRef(const SCH_SHEET_PATH *aSheet, bool aIncludeUnit=false) const override
A wrapper for reporting to a wxString object.
Definition reporter.h:189
static void LoadSchematic(SCHEMATIC *aSchematic, SCH_SHEET *aRootSheet, const wxString &aFileName)
Definition of the SCH_SHEET_PATH and SCH_SHEET_LIST classes for Eeschema.
std::vector< FAB_LAYER_COLOR > dummy
Per-component decomposition descriptor stored in the Sim.Decomposition field.
std::vector< wxString > sharedModelPins
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
std::string netlist
std::string path
IbisParser parser & reporter
BOOST_TEST_INFO("Two-port Series .op current = "<< iDevice)
BOOST_AUTO_TEST_CASE(Rectifier)
static SCH_SYMBOL * findPrimaryUnit(SCHEMATIC *aSchematic, const wxString &aRef, SCH_SHEET_PATH &aSheetOut)
static std::vector< wxString > splitNetlistLine(const wxString &aLine)
static wxString normalizeSpiceNetlist(const wxString &aNetlist)
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))
BOOST_TEST_CONTEXT("Test Clearance")
BOOST_CHECK_EQUAL(result, "25.4")
@ SCH_SYMBOL_T
Definition typeinfo.h:169