Pascal Schmid's personal website

LTspice Impedances and Transfer Functions from Measurement Data

For my bachelor's project, I have to do some circuit simulations in LTspice. The problem was that impedance measurement data has to be used in the simulations. After searching on how to achieve this for a long time, I finally came across the program bode2spice by Jakub Ladman. But this program can only work with data formatted in a specific way, which is why I adapted it to suit my needs. My adaptation can be found below:

#!/usr/bin/env python3

# Convert measurement data to impedances and transfer functions for LTspice.
#
# Copyright 2021 Pascal Schmid
# Copyright 2018 Jakub Ladman
#
# This program was adapted by Pascal Schmid from Jakub Ladman's
# bode2spice at the end of March 2021.
# The original program can be found at the following site:
# https://github.com/ladmanj/bode2spice
#
# To contact Pascal Schmid, please visit his personal website:
# https://paschmid.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, see <https://www.gnu.org/licenses/>.

import numpy as np


def make_content(frequencies, impedances, inverse=False):
    frequencies = np.array(frequencies)
    impedances = np.array(impedances)

    content = []

    for index, frequency in enumerate(frequencies):
        impedance = impedances[index]

        magnitude = np.abs(impedance)
        phase = np.angle(impedance, deg=True)

        magnitude = 1.0 / magnitude if inverse else magnitude
        phase = (-1.0 if inverse else 1.0) * phase

        # to dB
        magnitude = 20 * np.log10(magnitude)

        content.append("+ ({:.6f}, {:.6f}, {:.6f})".format(frequency, magnitude, phase))

    return "\n".join(content).strip()


def make_impedance_asy(model):
    template = (
        "Version 4\n"
        "SymbolType BLOCK\n"
        "LINE Normal 16 88 16 96\n"
        "LINE Normal 16 16 16 24\n"
        "LINE Normal 27 24 5 24\n"
        "LINE Normal 5 88 5 24\n"
        "LINE Normal 5 88 27 88\n"
        "LINE Normal 27 24 27 88\n"
        "WINDOW 0 31 40 Left 2\n"
        "WINDOW 3 31 76 Left 2\n"
        "SYMATTR Value {model:s}_z\n"
        "SYMATTR Prefix X\n"
        "SYMATTR Description {model:s} impedance\n"
        "SYMATTR ModelFile {model:s}_z.sub\n"
        "PIN 16 16 NONE 0\n"
        "PINATTR PinName A\n"
        "PINATTR SpiceOrder 1\n"
        "PIN 16 96 NONE 0\n"
        "PINATTR PinName B\n"
        "PINATTR SpiceOrder 2\n"
    )
    return template.format(model=model)


def make_impedance_sub(model, frequencies, impedances):
    template = (
        "* {model:s} Impedance\n"
        "*\n"
        ".subckt {model:s}_z 1 2\n"
        "* (freq in Hz, mag in dB, phase in deg)\n"
        "B1 1 2 I=V(1,2) Freq=\n"
        "{content:s}\n"
        ".ends {model:s}_z\n"
    )
    content = make_content(frequencies, impedances, inverse=True)
    return template.format(model=model, content=content)


def make_transfer_function_asy(model):
    template = (
        "Version 4\n"
        "SymbolType BLOCK\n"
        "LINE Normal -48 32 -32 32\n"
        "LINE Normal -32 32 -24 36\n"
        "LINE Normal -48 80 -32 80\n"
        "LINE Normal -32 80 -24 76\n"
        "LINE Normal 0 16 0 24\n"
        "LINE Normal 0 96 0 88\n"
        "LINE Normal -48 72 -40 72\n"
        "LINE Normal -48 40 -40 40\n"
        "LINE Normal -44 36 -44 44\n"
        "LINE Normal -4 72 4 72\n"
        "LINE Normal -4 40 4 40\n"
        "LINE Normal 0 36 0 44\n"
        "CIRCLE Normal -32 24 32 88\n"
        "WINDOW 0 24 16 Left 2\n"
        "WINDOW 3 24 96 Left 2\n"
        "SYMATTR Value {model:s}_tf\n"
        "SYMATTR Prefix X\n"
        "SYMATTR Description {model:s} transfer function\n"
        "SYMATTR ModelFile {model:s}_tf.sub\n"
        "PIN 0 16 NONE 0\n"
        "PINATTR PinName +\n"
        "PINATTR SpiceOrder 1\n"
        "PIN 0 96 NONE 0\n"
        "PINATTR PinName -\n"
        "PINATTR SpiceOrder 2\n"
        "PIN -48 32 NONE 0\n"
        "PINATTR PinName P\n"
        "PINATTR SpiceOrder 3\n"
        "PIN -48 80 NONE 0\n"
        "PINATTR PinName N\n"
        "PINATTR SpiceOrder 4\n"
    )
    return template.format(model=model)


def make_transfer_function_sub(model, frequencies, impedances):
    template = (
        "* {model:s} Transfer Function\n"
        ".subckt {model:s}_tf 1 2 3 4\n"
        "* (freq in Hz, mag in dB, phase in deg)\n"
        "B1 1 2 V=V(3,4) Freq=\n"
        "{content:s}\n"
        ".ends {model:s}_tf\n"
    )
    content = make_content(frequencies, impedances, inverse=False)
    return template.format(model=model, content=content)


def create_models(model, frequencies, impedances):
    files = {
        model + "_z.asy": make_impedance_asy(model),
        model + "_z.sub": make_impedance_sub(model, frequencies, impedances),
        model + "_tf.asy": make_transfer_function_asy(model),
        model + "_tf.sub": make_transfer_function_sub(model, frequencies, impedances),
    }
    for filename in files:
        print(filename)
        with open(filename, "w") as fp:
            fp.write(files[filename])


def main():
    import sys

    model = sys.argv[1]

    data = np.genfromtxt(
        model + ".csv",
        delimiter=",",
        skip_header=1,
    )
    data = np.transpose(data)

    # column 0 = frequency, column 1 = real part, column 2 = imaginary part
    frequencies = data[0]
    impedances = data[1] + 1j * data[2]
    create_models(model, frequencies, impedances)


if __name__ == "__main__":
    main()

Save this code as a file called bode2spice.py.

In the following example, the impedance measurement data is given in a CSV file called example.csv. The first row is ignored and frequency must be given in ascending order.

Frequency [Hz], Real Part [Ohm], Imaginary Part [Ohm]
100,40,90
1000,50,80
10000,60,70

The script can then be run as follows:

python3 ./bode2spice.py example

It outputs four files: example_z.asy, example_z.sub, example_tf.asy, and example_tf.sub. They should be copied to the same folder as the .asc file and will then be available in LTspice.

This blog post by Pascal Schmid is licensed under CC BY-SA 4.0.