kicad/eeschema/dialogs/dialog_sim_command.cpp

645 lines
22 KiB
C++

/*
* This program source code file is part of KiCad, a free EDA CAD application.
*
* Copyright (C) 2016-2022 CERN
* Copyright (C) 2016-2022 KiCad Developers, see AUTHORS.txt for contributors.
* @author Maciej Suminski <maciej.suminski@cern.ch>
*
* 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 "dialog_sim_command.h"
#include <sim/ngspice_circuit_model.h>
#include <sim/ngspice.h>
#include <confirm.h>
#include <wx/tokenzr.h>
#include <vector>
#include <utility>
/// @todo ngspice offers more types of analysis,
//so there are a few tabs missing (e.g. pole-zero, distortion, sensitivity)
// Helper function to shorten conditions
static bool empty( const wxTextEntryBase* aCtrl )
{
return aCtrl->GetValue().IsEmpty();
}
static void setStringSelection( wxChoice* aCtrl, const wxString& aStr )
{
aCtrl->SetSelection( aCtrl->FindString( aStr ) );
}
static wxString getStringSelection( const wxChoice* aCtrl )
{
return aCtrl->GetString( aCtrl->GetSelection() );
}
DIALOG_SIM_COMMAND::DIALOG_SIM_COMMAND( wxWindow* aParent,
std::shared_ptr<NGSPICE_CIRCUIT_MODEL> aCircuitModel,
std::shared_ptr<SPICE_SIMULATOR_SETTINGS>& aSettings ) :
DIALOG_SIM_COMMAND_BASE( aParent ),
m_circuitModel( aCircuitModel ),
m_settings( aSettings ),
m_spiceEmptyValidator( true )
{
m_posIntValidator.SetMin( 1 );
m_acPointsNumber->SetValidator( m_posIntValidator );
m_acFreqStart->SetValidator( m_spiceValidator );
m_acFreqStop->SetValidator( m_spiceValidator );
m_dcStart1->SetValidator( m_spiceValidator );
m_dcStop1->SetValidator( m_spiceValidator );
m_dcIncr1->SetValidator( m_spiceValidator );
m_dcStart2->SetValidator( m_spiceValidator );
m_dcStop2->SetValidator( m_spiceValidator );
m_dcIncr2->SetValidator( m_spiceValidator );
m_noisePointsNumber->SetValidator( m_posIntValidator );
m_noiseFreqStart->SetValidator( m_spiceValidator );
m_noiseFreqStop->SetValidator( m_spiceValidator );
m_transStep->SetValidator( m_spiceValidator );
m_transFinal->SetValidator( m_spiceValidator );
m_transInitial->SetValidator( m_spiceEmptyValidator );
m_transMaxStep->SetValidator( m_spiceEmptyValidator );
refreshUIControls();
// Hide pages that aren't fully implemented yet
// wxPanel::Hide() isn't enough on some platforms
m_simPages->RemovePage( m_simPages->FindPage( m_pgDistortion ) );
m_simPages->RemovePage( m_simPages->FindPage( m_pgNoise ) );
m_simPages->RemovePage( m_simPages->FindPage( m_pgPoleZero ) );
m_simPages->RemovePage( m_simPages->FindPage( m_pgSensitivity ) );
m_simPages->RemovePage( m_simPages->FindPage( m_pgTransferFunction ) );
if( !dynamic_cast<NGSPICE_SIMULATOR_SETTINGS*>( aSettings.get() ) )
m_compatibilityMode->Show( false );
SetupStandardButtons();
}
wxString DIALOG_SIM_COMMAND::evaluateDCControls( wxChoice* aDcSource, wxTextCtrl* aDcStart,
wxTextCtrl* aDcStop, wxTextCtrl* aDcIncr )
{
wxString dcSource;
wxWindow* ctrlWithError = nullptr;
if( aDcSource->GetSelection() >= 0 )
dcSource = aDcSource->GetString( aDcSource->GetSelection() );
if( dcSource.IsEmpty() )
{
DisplayError( this, _( "You need to select DC source" ) );
ctrlWithError = aDcSource;
}
/// @todo for some reason it does not trigger the assigned SPICE_VALIDATOR,
// hence try..catch below
else if( !aDcStart->Validate() )
ctrlWithError = aDcStart;
else if( !aDcStop->Validate() )
ctrlWithError = aDcStop;
else if( !aDcIncr->Validate() )
ctrlWithError = aDcIncr;
if( ctrlWithError )
{
ctrlWithError->SetFocus();
return wxEmptyString;
}
try
{
// pick device name from exporter when something different than temperature is selected
if( dcSource.Cmp( "TEMP" ) )
dcSource = m_circuitModel->GetItemName( std::string( dcSource.ToUTF8() ) );
return wxString::Format( "%s %s %s %s", dcSource,
SPICE_VALUE( aDcStart->GetValue() ).ToSpiceString(),
SPICE_VALUE( aDcStop->GetValue() ).ToSpiceString(),
SPICE_VALUE( aDcIncr->GetValue() ).ToSpiceString() );
}
catch( std::exception& e )
{
DisplayError( this, e.what() );
return wxEmptyString;
}
catch( const KI_PARAM_ERROR& e )
{
DisplayError( this, e.What() );
return wxEmptyString;
}
catch( ... )
{
return wxEmptyString;
}
}
bool DIALOG_SIM_COMMAND::TransferDataFromWindow()
{
if( !wxDialog::TransferDataFromWindow() )
return false;
// The simulator dependent settings always get transferred.
NGSPICE_SIMULATOR_SETTINGS* ngspiceSettings =
dynamic_cast<NGSPICE_SIMULATOR_SETTINGS*>( m_settings.get() );
if( ngspiceSettings )
{
switch( m_compatibilityModeChoice->GetSelection() )
{
case 0: ngspiceSettings->SetModelMode( NGSPICE_MODEL_MODE::USER_CONFIG ); break;
case 1: ngspiceSettings->SetModelMode( NGSPICE_MODEL_MODE::NGSPICE ); break;
case 2: ngspiceSettings->SetModelMode( NGSPICE_MODEL_MODE::PSPICE ); break;
case 3: ngspiceSettings->SetModelMode( NGSPICE_MODEL_MODE::LTSPICE ); break;
case 4: ngspiceSettings->SetModelMode( NGSPICE_MODEL_MODE::LT_PSPICE ); break;
case 5: ngspiceSettings->SetModelMode( NGSPICE_MODEL_MODE::HSPICE ); break;
}
}
wxString previousSimCommand = m_simCommand;
wxWindow* page = m_simPages->GetCurrentPage();
if( page == m_pgAC ) // AC analysis
{
if( !m_pgAC->Validate() )
return false;
m_simCommand.Printf( ".ac %s %s %s %s",
scaleToString( m_acScale->GetSelection() ),
m_acPointsNumber->GetValue(),
SPICE_VALUE( m_acFreqStart->GetValue() ).ToSpiceString(),
SPICE_VALUE( m_acFreqStop->GetValue() ).ToSpiceString() );
}
else if( page == m_pgDC ) // DC transfer analysis
{
wxString simCmd = wxString( ".dc " );
wxString src1 = evaluateDCControls( m_dcSource1, m_dcStart1, m_dcStop1, m_dcIncr1 );
if( src1.IsEmpty() )
return false;
else
simCmd += src1;
if( m_dcEnable2->IsChecked() )
{
wxString src2 = evaluateDCControls( m_dcSource2, m_dcStart2, m_dcStop2, m_dcIncr2 );
if( src2.IsEmpty() )
return false;
else
simCmd += " " + src2;
if( m_dcSource1->GetStringSelection() == m_dcSource2->GetStringSelection() )
{
DisplayError( this, _( "Source 1 and Source 2 must be different" ) );
return false;
}
}
m_simCommand = simCmd;
}
else if( page == m_pgNoise ) // Noise analysis
{
/*const std::map<wxString, int>& netMap = m_circuitModel->GetNetIndexMap();
if( empty( m_noiseMeas ) || empty( m_noiseSrc ) || empty( m_noisePointsNumber )
|| empty( m_noiseFreqStart ) || empty( m_noiseFreqStop ) )
{
return false;
}
wxString ref;
if( !empty( m_noiseRef ) )
ref = wxString::Format( ", %d", netMap.at( m_noiseRef->GetValue() ) );
wxString noiseSource = m_circuitModel->GetSpiceDevice( m_noiseSrc->GetValue() );
// Add voltage source prefix if needed
if( noiseSource[0] != 'v' && noiseSource[0] != 'V' )
noiseSource += 'v' + noiseSource;
m_simCommand.Printf( ".noise v(%d%s) %s %s %s %s %s",
netMap.at( m_noiseMeas->GetValue() ), ref,
noiseSource, scaleToString( m_noiseScale->GetSelection() ),
m_noisePointsNumber->GetValue(),
SPICE_VALUE( m_noiseFreqStart->GetValue() ).ToSpiceString(),
SPICE_VALUE( m_noiseFreqStop->GetValue() ).ToSpiceString() );*/
}
else if( page == m_pgOP ) // DC operating point analysis
{
m_simCommand = wxString( ".op" );
}
else if( page == m_pgTransient ) // Transient analysis
{
if( !m_pgTransient->Validate() )
return false;
const wxString spc = wxS( " " );
const SPICE_VALUE timeStep( m_transStep->GetValue() );
const SPICE_VALUE finalTime( m_transFinal->GetValue() );
SPICE_VALUE startTime( 0 );
if( !empty( m_transInitial ) )
startTime = SPICE_VALUE( m_transInitial->GetValue() );
wxString optionals;
if( m_useInitialConditions->GetValue() )
optionals = wxS( "uic" );
if( !empty( m_transMaxStep ) )
optionals = SPICE_VALUE( m_transMaxStep->GetValue() ).ToSpiceString() + spc + optionals;
else if( !optionals.IsEmpty() )
{
SPICE_VALUE maxStep = ( finalTime - startTime ) / 50.0;
if( maxStep > timeStep )
maxStep = timeStep;
optionals = maxStep.ToSpiceString() + spc + optionals;
}
if( !empty( m_transInitial ) )
optionals = startTime.ToSpiceString() + spc + optionals;
else if( !optionals.IsEmpty() )
optionals = wxS( "0 " ) + optionals;
m_simCommand.Printf( wxS( ".tran %s %s %s" ), timeStep.ToSpiceString(),
finalTime.ToSpiceString(), optionals );
}
else if( page == m_pgCustom ) // Custom directives
{
m_simCommand = m_customTxt->GetValue();
}
else
{
if( m_simCommand.IsEmpty() )
{
KIDIALOG dlg( this, _( "No valid simulation is configured." ), _( "Warning" ),
wxOK | wxCANCEL | wxICON_EXCLAMATION | wxCENTER );
dlg.SetExtendedMessage( _( "A valid simulation can be configured by selecting a "
"simulation tab, setting the simulation parameters and "
"clicking the OK button with the tab selected." ) );
dlg.SetOKCancelLabels(
wxMessageDialog::ButtonLabel( _( "Exit Without Valid Simulation" ) ),
wxMessageDialog::ButtonLabel( _( "Configure Valid Simulation" ) ) );
dlg.DoNotShowCheckbox( __FILE__, __LINE__ );
if( dlg.ShowModal() == wxID_OK )
return true;
}
return false;
}
if( previousSimCommand != m_simCommand )
m_simCommand.Trim();
m_settings->SetFixIncludePaths( m_fixIncludePaths->GetValue() );
return true;
}
bool DIALOG_SIM_COMMAND::TransferDataToWindow()
{
/// @todo one day it could interpret the sim command and fill out appropriate fields.
if( empty( m_customTxt ) )
loadDirectives();
m_fixIncludePaths->SetValue( m_settings->GetFixIncludePaths() );
NGSPICE_SIMULATOR_SETTINGS* ngspiceSettings =
dynamic_cast<NGSPICE_SIMULATOR_SETTINGS*>( m_settings.get() );
if( ngspiceSettings )
{
switch( ngspiceSettings->GetModelMode() )
{
case NGSPICE_MODEL_MODE::USER_CONFIG: m_compatibilityModeChoice->SetSelection( 0 ); break;
case NGSPICE_MODEL_MODE::NGSPICE: m_compatibilityModeChoice->SetSelection( 1 ); break;
case NGSPICE_MODEL_MODE::PSPICE: m_compatibilityModeChoice->SetSelection( 2 ); break;
case NGSPICE_MODEL_MODE::LTSPICE: m_compatibilityModeChoice->SetSelection( 3 ); break;
case NGSPICE_MODEL_MODE::LT_PSPICE: m_compatibilityModeChoice->SetSelection( 4 ); break;
case NGSPICE_MODEL_MODE::HSPICE: m_compatibilityModeChoice->SetSelection( 5 ); break;
default:
wxFAIL_MSG( wxString::Format( "Unknown NGSPICE_MODEL_MODE %d.",
ngspiceSettings->GetModelMode() ) );
break;
}
}
if( !m_dcSource1->GetCount() )
{
wxChar type1 = getStringSelection( m_dcSourceType1 ).Upper().GetChar( 0 );
updateDCSources( type1, m_dcSource1 );
}
if( !m_dcSource2->GetCount() )
{
wxChar type2 = getStringSelection( m_dcSourceType2 ).Upper().GetChar( 0 );
updateDCSources( type2, m_dcSource2 );
}
if( m_simCommand.IsEmpty() && !empty( m_customTxt ) )
return parseCommand( m_customTxt->GetValue() );
return true;
}
int DIALOG_SIM_COMMAND::ShowModal()
{
// Fill out comboboxes that allows one to select nets
// Map comoboxes to their current values
std::map<wxComboBox*, wxString> cmbNet = {
{ m_noiseMeas, m_noiseMeas->GetStringSelection() },
{ m_noiseRef, m_noiseRef->GetStringSelection() }
};
for( const std::pair<wxComboBox* const, wxString>& c : cmbNet )
c.first->Clear();
for( const std::string& net : m_circuitModel->GetNets() )
{
for( const std::pair<wxComboBox* const, wxString>& c : cmbNet )
c.first->Append( net );
}
// Try to restore the previous selection, if possible
for( const std::pair<wxComboBox* const, wxString>& c : cmbNet )
{
int idx = c.first->FindString( c.second );
if( idx != wxNOT_FOUND )
c.first->SetSelection( idx );
}
return DIALOG_SIM_COMMAND_BASE::ShowModal();
}
void DIALOG_SIM_COMMAND::updateDCSources( wxChar aType, wxChoice* aSource )
{
wxString prevSelection;
if( !aSource->IsEmpty() )
prevSelection = aSource->GetString( aSource->GetSelection() );
std::set<wxString> sourcesList;
bool enableSrcSelection = true;
if( aType != 'T' )
{
for( const SPICE_ITEM& item : m_circuitModel->GetItems() )
{
if( ( aType == 'R' && item.model->GetDeviceType() == SIM_MODEL::DEVICE_T::R )
|| ( aType == 'V' && item.model->GetDeviceType() == SIM_MODEL::DEVICE_T::V )
|| ( aType == 'I' && item.model->GetDeviceType() == SIM_MODEL::DEVICE_T::I ) )
{
sourcesList.insert( item.refName );
}
}
if( aSource == m_dcSource2 && !m_dcEnable2->IsChecked() )
enableSrcSelection = false;
}
else
{
prevSelection = wxT( "TEMP" );
sourcesList.insert( prevSelection );
enableSrcSelection = false;
}
aSource->Enable( enableSrcSelection );
aSource->Clear();
for( const wxString& src : sourcesList )
aSource->Append( src );
// Try to restore the previous selection, if possible
aSource->SetStringSelection( prevSelection );
}
bool DIALOG_SIM_COMMAND::parseCommand( const wxString& aCommand )
{
if( aCommand.IsEmpty() )
return false;
wxStringTokenizer tokenizer( aCommand, " " );
wxString tkn = tokenizer.GetNextToken().Lower();
try
{
if( tkn == ".ac" )
{
m_simPages->SetSelection( m_simPages->FindPage( m_pgAC ) );
tkn = tokenizer.GetNextToken().Lower();
if( tkn == "dec" )
m_acScale->SetSelection( 0 );
else if( tkn == "oct" )
m_acScale->SetSelection( 1 );
else if( tkn == "lin" )
m_acScale->SetSelection( 2 );
else
return false;
// If the fields below are empty, it will be caught by the exception handler
m_acPointsNumber->SetValue( tokenizer.GetNextToken() );
m_acFreqStart->SetValue( SPICE_VALUE( tokenizer.GetNextToken() ).ToSpiceString() );
m_acFreqStop->SetValue( SPICE_VALUE( tokenizer.GetNextToken() ).ToSpiceString() );
}
else if( tkn == ".dc" )
{
m_simPages->SetSelection( m_simPages->FindPage( m_pgDC ) );
SPICE_DC_PARAMS src1, src2;
src2.m_vincrement = SPICE_VALUE( -1 );
if( !m_circuitModel->ParseDCCommand( aCommand, &src1, &src2 ) )
return false;
if( src1.m_source.IsSameAs( wxT( "TEMP" ), false ) )
setStringSelection( m_dcSourceType1, wxT( "TEMP" ) );
else
setStringSelection( m_dcSourceType1, src1.m_source.GetChar( 0 ) );
updateDCSources( src1.m_source.GetChar( 0 ), m_dcSource1 );
m_dcSource1->SetStringSelection( src1.m_source );
m_dcStart1->SetValue( src1.m_vstart.ToSpiceString() );
m_dcStop1->SetValue( src1.m_vend.ToSpiceString() );
m_dcIncr1->SetValue( src1.m_vincrement.ToSpiceString() );
if( src2.m_vincrement.ToDouble() != -1 )
{
if( src2.m_source.IsSameAs( wxT( "TEMP" ), false ) )
setStringSelection( m_dcSourceType2, wxT( "TEMP" ) );
else
setStringSelection( m_dcSourceType2, src2.m_source.GetChar( 0 ) );
updateDCSources( src2.m_source.GetChar( 0 ), m_dcSource2 );
m_dcSource2->SetStringSelection( src2.m_source );
m_dcStart2->SetValue( src2.m_vstart.ToSpiceString() );
m_dcStop2->SetValue( src2.m_vend.ToSpiceString() );
m_dcIncr2->SetValue( src2.m_vincrement.ToSpiceString() );
m_dcEnable2->SetValue( true );
}
refreshUIControls();
}
else if( tkn == ".tran" )
{
m_simPages->SetSelection( m_simPages->FindPage( m_pgTransient ) );
// If the fields below are empty, it will be caught by the exception handler
m_transStep->SetValue( SPICE_VALUE( tokenizer.GetNextToken() ).ToSpiceString() );
m_transFinal->SetValue( SPICE_VALUE( tokenizer.GetNextToken() ).ToSpiceString() );
// Initial time is an optional field
tkn = tokenizer.GetNextToken();
if( !tkn.IsEmpty() )
m_transInitial->SetValue( SPICE_VALUE( tkn ).ToSpiceString() );
// Max step is an optional field
tkn = tokenizer.GetNextToken();
if( !tkn.IsEmpty() )
m_transMaxStep->SetValue( SPICE_VALUE( tkn ).ToSpiceString() );
// uic is an optional field
tkn = tokenizer.GetNextToken();
if( tkn.IsSameAs( wxS( "uic" ) ) )
m_useInitialConditions->SetValue( true );
}
else if( tkn == ".op" )
{
m_simPages->SetSelection( m_simPages->FindPage( m_pgOP ) );
}
else if( !empty( m_customTxt ) ) // Custom directives
{
m_simPages->SetSelection( m_simPages->FindPage( m_pgCustom ) );
}
}
catch( ... )
{
// Nothing really bad has happened
return false;
}
return true;
}
void DIALOG_SIM_COMMAND::onSwapDCSources( wxCommandEvent& event )
{
std::vector<std::pair<wxTextEntry*, wxTextEntry*>> textCtrl = { { m_dcStart1, m_dcStart2 },
{ m_dcStop1, m_dcStop2 },
{ m_dcIncr1, m_dcIncr2 } };
for( auto& couple : textCtrl )
{
wxString tmp = couple.first->GetValue();
couple.first->SetValue( couple.second->GetValue() );
couple.second->SetValue( tmp );
}
int src1 = m_dcSource1->GetSelection();
int src2 = m_dcSource2->GetSelection();
int sel = m_dcSourceType1->GetSelection();
m_dcSourceType1->SetSelection( m_dcSourceType2->GetSelection() );
m_dcSourceType2->SetSelection( sel );
wxChar type1 = getStringSelection( m_dcSourceType1 ).Upper().GetChar( 0 );
updateDCSources( type1, m_dcSource1 );
wxChar type2 = getStringSelection( m_dcSourceType2 ).Upper().GetChar( 0 );
updateDCSources( type2, m_dcSource2 );
m_dcSource1->SetSelection( src2 );
m_dcSource2->SetSelection( src1 );
updateDCUnits( type1, m_dcSource1, m_src1DCStartValUnit, m_src1DCEndValUnit, m_src1DCStepUnit );
updateDCUnits( type2, m_dcSource2, m_src2DCStartValUnit, m_src2DCEndValUnit, m_src2DCStepUnit );
}
void DIALOG_SIM_COMMAND::onDCEnableSecondSource( wxCommandEvent& event )
{
bool is2ndSrcEnabled = m_dcEnable2->IsChecked();
wxChar type = getStringSelection( m_dcSourceType2 ).Upper().GetChar( 0 );
m_dcSourceType2->Enable( is2ndSrcEnabled );
m_dcSource2->Enable( is2ndSrcEnabled && type != 'T' );
m_dcStart2->Enable( is2ndSrcEnabled );
m_dcStop2->Enable( is2ndSrcEnabled );
m_dcIncr2->Enable( is2ndSrcEnabled );
}
void DIALOG_SIM_COMMAND::updateDCUnits( wxChar aType, wxChoice* aSource,
wxStaticText* aStartValUnit, wxStaticText* aEndValUnit,
wxStaticText* aStepUnit )
{
wxString unit;
switch( aType )
{
case 'V': unit = wxS( "V" ); break;
case 'I': unit = wxS( "A" ); break;
case 'R': unit = wxS( "Ω" ); break;
case 'T': unit = wxS( "°C" ); break;
}
aStartValUnit->SetLabel( unit );
aEndValUnit->SetLabel( unit );
aStepUnit->SetLabel( unit );
m_pgDC->Refresh();
}
void DIALOG_SIM_COMMAND::loadDirectives()
{
if( m_circuitModel )
m_customTxt->SetValue( m_circuitModel->GetSchTextSimCommand() );
}