This is something I put together to automate and analyze my LTspice simulations in Python.
There are some existing libraries out there for this, such as this more fully-featured project of the same name, but at the time I couldn't find one that worked for me.
This project takes a lightweight, functional approach. It is structured as a single module (.py file) that contains a set of functions that output SPICE code, operate on netlists, dispatch LTspice, and parse the various output files.
This package is not registered on PyPI. To install,
- Clone/download this repo
- Run
pip install .
inside the pyltspice directory.
You will also need to install LTspice (XVII or later).
import pyltspice
from pyltspice import *
Simulation files will be stored at C:\Users\t\ltspice_sims. To change, overwrite the simfolder variable
Here's how you can change the output directory:
pyltspice.simfolder = r'C:\Users\t\ltspice_sims'
We represent netlists in python as a list of strings. You may write them directly in python or read them from a file using the function netlist_fromfile()
.
I drew this very interesting example circuit in the LTspice GUI:
We work with SPICE netlists, not the PCB manufacturing formats now in the export options of recent LTspice versions. For those, you can get a SPICE netlist from the GUI using View → Spice NETLIST
, then copy-paste the text.
The netlist for the circuit above is included in this repository, and its path is stored in the variable netlist_path
netlist = netlist_fromfile(netlist_path)
netlist
['* Example netlist',
'V1 in 0 PULSE(0 5 1m 1n 1n 10m)',
'R1 in N001 {R}',
'L1 N001 N002 {L}',
'C1 N002 0 {C}',
'.PARAM C=1e-6',
'.PARAM L=1e-3',
'.PARAM R=1',
'.tran 0 10m 0',
'.backanno',
'.end']
These netlists can be passed to the function runspice()
, which writes the netlist to a file, executes it with LTspice,
reads the output binary file and returns the simulation results as a dictionary.
data = runspice(netlist)
Writing C:\Users\t\ltspice_sims\2024-09-03_161937_288_Example_netlist.net
Executing C:\Users\t\ltspice_sims\2024-09-03_161937_288_Example_netlist.net
Reading C:\Users\t\ltspice_sims\2024-09-03_161937_288_Example_netlist.raw
Reading C:\Users\t\ltspice_sims\2024-09-03_161937_288_Example_netlist.log
Reading C:\Users\t\ltspice_sims\2024-09-03_161937_288_Example_netlist.net
The returned data combines information from the output .log file and the binary (.raw) file, and also includes the netlist and other metadata.
Here's what it looks like:
# pandas used here just for prettier printing
import pandas as pd
pd.Series(data)
filepath C:\Users\t\ltspice_sims\2024-09-03_161937_288_...
Title * Example netlist
Date Tue Sep 3 16:19:37 2024
Plotname Transient Analysis
Flags real forward
No. Variables 8
No. Points 932
Offset 0.0000000000000000e+00
Command Linear Technology Corporation LTspice
time [0.0, 0.001, 0.0010000001606947238, 0.00100000...
V(in) [0.0, 0.0, 0.8034736, 1.6069472, 3.1470428, 5....
V(n001) [0.0, 0.0, 0.80347353, 1.606947, 3.1470416, 4....
V(n002) [0.0, 0.0, 8.4442574e-12, 5.3361017e-11, 3.171...
I(C1) [0.0, 0.0, 7.907755e-08, 3.191806e-07, 1.13626...
I(L1) [0.0, 0.0, 7.907755e-08, 3.191806e-07, 1.13626...
I(R1) [0.0, 0.0, 7.907755e-08, 3.191806e-07, 1.13626...
I(V1) [0.0, 0.0, -7.907755e-08, -3.191806e-07, -1.13...
netlist [* Example netlist, V1 in 0 PULSE(0 5 1m 1n 1n...
C 0.000001
L 0.001
R 1.0
sim_time 0.007
solver Normal
method modified trap
WARNING None
sim_time_total 0.414094
dtype: object
Note that there is some overhead to do the simulation including the file IO:
dt = data['sim_time_total'] - data['sim_time']
print(f'Extra time needed: {dt:.3f} s')
Extra time needed: 0.407 s
Repeated runs of the same netlist will use the already existing result, which will be faster.
data = runspice(netlist)
Reading the previous result of a matching simulation from disk
Reading C:\Users\t\ltspice_sims\2024-09-03_161937_288_Example_netlist.raw
Reading C:\Users\t\ltspice_sims\2024-09-03_161937_288_Example_netlist.log
Reading C:\Users\t\ltspice_sims\2024-09-03_161937_288_Example_netlist.net
Plot the results
%matplotlib inline
from matplotlib import pyplot as plt
t = data['time']
I = data['I(R1)']
plt.figure(figsize=(4,2))
plt.plot(t * 1e3, I * 1e3)
plt.xlabel('Time [ms]')
plt.ylabel('Current [mA]');
If you are annoyed by the print statements, you can deactivate them like this:
pyltspice.verbose = False
From here, you could just modify the netlists directly through list and string operations.
But we also have a set of python functions that evaluate to spice code (strings). These should be a little more convenient than writing the spice directly. Only some of the common spice commands are implemented currently, but you are welcome to extend it.
For example:
resistor(num=1, cathode=0, anode='N001', val=300)
'R1 0 N001 300'
sine(freq=1e3, amp=2, delay=1e-3)
'SINE(0 2 1000.0 0.001 0 0 )'
There are also functions for inserting new commands into existing netlists.
The main function to use is netchange()
, which takes a netlist as the first argument and incorporates more lines passed as additional arguments. For each input line, it decides whether to add a new line to the netlist or to overwrite an existing line. You can pass it individual lines as strings, partial netlists as lists of strings, or both.
Input netlists are never modified. Rather, new netlists are always returned by the functions.
For example, to change a parameter,
netchange(netlist, R=3.3)
['* Example netlist',
'V1 in 0 PULSE(0 5 1m 1n 1n 10m)',
'R1 in N001 {R}',
'L1 N001 N002 {L}',
'C1 N002 0 {C}',
'.PARAM C=1e-6',
'.PARAM L=1e-3',
'.PARAM R=3.3',
'.tran 0 10m 0',
'.backanno',
'.end']
to add a circuit element,
netchange(netlist, resistor(2, 'in', 0, 50))
['* Example netlist',
'V1 in 0 PULSE(0 5 1m 1n 1n 10m)',
'R1 in N001 {R}',
'R2 in 0 50',
'L1 N001 N002 {L}',
'C1 N002 0 {C}',
'.PARAM C=1e-6',
'.PARAM L=1e-3',
'.PARAM R=1',
'.tran 0 10m 0',
'.backanno',
'.end']
You can add as many lines as you want:
newnet = netchange(netlist,
'* test',
capacitor(1, 'N003', 0, '{C}'),
voltage_source(2, 'N002', 'N003', pulse(0, 4, 1e-9, 1e-3, 1e-9, delay=2e-3)),
transient(0, 5e-3),
L=2e-3,
C=2e-6
)
print('\n'.join(newnet))
newdata = runspice(newnet)
t = newdata['time']
I = newdata['I(R1)']
plt.figure(figsize=(4,2))
plt.plot(t * 1e3, I * 1e3)
plt.xlabel('Time [ms]')
plt.ylabel('Current [mA]');
* test
V1 in 0 PULSE(0 5 1m 1n 1n 10m)
V2 N002 N003 PULSE(0 4 0.002 1e-09 1e-09 0.001 0.003000002 )
R1 in N001 {R}
L1 N001 N002 {L}
C1 N003 0 {C}
.PARAM C=2e-06
.PARAM L=0.002
.PARAM R=1
.tran 0 0.005 0 0.0001
.backanno
.end
Now we can automate our simulation runs and get back a useful data structure for further analysis.
For example, here we vary all the parameters by ±50%:
datalist = []
for name, value in get_params(netlist).items():
for v in np.linspace(0.5 * value, 1.5 * value, 5):
data = runspice(netchange(netlist, param(name, v)));
datalist.append(data)
pd.DataFrame(datalist)[['R', 'L', 'C', 'sim_time', 'time', 'I(R1)']]
R | L | C | sim_time | time | I(R1) | |
---|---|---|---|---|---|---|
0 | 1.00 | 0.00100 | 5.000000e-07 | 0.008 | [0.0, 0.001, 0.0010000001606947238, 0.00100000... | [0.0, 0.0, 7.907755e-08, 3.191806e-07, 1.13626... |
1 | 1.00 | 0.00100 | 7.500000e-07 | 0.008 | [0.0, 0.001, 0.0010000001606947238, 0.00100000... | [0.0, 0.0, 7.907755e-08, 3.191806e-07, 1.13626... |
2 | 1.00 | 0.00100 | 1.000000e-06 | 0.014 | [0.0, 0.001, 0.0010000001606947238, 0.00100000... | [0.0, 0.0, 7.907755e-08, 3.191806e-07, 1.13626... |
3 | 1.00 | 0.00100 | 1.250000e-06 | 0.009 | [0.0, 0.001, 0.0010000001606947238, 0.00100000... | [0.0, 0.0, 7.907755e-08, 3.191806e-07, 1.13626... |
4 | 1.00 | 0.00100 | 1.500000e-06 | 0.009 | [0.0, 0.001, 0.0010000001606947238, 0.00100000... | [0.0, 0.0, 7.907755e-08, 3.191806e-07, 1.13626... |
5 | 1.00 | 0.00050 | 1.000000e-06 | 0.009 | [0.0, 0.001, 0.0010000000229959374, 0.00100000... | [0.0, 0.0, 3.2065541e-09, 6.207098e-09, 1.5922... |
6 | 1.00 | 0.00075 | 1.000000e-06 | 0.009 | [0.0, 0.001, 0.001000000160694725, 0.001000000... | [0.0, 0.0, 1.0543673e-07, 4.255741e-07, 1.5150... |
7 | 1.00 | 0.00100 | 1.000000e-06 | 0.008 | [0.0, 0.001, 0.0010000001606947238, 0.00100000... | [0.0, 0.0, 7.907755e-08, 3.1918057e-07, 1.1362... |
8 | 1.00 | 0.00125 | 1.000000e-06 | 0.007 | [0.0, 0.001, 0.001000000160694725, 0.001000000... | [0.0, 0.0, 6.326204e-08, 2.5534447e-07, 9.0901... |
9 | 1.00 | 0.00150 | 1.000000e-06 | 0.008 | [0.0, 0.001, 0.0010000001606947245, 0.00100000... | [0.0, 0.0, 5.2718367e-08, 2.1278707e-07, 7.575... |
10 | 0.50 | 0.00100 | 1.000000e-06 | 0.008 | [0.0, 0.001, 0.001000000160694724, 0.001000000... | [0.0, 0.0, 7.907756e-08, 3.1918063e-07, 1.1362... |
11 | 0.75 | 0.00100 | 1.000000e-06 | 0.008 | [0.0, 0.001, 0.001000000160694725, 0.001000000... | [0.0, 0.0, 7.907755e-08, 3.191806e-07, 1.13626... |
12 | 1.00 | 0.00100 | 1.000000e-06 | 0.007 | [0.0, 0.001, 0.0010000001606947238, 0.00100000... | [0.0, 0.0, 7.907755e-08, 3.191806e-07, 1.13626... |
13 | 1.25 | 0.00100 | 1.000000e-06 | 0.008 | [0.0, 0.001, 0.0010000001606947247, 0.00100000... | [0.0, 0.0, 7.907755e-08, 3.1918057e-07, 1.1362... |
14 | 1.50 | 0.00100 | 1.000000e-06 | 0.008 | [0.0, 0.001, 0.0010000001606947253, 0.00100000... | [0.0, 0.0, 7.907755e-08, 3.1918057e-07, 1.1362... |
plt.figure(figsize=(4,2))
plt.xlabel('Time [ms]')
plt.ylabel('Current [mA]');
for data in datalist:
plt.plot(data['time'] * 1e3, data['I(R1)'] * 1e3)
plt.xlim(0.9, 5)
(0.9, 5.0)
I would not call this production-ready. If in doubt, use PyLTSpice.
We have had some success using multiprocessing to increase the speed when running many simulations, but that code is not included yet.
Error handling is not very sophisticated. If you write an error in your netlist, sometimes python just hangs while it waits for LTspice to respond. In this case, use task manager to end the SPICE Simulator.