Tcl SpiceGenTcl package (v0.71)

AdvancedTop, Main, Index

This section provides advanced examples from the examples folder in the root directory of SpiceGenTcl. Rather than focusing on basic operations, we highlight advanced use cases that showcase the full capabilities of both the package and the Tcl language. List of availible examples:

Monte-Carlo simulationTop, Main, Index

This example demonstrates multiple runs of a simple filter circuit and the collection of the resulting statistical distribution of frequency bandwidths. The original circuit source is from the ngspice source distribution. The target filter circuit is:

drawing

The filter is a 3rd-order Chebyshev bandpass. The first step is to build the circuit and obtain the magnitude of the transfer characteristic:

# create top-level circuit
set circuit [Circuit new {Monte-Carlo}]
# add elements to circuit
$circuit add [Vac new 1 n001 0 -ac 1]
$circuit add [R new 1 n002 n001 -r 141]
$circuit add [R new 2 0 out -r 141]
C create c1 1 out 0 -c 1e-9
L create l1 1 out 0 -l 10e-6
C create c2 2 n002 0 -c 1e-9
L create l2 2 n002 0 -l 10e-6
C create c3 3 out n003 -c 250e-12
L create l3 3 n003 n002 -l 40e-6
foreach elem [list c1 l1 c2 l2 c3 l3] {
    $circuit add $elem
}
$circuit add [Ac new -variation oct -n 100 -fstart 250e3 -fstop 10e6]
#set simulator with default 
set simulator [Batch new {batch1}]
# attach simulator object to circuit
$circuit configure -simulator $simulator

Here, we use a different method for creating a class instance: create instead of new. With create, we can directly set a custom object reference name, rather than relying on the automatically generated one by Tcl.

C create c1 1 out 0 -c 1e-9

Keep in mind that c1 is an object reference, not the name of a variable storing the reference. Therefore, it can be used as an object command directly, without the need for a $.

To calculate the magnitude of the transfer function in dB scale from the output voltage phasor, we create a procedure:

proc calcDbMag {re im} {
    set mag [= {sqrt($re*$re+$im*$im)}]
    set db [= {10*log($mag)}]
    return $db
}

proc calcDbMagVec {vector} {
    foreach value $vector {
        lappend db [calcDbMag [@ $value 0] [@ $value 1]]
    }
    return $db
}
calcDbMagVecProcedure that apply calcDbMag to list of complex values.

Run and plot the result:

# run simulation
$circuit runAndRead
# get data dictionary
set data [$circuit getDataDict]
set trace [calcDbMagVec [dget $data v(out)]]
set freqs [dget $data frequency]
foreach x $freqs y $trace {
    lappend xydata [list [@ $x 0] $y]
}
puts [findBW [lmap freq $freqs {@ $freq 0}] $trace -10]
set chartTransMag [ticklecharts::chart new]
$chartTransMag Xaxis -name "Frequency, Hz" -minorTick {show "True"} -type "log" -splitLine {show "True"}
$chartTransMag Yaxis -name "Magnitude, dB" -minorTick {show "True"} -type "value" -splitLine {show "True"}
$chartTransMag SetOptions -title {} -tooltip {trigger "axis"} -animation "False" -toolbox {feature {dataZoom {yAxisIndex "none"}}} -grid {left "10%" right "15%"} -backgroundColor "#212121"
$chartTransMag Add "lineSeries" -data $xydata -showAllSymbol "nothing" -symbolSize "1"
set fbasename [file rootname [file tail [info script]]]
$chartTransMag Render -outfile [file normalize [file join .. html_charts ${fbasename}_typ.html]] -width 1000px

ticklEcharts !!!

We define pass bandwidth by edge values -10dB, to find them we use next procedure:

proc findBW {freqs vals trigVal} {
    # calculate bandwidth of results
    set bw [dget [measure -xname freqs -data [dcreate freqs $freqs vals $vals] -trig "-vec vals -val $trigVal -rise 1" -targ "-vec vals -val $trigVal -fall 1"] xdelta]
    return $bw
}

Find bandwidth:

puts [findBW [lmap freq $freqs {@ $freq 0}] $trace -10]

The value is 1.086255 Mhz.

Our goal is to obtain a distribution of bandwidths by varying the filter parameters. To generate random values for these parameters, we use the built-in functions of the math::statistics package from Tcllib. The parameters can be distributed either normally or uniformly. For uniform distribution, we use ::math::statistics::random-uniform xmin xmax number; for normal distribution, we use ::math::statistics::random-normal mean stdev number. For uniform distribution, we define the following min and max limits for each C and L element:

set distLimits [dcreate c1 [dcreate min 0.9e-9 max 1.1e-9] l1 [dcreate min 9e-6 max 11e-6] c2 [dcreate min 0.9e-9 max 1.1e-9] l2 [dcreate min 9e-6 max 11e-6] c3 [dcreate min 225e-12 max 275e-12] l3 [dcreate min 36e-6 max 44e-6]]

We can specify different numbers of simulations; the more runs we perform, the more accurate the representation becomes. For example, we set the number of simulations to 1,000 runs with 15 intervals for constructing a boxplot:

# set number of simulations
set mcRuns 100
set numOfIntervals 15

Now we ready to run simulations 1000 times and collect results:

# loop in which we run simulation with uniform distribution
for {set i 0} {$i<$mcRuns} {incr i} {
    #set elements values according to uniform distribution
    foreach elem [list c1 l1 c2 l2 c3 l3] {
        $elem actOnParam -set [string index $elem 0] [random-uniform {*}[dict values [dget $distLimits $elem]] 1]
    }
    # run simulation
    $circuit runAndRead
    # get data dictionary
    set data [$circuit getDataDict]
    # get results
    if {$i==0} {
        set freqs [dget $data frequency]
        foreach freq $freqs {
            lappend freqRes [@ $freq 0]
        }
    }
    # get vout frequency curve
    lappend traceListUni [calcDbMagVec [dget $data v(out)]]
    # calculate bandwidths values
    lappend bwsUni [findBW $freqRes [@ $traceListUni end] -10]
}

To obtain the distribution, we need to determine reasonable limits based on the minimum and maximum of the generated bandwidth values. Using the specified number of intervals (15), we apply the following procedure:

proc createIntervals {data numOfIntervals} {
    set intervals [::math::statistics::minmax-histogram-limits [tcl::mathfunc::min {*}$data]  [tcl::mathfunc::max {*}$data] $numOfIntervals]
    lappend intervalsStrings [format "<=%.2e" [@ $intervals 0]]
    for {set i 0} {$i<[- [llength $intervals] 1]} {incr i} {
        lappend intervalsStrings [format "%.2e-%.2e" [@ $intervals $i] [@ $intervals [+ $i 1]]]
    }
    return [dcreate intervals $intervals intervalsStr $intervalsStrings]
}

Here, we use the dedicated procedure from the statistics package: ::math::statistics::minmax-histogram-limits min max number. Additionally, we construct strings in form leftVal-rightVal that represent the intervals on the boxplot chart. We call this procedure and store the results:

set uniIntervals [createIntervals $bwsUni $numOfIntervals]

Finally, to obtain the expected histogram for a uniform distribution, we use the following procedure with the built-in function: ::math::statistics::histogram-uniform xmin xmax limits number:

proc createDist {data intervals} {
    set dist [::math::statistics::histogram $intervals $data]
    return [lrange $dist 0 end-1]
}

Call the createDist procedure to calculate the histogram y-axis values, which correspond to the number of bandwidths within each interval:

set normDist [createDist $bwsNorm [dget $normIntervals intervals]]

The same sequence of steps is applied for the normal distribution. We assume that std = (xmax - xmin) / 6, where xmax and xmin are the limits of the uniform distribution.

# set parameter's normal distributions limits
set normalLimits [dcreate c1 [dcreate mean 1e-9 std [/ 0.1e-9 3]] l1 [dcreate mean 10e-6 std [/ 1e-6 3]] c2 [dcreate mean 1e-9 std [/ 0.1e-9 3]] l2 [dcreate mean 10e-6 std [/ 1e-6 3]] c3 [dcreate mean 250e-12 std [/ 25e-12 3]] l3 [dcreate mean 40e-6 std [/ 4e-6 3]]]

## loop in which we run simulation with normal distribution
for {set i 0} {$i<$mcRuns} {incr i} {
    #set elements values according to normal distribution
    foreach elem [list c1 l1 c2 l2 c3 l3] {
        $elem $elem actOnParam -set [string index $elem 0] [random-normal {*}[dict values [dget $normalLimits $elem]] 1]
    }
    # run simulation
    $circuit runAndRead
    # get data dictionary
    set data [$circuit getDataDict]
    # get results
    if {$i==0} {
        set freqs [dget $data frequency]
        foreach freq $freqs {
            lappend freqRes [@ $freq 0]
        }
    }
    # get vout frequency curve
    lappend traceListNorm [calcDbMagVec [dget $data v(out)]]
    # calculate bandwidths values
    lappend bwsNorm [findBW $freqRes [@ $traceListNorm end] -10]
}
# get distribution of bandwidths with normal parameters distribution
set normIntervals [createIntervals $bwsNorm $numOfIntervals]
set normDist [createDist $bwsNorm [dget $normIntervals intervals]]

Finally, we plot resulted distributions:

# plot results with ticklecharts
# chart for uniformly distributed parameters
set chartUni [ticklecharts::chart new]
$chartUni Xaxis -name "Frequency intervals, Hz" -data [list [dget $uniIntervals intervalsStr]] -axisTick {show "True" alignWithLabel "True"} -axisLabel {interval "0" rotate "45" fontSize "8"}
$chartUni Yaxis -name "Bandwidths per interval" -minorTick {show "True"} -type "value"
$chartUni SetOptions -title {} -tooltip {trigger "axis"} -animation "False" -toolbox {feature {dataZoom {yAxisIndex "none"}}} -backgroundColor "#212121"
$chartUni Add "barSeries" -data [list $uniDist]
# chart for normally distributed parameters
set chartNorm [ticklecharts::chart new]
$chartNorm Xaxis -name "Frequency intervals, Hz" -data [list [dget $normIntervals intervalsStr]] -axisTick {show "True" alignWithLabel "True"} -axisLabel {interval "0" rotate "45" fontSize "8"}
$chartNorm Yaxis -name "Bandwidths per interval" -minorTick {show "True"} -type "value"
$chartNorm SetOptions -title {} -tooltip {trigger "axis"} -animation "False" -toolbox {feature {dataZoom {yAxisIndex "none"}}} -backgroundColor "#212121"
$chartNorm Add "barSeries" -data [list $normDist]
# create multiplot
set layout [ticklecharts::Gridlayout new]
$layout Add $chartNorm -bottom "10%" -height "35%" -width "75%"
$layout Add $chartUni -bottom "60%" -height "35%" -width "75%"

set fbasename [file rootname [file tail [info script]]]
$layout Render -outfile [file normalize [file join .. html_charts $fbasename.html]] -height 800px -width 1200px

ticklEcharts !!!

We can clearly see the difference between normal and uniform distributions; the intervals are close due to setting the standard deviation of the normal distribution as std = (xmax - xmin) / 6.

We can also take the uniform intervals and calculate the normal distribution values at these intervals:

# find distribution of normal distributed values in uniform intervals       
set normDistWithUniIntervals [createDist $bwsNorm [dget $uniIntervals intervals]]
set chartCombined [ticklecharts::chart new]
$chartCombined Xaxis -name "Frequency intervals, Hz" -data [list [dget $uniIntervals intervalsStr]] -axisTick {show "True" alignWithLabel "True"} -axisLabel {interval "0" rotate "45" fontSize "8"}
$chartCombined Yaxis -name "Bandwidths per interval" -minorTick {show "True"} -type "value"
$chartCombined SetOptions -title {} -legend {} -tooltip {trigger "axis"} -animation "False" -toolbox {feature {dataZoom {yAxisIndex "none"}}} -grid {left "10%" right "15%"} -backgroundColor "#212121"
$chartCombined Add "barSeries" -data [list $uniDist] -name "Uniform"
$chartCombined Add "barSeries" -data [list $normDistWithUniIntervals] -name "Normal"
$chartCombined Render -outfile [file normalize [file join .. html_charts ${fbasename}_combined.html]] -height 800px -width 1200px

ticklEcharts !!!

Parameters extraction of diode model parametersTop, Main, Index

In this advanced example we will do curve fitting procedure to extract parameters of diode model. We use measured (emulated with more advanced diode model generated data, with added small distorsion and make non-equal voltage steps) forward current characteristic of diode.

This example demands two additional Tcl packages to make fast interpolation and Levenberg-Marquardt optimization algorithm:

Also, the tclcsv package is used for data reading.

The main steps of this procedure are:

First step is to define the cost function that will be used by the optimizer:

proc diodeIVcalc {xall pdata args} {
    dict with pdata {}
    $model actOnParam -set is [@ $xall 0] n [@ $xall 1] rs [@ $xall 2] ikf [@ $xall 3]
    $vSrc actOnParam -set start $vMin stop $vMax incr $vStep
    $circuit runAndRead
    set data [$circuit getDataDict]
    foreach iVal $i iSim [lmap i [dget $data i(va)] {= {-$i}}] {
        lappend fvec [= {log(abs($iVal))-log(abs($iSim))}]
        lappend fval $iSim
    }
    return [dcreate fvec $fvec fval $fval]
}

The input arguments are:

Let me expand the values in pdata dictionary:

First line creates variables with the same name as keys of pdata dictionary:

dict with pdata {}

Then we set parameters values of the diode model object using ::SpiceGenTcl::Model::actOnParam method:

$model actOnParam -set is [@ $xall 0] n [@ $xall 1] rs [@ $xall 2] ikf [@ $xall 3]

The same we do with voltage source element:

$vSrc actOnParam -set start $vMin stop $vMax incr $vStep

Then we run circuit with new parameters and read data back to calculate residuals:

$circuit runAndRead
set data [$circuit getDataDict]

Calculating residuals and save simulated data in the loop:

foreach iVal $i iSim [lmap i [dget $data i(va)] {= {-$i}}] {
    lappend fvec [= {log(abs($iVal))-log(abs($iSim))}]
    lappend fval $iSim
}

The residuals is calculated with the next formula:

f = log ⎛|i    |⎞ - log ⎛|i   |⎞
        ⎝| meas|⎠       ⎝| sim|⎠

We use logarithm of values because the current values span across multiple orders, from 1e-8 to 1e-2, that creates issue for fitting algorithm.

Procedure returns dictionary that contains lists with residuals and diode current under the keys fvec and fval, and fvec key is mandatory because list under this key is used by optimization procedure.

To run simulation we need to build circuit and define necessary objects:

set diodeModel [DiodeModel new diomod -is 1e-12 -n 1.0 -rs 30 -cj0 1e-9 -trs1 0.001 -xti 5 -ikf 1e-4]
set vSrc [Dc new -src va -start 0 -stop 2 -incr 0.02]
set circuit [Circuit new {diode IV}]
$circuit add [D new 1 anode 0 -model diomod -area 1]
$circuit add [Vdc new a anode 0 -dc 0]
$circuit add $diodeModel
$circuit add $vSrc
set tempSt [Temp new 25]
$circuit add $tempSt
$circuit configure -simulator [Batch new {batch1}]

We save object references for diode model and voltage source into dedicated variable to use it in cost function - it will be saved into pdata dictionary.

Next, we read measured data from .csv file with tclcsv package:

set file [open [file join $fileDataPath iv25.csv]]
set ivTemp25 [csv_read -startline 1 $file]
close $file
set vRaw [lmap elem $ivTemp25 {@ $elem 0}]
set iRaw [lmap elem $ivTemp25 {@ $elem 1}]

The loaded data is (with selected regions of fitting):

drawing

Fitting in first regionTop, Main, Index

First region we want to fit is the region of ideal diode, it spans from minimum voltage to approximately 0.85V. We set these limits in variables vMin, vMax and define the equal step of voltage vStep.

set vMin [min {*}$vRaw]
set vMax 0.85
set vStep 0.02

We use fixed voltage step instead of data points defined in measured data with non-equal steps to ease the defining the sweep of voltage in simulator - we can use DC sweep of voltage source with fixed step. So, we use linear interpolation to get values of current at even grid with defined minimum and maximum values. Then, we create pdata dictionary with corresponding values:

set vInterp [lseq $vMin to $vMax by $vStep]
set iInterp [lin1d -x $vRaw -y $iRaw -xi $vInterp]
set pdata [dcreate v $vInterp i $iInterp circuit $circuit model $diodeModel vMin $vMin vMax $vMax vStep $vStep vSrc $vSrc]

Command lin1d is imported from tclinterp package.

Parameters that we need to find (extract) are:

isSaturation current.
nIdeality factor.
rsSeries resistance.
ikfForward knee current.

In first region the parameters of interest are n and is0 - it affects current curve the most in this region.

Parameter are represented as special objects ::tclopt::ParameterMpfit from tclopt package, where we can define initial values and lower/upper limits:

set iniPars [list 1e-14 1.0 30 1e-4]
set par0 [::tclopt::ParameterMpfit new is [@ $iniPars 0] -lowlim 1e-17 -uplim 1e-12]
set par1 [::tclopt::ParameterMpfit new n [@ $iniPars 1] -lowlim 0.5 -uplim 2]
set par2 [::tclopt::ParameterMpfit new rs [@ $iniPars 2] -fixed -lowlim 1e-10 -uplim 100]
set par3 [::tclopt::ParameterMpfit new ikf [@ $iniPars 3] -fixed -lowlim 1e-12 -uplim 0.1]

For rs and ikf parameters we specify option -fixed to make them constant during the first region fitting.

Optimization is done with Levenberg-Marquardt algorithm based optimizator from tclopt package. First step is to create ::tclopt::Mpfit object that defines the optimization parameters:

set optimizer [::tclopt::Mpfit new -funct diodeIVcalc -m [llength $vInterp] -pdata $pdata]

The arguments we pass here are:

-functName of the procedure calculating the residuals.
-mNumber of fitting points.
-pdataDictionary that is passed to cost function procedure.

Then we add parameters to $optimizer object, and the order of adding them should match the order of the elements list xall we pass values to cost function:

$optimizer addPars $par0 $par1 $par2 $par3

Then we can run the simulation, read the resulted list of parameters x and print formatted values:

set result [$optimizer run]
set resPars [dget $result x]
puts [format "is=%.3e, n=%.3e, rs=%.3e, ikf=%.3e" {*}[dget $result x]]

The result is:

is=5.771e-17, n=1.050e+00, rs=3.000e+01, ikf=1.000e-04

You can notice that is and n now have different values, and rs and ikf remains the same because of -fixed option.

Fitting in second regionTop, Main, Index

Now we are ready to fit curve in the second ohmic/high-injection region. It started at the upper end of first region and continue until the end of the data. We interpolate this range with the same voltage step:

set vMin 0.85
set vMax [max {*}$vRaw]
set vInterp [lseq $vMin to $vMax by $vStep]
set iInterp [lin1d -x $vRaw -y $iRaw -xi $vInterp]

We will use the same pdata variable containing dictionary, but change the current and voltage data and limits:

dset pdata v $vInterp
dset pdata i $iInterp
dset pdata vMin $vMin
dset pdata vMax $vMax

We make is and n parameters fixed this time, and free the rs and ikf:

$par0 configure -fixed 1 -initval [@ $resPars 0]
$par1 configure -fixed 1 -initval [@ $resPars 1]
$par2 configure -fixed 0
$par3 configure -fixed 0

You can notice that we supply the resulted values of is and n as the initial values to the second fitting, so we have two parameters already fitted before start the next optimization.

Replace $optimizer object fields with the new values:

$optimizer configure -m [llength $vInterp] -pdata $pdata

Run, read result and print new parameters values:

set result [$optimizer run]
set fittedIdiode [dget [diodeIVcalc [dget $result x] $pdata] fval]
set resPars [dget $result x]
puts [format "is=%.3e, n=%.3e, rs=%.3e, ikf=%.3e" {*}[dget $result x]]

The result is:

is=5.771e-17, n=1.050e+00, rs=5.430e+00, ikf=3.069e-04

Now we can see that rs and ikf were changed during fitting, just as planned.

Fitting the whole curveTop, Main, Index

The last step is the fitting to the whole curve, with initial values of parameters replaced with resulted values.

Again, set voltage limits to minimum and maximum of the data, interpolate it and replace data in $pdata dictionary:

set vMin [min {*}$vRaw]
set vMax [max {*}$vRaw]
set vInterp [lseq $vMin to $vMax by $vStep]
set iInterp [lin1d -x $vRaw -y $iRaw -xi $vInterp]
dset pdata v $vInterp
dset pdata i $iInterp
dset pdata vMin $vMin
dset pdata vMax $vMax

We unfix rs and ikf parameters, and set new limits within ±10% of previous values to forbid parameters becoming too different:

$par0 configure -fixed 0 -initval [@ $resPars 0] -lowlim [= {[@ $resPars 0]*0.9}] -uplim [= {[@ $resPars 0]*1.1}]
$par1 configure -fixed 0 -initval [@ $resPars 1] -lowlim [= {[@ $resPars 1]*0.9}] -uplim [= {[@ $resPars 1]*1.1}]
$par2 configure -initval [@ $resPars 2] -lowlim [= {[@ $resPars 2]*0.9}] -uplim [= {[@ $resPars 2]*1.1}]
$par3 configure -initval [@ $resPars 3] -lowlim [= {[@ $resPars 3]*0.9}] -uplim [= {[@ $resPars 3]*1.1}]

The set new data in optimizer object, run and print results:

$optimizer configure -m [llength $vInterp] -pdata $pdata
set result [$optimizer run]
puts [format "is=%.3e, n=%.3e, rs=%.3e, ikf=%.3e" {*}[dget $result x]]

The result is:

is=6.348e-17, n=1.061e+00, rs=5.232e+00, ikf=3.376e-04

All parameters were changed to produce best fit for the whole curve.

Plotting resulted dataTop, Main, Index

Now we are ready to see the result of the fitting, we need to calculate curves with initial values and final values of parameters:

set initIdiode [dget [diodeIVcalc $iniPars $pdata] fval]
set fittedVIdiode [lmap vVal $vInterp iVal $fittedIdiode {list $vVal $iVal}]
set initVIdiode [lmap vVal $vInterp iVal $initIdiode {list $vVal $iVal}]
set viRaw [lmap vVal $vRaw iVal $iRaw {list $vVal $iVal}]

Plot it with ticklecharts package in linear and logarithmic scales:

set chart [ticklecharts::chart new]
$chart Xaxis -name "v(anode), V" -minorTick {show "True"}  -type "value" -splitLine {show "True"} -min "0.4" -max "1.6"
$chart Yaxis -name "Idiode, A" -minorTick {show "True"}  -type "value" -splitLine {show "True"} -min "0.0" -max "dataMax"
$chart SetOptions -title {} -tooltip {trigger "axis"} -animation "False" -legend {} -toolbox {feature {dataZoom {yAxisIndex "none"}}} -grid {left "10%" right "15%"} -backgroundColor "#212121" 
$chart Add "lineSeries" -data $fittedVIdiode -showAllSymbol "nothing" -name "fitted" -symbolSize "4" 
$chart Add "lineSeries" -data $initVIdiode -showAllSymbol "nothing" -name "unfitted" -symbolSize "4"
$chart Add "lineSeries" -data $viRaw -showAllSymbol "nothing" -name "measured" -symbolSize "4"
set chartLog [ticklecharts::chart new]
$chartLog Xaxis -name "v(anode), V" -minorTick {show "True"}  -type "value" -splitLine {show "True"} -min "0.4" -max "1.6"
$chartLog Yaxis -name "Idiode, A" -minorTick {show "True"}  -type "log" -splitLine {show "True"} -min "dataMin" -max "0.1"
$chartLog SetOptions -title {} -tooltip {trigger "axis"} -animation "False" -legend {} -toolbox {feature {dataZoom {yAxisIndex "none"}}} -grid {left "10%" right "15%"} -backgroundColor "#212121"
$chartLog Add "lineSeries" -data $fittedVIdiode -showAllSymbol "nothing" -name "fitted" -symbolSize "4"
$chartLog Add "lineSeries" -data $initVIdiode -showAllSymbol "nothing" -name "unfitted" -symbolSize "4"
$chartLog Add "lineSeries" -data $viRaw -showAllSymbol "nothing" -name "measured" -symbolSize "4"

set layout [ticklecharts::Gridlayout new]
$layout Add $chartLog -bottom "5%" -height "40%" -width "80%"
$layout Add $chart -bottom "55%" -height "40%" -width "80%"

set fbasename [file rootname [file tail [info script]]]
$layout Render -outfile [file normalize [file join .. html_charts $fbasename.html]] -height 900px -width 700px

ticklEcharts !!!

On the plot we can see the initial simulated curve, the measured data and the final fitting result.

Inverter performance optimizationTop, Main, Index

This example was taken from ASCO (A SPICE Circuit Optimizer) - software to make optimization of different circuit performance with usage of differential evolution (DE) optimization algorithm.

Example uses next additional packages (in addition to ticklecharts and tclcsv):

Circuit is the simplest possible invertor with capacitive load:

drawing

Simulation is done with pulsed input signal 4 periods long. We use three diffrerent supply voltage values to make result be optimal across possible range of power levels. The goal is to find width PWIDTH of PMOS transistor that provide minimum possible power consumption while satisfy certain constraints: low state output voltage value to be no more than certain value VLOWLIM and high state output voltage value to be no less than certain value VHIGHLIM.

Input parameters and constraints values are:

VSUPPLY2V, 2.1V, 2.2V.
VLOWLIM0.05V.
VHIGHLIM1.95V.
INPFREQ850MHz.
LMIN0.35um.
NOPERIODS4.

Width NWIDTH of NMOS device is 3 times lower than width PWIDTH of PMOS device. Lengths of bothe devices are equal to LENGTH. Perimeters of source and drain for both devices are defined by the equation:

PD = PS = 2 ⋅ (WIDTH + LENGTH)

Input period is:

               1   
INPPERIOD = ───────
            INPFREQ

The power consumption is defined as RMS value of instant power I(VDD)*V(VDD,VSS) across fourth period time interval defined by TMEASSTART and TMEASSTOP:

TMEASSTART = (NOPERIODS - 1) ⋅ INPPERIOD
TMEASSTOP = NOPERIODS ⋅ INPPERIOD
            ________________________________________________________________
           ╱                          TMEASSTOP                             
          ╱            1                  ⌠                           2     
PRMS =   ╱  ────────────────────── ⋅      ⌡     (I(VDD) ⋅ V(VDD, VSS))  ⋅ dt
       ╲╱   TMEASSTOP - TMEASSTART   TMEASSTART                             

Low and high voltages values are measured at fourth output pulse at times TMEAS1 and TMEAS2 correspondingly:

                     3.0 ⋅ INPPERIOD
TMEAS1 = TMEASSTOP - ───────────────
                           4.0      
                     INPPERIOD
TMEAS2 = TMEASSTOP - ─────────
                        4.0      

In the next code lines we define all above mentioned constants as well as equations for PD and PS:

proc pdPsCalc {width length} {
    return [= {2*$width+2*$length}]
}
# set parameters
set vSupply 2.0
set inpFreq 850e6
set inpPeriod [= {1.0/$inpFreq}]
set noPeriods 4
set tmeasStart [= {($noPeriods-1)*$inpPeriod}]
set tmeasStop [= {$noPeriods*$inpPeriod}]
set tmeas1 [= {$tmeasStop-3.0*$inpPeriod/4.0}]
set tmeas2 [= {$tmeasStop-1.0*$inpPeriod/4.0}]
set initialPWidth 10e-3
set pWidth $initialPWidth
set nWidth [= {$pWidth/3.0}]
set length 0.35e-6
set vLowLim 0.05
set vHighLim 1.95
set pd [pdPsCalc $pWidth $length]
set ps [pdPsCalc $pWidth $length]
set vSupplyVals {2.0 2.1 2.2}

Next step is to define circuit with all mentioned values calculated previously, and add .include statement for NMOS and PMOS models with class ::SpiceGenTcl::Include:

# build circuit
set circuit [Circuit new {Inverter}]
set vdd [Vdc new dd vdd 0 -dc $vSupply]
set vss [Vdc new ss vss 0 -dc 0.0]
set vIn [Vpulse new 1 in vss -low $vSupply -high 0.0 -td [= {$inpPeriod/2.0}] -tr [= {$inpPeriod/1000.0}] -tf [= {$inpPeriod/1000.0}] -pw [= {$inpPeriod/2.0}] -per $inpPeriod]
set capLoad [C new l out vss -c 3p]
set mp [M new p out in vdd -model pmos -l $length -w $pWidth -n4 vdd -pd $pd -ps $ps]
set mn [M new n out in vss -model nmos -l $length -w $nWidth -n4 vss -pd $pd -ps $ps]
$circuit add $vdd $vss $vIn $capLoad $mp $mn
$circuit add [Tran new -tstep [= {$inpPeriod/1000.0}] -tstop [= {$inpPeriod*$noPeriods}]]
$circuit add [Include new [file join $scriptPath models n.typ]]
$circuit add [Include new [file join $scriptPath models p.typ]]

Also we define simulator (Ngspice) that should be used:

# set simulator
if {[catch {set simulator [Shared new batch1]}]} {
    set simulator [Batch new batch1]
}
$circuit configure -simulator $simulator

It is better to use ::SpiceGenTcl::Ngspice::Simulators::Shared because of speed improvement - we read data right from the memory and skipping phase of reading saving and reading raw file. But for that we need to have companion package NgspiceTclBridge and compiled shared Ngspice for your platform. If it is not done, we fallback to usual ::SpiceGenTcl::Ngspice::Simulators::Batch version.

Next large step is to define correct cost function that taking into account the constraints voltages VLOWLIM and VHIGHLIM, the power consumption PRMS and different values of VSUPPLY voltages.

The general equation for cost function is:

              i = n                    ⎛P      - P    ⎞  
              ____                     ⎜ spec     sim ⎟  
              ╲                        ⎜     j       j⎟  
Cost = W    ⋅  ╲    P     + W    ⋅ max ⎜──────────────⎟  
        obj    ╱     sim     con       ⎜    P         ⎟  
              ╱         i              ⎜     spec     ⎟  
              ‾‾‾‾                     ⎝         j    ⎠  
              i = 1                                    j∈[1,m]

where:

In our case we use three supply voltages VSUPPLY, so cost function is calculated for each one and summed.

The tricky part is to select right weights for both objectives and constraints. When we are satisfied constraints, the value should be neglegible to objective member value, but when we are do not satisfy the constraints, we should get large increase in overall cost function so we avoid going beyond these constraints. Sometimes correction is necessary in case optimization does not gradually improving, or we ended up with violated constraints.

In our case we have constraints:

By default the W_obj is set to 10, so W_con should be in between 100-10000, and it's better to start from the higher value, so we set it to 10000.

So, the final cost function is:

set pdata [dcreate vSupplyVals $vSupplyVals nDevice $mn pDevice $mp vLowLim $vLowLim vHighLim $vLowLim length $length vdd $vdd tmeas1 $tmeas1 tmeas2 $tmeas2 objWeight 10 constrWeight 10000 circuit $circuit]
proc costFunc {xall pdata args} {
    dict with pdata {}
    set width [@ $xall 0]
    set pdPs [pdPsCalc $width $length]
    $nDevice actOnParam -set l $length w [= {$width/3.0}] pd $pdPs ps $pdPs
    $pDevice actOnParam -set l $length w $width pd $pdPs ps $pdPs
    foreach val $vSupplyVals {
        $vdd actOnParam -set dc $val
        if {[catch {$circuit runAndRead}]} {
            # huge penalty in case of non-convergence
            lappend vlowList 1
            lappend vhighList 1
            lappend psupplyList 1
            continue
        }
        set data [$circuit getDataDict]
        lappend vlowList [measure -xname time -data $data -find v(out) -at $tmeas1]
        lappend vhighList [measure -xname time -data $data -find v(out) -at $tmeas2]
        set instantPower [= {mul([dget $data i(vdd)], [dget $data v(vdd)])}]
        lappend psupplyList [measure -xname time -data [dcreate time [dget $data time] instantPower $instantPower] -rms "-vec instantPower -from $tmeas1 -to $tmeas2"]
    }
    set costObj 0.0
    set costConstr 0.0
    foreach psupply $psupplyList vlow $vlowList vhigh $vhighList {
        set constrVlow [= {($vlow-$vLowLim)/$vLowLim}]
        if {$constrVlow<0} {
            set constrVlow 0.0
        }
        set constrVhigh [= {($vHighLim-$vhigh)/$vHighLim}]
        if {$constrVhigh<0} {
            set constrVhigh 0.0
        }
        set costObj [= {$costObj+$psupply}]
        set costConstr [= {$costConstr+max($constrVlow, $constrVhigh)}]
    }
    return [= {$objWeight*$costObj+$constrWeight*$costConstr}]
}

Let's breakdown it line by line to clarify every step while keeping in mind the previous information.

First, we build pdata dictionary with information that is necessary to evaluate the cost function:

set pdata [dcreate vSupplyVals $vSupplyVals nDevice $mn pDevice $mp vLowLim $vLowLim vHighLim $vLowLim length $length vdd $vdd tmeas1 $tmeas1 tmeas2 $tmeas2 objWeight 10 constrWeight 10000 circuit $circuit]

At the start we unpack the dictionary to local variables with the same name as key of the dictionary. Then we extract the WIDTH parameter from vectors of variables. We have only one variable, so we just get first and the only element:

dict with pdata {}
set width [@ $xall 0]

Next we calculate the pd and ps parameters from the given length and and new width, and change the parameters of NMOS and PMOS devices with the new values:

set pdPs [pdPsCalc $width $length]
$nDevice actOnParam -set l $length w [= {$width/3.0}] pd $pdPs ps $pdPs
$pDevice actOnParam -set l $length w $width pd $pdPs ps $pdPs

In the next block we loop over all power supply voltages, run circuit, read the data and extract values we need:

foreach val $vSupplyVals {
    $vdd actOnParam -set dc $val
    if {[catch {$circuit runAndRead}]} {
        # huge penalty in case of non-convergence
        lappend vlowList 1
        lappend vhighList 1
        lappend psupplyList 1
        continue
    }
    set data [$circuit configure -data]
    set dataDict [$circuit getDataDict]
    lappend vlowList [$data measure -find v(out) -at $tmeas1]
    lappend vhighList [$data measure -find v(out) -at $tmeas2]
    set instantPower [= {mul([dget $dataDict i(vdd)], [dget $dataDict v(vdd)])}]
    lappend psupplyList [measure -xname time -data [dcreate time [dget $dataDict time] instantPower $instantPower] -rms "-vec instantPower -from $tmeas1 -to $tmeas2"]
}

An additional guard was added to prevent stepping into regions where circuit could fail due to non-convergence, so we set objective and constraints metric to high values:

if {[catch {$circuit runAndRead}]} {
    # huge penalty in case of non-convergence
    lappend vlowList 1
    lappend vhighList 1
    lappend psupplyList 1
    continue
}

To find values VLOW and VHIGH we use method ::SpiceGenTcl::RawFile::measure, that is wrapper to measure command from package tclmeasure. Then we calculate calculate instant power by multiplying current through power source to power voltage, and then directly use command measure from package tclmeasure:

set data [$circuit configure -data]
set dataDict [$circuit getDataDict]
lappend vlowList [$data measure -find v(out) -at $tmeas1]
lappend vhighList [$data measure -find v(out) -at $tmeas2]
set instantPower [= {mul([dget $dataDict i(vdd)], [dget $dataDict v(vdd)])}]
lappend psupplyList [measure -xname time -data [dcreate time [dget $dataDict time] instantPower $instantPower] -rms "-vec instantPower -from $tmeas1 -to $tmeas2"]

After looping through all power voltages, we get values of objective in list psupplyList and constraints for VLOW and VHIGH in lists vlowList and vhighList correspondingly.

In the final block we calculate value of cost function according to equations provided previously:

set costObj 0.0
set costConstr 0.0
foreach psupply $psupplyList vlow $vlowList vhigh $vhighList {
    set constrVlow [= {($vlow-$vLowLim)/$vLowLim}]
    if {$constrVlow<0} {
        set constrVlow 0.0
    }
    set constrVhigh [= {($vHighLim-$vhigh)/$vHighLim}]
    if {$constrVhigh<0} {
        set constrVhigh 0.0
    }
    set costObj [= {$costObj+$psupply}]
    set costConstr [= {$costConstr+max($constrVlow, $constrVhigh)}]
}
return [= {$objWeight*$costObj+$constrWeight*$costConstr}]

After defining cost function, we can initialize and run optimization. As optimization engine we use Differential Evolution algorithm from tclopt packkage:

set par [::tclopt::Parameter new width $pWidth -lowlim 1e-4 -uplim 10e-3]
set optimizer [::tclopt::DE new -funct costFunc -pdata $pdata -strategy rand-to-best/1/exp -genmax 50 -refresh 1 -np 10 -f 0.5 -cr 1 -seed 3 -debug -abstol 1e-6 -history -histfreq 1]
$optimizer addPars $par
set width [dget [$optimizer run] x]

In first line we create parameter object and set the boundaries for the width value, from 100um to 10mm. Strategy selection, values of f, cr, np and genmax are the same as in ASCO example. Then we add parameter object par to optimizer object optimizer, run it, and wait for the result. We set debug mode with -debug switch to see progress output, and also we save history to look at the best trajectory of optimum searching.

In the next block we read history information that is saved during the optimization, it was turned on by -history switch:

# get results and history
set results [$optimizer run]
set width [dget $results x]
set trajectory [dict get $results besttraj]
set bestf [dict get $results history]
foreach genTr $trajectory genF $bestf {
    lappend optData [list {*}[dict get $genTr x] [dict get $genF bestf]]
    lappend functionTrajectory [list [dict get $genTr gen] [dict get $genTr x]]
}

The we plot trajectory with ticklecharts package:

# plot 2D trajectory
set chart [ticklecharts::chart new]
$chart Xaxis -name "Generation" -minorTick {show "True"} -type "value" -splitLine {show "True"}
$chart Yaxis -name "Cost function" -minorTick {show "True"}  -splitLine {show "True"}
$chart SetOptions -title {} -tooltip {trigger "axis"} -animation "False" -toolbox {feature {dataZoom {yAxisIndex "none"}}}
$chart Add "lineSeries" -name "Best trajectory" -data $functionTrajectory -showAllSymbol "nothing"
set fbasename [file rootname [file tail [info script]]]
$chart Render -outfile [file normalize [file join .. html_charts ${fbasename}_plot.html]]

ticklEcharts !!!

The final block of code is for calculating initial and final values of VLOW, VHIGH, VSUPPLY and preparing waveform for plotting. We calculate it only for highest supply voltage to avoid cluttering:

# calculate initial waveforms the highest supply voltage
set pdPs [pdPsCalc $initialPWidth $length]
$mn actOnParam -set l $length w [= {$initialPWidth/3.0}] pd $pdPs ps $pdPs
$mp actOnParam -set l $length w $initialPWidth pd $pdPs ps $pdPs
$vdd actOnParam -set dc [@ $vSupplyVals 2]
$circuit runAndRead
set data [$circuit configure -data]
set dataDict [$circuit getDataDict]
foreach timeVal [dget $dataDict time] voutVal [dget $dataDict v(out)] {
    lappend initialWaveform [list $timeVal $voutVal]
}
set vlowInitial [$data measure -find v(out) -at $tmeas1]
set vhighInitial [$data measure -find v(out) -at $tmeas2]
set instantPower [= {mul([dget $dataDict i(vdd)], [dget $dataDict v(vdd)])}]
set psupplyInitial [measure -xname time -data [dcreate time [dget $dataDict time] instantPower $instantPower] -rms "-vec instantPower -from $tmeas1 -to $tmeas2"]

# calculate final waveform for the highest supply voltage
set pdPs [pdPsCalc $width $length]
$mn actOnParam -set l $length w [= {$width/3.0}] pd $pdPs ps $pdPs
$mp actOnParam -set l $length w $width pd $pdPs ps $pdPs
$vdd actOnParam -set dc [@ $vSupplyVals 2]
$circuit runAndRead
set data [$circuit configure -data]
set dataDict [$circuit getDataDict]
foreach timeVal [dget $dataDict time] voutVal [dget $dataDict v(out)] {
    lappend finalWaveform [list $timeVal $voutVal]
}
set vlowFinal [$data measure -find v(out) -at $tmeas1]
set vhighFinal [$data measure -find v(out) -at $tmeas2]
set instantPower [= {mul([dget $dataDict i(vdd)], [dget $dataDict v(vdd)])}]
set psupplyFinal [measure -xname time -data [dcreate time [dget $dataDict time] instantPower $instantPower] -rms "-vec instantPower -from $tmeas1 -to $tmeas2"]

Plotting initial and final waveforms, and result values:

# plot waveforms
set chart [ticklecharts::chart new]
$chart Xaxis -name "Time, s" -minorTick {show "True"} -type "value" -splitLine {show "True"}
$chart Yaxis -name "v(out)" -minorTick {show "True"}  -splitLine {show "True"}
$chart SetOptions -title {} -legend {} -tooltip {trigger "axis"} -animation "False" -toolbox {feature {dataZoom {yAxisIndex "none"}}}
$chart Add "lineSeries" -name "initial, vdd=[@ $vSupplyVals 2]" -data $initialWaveform -showAllSymbol "nothing" -symbolSize "0"
$chart Add "lineSeries" -name "final, vdd=[@ $vSupplyVals 2]" -data $finalWaveform -showAllSymbol "nothing" -symbolSize "0"

set fbasename [file rootname [file tail [info script]]]
$chart Render -outfile [file normalize [file join .. html_charts ${fbasename}_waveforms_plot.html]]

# print resulted values for the highest supply voltage
puts "Optimization succesfully finished at generation [dget $results generation], total number of function evaluations - [dget $results nfev]"
puts "Convergence info: [dget $results info]"
puts "Best value of cost function is [format "%3e" [dget $results objfunc]]"
puts "Final width of PMOS is [format "%.3f" [= {$width/1e-3}]]mm, from initial value [format "%.3f" [= {$initialPWidth/1e-3}]]mm"
puts "For VDD=[@ $vSupplyVals 2]V, VLOW: [format "%.3f" $vlowInitial]V → [format "%.3f" $vlowFinal]V<[format "%.3f" $vLowLim]V, VHIGH: [format "%.3f" $vhighInitial]V → [format "%.3f" $vhighFinal]V>[format "%.3f" $vHighLim]V, PSUPPLY: [format "%.3f" $psupplyInitial]W → [format "%.3f" $psupplyFinal]W"

Waveforms are:

ticklEcharts !!!

And final numbers:

Optimization succesfully finished at generation 12, total number of function evaluations - 130
Convergence info: Optimization stopped due to crossing threshold 'abstol+reltol*abs(mean)=0.030176353097193406' of objective function population member standard deviation
Best value of cost function is 2.984087e+00
Final width of PMOS is 0.789mm, from initial value 10.000mm
For VDD=2.2V, VLOW: 0.147V → 0.046V<0.050V, VHIGH: 2.020V → 2.161V>1.950V, PSUPPLY: 1.101W → 0.107W