####################################################################################################
#
# PySpice - A Spice Package for Python
# Copyright (C) 2014 Fabrice Salvaire
#
# 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, see <http://www.gnu.org/licenses/>.
#
####################################################################################################
"""This module provides a Python interface to the Ngspice shared library described in the *ngspice
as shared library or dynamic link library* section of the Ngspice user manual.
In comparison to the subprocess interface, it provides an interaction with the simulator through
commands and callbacks and it enables the usage of external voltage and current source in the
circuit.
.. This approach corresponds to the *standard way* to make an interface to a simulator code.
.. warning:: Since we don't simulate a circuit in a fresh environment on demand, this approach is
less safe than the subprocess approach. In case of bugs within Ngspice, we can expect some side
effects like memory leaks or worse unexpected things.
This interface use the CFFI module to interface with the shared library. It is thus suited to run
within the Pypy interpreter which implements JIT optimisation for Python.
It can also be used to experiment parallel simulation as explained in the Ngspice user manual. But
it seems the Ngspice source code was designed with global variables which imply to use one copy of
the shared library by worker as explained in the manual.
.. warning:: This interface can strongly slow down the simulation if the input or output callbacks
is used. If the simulation time goes wrong for you then you need to implement the callbacks at a
lower level than Python. You can have look to Pypy, Cython or a C extension module.
"""
####################################################################################################
# 16.7 Environmental variables
# 16.7.1 Ngspice specific variables
#
# SPICE_LIB_DIR
# default: /usr/local/share/ngspice (Linux, CYGWIN), C:\Spice\share\ngspice (Windows)
# SPICE_EXEC_DIR
# default: /usr/local/bin (Linux, CYGWIN), C:\Spice\bin (Windows)
# SPICE_ASCIIRAWFILE
# default: 0
# Format of the rawfile. 0 for binary, and 1 for ascii.
# SPICE_SCRIPTS
# default: $SPICE_LIB_DIR/scripts
# In this directory the spinit file will be searched.
# NGSPICE_MEAS_PRECISION
# default: 5
# Sets the number of digits if output values are printed by the meas(ure) command.
# SPICE_NO_DATASEG_CHECK
# default: undefined
# If defined, will suppress memory resource info (probably obsolete, not used on Windows
# or where the /proc information system is available.)
# NGSPICE_INPUT_DIR
# default: undefined
# If defined, using a valid directory name, will add the given directory to the search path
# when looking for input files (*.cir, *.inc, *.lib).
####################################################################################################
__all__ = [
'NgSpiceCircuitError',
'NgSpiceCommandError',
'NgSpiceShared',
]
####################################################################################################
from pathlib import Path
import logging
import os
import platform
import re
import numpy as np
from cffi import FFI
####################################################################################################
from PySpice.Config import ConfigInstall
from PySpice.Probe.WaveForm import (
OperatingPoint, SensitivityAnalysis,
DcAnalysis, AcAnalysis, TransientAnalysis,
PoleZeroAnalysis, NoiseAnalysis, DistortionAnalysis, TransferFunctionAnalysis,
WaveForm,
)
from PySpice.Tools.EnumFactory import EnumFactory
from PySpice.Unit import u_V, u_A, u_s, u_Hz, u_F, u_Degree
from .SimulationType import SIMULATION_TYPE
####################################################################################################
ffi = FFI()
####################################################################################################
_module_logger = logging.getLogger(__name__)
####################################################################################################
[docs]class NgSpiceCircuitError(NameError):
pass
[docs]class NgSpiceCommandError(NameError):
pass
####################################################################################################
[docs]def ffi_string_utf8(_):
_ = ffi.string(_)
try:
return _.decode('utf8')
except UnicodeDecodeError:
return _
####################################################################################################
[docs]class Vector:
""" This class implements a vector in a simulation output.
Public Attributes:
:attr:`data`
:attr:`name`
"""
_logger = _module_logger.getChild('Vector')
##############################################
def __init__(self, ngspice_shared, name, type_, data):
self._ngspice_shared = ngspice_shared
self._name = str(name)
self._type = type_
self._data = data
self._unit = ngspice_shared.type_to_unit(type_)
if self._unit is None:
self._logger.warning('Unit is None for {0._name} {0._type}'.format(self))
##############################################
def __repr__(self):
return 'variable: {0._name} {0._type}'.format(self)
##############################################
@property
def is_interval_parameter(self):
return self._name.startswith('@')
##############################################
@property
def is_voltage_node(self):
return self._type == self._ngspice_shared.simulation_type.voltage and not self.is_interval_parameter
##############################################
@property
def is_branch_current(self):
return self._type == self._ngspice_shared.simulation_type.current and not self.is_interval_parameter
##############################################
@property
def simplified_name(self):
if self.is_voltage_node and self._name.startswith('V('):
return self._name[2:-1]
elif self.is_branch_current:
# return self._name.replace('#branch', '')
return self._name[:-7]
else:
return self._name
##############################################
####################################################################################################
[docs]class Plot(dict):
""" This class implements a plot in a simulation output.
Public Attributes:
:attr:`plot_name`
"""
##############################################
def __init__(self, simulation, plot_name):
super().__init__()
self._simulation = simulation
self.plot_name = plot_name
##############################################
[docs] def nodes(self, to_float=False, abscissa=None):
return [variable.to_waveform(abscissa, to_float=to_float)
for variable in self.values()
if variable.is_voltage_node]
##############################################
[docs] def branches(self, to_float=False, abscissa=None):
return [variable.to_waveform(abscissa, to_float=to_float)
for variable in self.values()
if variable.is_branch_current]
##############################################
[docs] def internal_parameters(self, to_float=False, abscissa=None):
return [variable.to_waveform(abscissa, to_float=to_float)
for variable in self.values()
if variable.is_interval_parameter]
##############################################
[docs] def elements(self, abscissa=None):
return [variable.to_waveform(abscissa, to_float=True)
for variable in self.values()]
##############################################
[docs] def to_analysis(self):
if self.plot_name.startswith('op'):
return self._to_operating_point_analysis()
elif self.plot_name.startswith('sens'):
return self._to_sensitivity_analysis()
elif self.plot_name.startswith('dc'):
return self._to_dc_analysis()
elif self.plot_name.startswith('ac'):
return self._to_ac_analysis()
elif self.plot_name.startswith('tran'):
return self._to_transient_analysis()
elif self.plot_name.startswith('disto'):
return self._to_distortion_analysis()
elif self.plot_name.startswith('noise'):
return self._to_noise_analysis()
elif self.plot_name.startswith('pz'):
return self._to_polezero_analysis()
elif self.plot_name.startswith('tf'):
return self._to_transfer_function_analysis()
else:
raise NotImplementedError("Unsupported plot name {}".format(self.plot_name))
##############################################
def _to_operating_point_analysis(self):
return OperatingPoint(
simulation=self._simulation,
nodes=self.nodes(to_float=True),
branches=self.branches(to_float=True),
internal_parameters=self.internal_parameters(),
)
##############################################
def _to_sensitivity_analysis(self):
# Fixme: separate v(vinput), analysis.R2.m
return SensitivityAnalysis(
simulation=self._simulation,
elements=self.elements(), # Fixme: internal parameters ???
internal_parameters=self.internal_parameters(),
)
##############################################
def _to_dc_analysis(self):
for name in ('v-sweep', 'i-sweep', 'temp-sweep'):
if name in self:
sweep_variable = self[name]
break
else:
raise NotImplementedError(str(self))
sweep = sweep_variable.to_waveform()
return DcAnalysis(
simulation=self._simulation,
sweep=sweep,
nodes=self.nodes(),
branches=self.branches(),
internal_parameters=self.internal_parameters(),
)
##############################################
def _to_ac_analysis(self):
frequency = self['frequency'].to_waveform(to_real=True)
return AcAnalysis(
simulation=self._simulation,
frequency=frequency,
nodes=self.nodes(),
branches=self.branches(),
internal_parameters=self.internal_parameters(),
)
##############################################
def _to_transient_analysis(self):
time = self['time'].to_waveform(to_real=True)
return TransientAnalysis(
simulation=self._simulation,
time=time,
nodes=self.nodes(abscissa=time),
branches=self.branches(abscissa=time),
internal_parameters=self.internal_parameters(abscissa=time),
)
##############################################
def _to_polezero_analysis(self):
return PoleZeroAnalysis(
simulation=self._simulation,
nodes=self.nodes(),
branches=self.branches(),
internal_parameters=self.internal_parameters(),
)
##############################################
def _to_noise_analysis(self):
return NoiseAnalysis(
simulation=self._simulation,
nodes=self.nodes(),
branches=self.branches(),
internal_parameters=self.internal_parameters(),
)
##############################################
def _to_distortion_analysis(self):
frequency = self['frequency'].to_waveform(to_real=True)
return DistortionAnalysis(
simulation=self._simulation,
frequency=frequency,
nodes=self.nodes(),
branches=self.branches(),
internal_parameters=self.internal_parameters(),
)
##############################################
def _to_transfer_function_analysis(self):
return TransferFunctionAnalysis(
simulation=self._simulation,
nodes=self.nodes(),
branches=self.branches(),
internal_parameters=self.internal_parameters(),
)
####################################################################################################
[docs]class NgSpiceShared:
_logger = _module_logger.getChild('NgSpiceShared')
NGSPICE_PATH = None
LIBRARY_PATH = None
MAX_COMMAND_LENGTH = 1023
NUMBER_OF_EXEC_CALLS_TO_RELEASE_MEMORY = 10_000
##############################################
##############################################
_instances = {}
[docs] @classmethod
def new_instance(cls, ngspice_id=0, send_data=False, verbose=False):
"""Create a NgSpiceShared instance"""
# Fixme: send_data
if ngspice_id in cls._instances:
return cls._instances[ngspice_id]
else:
cls._logger.debug("New instance for id {}".format(ngspice_id))
instance = cls(ngspice_id=ngspice_id, send_data=send_data, verbose=verbose)
cls._instances[ngspice_id] = instance
return instance
##############################################
def __init__(self, ngspice_id=0, send_data=False, verbose=False):
""" Set the *send_data* flag if you want to enable the output callback.
Set the *ngspice_id* to an integer value if you want to run NgSpice in parallel.
"""
self._ngspice_id = ngspice_id
self._spinit_not_found = False
self._number_of_exec_calls = 0
self._stdout = []
self._stderr = []
self._error_in_stdout = None
self._error_in_stderr = None
self._has_cider = None
self._has_xspice = None
self._ngspice_version = None
self._extensions = []
self._library_path = None
self._load_library(verbose)
self._init_ngspice(send_data)
self._is_running = False
##############################################
@property
def spinit_not_found(self):
return self._spinit_not_found
##############################################
@property
def library_path(self):
if self._library_path is None:
if not self._ngspice_id:
library_prefix = ''
else:
library_prefix = '{}'.format(self._ngspice_id) # id =
library_path = self.LIBRARY_PATH.format(library_prefix)
self._library_path = library_path
return self._library_path
##############################################
def _load_library(self, verbose):
if ConfigInstall.OS.on_windows:
# https://sourceforge.net/p/ngspice/discussion/133842/thread/1cece652/#4e32/5ab8/9027
# When environment variable SPICE_LIB_DIR is empty, ngspice looks in C:\Spice64\share\ngspice\scripts
# Else it tries %SPICE_LIB_DIR%\scripts\spinit
if 'SPICE_LIB_DIR' not in os.environ:
_ = str(Path(self.NGSPICE_PATH).joinpath('share', 'ngspice'))
os.environ['SPICE_LIB_DIR'] = _
# self._logger.warning('Set SPICE_LIB_DIR = %s', _)
# Fixme: not compatible with supra
# if 'CONDA_PREFIX' in os.environ:
# _ = str(Path(os.environ['CONDA_PREFIX']).joinpath('share', 'ngspice'))
# os.environ['SPICE_LIB_DIR'] = _
# self._logger.warning('Set SPICE_LIB_DIR = %s', _)
# https://sourceforge.net/p/ngspice/bugs/490
# ngspice and Kicad do setlocale(LC_NUMERIC, "C");
if ConfigInstall.OS.on_windows:
self._logger.debug('locale LC_NUMERIC is not forced to C')
elif ConfigInstall.OS.on_linux or ConfigInstall.OS.on_osx:
self._logger.debug('Set locale LC_NUMERIC to C')
import locale
locale.setlocale(locale.LC_NUMERIC, 'C')
api_path = Path(__file__).parent.joinpath('api.h')
with open(api_path) as fh:
ffi.cdef(fh.read())
message = 'Load library {}'.format(self.library_path)
self._logger.debug(message)
if verbose:
print(message)
self._ngspice_shared = ffi.dlopen(self.library_path)
# Note: cannot yet execute command
##############################################
def _init_ngspice(self, send_data):
# Ngspice API: ngSpice_Init ngSpice_Init_Sync
self._send_char_c = ffi.callback('int (char *, int, void *)', self._send_char)
self._send_stat_c = ffi.callback('int (char *, int, void *)', self._send_stat)
self._exit_c = ffi.callback('int (int, bool, bool, int, void *)', self._exit)
self._send_init_data_c = ffi.callback('int (pvecinfoall, int, void *)', self._send_init_data)
self._background_thread_running_c = ffi.callback('int (bool, int, void *)', self._background_thread_running)
if send_data:
self._send_data_c = ffi.callback('int (pvecvaluesall, int, int, void *)', self._send_data)
else:
self._send_data_c = FFI.NULL
self._get_vsrc_data_c = ffi.callback('int (double *, double, char *, int, void *)', self._get_vsrc_data)
self._get_isrc_data_c = ffi.callback('int (double *, double, char *, int, void *)', self._get_isrc_data)
self_c = ffi.new_handle(self)
self._self_c = self_c # To prevent garbage collection
rc = self._ngspice_shared.ngSpice_Init(self._send_char_c,
self._send_stat_c,
self._exit_c,
self._send_data_c,
self._send_init_data_c,
self._background_thread_running_c,
self_c)
if rc:
raise NameError("Ngspice_Init returned {}".format(rc))
ngspice_id_c = ffi.new('int *', self._ngspice_id)
self._ngspice_id = ngspice_id_c # To prevent garbage collection
rc = self._ngspice_shared.ngSpice_Init_Sync(self._get_vsrc_data_c,
self._get_isrc_data_c,
FFI.NULL, # GetSyncData
ngspice_id_c,
self_c)
if rc:
raise NameError("Ngspice_Init_Sync returned {}".format(rc))
self._get_version()
try:
self._simulation_type = EnumFactory('SimulationType', SIMULATION_TYPE[self._ngspice_version])
except KeyError:
# See SimulationType.py
self._simulation_type = EnumFactory('SimulationType', SIMULATION_TYPE['last'])
self._logger.warning("Unsupported Ngspice version {}".format(self._ngspice_version))
self._type_to_unit = {
self._simulation_type.time: u_s,
self._simulation_type.voltage: u_V,
self._simulation_type.current: u_A,
self._simulation_type.frequency: u_Hz,
self._simulation_type.capacitance: u_F,
self._simulation_type.temperature: u_Degree,
}
# Prevent paging output of commands (hangs)
self.set('nomoremode')
##############################################
@staticmethod
def _send_char(message_c, ngspice_id, user_data):
"""Callback for sending output from stdout, stderr to caller"""
self = ffi.from_handle(user_data)
_module_logger.debug(str(ffi.string(message_c)))
message = ffi_string_utf8(message_c)
# split message in "<prefix><match = ' '><content>"
prefix, _, content = message.partition(' ')
if prefix == 'stderr':
self._stderr.append(content)
if content.startswith('Warning:'):
func = self._logger.warning
# elif content.startswith('Warning:'):
else:
self._error_in_stderr = True
func = self._logger.error
if content.strip() == "Note: can't find init file.":
self._spinit_not_found = True
self._logger.warning('spinit was not found')
func(content)
else:
self._stdout.append(content)
# Fixme: Ngspice writes error on stdout and stderr ...
if 'error' in content.lower():
self._error_in_stdout = True
# if self._error_in_stdout:
# self._logger.warning(content)
# Fixme: ???
return self.send_char(message, ngspice_id)
##############################################
@staticmethod
def _send_stat(message, ngspice_id, user_data):
"""Callback for simulation status to caller"""
self = ffi.from_handle(user_data)
return self.send_stat(ffi_string_utf8(message), ngspice_id)
##############################################
@staticmethod
def _exit(exit_status, immediate_unloding, quit_exit, ngspice_id, user_data):
"""Callback for asking for a reaction after controlled exit"""
self = ffi.from_handle(user_data)
self._logger.debug('ngspice_id-{} exit status={} immediate_unloding={} quit_exit={}'.format(
ngspice_id,
exit_status,
immediate_unloding,
quit_exit))
return exit_status
##############################################
@staticmethod
def _send_data(data, number_of_vectors, ngspice_id, user_data):
"""Callback to send back actual vector data"""
self = ffi.from_handle(user_data)
# self._logger.debug('ngspice_id-{} send_data [{}]'.format(ngspice_id, data.vecindex))
actual_vector_values = {}
for i in range(int(number_of_vectors)):
actual_vector_value = data.vecsa[i]
vector_name = ffi_string_utf8(actual_vector_value.name)
value = complex(actual_vector_value.creal, actual_vector_value.cimag)
actual_vector_values[vector_name] = value
# self._logger.debug(' Vector: {} {}'.format(vector_name, value))
return self.send_data(actual_vector_values, number_of_vectors, ngspice_id)
##############################################
@staticmethod
def _send_init_data(data, ngspice_id, user_data):
"""Callback to send back initialization vector data"""
self = ffi.from_handle(user_data)
# if self._logger.isEnabledFor(logging.DEBUG):
# self._logger.debug('ngspice_id-{} send_init_data'.format(ngspice_id))
# number_of_vectors = data.veccount
# for i in range(number_of_vectors):
# self._logger.debug(' Vector: ' + ffi_string_utf8(data.vecs[i].vecname))
return self.send_init_data(data, ngspice_id) # Fixme: should be a Python object
##############################################
@staticmethod
def _background_thread_running(is_running, ngspice_id, user_data):
"""Callback to indicate if background thread is runnin"""
self = ffi.from_handle(user_data)
self._logger.debug('ngspice_id-{} background_thread_running {}'.format(ngspice_id, is_running))
self._is_running = is_running
##############################################
@staticmethod
def _get_vsrc_data(voltage, time, node, ngspice_id, user_data):
"""FFI Callback"""
self = ffi.from_handle(user_data)
return self.get_vsrc_data(voltage, time, ffi_string_utf8(node), ngspice_id)
##############################################
@staticmethod
def _get_isrc_data(current, time, node, ngspice_id, user_data):
"""FFI Callback"""
self = ffi.from_handle(user_data)
return self.get_isrc_data(current, time, ffi_string_utf8(node), ngspice_id)
##############################################
[docs] def send_char(self, message, ngspice_id):
""" Reimplement this callback in a subclass to process logging messages from the simulator. """
# self._logger.debug('ngspice-{} send_char {}'.format(ngspice_id, message))
return 0
##############################################
[docs] def send_stat(self, message, ngspice_id):
""" Reimplement this callback in a subclass to process statistic messages from the simulator. """
# self._logger.debug('ngspice-{} send_stat {}'.format(ngspice_id, message))
return 0
##############################################
[docs] def send_data(self, actual_vector_values, number_of_vectors, ngspice_id):
""" Reimplement this callback in a subclass to process the vector actual values. """
return 0
##############################################
[docs] def send_init_data(self, data, ngspice_id):
""" Reimplement this callback in a subclass to process the initial data. """
return 0
##############################################
[docs] def get_vsrc_data(self, voltage, time, node, ngspice_id):
""" Reimplement this callback in a subclass to provide external voltage source. """
self._logger.debug('ngspice_id-{} get_vsrc_data @{} node {}'.format(ngspice_id, time, node))
return 0
##############################################
[docs] def get_isrc_data(self, current, time, node, ngspice_id):
""" Reimplement this callback in a subclass to provide external current source. """
self._logger.debug('ngspice_id-{} get_isrc_data @{} node {}'.format(ngspice_id, time, node))
return 0
##############################################
@staticmethod
def _convert_string_array(array):
strings = []
i = 0
while True:
if array[i] == FFI.NULL:
break
strings.append(ffi_string_utf8(array[i]))
i += 1
return strings
##############################################
@staticmethod
def _to_python(value):
try:
return int(value)
except ValueError:
try:
# Fixme: return float(value.replace(',', '.'))
return float(value)
except ValueError:
return str(value)
##############################################
@staticmethod
def _lines_to_dicts(lines):
if lines:
values = dict(description=lines[0])
values.update({
parts[0]: NgSpiceShared._to_python(parts[1])
for parts in map(str.split, lines)
})
return values
else:
raise ValueError
##############################################
@property
def is_running(self):
return self._is_running
##############################################
[docs] def clear_output(self):
self._stdout = []
self._stderr = []
self._error_in_stdout = False
self._error_in_stderr = False
##############################################
@property
def stdout(self):
return os.linesep.join(self._stdout)
@property
def stderr(self):
return os.linesep.join(self._stderr)
##############################################
[docs] def exec_command(self, command, join_lines=True):
""" Execute a command and return the output. """
# Ngspice API: ngSpice_Command
# Prevent memory leaks by periodically freeing ngspice history of past commands
# Each command sent to ngspice is stored in the control structures
if self._number_of_exec_calls > self.NUMBER_OF_EXEC_CALLS_TO_RELEASE_MEMORY:
# Clear the internal control structures
self._ngspice_shared.ngSpice_Command(FFI.NULL)
self._number_of_exec_calls = 0
self._number_of_exec_calls += 1
if len(command) > self.MAX_COMMAND_LENGTH:
raise ValueError('Command must not exceed {} characters'.format(self.MAX_COMMAND_LENGTH))
self._logger.debug('Execute command: {}'.format(command))
self.clear_output()
encoded_command = command.encode('ascii')
rc = self._ngspice_shared.ngSpice_Command(encoded_command)
if rc: # Fixme: when not 0 ???
raise NameError("ngSpice_Command '{}' returned {}".format(command, rc))
if self._error_in_stdout or self._error_in_stderr:
raise NgSpiceCommandError("Command '{}' failed".format(command))
if join_lines:
return self.stdout
else:
return self._stdout
##############################################
def _get_version(self):
self._ngspice_version = None
self._has_xspice = False
self._has_cider = False
self._extensions = []
output = self.exec_command('version -f')
for line in output.split('\n'):
match = re.match(r'\*\* ngspice\-(\d+)', line)
if match is not None:
self._ngspice_version = int(match.group(1))
# if '** XSPICE extensions included' in line:
if '** XSPICE' in line:
self._has_xspice = True
self._extensions.append('XSPICE')
# if '** CIDER 1.b1 (CODECS simulator) included' in line:
if 'CIDER' in line:
self._has_cider = True
self._extensions.append('CIDER')
self._logger.debug(
'Ngspice version %s with extensions: %s',
self._ngspice_version,
', '.join(self._extensions),
)
##############################################
@property
def ngspice_version(self):
return self._ngspice_version
@property
def has_xspice(self):
"""Return True if libngspice was compiled with XSpice support."""
return self._has_xspice
@property
def has_cider(self):
"""Return True if libngspice was compiled with CIDER support."""
return self._has_cider
##############################################
@property
def simulation_type(self):
return self._simulation_type
[docs] def type_to_unit(self, vector_type):
return self._type_to_unit.get(vector_type, None)
##############################################
def _alter(self, command, device, kwargs):
# Performance optimization: dispatch multiple alter commands jointly
device_name = device.lower()
commands = []
commands_str_len = 0
for key, value in kwargs.items():
if isinstance(value, (list, tuple)):
value = '[ ' + ' '.join(value) + ' ]'
cmd = '{} {} {} = {}'.format(command, device_name, key, value)
# performance optimization: collect multiple alter commands and
# dispatch them jointly
commands.append(cmd)
commands_str_len += len(cmd)
if commands_str_len + len(commands) > self.MAX_COMMAND_LENGTH:
self.exec_command(';'.join(commands[:-1]))
commands = commands[-1:]
commands_str_len = len(commands[0])
if commands:
self.exec_command(';'.join(commands))
##############################################
[docs] def alter_device(self, device, **kwargs):
"""Alter device parameters"""
self._alter('alter', device, kwargs)
##############################################
[docs] def alter_model(self, model, **kwargs):
"""Alter model parameters"""
self._alter('altermod', model, kwargs)
##############################################
[docs] def delete(self, debug_number):
"""Remove a trace or breakpoint"""
self.exec_command('delete {}'.format(debug_number))
##############################################
[docs] def destroy(self, plot_name='all'):
"""Release the memory holding the output data (the given plot or all plots) for the specified runs."""
self.exec_command('destroy ' + plot_name)
##############################################
[docs] def device_help(self, device):
"""Shows the user information about the devices available in the simulator. """
return self.exec_command('devhelp ' + device.lower())
##############################################
[docs] def save(self, vector):
self.exec_command('save ' + vector)
##############################################
def _show(self, command):
lines = self.exec_command(command, join_lines=False)
if lines:
values = self._lines_to_dicts(lines)
return values
else:
return ''
##############################################
[docs] def show(self, device):
return self._show('show ' + device.lower())
##############################################
[docs] def showmod(self, device):
return self._show('showmod ' + device.lower())
##############################################
[docs] def source(self, file_path):
"""Read a ngspice input file"""
self.exec_command('source ' + file_path)
##############################################
[docs] def option(self, **kwargs):
"""Set any of the simulator variables."""
for key, value in kwargs.items():
self.exec_command('option {} = {}'.format(key, value))
##############################################
[docs] def quit(self):
self.set('noaskquit')
return self.exec_command('quit')
##############################################
[docs] def remove_circuit(self):
"""Removes the current circuit from the list of circuits sourced into ngspice."""
self.exec_command('remcirc')
##############################################
[docs] def reset(self):
"""Throw out any intermediate data in the circuit (e.g, after a breakpoint or after one or more
analyses have been done already), and re-parse the input file. The circuit can then be
re-run from it’s initial state, overriding the affect of any set or alter commands.
"""
self.exec_command('reset')
##############################################
[docs] def ressource_usage(self, *ressources):
"""Print resource usage statistics. If any resources are given, just print the usage of that resource.
Most resources require that a circuit be loaded. Currently valid resources are:
* decklineno Number of lines in deck
* netloadtime Nelist loading time
* netparsetime Netlist parsing time
* elapsed The amount of time elapsed since the last rusage elapsed call.
* faults Number of page faults and context switches (BSD only).
* space Data space used.
* time CPU time used so far.
* temp Operating temperature.
* tnom Temperature at which device parameters were measured.
* equations Circuit Equations
* time Total Analysis Time
* totiter Total iterations
* accept Accepted time-points
* rejected Rejected time-points
* loadtime Time spent loading the circuit matrix and RHS.
* reordertime Matrix reordering time
* lutime L-U decomposition time
* solvetime Matrix solve time
* trantime Transient analysis time
* tranpoints Transient time-points
* traniter Transient iterations
* trancuriters Transient iterations for the last time point*
* tranlutime Transient L-U decomposition time
* transolvetime Transient matrix solve time
* everything All of the above.
"""
if not ressources:
ressources = ['everything']
command = 'rusage ' + ' '.join(ressources)
lines = self.exec_command(command, join_lines=False)
values = {}
for line in lines:
if '=' in line:
parts = line.split(' = ')
else:
parts = line.split(': ')
values[parts[0]] = NgSpiceShared._to_python(parts[1])
return values
##############################################
[docs] def set(self, *args, **kwargs):
"""Set the value of variables"""
for key in args:
self.exec_command('set {}'.format(key))
for key, value in kwargs.items():
self.exec_command('option {} = {}'.format(key, value))
##############################################
[docs] def set_circuit(self, name):
"""Change the current circuit"""
self.exec_command('setcirc {}'.format(name))
##############################################
[docs] def status(self):
"""Display breakpoint information"""
return self.exec_command('status')
##############################################
[docs] def step(self, number_of_steps=None):
"""Run a fixed number of time-points"""
if number_of_steps is not None:
self.exec_command('step {}'.format(number_of_steps))
else:
self.exec_command('step')
##############################################
[docs] def stop(self, *args, **kwargs):
"""Set a breakpoint.
Examples::
ngspice.stop('v(out) > 1', 'v(1) > 10', after=10)
A when condition can use theses symbols: = <> > < >= <=.
"""
command = 'stop'
if 'after' in kwargs:
command += ' after {}'.format(kwargs['after'])
for condition in args:
command += ' when {}'.format(condition)
self.exec_command(command)
##############################################
[docs] def trace(self, *args):
"""Trace nodes"""
self.exec_command('trace ' + ' '.join(args))
##############################################
[docs] def unset(self, *args):
"""Unset variables"""
for key in args:
self.exec_command('unset {}'.format(key))
##############################################
[docs] def where(self):
"""Identify troublesome node or device"""
return self.exec_command('where')
##############################################
[docs] def load_circuit(self, circuit):
"""Load the given circuit string."""
# Ngspice API: ngSpice_Circ
circuit_lines = [line for line in str(circuit).splitlines() if line]
self._logger.debug('ngSpice_Circ\n' + str(circuit))
# ngspice 33 requires an empty line at the end
circuit_lines.append("")
circuit_lines_keepalive = [ffi.new("char[]", line.encode('utf8'))
for line in circuit_lines]
circuit_lines_keepalive += [FFI.NULL]
circuit_array = ffi.new("char *[]", circuit_lines_keepalive)
self.clear_output()
rc = self._ngspice_shared.ngSpice_Circ(circuit_array)
if rc: # Fixme: when not 0 ???
raise NameError("ngSpice_Circ returned {}".format(rc))
# Fixme: when Ngspice found an error in the circuit, it reports the error in stdout
# Fixme: https://sourceforge.net/p/ngspice/bugs/496/
if self._error_in_stdout:
self._logger.error('\n' + self.stdout)
raise NgSpiceCircuitError('')
# for line in circuit_lines:
# rc = self._ngspice_shared.ngSpice_Command(('circbyline ' + line).encode('utf8'))
# if rc:
# raise NameError("ngSpice_Command circbyline returned {}".format(rc))
##############################################
[docs] def listing(self):
command = 'listing'
return self.exec_command(command)
##############################################
[docs] def run(self, background=False):
""" Run the simulation. """
# in the background thread and wait until the simulation is done
command = 'bg_run' if background else 'run'
self.exec_command(command)
if background:
self._is_running = True
else:
self._logger.debug("Simulation is done")
# time.sleep(.1) # required before to test if the simulation is running
# while (self._ngspice_shared.ngSpice_running()):
# time.sleep(.1)
# self._logger.debug("Simulation is done")
##############################################
[docs] def halt(self):
""" Halt the simulation in the background thread. """
self.exec_command('bg_halt')
##############################################
[docs] def resume(self, background=True):
""" Halt the simulation in the background thread. """
command = 'bg_resume' if background else 'resume'
self.exec_command(command)
##############################################
@property
def plot_names(self):
""" Return the list of plot names. """
# Ngspice API: ngSpice_AllPlots
return self._convert_string_array(self._ngspice_shared.ngSpice_AllPlots())
##############################################
@property
def last_plot(self):
""" Return the last plot name. """
return self.plot_names[0]
##############################################
@staticmethod
def _flags_to_str(flags):
# enum dvec_flags {
# VF_REAL = (1 << 0), // The data is real.
# VF_COMPLEX = (1 << 1), // The data is complex.
# VF_ACCUM = (1 << 2), // writedata should save this vector.
# VF_PLOT = (1 << 3), // writedata should incrementally plot it.
# VF_PRINT = (1 << 4), // writedata should print this vector.
# VF_MINGIVEN = (1 << 5), // The v_minsignal value is valid.
# VF_MAXGIVEN = (1 << 6), // The v_maxsignal value is valid.
# VF_PERMANENT = (1 << 7) // Don't garbage collect this vector.
# };
if flags & 1:
return 'real'
elif flags & 2:
return 'complex'
else:
raise NotImplementedError
##############################################
@staticmethod
def _vector_is_real(flags):
return flags & 1
##############################################
@staticmethod
def _vector_is_complex(flags):
return flags & 2
##############################################
[docs] def plot(self, simulation, plot_name):
""" Return the corresponding plot. """
# Ngspice API: ngSpice_AllVecs ngGet_Vec_Info
# plot_name is for example dc with an integer suffix which is increment for each run
plot = Plot(simulation, plot_name)
all_vectors_c = self._ngspice_shared.ngSpice_AllVecs(plot_name.encode('utf8'))
i = 0
while True:
if all_vectors_c[i] == FFI.NULL:
break
vector_name = ffi_string_utf8(all_vectors_c[i])
name = '.'.join((plot_name, vector_name))
vector_info = self._ngspice_shared.ngGet_Vec_Info(name.encode('utf8'))
vector_type = self._simulation_type[vector_info.v_type]
length = vector_info.v_length
# template = 'vector[{}] {} type {} flags {} length {}'
# self._logger.debug(template.format(
# i,
# vector_name,
# vector_type,
# self._flags_to_str(vector_info.v_flags),
# length,
# ))
if vector_info.v_compdata == FFI.NULL:
# for k in range(length):
# print(" [{}] {}".format(k, vector_info.v_realdata[k]))
tmp_array = np.frombuffer(ffi.buffer(vector_info.v_realdata, length*8), dtype=np.float64)
array = np.array(tmp_array, dtype=tmp_array.dtype) # copy data
# import json
# with open(name + '.json', 'w') as fh:
# json.dump(list(array), fh)
else:
# for k in range(length):
# value = vector_info.v_compdata[k]
# print(ffi.addressof(value, field='cx_real'), ffi.addressof(value, field='cx_imag'))
# print(" [{}] {} + i {}".format(k, value.cx_real, value.cx_imag))
tmp_array = np.frombuffer(ffi.buffer(vector_info.v_compdata, length*8*2), dtype=np.float64)
array = np.array(tmp_array[0::2], dtype=np.complex128)
array.imag = tmp_array[1::2]
plot[vector_name] = Vector(self, vector_name, vector_type, array)
i += 1
return plot
####################################################################################################
#
# Platform setup
#
NgSpiceShared.setup_platform()