-
Notifications
You must be signed in to change notification settings - Fork 3
/
LTspice_opt.py
1227 lines (997 loc) · 47.4 KB
/
LTspice_opt.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
## Python optimizer for LTspice, Bob Adams, Fellow Emeritus, Analog Devices 2023 (C)
## optimizer will adjust selected schematic components (designated in the setup file function 'simControl')
## in an attempt to match the target response, set in the setup file function 'setTarget'
## Note, the schematic name is read from the simControl function
## which is imported from the setup python file (see line below)
## The setup file must be generated by the user for a particular LTspice schematic and sim
## See any of the example files in this distribution for an example
## Un-comment 1 of the following to run the example, or import your own setup file
# from example1_setup import simControl, setTarget
from example2_setup import simControl, setTarget
# from hilbert_example_setup import simControl, setTarget
# from testTran_setup import simControl, setTarget
# notes:
# the simControl function sets the following;
# ** paths to the LTspice executable
# ** working LTspice directory
# ** schematic instance names and/or parameter names to be optimized
# ** min and max values of those instances or parameters
# ** tolerance of those instances
# ** match mode (amplitude only, phase only, or both)
# ** The max number of iterations for both the particle swarm algorithm and the least-squares algorithm
# the setTarget function sets the target amplitude and/or phase response
# It is calculated at the same frequencies as used in
# the .ac spice control line in the schematic
# It also sets the error weights; if you want more precise matching in some frequency
# regions, you can increase the error weights in that frequency region. If the specification style is
# passband/don't-care band/stopband, then the error weights can be set to be constant within those 3 frequency regions (see example2)
import numpy as np
from numpy import random
import matplotlib.pyplot as plt
import os
import subprocess
import time
import sys
import csv
# import re # regular expressions
from itertools import chain
import hashlib
import logging
from PyLTSpice import RawRead # user must install into env from https://pypi.org/project/PyLTSpice/
from PyLTSpice import SimRunner
from scipy.optimize import least_squares
import pyswarms as ps
from pyswarms.utils.functions import single_obj as fx
from myPlots import myPlot_1x,myPlot_2x,myPlot_3x,myPlot_2x_errweights,myPlot_1x_errweights
print('******\n******\nCopyright (C) Robert Adams 2024\n******\n******')
# LTspice Optimizer
# Copyright (C) Robert Adams 2023
# 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 <http://www.gnu.org/licenses/>.
plt.interactive(True)
# Initialize global variables
spiceSimCount_lsq = 0 # spice sim count for the least-squares
iterationCount_ps = 0 # iteration count for the particle swarm
passCellDict = {} # empty dict
spiceSimCount = 1 # keeps track of cummulative spice sims so we know which result fle to parse
# ******************************* functions ***************************
def pswarm(optParams): # this is the evaluation function called particle swarm
global passCellDict
global iterationCount_ps
global spiceSimCount
global restartCount
LTspice_outputfile = passCellDict['LTspice_outputfileD']
LTspice_output_expr = passCellDict['LTspice_output_exprD']
matchMode = passCellDict['matchModeD']
target = passCellDict['targetD']
errWeights = passCellDict['errWeightsD']
nomParams = passCellDict['nomParamsD']
numOptd = passCellDict['numOptdD']
LTC = passCellDict['LTC_D']
netlist_fname = passCellDict['netlist_fnameD']
fileName = passCellDict['fileNameD']
numParticles = len(optParams)
rmserrRet = np.zeros(numParticles)
bestErr = 1e6
bestKK = 0
for kk in range(numParticles): # this is called with a vector of all particle positions at once
componentVal = np.zeros(numOptd)
for k in range(numOptd):
componentVal[k] = optParams[kk,k] * nomParams[k] # de-normalize
update_netlist(passCellDict,componentVal)
# time.sleep(0.02)
LTR = runMySim(LTC, fileName, netlist_fname,False)
outNode = LTR.get_trace(LTspice_output_expr)
fresp = np.abs(outNode)
freqx = LTR.get_trace('frequency')
freqx = np.abs(freqx)
if matchMode == 2:
optCurrent = np.unwrap(np.angle(outNode))
if matchMode == 1: # ampl only
optCurrent = fresp
# if matchMode == 3: # ampl and phase
# optCurrent = np.concatenate((fresp, phase))
if len(target) != len(optCurrent):
print('ERROR, something went wrong with the LTspice sim...')
sys.exit()
err = target - optCurrent # error between target and current response
err = err * errWeights # apply frequency-dependent optimization
rmsErr = np.sqrt(np.mean(err ** 2))
if rmsErr < bestErr:
bestErr = rmsErr
bestKK = kk
rmserrRet[kk] = rmsErr
print(f'Particle Swarm best kk, rms error = {bestKK}, {bestErr}')
return rmserrRet
def optLTspice(optParams, *args, **kwargs): # this is the evaluation function called by sciPy least-squares
global spiceSimCount_lsq # the iterCount_lsq is derived from the spice sim count
global spiceSimCount
global restartCount
netlist_fname = kwargs['netlist_fnameD']
LTspice_outputfile = kwargs['LTspice_outputfileD']
LTspice_output_expr = kwargs['LTspice_output_exprD']
matchMode = kwargs['matchModeD']
target = kwargs['targetD']
errWeights = kwargs['errWeightsD']
fileName = kwargs['fileNameD']
# numlines_netlist = kwargs['numlines_netlistD']
# netlist = kwargs['netlistD']
nomParams = kwargs['nomParamsD']
# OptLine = kwargs['OptLineD']
numOptd = kwargs['numOptdD']
maxIter_lsq = kwargs['maxIter_lsqD']
LTC = kwargs['LTC_D']
componentVal = np.zeros(numOptd)
for k in range(numOptd):
componentVal[k] = optParams[k]*nomParams[k] # de-normalize
update_netlist(passCellDict,componentVal)
# time.sleep(0.1)
LTR = runMySim(LTC, fileName, netlist_fname,False)
outNode = LTR.get_trace(LTspice_output_expr)
fresp = np.abs(outNode)
freqx = LTR.get_trace('frequency')
freqx = np.abs(freqx)
if matchMode == 1: # ampl only
optCurrent = fresp
if matchMode == 2: # phase only
optCurrent = np.unwrap(np.angle(outNode))
if len(target) != len(optCurrent):
print('ERROR, something went wrong with the LTspice sim...')
sys.exit()
err = target - optCurrent # error between target and current response
err = err * errWeights # apply frequency-dependent optimization
iterationCount_lsq = int(spiceSimCount_lsq/(numOptd+1))
if iterationCount_lsq > maxIter_lsq: # set error to large # to stop the algorithm
err.fill(1e3)
printme = 'Stopping ...'
print('\r', printme, end="")
else:
rmsErr = np.sqrt(np.mean(err ** 2))
# printme = "least-sq spice sim count, rms Err = " + str(spiceSimCount_lsq) + " " + str(rmsErr)
printme = "least-sq iteration count, rms Err = " + str(iterationCount_lsq) + " " + str(rmsErr)
# print('\r',printme, end = "")
print(printme)
spiceSimCount_lsq += 1
return err
def update_netlist(kwargs,componentVal) :
netlist_fname = kwargs['netlist_fnameD']
numlines_netlist = kwargs['numlines_netlistD']
netlist = kwargs['netlistD']
OptLine = kwargs['OptLineD']
numOptd = kwargs['numOptdD']
paramFlag = kwargs['paramFlagD']
# print('\ncurrent component values')
for k in range(numOptd): # only write the opt lines to the in-memory netlist
if paramFlag[k] == 0:
netlist[OptLine[k]][3] = f'{componentVal[k]:.12e}'
# print(f'{netlist[OptLine[k]][0]} {componentVal[k]:.12e}')
else:
netlist[OptLine[k]][2] = f'{componentVal[k]:.12e}'
# print(f'{netlist[OptLine[k]][1]} {componentVal[k]:.12e}')
with open(netlist_fname, 'w') as fid_wr_netlist:
for k in range(numlines_netlist):
thisLine = netlist[k]
fid_wr_netlist.write(' '.join(thisLine) + '\n')
return
# **********************************************************
# reads in the original schematic and replaces the instance values
# with the optimized insatnce values
def update_schematic(pass2schem, optParams):
numOptd = pass2schem['numOptdD']
OptLine = pass2schem['OptLineD']
nomParams = pass2schem['nomParamsD']
netlist = pass2schem['netlistD']
filePath = pass2schem['filePathD']
fileName = pass2schem['fileNameD']
simctrlInstTol = pass2schem['simControlInstTolD']
simctrlOptInstNames = pass2schem['simControlOptInstNamesD']
# Read in schematic to update
schem_fname = os.path.join(filePath, fileName + '.asc')
with open(schem_fname, 'r') as file:
schem = file.readlines()
changeNext = False
roundStringNext = 'E96'
new_schem = []
optFile = open("optParams.txt", "w")
optFile.write("InstName\t\t OptValue\t\t quantizedValue\n")
for line in schem:
line = line.strip().split()
if changeNext: # previous line was something like 'SYMATTR InstName R1' current line is like 'SYMATTR Value 600'
newVal = round63(instValNext, roundStringNext)
newVal = newVal[0].astype('float') # change from type ndarray to single float
line[2] = f'{newVal:.3e}'
print(f'Inst, opt val, quantized val = {instNm} {instValNext} {newVal:.3e}')
optFile.write(f'{instNm}\t\t{instValNext}\t\t{newVal:.3e}\n')
changeNext = False
if line[1] == 'InstName':
instNm = line[2]
changeNext = False
for kk in range(numOptd):
if netlist[OptLine[kk]][0] == instNm:
# Find the index to this instance in simctrlOptInstNames
# so that we know which tolerance to use
xx = simctrlOptInstNames.index(instNm)
# Next line has the value to change
changeNext = True
instValNext = optParams[kk] * nomParams[kk] # de-normalize
roundStringNext = simctrlInstTol[xx]
# test if there is a .param line, like 'TEXT -40 -88 Left 2 !.param C2VAL 1.3nf'
testParam = '!.param'
if testParam in line:
idx = line.index(testParam)+1
if len(line) == (idx + 1): # new style schem with =, split to create a stand-alone value field
try:
temp = line[idx].split('=')
line[idx] = temp[0]
line.append(temp[1])
# print('new Line = ',Line)
except:
pass
if testParam in line:
idx = line.index(testParam) # next position in line holds value of param
paramName = line[idx+1]
for kk in range(numOptd):
if netlist[OptLine[kk]][1] == paramName:
xx = simctrlOptInstNames.index(paramName) # find position in simcontrol list so we can get the tolerance
roundString = simctrlInstTol[xx] # the E series tolerance
instVal = optParams[kk] * nomParams[kk]
newVal = round63(instVal, roundString)
newVal = newVal[0].astype('float') # change from type ndarray to single float
line[idx + 2] = f'{newVal:.3e}'
print(f'Param, opt val, quantized val = {paramName} {instVal} {newVal:.3e}')
optFile.write(f'{paramName}\t\t{instVal}\t\t{newVal:.3e}\n')
new_schem.append(' '.join(line) + '\n')
optFile.close()
# Write new schem file
schem_fname = os.path.join(filePath, fileName + '_opt.asc')
with open(schem_fname, 'w') as file:
file.writelines(new_schem)
# **********************************************************
# function to round component values to tolerance defined by "E-series"
# (c) 2014-2022 Stephen Cobeldick, converted from Matlab distribution
def round63(X, ser, rnd=None):
# Constants for E-Series
E_SERIES = {
'E3': np.array([100, 220, 470]), # 40% tol
'E6': np.array([100, 150, 220, 330, 470, 680]), # 20% tol
'E12': np.array([100, 120, 150, 180, 220, 270, 330, 390, 470, 560, 680, 820]), #10%
'E24': np.array([ # 5% tol
100, 110, 120, 130, 150, 160, 180, 200, 220, 240, 270, 300,
330, 360, 390, 430, 470, 510, 560, 620, 680, 750, 820, 910
]),
'E48': np.array([ # 2% tol
100, 105, 110, 115, 121, 127, 133, 140, 147, 154, 162, 169,
178, 187, 196, 205, 215, 226, 237, 249, 261, 274, 287, 301,
316, 332, 348, 365, 383, 402, 422, 442, 464, 487, 511, 536,
562, 590, 619, 649, 681, 715, 750, 787, 825, 866, 909, 953
]),
'E96': np.array([ # 1% tol
100, 102, 105, 107, 110, 113, 115, 118, 121, 124, 127, 130,
133, 137, 140, 143, 147, 150, 154, 158, 162, 165, 169, 174,
178, 182, 187, 191, 196, 200, 205, 210, 215, 221, 226, 232,
237, 243, 249, 255, 261, 267, 274, 280, 287, 294, 301, 309,
316, 324, 332, 340, 348, 357, 365, 374, 383, 392, 402, 412,
422, 432, 442, 453, 464, 475, 487, 499, 511, 523, 536, 549,
562, 576, 590, 604, 619, 634, 649, 665, 681, 698, 715, 732,
750, 768, 787, 806, 825, 845, 866, 887, 909, 931, 953, 976
]),
'E192': np.array([ # 1/2 % tolerance
100, 101, 102, 104, 105, 106, 107, 109, 110, 111, 113, 114,
115, 117, 118, 120, 121, 123, 124, 126, 127, 129, 130, 132,
133, 135, 137, 138, 140, 142, 143, 145, 147, 149, 150, 152,
154, 156, 158, 160, 162, 164, 165, 167, 169, 172, 174, 176,
178, 180, 182, 184, 187, 189, 191, 193, 196, 198, 200, 203,
205, 208, 210, 213, 215, 218, 221, 223, 226, 229, 232, 234,
237, 240, 243, 246, 249, 252, 255, 258, 261, 264, 267, 271,
274, 277, 280, 284, 287, 291, 294, 298, 301, 305, 309, 312,
316, 320, 324, 328, 332, 336, 340, 344, 348, 352, 357, 361,
365, 370, 374, 379, 383, 388, 392, 397, 402, 407, 412, 417,
422, 427, 432, 437, 442, 448, 453, 459, 464, 470, 475, 481,
487, 493, 499, 505, 511, 517, 523, 530, 536, 542, 549, 556,
562, 569, 576, 583, 590, 597, 604, 612, 619, 626, 634, 642,
649, 657, 665, 673, 681, 690, 698, 706, 715, 723, 732, 741,
750, 759, 768, 777, 787, 796, 806, 816, 825, 835, 845, 856,
866, 876, 887, 898, 909, 920, 931, 942, 953, 965, 976, 988
])
}
def r63ss2c(arr):
if isinstance(arr, str) and len(arr) == 1:
return arr
return arr
def round_to_series(x, series):
return series[np.argmin(np.abs(x - series))]
if rnd is None:
#rnd = 'harmonic'
rnd = 'arithmetic'
rnd = r63ss2c(rnd).lower()
if ser not in E_SERIES:
raise ValueError(f'Series "{ser}" is not supported.')
ns = E_SERIES[ser]
pwr = np.log10(X)
idr = np.isfinite(pwr) & np.isreal(pwr)
if not np.any(idr):
return np.full_like(X, np.nan)
# Determine the order of PNS magnitude required
omn = np.floor(np.min(pwr[idr])) # -4 debug
omx = np.ceil(np.max(pwr[idr])) # -3 debug
# Extrapolate the PNS vector to cover all input values
temp = 10.0 ** np.arange(omn - 3, omx-1)
temp = temp.reshape((-1,1)) # make 2D row vect
temp = temp.T # transpose, change shape from 4x1 to 1x4
ns = ns.reshape((-1,1)) # make 2d row vect
pns = ns * temp
# now we need to flatten
pns = pns.flatten(order = 'F')
# Generate bin edge values
if rnd == 'harmonic':
edg = 2 * pns[:-1] * pns[1:] / (pns[:-1] + pns[1:])
elif rnd == 'arithmetic':
edg = (pns[:-1] + pns[1:]) / 2
elif rnd == 'up':
edg = pns[:-1]
elif rnd == 'down':
edg = pns[1:]
else:
raise ValueError(f'Rounding method "{rnd}" is not supported.')
# Place values of X into PNS bins
idx = np.digitize(X[idr], edg)
idx[idx == 0] = 1 # Handle values below the smallest bin edge
# Use the bin indices to select output values from the PNS
Y = pns[idx - 0]
return Y
def csvParseTarget(rows,matchMode,argc):
numrows = len(rows)
numcols = len(rows[0])
ret = {}
found = 0
Xvar = rows[0][0] # can be frequency or time
# column headers can be 'target_ampl', 'target_phase','target_voltage', or 'weight'
if Xvar != 'frequency':
print('ERROR, 1st column header in target file is not "frequency"')
sys.exit()
# search for 'weight'
found = 0
for k in range(numcols):
if rows[0][k] == 'weight':
found = 1
err_weights = [i[k] for i in rows]
err_weights = err_weights[1:]
if found == 0 and argc == 3:
print('INFO: There are 3 input files, but no column is found for weights in 2nd (csv) file\n')
print('INFO: The target will be taken from the .raw file; therefore the 2nd csv file is not used\n')
err_weights = [1.0] * (numrows - 1)
if argc == 2:
if found == 0:
print('INFO: no column found for weights, filling with 1.0')
err_weights = [1.0] * (numrows - 1)
if matchMode == 1 and (rows[0][1] != 'target_ampl' and rows[0][1] != 'weight'):
print('ERROR, matchMode set to 1, but 2nd column header in target file is not "target_ampl" or "weight"')
sys.exit()
if matchMode == 2 and (rows[0][1] != 'target_phase' and rows[0][1] != 'weight'):
print('ERROR, matchMode set to 2, but 2nd column header in target file is not "target_phase" or "weight"')
sys.exit()
freqx = [i[0] for i in rows]
freqx = freqx[1:]
if rows[0][1] == 'target_ampl':
target = [i[1] for i in rows]
target = target[1:]
elif rows[1][0] == 'target_phase':
target = [i[1] for i in rows]
target = target[1:]
else: # set target to 0's, assume it will be written later by reading a .raw file
target = [0.0] * (numrows-1)
ret['matchMode'] = matchMode
ret['err_weights_csv'] = err_weights
ret['target_csv'] = target
ret['freqx_csv'] = freqx
return ret
def csvParse(rows):
numrows = len(rows)
ret = {}
found = 0
for k in range(numrows):
if rows[k][0] == 'varInstNames':
ret['simControlOptInstNamesD'] = rows[k][1:]
found = 1
if found == 0:
print('ERROR, did not find varInstNames row in csv control input file')
sys.exit()
found = 0
for k in range(numrows):
if rows[k][0] == 'varMinVals':
ret['simControlMinValsD'] = (rows[k][1:])
found = 1
if found == 0:
print('ERROR, did not find varMinVals row in csv input file')
sys.exit()
found = 0
for k in range(numrows):
if rows[k][0] == 'varMaxVals':
ret['simControlMaxValsD'] = (rows[k][1:])
found = 1
if found == 0:
print('ERROR, did not find varMaxVals row in csv input file')
sys.exit()
found = 0
for k in range(numrows):
if rows[k][0] == 'instTol':
ret['simControlInstTolD'] = rows[k][1:]
found = 1
if found == 0:
print('ERROR, did not find instTol row in csv input file')
sys.exit()
found = 0
for k in range(numrows):
if rows[k][0] == 'spicePath':
ret['spicePathD'] = rows[k][1]
found = 1
if found == 0:
print('ERROR, did not find spicePath row in csv input file')
sys.exit()
found = 0
for k in range(numrows):
if rows[k][0] == 'filePath':
ret['filePathD'] = rows[k][1]
found = 1
if found == 0:
print('ERROR, did not find filePath row in csv input file')
sys.exit()
found = 0
for k in range(numrows):
if rows[k][0] == 'fileName':
base, extension = os.path.splitext(rows[k][1]) # remove .asc if user types it
ret['fileNameD'] = base
found = 1
if found == 0:
print('ERROR, did not find fileName row in csv input file')
sys.exit()
found = 0
for k in range(numrows):
if rows[k][0] == 'outputVar':
ret['LTspice_output_exprD'] = rows[k][1]
found = 1
if found == 0:
print('ERROR, did not find outputVar row in csv input file')
sys.exit()
found = 0
for k in range(numrows):
if rows[k][0] == 'maxIter_lsq':
ret['maxIter_lsqD'] = int(rows[k][1])
found = 1
if found == 0:
print('ERROR, did not find maxIter_lsq row in csv input file')
sys.exit()
found = 0
for k in range(numrows):
if rows[k][0] == 'maxIter_ps':
ret['maxIter_psD'] = int(rows[k][1])
found = 1
if found == 0:
print('ERROR, did not find maxIter_ps row in csv input file')
sys.exit()
found = 0
for k in range(numrows):
if rows[k][0] == 'matchMode':
ret['matchModeD'] = int(rows[k][1])
found = 1
if found == 0:
print('ERROR, did not find matchMode row in csv input file')
sys.exit()
found = 0
for k in range(numrows):
if rows[k][0] == 'plotXaxis':
ret['plotXaxisD'] = rows[k][1]
found = 1
if found == 0:
print('Warning, did not find plotXaxis row in csv input file, defaulting to Log')
ret['plotXaxisD'] = 'Log'
return ret
def runMySim(LTC,fileName,netlist_fname,optFlag):
global spiceSimCount, restartCount
try:
if optFlag:
LTspice_outputfile2 = f'.\\tempSim\\{fileName}_opt_{spiceSimCount}.raw '
else:
LTspice_outputfile2 = f'.\\tempSim\\{fileName}_{spiceSimCount}.raw '
LTC.run_now(netlist_fname)
time.sleep(0.02)
LTR = RawRead(LTspice_outputfile2)
# outNode = LTR.get_trace(LTspice_output_expr)
except:
print('re-trying sim, raw file read and get-trace ...\n')
time.sleep(2)
try:
restartCount += 1
spiceSimCount += 1
if optFlag:
LTspice_outputfile2 = f'.\\tempSim\\{fileName}_opt_{spiceSimCount}.raw '
else:
LTspice_outputfile2 = f'.\\tempSim\\{fileName}_{spiceSimCount}.raw '
LTC.run_now(netlist_fname)
time.sleep(0.2)
LTR = RawRead(LTspice_outputfile2)
except:
print('ERROR, raw file read re-try after delay failed for file ', LTspice_outputfile2)
e_type, e_object, e_traceback = sys.exc_info()
e_line_number = e_traceback.tb_lineno
print(f'exception line number: {e_line_number}')
sys.exit(0)
spiceSimCount += 1
if (spiceSimCount % 64) == 0:
LTC.file_cleanup()
return LTR
# *********************************************************
# ************************** Main *************************
# *********************************************************
# *********************************************************
# ************************** Main *************************
# *********************************************************
# *********************************************************
# ************************** Main *************************
# *********************************************************
def main():
# global passcellDict
global spiceSimCount
global restartCount
# ********* parse the command-line input args, read in the setup and target files *****
# *** possible input scenarios
## setup file, always required, sets matchMode
# mode 1 - fresp ampl match, target and weights from csv file, 3 cols, freq, ampl, weights)
# mode 2 - phase match, target and weights from csv file ( 3 cols, freq, phaseTarget, weights)
# if weights not found, default to all 1's
# input args are one of
# setup file, target file, or
# setup file, target file (weights only), .raw file for target
# setup file, .raw file for target (weights defualt to 1.0)
restartCount = 0
argc= len(sys.argv) # 1 means no extra args
argc-= 1 # only include the passed arge
if argc< 2: # valid lengths are 2 or 3
print('ERROR, need a least 2 input arguments')
sys.exit(0)
fileName_argv = [None] * argc
ext = [None] * argc
for k in range(0,argc):
fileName_argv[k] = sys.argv[k+1]
split_tup = os.path.splitext(fileName_argv[k])
ext[k] = split_tup[1] # get extension
if ext[0] != '.csv':
print('error, 1st command-line argument must be a csv setup filename!')
sys.exit(0)
if ext[1] != '.csv' and ext[1] != '.raw':
print('ERROR, 2nd argument ( to set target response) must be either a csv file or a LTspice .raw file\n')
sys.exit(0)
if argc == 3 and ext[2] != '.raw':
print('ERROR, 3rd argument must be an LTspice .raw file\n')
sys.exit(0)
rawRefFlag = False
if argc== 3:
if ext[2] == '.raw':
rawRefFlag = True
if ext[1] == '.raw':
rawRefFlag = True
try:
fcsv = open(fileName_argv[0])
except:
print('ERROR, file ', fileName_argv[0], 'not found\n')
e_type, e_object, e_traceback = sys.exc_info()
e_line_number = e_traceback.tb_lineno
print(f'exception line number: {e_line_number}')
sys.exit(0)
print('Reading setup file ',fileName_argv[0], '\n')
csvreader = csv.reader(fcsv)
rows = []
for row in csvreader:
rows.append(row)
ret = csvParse(rows)
simControlOptInstNames = ret['simControlOptInstNamesD']
simControlMinVals = ret['simControlMinValsD']
simControlMaxVals = ret['simControlMaxValsD']
simControlInstTol = ret['simControlInstTolD']
spicePath = ret['spicePathD']
filePath = ret['filePathD']
fileName = ret['fileNameD']
LTspice_output_expr = ret['LTspice_output_exprD']
maxIter_lsq = ret['maxIter_lsqD']
maxIter_ps = ret['maxIter_psD']
matchMode = ret['matchModeD']
plotXaxis = ret['plotXaxisD']
if plotXaxis == 'Log':
logFlag = 1
else:
logFlag = 0
# if matchMode == 4:
# numTimePoints = ret['numTimePointsD']
fcsv.close()
# Derived file paths and run scripts
netlist_fname = f'{filePath}{fileName}.net' # Netlist filename
schem_fname = f'{filePath}{fileName}.asc' # Netlist filename
LTspice_outputfile = f'{filePath}{fileName}.raw' # sim results filename
print('Done reading setup file ', fileName_argv[0], '\n')
# READ 2nd file from command line
err_weights_defined = 0
if ext[1] == '.csv':
try:
fcsv = open(fileName_argv[1])
except:
print('ERROR, file ',fileName_argv[1],' not found\n')
e_type, e_object, e_traceback = sys.exc_info()
e_line_number = e_traceback.tb_lineno
print(f'exception line number: {e_line_number}')
sys.exit(0)
csvreader = csv.reader(fcsv)
rows = []
for row in csvreader:
rows.append(row)
ret2 = csvParseTarget(rows, matchMode,argc)
err_weights_csv = (np.array(ret2['err_weights_csv'])).astype(float) # if not found, it is set to 1.0
err_weights_defined = 1
target_csv = (np.array(ret2['target_csv'])).astype(float) # if not found, set to 0.0, will be set later by .raw reference file
freqx_csv = (np.array(ret2['freqx_csv'])).astype(float)
fcsv.close()
print('Done reading target csv file ', fileName_argv[1])
# case when the 2nd arg is a .raw file; weights are stuck at 1.0
if (argc == 2) and ext[1] == '.raw':
refSchem_fname = f'{filePath}{fileName_argv[1]}'
print("Reading raw target file ", refSchem_fname)
try:
LTR = RawRead(refSchem_fname)
except:
print('ERROR, could not find or read .raw file\n')
e_type, e_object, e_traceback = sys.exc_info()
e_line_number = e_traceback.tb_lineno
print(f'exception line number: {e_line_number}')
sys.exit(0)
# note that this is not an auto-run sim, so don't append the simcount to the filename
# (and don't increment the simcount)
try:
outNode = LTR.get_trace('V(optTarget)')
freqxRefSchem = LTR.get_trace('frequency')
freqxRefSchem = np.abs(freqxRefSchem)
except:
print('ERROR, could not find node "optTarget" in reference raw file, please check your reference schematic\n')
sys.exit(0)
time.sleep(0.01)
if matchMode == 1:
targetRefSchem = np.abs(outNode)
if matchMode == 2:
targetRefSchem = np.unwrap(np.angle(outNode))
# optional 3rd input arg, expecting a .raw file from the simulation of another schematic
if (argc== 3):
if ext[2] != '.raw':
print('error, 3rd command-line argument must be an LTspice .raw file!')
sys.exit(0)
refSchem_fname = f'{filePath}{fileName_argv[2]}'
print("Reading raw file ", refSchem_fname)
try:
LTR = RawRead(refSchem_fname)
except:
print('ERROR, could not find or read .raw target file\n')
e_type, e_object, e_traceback = sys.exc_info()
e_line_number = e_traceback.tb_lineno
print(f'exception line number: {e_line_number}')
sys.exit(0)
# note that this is not an auto-run sim, so don't append the simcount to the filename
# (and don't increment the simcount)
try:
outNode = LTR.get_trace('V(optTarget)')
freqxRefSchem = LTR.get_trace('frequency')
freqxRefSchem = np.abs(freqxRefSchem)
except:
print('ERROR, could not find node "optTarget" in reference raw file, please check your reference schematic\n')
sys.exit(0)
time.sleep(0.01)
if matchMode == 1:
targetRefSchem = np.abs(outNode) # pretend this came from the csv file instead of a sim
if matchMode == 2:
targetRefSchem = np.unwrap(np.angle(outNode))
# done input command line parsing
LTC = SimRunner(output_folder='./tempSim')
LTC.file_cleanup()
passCellDict['LTC_D'] = LTC
updateType = 0
passCellDict['spicePathD'] = spicePath
passCellDict['filePathD'] = filePath
passCellDict['fileNameD'] = fileName
passCellDict['filePathD'] = filePath
passCellDict['netlist_fnameD'] = netlist_fname
passCellDict['LTspice_outputfileD'] = LTspice_outputfile
passCellDict['LTspice_output_exprD'] = LTspice_output_expr
passCellDict['matchModeD'] = matchMode
passCellDict['maxIter_lsqD'] = maxIter_lsq
passCellDict['maxIter_psD'] = maxIter_ps
passCellDict['simControlInstTolD'] = simControlInstTol
passCellDict['simControlOptInstNamesD'] = simControlOptInstNames
# if matchMode == 4:
# passCellDict['numTimePointsD'] = numTimePoints
# Send command to write netlist
print(f'Issuing command to write LTspice netlist from asc file ',schem_fname)
try:
LTC.create_netlist(schem_fname)
except:
print('ERROR, could not create netlist from file ',schem_fname)
sys.exit(0)
time.sleep(0.1)
# Read in the initial netlist. This will be held in memory and modified for every
# pass through the least-squares. Inside the least-squares function
# the netlist will be written to a file for each pass, so that LTspice can run
with open(netlist_fname, 'r') as fid:
netlist = fid.readlines()
test = 1
netlist = [line.strip().split() for line in netlist] # Split list into one list per line
numlines_netlist = len(netlist)
for k in range(numlines_netlist): # Find all .inst and change to old style netlist with no "="
Line = netlist[k]
if Line[0] == '.param' and len(Line) == 2: # new style, old style would have length 3 line (.param, paramName, paramValue)
try:
temp = Line[1].split('=')
Line[1] = temp[0]
Line.append(temp[1])
# print('new Line = ',Line)
except:
pass
netlist[k] = Line
passCellDict['netlistD'] = netlist
passCellDict['numlines_netlistD'] = numlines_netlist
# Find how many components are being optimized and make an index that points
# to the line number in the netlist of those components
# this makes the least-squares evaluation function faster because it doesn't
# need to search through the entire netlist each time
numOptd = len(simControlOptInstNames) # Number of instances being optimized
OptLine = [0] * numOptd # An array that points to the netlist lines with the instance names to be optimized
kkk = 0
OptLine = [0] * numOptd # Initialize the OptLine array
UB = [0.0] * numOptd # Initialize the upper bound array
LB = [0.0] * numOptd # Initialize the lower bound array
# refType = [0] * numOptd # Initialize the r-l-c part type array (0=R,1=L,2=C)
# print(' scanning to look for any of these ',simControlOptInstNames)
for kk in range(numOptd): # search all opt instance names to see if they are on the kth netlist line
for k in range(numlines_netlist): # Go through all netlist lines to look for this instance
thisLine = netlist[k]
for j in range(len(thisLine)): # scan the netlist line to see if it contains the optInstName
# print('thisLine[j], simControlOptInstNames[kk] = ',thisLine[j], " ", simControlOptInstNames[kk])
cond1 = (thisLine[j] == simControlOptInstNames[kk])
if cond1: # this could be an instance OR a parameter
OptLine[kkk] = k
UB[kkk] = float(simControlMaxVals[kk]) # Upper bound to pass to optimizer
LB[kkk] = float(simControlMinVals[kk]) # Lower bound to pass to optimizer
kkk += 1
break
numMatchingInstFound = kkk
print('NUm Matches = ',numMatchingInstFound)
if numOptd != numMatchingInstFound:
print('ERROR;')
print(f'number of instances and/or params to be optimized in control file = {numOptd}')
print(f'number of matching instances found in netlist = {numMatchingInstFound}')
print('check Instance/param name spelling in control file')
sys.exit()
passCellDict['numOptdD'] = numOptd
passCellDict['OptLineD'] = OptLine
passCellDict['updateTypeD'] = updateType
nomParams = [0.0] * numOptd
# This holds the nominal values, initialized to schematic values
paramFlag = [0] * numOptd # this will indicate a netlist line that contains a wiggleable parameter instead of a component value
for k in range(numOptd):
thisLine = netlist[OptLine[k]] # Only lines that will be optimized here
# print('*** debug, netlist line = ',thisLine)
if thisLine[0] == '.param':
paramFlag[k] = 1
newStr = thisLine[2] # value is 3rd elemnt in line
else:
newStr = thisLine[3] # Value is 4th element in line
# Replace any 'micro' symbols from LTspice with 'u'
newStr = newStr.replace(chr(181), 'u') # Replace micro symbol with 'u'
if not newStr.isnumeric(): # If it's not a number, handle symbols like k, M, pf, etc.
newStr = newStr.replace('M', 'e6')
newStr = newStr.replace('G', 'e9')
newStr = newStr.lower()
newStr = newStr.replace('k', 'e3')
newStr = newStr.replace('pf', 'e-12')
newStr = newStr.replace('ph', 'e-12')
newStr = newStr.replace('p', 'e-12')
newStr = newStr.replace('nf', 'e-9')
newStr = newStr.replace('nh', 'e-9')
newStr = newStr.replace('n', 'e-9')
newStr = newStr.replace('uf', 'e-6')
newStr = newStr.replace('uh', 'e-6')
newStr = newStr.replace('u', 'e-6')
newStr = newStr.replace('mf', 'e-3')
newStr = newStr.replace('mh', 'e-3')
newStr = newStr.replace('m', 'e-3')
nomParams[k] = float(newStr)
passCellDict['nomParamsD'] = nomParams
passCellDict['paramFlagD'] = paramFlag
print('\n*** setup file info, please check ***\n')
print('inst name, init value, Min, Max, Tolerance ***\n')
for k in range(numOptd):
if paramFlag[k] == 0:
print(f'{netlist[OptLine[k]][0]} {nomParams[k]:.12e} {LB[k]} {UB[k]} {simControlInstTol[k]}')
else:
print(f'{netlist[OptLine[k]][1]} {nomParams[k]:.12e} {LB[k]} {UB[k]} {simControlInstTol[k]}')
# if nomParams[k] > UB[k] or nomParams[k] < LB[k]:
# print("error, nom instance value outside given bounds ",netlist[OptLine[k]][0])
# raise Exception("ERROR, component value outside given bounds")
print('\nLTspice output expression from setup file = ',LTspice_output_expr,'\n')
if matchMode == 1: