ngspice raw and numpy

Numpy makes it almost trivial to read ngspice raw binary files into python:

# MIT license: https://opensource.org/licenses/MIT
# See https://github.com/Isotel/mixedsim/blob/master/python/ngspice_read.py
# for a more complete library. Isotel's version is GPL licensed
from __future__ import division
import numpy as np
BSIZE_SP = 512 # Max size of a line of data; we don't want to read the
# whole file to find a line, in case file does not have
# expected structure.
MDATA_LIST = [b'title', b'date', b'plotname', b'flags', b'no. variables',
b'no. points', b'dimensions', b'command', b'option']
def rawread(fname: str):
"""Read ngspice binary raw files. Return tuple of the data, and the
plot metadata. The dtype of the data contains field names. This is
not very robust yet, and only supports ngspice.
>>> darr, mdata = rawread('test.py')
>>> darr.dtype.names
>>> plot(np.real(darr['frequency']), np.abs(darr['v(out)']))
"""
# Example header of raw file
# Title: rc band pass example circuit
# Date: Sun Feb 21 11:29:14 2016
# Plotname: AC Analysis
# Flags: complex
# No. Variables: 3
# No. Points: 41
# Variables:
# 0 frequency frequency grid=3
# 1 v(out) voltage
# 2 v(in) voltage
# Binary:
fp = open(fname, 'rb')
plot = {}
count = 0
arrs = []
plots = []
while (True):
try:
mdata = fp.readline(BSIZE_SP).split(b':', maxsplit=1)
except:
raise
if len(mdata) == 2:
if mdata[0].lower() in MDATA_LIST:
plot[mdata[0].lower()] = mdata[1].strip()
if mdata[0].lower() == b'variables':
nvars = int(plot[b'no. variables'])
npoints = int(plot[b'no. points'])
plot['varnames'] = []
plot['varunits'] = []
for varn in range(nvars):
varspec = (fp.readline(BSIZE_SP).strip()
.decode('ascii').split())
assert(varn == int(varspec[0]))
plot['varnames'].append(varspec[1])
plot['varunits'].append(varspec[2])
if mdata[0].lower() == b'binary':
rowdtype = np.dtype({'names': plot['varnames'],
'formats': [np.complex_ if b'complex'
in plot[b'flags']
else np.float_]*nvars})
# We should have all the metadata by now
arrs.append(np.fromfile(fp, dtype=rowdtype, count=npoints))
plots.append(plot)
fp.readline() # Read to the end of line
else:
break
return (arrs, plots)
if __name__ == '__main__':
arrs, plots = rawread('test.raw')
print(arrs)
# Local Variables:
# mode: python
# End:
view raw rawread.py hosted with ❤ by GitHub

2 thoughts on “ngspice raw and numpy

  1. THANK YOU! This is the nicest way of reading SPICE .raw files I’ve seen. Though you use ngspice, your method solves a problem for LTspice users. In LTspice transient simulations, only time is saved as a double (64 bit float) with the data for the traces saved as singles (32 bit floats). Until I saw your post I was reading into numpy a single float at a time to cope with the varying types: your approach can read in milliseconds what takes minutes that way! Here’s a modification of your code that detects LTspice and deals with transient simulations accordingly (it’s also changed to work in Python 3). Thanks again!

    “””From https://grokcircuits.com/author/snmishra/ Posted on February 28, 2016
    Retrieved 26/6/16
    Modifed by Adrian Thompson to work with LTspice, tested for transient simulations only.
    ngspice stores time and data as doubles (64 bit float), whereas in binary mode (as distinct from ASCII) LTspice stores only time as 64-bit, and the traces are singles (32 bit float).
    NB. LTSpice allegedly uses a unique format for .STEP simulations, but that is not addressed here.
    NB2. With reference to Paul Wagner’s LTspice2Matlab matlab code, the “Offset” field of metadata in LTspice is a constant to be added to all times or frequencies. I’ve seen it to be the value of Tstart if it is given for a .TRAN simulation. It would be easy to process this if required.

    Changes made for Python 3.5 (original code is version <3):
    1. Removed “from future import division”
    2. Changed to a context manager arrangement for reading files (not necessary)
    3. Adjusted throughout for changes in the way python reads binary files

    Changes made for LTspice:
    1. Added the “if ‘ltspice’ in plot[‘command’]:” section (the “else:” is the original code)

    Changed for simplification/personal requirements:
    1. Removed the BSIZE_SP guard on line length. Files generated by SPICEs should be OK; if not we want something to go wrong.
    2. Convert all metadata to lower case at the point of reading. SPICE netlists are case-insensitive and we don’t want confusion, especially with the variable names that will become case sensitive as field names in the numpy structured arrays.
    3. Updated rawread() docstring to mention LTspice and attempt to fix an error in the example usage (rawread returns lists)
    “””

    import numpy as np

    MDATA_LIST = [‘title’, ‘date’, ‘plotname’, ‘flags’, ‘no. variables’, ‘no. points’, ‘dimensions’, ‘command’, ‘option’]

    def rawread(fname: str):
    “””Read ngspice / LTspice binary raw files. Return tuple of a list of the data, and a list of the
    plot metadata. The dtype of the data contains field names. This is not very robust yet. For LTspice, only
    transient simulations are implemented and tested, though it is obvious how to easily extend to ac
    analysis as is done for ngspice.

    data_list, metadata_list = rawread(‘test.cir’)
    first_plot = data_list[0]
    plot(np.real(first_plot[‘frequency’]), np.abs(first_plot[‘v(out)’]))
    “””
    # Example header of ngspice raw file
    # Title: rc band pass example circuit
    # Date: Sun Feb 21 11:29:14 2016
    # Plotname: AC Analysis
    # Flags: complex
    # No. Variables: 3
    # No. Points: 41
    # Variables:
    # 0 frequency frequency grid=3
    # 1 v(out) voltage
    # 2 v(in) voltage
    # Binary:

    plot = {}
    count = 0
    arrs = []
    plots = []
    with open(fname, 'rb') as fp:
        for line in fp:
            mdata = line.decode().lower().split(':', maxsplit=1)
            if len(mdata) == 2:
                if mdata[0] in MDATA_LIST:
                    plot[mdata[0]] = mdata[1].strip()
                if mdata[0] == 'variables':
                    nvars = int(plot['no. variables'])
                    npoints = int(plot['no. points'])
                    plot['varnames'] = []
                    plot['varunits'] = []
                    for varn in range(nvars):
                        varspec = fp.readline().decode().lower().strip().split()
                        assert(varn == int(varspec[0]))
                        plot['varnames'].append(varspec[1])
                        plot['varunits'].append(varspec[2])
                if mdata[0] == 'binary':
                    if 'ltspice' in plot['command']:
                        if 'complex' in plot['flags']:
                            print("Complex data in LTspice untested, but shouldn't be difficult.")
                            exit()
                        else:
                            #Assume transient analysis: the only thing tested for LTspice
                            if 'nocompression' not in plot['flags']:  #Should we also check this for non-LTspice's?
                                print('Turn off compression by adding ".options plotwinsize=0" to the netlist')
                                assert False
                            assert 'real' in plot['flags']
                            assert plot['varnames'][0] == 'time'
                            assert plot['varunits'][0] == 'time'
                            #time is the first item and in LTspice is float64 with the traces being only float32
                            rowdtype = np.dtype({'names': plot['varnames'],
                                                 'formats': [np.float64] + [np.float32]*(nvars-1)})
                    else:
                        rowdtype = np.dtype({'names': plot['varnames'],
                                             'formats': [np.complex_ if 'complex' in plot['flags'] else np.float_]*nvars})
                    # We should have all the metadata by now
                    arrs.append(np.fromfile(fp, dtype=rowdtype, count=npoints))
                    plots.append(plot)
                    fp.readline() # Read to the end of line
            else:
                break
    return (arrs, plots)
    

    Like

    1. Adrian,

      Thanks a lot for adapting this to LTSpice. I have one quibble with your modifications. I’d keep the BSIZE_SP argument to readline just to avoid crashing the script from reading in a very large malformed file. The limit is generous enough that well-formed files should be fine. The limited readline will still give an error if the file is not well-formed.

      Cheers,
      Satya

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s