Search code examples
pythonpython-3.xpandasimportpspice

Python read LTspice plot export


I'd like to plot some data from LTspice with Python and matplotlib, and I'm searching for a solution to import the exported plot data from LTspice in Python.

I found no way to do this using Pandas, since the format of the data looks like this:

5.00000000000000e+006\t(2.84545891331278e+001dB,8.85405282381414e+001°)

Is there a possibility to import this with Pandas (e.g. with an own dialect) or does someone know a simple workaround (like reading the file line-by-line and extracting the values)?

To make things worse, when exporting the plot of multiple steps, the data is separated by lines like

Step Information: L=410n  (Run: 2/4)

In Java, I may have used a Scanner object to read the data. Is there a similar function in Python or even a simpler way to get the plot data into Python?


Solution

  • I am not familiar with exported plot data from LTspice, so I am assuming that the formatting of the example lines you provided are valid for all times.

    Looking at the IO Tools section of the pandas-0.18 documentation (here), I don't see any ready-to-use parser utility for your data format. The first thing that comes to mind is to do your own parsing and preparing before filling out a pandas dataframe.

    I am assuming the crucial part of your problem is to parse the data file, it's been a while since I played with pandas and matplotlib so expect mistakes relating to those.

    Example

    Here is a quick & dirty python3 script to parse your data into a list of dictionaries, build a pandas dataframe with it and plot it using the DataFrame's plot method. I tried to explain the steps in the comments :

    # ltspice.py
    """ Use it as: 
        > python3 ltspice.py /path/to/datafile """
    
    import pandas
    import sys
    
    data_header = "Time Gain Degree".split()
    
    # Valid line example:
    # 5.00000000000000e+006\t(2.84545891331278e+001dB,8.85405282381414e+001°) 
    
    def parse_line(linestr):
        # ValueError and IndexError exceptions are used to mark the failure of
        # the parse.
        try:
            # First we split at the '\t' character. This will raise ValueError if
            # there is no \t character or there is more than 1 \t
            timestr, rest = linestr.split('\t')
    
            # Then we find the indexes of the '(' and ')' in the rest string.
            parenst, parenend = (rest.find('(')+1,  rest.find(')'))
            if (parenst == -1) or (parenend == -1):
                # find() method returns -1 if nothing is found, I raise ValueError
                # to mark it as a parsing failure
                raise ValueError
    
            # rest[parenst:parenend] returns the string inside parens. split method
            # splits the string into words separated by the given character (i.e.
            # ',')
            powstr, degstr = rest[parenst:parenend].split(',')
    
            # converting strings into floats. Replacing units as necessary.
            time = float(timestr)
            power = float(powstr.replace('dB', ''))
    
            # this will fail with python 2.x
            deg = float(degstr.replace('°', ''))
    
            # You can use dict() instead of tuple()
            return tuple(zip(data_header, (time, power, deg)))
    
        except (ValueError,IndexError) as e:
            return None
    
    
    def fileparser(fname):
        """ A generator function to return a parsed line on each iteration """
        with open(fname, mode='r') as fin:
            for line in fin:
                res = parse_line(line)
                if res is not None:
                    yield res
    
    def create_dataframe(fname):
        p = fileparser(fname)
        # rec is a tuple of 2-tuples that can be used to directly build a python
        # dictionary
        recs = [dict(rec) for rec in p]
        return pandas.DataFrame.from_records(recs)
    
    if __name__ == '__main__':
        data_fname = sys.argv[1]
        df = create_dataframe(data_fname)
    
        ax = df.plot(x='Time', y='Gain')
        fig = ax.get_figure()
        fig.savefig('df.png')
    

    You can copy this code to a text editor and save it as ltspice.py and run it with python3 ltspice.py yourdata.dat from your terminal.

    Note that, parse_line function actually returns a tuple of 2-tuples in the form of ('key', value) where 'key' represents the column name. This value is then used to build the list of dictionaries in the create_dataframe function.

    Extra

    I wrote another script to test the behaviour:

    # test.py
    import random
    from ltspice import fileparser
    
    
    def gen_data():
        time = random.randint(0,100)*1e6
        db = random.lognormvariate(2,0.5)
        degree = random.uniform(0,360)
        # this is necessary for comparing parsed values with values generated
        truncate = lambda x: float('{:.15e}'.format(x))
        return (truncate(time),truncate(db),truncate(degree))
    
    
    def format_data_line(datatpl):
        time, db, degree = datatpl[0], datatpl[1], datatpl[2]
        formatted = "{0:.15e}\t({1:.15e}dB,{2:.15e}°)\n"
        return formatted.format(time, db, degree)
    
    
    def gen_ignore_line():
        tmpl = "Step Information: L={}n  (Run:{}/{})\n"
        l = random.randint(100,1000)
        r2 = random.randint(1,100)
        r1 = random.randint(0,r2)
        return tmpl.format(l,r1,r2)
    
    
    def create_test_file(fname, valid_count, invalid_count):
        """ Creates a test file containing data lines mixed with lines to be
        ignored. Returns the data created.
        valid_count: number of the actual data lines
        invalid_count: number of the to-be-ignored lines
        """ 
        header = 'Time Gain Degree'.split()
        data = []
        formatteddatalines = []
        for i in range(valid_count):
            unfmtdata = gen_data()
            data.append(tuple(zip(header, unfmtdata)))
            formatteddatalines.append(format_data_line(unfmtdata))
    
        invalidlines = []
        for i in range(invalid_count):
            invalidlines.append(gen_ignore_line())
    
        lines = formatteddatalines + invalidlines
        random.shuffle(lines)
        with open(fname, mode='w') as fout:
            fout.writelines(lines)
    
        return data
    
    if __name__ == '__main__':
        fname = 'test.data'
        validcnt = 10
        invalidcnt = 2
    
        validdata = create_test_file(fname, validcnt, invalidcnt)
        parseddata = [data for data in fileparser(fname)]
    
        # Note: this check ignores duplicates.
        assert(set(validdata) == set(parseddata))