KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_netlist_exporter_spice.h
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?).
22#include <eeschema_test_utils.h>
24#include <sim/ngspice.h>
26#include <wx/ffile.h>
27#include <mock_pgm_base.h>
28#include <locale_io.h>
29
30// A relative max error accepted when comparing 2 values
31#define MAX_DEFAULT_REL_ERROR 2e-2
32
33
35{
36public:
38 {
39 public:
40 SPICE_TEST_REPORTER( std::shared_ptr<wxString> aLog ) :
41 m_log( std::move( aLog ) )
42 {}
43
44 REPORTER& Report( const wxString& aText,
45 SEVERITY aSeverity = RPT_SEVERITY_UNDEFINED ) override
46 {
47 *m_log << aText << "\n";
48
49 // You can add a debug trace here.
50 return *this;
51 }
52
53 bool HasMessage() const override { return false; }
54
55 void OnSimStateChange( SIMULATOR* aObject, SIM_STATE aNewState ) override { }
56
57 private:
58 std::shared_ptr<wxString> m_log;
59 };
60
63 m_simulator( SPICE_SIMULATOR::CreateInstance( "ngspice" ) ),
64 m_log( std::make_shared<wxString>() ),
65 m_reporter( std::make_unique<SPICE_TEST_REPORTER>( m_log ) ),
66 m_abort( false )
67 {
68 }
69
71 {
72 using namespace boost::unit_test;
73
74 // The NGSPICE instance is a singleton that outlives this fixture, so a background
75 // simulation left running would call back into m_reporter after it is destroyed and
76 // bleed state into the next test case. Halt it and wait for the background thread to
77 // settle, then detach the reporter (SetReporter blocks until any in-flight callback
78 // returns). Guard the whole sequence because a destructor must never throw.
79 if( m_simulator )
80 {
81 try
82 {
83 if( m_simulator->IsRunning() )
84 {
85 m_simulator->Stop();
86
87 // Bounded wait so a wedged bg_halt fails the test instead of hanging QA.
88 for( int i = 0; i < 200 && m_simulator->IsRunning(); ++i )
89 wxMilliSleep( 10 );
90
91 BOOST_CHECK_MESSAGE( !m_simulator->IsRunning(),
92 "Timed out waiting for ngspice to stop during teardown" );
93 }
94
95 m_simulator->SetReporter( nullptr );
96 }
97 catch( ... )
98 {
99 }
100 }
101
102 test_case::id_t id = framework::current_test_case().p_id;
103 test_results results = results_collector.results( id );
104
105 // Output a log if the test has failed.
106 // Don't use BOOST_CHECK_MESSAGE because it triggers a checkpoint which affects debugging
107 if( !results.passed() )
108 {
109 BOOST_TEST_MESSAGE( "\nNGSPICE LOG\n===========\n" << *m_log );
110 }
111 }
112
113 wxFileName SchematicQAPath( const wxString& aBaseName ) override
114 {
115 wxFileName fn( KI_TEST::GetEeschemaTestDataDir() );
116 fn.AppendDir( "spice_netlists" );
117 fn.AppendDir( aBaseName );
118 fn.SetName( aBaseName );
120
121 return fn;
122 }
123
124 wxString GetNetlistPath( bool aTest = false ) override
125 {
126 wxFileName netFile = m_schematic->Project().GetProjectFullName();
127
128 if( aTest )
129 netFile.SetName( netFile.GetName() + "_test" );
130
131 netFile.SetExt( "spice" );
132 return netFile.GetFullPath();
133 }
134
135 void CompareNetlists() override
136 {
137 wxString netlistPath = GetNetlistPath( true );
138 BOOST_TEST_CHECKPOINT( "Comparing netlist " << netlistPath );
139
140 m_abort = false;
141
142 NGSPICE* ngspice = dynamic_cast<NGSPICE*>( m_simulator.get() );
143 BOOST_REQUIRE( ngspice );
144
145 ngspice->SetReporter( m_reporter.get() );
146
147 // Free vectors from any previous simulation to reduce memory pressure.
148 // The NGSPICE instance is a singleton shared across all test cases.
149 ngspice->Clean();
150
151 wxFFile file( netlistPath, "rt" );
152 wxString netlist;
153
154 BOOST_REQUIRE( file.IsOpened() );
155 file.ReadAll( &netlist );
156
157 ngspice->Command( "set ngbehavior=ps" );
158 ngspice->Command( "setseed 1" );
159 BOOST_REQUIRE( ngspice->LoadNetlist( std::string( netlist.ToUTF8() ) ) );
160
161 if( ngspice->Run() )
162 {
163 // wait for end of simulation.
164 // calling wxYield() allows printing activity, and stopping ngspice from GUI
165 // Also note: do not user wxSafeYield, because when using it we cannot stop
166 // ngspice from the GUI
167 do
168 {
169 wxMilliSleep( 50 );
170 wxYield();
171 } while( ngspice->IsRunning() );
172 }
173
174 // Detach the reporter while we read m_log on the main thread. m_ngSpice_Running
175 // can return false while a cbSendChar callback is still in flight, and that
176 // callback writes to *m_log. SetReporter(nullptr) blocks until the in-flight
177 // call returns.
178 ngspice->SetReporter( nullptr );
179
180 // Test if ngspice cannot run a simulation (missing code models).
181 // in this case the log contains "MIF-ERROR" and/or "Error: circuit not parsed"
182 // when the simulation is not run the spice command "linearize" crashes.
183 bool mif_error = m_log->Find( wxT( "MIF-ERROR" ) ) != wxNOT_FOUND;
184
185 BOOST_TEST_INFO( "Cannot run ngspice. test skipped. Missing code model files?" );
186 BOOST_CHECK( !mif_error );
187
188 bool err_found = m_log->Find( wxT( "Error: circuit not parsed" ) ) != wxNOT_FOUND;
189
190 // Re-attach so the rest of the foreground ngspice->Command calls below feed
191 // their output back into the log. These run on the main thread, so the
192 // cbSendChar callback fires synchronously and there's no concurrent reader.
193 ngspice->SetReporter( m_reporter.get() );
194
195 BOOST_TEST_INFO( "Cannot run ngspice. test skipped. Install error?" );
196 BOOST_CHECK( !err_found );
197
198 if( mif_error || err_found )
199 {
200 m_abort = true;
201
202 // Still display the original netlist in this case.
203 *m_log << "Original Netlist\n";
204 *m_log << "----------------\n";
205 *m_log << netlist << "\n";
206
207 return;
208 }
209
210 // We need to make sure that the number of points always the same.
211 ngspice->Command( "linearize" );
212
213 // Debug info.
214
215 // Display all vectors.
216 *m_log << "\n";
217 ngspice->Command( "echo Available Vectors" );
218 ngspice->Command( "echo -----------------" );
219 ngspice->Command( "display" );
220
221 // Display the original netlist.
222 *m_log << "\n";
223 *m_log << "Original Netlist\n";
224 *m_log << "----------------\n";
225 *m_log << netlist << "\n";
226
227 // Display the expanded netlist.
228 ngspice->Command( "echo Expanded Netlist" );
229 ngspice->Command( "echo ----------------" );
230 ngspice->Command( "listing runnable" );
231 }
232
233 void TestOpPoint( double aRefValue, const std::string& aVectorName,
234 double aMaxRelError = MAX_DEFAULT_REL_ERROR )
235 {
236 BOOST_TEST_CONTEXT( "Vector name: " << aVectorName )
237 {
238 NGSPICE* ngspice = static_cast<NGSPICE*>( m_simulator.get() );
239
240 std::vector<double> vector = ngspice->GetRealVector( aVectorName );
241
242 BOOST_REQUIRE_EQUAL( vector.size(), 1 );
243
244 double maxError = abs( aRefValue * aMaxRelError );
245 BOOST_CHECK_LE( abs( vector[0] - aRefValue ), aMaxRelError );
246 }
247 }
248
249 void TestPoint( const std::string& aXVectorName, double aXValue,
250 const std::map<const std::string, double> aTestVectorsAndValues,
251 double aMaxRelError = MAX_DEFAULT_REL_ERROR )
252 {
253 // The default aMaxRelError is fairly large because we have some problems with determinism
254 // in QA pipeline. We don't need to fix this for now because, if this has to be fixed in
255 // the first place, this has to be done from Ngspice's side.
256
257 BOOST_TEST_CONTEXT( "X vector name: " << aXVectorName << ", X value: " << aXValue )
258 {
259 NGSPICE* ngspice = static_cast<NGSPICE*>( m_simulator.get() );
260
261 std::vector<double> xVector = ngspice->GetRealVector( aXVectorName );
262 std::size_t i = 0;
263
264 for(; i < xVector.size(); ++i )
265 {
266 double inf = std::numeric_limits<double>::infinity();
267
268 double leftDelta = ( aXValue - ( i >= 1 ? xVector[i - 1] : -inf ) );
269 double middleDelta = ( aXValue - xVector[i] );
270 double rightDelta = ( aXValue - ( i < xVector.size() - 1 ? xVector[i + 1] : inf ) );
271
272 // Check if this point is the closest one.
273 if( abs( middleDelta ) <= abs( leftDelta )
274 && abs( middleDelta ) <= abs( rightDelta ) )
275 {
276 break;
277 }
278 }
279
280 BOOST_REQUIRE_LT( i, xVector.size() );
281
282 for( auto& [vectorName, refValue] : aTestVectorsAndValues )
283 {
284 std::vector<double> yVector = ngspice->GetGainVector( vectorName );
285
286 BOOST_REQUIRE_GE( yVector.size(), i + 1 );
287
288 BOOST_TEST_CONTEXT( "Y vector name: " << vectorName
289 << ", Ref value: " << refValue
290 << ", Actual value: " << yVector[i] )
291 {
292 double maxError = abs( refValue * aMaxRelError );
293
294 if( maxError == 0 )
295 {
296 // If refValue is 0, we need a obtain the max. error differently.
297 maxError = aMaxRelError;
298 }
299
300 BOOST_CHECK_LE( abs( yVector[i] - refValue ), maxError );
301 }
302 }
303 }
304 }
305
306 void TestTranPoint( double aTime,
307 const std::map<const std::string, double> aTestVectorsAndValues,
308 double aMaxRelError = MAX_DEFAULT_REL_ERROR )
309 {
310 TestPoint( "time", aTime, aTestVectorsAndValues, aMaxRelError );
311 }
312
313 void TestACPoint( double aFrequency,
314 const std::map<const std::string, double> aTestVectorsAndValues,
315 double aMaxRelError = MAX_DEFAULT_REL_ERROR )
316 {
317 TestPoint( "frequency", aFrequency, aTestVectorsAndValues, aMaxRelError );
318 }
319
320 wxString GetResultsPath( bool aTest = false )
321 {
322 wxFileName netlistPath( GetNetlistPath( aTest ) );
323 netlistPath.SetExt( "csv" );
324
325 return netlistPath.GetFullPath();
326 }
327
337
338 std::shared_ptr<SPICE_SIMULATOR> m_simulator;
339 std::shared_ptr<wxString> m_log;
340 std::unique_ptr<SPICE_TEST_REPORTER> m_reporter;
341 bool m_abort; // set to true to force abort durint a test
342};
bool Command(const std::string &aCmd) override final
Definition ngspice.cpp:412
bool IsRunning() override final
Execute a Spice command as if it was typed into console.
Definition ngspice.cpp:369
bool Run() override final
Halt the simulation.
Definition ngspice.cpp:346
bool LoadNetlist(const std::string &aNetlist) override final
Execute the simulation with currently loaded netlist.
Definition ngspice.cpp:312
std::vector< double > GetGainVector(const std::string &aName, int aMaxLen=-1) override final
Return a requested vector with phase values.
Definition ngspice.cpp:222
std::vector< double > GetRealVector(const std::string &aName, int aMaxLen=-1) override final
Return a requested vector with imaginary values.
Definition ngspice.cpp:167
void Clean() override final
Cleans simulation data (i.e.
Definition ngspice.cpp:815
REPORTER()
Definition reporter.h:73
Interface to receive simulation updates from SPICE_SIMULATOR class.
virtual void SetReporter(SIMULATOR_REPORTER *aReporter)
Set a SIMULATOR_REPORTER object to receive the simulation log.
bool HasMessage() const override
Returns true if any messages were reported.
void OnSimStateChange(SIMULATOR *aObject, SIM_STATE aNewState) override
REPORTER & Report(const wxString &aText, SEVERITY aSeverity=RPT_SEVERITY_UNDEFINED) override
Report a string with a given severity.
std::shared_ptr< SPICE_SIMULATOR > m_simulator
void TestOpPoint(double aRefValue, const std::string &aVectorName, double aMaxRelError=MAX_DEFAULT_REL_ERROR)
void TestPoint(const std::string &aXVectorName, double aXValue, const std::map< const std::string, double > aTestVectorsAndValues, double aMaxRelError=MAX_DEFAULT_REL_ERROR)
wxFileName SchematicQAPath(const wxString &aBaseName) override
void TestTranPoint(double aTime, const std::map< const std::string, double > aTestVectorsAndValues, double aMaxRelError=MAX_DEFAULT_REL_ERROR)
void TestACPoint(double aFrequency, const std::map< const std::string, double > aTestVectorsAndValues, double aMaxRelError=MAX_DEFAULT_REL_ERROR)
wxString GetNetlistPath(bool aTest=false) override
std::unique_ptr< SPICE_TEST_REPORTER > m_reporter
static const std::string KiCadSchematicFileExtension
std::string GetEeschemaTestDataDir()
Get the configured location of Eeschema test data.
STL namespace.
SEVERITY
@ RPT_SEVERITY_UNDEFINED
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
std::string netlist
BOOST_TEST_INFO("Two-port Series .op current = "<< iDevice)
#define MAX_DEFAULT_REL_ERROR
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")