Source code for ngspicepy.ngspicepy

"""The API wrapper for ngspice's shared library."""
import os
import string
from collections import OrderedDict
from ctypes import c_bool, c_char_p, c_double, c_int, c_short,\
    c_void_p, cast, cdll, CFUNCTYPE, create_string_buffer,\
    POINTER, Structure
from queue import Queue

import numpy as np

# Load the ngspice shared library.
# TODO: Figure out the path intelligently
libpath = "/usr/local/lib/libngspice.so.0"
if os.path.isfile(libpath):
    libngspice = cdll.LoadLibrary(libpath)
else:  # pragma: no cover
    raise SystemError('Shared library libngspice.so not found in ' + libpath)

send_char_queue = Queue()
send_stat_queue = Queue()
is_simulating = False


# enums for v_type.
# See src/include/ngspice/sim.h in the ngspice source.
class v_types:
    """Definition for the values for v_types."""

    SV_NOTYPE         = 0
    SV_TIME           = 1
    SV_FREQUENCY      = 2
    SV_VOLTAGE        = 3
    SV_CURRENT        = 4
    SV_OUTPUT_N_DENS  = 5
    SV_OUTPUT_NOISE   = 6
    SV_INPUT_N_DENS   = 7
    SV_INPUT_NOISE    = 8
    SV_POLE           = 9
    SV_ZERO           = 10
    SV_SPARAM         = 11
    SV_TEMP           = 12
    SV_RES            = 13
    SV_IMPEDANCE      = 14
    SV_ADMITTANCE     = 15
    SV_POWER          = 16
    SV_PHASE          = 17
    SV_DB             = 18
    SV_CAPACITANCE    = 19
    SV_CHARGE         = 20


# enums for v_flags.
# See src/include/ngspice/dvec.h in the ngspice source.
class v_flags:
    """Bit fields for v_flags."""

    VF_REAL = (1 << 0)
    VF_COMPLEX = (1 << 1)
    VF_ACCUM = (1 << 2)
    VF_PLOT = (1 << 3)
    VF_PRINT = (1 << 4)
    VF_MINGIVEN = (1 << 5)
    VF_MAXGIVEN = (1 << 6)
    VF_PERMANENT = (1 << 7)

# ngspice scale factors
scale_factors = OrderedDict()
scale_factors['meg'] = 'e6'
scale_factors['t'] = 'e12'
scale_factors['g'] = 'e9'
scale_factors['k'] = 'e3'
scale_factors['m'] = 'e-3'
scale_factors['u'] = 'e-6'
scale_factors['n'] = 'e-9'
scale_factors['p'] = 'e-12'
scale_factors['f'] = 'e-15'


# C structs that are required by the shared library
class ngcomplex_t(Structure):
    """ctypes definition for struct ngcomplex_t."""

    _fields_ = [("cx_real", c_double),
                ("cx_imag", c_double)]


class vector_info(Structure):
    """ctypes definition for struct vector_info."""

    _fields_ = [("v_name", c_char_p),
                ("v_type", c_int),
                ("v_flags", c_short),
                ("v_realdata", POINTER(c_double)),
                ("v_compdata", POINTER(ngcomplex_t)),
                ("v_length", c_int)]


class vecinfo(Structure):
    """ctypes definition for struct vecinfo."""

    _fields_ = [("number", c_int),
                ("vecname", c_char_p),
                ("is_real", c_bool),
                ("pdvec", c_void_p),
                ("pdvecscale", c_void_p)]


class vecinfoall(Structure):
    """ctypes definition for struct vecinfoall."""

    _fields_ = [("name", c_char_p),
                ("title", c_char_p),
                ("date", c_char_p),
                ("type", c_char_p),
                ("veccount", c_int),
                ("vecs", POINTER(POINTER(vecinfo)))]


class vecvalues(Structure):
    """ctypes definition for struct vecvalues."""

    _fields_ = [("name", c_char_p),
                ("creal", c_double),
                ("cimag", c_double),
                ("is_scale", c_bool),
                ("is_complex", c_bool)]


class vecvaluesall(Structure):
    """ctypes definition for struct vecvaluesall."""

    _fields_ = [("veccount", c_int),
                ("vecindex", c_int),
                ("vecsa", POINTER(POINTER(vecvalues)))]


# Callback functions
@CFUNCTYPE(c_int, c_int, c_bool, c_bool, c_int, c_void_p)
def ControlledExit(exit_status, is_unload, is_quit,
                   lib_id, ret_ptr):  # pragma: no cover
    """Callback function called when one exits from ngspice."""
    if not exit_status == 0 or not is_quit:
        raise SystemError('Invalid command or netlist.')
    return 0


@CFUNCTYPE(c_int, c_char_p, c_int, c_void_p)
def SendChar(output, lib_id, ret_ptr):
    """Callback function that captures what's sent by ngspice to stdout."""
    global send_char_queue

    clean_output = "".join(output.decode().split('*'))
    if 'stdout' in clean_output:
        to_print = ' '.join(clean_output.split(' ')[1:]).strip()
        if "ngspice" in to_print and "done" in to_print:  # pragma: no cover
            send_char_queue.put("Quitting ngspice")
        elif "Note: 'quit' asks for detaching ngspice.dll"\
                in to_print:  # pragma: no cover
            pass
        elif to_print not in string.whitespace:
            send_char_queue.put(to_print)
    elif 'stderr' in clean_output:  # pragma: no cover
        if 'warning' not in clean_output.lower():
            raise SystemError(" ".join(clean_output.split(' ')[1:]))
        else:
            send_char_queue.put(clean_output)
    return 0


@CFUNCTYPE(c_int, c_char_p, c_int, c_void_p)
def SendStat(sim_stat, lib_id, ret_ptr):
    """Callback function that captures status messages."""
    send_stat_queue.put(sim_stat.decode())
    return 0


# Initialize ngspice
libngspice.ngSpice_Init(SendChar, SendStat, ControlledExit, None,
                        None, None)

# Specify API argument types and return types
libngspice.ngSpice_Command.argtypes = [c_char_p]
libngspice.ngGet_Vec_Info.argtypes  = [c_char_p]
libngspice.ngSpice_Circ.argtypes    = [POINTER(c_char_p)]
libngspice.ngSpice_AllVecs.argtypes = [c_char_p]
libngspice.ngSpice_Command.restype  = c_int
libngspice.ngSpice_running.restype  = c_int
libngspice.ngGet_Vec_Info.restype   = POINTER(vector_info)
libngspice.ngSpice_Circ.restype     = c_int
libngspice.ngSpice_CurPlot.restype  = c_char_p
libngspice.ngSpice_AllPlots.restype = POINTER(c_char_p)
libngspice.ngSpice_AllVecs.restype  = POINTER(c_char_p)


# Utility functions
def xstr(string):
    """Like str(), except that None is converted to ''."""
    if string is None:
        return ''
    else:
        return str(string)


def to_num(ng_number):
    """Convert an ngspice number to a float.
    
    ng_number - a string containing a number that ngspice recognizes. This can
    either be a float or a number with the appropriate scale factor.
    
    Examples:

    >>> to_num('1.3')
    >>> to_num('1Meg')
    """
    num_text = ng_number.lower()
    for scale_factor in scale_factors:
        if scale_factor in num_text:
            num_text = num_text.replace(scale_factor,
                                        scale_factors[scale_factor])
            break
    try:
        num = float(num_text)
        return num
    except ValueError:
        raise ValueError('Invalid ngspice number: ' + ng_number)


def check_sim_param(start, stop, step=None):
    """Check if start < stop if step is positive and start > stop otherwise.

    Parameters:
        start : int
            Denotes the start point of dc voltage or
            frequecny.
        stop : int
            Denotes the stop point of dc voltage or frequencyor of transient
            analysis.
        step : int
            Denotes the step size of the dc, ac or transient analysis.

    """
    if step is None:
        step = 1
    if step == 0:
        return (False, "step size is zero")
    if step > 0 and stop < start:
        return (False, "step size > 0 but stop < start ")
    if step < 0 and stop > start:
        return (False, "step size < 0 but stop > start")
    return (True, "All good")


def __parse__(sim_cmd, *args, **kwargs):
    """Parse the arguments and check for correctness depending on the simulation chosen .

    Parameters:
        sim_cmd : str
            It denotes the type of the simulation to be run either dc, ac, tran or
            op
        *args 
            A single string containing the source(s) followed by their start, stop
            and step values.
        **kwargs 
            The arguments specified as keyword arguments

    Example:
        ac simulation
            >>> parsed_args = __parse__('ac', 'dec 10 1 10')
        dc simulation
            >>> parsed_args = __parse__('dc', ' v1 0 1 .1')
    """
    cmd_dc = OrderedDict()
    cmd_dc['src'] = ""
    cmd_dc['start'] = ""
    cmd_dc['stop'] = ""
    cmd_dc['step'] = ""
    cmd_dc['src2'] = ""
    cmd_dc['start2'] = ""
    cmd_dc['stop2'] = ""
    cmd_dc['step2'] = ""

    is_parametric = False

    pdc_keys = ['start', 'stop', 'step']
    cmd_ac = OrderedDict()
    cmd_ac['variation'] = ""
    cmd_ac['npoints'] = ""
    cmd_ac['fstart'] = ""
    cmd_ac['fstop'] = ""
    pac_keys = ['fstart', 'fstop', 'npoints']
    cmd_tran = OrderedDict()
    cmd_tran['tstep'] = ""
    cmd_tran['tstop'] = ""
    cmd_tran['tstart'] = "0"
    cmd_tran['tmax'] = ""
    ptran_keys = ['tstart', 'tstop', 'tstep']

    if sim_cmd == 'ac':
        cmd = cmd_ac
        p_keys = pac_keys
        required_args = set(['variation', 'npoints', 'fstart', 'fstop'])
    elif sim_cmd == 'dc':
        cmd = cmd_dc
        p_keys = pdc_keys
        required_args = set(['src', 'start', 'stop', 'step'])
    elif sim_cmd == 'tran':
        cmd = cmd_tran
        p_keys = ptran_keys
        required_args = set(['tstep', 'tstop'])

    # Parse arguments:
    #
    # Case 1:
    # If just one arg is given, assume that the entire string is a
    # command. Separate it out and assign it to the cmd dictionary
    # for error checking.
    if len(args) == 1:
        clean_arg = ' '.join(args[0].split())
        for key, arg in zip(cmd.keys(), clean_arg.split(' ')):
            cmd[key] = xstr(arg)
    else:
        # Case 2:
        # If the simulation args are given as comma separated values,
        # assign them to the dictionary for error checking.
        for key, value in zip(cmd.keys(), args):
            cmd[key] = xstr(value)

    # Case 3:
    # Finally parse the keyword args. Overwrite any args that
    # were already given.
        for key in kwargs:
            if key not in cmd:
                raise KeyError('invalid keyword argument')
            else:
                cmd[key] = xstr(kwargs[key])

    # Check if the arguments were entered correctly:
    #
    # 1. Checks for first source
    # Check if any of the required arguments are empty.
    empty_args = set([key for key in cmd if cmd[key] == ""])
    keys = list(cmd.keys())
    if any(arg in empty_args for arg in required_args):
        missing_args =\
            empty_args.intersection(required_args)
        raise ValueError('Arguments missing: ' +
                         ' '.join(missing_args))

    # 2. Checks for the second source
    #
    # 2a. Arguments of fsecond source given, check if source is given.
    if sim_cmd == 'dc':
        required_args = set([keys[5], keys[6], keys[7]])
        if any(arg not in empty_args for arg in required_args) and\
                cmd['src2'] == "":
            raise ValueError('Second source not specified.')

    # 2b. Second source is specified, check if its required arguments
    # are empty.
        if cmd['src2'] != "":
            required_args = set([keys[5], keys[6], keys[7]])
            if any(arg in empty_args for arg in required_args):
                missing_args = empty_args.intersection(required_args)
                raise ValueError('Arguments missing: ' +
                                 ' '.join(missing_args))
            else:
                is_parametric = True

    # Check if the arguments are correct, i.e., is start < stop if
    # step is positive, is start > stop if step is negative, is
    # start != step?
    start = to_num(cmd[p_keys[0]])
    stop = to_num(cmd[p_keys[1]])
    step = to_num(cmd[p_keys[2]])
    is_good, msg = check_sim_param(start, stop, step)
    if not is_good:
        raise ValueError('Wrong values')
    # Do the same for the second source if it exists.

    if sim_cmd == 'dc':
        if is_parametric:
            start = to_num(cmd['start2'])
            stop = to_num(cmd['stop2'])
            step = to_num(cmd['step2'])
            is_good, msg = check_sim_param(start, stop, step)
            if not is_good:
                raise ValueError('Wrong Values')

    return [cmd[key] for key in cmd if cmd[key] != '']


# User functions
[docs]def send_command(command): """Send a command to ngspice. The argument `command` is string that contains a valid ngspice command. See the chapter 'Interactive Interpreter' of the ngspice manual: http://ngspice.sourceforge.net/docs/ngspice26-manual.pdf """ while not send_stat_queue.empty(): send_stat_queue.get_nowait() libngspice.ngSpice_Command(create_string_buffer(command.encode())) output = [] while not send_char_queue.empty(): output.append(send_char_queue.get_nowait()) return output
[docs]def run_dc(*args, **kwargs): r"""Run a DC simulation on ngspice. Parameters: ``*args`` 1. A single string containing the source(s) followed by their start, stop and step values. 2. src, start, stop, step[, src2, start, stop, step] ``**kwargs`` The arguments specified as keyword arugments src and src2 must be strings. start, stop and step can be either strings or floats. If they are strings, they must contain only a float and optionally one of ngspice's scale factors and no spaces. Examples: >>> run_dc('v1 0 1 0.1') >>> run_dc('v2 0 1 1m v2 0 1 0.3') >>> run_dc('v1', 0, '1meg', '1k') >>> run_dc(src='v1', start=0, stop=1, step=0.1\\ src2='v2', start2=0, step2=0.3, stop2=1) """ parsed_args = __parse__('dc', *args, **kwargs) return send_command('dc ' + ' '.join(parsed_args))
[docs]def run_ac(*args, **kwargs): """Run an AC simulation on ngspice. An AC simulation requires one to specify the start (`fstart`) and stop (`fstop`) frequecies, the type of `variation` (dec/oct/lin) and the number of points (`npoints`; per decade or octave if dec or oct are used) Parameters ``*args`` A single string of the form '<variation> <npoints> <fstart> <fstop>' ``*kwargs`` The arguements in variation, npoints, fstart or fstop specified as keyword arguments Examples: >>> run_ac('dec 10 1 10') >>> run_ac('dec 10 1k 10meg') >>> run_ac('dec', 10, '1k', '100k') >>> run_ac(variation='dec', npoints=0, fstart=1, fstop=10) """ parsed_args = __parse__('ac', *args, **kwargs) return send_command('ac ' + ' '.join(parsed_args))
[docs]def run_tran(*args, **kwargs): """Run a TRAN simulation on ngspice. Parameters: ``*args`` 1. A single string containing tstep, tstop, tstart, tmax and uic values. 2. The values of tmax and uic are optional. 3. tstep, tstop[, tstart, tmax, uic] ``**kwargs`` The arguments in 2 specified as keyword arguments. start, stop and step can be either strings or floats. If they are string, they must contain only a float and optionally one of the ngspice's scale factor ans no spaces. Examples: >>> run_tran('1 10 0 11 ') >>> run_tran('1ns 10ns 0 11ns') >>> run_tran('1ns', 0, '10ns', '11ns') >>> run_tran(tstep=1, tstop=10, tstart=0, tmax=11) """ parsed_args = __parse__('tran', *args, **kwargs) return send_command('tran ' + ' '.join(parsed_args))
[docs]def run_op(): """Run operating point analysis.""" op_result = send_command('op') return op_result
[docs]def clear_plots(*args): """Clear the specified plots names. Parameters: ``*args`` 1. Empty, which will clear all plots. 2. Multiple arguments, each containing the name of a plot (a string). 3. A string containing comma separated names of the plots that need to be deleted. 4. A list or tuple of strings contianing the plots that need to be deleted. Examples: >>> clear_plots() >>> clear_plots('dc dc2 dc3') >>> clear_plots(('dc1','dc2','dc3')) >>> clear_plots('dc1','dc2','dc3') >>> clear_plots(['dc1','dc2','dc3']) """ if len(args) == 0: clear_cmd = 'all' elif len(args) == 1: if type(args[0]) == str: clear_cmd = args[0] elif type(args[0]) == list or type(args[0]) == tuple: clear_cmd = ' '.join(args[0]) else: raise TypeError('Type must be string,list or tuple') else: clear_cmd = ' '.join(args) return send_command('destroy ' + clear_cmd)
[docs]def reset(): """Same as calling clear_plots(). Resets the ngspice environment.""" clear_plots()
[docs]def get_plot_names(): """Return a list of plot names. A plot is the name for a group of vectors. Example: A DC simulation run right after ngspice is loaded creates a plot called dc1 which contains the vectors generated by the DC simulation. """ plot_name_array = libngspice.ngSpice_AllPlots() names_list = [] name = plot_name_array[0] i = 1 while name is not None: names_list.append(name.decode()) name = plot_name_array[i] i += 1 return names_list
[docs]def current_plot(): """Return the name of the current plot.""" plot_name = libngspice.ngSpice_CurPlot() return (plot_name.decode())
[docs]def get_vector_names(plot_name=None): """Return a list of the names of the vectors in the given plot. Parameter: plot_name : str specifies the plot whose vectors need to be returned. If it unspecified, the vector names from the current plot are returned. """ if plot_name is None: plot_name = current_plot() if plot_name not in get_plot_names(): raise ValueError("Given plot name doesn't exist") else: vector_names = libngspice.ngSpice_AllVecs( create_string_buffer(plot_name.encode())) names_list = [] name = vector_names[0] i = 1 while name is not None: names_list.append(name.decode()) name = vector_names[i] i = i + 1 return names_list
[docs]def get_data(vector_arg, plot_arg=None): """Get the data in a vector as a numpy array. Parameters: vector_arg denotes the vector name plot_agr denotes the plot name """ if '.' in vector_arg: plot_name, vector_name = vector_arg.split('.') if vector_name not in get_vector_names(plot_name): raise ValueError("Incorrect vector name") else: if vector_arg not in get_vector_names(plot_arg): raise ValueError("Incorrect vector name") if plot_arg is not None: vector_arg = ".".join([plot_arg, vector_arg]) info = libngspice.ngGet_Vec_Info( create_string_buffer(vector_arg.encode())) if info.contents.v_flags & v_flags.VF_REAL != 0: data = np.ctypeslib.as_array(info.contents.v_realdata, shape=(info.contents.v_length,)) elif info.contents.v_flags & v_flags.VF_COMPLEX != 0: data = np.ctypeslib.as_array( info.contents.v_compdata, shape=(info.contents.v_length,)).view('complex128') return data
[docs]def get_all_data(plot_name=None): """Return a dictionary of all vectors in the specified plot. Parameter: plot_name denotes the plot name """ vector_names = get_vector_names(plot_name) vector_data = {} for vector_name in vector_names: vector_data[vector_name] = get_data(vector_name) return vector_data
[docs]def set_options(*args, **kwargs): """Pass simulator options to ngspice. Parameters: ``*args`` Options can be entered as a string ``**kwargs`` Options can be entered as keyword arguments. Examples: >>> set_options(trtol=1, temp=300) >>> set_options('trtol=1') """ for option in args: return send_command('option ' + str(option)) for option in kwargs: return send_command('option ' + option + '=' + str(kwargs[option]))
[docs]def load_netlist(netlist): """Load ngspice with the specified netlist. Parameters: netlist : str 1. The path to a file that contains the netlist. 2. A list of strings where each string is one line of the netlist. 3. A string containing the entire netlist with each line separated by a newline character. The function does not check if the netlist is valid. An invalid netlist may cause ngspice to crash. """ if type(netlist) == str: if os.path.isfile(netlist): return send_command('source ' + netlist) elif '\n' in netlist: netlist_list = netlist.split('\n') else: raise ValueError('Invalid netlist file or string') elif type(netlist) == list: netlist_list = netlist else: raise TypeError('Netlist format unsupported.\ Must be a string or list') c_char_p_array = c_char_p * (len(netlist_list) + 1) netlist_str = c_char_p_array() for i, line in enumerate(netlist_list): netlist_str[i] = cast(create_string_buffer(line.encode()), c_char_p) netlist_str[len(netlist_list)] = None libngspice.ngSpice_Circ(netlist_str) output = [] while not send_char_queue.empty(): output.append(send_char_queue.get_nowait()) return output