/* * This program source code file is part of KiCad, a free EDA CAD application. * * Copyright (C) 2016-2023 CERN * Copyright (C) 2016-2024 KiCad Developers, see AUTHORS.txt for contributors. * @author Tomasz Wlostowski * @author Maciej Suminski * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 3 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, you may find one here: * https://www.gnu.org/licenses/gpl-3.0.html * or you may search the http://www.gnu.org website for the version 3 license, * or you may write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA */ #include // For some obscure reason, needed on msys2 with some wxWidgets versions (3.0) to avoid // undefined symbol at link stage (due to use of #include ) // Should not create issues on other platforms #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "ngspice.h" #include #include #include #include #include #include #include #include // Reporter is stored by pointer in KIBIS, so keep this here to avoid crashes static wxString s_errors; static WX_STRING_REPORTER s_reporter( &s_errors ); class SIM_THREAD_REPORTER : public SIMULATOR_REPORTER { public: SIM_THREAD_REPORTER( SIMULATOR_FRAME* aParent ) : m_parent( aParent ) { } REPORTER& Report( const wxString& aText, SEVERITY aSeverity = RPT_SEVERITY_UNDEFINED ) override { wxCommandEvent* event = new wxCommandEvent( EVT_SIM_REPORT ); event->SetString( aText ); wxQueueEvent( m_parent, event ); return *this; } bool HasMessage() const override { return false; // Technically "indeterminate" rather than false. } void OnSimStateChange( SIMULATOR* aObject, SIM_STATE aNewState ) override { wxCommandEvent* event = nullptr; switch( aNewState ) { case SIM_IDLE: event = new wxCommandEvent( EVT_SIM_FINISHED ); break; case SIM_RUNNING: event = new wxCommandEvent( EVT_SIM_STARTED ); break; default: wxFAIL; return; } wxQueueEvent( m_parent, event ); } private: SIMULATOR_FRAME* m_parent; }; BEGIN_EVENT_TABLE( SIMULATOR_FRAME, KIWAY_PLAYER ) EVT_MENU( wxID_EXIT, SIMULATOR_FRAME::onExit ) EVT_MENU( wxID_CLOSE, SIMULATOR_FRAME::onExit ) END_EVENT_TABLE() SIMULATOR_FRAME::SIMULATOR_FRAME( KIWAY* aKiway, wxWindow* aParent ) : KIWAY_PLAYER( aKiway, aParent, FRAME_SIMULATOR, _( "Simulator" ), wxDefaultPosition, wxDefaultSize, wxDEFAULT_FRAME_STYLE, wxT( "simulator" ), unityScale ), m_schematicFrame( nullptr ), m_toolBar( nullptr ), m_ui( nullptr ), m_simFinished( false ), m_workbookModified( false ) { m_schematicFrame = (SCH_EDIT_FRAME*) Kiway().Player( FRAME_SCH, false ); wxASSERT( m_schematicFrame ); // Give an icon wxIcon icon; icon.CopyFromBitmap( KiBitmap( BITMAPS::simulator ) ); SetIcon( icon ); wxBoxSizer* mainSizer = new wxBoxSizer( wxVERTICAL ); SetSizer( mainSizer ); m_infoBar = new WX_INFOBAR( this ); mainSizer->Add( m_infoBar, 0, wxEXPAND, 0 ); m_toolBar = new ACTION_TOOLBAR( this, wxID_ANY, wxDefaultPosition, wxDefaultSize, wxAUI_TB_DEFAULT_STYLE|wxAUI_TB_HORZ_LAYOUT|wxAUI_TB_PLAIN_BACKGROUND ); m_toolBar->Realize(); mainSizer->Add( m_toolBar, 0, wxEXPAND, 5 ); m_ui = new SIMULATOR_FRAME_UI( this, m_schematicFrame ); mainSizer->Add( m_ui, 1, wxEXPAND, 5 ); m_simulator = SIMULATOR::CreateInstance( "ngspice" ); wxASSERT( m_simulator ); LoadSettings( config() ); NGSPICE_SETTINGS* settings = dynamic_cast( m_simulator->Settings().get() ); wxCHECK2( settings, /* do nothing in release builds*/ ); if( settings && settings->GetWorkbookFilename().IsEmpty() ) settings->SetCompatibilityMode( NGSPICE_COMPATIBILITY_MODE::LT_PSPICE ); m_simulator->Init(); m_reporter = new SIM_THREAD_REPORTER( this ); m_simulator->SetReporter( m_reporter ); m_circuitModel = std::make_shared( &m_schematicFrame->Schematic(), this ); setupTools(); setupUIConditions(); ReCreateHToolbar(); ReCreateMenuBar(); Bind( wxEVT_COMMAND_MENU_SELECTED, wxCommandEventHandler( SIMULATOR_FRAME::onExit ), this, wxID_EXIT ); Bind( EVT_SIM_UPDATE, &SIMULATOR_FRAME::onUpdateSim, this ); Bind( EVT_SIM_REPORT, &SIMULATOR_FRAME::onSimReport, this ); Bind( EVT_SIM_STARTED, &SIMULATOR_FRAME::onSimStarted, this ); Bind( EVT_SIM_FINISHED, &SIMULATOR_FRAME::onSimFinished, this ); // Ensure new items are taken in account by sizers: Layout(); // resize the subwindows size. At least on Windows, calling wxSafeYield before // resizing the subwindows forces the wxSplitWindows size events automatically generated // by wxWidgets to be executed before our resize code. // Otherwise, the changes made by setSubWindowsSashSize are overwritten by one these // events wxSafeYield(); m_ui->SetSubWindowsSashSize(); // Ensure the window is on top Raise(); m_ui->InitWorkbook(); UpdateTitle(); } SIMULATOR_FRAME::~SIMULATOR_FRAME() { NULL_REPORTER devnull; m_simulator->Attach( nullptr, wxEmptyString, 0, wxEmptyString, devnull ); m_simulator->SetReporter( nullptr ); delete m_reporter; } void SIMULATOR_FRAME::setupTools() { // Create the manager m_toolManager = new TOOL_MANAGER; m_toolManager->SetEnvironment( nullptr, nullptr, nullptr, config(), this ); m_toolDispatcher = new TOOL_DISPATCHER( m_toolManager ); // Attach the events to the tool dispatcher Bind( wxEVT_CHAR, &TOOL_DISPATCHER::DispatchWxEvent, m_toolDispatcher ); Bind( wxEVT_CHAR_HOOK, &TOOL_DISPATCHER::DispatchWxEvent, m_toolDispatcher ); // Register tools m_toolManager->RegisterTool( new COMMON_CONTROL ); m_toolManager->RegisterTool( new SIMULATOR_CONTROL ); m_toolManager->InitTools(); } void SIMULATOR_FRAME::ShowChangedLanguage() { EDA_BASE_FRAME::ShowChangedLanguage(); UpdateTitle(); m_ui->ShowChangedLanguage(); } void SIMULATOR_FRAME::LoadSettings( APP_SETTINGS_BASE* aCfg ) { EESCHEMA_SETTINGS* cfg = dynamic_cast( aCfg ); wxASSERT( cfg ); if( cfg ) { EDA_BASE_FRAME::LoadSettings( cfg ); m_ui->LoadSettings( cfg ); } PROJECT_FILE& project = Prj().GetProjectFile(); NGSPICE* currentSim = dynamic_cast( m_simulator.get() ); if( currentSim ) m_simulator->Settings() = project.m_SchematicSettings->m_NgspiceSettings; } void SIMULATOR_FRAME::SaveSettings( APP_SETTINGS_BASE* aCfg ) { EESCHEMA_SETTINGS* cfg = dynamic_cast( aCfg ); wxASSERT( cfg ); if( cfg ) { EDA_BASE_FRAME::SaveSettings( cfg ); m_ui->SaveSettings( cfg ); } PROJECT_FILE& project = Prj().GetProjectFile(); if( project.m_SchematicSettings ) { bool modified = project.m_SchematicSettings->m_NgspiceSettings->SaveToFile(); if( m_schematicFrame && modified ) m_schematicFrame->OnModify(); } } WINDOW_SETTINGS* SIMULATOR_FRAME::GetWindowSettings( APP_SETTINGS_BASE* aCfg ) { EESCHEMA_SETTINGS* cfg = dynamic_cast( aCfg ); wxASSERT( cfg ); return cfg ? &cfg->m_Simulator.window : nullptr; } wxString SIMULATOR_FRAME::GetCurrentSimCommand() const { if( m_ui->GetCurrentSimTab() ) return m_ui->GetCurrentSimTab()->GetSimCommand(); else return m_circuitModel->GetSchTextSimCommand(); } SIM_TYPE SIMULATOR_FRAME::GetCurrentSimType() const { return SPICE_CIRCUIT_MODEL::CommandToSimType( GetCurrentSimCommand() ); } int SIMULATOR_FRAME::GetCurrentOptions() const { if( SIM_TAB* simTab = m_ui->GetCurrentSimTab() ) return simTab->GetSimOptions(); else return NETLIST_EXPORTER_SPICE::OPTION_DEFAULT_FLAGS; } void SIMULATOR_FRAME::UpdateTitle() { bool unsaved = true; bool readOnly = false; wxString title; wxFileName filename = Prj().AbsolutePath( m_simulator->Settings()->GetWorkbookFilename() ); if( filename.IsOk() && filename.FileExists() ) { unsaved = false; readOnly = !filename.IsFileWritable(); } if( m_workbookModified ) title = wxT( "*" ) + filename.GetName(); else title = filename.GetName(); if( readOnly ) title += wxS( " " ) + _( "[Read Only]" ); if( unsaved ) title += wxS( " " ) + _( "[Unsaved]" ); title += wxT( " \u2014 " ) + _( "Spice Simulator" ); SetTitle( title ); } // Don't let the dialog grow too tall: you may not be able to get to the OK button #define MAX_MESSAGES 20 void SIMULATOR_FRAME::showNetlistErrors( const wxString& aErrors ) { wxArrayString lines = wxSplit( aErrors, '\n' ); if( lines.size() > MAX_MESSAGES ) { lines.RemoveAt( MAX_MESSAGES, lines.size() - MAX_MESSAGES ); lines.Add( wxS( "..." ) ); } DisplayErrorMessage( this, _( "Errors during netlist generation." ), wxJoin( lines, '\n' ) ); } bool SIMULATOR_FRAME::LoadSimulator( const wxString& aSimCommand, unsigned aSimOptions ) { s_errors.clear(); if( !m_schematicFrame->ReadyToNetlist( _( "Simulator requires a fully annotated schematic." ) ) ) return false; // If we are using the new connectivity, make sure that we do a full-rebuild if( ADVANCED_CFG::GetCfg().m_IncrementalConnectivity ) m_schematicFrame->RecalculateConnections( nullptr, GLOBAL_CLEANUP ); if( !m_simulator->Attach( m_circuitModel, aSimCommand, aSimOptions, Prj().GetProjectPath(), s_reporter ) ) { showNetlistErrors( s_errors ); return false; } return true; } void SIMULATOR_FRAME::ReloadSimulator( const wxString& aSimCommand, unsigned aSimOptions ) { s_errors.clear(); if( !m_simulator->Attach( m_circuitModel, aSimCommand, aSimOptions, Prj().GetProjectPath(), s_reporter ) ) { showNetlistErrors( s_errors ); } } void SIMULATOR_FRAME::StartSimulation() { SIM_TAB* simTab = m_ui->GetCurrentSimTab(); if( !simTab ) return; if( simTab->GetSimCommand().Upper().StartsWith( wxT( "FFT" ) ) || simTab->GetSimCommand().Upper().Contains( wxT( "\nFFT" ) ) ) { wxString tranSpicePlot; if( SIM_TAB* tranPlotTab = m_ui->GetSimTab( ST_TRAN ) ) tranSpicePlot = tranPlotTab->GetSpicePlotName(); if( tranSpicePlot.IsEmpty() ) { DisplayErrorMessage( this, _( "You must run a TRAN simulation first; its results" "will be used for the fast Fourier transform." ) ); } else { m_simulator->Command( "setplot " + tranSpicePlot.ToStdString() ); wxArrayString commands = wxSplit( simTab->GetSimCommand(), '\n' ); for( const wxString& command : commands ) { wxBusyCursor wait; m_simulator->Command( command.ToStdString() ); } simTab->SetSpicePlotName( m_simulator->CurrentPlotName() ); m_ui->OnSimRefresh( true ); #if 0 m_simulator->Command( "setplot" ); // Print available plots to console m_simulator->Command( "display" ); // Print vectors in current plot to console #endif } return; } else { if( m_ui->GetSimTabIndex( simTab ) == 0 && m_circuitModel->GetSchTextSimCommand() != simTab->GetLastSchTextSimCommand() ) { if( simTab->GetLastSchTextSimCommand().IsEmpty() || IsOK( this, _( "Schematic sheet simulation command directive has changed. " "Do you wish to update the Simulation Command?" ) ) ) { simTab->SetSimCommand( m_circuitModel->GetSchTextSimCommand() ); simTab->SetLastSchTextSimCommand( simTab->GetSimCommand() ); OnModify(); } } } if( !LoadSimulator( simTab->GetSimCommand(), simTab->GetSimOptions() ) ) return; std::unique_lock simulatorLock( m_simulator->GetMutex(), std::try_to_lock ); if( simulatorLock.owns_lock() ) { m_ui->OnSimUpdate(); m_simulator->Run(); // Netlist from schematic may have changed; update signals list, measurements list, // etc. m_ui->OnPlotSettingsChanged(); } else { DisplayErrorMessage( this, _( "Another simulation is already running." ) ); } } SIM_TAB* SIMULATOR_FRAME::NewSimTab( const wxString& aSimCommand ) { return m_ui->NewSimTab( aSimCommand ); } const std::vector SIMULATOR_FRAME::SimPlotVectors() { return m_ui->SimPlotVectors(); } const std::vector SIMULATOR_FRAME::Signals() { return m_ui->Signals(); } const std::map& SIMULATOR_FRAME::UserDefinedSignals() { return m_ui->UserDefinedSignals(); } void SIMULATOR_FRAME::SetUserDefinedSignals( const std::map& aSignals ) { m_ui->SetUserDefinedSignals( aSignals ); } void SIMULATOR_FRAME::AddVoltageTrace( const wxString& aNetName ) { m_ui->AddTrace( aNetName, SPT_VOLTAGE ); } void SIMULATOR_FRAME::AddCurrentTrace( const wxString& aDeviceName ) { m_ui->AddTrace( aDeviceName, SPT_CURRENT ); } void SIMULATOR_FRAME::AddTuner( const SCH_SHEET_PATH& aSheetPath, SCH_SYMBOL* aSymbol ) { m_ui->AddTuner( aSheetPath, aSymbol ); } SIM_TAB* SIMULATOR_FRAME::GetCurrentSimTab() const { return m_ui->GetCurrentSimTab(); } bool SIMULATOR_FRAME::LoadWorkbook( const wxString& aPath ) { if( m_ui->LoadWorkbook( aPath ) ) { UpdateTitle(); // Successfully loading a workbook does not count as modifying it. Clear the modified // flag after all the EVT_WORKBOOK_MODIFIED events have been processed. CallAfter( [=]() { m_workbookModified = false; } ); return true; } return false; } bool SIMULATOR_FRAME::SaveWorkbook( const wxString& aPath ) { if( m_ui->SaveWorkbook( aPath ) ) { m_workbookModified = false; UpdateTitle(); return true; } return false; } void SIMULATOR_FRAME::ToggleDarkModePlots() { m_ui->ToggleDarkModePlots(); } bool SIMULATOR_FRAME::EditAnalysis() { SIM_TAB* simTab = m_ui->GetCurrentSimTab(); DIALOG_SIM_COMMAND dlg( this, m_circuitModel, m_simulator->Settings() ); s_errors.clear(); if( !simTab ) return false; if( !m_circuitModel->ReadSchematicAndLibraries( NETLIST_EXPORTER_SPICE::OPTION_DEFAULT_FLAGS, s_reporter ) ) { showNetlistErrors( s_errors ); } dlg.SetSimCommand( simTab->GetSimCommand() ); dlg.SetSimOptions( simTab->GetSimOptions() ); dlg.SetPlotSettings( simTab ); if( dlg.ShowModal() == wxID_OK ) { simTab->SetSimCommand( dlg.GetSimCommand() ); dlg.ApplySettings( simTab ); m_ui->OnPlotSettingsChanged(); OnModify(); return true; } return false; } bool SIMULATOR_FRAME::canCloseWindow( wxCloseEvent& aEvent ) { if( m_workbookModified ) { wxFileName filename = m_simulator->Settings()->GetWorkbookFilename(); if( filename.GetName().IsEmpty() ) { if( Prj().GetProjectName().IsEmpty() ) filename.SetFullName( wxT( "noname.wbk" ) ); else filename.SetFullName( Prj().GetProjectName() + wxT( ".wbk" ) ); } return HandleUnsavedChanges( this, _( "Save changes to workbook?" ), [&]() -> bool { return SaveWorkbook( Prj().AbsolutePath( filename.GetFullName() ) ); } ); } return true; } void SIMULATOR_FRAME::doCloseWindow() { if( m_simulator->IsRunning() ) m_simulator->Stop(); // Prevent memory leak on exit by deleting all simulation vectors m_simulator->Clean(); // Cancel a running simProbe or simTune tool m_schematicFrame->GetToolManager()->PostAction( ACTIONS::cancelInteractive ); SaveSettings( config() ); m_simulator->Settings() = nullptr; Destroy(); } void SIMULATOR_FRAME::setupUIConditions() { EDA_BASE_FRAME::setupUIConditions(); ACTION_MANAGER* mgr = m_toolManager->GetActionManager(); wxASSERT( mgr ); auto showGridCondition = [this]( const SELECTION& aSel ) { SIM_PLOT_TAB* plotTab = dynamic_cast( GetCurrentSimTab() ); return plotTab && plotTab->IsGridShown(); }; auto showLegendCondition = [this]( const SELECTION& aSel ) { SIM_PLOT_TAB* plotTab = dynamic_cast( GetCurrentSimTab() ); return plotTab && plotTab->IsLegendShown(); }; auto showDottedCondition = [this]( const SELECTION& aSel ) { SIM_PLOT_TAB* plotTab = dynamic_cast( GetCurrentSimTab() ); return plotTab && plotTab->GetDottedSecondary(); }; auto darkModePlotCondition = [this]( const SELECTION& aSel ) { return m_ui->DarkModePlots(); }; auto simRunning = [this]( const SELECTION& aSel ) { return m_simulator && m_simulator->IsRunning(); }; auto simFinished = [this]( const SELECTION& aSel ) { return m_simFinished; }; auto haveSim = [this]( const SELECTION& aSel ) { return GetCurrentSimTab() != nullptr; }; auto havePlot = [this]( const SELECTION& aSel ) { return dynamic_cast( GetCurrentSimTab() ) != nullptr; }; auto haveZoomUndo = [this]( const SELECTION& aSel ) { SIM_PLOT_TAB* plotTab = dynamic_cast( GetCurrentSimTab() ); return plotTab && plotTab->GetPlotWin()->UndoZoomStackSize() > 0; }; auto haveZoomRedo = [this]( const SELECTION& aSel ) { SIM_PLOT_TAB* plotTab = dynamic_cast( GetCurrentSimTab() ); return plotTab && plotTab->GetPlotWin()->RedoZoomStackSize() > 0; }; #define ENABLE( x ) ACTION_CONDITIONS().Enable( x ) #define CHECK( x ) ACTION_CONDITIONS().Check( x ) mgr->SetConditions( EE_ACTIONS::openWorkbook, ENABLE( SELECTION_CONDITIONS::ShowAlways ) ); mgr->SetConditions( EE_ACTIONS::saveWorkbook, ENABLE( SELECTION_CONDITIONS::ShowAlways ) ); mgr->SetConditions( EE_ACTIONS::saveWorkbookAs, ENABLE( SELECTION_CONDITIONS::ShowAlways ) ); mgr->SetConditions( EE_ACTIONS::exportPlotAsPNG, ENABLE( havePlot ) ); mgr->SetConditions( EE_ACTIONS::exportPlotAsCSV, ENABLE( havePlot ) ); mgr->SetConditions( ACTIONS::zoomUndo, ENABLE( haveZoomUndo ) ); mgr->SetConditions( ACTIONS::zoomRedo, ENABLE( haveZoomRedo ) ); mgr->SetConditions( EE_ACTIONS::toggleGrid, CHECK( showGridCondition ) ); mgr->SetConditions( EE_ACTIONS::toggleLegend, CHECK( showLegendCondition ) ); mgr->SetConditions( EE_ACTIONS::toggleDottedSecondary, CHECK( showDottedCondition ) ); mgr->SetConditions( EE_ACTIONS::toggleDarkModePlots, CHECK( darkModePlotCondition ) ); mgr->SetConditions( EE_ACTIONS::newAnalysisTab, ENABLE( SELECTION_CONDITIONS::ShowAlways ) ); mgr->SetConditions( EE_ACTIONS::simAnalysisProperties, ENABLE( haveSim ) ); mgr->SetConditions( EE_ACTIONS::runSimulation, ENABLE( !simRunning ) ); mgr->SetConditions( EE_ACTIONS::stopSimulation, ENABLE( simRunning ) ); mgr->SetConditions( EE_ACTIONS::simProbe, ENABLE( simFinished ) ); mgr->SetConditions( EE_ACTIONS::simTune, ENABLE( simFinished ) ); mgr->SetConditions( EE_ACTIONS::showNetlist, ENABLE( SELECTION_CONDITIONS::ShowAlways ) ); #undef CHECK #undef ENABLE } void SIMULATOR_FRAME::onSimStarted( wxCommandEvent& aEvent ) { SetCursor( wxCURSOR_ARROWWAIT ); } void SIMULATOR_FRAME::onSimFinished( wxCommandEvent& aEvent ) { // Sometimes (for instance with a directive like wrdata my_file.csv "my_signal") // the simulator is in idle state (simulation is finished), but still running, during // the time the file is written. So gives a slice of time to fully finish the work: if( m_simulator->IsRunning() ) { int max_time = 40; // For a max timeout = 2s do { wxMilliSleep( 50 ); wxYield(); if( max_time ) max_time--; } while( max_time && m_simulator->IsRunning() ); } // ensure the shown cursor is the default cursor, not the wxCURSOR_ARROWWAIT set when // staring the simulator in onSimStarted: SetCursor( wxNullCursor ); // Is a warning message useful if the simulatior is still running? SCHEMATIC& schematic = m_schematicFrame->Schematic(); schematic.ClearOperatingPoints(); m_simFinished = true; m_ui->OnSimRefresh( true ); m_schematicFrame->RefreshOperatingPointDisplay(); m_schematicFrame->GetCanvas()->Refresh(); } void SIMULATOR_FRAME::onUpdateSim( wxCommandEvent& aEvent ) { static bool updateInProgress = false; // skip update when events are triggered too often and previous call didn't end yet if( updateInProgress ) return; updateInProgress = true; if( m_simulator->IsRunning() ) m_simulator->Stop(); std::unique_lock simulatorLock( m_simulator->GetMutex(), std::try_to_lock ); if( simulatorLock.owns_lock() ) { m_ui->OnSimUpdate(); m_simulator->Run(); } else { DisplayErrorMessage( this, _( "Another simulation is already running." ) ); } updateInProgress = false; } void SIMULATOR_FRAME::onSimReport( wxCommandEvent& aEvent ) { m_ui->OnSimReport( aEvent.GetString() ); } void SIMULATOR_FRAME::onExit( wxCommandEvent& aEvent ) { if( aEvent.GetId() == wxID_EXIT ) Kiway().OnKiCadExit(); if( aEvent.GetId() == wxID_CLOSE ) Close( false ); } void SIMULATOR_FRAME::OnModify() { KIWAY_PLAYER::OnModify(); m_workbookModified = true; UpdateTitle(); } wxDEFINE_EVENT( EVT_SIM_UPDATE, wxCommandEvent ); wxDEFINE_EVENT( EVT_SIM_REPORT, wxCommandEvent ); wxDEFINE_EVENT( EVT_SIM_STARTED, wxCommandEvent ); wxDEFINE_EVENT( EVT_SIM_FINISHED, wxCommandEvent );