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.