Add plotting scripts
diff --git a/scripts/.ztr-directory b/scripts/.ztr-directory
new file mode 100644
index 0000000..f5acee9
--- /dev/null
+++ b/scripts/.ztr-directory
@@ -0,0 +1 @@
+{"sorting":"name-up","project":null,"icon":null}
\ No newline at end of file
diff --git a/scripts/esd_clamp.ini b/scripts/esd_clamp.ini
new file mode 100644
index 0000000..66b7346
--- /dev/null
+++ b/scripts/esd_clamp.ini
@@ -0,0 +1,19 @@
+[Setup]
+maxCpus=10
+voltages=[1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5]
+corners=["tt", "sf", "ff", "fs", "ss", "ll", "hh", "hl", "lh"]
+
+[Files]
+baseDirectory='../analog/test/'
+spiceFile='test_esd_mc.spice'
+outputDirectory='esd_clamp'
+
+[Parameter]
+; ID=[-20e-6, -10e-6, 0, 10e-6, 20e-6]
+; FB_DIS=[0]
+; FB_PP_EN=[0]
+
+[Plot]
+tran_tran=None
+op_op=None
+
diff --git a/scripts/investigate.py b/scripts/investigate.py
new file mode 100644
index 0000000..aaf77fc
--- /dev/null
+++ b/scripts/investigate.py
@@ -0,0 +1,548 @@
+
+from configparser import ConfigParser
+
+# Path to ngspice library
+
+import os
+import math
+from spice import NgSpice, NgSpiceFile
+from ngspice_read import ngspice_read
+import numpy as np
+import pandas as pd
+from optimizer import NgOptimizer
+import re
+import sys
+from tool_box import ProcessingHelper
+import xarray as xr
+import pickle
+import time
+import ast
+from transistors import plotGmIdCurves
+from configparser import ConfigParser
+import gc
+# xr.show_versions()
+
+def example():
+ file_in='/home/simon/code/asic/analog/tia2/shahdoost.spice'
+ configfile='/home/simon/code/asic/analog/tia2/shahdoost.ini'
+ file_out='/home/simon/code/asic/analog/tia2/shahdoost_mod.spice'
+ # Read the config file
+ config = ConfigParser()
+ config.read(configfile)
+ parameters=config['Parameters']
+
+ # Generate a list parameters to be replaced
+ param={}
+ for k in parameters:
+ param[k]=parameters[k]
+
+ # Read the file
+ ngspice=NgSpice(file_in)
+ # Replace the parameters
+ replaceable=ngspice.findReplaceable()
+ for r in replaceable:
+ if not r.lower() in param.keys():
+ print(f'Warning: replaceable parameter found in spice but not present in .ini {r}')
+
+ ngspice.replace(param)
+
+ ngspice.write(file_out)
+ print (ngspice.pairs)
+
+
+def operatingPoint():
+ '''
+ Opens the given spice file and replaces the *#OP# with statements for extracting the operating point.
+ '''
+ spiceFile='../analog/tia2/shahdoost_optim1_manual.spice'
+ outDir='../analog/tia2/op'
+ outFile=os.path.join(outDir, 'run.spice')
+ if not os.path.exists(outDir):
+ os.mkdir(outDir)
+
+
+ ngSpiceFile=NgSpiceFile(spiceFile)
+ ngSpiceFile.opExtraction(outDir)
+ ngSpiceFile.write(outFile)
+
+
+def getAdditionalCoord(simu_result):
+ '''
+ Returns a list of user defined dimensions. User defined dimensions are dimensions which have a
+ name different than ['frequency', 'temp', 'model', 'mcrunno', 'spice_vector']
+
+
+ :returns: one_dim_coord, param_coord
+ '''
+
+ EXPECTED_DIMENSIONS=['frequency', 'temp', 'model', 'mcrunno', 'spice_vector']
+
+ one_dim_coord={}
+ param_coord={}
+ for dim in simu_result.dims:
+ if not dim in EXPECTED_DIMENSIONS:
+ if len(simu_result[dim])==1:
+ one_dim_coord[dim]=simu_result[dim].values
+ else:
+ param_coord[dim]=simu_result[dim].values
+ return one_dim_coord, param_coord
+
+def setAxisFromConig(ax, config, ngvector):
+ '''
+ Helper function for plotting. Sets the axis properties based on configuration values.
+ '''
+ # If we get None as configuration, the full plot is not configured. In that case return immediately
+ if config is None:
+ return ax
+
+ # We expect that all vectors are configured.
+ if not ngvector in config:
+ raise RuntimeError('No configuration found for vector {}'.format(ngvector))
+ config=config[ngvector]
+ if not config is None:
+ if 'ylim_top' in config:
+ ax.set_ylim(top=config['ylim_top'])
+ if 'ylim_bottom' in config:
+ ax.set_ylim(bottom=config['ylim_bottom'])
+ if 'title' in config:
+ ax.set_title(config['title'])
+ if 'y_label' in config:
+ ax.set_ylabel(config['y_label'])
+
+ return ax
+
+def plotOP(simu_result, outDir, simu_type, config):
+ '''
+ Generate plots for AC and NOISE analysis.
+
+ The following plots are generated
+ - x: frequency, y: AC analysis result, color: temperature.
+
+ '''
+ from matplotlib import pyplot as plt
+ print('Function plotOP')
+
+ spice_vectors=simu_result['spice_vector'].values
+ #temperatures=simu_result['temp'].values
+ #models=simu_result['model'].values
+ #freq=simu_result['frequency'].values
+ # Get rid of one dimensional coordinates
+ one_dim_coord, param_coord=getAdditionalCoord(simu_result)
+ coord=one_dim_coord
+ for ngvector in spice_vectors:
+ fig, ax = plt.subplots()
+ t=time.time()
+ plot_xr=simu_result.loc[{'spice_vector': ngvector}].to_dataset(name=ngvector)
+ #min_temp=min(temperatures)
+ #max_temp=max(temperatures)
+ plot_xr.plot.scatter(x='temp', y=ngvector, hue='model')
+ #, vmin=, vmax=85
+ print('Plot:', time.time()-t)
+
+ #ax.set_xscale("log")
+ ax.set_xlabel('Temperature / °C') # Add an x-label to the axes.
+ ax.set_ylabel(ngvector) # Add a y-label to the axes.
+ ax.set_title("{}".format(ngvector)) # Add a title to the axes.
+ ax.grid()
+ ax.set_ylim(bottom=0)
+ # Set the axis propoerties based on the configuration
+ ax=setAxisFromConig(ax, config, ngvector)
+ plt.tight_layout()
+ #ax.legend() # Add a legend.
+
+ #if ngvector=="v(v_vout)" or ngvector=="i(V0)":
+ # ax.set_ylim(ymin=0)
+ #
+ savePlot(plt, fig, simu_type, coord, ngvector, outDir)
+
+
+def plotMeasurements(simu_result, outDir, simu_type, config):
+ '''
+ Generate plots for measurements.
+
+ The following plots are generated
+ - x: temperature, y: Measurement result, color: Process corner.
+
+ '''
+ from matplotlib import pyplot as plt
+ print('Function plotMeasurements')
+
+ spice_vectors=simu_result['spice_vector'].values
+ #temperatures=simu_result['temp'].values
+ #models=simu_result['model'].values
+ #freq=simu_result['frequency'].values
+ # Get rid of one dimensional coordinates
+ one_dim_coord, param_coord=getAdditionalCoord(simu_result)
+ coord_fn=one_dim_coord
+ if param_coord == {}:
+ param_coord[None]=[None]
+ for ngvector in spice_vectors:
+ for p_k, p_v in param_coord.items():
+ for v in p_v:
+ fig, ax = plt.subplots()
+ t=time.time()
+ coord={'spice_vector': ngvector}
+ if not p_k is None:
+ coord[p_k]=v
+ coord_fn[p_k]=v
+ plot_xr=simu_result.loc[coord].to_dataset(name=ngvector)
+ #min_temp=min(temperatures)
+ #max_temp=max(temperatures)
+ plot_xr.plot.scatter(x='temp', y=ngvector, hue='model')
+ #, vmin=, vmax=85
+ print('Plot:', time.time()-t)
+
+ ax.set_xlabel('Temperature / °C') # Add an x-label to the axes.
+ ax.set_ylabel(ngvector) # Add a y-label to the axes.
+ ax.set_title("{}".format(ngvector)) # Add a title to the axes.
+ # Set the axis properties based on the configuration
+ ax=setAxisFromConig(ax, config, ngvector)
+
+ ax.grid()
+ plt.tight_layout()
+ #ax.legend() # Add a legend.
+
+ #if ngvector=="v(v_vout)" or ngvector=="i(V0)":
+ # ax.set_ylim(ymin=0)
+ #
+ savePlot(plt, fig, simu_type, coord_fn, ngvector, outDir)
+
+def plotAC(simu_result, outDir, simu_type, config):
+ '''
+ Generate plots for AC and NOISE analysis.
+
+ The following plots are generated
+ - x: frequency, y: AC analysis result, color: temperature.
+
+ '''
+ from matplotlib import pyplot as plt
+ print('Function plotAC')
+
+ spice_vectors=simu_result['spice_vector'].values
+ temperatures=simu_result['temp'].values
+ #models=simu_result['model'].values
+ #freq=simu_result['frequency'].values
+ # Get rid of one dimensional coordinates
+ one_dim_coord, param_coord=getAdditionalCoord(simu_result)
+ coord=one_dim_coord
+ for ngvector in spice_vectors:
+ fig, ax = plt.subplots()
+ t=time.time()
+ plot_xr=simu_result.loc[{'spice_vector': ngvector}].to_dataset(name=ngvector)
+ #min_temp=min(temperatures)
+ #max_temp=max(temperatures)
+ plot_xr.plot.scatter(x='frequency', y=ngvector, hue='temp', cmap='coolwarm', s=3, alpha=0.3, cbar_kwargs={'label': 'Temperature / °C'})
+ #, vmin=, vmax=85
+ print('Plot:', time.time()-t)
+
+ ax.set_xscale("log")
+ ax.set_xlabel('Frequency / Hz') # Add an x-label to the axes.
+ ax.set_ylabel(ngvector) # Add a y-label to the axes.
+ ax.set_title("{}".format(ngvector)) # Add a title to the axes.
+ # Set the axis propoerties based on the configuration
+ ax=setAxisFromConig(ax, config, ngvector)
+ ax.grid()
+ plt.tight_layout()
+ #ax.legend() # Add a legend.
+
+ #if ngvector=="v(v_vout)" or ngvector=="i(V0)":
+ # ax.set_ylim(ymin=0)
+ #
+ savePlot(plt, fig, simu_type, coord, ngvector, outDir)
+
+def plotTran(simu_result, outDir, simu_type, config):
+ '''
+ Generate plots for AC and NOISE analysis.
+
+ The following plots are generated
+ - x: frequency, y: AC analysis result, color: temperature.
+
+ '''
+ from matplotlib import pyplot as plt
+ print('Function plotTran')
+
+ spice_vectors=simu_result['spice_vector'].values
+ temperatures=simu_result['temp'].values
+ #models=simu_result['model'].values
+ #freq=simu_result['frequency'].values
+ # Get rid of one dimensional coordinates
+ one_dim_coord, param_coord=getAdditionalCoord(simu_result)
+ coord=one_dim_coord
+ for ngvector in spice_vectors:
+ fig, ax = plt.subplots()
+ t=time.time()
+ plot_xr=simu_result.loc[{'spice_vector': ngvector}].to_dataset(name=ngvector)
+ #min_temp=min(temperatures)
+ #max_temp=max(temperatures)
+ plot_xr.plot.scatter(x='time', y=ngvector, hue='temp', cmap='coolwarm', s=3, alpha=0.3, cbar_kwargs={'label': 'Temperature / °C'})
+ #, vmin=, vmax=85
+ print('Plot:', time.time()-t)
+
+ # ax.set_xscale("log")
+ ax.set_xlabel('Time / s') # Add an x-label to the axes.
+ ax.set_ylabel(ngvector) # Add a y-label to the axes.
+ ax.set_title("{}".format(ngvector)) # Add a title to the axes.
+ # Set the axis propoerties based on the configuration
+ ax=setAxisFromConig(ax, config, ngvector)
+ ax.grid()
+ plt.tight_layout()
+ #ax.legend() # Add a legend.
+
+ #if ngvector=="v(v_vout)" or ngvector=="i(V0)":
+ # ax.set_ylim(ymin=0)
+ #
+ savePlot(plt, fig, simu_type, coord, ngvector, outDir)
+
+
+def savePlot(plt, fig, simu_type, coord, ngvector, outDir):
+ '''
+ Saves the plot. Used by plotAC, plotMeasurements etc. to avoid redundant code.
+ '''
+ t=time.time()
+ fname_base="{}_".format(simu_type)
+ for k, v in coord.items():
+ fname_base+="{}_{}_".format(k, v)
+ fname_base+="{}".format(ngvector)
+
+ file=os.path.join(outDir, fname_base+".svg")
+ image_format = 'svg' # e.g .png, .svg, etc.
+ #fig.savefig(file, format=image_format, dpi=1200)
+ file=os.path.join(outDir, fname_base+".png")
+ file_small=os.path.join(outDir, fname_base+"_small.png")
+ image_format = 'png' # e.g .png, .svg, etc.
+ fig.savefig(file, format=image_format, dpi=1200)
+
+ fig.savefig(file_small, format=image_format, dpi=300)
+ plt.close(fig)
+ print('Save plot:', time.time()-t)
+
+def investigateCorners(workingDir, spiceFile, voltages, corners, maxCpu, plot_config=None, param={}, mc=True):
+ '''
+ Replaces #model# by the name of a simulation model and investigates the corners.
+
+
+ '''
+
+ # Operating voltages
+
+ # Typically troublesome corners
+ #corners=["tt"]#""sf", "tt", "fs"]
+ # Device mismatch corners
+ if mc:
+ corners_mm=[c+'_mm' for c in corners]
+
+ run=True
+
+ # Run spice
+ ngspice_instances=[]
+ for voltage in voltages:
+ outDir=os.path.join(workingDir, 'corners_{}V'.format(voltage))
+ if not os.path.exists(outDir):
+ os.mkdir(outDir)
+ spiceParam=param
+ spiceParam['model']=corners_mm
+ spiceParam['UB']= voltage
+
+ # Run spice
+
+ #xr_file="ngspice_small_v{}.xr".format(voltage)
+ pickleFile="ngspice_rawres_v{}.pickle".format(voltage)
+ pickleFile=os.path.join(workingDir, pickleFile)
+ ngspice=NgSpice(spiceFile, outDir, parallel=True, max_cpus=maxCpu)
+ if run:
+ result=ngspice.run(spiceParam, delete=True)
+ ngspice.savePickle(pickleFile)
+
+ else:
+ ngspice.loadPickle(pickleFile)
+
+ ngspice_instances.append(ngspice)
+
+ # Convert to xarray
+ # We limit ourselves to 4 cores to not use excessive memory
+ p=ProcessingHelper(parallel=True, maxCpu=4)
+ for ngspice in ngspice_instances:
+ p.add(ngspice.getResultAsDataArray, [])
+ results_xr=p.run()
+
+ # Free memory
+ for ngspice in ngspice_instances:
+ del ngspice
+ del ngspice_instances
+ gc.collect()
+
+ # Plot
+ # We limit ourselves to 4 cores to not use excessive memory
+ # TODO: use multiprocessing.shared_memory to make out xarrays accessible to all
+ # processes, this should enable a significant speedup.
+ p=ProcessingHelper(parallel=True, maxCpu=4)
+ for result_xr in results_xr:
+ if False:
+ pickleFileXr="ngspice_small_xr_v{}.pickle".format(voltage)
+ if run:
+ with open(pickleFileXr, 'wb') as file:
+ pickle.dump(result_xr, file)
+ else:
+ with open(pickleFile, 'rb') as file:
+ result_xr=pickle.load(file)
+
+ # Plot measurements
+ for simu_type in result_xr.keys():
+ for simu_name in result_xr[simu_type]:
+ print(simu_type, simu_name)
+ simu_result=result_xr[simu_type][simu_name]
+ outDir="{}_{}".format(simu_type, simu_name)
+ if not outDir.lower() in plot_config:
+ raise(RuntimeError('Plot configuration missing for {}'.format(outDir.lower())))
+ config_thisplot=plot_config[outDir.lower()]
+ outDir=os.path.join(workingDir, outDir)
+ if not os.path.exists(outDir):
+ os.mkdir(outDir)
+ if simu_type.upper() == 'MEAS':
+ p.add(plotMeasurements, [simu_result, outDir, simu_type, config_thisplot])
+ elif simu_type.upper() == 'AC':
+ p.add(plotAC, [simu_result, outDir, simu_type, config_thisplot])
+ elif simu_type.upper() == 'NOISE':
+ p.add(plotAC, [simu_result, outDir, simu_type, config_thisplot])
+ elif simu_type.upper() == 'OP':
+ p.add(plotOP, [simu_result, outDir, simu_type, config_thisplot])
+ elif simu_type.upper() == 'TRAN':
+ p.add(plotTran, [simu_result, outDir, simu_type, config_thisplot])
+ else:
+ raise(RuntimeError('Unrecognized simulation type {}'.format(simu_type)))
+
+ #result_ds=xr.Dataset.from_dict(result_xr)
+ #result_xr=result_ds.to_dict()
+
+ # xr_list=[]
+ # xr_file_list=[]
+ # for simu_type in result_xr.keys():
+ # for simu_name in result_xr[simu_type]:
+ # xr_file="ngspice_{}_{}_v{}.xr".format(simu_type, simu_name, voltage)
+ # xr_file=os.path.join(workingDir, xr_file)
+ # xr_file_list.append(xr_file)
+ # result_ds=result_xr[simu_type][simu_name].to_dataset(name=f"{simu_type}_{simu_name}")
+ # xr_list.append(result_ds)
+ #
+ # xr.save_mfdataset(xr_list, xr_file_list)
+ #
+ #
+ # for simu_type in result_xr.keys():
+ # for simu_name in result_xr[simu_type]:
+ # xr_file="ngspice_{}_{}_v{}.xr".format(simu_type, simu_name, voltage)
+ # xr_file=os.path.join(workingDir, xr_file)
+ # xr_file_list.append(xr_file)
+ # result_ds=result_xr[simu_type][simu_name].to_dataset(name=f"{simu_type}_{simu_name}")
+ # xr_list.append(result_ds)
+ #
+ # xr.save_mfdataset(xr_list, xr_file_list)
+ #
+
+ print('Now starting parallel plotting')
+ p.run()
+ print("Done!")
+
+class AcSimulationResult:
+ '''
+ Provides access to AC simulation results and helper functions for plotting.
+ '''
+ def __init__(self, simulationResult):
+ '''
+ :param simulationResult.
+ '''
+ pass
+
+ def getParameters(self):
+ '''
+ Returns the parameters supplied to the simulation as data frame. This also includes the temperature.
+ '''
+ pass
+
+ def getReslutByParameter(self):
+ pass
+
+def main_raw():
+ file='../analog/transistors/test_nmos3.raw'
+ ngread=ngspice_read(file)
+ print('Breakpoint')
+
+
+def test_tia():
+ workingDirBase='../analog/test/'
+ # spiceFile=os.path.join(workingDir, 'test_bandgap_mc.spice')
+ spiceFile=os.path.join(workingDirBase, 'test_tia_rgc_full_mc.spice')
+ darkcur = [-5e-6, -2.5e-6, 0, 2.5e-6, 5e-6]
+ #, -2.5e-6, 0, 2.5e-6, 5e-6]
+ # for darkcur in [-5e-6, -2.5e-6, 0, 2.5e-6, 5e-6]:
+ param={'ID': darkcur}
+ workingDir= os.path.join(workingDirBase, 'wd')
+ if not os.path.exists(workingDir):
+ os.mkdir(workingDir)
+ investigateCorners(workingDir, spiceFile, param)
+
+def investigateIni():
+ '''
+ Investigate corners using parameters given in an ini file.
+ '''
+ if len(sys.argv) != 2:
+ print('Wrong arguments. Usage: investigate.py iniFile.')
+ quit()
+
+ parser=ConfigParser()
+ parser.read(sys.argv[1])
+ # Read files section and assemble pathes
+ baseDir=ast.literal_eval(parser['Files']['baseDirectory'])
+ spiceFile=ast.literal_eval(parser['Files']['spiceFile'])
+ spiceFile=os.path.join(baseDir, spiceFile)
+ outDir=ast.literal_eval(parser['Files']['outputDirectory'])
+ outDir= os.path.join(baseDir, outDir)
+ # Read voltages and corners
+ voltages=ast.literal_eval(parser['Setup']['voltages'])
+ corners=ast.literal_eval(parser['Setup']['corners'])
+ maxCpu=ast.literal_eval(parser['Setup']['maxCpus'])
+
+ plot_config={}
+ for section in parser['Plot']:
+ value=parser['Plot'][section]
+ plot_config[section]=ast.literal_eval(value)
+
+ # Create the output directory
+ if not os.path.exists(outDir):
+ os.mkdir(outDir)
+
+ param={}
+ for k,v in parser['Parameter'].items():
+ param[k.upper()]=ast.literal_eval(v)
+
+ investigateCorners(outDir, spiceFile, voltages, corners, maxCpu, plot_config, param)
+ print('Done')
+
+
+
+def test_iref():
+ mode='OP'
+ workingDirBase='../analog/test/'
+ # spiceFile=os.path.join(workingDir, 'test_bandgap_mc.spice')
+ spiceFile=os.path.join(workingDirBase, 'test_low_pvt_source_mc.spice')
+ workingDir= os.path.join(workingDirBase, 'isource')
+ if not os.path.exists(workingDir):
+ os.mkdir(workingDir)
+ investigateCorners(workingDir, spiceFile, mode)
+
+
+if __name__ == "__main__":
+
+ investigateIni()
+ #task='MC'
+ #
+ #if task == 'MC':
+ # test_iref()
+ #test_tia()
+ #MODE='OP'
+ # elif task == 'plotOP':
+ # TRANSISTOR_TYPES=['nfet_01v8_lvt', 'pfet_01v8_lvt', 'pfet_01v8_hvt', 'nfet_01v8', 'pfet_01v8']
+ # for t in TRANSISTOR_TYPES:
+ # plotGmIdCurves(t, 0.4)
+
+ #operatingPoint()
+ # optimize2()
diff --git a/scripts/isource.ini b/scripts/isource.ini
new file mode 100644
index 0000000..9d8f261
--- /dev/null
+++ b/scripts/isource.ini
@@ -0,0 +1,14 @@
+[Setup]
+maxCpus=15
+voltages=[1.7, 1.8, 1.9]
+corners=["tt", "sf", "ff", "fs", "ss", "ll", "hh", "hl", "lh"]
+
+[Files]
+baseDirectory='../analog/test/'
+spiceFile='test_low_pvt_source_mc.spice'
+outputDirectory='isource'
+
+[Parameter]
+; ID=[-20e-6, -10e-6, 0, 10e-6, 20e-6]
+; FB_DIS=[0]
+; FB_PP_EN=[0]
diff --git a/scripts/isource_pls.ini b/scripts/isource_pls.ini
new file mode 100644
index 0000000..f284bd2
--- /dev/null
+++ b/scripts/isource_pls.ini
@@ -0,0 +1,18 @@
+[Setup]
+maxCpus=10
+voltages=[1.7, 1.8, 1.9]
+corners=["tt", "sf", "ff", "fs", "ss", "ll", "hh", "hl", "lh"]
+
+[Files]
+baseDirectory='../layout/isource/'
+spiceFile='test_low_pvt_source_mc.spice'
+outputDirectory='pls'
+
+[Parameter]
+; ID=[-20e-6, -10e-6, 0, 10e-6, 20e-6]
+; FB_DIS=[0]
+; FB_PP_EN=[0]
+
+[Plot]
+OP_OP={'i(V0)':{'ylim_top': 1e-5, 'title':'Current source output', 'y_label':'Output current / A'}, 'vdd':None}
+
diff --git a/scripts/optimize.py b/scripts/optimize.py
new file mode 100644
index 0000000..a0b86a6
--- /dev/null
+++ b/scripts/optimize.py
@@ -0,0 +1,96 @@
+def optimize():
+
+ file_in='/home/simon/code/asic/analog/tia2/shahdoost_optim1.spice'
+ workingDir='/home/simon/code/asic/analog/tia2/shahdoost_optim1'
+ cacheDir='/home/simon/optimCache'
+ if not os.path.exists(workingDir):
+ os.mkdir(workingDir)
+ if not os.path.exists(cacheDir):
+ os.mkdir(cacheDir)
+
+ ngSpice=NgSpice(file_in, workingDir, parallel=True)
+ spiceReplace={'vdd#branch':'supply_current'}
+ ngOpimizer=NgOptimizer(ngSpice, spiceReplace, cacheDir)
+ #fixedParam={'V_SUPPLY': 1.8, 'M1_L': 2, 'M2_L': 0.3, 'M3_L': 0.3}
+ fixedParam={'V_SUPPLY': 1.8} #, 'M1_L': 2, 'M2_L': 0.3, 'M3_L': 0.3}
+ # optimize={
+ # 'M1_W':[2, 1, 25],\
+ # 'M2_W':[2, 1, 25],\
+ # 'M3_W':[2, 1, 25],\
+ # 'V_BIAS1': [0.8, 0.5, 1.8],\
+ # 'V_BIAS2': [1, 0.5, 1.8],\
+ # 'R3_L': [10, 5, 100],\
+ # }
+ #
+ optimize={
+ 'M1_W':[2, 1, 25],\
+ 'M2_W':[2, 1, 25],\
+ 'M3_W':[2, 1, 25],\
+ 'M1_L':[2, 0.15, 6],\
+ 'M2_L':[0.3, 0.15, 3],\
+ 'M3_L':[0.3, 0.15, 3],\
+ 'V_BIAS1': [0.8, 0, 1.2],\
+ 'V_BIAS2': [1, 0.6, 1.8],\
+ 'R3_L': [10, 5, 100],\
+ }
+
+
+ ngOpimizer.setFixedParam(fixedParam)
+ ngOpimizer.setOptimizerParam(optimize)
+ # Define the cost functions.
+ ngOpimizer.setCost({'bw': "lambda x: 0 if x > 1E9 else math.log(abs(x-1E9), 10)*100",\
+ 'dc_gain': "lambda x: 0 if x > 45 else abs((x-45)*100)",\
+ 'supply_current': "lambda x: 0 if abs(x) < 1E-2 else (abs(x)-1E-2)*100",\
+ })
+ ngOpimizer.optimize()
+
+
+
+
+def optimize2():
+
+ file_in='/home/simon/code/asic/analog/tia/tia_lownoise2_optim.spice'
+ workingDir='/home/simon/code/asic/analog/tia/optim'
+ cacheDir='/home/simon/optimCache'
+ if not os.path.exists(workingDir):
+ os.mkdir(workingDir)
+ if not os.path.exists(cacheDir):
+ os.mkdir(cacheDir)
+
+ ngSpice=NgSpice(file_in, workingDir, parallel=True)
+ spiceReplace={'vdd#branch':'supply_current'}
+ ngOpimizer=NgOptimizer(ngSpice, spiceReplace, cacheDir)
+ #fixedParam={'V_SUPPLY': 1.8, 'M1_L': 2, 'M2_L': 0.3, 'M3_L': 0.3}
+ fixedParam={}#'V_SUPPLY': 1.8} #, 'M1_L': 2, 'M2_L': 0.3, 'M3_L': 0.3}
+ # optimize={
+ # 'M1_W':[2, 1, 25],\
+ # 'M2_W':[2, 1, 25],\
+ # 'M3_W':[2, 1, 25],\
+ # 'V_BIAS1': [0.8, 0.5, 1.8],\
+ # 'V_BIAS2': [1, 0.5, 1.8],\
+ # 'R3_L': [10, 5, 100],\
+ # }
+ #
+ optimize={
+ 'M8':[2, 0.3, 3],\
+ 'M7':[2, 0.3, 3],\
+ 'M5':[2, 0.3, 3],\
+ 'I3':[50, 30, 100],\
+ 'M15':[0.5, 0.3, 3],\
+ 'R1':[0.5, 1, 6]
+ }
+
+
+ ngOpimizer.setFixedParam(fixedParam)
+ ngOpimizer.setOptimizerParam(optimize)
+ # Define the cost functions.
+ #gain max_gain min_gain edge_freq
+ #inoise_total onoise_total
+ # 'max_gain': "lambda x: 0 if x > 45 else abs((x-45)*100)",\
+ # 'min_gain': "lambda x: 0 if abs(x) < 1E-2 else (abs(x)-1E-2)*100",\
+
+ ngOpimizer.setCost({'gain': "lambda x: 0 if x > 50 else (50-x)*1000",\
+ 'edge_freq': "lambda x: 0 if x > 1E9 else math.log(abs(x-1E9), 10)*100",\
+ 'inoise_total': "lambda x: 0 if x < 1.6E-7 else (x-1.6E-7)*1E7",\
+ })
+ ngOpimizer.optimize()
diff --git a/scripts/optimizer.py b/scripts/optimizer.py
new file mode 100644
index 0000000..dfbc6b3
--- /dev/null
+++ b/scripts/optimizer.py
@@ -0,0 +1,310 @@
+from scipy.optimize import least_squares, differential_evolution
+import multiprocessing
+import pandas as pd
+from matplotlib.mlab import angle_spectrum
+import numpy as np
+import inspect
+import gc
+import time
+from multiprocessing import reduction
+import math
+import os
+import hashlib
+import pickle
+
+class NgOptimizer():
+ '''
+ Implements a circuit optimizer
+
+ '''
+ def __init__(self, ngSpiceInstance, spiceReplace, cacheDir):
+ '''
+
+ :param ngSpiceInstance:
+ :param spiceReplace:
+
+ '''
+ self.fixedParam={}
+ self.optimizeParam={}
+ self.spiceReplace=spiceReplace
+ self.method='diffevol'
+ self.ngSpiceInstance=ngSpiceInstance
+ if self.method=='diffevol':
+ self.ngSpiceInstance.parallel=False
+ self.cacheDir=cacheDir
+
+
+ def setFixedParam(self, param, keepOld=False):
+ '''
+
+ '''
+ if not keepOld: self.fixedParam={}
+
+ for k, v in param.items():
+ self.fixedParam[k] = v
+
+ def setOptimizerParam(self, optimizeParam, keepOld=False):
+ '''
+
+ :param initialGuess: {value: [ignored, min, max] }
+ :tpye initialGuess: dict
+ '''
+ #key: value: list; [0]: ignored [1]: min [2]: max
+ if not keepOld: self.optimizeParam={}
+
+ for k, v in optimizeParam.items():
+ self.optimizeParam[k] = v
+
+
+
+ def setCost(self, cost):
+ '''
+ '''
+ self.cost=cost
+
+
+ def optimize(self):
+ '''
+
+ '''
+ self.ngSpiceInstance.setConstants(self.fixedParam, keepOld=False)
+ self.spiceFileHash=self.ngSpiceInstance.getFileHash()
+ self.cacheDir=os.path.join(self.cacheDir, self.spiceFileHash)
+
+ if not os.path.exists(self.cacheDir):
+ os.mkdir(self.cacheDir)
+
+ # We need a defined sequence of parameters
+ self.parSequence=list(self.optimizeParam.keys())
+ self.costSequence=list(self.cost.keys())
+ # Generate a list out of the initial guess dictionary.
+ initialGuess=[self.optimizeParam[k][0] for k in self.parSequence]
+ # Generate a dummy result
+
+ # TODO: Perform a test run with spice using the initial guess to verify we get results for all parameters needed for cost calculation.
+
+
+ # Get boudaries for parameter space
+ bounds=[(self.optimizeParam[k][1],self.optimizeParam[k][2]) for k in self.parSequence]
+ if self.method == 'diffevol':
+ self.firstRun=True
+ default=9e10
+# bestResult=differential_evolution(differentialEvolutionFunc, bounds, args=(default), maxiter=1)#, workers=-1)#, bounds, args=default, )
+
+ bestResult=differential_evolution(self._deFunc, bounds, workers=-1, mutation=(0.1,1.5), seed=1)#, )#, bounds, args=default, )
+ print(bestResult)
+ resultDict={k:v for k,v in zip(self.parSequence, bestResult['x'])}
+ print('Optimization result: ', resultDict)
+
+ elif self.method == 'leastsq':
+ default=[9e10 for k in self.cost.keys()]
+
+ # Iterate
+ for iter in range(10):
+ simulationResult={}
+ for iteration in range(20):
+ maxfev=(iteration+1)*multiprocessing.cpu_count()*2
+
+ print("maxfev: ", maxfev)
+ bestResult=least_squares(leastsqFunc, initialGuess, args=(simulationResult, default), max_nfev=maxfev, method='dogbox')
+ print('Iteration: ', iteration)
+ print("Best result: ", bestResult['x'])
+ print("Cost: ", bestResult['fun'])
+ spiceResult=self._runSpiceMulti(simulationResult)
+ simulationResult=self._calcCost(simulationResult, spiceResult)
+ #print(simulationResult)
+
+ initialGuess=bestResult['x']
+ gc.collect()
+
+ #elif self.method == 'leastsq'
+
+
+ def _deFunc(self, param):
+ '''
+
+ '''
+
+ # We use a cache to speed up multiple runs.
+ paramHash=hashlib.sha1(param.tobytes()).hexdigest()
+
+ cacheFile=os.path.join(self.cacheDir, paramHash)
+
+
+ if os.path.exists(cacheFile):
+ # If we have a cache hit simply load the result from the file.
+ with open(cacheFile, 'rb') as file:
+ result, cost=pickle.load(file)
+ else:
+ spiceResult=self._runSpiceSingle(param)
+ result=self._spiceResultToDict(spiceResult, True)
+ if len(result) != 1:
+ raise(RuntimeError(f'This is a bug. Expected 1 simulation result. Got {len(result)}'))
+ cost=self._calcCostSingle(result[0])
+ with open(cacheFile, 'wb') as file:
+ pickle.dump((result, cost), file)
+ sumCost=np.sum(cost)
+ print('\nResult: ', result[0], ' Cost: ', cost, ' Sum Cost: ', sumCost)
+ return sumCost
+
+ def _runSpiceSingle(self, param):
+ forSpice=[]
+ # Generate a dataframe we can provide to out NgSpice class as input.
+ # Combine the parameter names from parSequence with the values requested by least_squares.
+ forSpice.append({k:p for k,p in zip(self.parSequence, param)})
+ # Transform the dict to a dataframe
+ df=pd.DataFrame.from_dict(forSpice)
+ # Run Ngspice
+ spiceResult=self.ngSpiceInstance.run(df, delete=True, rename=self.spiceReplace)
+
+ return spiceResult
+
+ def _runSpiceMulti(self, simulationResult):
+ '''
+ Run the spice simulation
+ '''
+
+ forSpice=[]
+ # Generate a dataframe we can provide to out NgSpice class as input.
+ for par, simResult in simulationResult.items():
+ # If for given parameters we don't have a simulation result create an input for Spice
+ if simResult[1] is None:
+ # Combine the parameter names from parSequence with the values requested by least_squares.
+ forSpice.append({k:p for k,p in zip(self.parSequence, simResult[0])})
+
+ # Transform the dict to a dataframe
+ df=pd.DataFrame.from_dict(forSpice)
+
+ # Run Ngspice
+ spiceResult=self.ngSpiceInstance.run(df, delete=True, rename=self.spiceReplace)
+ return spiceResult
+
+ def _spiceResultToDict(self, spiceResult, ignoreMissingResult):
+ '''
+ Converts the results of a series of spice simulations to dictionaries.
+
+ :param spiceResult: Result of :py:meth:ǸgSpice.run.
+
+ :returns: A list of dictionaries: The key in the dictionary is part of self.costSequence. The value corresponds to the output of spice.
+ '''
+ # Our simulation result should match our cost functions.
+ # Flatten the result and convert the pandas data frames to a dictionary.
+ # The column names of interest are the key of the initial guess.
+ columnNames=self.cost.keys()
+
+ result=[]
+
+ for spiceR in spiceResult:
+
+ resultDict={}
+ # Fill the dictionary.
+ for column in columnNames:
+ for res in spiceR['results']:
+ if column in res.columns:
+ # Only consider unique results.
+ resultDict[column] = res[column].unique()
+
+ # Do sanity checks.
+ for column in columnNames:
+
+ # First of all we want a result for each parameter for which a cost was defined.
+ if not column in resultDict:
+ # Handle the situation in which we don't have a result.
+ if ignoreMissingResult:
+ resultDict[column] = np.NaN
+ else:
+ raise(RuntimeError(f'No spice simulation result for item {column}'))
+ self.firstRun=False
+ # Try to convert numpy arrays to float.
+ if type(resultDict[column]) == np.ndarray:
+ if len(resultDict[column]) == 1:
+ resultDict[column]=resultDict[column][0]
+ # If the have a result it has to be unique.
+ datatype= type(resultDict[column])
+ if not datatype == float and not datatype == np.float64:
+ raise(RuntimeError(f"Got data type {datatype}. I'm interpreting this as non-unique simulation result"))
+ # Add the dictionary to the result
+ result.append(resultDict)
+
+ return result
+
+ def _calcCost(self, simulationResult, spiceResult):
+
+ # Fill simulationResult with the results of the simulation.
+ for par, simResult in simulationResult.items():
+ # We only need to add a result if none is there.
+ if simResult[1] is None:
+ # Reconstruct the spice input parameters
+ forSpice={k:p for k,p in zip(self.parSequence, simResult[0])}
+ # Search the simulation result for the given simulation set of input parameters.
+ for s in spiceResult:
+ if forSpice == s['param']:
+ # Apply cost funtion
+ #result=s['results']
+ resultDict=self._spiceResultToDict(s, True)
+
+ costResult=self._calcCostSingle(resultDict)
+
+ simulationResult[par] = (simResult[0], costResult)
+ # Do some sanity checks. E.g. if we don't have a result, we should fail.
+ if simulationResult[par][1] is None:
+ raise(RuntimeError('Failed to find simulation results for a given set of parameters. This is a Bug.'))
+ return simulationResult
+
+ def _calcCostSingle(self, result):
+ '''
+ Apply the cost function to a single simulation result.
+ '''
+ costResult=[]
+
+ for costName in self.costSequence:
+ # Get the numeric result
+ resultValue=result[costName]
+ # Get the cost function
+ costFunction=eval(self.cost[costName])
+ # Apply the const function
+ # If we have a NaN reutrn a very large cost
+ if np.isnan(resultValue):
+ costResult=[1E9 for x in self.costSequence]
+ #if sumCost:
+ return costResult
+ else:
+ try:
+ cost=costFunction(resultValue)
+ except:
+ print("Cost calculation failed.")
+ print(f"Spice simulation result: {resultValue}")
+ print(f"Cost name: {costName}")
+ print(f"Cost function: ", inspect.getsource(costFunction))
+ raise
+ costResult.append(cost)
+ # Write the result of the cost calculation to the simulation result.
+# if sumCost:
+ # costResult=np.sum(costResult)
+
+ return costResult
+
+def differentialEvolutionFunc(a, b):
+ '''
+
+ '''
+ print("Function call")
+
+
+def leastsqFunc(param, result, default):
+ '''
+ Funtion for least_squares. We don't run spice in here. Instead we collect input for spice and return know spice parameters.
+ '''
+
+ hash=str(param)
+
+ if hash in result:
+ res= result[hash][1]
+ else:
+ result[hash] = (param, None)
+ res=default
+ print(param,res)
+ if res is None:
+ res = default
+ return res
+
diff --git a/scripts/spice.py b/scripts/spice.py
new file mode 100644
index 0000000..4ed1b8b
--- /dev/null
+++ b/scripts/spice.py
@@ -0,0 +1,634 @@
+import os
+import subprocess
+from subprocess import PIPE
+from multiprocessing import Pool, cpu_count
+import shutil
+import re
+import pandas as pd
+from multiprocessing import Lock
+import hashlib
+import math
+import xarray as xr
+import numpy as np
+import time
+import pickle
+class SpiceAnalysis():
+ def __init__(self):
+ pass
+
+
+class RawFile():
+ '''
+ Read the given ngspice .raw file
+ '''
+ def __init__(self, file):
+ pass
+
+class NgSpiceFile():
+ '''
+ Reading and modifying an ngspice file
+
+ '''
+ def __init__(self, file):
+ self.spice=[]
+ self.file=file
+ self.paramPairs={}
+ self.commandBlPairs={}
+ with open(self.file, 'r') as myfile:
+ for line in myfile:
+ self.spice.append(line)
+
+ def findReplaceable(self):
+ '''
+ Searches the file for replaceable parameters.
+ Replaceable parameters are characterized by a name enclosed in # without spaces. E.g. #Name#
+ Note: The name #OP# is reserved. It must not be used and will be ignored.
+
+ '''
+ result=[]
+ for line in self.spice:
+ candidate=line.split('#')
+ for i, c in enumerate(candidate):
+ # The first candidate in a line is not valid
+ if i == 0:
+ continue
+ # OP is not a valid candidate
+ if c == 'OP':
+ continue
+ if not ' ' in c and not '\n' in c:
+ result.append(c)
+ return result
+
+ def opExtraction(self, directory):
+ '''
+ Replaces the keyword *#OP# with a block saving information about operating point of all transistors including capacitances.
+ Make sure the have an OP statement before the keyword *#OP#. Also, *#OP# must be part of a control block.
+
+ :param directory: Directory into which the operating point information should be written.
+ '''
+
+ EXTRACT_PARAM=["vds", "gm", "vth", "vgs", "id", "gds", "cgg", "cgs", "cgd", "cgb", "cdg", "cds", "cdd", "cdb", "csg", "css", "csd", "csb", "cbg", "cbs", "cbd", "cbb"]
+
+ transistors = self._getTransistorModelNames()
+
+ # Generate the string for
+
+ string = ""
+ for name, model in transistors.items():
+ outfile=os.path.join(directory, f"op_{name}.csv")
+ subString="wrdata {} ".format(str(outfile))
+ for param in EXTRACT_PARAM:
+ subString+="@m.{}.m{}[{}] ".format(name.lower(), model, param)
+
+ string += f"{subString}\n"
+
+ self.commandBlPairs["*#OP#"] = string
+
+ def _getTransistorModelNames(self):
+ '''
+ Returns a list of the model names of transistors. Note: This only works for SK130A for now as we search for sky130 in the model name.
+
+ '''
+ result={}
+ for line in self.spice:
+ candidate=line.split(' ')
+ # Check if the name contains XM which stands for transistor
+ #print(candidate[0][:2])
+ if candidate[0][:2] == 'XM':
+ for c in candidate:
+ #print(c[:6])
+ # Model names contain "sky130" at the beginning. Find the model name.
+ if c[:6] == "sky130":
+ result[candidate[0]] = c
+
+ return result
+
+ def replace(self, pairs, keepOld=False):
+ '''
+ Replace the replaceable parameters
+
+ :param pairs: Dictionary, They key is the name of the replaceable parameters. The value the value to be written to the output file.
+ :type pairs: dict
+ '''
+
+ replaceable=self.findReplaceable()
+ #
+ if not keepOld: self.paramPairs={}
+
+ for k, v in pairs.items():
+ if not k in replaceable:
+ raise(RuntimeError(f'Cannot replace {k}. {k} not found in spice file'))
+ self.paramPairs[k] = v
+
+ def write(self, file):
+ '''
+ Writes out the modified spice file. The file is written to the rundir given when instatiating the class.
+
+ :param
+
+ '''
+
+ # Check if we have parameters for all replaceable strings in the spice file.
+ replaceable=self.findReplaceable()
+ for r in replaceable:
+ if not r in self.paramPairs.keys():
+ raise(RuntimeError(f'No value found for {r}. Please provide values for all replaceable strings.'))
+
+ self.outfile=file
+ #outfile=os.path.join(self.rundir, file)
+
+ outspice=[]
+ for line in self.spice:
+ # Replace the parameters
+ for k,v in self.paramPairs.items():
+ line=line.replace('#'+k+'#', str(v))
+
+ # Replace command blocks
+ for k,v in self.commandBlPairs.items():
+ line=line.replace(k, v)
+
+ outspice.append(line)
+
+ # Write to file.
+ with open(self.outfile, 'w') as outfile:
+ for line in outspice:
+ outfile.write(line)
+
+def splitNgspiceFileName(fileName):
+ '''
+ Splits the file name returned by ngspice into it's parts.
+
+ Please follow the following naming scheme:
+ result_type_name_temperature_mcrunno.csv
+ In detail:
+
+ - "result": Constant. Must be present for :py:class:`Ngspice` to recognize the file.
+ - "type": Currently allowed are "AC", "NOISE", "MEAS".
+ - "name": You might want to have multiple simulations of the same type. You can distinguish them by name.
+ - "temperature": The temperature followed by deg. E.g. 80deg.
+ - "mcrunno": Number of the monte carlo run.
+
+ :returns: type, name, temperature, mcrunno
+ '''
+ try:
+ # Remove extension
+ fileName=os.path.splitext(fileName)[0]
+ fileName_split=fileName.split('_')
+ simu_type=fileName_split[1]
+ simu_name=fileName_split[2]
+ simu_temp=re.findall('[0-9]*deg', fileName_split[3])
+ simu_temp=re.findall('[0-9]*', simu_temp[0])
+ simu_temp=int(simu_temp[0])
+ #mcrunno=fileName_split[4].split('.')
+ mcrunno=int(fileName_split[4])
+ except:
+ print('Failed to parse file name {}'.format(fileName))
+ raise
+
+ return simu_type, simu_name, simu_temp, mcrunno
+
+
+class NgSpice():
+ '''
+ Enables investigation of a parameter space using parallel processes of ngspice.
+
+ Concept and usage:
+ * In the spice file mark parameters to be altered with #Name#.
+ * In the spice file mark the position at which the operating point should be saved with "*#OP#".
+ * In the spice file write results to .csv files. The filename has to comply with the convetions defined in :py:func:`splitNgspiceFileName`
+ * Instantiate this class for the given spice file. You can then call :py:meth:`setParameterSpace` to replace the parameters to be altered.
+
+ '''
+ def __init__(self, file, rundir, parallel=True, max_cpus=24):
+ '''
+ :param rundir: Directory used for temporary storage.
+ :param file: Spice file.
+ :param parallel: Optional. Defaults to True. Set to false to disable processing on multiple cpus.
+ :param max_cpus: Maximum number of CPUs to be used. Defaults to 24.
+
+ '''
+ self.rundir=rundir
+ self.file=file
+ #self.includeFiles=includeFiles
+ self.constantPairs={}
+
+ self.ngspiceFile = file
+ self.parallel=parallel
+ self.folderOffset=0
+ self.simulation_result=None
+ self.max_cpus=max_cpus
+ # This is obsolete. The function is coverd by spiceParam.
+ # def setConstants(self, pairs, keepOld):
+ # '''
+ # Sets the parameters to be replaced by constants.
+ #
+ # :param pairs: Dictionary, They key is the name of the replaceable parameters. The value the value to be written to the output file.
+ # :type pairs: dict
+ # '''
+ # if not keepOld: self.pairs={}
+ #
+ # for k, v in pairs.items():
+ # self.constantPairs[k] = v
+ #
+
+ def getFileHash(self):
+ '''
+ Returns a hash for the spice file.
+
+ '''
+ hash=hashlib.sha1()
+
+ with open(self.ngspiceFile) as file:
+ for line in file:
+ hash.update(line.encode('utf-8'))
+
+ return hash.hexdigest()
+
+ def getResultAsDataArray(self):
+ '''
+ Groups the results from :py:meth:`Ngspice.run` by simulation type and converts the results
+ to a big dataframe.
+
+ The function :py:meth:`Ngspice.run` returns a list of simulation results.
+ The results may stem from AC analysis, OP calculation, measurements, etc.
+ The results are also not organized in a very handy form.
+
+ This function will group the results by measurement type. Since there is no easy way to infer
+ the type of simulation from the results you must provide the type of data via the file
+ name generated by ngspice.
+
+ :returns: {"type": {"name": xarray}}
+
+ '''
+ # Our simulation result is multidimensional. We use xarrays to store the data.
+ # Uppon creation of the array we need to know the exact size of the array.
+ # Some dimensions are known from spiceParam. Others, in particular temperature need to be
+ # inferred from the simulation results. Therefore we iterate over our simulation results twice.
+ # Firstly we extract the parameters given to the simulation from within ngspice.
+ # Secondly we initialize fill the xarray, then we fill the xarray.
+
+ if self.simulation_result is None:
+ raise(RuntimeError('You must run a simulation first'))
+
+ simulationResult=self.simulation_result
+ spiceParam=self.spiceParam
+ # Fill params from simulation results
+ param={}
+ t=time.time()
+ simulationResultNext=[]
+ for res in simulationResult:
+ # Iterate over the results of a simulation run.
+ keepResult=True
+ for fileName, simu_result in zip(res['resultFiles'], res['results']) :
+ # Get the type and name of the simulation
+ simu_type, simu_name, simu_temp, mcrunno=splitNgspiceFileName(fileName)
+ # Check if the type has already been added if not initialize
+ # TODO: Consider doing some sanity check on simulation types.
+ # We might want to reduce the amount of data stored for measurements.
+
+ if not simu_type in param:
+ param[simu_type]={}
+
+ # Check if the name has already been added if not initialize
+ if not simu_name in param[simu_type]:
+ param[simu_type][simu_name]={'temp':[], 'mcrunno':[]}
+ # Add frequencies
+ columns=list(simu_result.columns)
+ if 'frequency' in columns:
+ param[simu_type][simu_name]['frequency']=simu_result['frequency'].values
+ columns.remove('frequency')
+ # Same with time
+ if 'time' in columns:
+ param[simu_type][simu_name]['time']=simu_result['time'].values
+ columns.remove('time')
+
+
+ # Add spice vectors
+ param[simu_type][simu_name]['spice_vector']=columns
+
+ # See if the temperature is already listed
+ if not simu_temp in param[simu_type][simu_name]['temp']:
+ param[simu_type][simu_name]['temp'].append(simu_temp)
+
+ # See if the mcrunno is already listed-
+ # Note: Some run numbers might be skipped sometimes.
+ # This way we get all unique numbers.
+ if not mcrunno in param[simu_type][simu_name]['mcrunno']:
+ param[simu_type][simu_name]['mcrunno'].append(mcrunno)
+
+ # Ensure frequencies match.
+ if 'frequency' in simu_result.columns:
+ freq_saved= param[simu_type][simu_name]['frequency']
+ freq_this_result = simu_result['frequency'].values
+ if not np.array_equal(freq_saved,freq_this_result):
+ raise(RuntimeError("Frequencies don't match. Something went wrong."))
+
+ # Remove additional points from time axis for transient simulation
+ if 'time' in simu_result.columns:
+ time_saved= param[simu_type][simu_name]['time']
+ time_this_result = simu_result['time'].values
+ if not np.array_equal(time_saved,time_this_result):
+ time_intersect=np.msort(np.union1d(time_saved, time_this_result))
+ param[simu_type][simu_name]['time']=time_intersect
+
+ print('Assemble parameter space:', time.time()-t )
+
+ #
+ if keepResult:
+ simulationResultNext.append(res)
+ # Fill params from spiceParam and
+ # compute the dimensions from params
+ for simu_type in param.keys():
+ for simu_name in param[simu_type].keys():
+ # Fill params from spiceParam
+ for key, value in spiceParam.items():
+ # Convert floats or integers to lists, as required by xarray.
+ # TODO: We might need to convert more data types.
+ if type(value) == float or type(value) == int:
+ value=[value]
+ param[simu_type][simu_name][key]=value
+
+ # Iterate over simulation runs and fill the xarray.
+ # One simulation run is one invocation of ngspice.
+ result={}
+ t=time.time()
+ t_fill=0
+ for res in simulationResultNext:
+ # Iterate over the results of a simulation run.
+ simu_param=res['param']
+ for fileName, simu_result in zip(res['resultFiles'],res['results']) :
+ # Get the type and name of the simulation
+ simu_type, simu_name, simu_temp, mcrunno=splitNgspiceFileName(fileName)
+
+ if not simu_type in result:
+ result[simu_type]={}
+
+ # Check if we already encountered another run of the simulation.
+ # If this is the first time we come across the simulation, initialize the
+ # xarray.
+ if not simu_name in result[simu_type]:
+ coords=param[simu_type][simu_name]
+ dims=param[simu_type][simu_name].keys()
+ result[simu_type][simu_name]=xr.DataArray(None, coords=coords, dims=dims)
+
+ # Assemble our current coordinates :)
+ # Frequency and spice vectors are treated separately.
+ t1=time.time()
+ coord=simu_param
+ coord['temp']=simu_temp
+ coord['mcrunno']=mcrunno
+ columns=list(simu_result.columns)
+ # Attach the frequency to the coordinates
+ #if simu_type=='op':
+ # print('Breakpoint')
+ if 'frequency' in columns:
+ columns.remove('frequency')
+ frequency=simu_result['frequency'].values
+ coord['frequency']=frequency
+ else:
+ if 'frequency' in coord:
+ coord.pop('frequency')
+
+ # Handle time in transient simulations
+ if 'time' in columns:
+ columns.remove('time')
+ time_param=coords['time']
+ time_simu=simu_result['time']
+ else:
+ time_param=None
+
+ # Attach the data to the array.
+ for column in columns:
+ coord['spice_vector']=column
+ values=simu_result[column].values
+ if len(values) == 1:
+ values=values[0]
+ try:
+ if time_param is None:
+ result[simu_type][simu_name].loc[coord]=values
+ elif np.array_equal(time_param, time_simu):
+ result[simu_type][simu_name].loc[coord]=values
+ else:
+ i=0
+ val=np.empty_like(time_param)
+ for j, tp in enumerate(time_param):
+ ts= time_simu[i]
+ if tp == ts:
+ val[j]=values[i]
+ i+=1
+ else:
+ val[j]=np.nan
+
+ result[simu_type][simu_name].loc[coord]=val
+ except:
+ print(simu_type, simu_name, coord)
+ raise
+ t_fill+=time.time()-t1
+
+ print('Fill parameter space:', time.time()-t )
+ print('xrtime:', t_fill)
+ return result
+
+ def savePickle(self, fileName):
+ '''
+ Stores the simulation result and the spiceParameters in a pickle file.
+ '''
+ d_out={'simulationResult': self.simulation_result, 'spiceParam':self.spiceParam}
+ with open(fileName, 'wb') as file:
+ pickle.dump(d_out, file)
+
+ def loadPickle(self, fileName):
+ '''
+ Loads the simulation result and the spiceParameters from a pickle file.
+ '''
+ with open(fileName, 'rb') as file:
+ d_out=pickle.load(file)
+
+ self.simulation_result=d_out['simulationResult']
+ self.spiceParam=d_out['spiceParam']
+
+ def _spiceParamToParSpace(self, spiceParam, parSpace, spiceParamKeyList, indexParam):
+ '''
+ Iteratively create the parameter space for ngspice
+ '''
+ parSpaceNew={k: [] for k in parSpace.keys()}
+ new_key=spiceParamKeyList[indexParam]
+ parSpaceNew[new_key]=[]
+ for new_value in spiceParam[new_key]:
+ # Handle the first iteration
+ if parSpace =={}:
+ parSpaceNew[new_key].append(new_value)
+ continue
+ # All further iteration
+ # Copy existing parameters
+ for key, value in parSpace.items():
+ length=len(value)
+ for v in value:
+ parSpaceNew[key].append(v)
+ # Add new parameter
+ for v in range(length):
+ parSpaceNew[new_key].append(new_value)
+
+ if indexParam >= len(spiceParamKeyList)-1:
+ return parSpaceNew
+ else:
+ parSpaceNew=self._spiceParamToParSpace(spiceParam, parSpaceNew, spiceParamKeyList, indexParam+1)
+ return parSpaceNew
+
+ def spiceParamToParSpace(self, spiceParam):
+ '''
+ Create the parameter space for ngspice
+
+ '''
+ parSpaceDict={}
+ spiceParamKeyList=list(spiceParam.keys())
+ parSpaceDict=self._spiceParamToParSpace(spiceParam, parSpaceDict, spiceParamKeyList, 0)
+ parSpace=pd.DataFrame(parSpaceDict)
+ return parSpace
+
+ def run(self, spiceParam, delete=False, rename={}):
+ '''
+ Executes ngspice.
+
+ Operations performed:
+ 1. In the rundir provided to __init__ a directory will be created for each row in parSpace.
+ 2. In the created directories, a spice file run.spice will be created for each row in parSpace.
+ 3. ngspice will be called executed. The terminal output will be stored in run.log
+ 4. Files created by ngspice called result{}.csv (where {} is arbitrary) will be parsed as csv files and content returned.
+
+
+ :param spiceParam: Dictionary. Keys are the parameters to be varies. Values are lists containing the values.
+ :param delete:
+ :param replace: Optional. Dict. If given, the column names given by key in the output will be replaced by the corresponding value. This should help in making the output human readable.
+ :param threadSafe: Will not overWrite existing results
+
+ :returns: List. Each entry corresponds to ngspice run.
+ Each entry is a dict containing the variable parameters used for the call as well as the contents of the read result files.
+ '''
+ # Convert everything to lists
+ # TODO: Add more data types
+ self.spiceParam={}
+ for k, v in spiceParam.items():
+ if type(v) == float:
+ v=[v]
+ self.spiceParam[k]=v
+
+ # Create the parameter space for ngspice.
+ self.parSpace=self.spiceParamToParSpace(self.spiceParam)
+ parSpace=self.parSpace
+
+ # Generate parameter map.
+
+ # Create run directories
+ noInstances=parSpace.shape[0]
+ print('Number of spice Instances to be started: ', noInstances)
+ rundirs=[]
+
+ # This needs to be thread safe so we can have multiple calls of run in parallel.
+ for s in range(noInstances):
+ # Assemble the directory name for the run.
+ hash=hashlib.sha1()
+ for col in parSpace.columns:
+ # If we have a string encode it as utf 8 before calculating a hash.
+ # For other data types attempt to use the tobyte functions.
+ # Note: This might fail for some data types.
+ val=parSpace[col].values[s]
+ if type(val) == str:
+ hash.update(val.encode('utf-8'))
+ else:
+ hash.update(val.tobytes())
+
+ dirName=hash.hexdigest()
+ rundir=os.path.join(self.rundir, f'rundir_{dirName}')
+ if os.path.exists(rundir):
+ # If a run directory exists, delete it or give up.
+ if delete:
+ shutil.rmtree(rundir)
+ else:
+ raise(RuntimeError('Rundir exists.'))
+ # Create the directory.
+ os.mkdir(rundir)
+ rundirs.append(rundir)
+
+ # We instantiate NgSpiceFile at each call to be thread safe.
+
+ ngspiceFile=NgSpiceFile(self.ngspiceFile)
+ ngspiceFile.replace(self.constantPairs, keepOld=False)
+
+ # Write spice files
+ spiceFiles=[]
+ paramForRes=[]
+ for (_, item), rundir in zip(parSpace.iterrows(), rundirs):
+ # Set the new parameter
+ param=item.to_dict()
+ ngspiceFile.replace(param, keepOld=True)
+ paramForRes.append(param)
+
+ # Write the spice file to the output directory.
+ # Spice will be run in rundir, so we need to use absolute pathes.
+ spiceFile=os.path.abspath(os.path.join(rundir, 'run.spice'))
+ spiceFiles.append(spiceFile)
+ ngspiceFile.write(spiceFile)
+
+ # A bit of cryptic code :)
+ # Pool only allows one parameter to be passed to the function. So we compress the parameters to a dict.
+ param=[ {'runDir':runDir, 'spiceFile':spiceFile, 'param': pForRes} for (runDir, spiceFile, pForRes) in zip(rundirs, spiceFiles, paramForRes)]
+
+ # Ngspice also seems to do some multithreading. So let's split the task into batches of equal number of processes.
+ # E.g. if we have 9 spice instances we need to run don't start 8 at once and then 1 but run up to 5 in parallel, resulting in approx. 5+4 instances.
+ cpus= min(cpu_count(), self.max_cpus)
+ if cpus != cpu_count():
+ print(f'Limiting the maximum number of processes to {cpus}. However, {cpu_count()} CPUs would be available. \n \
+ Change the max_cpus if you would like to use more CPUs.')
+ batches= math.ceil(noInstances/cpus)
+ processes=int(math.ceil(noInstances/batches))
+
+ print(f'Will need {batches} batch(es). Max processes per batch: {processes}.')
+
+ # Execute ngspice
+ if self.parallel:
+ p=Pool(processes=processes)
+ with p as pool:
+ result=pool.map(_runNgspice, param)
+ del p
+ else:
+ result=[]
+ for p in param:
+ result.append(_runNgspice(p))
+
+ # Rename columns to something more easily understandable than spice internal variable names
+ for res in result:
+ for df in res['results']:
+ df.rename(columns=rename, inplace=True)
+
+ self.simulation_result=result
+ return result
+
+def _runNgspice(param):
+ '''
+ Runs ngspice, to be called as part of a multiprocessing pool
+ '''
+
+ # Execute ngspice in a shell, so the user can see what's happening. Also output the stdout to a file
+ runDir=param['runDir']
+ spiceFile=param['spiceFile']
+ logfile= os.path.join(runDir, 'run.log')
+ with open(logfile, 'wb') as nglog:
+ with subprocess.Popen(['ngspice', '-b', f'{spiceFile}'], shell=False, cwd=runDir, stdout=PIPE) as proc:
+ nglog.write(proc.stdout.read())
+
+ #Search for result files
+ regex=r'result.*\.csv$'
+ result={'param': param['param'], 'resultFiles': [], 'results': [] }
+ for root, _, files in os.walk(runDir):
+ for file in files:
+ m = re.match(regex, file)
+ if not m is None:
+ df=pd.read_csv(os.path.join(root, file), delim_whitespace=True)
+ #print(df)
+ result['resultFiles'].append(file)
+ result['results'].append(df)
+ return result
+
+
+
\ No newline at end of file
diff --git a/scripts/tia_fb_en.ini b/scripts/tia_fb_en.ini
new file mode 100644
index 0000000..dcf81c1
--- /dev/null
+++ b/scripts/tia_fb_en.ini
@@ -0,0 +1,17 @@
+[Setup]
+maxCpus=10
+voltages=[1.7, 1.8, 1.9]
+corners=["tt", "sf", "ff", "fs", "ss", "ll", "hh", "hl", "lh"]
+
+[Files]
+baseDirectory='../analog/test/'
+spiceFile='test_tia_rgc_full_mc.spice'
+outputDirectory='fb_on'
+
+[Parameter]
+ID=[-20e-6, -10e-6, 0, 10e-6, 20e-6]
+FB_DIS=[0]
+FB_PP_EN=[0]
+
+[Plot]
+ylim_top=None
\ No newline at end of file
diff --git a/scripts/tia_fb_pp_en.ini b/scripts/tia_fb_pp_en.ini
new file mode 100644
index 0000000..7d2da59
--- /dev/null
+++ b/scripts/tia_fb_pp_en.ini
@@ -0,0 +1,14 @@
+[Setup]
+maxCpus=10
+voltages=[1.7, 1.8, 1.9]
+corners=["tt", "sf", "ff", "fs", "ss", "ll", "hh", "hl", "lh"]
+
+[Files]
+baseDirectory='../analog/test/'
+spiceFile='test_tia_rgc_full_mc.spice'
+outputDirectory='fb_pp'
+
+[Parameter]
+ID=[-200e-6, -10e-6, 0, 10e-6, 200e-6]
+FB_DIS=[0]
+FB_PP_EN=[1.8]
diff --git a/scripts/tia_integration.ini b/scripts/tia_integration.ini
new file mode 100644
index 0000000..bd52bbf
--- /dev/null
+++ b/scripts/tia_integration.ini
@@ -0,0 +1,24 @@
+[Setup]
+maxCpus=10
+voltages=[1.7, 1.8, 1.9]
+corners=["tt", "sf", "ff", "fs", "ss", "ll", "hh", "hl", "lh"]
+
+[Files]
+baseDirectory='../analog/test/'
+spiceFile='test_tia_rgc_integration_mc.spice'
+outputDirectory='integration'
+
+[Parameter]
+; ID=[-20e-6, -10e-6, 0, 10e-6, 20e-6]
+; FB_DIS=[0]
+; FB_PP_EN=[0]
+
+[Plot]
+noise_outov=None
+noise_outzoom=None
+noise_tiaov=None
+noise_tiazoom=None
+ac_ac={'db(v(v_outp)-v(v_outn))': {'ylim_bottom': -20, 'title':'Output gain', 'y_label':'Gain / dBOhm'}, 'vdb(vtia_outp_1)': {'ylim_bottom': -20, 'title':'Tia gain', 'y_label':'Gain / dBOhm'}, 'v(vinb)': {'title':'Input impedance', 'y_label':'Impedance / Ohm'}, 'v(vinb).1':None}
+meas_out=None
+meas_tia=None
+op_op=None
\ No newline at end of file
diff --git a/scripts/tia_nofb.ini b/scripts/tia_nofb.ini
new file mode 100644
index 0000000..711157e
--- /dev/null
+++ b/scripts/tia_nofb.ini
@@ -0,0 +1,14 @@
+[Setup]
+maxCpus=10
+voltages=[1.7, 1.8, 1.9]
+corners=["tt", "sf", "ff", "fs", "ss", "ll", "hh", "hl", "lh"]
+
+[Files]
+baseDirectory='../analog/test/'
+spiceFile='test_tia_rgc_full_mc.spice'
+outputDirectory='fb_off'
+
+[Parameter]
+ID=[-2e-6, -1e-6, 0, 1e-6, 2e-6]
+FB_DIS=[1.8]
+FB_PP_EN=[0]
diff --git a/scripts/tool_box.py b/scripts/tool_box.py
new file mode 100644
index 0000000..5d372f1
--- /dev/null
+++ b/scripts/tool_box.py
@@ -0,0 +1,132 @@
+import sys
+import os
+from multiprocessing import Pool, cpu_count, Process
+import json
+
+def _except_hook(cls, exception, traceback):
+ '''
+ Function used by :py:func:`pqt5_exception_workaround`. Don't call this directly.
+ '''
+ sys.__excepthook__(cls, exception, traceback)
+
+
+
+def pqt5_exception_workaround():
+ '''
+ Workaround to prevent pyqt5 from silently eating exceptions.
+ Call this at the beginning of your progame and you will experience relieve.
+ '''
+ sys.excepthook = _except_hook
+
+
+class Persival():
+ '''
+ Keeps values persistent between program runs.
+
+ '''
+
+ def __init__(self, persival_file='persival.json'):
+ '''
+ Will try to load parameters from the persival_file.
+ If the file does not exists this will be silently ignored and the file will be created when :py:meth:`save` will be called.
+
+ '''
+
+ self.persival_file=persival_file
+ if os.path.exists(persival_file):
+ with open(self.persival_file, 'r') as file:
+ self.data=json.load(file)
+ else:
+ self.data={}
+
+ def setDefault(self, name, value):
+ '''
+ Sets the given value only if no information was present in the persival file or if the persival file was not found.
+ '''
+ if not name in self.data.keys():
+ self.data[name]=value
+
+ def get(self, name):
+ '''
+ Returns the saved value for the given parameter.
+ '''
+ return self.data[name]
+
+
+ def set(self, name, value):
+ '''
+ Store the given value in memory. You have to call save to dump the value to the disk.
+ '''
+ self.data[name] = value
+ # print('Break')
+
+
+ def save(self):
+ '''
+ Saves the content to a file.
+ '''
+ with open(self.persival_file, 'w') as file:
+ json.dump(self.data, file)
+
+
+
+
+class ProcessingHelper():
+ '''
+ Helper working around the limitations of Pool.
+
+ '''
+ def __init__(self, parallel=False, maxCpu=None):
+ self.funcs=[]
+ self.parallel=parallel
+ self.maxCpu=maxCpu
+ self.retval=[]
+
+ def add(self, function, args, kwargs={}):
+ '''
+ Add a function to the processing pool.
+
+ :param function: function to be executed
+ :param args: list. Provice the positional arguments
+ :param kwargs: dict. Optional, provide the kwargs
+ '''
+ if self.parallel:
+ self.funcs.append({'func': function, 'args': args, 'kwargs': kwargs})
+ else:
+ retval=function(*args, **kwargs)
+ self.retval.append(retval)
+
+ def run(self):
+ '''
+ Start processing.
+
+ :returns: list of results. The orgdering matches the sequence of add calls.
+ '''
+ processes=cpu_count()
+ if not self.maxCpu is None:
+ processes=min(self.maxCpu, processes)
+ # Windows does not handle full cpu loads well, so leave one CPU unutilized for Windows.
+ if os.name == 'nt':
+ processes -=1
+
+ if self.parallel:
+ with Pool(processes=processes) as pool:
+ result=pool.map(self._myrun, range(len(self.funcs)))
+ else:
+ result=self.retval
+ return result
+
+
+ def _myrun(self, paramNo):
+ '''
+ wrapper around the function for pool
+
+ :param paramNo:
+ '''
+ param=self.funcs[paramNo]
+ func=param['func']
+ args= param['args']
+ kwargs= param['kwargs']
+ retval=func(*args, **kwargs)
+ return retval
+
\ No newline at end of file
diff --git a/scripts/transistors.py b/scripts/transistors.py
new file mode 100644
index 0000000..282b201
--- /dev/null
+++ b/scripts/transistors.py
@@ -0,0 +1,173 @@
+import os
+import pandas as pd
+from spice import NgSpice
+
+def plotGmIdCurves(transistor_type, vsb):
+ from matplotlib import pyplot as plt
+
+ #TRANSISTOR_TYPES=['nfet_01v8_lvt', 'pfet_01v8_lvt', 'pfet_01v8_hvt', 'nfet_01v8', 'pfet_01v8']
+ #TRANSISTOR_TYPE='pfet_01v8'
+ #TRANSISTOR_TYPE='nfet_01v8'
+ TRANSISTOR_TYPE=transistor_type
+
+
+ #file_in='/home/simon/code/asic/analog/transistors/test_nmos4.spice'
+ #workingDir='/home/simon/code/asic/analog/transistors/outdata_nmos_lvt_US_0_6V'
+ #workingDir='/home/simon/code/asic/analog/transistors/outdata_nmos_lvt'
+ workingDir='/home/simon/code/asic/analog/transistors/'
+ workingDir=os.path.join(workingDir, 'vsb_{}'.format(vsb))
+ if TRANSISTOR_TYPE=='pfet_01v8':
+ file_in='/home/simon/code/asic/analog/transistors/test_pmos.spice'
+ workingDirT='outdata_pmos'
+ l=[0.15, 0.2, 0.3, 0.5, 1, 2, 4, 6]
+ if TRANSISTOR_TYPE=='pfet_01v8_lvt':
+ file_in='/home/simon/code/asic/analog/transistors/test_pmos_lvt.spice'
+ workingDirT='outdata_pmos_lvt'
+ l=[0.35, 0.5, 1, 2, 4, 6]
+ if TRANSISTOR_TYPE=='pfet_01v8_hvt':
+ file_in='/home/simon/code/asic/analog/transistors/test_pmos_hvt.spice'
+ workingDirT='outdata_pmos_hvt'
+ l=[0.15, 0.2, 0.3, 0.5, 1, 2, 4, 6]
+ if TRANSISTOR_TYPE=='nfet_01v8':
+ file_in='/home/simon/code/asic/analog/transistors/test_nmos.spice'
+ workingDirT='outdata_nmos'
+ l=[0.15, 0.2, 0.3, 0.5, 1, 2, 4, 6]
+ if TRANSISTOR_TYPE=='nfet_01v8_lvt':
+ file_in='/home/simon/code/asic/analog/transistors/test_nmos_lvt.spice'
+ workingDirT='outdata_nmos_lvt'
+ l=[0.15, 0.2, 0.3, 0.5, 1, 2, 4, 6]
+ vsb=[vsb]*len(l)
+ width={'L': l, 'VSB': vsb}
+
+ if not os.path.exists(workingDir):
+ os.mkdir(workingDir)
+ workingDir=os.path.join(workingDir, workingDirT)
+
+ if not os.path.exists(workingDir):
+ os.mkdir(workingDir)
+
+ #workingDir='/home/simon/code/asic/analog/transistors/outdata_nmos'
+
+ # Generate parameter map.
+ df=pd.DataFrame(width)
+
+ # Replace the spice specific names by something human readable
+ # nfet
+ #spiceReplace={'var_vgs': 'V_GS', 'var_vdb': 'V_DS', 'i(V_ID)' : 'I_D', '@m.xm1.msky130_fd_pr__nfet_01v8[gm]': 'gm', '@m.xm1.msky130_fd_pr__nfet_01v8[gds]': 'gds'}
+ # nfet_lvt
+ #spiceReplace={'var_vgb': 'V_GB', 'var_vds': 'V_DS', 'i(V_ID)' : 'I_D', '@m.xm1.msky130_fd_pr__nfet_01v8_lvt[gm]': 'gm', '@m.xm1.msky130_fd_pr__nfet_01v8_lvt[gds]': 'gds'}
+
+ spiceReplace={'var_vgb': 'V_GB', 'var_vds': 'V_DS', 'i(V_ID)' : 'I_D', f'@m.xm1.msky130_fd_pr__{TRANSISTOR_TYPE}[gm]': 'gm', f'@m.xm1.msky130_fd_pr__{TRANSISTOR_TYPE}[gds]': 'gds'}
+
+ # Run spice
+ ngspice=NgSpice(file_in, workingDir, parallel=True)
+ try:
+ result=ngspice.run(df, delete=True, rename=spiceReplace)
+ except:
+ print(TRANSISTOR_TYPE)
+ raise
+
+
+ for r in result:
+ df=r['results'][0]
+ u_GS_unique=df['V_GB'].unique()
+
+ # First plot: For a given transistor length plot gm as a function of the Gate voltage.
+ fig, ax = plt.subplots()
+ for u_GS_filter in u_GS_unique:
+ myfilter = df['V_GB'].values == u_GS_filter
+ I_D=df['I_D'].values[myfilter]
+ gm=df['gm'].values[myfilter]
+ ax.plot(I_D, gm, label=f'U_GB={u_GS_filter}')
+ ax.set_xlabel('I_D') # Add an x-label to the axes.
+ ax.set_ylabel('gm') # Add a y-label to the axes.
+ ax.set_title("Lenght={:.2f}".format(r['param']['L'])) # Add a title to the axes.
+ ax.legend() # Add a legend.
+ #plt.show()
+ file=os.path.join(workingDir, "gm vs ID, Lenght={:.2f}.png".format(r['param']['L']))
+ plt.savefig(file)
+ plt.close(fig)
+
+ # Second plot: For a given transistor length plot gds as a function of the Gate voltage.
+ fig, ax = plt.subplots()
+ for u_GS_filter in u_GS_unique:
+ myfilter = df['V_GB'].values == u_GS_filter
+ I_D=df['I_D'].values[myfilter]
+ gds=df['gds'].values[myfilter]
+ rds=1/gds
+ plt.semilogy(I_D, rds, label=f'U_GB={u_GS_filter}')
+ ax.set_xlabel('I_D') # Add an x-label to the axes.
+ ax.set_ylabel('1/gds') # Add a y-label to the axes.
+ ax.set_title("Lenght={:.2f}".format(r['param']['L'])) # Add a title to the axes.
+ ax.legend() # Add a legend.
+ file=os.path.join(workingDir, "gds vs ID, Lenght={:.2f}.png".format(r['param']['L']))
+ plt.savefig(file)
+ plt.close(fig)
+
+ # Third plot: For a given transistor length plot the intrinsic amplification as a function of the Gate voltage.
+ fig, ax = plt.subplots()
+ for u_GS_filter in u_GS_unique:
+ myfilter = df['V_GB'].values == u_GS_filter
+ I_D=df['I_D'].values[myfilter]
+ gds=df['gds'].values[myfilter]
+ gm=df['gm'].values[myfilter]
+ gain=gm/gds
+ plt.plot(I_D, gain, label=f'U_GB={u_GS_filter}')
+ ax.set_xlabel('I_D') # Add an x-label to the axes.
+ ax.set_ylabel('gm/gds') # Add a y-label to the axes.
+ ax.set_title("Lenght={:.2f}".format(r['param']['L'])) # Add a title to the axes.
+ ax.legend() # Add a legend.
+ file=os.path.join(workingDir, "gain vs ID, Lenght={:.2f}.png".format(r['param']['L']))
+ plt.savefig(file)
+ plt.close(fig)
+
+ # Fourth plot: For a given transistor length plot gm as a function of the Drain Source voltage. The gate voltage.
+ fig, ax = plt.subplots()
+ for u_GS_filter in u_GS_unique:
+ myfilter = df['V_GB'].values == u_GS_filter
+ V_DS=df['V_DS'].values[myfilter]
+ gds=df['gds'].values[myfilter]
+ gm=df['gm'].values[myfilter]
+ gain=gm/gds
+ ax.plot(V_DS, gm, label=f'U_GB={u_GS_filter}')
+ ax.set_xlabel('V_DS') # Add an x-label to the axes.
+ ax.set_ylabel('gm') # Add a y-label to the axes.
+ ax.set_title("Lenght={:.2f}".format(r['param']['L'])) # Add a title to the axes.
+ ax.legend() # Add a legend.
+ file=os.path.join(workingDir, "gm vs UDS, Lenght={:.2f}.png".format(r['param']['L']))
+ plt.savefig(file)
+ plt.close(fig)
+
+ # Fifth plot: For a given transistor length plot gds as a function of the Drain Source voltage. The gate voltage.
+ fig, ax = plt.subplots()
+ for u_GS_filter in u_GS_unique:
+ myfilter = df['V_GB'].values == u_GS_filter
+ V_DS=df['V_DS'].values[myfilter]
+ gds=df['gds'].values[myfilter]
+ gm=df['gm'].values[myfilter]
+ gain=gm/gds
+ plt.semilogy(V_DS, gds, label=f'U_GB={u_GS_filter}')
+ ax.set_xlabel('V_DS') # Add an x-label to the axes.
+ ax.set_ylabel('gds') # Add a y-label to the axes.
+ ax.set_title("Lenght={:.2f}".format(r['param']['L'])) # Add a title to the axes.
+ ax.legend() # Add a legend.
+ file=os.path.join(workingDir, "gds vs UDS, Lenght={:.2f}.png".format(r['param']['L']))
+ plt.savefig(file)
+ plt.close(fig)
+
+ # Sixth plot: For a given transistor length plot the intrinsic gain as a function of the Drain Source voltage. The gate voltage.
+ fig, ax = plt.subplots()
+ for u_GS_filter in u_GS_unique:
+ myfilter = df['V_GB'].values == u_GS_filter
+ V_DS=df['V_DS'].values[myfilter]
+ gds=df['gds'].values[myfilter]
+ gm=df['gm'].values[myfilter]
+ gain=gm/gds
+ plt.semilogy(V_DS, gain, label=f'U_GB={u_GS_filter}')
+ ax.set_xlabel('V_DS') # Add an x-label to the axes.
+ ax.set_ylabel('gm/gds') # Add a y-label to the axes.
+ ax.set_title("Lenght={:.2f}".format(r['param']['L'])) # Add a title to the axes.
+ ax.legend() # Add a legend.
+ file=os.path.join(workingDir, "gain, Lenght={:.2f}.png".format(r['param']['L']))
+ plt.savefig(file)
+ plt.close(fig)
diff --git a/xschem/user_analog_project_wrapper.spice b/xschem/user_analog_project_wrapper.spice
index 57be0cb..5f2f944 100644
--- a/xschem/user_analog_project_wrapper.spice
+++ b/xschem/user_analog_project_wrapper.spice
@@ -1,92 +1,20 @@
-
-* expanding symbol: user_analog_project_wrapper.sym # of pins=32
-** sym_path: /home/simon/code/caravel_tia/xschem/user_analog_project_wrapper.sym
** sch_path: /home/simon/code/caravel_tia/xschem/user_analog_project_wrapper.sch
-.subckt user_analog_project_wrapper vdda1 vdda2 vssa1 vssa2 vccd1 vccd2 vssd1 vssd2 wb_clk_i
-+ wb_rst_i wbs_stb_i wbs_cyc_i wbs_we_i wbs_sel_i[3] wbs_sel_i[2] wbs_sel_i[1] wbs_sel_i[0] wbs_dat_i[31]
-+ wbs_dat_i[30] wbs_dat_i[29] wbs_dat_i[28] wbs_dat_i[27] wbs_dat_i[26] wbs_dat_i[25] wbs_dat_i[24] wbs_dat_i[23]
-+ wbs_dat_i[22] wbs_dat_i[21] wbs_dat_i[20] wbs_dat_i[19] wbs_dat_i[18] wbs_dat_i[17] wbs_dat_i[16] wbs_dat_i[15]
-+ wbs_dat_i[14] wbs_dat_i[13] wbs_dat_i[12] wbs_dat_i[11] wbs_dat_i[10] wbs_dat_i[9] wbs_dat_i[8] wbs_dat_i[7]
-+ wbs_dat_i[6] wbs_dat_i[5] wbs_dat_i[4] wbs_dat_i[3] wbs_dat_i[2] wbs_dat_i[1] wbs_dat_i[0] wbs_adr_i[31]
-+ wbs_adr_i[30] wbs_adr_i[29] wbs_adr_i[28] wbs_adr_i[27] wbs_adr_i[26] wbs_adr_i[25] wbs_adr_i[24] wbs_adr_i[23]
-+ wbs_adr_i[22] wbs_adr_i[21] wbs_adr_i[20] wbs_adr_i[19] wbs_adr_i[18] wbs_adr_i[17] wbs_adr_i[16] wbs_adr_i[15]
-+ wbs_adr_i[14] wbs_adr_i[13] wbs_adr_i[12] wbs_adr_i[11] wbs_adr_i[10] wbs_adr_i[9] wbs_adr_i[8] wbs_adr_i[7]
-+ wbs_adr_i[6] wbs_adr_i[5] wbs_adr_i[4] wbs_adr_i[3] wbs_adr_i[2] wbs_adr_i[1] wbs_adr_i[0] wbs_ack_o
-+ wbs_dat_o[31] wbs_dat_o[30] wbs_dat_o[29] wbs_dat_o[28] wbs_dat_o[27] wbs_dat_o[26] wbs_dat_o[25] wbs_dat_o[24]
-+ wbs_dat_o[23] wbs_dat_o[22] wbs_dat_o[21] wbs_dat_o[20] wbs_dat_o[19] wbs_dat_o[18] wbs_dat_o[17] wbs_dat_o[16]
-+ wbs_dat_o[15] wbs_dat_o[14] wbs_dat_o[13] wbs_dat_o[12] wbs_dat_o[11] wbs_dat_o[10] wbs_dat_o[9] wbs_dat_o[8]
-+ wbs_dat_o[7] wbs_dat_o[6] wbs_dat_o[5] wbs_dat_o[4] wbs_dat_o[3] wbs_dat_o[2] wbs_dat_o[1] wbs_dat_o[0]
-+ la_data_in[127] la_data_in[126] la_data_in[125] la_data_in[124] la_data_in[123] la_data_in[122] la_data_in[121]
-+ la_data_in[120] la_data_in[119] la_data_in[118] la_data_in[117] la_data_in[116] la_data_in[115] la_data_in[114]
-+ la_data_in[113] la_data_in[112] la_data_in[111] la_data_in[110] la_data_in[109] la_data_in[108] la_data_in[107]
-+ la_data_in[106] la_data_in[105] la_data_in[104] la_data_in[103] la_data_in[102] la_data_in[101] la_data_in[100]
-+ la_data_in[99] la_data_in[98] la_data_in[97] la_data_in[96] la_data_in[95] la_data_in[94] la_data_in[93]
-+ la_data_in[92] la_data_in[91] la_data_in[90] la_data_in[89] la_data_in[88] la_data_in[87] la_data_in[86]
-+ la_data_in[85] la_data_in[84] la_data_in[83] la_data_in[82] la_data_in[81] la_data_in[80] la_data_in[79]
-+ la_data_in[78] la_data_in[77] la_data_in[76] la_data_in[75] la_data_in[74] la_data_in[73] la_data_in[72]
-+ la_data_in[71] la_data_in[70] la_data_in[69] la_data_in[68] la_data_in[67] la_data_in[66] la_data_in[65]
-+ la_data_in[64] la_data_in[63] la_data_in[62] la_data_in[61] la_data_in[60] la_data_in[59] la_data_in[58]
-+ la_data_in[57] la_data_in[56] la_data_in[55] la_data_in[54] la_data_in[53] la_data_in[52] la_data_in[51]
-+ la_data_in[50] la_data_in[49] la_data_in[48] la_data_in[47] la_data_in[46] la_data_in[45] la_data_in[44]
-+ la_data_in[43] la_data_in[42] la_data_in[41] la_data_in[40] la_data_in[39] la_data_in[38] la_data_in[37]
-+ la_data_in[36] la_data_in[35] la_data_in[34] la_data_in[33] la_data_in[32] la_data_in[31] la_data_in[30]
-+ la_data_in[29] la_data_in[28] la_data_in[27] la_data_in[26] la_data_in[25] la_data_in[24] la_data_in[23]
-+ la_data_in[22] la_data_in[21] la_data_in[20] la_data_in[19] la_data_in[18] la_data_in[17] la_data_in[16]
-+ la_data_in[15] la_data_in[14] la_data_in[13] la_data_in[12] la_data_in[11] la_data_in[10] la_data_in[9]
-+ la_data_in[8] la_data_in[7] la_data_in[6] la_data_in[5] la_data_in[4] la_data_in[3] la_data_in[2] la_data_in[1]
-+ la_data_in[0] la_data_out[127] la_data_out[126] la_data_out[125] la_data_out[124] la_data_out[123]
-+ la_data_out[122] la_data_out[121] la_data_out[120] la_data_out[119] la_data_out[118] la_data_out[117]
-+ la_data_out[116] la_data_out[115] la_data_out[114] la_data_out[113] la_data_out[112] la_data_out[111]
-+ la_data_out[110] la_data_out[109] la_data_out[108] la_data_out[107] la_data_out[106] la_data_out[105]
-+ la_data_out[104] la_data_out[103] la_data_out[102] la_data_out[101] la_data_out[100] la_data_out[99] la_data_out[98]
-+ la_data_out[97] la_data_out[96] la_data_out[95] la_data_out[94] la_data_out[93] la_data_out[92] la_data_out[91]
-+ la_data_out[90] la_data_out[89] la_data_out[88] la_data_out[87] la_data_out[86] la_data_out[85] la_data_out[84]
-+ la_data_out[83] la_data_out[82] la_data_out[81] la_data_out[80] la_data_out[79] la_data_out[78] la_data_out[77]
-+ la_data_out[76] la_data_out[75] la_data_out[74] la_data_out[73] la_data_out[72] la_data_out[71] la_data_out[70]
-+ la_data_out[69] la_data_out[68] la_data_out[67] la_data_out[66] la_data_out[65] la_data_out[64] la_data_out[63]
-+ la_data_out[62] la_data_out[61] la_data_out[60] la_data_out[59] la_data_out[58] la_data_out[57] la_data_out[56]
-+ la_data_out[55] la_data_out[54] la_data_out[53] la_data_out[52] la_data_out[51] la_data_out[50] la_data_out[49]
-+ la_data_out[48] la_data_out[47] la_data_out[46] la_data_out[45] la_data_out[44] la_data_out[43] la_data_out[42]
-+ la_data_out[41] la_data_out[40] la_data_out[39] la_data_out[38] la_data_out[37] la_data_out[36] la_data_out[35]
-+ la_data_out[34] la_data_out[33] la_data_out[32] la_data_out[31] la_data_out[30] la_data_out[29] la_data_out[28]
-+ la_data_out[27] la_data_out[26] la_data_out[25] la_data_out[24] la_data_out[23] la_data_out[22] la_data_out[21]
-+ la_data_out[20] la_data_out[19] la_data_out[18] la_data_out[17] la_data_out[16] la_data_out[15] la_data_out[14]
-+ la_data_out[13] la_data_out[12] la_data_out[11] la_data_out[10] la_data_out[9] la_data_out[8] la_data_out[7]
-+ la_data_out[6] la_data_out[5] la_data_out[4] la_data_out[3] la_data_out[2] la_data_out[1] la_data_out[0]
-+ la_oenb[127] la_oenb[126] la_oenb[125] la_oenb[124] la_oenb[123] la_oenb[122] la_oenb[121] la_oenb[120]
-+ la_oenb[119] la_oenb[118] la_oenb[117] la_oenb[116] la_oenb[115] la_oenb[114] la_oenb[113] la_oenb[112]
-+ la_oenb[111] la_oenb[110] la_oenb[109] la_oenb[108] la_oenb[107] la_oenb[106] la_oenb[105] la_oenb[104]
-+ la_oenb[103] la_oenb[102] la_oenb[101] la_oenb[100] la_oenb[99] la_oenb[98] la_oenb[97] la_oenb[96] la_oenb[95]
-+ la_oenb[94] la_oenb[93] la_oenb[92] la_oenb[91] la_oenb[90] la_oenb[89] la_oenb[88] la_oenb[87] la_oenb[86]
-+ la_oenb[85] la_oenb[84] la_oenb[83] la_oenb[82] la_oenb[81] la_oenb[80] la_oenb[79] la_oenb[78] la_oenb[77]
-+ la_oenb[76] la_oenb[75] la_oenb[74] la_oenb[73] la_oenb[72] la_oenb[71] la_oenb[70] la_oenb[69] la_oenb[68]
-+ la_oenb[67] la_oenb[66] la_oenb[65] la_oenb[64] la_oenb[63] la_oenb[62] la_oenb[61] la_oenb[60] la_oenb[59]
-+ la_oenb[58] la_oenb[57] la_oenb[56] la_oenb[55] la_oenb[54] la_oenb[53] la_oenb[52] la_oenb[51] la_oenb[50]
-+ la_oenb[49] la_oenb[48] la_oenb[47] la_oenb[46] la_oenb[45] la_oenb[44] la_oenb[43] la_oenb[42] la_oenb[41]
-+ la_oenb[40] la_oenb[39] la_oenb[38] la_oenb[37] la_oenb[36] la_oenb[35] la_oenb[34] la_oenb[33] la_oenb[32]
-+ la_oenb[31] la_oenb[30] la_oenb[29] la_oenb[28] la_oenb[27] la_oenb[26] la_oenb[25] la_oenb[24] la_oenb[23]
-+ la_oenb[22] la_oenb[21] la_oenb[20] la_oenb[19] la_oenb[18] la_oenb[17] la_oenb[16] la_oenb[15] la_oenb[14]
-+ la_oenb[13] la_oenb[12] la_oenb[11] la_oenb[10] la_oenb[9] la_oenb[8] la_oenb[7] la_oenb[6] la_oenb[5]
-+ la_oenb[4] la_oenb[3] la_oenb[2] la_oenb[1] la_oenb[0] io_in[26] io_in[25] io_in[24] io_in[23] io_in[22]
-+ io_in[21] io_in[20] io_in[19] io_in[18] io_in[17] io_in[16] io_in[15] io_in[14] io_in[13] io_in[12] io_in[11]
-+ io_in[10] io_in[9] io_in[8] io_in[7] io_in[6] io_in[5] io_in[4] io_in[3] io_in[2] io_in[1] io_in[0]
-+ io_in_3v3[26] io_in_3v3[25] io_in_3v3[24] io_in_3v3[23] io_in_3v3[22] io_in_3v3[21] io_in_3v3[20] io_in_3v3[19]
-+ io_in_3v3[18] io_in_3v3[17] io_in_3v3[16] io_in_3v3[15] io_in_3v3[14] io_in_3v3[13] io_in_3v3[12] io_in_3v3[11]
-+ io_in_3v3[10] io_in_3v3[9] io_in_3v3[8] io_in_3v3[7] io_in_3v3[6] io_in_3v3[5] io_in_3v3[4] io_in_3v3[3]
-+ io_in_3v3[2] io_in_3v3[1] io_in_3v3[0] io_out[26] io_out[25] io_out[24] io_out[23] io_out[22] io_out[21]
-+ io_out[20] io_out[19] io_out[18] io_out[17] io_out[16] io_out[15] io_out[14] io_out[13] io_out[12] io_out[11]
-+ io_out[10] io_out[9] io_out[8] io_out[7] io_out[6] io_out[5] io_out[4] io_out[3] io_out[2] io_out[1] io_out[0]
-+ io_oeb[26] io_oeb[25] io_oeb[24] io_oeb[23] io_oeb[22] io_oeb[21] io_oeb[20] io_oeb[19] io_oeb[18] io_oeb[17]
-+ io_oeb[16] io_oeb[15] io_oeb[14] io_oeb[13] io_oeb[12] io_oeb[11] io_oeb[10] io_oeb[9] io_oeb[8] io_oeb[7]
-+ io_oeb[6] io_oeb[5] io_oeb[4] io_oeb[3] io_oeb[2] io_oeb[1] io_oeb[0] gpio_analog[17] gpio_analog[16]
-+ gpio_analog[15] gpio_analog[14] gpio_analog[13] gpio_analog[12] gpio_analog[11] gpio_analog[10] gpio_analog[9]
-+ gpio_analog[8] gpio_analog[7] gpio_analog[6] gpio_analog[5] gpio_analog[4] gpio_analog[3] gpio_analog[2]
-+ gpio_analog[1] gpio_analog[0] gpio_noesd[17] gpio_noesd[16] gpio_noesd[15] gpio_noesd[14] gpio_noesd[13]
-+ gpio_noesd[12] gpio_noesd[11] gpio_noesd[10] gpio_noesd[9] gpio_noesd[8] gpio_noesd[7] gpio_noesd[6] gpio_noesd[5]
-+ gpio_noesd[4] gpio_noesd[3] gpio_noesd[2] gpio_noesd[1] gpio_noesd[0] io_analog[10] io_analog[9] io_analog[8]
-+ io_analog[7] io_analog[6] io_analog[5] io_analog[4] io_analog[3] io_analog[2] io_analog[1] io_analog[0]
-+ io_clamp_high[2] io_clamp_high[1] io_clamp_high[0] io_clamp_low[2] io_clamp_low[1] io_clamp_low[0] user_clock2
-+ user_irq[2] user_irq[1] user_irq[0]
+**.subckt user_analog_project_wrapper vdda1 vdda2 vssa1 vssa2 vccd1 vccd2 vssd1 vssd2 wb_clk_i
+*+ wb_rst_i wbs_stb_i wbs_cyc_i wbs_we_i wbs_sel_i[3],wbs_sel_i[2],wbs_sel_i[1],wbs_sel_i[0]
+*+ wbs_dat_i[31],wbs_dat_i[30],wbs_dat_i[29],wbs_dat_i[28],wbs_dat_i[27],wbs_dat_i[26],wbs_dat_i[25],wbs_dat_i[24],wbs_dat_i[23],wbs_dat_i[22],wbs_dat_i[21],wbs_dat_i[20],wbs_dat_i[19],wbs_dat_i[18],wbs_dat_i[17],wbs_dat_i[16],wbs_dat_i[15],wbs_dat_i[14],wbs_dat_i[13],wbs_dat_i[12],wbs_dat_i[11],wbs_dat_i[10],wbs_dat_i[9],wbs_dat_i[8],wbs_dat_i[7],wbs_dat_i[6],wbs_dat_i[5],wbs_dat_i[4],wbs_dat_i[3],wbs_dat_i[2],wbs_dat_i[1],wbs_dat_i[0]
+*+ wbs_adr_i[31],wbs_adr_i[30],wbs_adr_i[29],wbs_adr_i[28],wbs_adr_i[27],wbs_adr_i[26],wbs_adr_i[25],wbs_adr_i[24],wbs_adr_i[23],wbs_adr_i[22],wbs_adr_i[21],wbs_adr_i[20],wbs_adr_i[19],wbs_adr_i[18],wbs_adr_i[17],wbs_adr_i[16],wbs_adr_i[15],wbs_adr_i[14],wbs_adr_i[13],wbs_adr_i[12],wbs_adr_i[11],wbs_adr_i[10],wbs_adr_i[9],wbs_adr_i[8],wbs_adr_i[7],wbs_adr_i[6],wbs_adr_i[5],wbs_adr_i[4],wbs_adr_i[3],wbs_adr_i[2],wbs_adr_i[1],wbs_adr_i[0] wbs_ack_o
+*+ wbs_dat_o[31],wbs_dat_o[30],wbs_dat_o[29],wbs_dat_o[28],wbs_dat_o[27],wbs_dat_o[26],wbs_dat_o[25],wbs_dat_o[24],wbs_dat_o[23],wbs_dat_o[22],wbs_dat_o[21],wbs_dat_o[20],wbs_dat_o[19],wbs_dat_o[18],wbs_dat_o[17],wbs_dat_o[16],wbs_dat_o[15],wbs_dat_o[14],wbs_dat_o[13],wbs_dat_o[12],wbs_dat_o[11],wbs_dat_o[10],wbs_dat_o[9],wbs_dat_o[8],wbs_dat_o[7],wbs_dat_o[6],wbs_dat_o[5],wbs_dat_o[4],wbs_dat_o[3],wbs_dat_o[2],wbs_dat_o[1],wbs_dat_o[0]
+*+ la_data_in[127],la_data_in[126],la_data_in[125],la_data_in[124],la_data_in[123],la_data_in[122],la_data_in[121],la_data_in[120],la_data_in[119],la_data_in[118],la_data_in[117],la_data_in[116],la_data_in[115],la_data_in[114],la_data_in[113],la_data_in[112],la_data_in[111],la_data_in[110],la_data_in[109],la_data_in[108],la_data_in[107],la_data_in[106],la_data_in[105],la_data_in[104],la_data_in[103],la_data_in[102],la_data_in[101],la_data_in[100],la_data_in[99],la_data_in[98],la_data_in[97],la_data_in[96],la_data_in[95],la_data_in[94],la_data_in[93],la_data_in[92],la_data_in[91],la_data_in[90],la_data_in[89],la_data_in[88],la_data_in[87],la_data_in[86],la_data_in[85],la_data_in[84],la_data_in[83],la_data_in[82],la_data_in[81],la_data_in[80],la_data_in[79],la_data_in[78],la_data_in[77],la_data_in[76],la_data_in[75],la_data_in[74],la_data_in[73],la_data_in[72],la_data_in[71],la_data_in[70],la_data_in[69],la_data_in[68],la_data_in[67],la_data_in[66],la_data_in[65],la_data_in[64],la_data_in[63],la_data_in[62],la_data_in[61],la_data_in[60],la_data_in[59],la_data_in[58],la_data_in[57],la_data_in[56],la_data_in[55],la_data_in[54],la_data_in[53],la_data_in[52],la_data_in[51],la_data_in[50],la_data_in[49],la_data_in[48],la_data_in[47],la_data_in[46],la_data_in[45],la_data_in[44],la_data_in[43],la_data_in[42],la_data_in[41],la_data_in[40],la_data_in[39],la_data_in[38],la_data_in[37],la_data_in[36],la_data_in[35],la_data_in[34],la_data_in[33],la_data_in[32],la_data_in[31],la_data_in[30],la_data_in[29],la_data_in[28],la_data_in[27],la_data_in[26],la_data_in[25],la_data_in[24],la_data_in[23],la_data_in[22],la_data_in[21],la_data_in[20],la_data_in[19],la_data_in[18],la_data_in[17],la_data_in[16],la_data_in[15],la_data_in[14],la_data_in[13],la_data_in[12],la_data_in[11],la_data_in[10],la_data_in[9],la_data_in[8],la_data_in[7],la_data_in[6],la_data_in[5],la_data_in[4],la_data_in[3],la_data_in[2],la_data_in[1],la_data_in[0]
+*+ la_data_out[127],la_data_out[126],la_data_out[125],la_data_out[124],la_data_out[123],la_data_out[122],la_data_out[121],la_data_out[120],la_data_out[119],la_data_out[118],la_data_out[117],la_data_out[116],la_data_out[115],la_data_out[114],la_data_out[113],la_data_out[112],la_data_out[111],la_data_out[110],la_data_out[109],la_data_out[108],la_data_out[107],la_data_out[106],la_data_out[105],la_data_out[104],la_data_out[103],la_data_out[102],la_data_out[101],la_data_out[100],la_data_out[99],la_data_out[98],la_data_out[97],la_data_out[96],la_data_out[95],la_data_out[94],la_data_out[93],la_data_out[92],la_data_out[91],la_data_out[90],la_data_out[89],la_data_out[88],la_data_out[87],la_data_out[86],la_data_out[85],la_data_out[84],la_data_out[83],la_data_out[82],la_data_out[81],la_data_out[80],la_data_out[79],la_data_out[78],la_data_out[77],la_data_out[76],la_data_out[75],la_data_out[74],la_data_out[73],la_data_out[72],la_data_out[71],la_data_out[70],la_data_out[69],la_data_out[68],la_data_out[67],la_data_out[66],la_data_out[65],la_data_out[64],la_data_out[63],la_data_out[62],la_data_out[61],la_data_out[60],la_data_out[59],la_data_out[58],la_data_out[57],la_data_out[56],la_data_out[55],la_data_out[54],la_data_out[53],la_data_out[52],la_data_out[51],la_data_out[50],la_data_out[49],la_data_out[48],la_data_out[47],la_data_out[46],la_data_out[45],la_data_out[44],la_data_out[43],la_data_out[42],la_data_out[41],la_data_out[40],la_data_out[39],la_data_out[38],la_data_out[37],la_data_out[36],la_data_out[35],la_data_out[34],la_data_out[33],la_data_out[32],la_data_out[31],la_data_out[30],la_data_out[29],la_data_out[28],la_data_out[27],la_data_out[26],la_data_out[25],la_data_out[24],la_data_out[23],la_data_out[22],la_data_out[21],la_data_out[20],la_data_out[19],la_data_out[18],la_data_out[17],la_data_out[16],la_data_out[15],la_data_out[14],la_data_out[13],la_data_out[12],la_data_out[11],la_data_out[10],la_data_out[9],la_data_out[8],la_data_out[7],la_data_out[6],la_data_out[5],la_data_out[4],la_data_out[3],la_data_out[2],la_data_out[1],la_data_out[0]
+*+ io_in[26],io_in[25],io_in[24],io_in[23],io_in[22],io_in[21],io_in[20],io_in[19],io_in[18],io_in[17],io_in[16],io_in[15],io_in[14],io_in[13],io_in[12],io_in[11],io_in[10],io_in[9],io_in[8],io_in[7],io_in[6],io_in[5],io_in[4],io_in[3],io_in[2],io_in[1],io_in[0]
+*+ io_in_3v3[26],io_in_3v3[25],io_in_3v3[24],io_in_3v3[23],io_in_3v3[22],io_in_3v3[21],io_in_3v3[20],io_in_3v3[19],io_in_3v3[18],io_in_3v3[17],io_in_3v3[16],io_in_3v3[15],io_in_3v3[14],io_in_3v3[13],io_in_3v3[12],io_in_3v3[11],io_in_3v3[10],io_in_3v3[9],io_in_3v3[8],io_in_3v3[7],io_in_3v3[6],io_in_3v3[5],io_in_3v3[4],io_in_3v3[3],io_in_3v3[2],io_in_3v3[1],io_in_3v3[0] user_clock2
+*+ io_out[26],io_out[25],io_out[24],io_out[23],io_out[22],io_out[21],io_out[20],io_out[19],io_out[18],io_out[17],io_out[16],io_out[15],io_out[14],io_out[13],io_out[12],io_out[11],io_out[10],io_out[9],io_out[8],io_out[7],io_out[6],io_out[5],io_out[4],io_out[3],io_out[2],io_out[1],io_out[0]
+*+ io_oeb[26],io_oeb[25],io_oeb[24],io_oeb[23],io_oeb[22],io_oeb[21],io_oeb[20],io_oeb[19],io_oeb[18],io_oeb[17],io_oeb[16],io_oeb[15],io_oeb[14],io_oeb[13],io_oeb[12],io_oeb[11],io_oeb[10],io_oeb[9],io_oeb[8],io_oeb[7],io_oeb[6],io_oeb[5],io_oeb[4],io_oeb[3],io_oeb[2],io_oeb[1],io_oeb[0]
+*+ gpio_analog[17],gpio_analog[16],gpio_analog[15],gpio_analog[14],gpio_analog[13],gpio_analog[12],gpio_analog[11],gpio_analog[10],gpio_analog[9],gpio_analog[8],gpio_analog[7],gpio_analog[6],gpio_analog[5],gpio_analog[4],gpio_analog[3],gpio_analog[2],gpio_analog[1],gpio_analog[0]
+*+ gpio_noesd[17],gpio_noesd[16],gpio_noesd[15],gpio_noesd[14],gpio_noesd[13],gpio_noesd[12],gpio_noesd[11],gpio_noesd[10],gpio_noesd[9],gpio_noesd[8],gpio_noesd[7],gpio_noesd[6],gpio_noesd[5],gpio_noesd[4],gpio_noesd[3],gpio_noesd[2],gpio_noesd[1],gpio_noesd[0]
+*+ io_analog[10],io_analog[9],io_analog[8],io_analog[7],io_analog[6],io_analog[5],io_analog[4],io_analog[3],io_analog[2],io_analog[1],io_analog[0] io_clamp_high[2],io_clamp_high[1],io_clamp_high[0] io_clamp_low[2],io_clamp_low[1],io_clamp_low[0]
+*+ user_irq[2],user_irq[1],user_irq[0]
+*+ la_oenb[127],la_oenb[126],la_oenb[125],la_oenb[124],la_oenb[123],la_oenb[122],la_oenb[121],la_oenb[120],la_oenb[119],la_oenb[118],la_oenb[117],la_oenb[116],la_oenb[115],la_oenb[114],la_oenb[113],la_oenb[112],la_oenb[111],la_oenb[110],la_oenb[109],la_oenb[108],la_oenb[107],la_oenb[106],la_oenb[105],la_oenb[104],la_oenb[103],la_oenb[102],la_oenb[101],la_oenb[100],la_oenb[99],la_oenb[98],la_oenb[97],la_oenb[96],la_oenb[95],la_oenb[94],la_oenb[93],la_oenb[92],la_oenb[91],la_oenb[90],la_oenb[89],la_oenb[88],la_oenb[87],la_oenb[86],la_oenb[85],la_oenb[84],la_oenb[83],la_oenb[82],la_oenb[81],la_oenb[80],la_oenb[79],la_oenb[78],la_oenb[77],la_oenb[76],la_oenb[75],la_oenb[74],la_oenb[73],la_oenb[72],la_oenb[71],la_oenb[70],la_oenb[69],la_oenb[68],la_oenb[67],la_oenb[66],la_oenb[65],la_oenb[64],la_oenb[63],la_oenb[62],la_oenb[61],la_oenb[60],la_oenb[59],la_oenb[58],la_oenb[57],la_oenb[56],la_oenb[55],la_oenb[54],la_oenb[53],la_oenb[52],la_oenb[51],la_oenb[50],la_oenb[49],la_oenb[48],la_oenb[47],la_oenb[46],la_oenb[45],la_oenb[44],la_oenb[43],la_oenb[42],la_oenb[41],la_oenb[40],la_oenb[39],la_oenb[38],la_oenb[37],la_oenb[36],la_oenb[35],la_oenb[34],la_oenb[33],la_oenb[32],la_oenb[31],la_oenb[30],la_oenb[29],la_oenb[28],la_oenb[27],la_oenb[26],la_oenb[25],la_oenb[24],la_oenb[23],la_oenb[22],la_oenb[21],la_oenb[20],la_oenb[19],la_oenb[18],la_oenb[17],la_oenb[16],la_oenb[15],la_oenb[14],la_oenb[13],la_oenb[12],la_oenb[11],la_oenb[10],la_oenb[9],la_oenb[8],la_oenb[7],la_oenb[6],la_oenb[5],la_oenb[4],la_oenb[3],la_oenb[2],la_oenb[1],la_oenb[0]
*.iopin vdda1
*.iopin vdda2
*.iopin vssa1
@@ -145,5 +73,433 @@
R3 vccd1 io_clamp_high[1] sky130_fd_pr__res_generic_m3 W=11 L=0.25 m=1
R4 vssd1 io_clamp_low[2] sky130_fd_pr__res_generic_m3 W=11 L=0.25 m=1
R6 vccd1 io_clamp_high[2] sky130_fd_pr__res_generic_m3 W=11 L=0.25 m=1
+**.ends
+
+* expanding symbol: esd/esd_diodes.sym # of pins=3
+** sym_path: /home/simon/code/caravel_tia/xschem/esd/esd_diodes.sym
+** sch_path: /home/simon/code/caravel_tia/xschem/esd/esd_diodes.sch
+.subckt esd_diodes VP io VN
+*.iopin VP
+*.iopin io
+*.iopin VN
+D1 io VP sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D2 io VP sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D3 io VP sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D4 io VP sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D5 io VP sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D6 io VP sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D7 io VP sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D8 io VP sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D9 io VP sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D10 io VP sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D11 VN io sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D12 VN io sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D13 VN io sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D14 VN io sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D15 VN io sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D16 VN io sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D17 VN io sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D18 VN io sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D19 VN io sky130_fd_pr__diode_pw2nd_05v5 area=4e12
+D20 VN io sky130_fd_pr__diode_pw2nd_05v5 area=4e12
.ends
+
+* expanding symbol: mpw5_submission.sym # of pins=7
+** sym_path: /home/simon/code/caravel_tia/xschem/mpw5_submission.sym
+** sch_path: /home/simon/code/caravel_tia/xschem/mpw5_submission.sch
+.subckt mpw5_submission VP I_out Dis_TIA In_TIA Out_N Out_P VN
+*.iopin VP
+*.ipin In_TIA
+*.opin Out_N
+*.iopin VN
+*.opin Out_P
+*.ipin Dis_TIA
+*.opin I_out
+x4 VP Out_N Out_P net2 net3 net4 VN outdriver
+x5 VP net9 I_out net8 net7 net10 net11 net12 net13 net14 VN current_mirrorx8
+x6 VP net8 VN low_pvt_source
+x7 VP net1 net2 net5 net7 VN current_mirror_channel
+x8 VP net6 net4 net3 Dis_TIA In_TIA net1 VN tia_rgc_core
+.ends
+
+
+* expanding symbol: outdriver/outdriver.sym # of pins=7
+** sym_path: /home/simon/code/caravel_tia/xschem/outdriver/outdriver.sym
+** sch_path: /home/simon/code/caravel_tia/xschem/outdriver/outdriver.sch
+.subckt outdriver VP OutputN OutputP I_Bias InputSignal InputRef VN
+*.iopin VN
+*.ipin InputRef
+*.iopin VP
+*.ipin I_Bias
+*.ipin InputSignal
+*.opin OutputN
+*.opin OutputP
+XM1 V_da1_P InputSignal VM6D VM6D sky130_fd_pr__nfet_01v8_lvt L=0.15 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=11*2 m=11*2
+XM3 VM3D I_Bias net4 VN sky130_fd_pr__nfet_01v8 L=0.5 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=2*(64) m=2*(64)
+XM6 VM6D I_Bias VM3D VN sky130_fd_pr__nfet_01v8 L=0.15 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=64 m=64
+XM7 I_Bias I_Bias net1 VN sky130_fd_pr__nfet_01v8 L=0.15 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=2 m=2
+XM8 net1 I_Bias VN VN sky130_fd_pr__nfet_01v8 L=0.5 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=2*2 m=2*2
+XM2 V_da2_P V_da1_P VM1_D VM1_D sky130_fd_pr__nfet_01v8_lvt L=0.15 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=44*2 m=44*2
+XM4 V_da2_N V_da1_N VM1_D VM1_D sky130_fd_pr__nfet_01v8_lvt L=0.15 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=44*2 m=44*2
+XM9 net2 I_Bias net5 VN sky130_fd_pr__nfet_01v8 L=0.5 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=2*(256+64) m=2*(256+64)
+XM10 VM1_D I_Bias net2 VN sky130_fd_pr__nfet_01v8 L=0.15 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=256+64 m=256+64
+XM11 OutputP V_da2_P VM14D VM14D sky130_fd_pr__nfet_01v8_lvt L=0.15 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=44*4*2 m=44*4*2
+XM12 OutputN V_da2_N VM14D VM14D sky130_fd_pr__nfet_01v8_lvt L=0.15 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=44*4*2 m=44*4*2
+XM13 net3 I_Bias net6 VN sky130_fd_pr__nfet_01v8 L=0.5 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=2*(1024+256) m=2*(1024+256)
+XM14 VM14D I_Bias net3 VN sky130_fd_pr__nfet_01v8 L=0.15 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=1024+256 m=1024+256
+XR7 V_da2_N VP VP sky130_fd_pr__res_high_po_5p73 L=4 mult=4*2 m=4*2
+XR5 V_da2_P VP VP sky130_fd_pr__res_high_po_5p73 L=4 mult=4*2 m=4*2
+XR3 V_da1_N VP VP sky130_fd_pr__res_high_po_2p85 L=6 mult=4 m=4
+XR4 V_da1_P VP VP sky130_fd_pr__res_high_po_2p85 L=6 mult=4 m=4
+XC2 InputRef VN sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+XM5 V_da1_N InputRef VM6D VM6D sky130_fd_pr__nfet_01v8_lvt L=0.15 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=11*2 m=11*2
+XR8 OutputP VP VP sky130_fd_pr__res_high_po_5p73 L=4 mult=16*2 m=16*2
+XC1 InputRef VN sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+XC6 VP VN sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+XC8 VP VN sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+V1 net4 VN 0
+V2 net5 VN 0
+V3 net6 VN 0
+XR1 OutputN VP VP sky130_fd_pr__res_high_po_5p73 L=4 mult=16*2 m=16*2
+XC7 VP VN sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+XC9 VP VN sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+XC10 VP VN sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+XC11 VP VN sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+XC12 VP VN sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+XC13 VP VN sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+XC14 VP VN sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+XC5 I_Bias VN sky130_fd_pr__cap_mim_m3_1 W=20 L=20 MF=1 m=1
+XC3 I_Bias VN sky130_fd_pr__cap_mim_m3_1 W=20 L=20 MF=1 m=1
+.ends
+
+
+* expanding symbol: bias/current_mirrorx8.sym # of pins=11
+** sym_path: /home/simon/code/caravel_tia/xschem/bias/current_mirrorx8.sym
+** sch_path: /home/simon/code/caravel_tia/xschem/bias/current_mirrorx8.sch
+.subckt current_mirrorx8 VP I_out_3 I_out_1 I_In I_out_0 I_out_4 I_out_5 I_out_6 I_out_7 I_out_2 VN
+*.opin I_out_0
+*.ipin I_In
+*.iopin VP
+*.opin I_out_1
+*.opin I_out_2
+*.opin I_out_3
+*.opin I_out_4
+*.opin I_out_5
+*.opin I_out_6
+*.opin I_out_7
+*.iopin VN
+XM1 net1 I_In VP VP sky130_fd_pr__pfet_01v8 L=1 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4*4 m=4*4
+XM2 I_In I_In net1 VP sky130_fd_pr__pfet_01v8 L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4 m=4
+XC1 I_In VP sky130_fd_pr__cap_mim_m3_1 W=20 L=20 MF=1 m=1
+XC2 VP I_In sky130_fd_pr__cap_mim_m3_2 W=20 L=20 MF=1 m=1
+XC4 VP VN VN sky130_fd_pr__cap_var_lvt W=2 L=5 VM=5
+XC3 I_In VN VN sky130_fd_pr__cap_var_lvt W=2 L=5 VM=5
+XM3 net2 I_In VP VP sky130_fd_pr__pfet_01v8 L=1 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4*4 m=4*4
+XM4 I_out_0 I_In net2 VP sky130_fd_pr__pfet_01v8 L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4 m=4
+XM5 net3 I_In VP VP sky130_fd_pr__pfet_01v8 L=1 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4*4 m=4*4
+XM6 I_out_1 I_In net3 VP sky130_fd_pr__pfet_01v8 L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4 m=4
+XM7 net4 I_In VP VP sky130_fd_pr__pfet_01v8 L=1 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4*4 m=4*4
+XM8 I_out_2 I_In net4 VP sky130_fd_pr__pfet_01v8 L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4 m=4
+XM9 net5 I_In VP VP sky130_fd_pr__pfet_01v8 L=1 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4*4 m=4*4
+XM10 I_out_3 I_In net5 VP sky130_fd_pr__pfet_01v8 L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4 m=4
+XM11 net6 I_In VP VP sky130_fd_pr__pfet_01v8 L=1 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4*4 m=4*4
+XM12 I_out_4 I_In net6 VP sky130_fd_pr__pfet_01v8 L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4 m=4
+XM13 net7 I_In VP VP sky130_fd_pr__pfet_01v8 L=1 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4*4 m=4*4
+XM14 I_out_5 I_In net7 VP sky130_fd_pr__pfet_01v8 L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4 m=4
+XM15 net8 I_In VP VP sky130_fd_pr__pfet_01v8 L=1 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4*4 m=4*4
+XM16 I_out_6 I_In net8 VP sky130_fd_pr__pfet_01v8 L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4 m=4
+XM17 net9 I_In VP VP sky130_fd_pr__pfet_01v8 L=1 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4*4 m=4*4
+XM18 I_out_7 I_In net9 VP sky130_fd_pr__pfet_01v8 L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4 m=4
+.ends
+
+
+* expanding symbol: bias/low_pvt_source.sym # of pins=3
+** sym_path: /home/simon/code/caravel_tia/xschem/bias/low_pvt_source.sym
+** sch_path: /home/simon/code/caravel_tia/xschem/bias/low_pvt_source.sch
+.subckt low_pvt_source VP I_ref VN
+*.iopin VP
+*.opin I_ref
+*.iopin VN
+XM1 VM1D VM8D VP VP sky130_fd_pr__pfet_01v8 L=1 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=10 m=10
+XM2 VM2D VM2D VN VN sky130_fd_pr__nfet_01v8 L=6 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=30 m=30
+XM5 VM9D VM8D VM1D VP sky130_fd_pr__pfet_01v8 L=0.2 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=2 m=2
+XM11 VM11D VM2D VM12D VN sky130_fd_pr__nfet_01v8 L=6 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=65 m=65
+XM12 VM12D VM12G VN VN sky130_fd_pr__nfet_01v8 L=6 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=2 m=2
+XM14 VM14D VM12G VN VN sky130_fd_pr__nfet_01v8 L=6 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=2 m=2
+XM16 VM16D VM8D VP VP sky130_fd_pr__pfet_01v8 L=1 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=10 m=10
+XM17 VM8D VM8D VM16D VP sky130_fd_pr__pfet_01v8 L=0.2 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=2 m=2
+XM18 VM18D VM8D VP VP sky130_fd_pr__pfet_01v8 L=1 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=10*3*2 m=10*3*2
+XM19 VM14D VM8D VM18D VP sky130_fd_pr__pfet_01v8 L=0.2 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=2*3*2 m=2*3*2
+XM20 VM20D VM8D VP VP sky130_fd_pr__pfet_01v8 L=1 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=10*1*1 m=10*1*1
+XM21 VM22D VM8D VM20D VP sky130_fd_pr__pfet_01v8 L=0.2 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=2*1*1 m=2*1*1
+XM22 VM22D VM4S VM3D VN sky130_fd_pr__nfet_01v8 L=6 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=20 m=20
+XM3 VM3D VM3G VN VN sky130_fd_pr__nfet_01v8 L=6 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4 m=4
+XM13 VP VM14D VM12G VM12G sky130_fd_pr__nfet_01v8_lvt L=0.15 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=20 m=20
+XM4 I_ref VM22D VM4S VN sky130_fd_pr__nfet_01v8_lvt L=0.15 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=20 m=20
+XM48 VM50D VM11D VP VP sky130_fd_pr__pfet_01v8 L=2 W=0.5 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=1 m=1
+XM50 VM50D VM11D VN VN sky130_fd_pr__nfet_01v8_lvt L=0.2 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=10 m=10
+XC3 VP VM8D sky130_fd_pr__cap_mim_m3_1 W=20 L=20 MF=1 m=1
+XM8 VM8D VM9D VM11D VM11D sky130_fd_pr__nfet_01v8_lvt L=1 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=20 m=20
+XM9 VM9D VM9D VM2D VM2D sky130_fd_pr__nfet_01v8_lvt L=1 W=4 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=20 m=20
+XM10 VM8D VM50D VN VN sky130_fd_pr__nfet_01v8 L=2 W=0.5 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=1 m=1
+XC4 VN VP sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+XR7 net3 VM4S VN sky130_fd_pr__res_xhigh_po_1p41 L=10 mult=1 m=1
+XR3 net4 net3 VN sky130_fd_pr__res_xhigh_po_1p41 L=10 mult=1 m=1
+XR4 net2 net4 VN sky130_fd_pr__res_xhigh_po_1p41 L=10 mult=1 m=1
+XR5 net1 net2 VN sky130_fd_pr__res_xhigh_po_1p41 L=10 mult=1 m=1
+XR6 VN net1 VN sky130_fd_pr__res_xhigh_po_1p41 L=10 mult=1 m=1
+XR8 net5 VM12G VN sky130_fd_pr__res_xhigh_po_1p41 L=10 mult=1 m=1
+XR1 net5 VM3G VN sky130_fd_pr__res_xhigh_po_1p41 L=10 mult=1 m=1
+XR2 net7 net6 VN sky130_fd_pr__res_xhigh_po_1p41 L=10 mult=1 m=1
+XR9 VN net6 VN sky130_fd_pr__res_xhigh_po_1p41 L=10 mult=1 m=1
+XR10 net11 net7 VN sky130_fd_pr__res_xhigh_po_1p41 L=10 mult=1 m=1
+XR11 net12 net11 VN sky130_fd_pr__res_xhigh_po_1p41 L=10 mult=1 m=1
+XR12 net8 net12 VN sky130_fd_pr__res_xhigh_po_1p41 L=10 mult=1 m=1
+XR13 net10 VM3G VN sky130_fd_pr__res_xhigh_po_1p41 L=10 mult=1 m=1
+XR14 net9 net10 VN sky130_fd_pr__res_xhigh_po_1p41 L=10 mult=1 m=1
+XR15 net8 net9 VN sky130_fd_pr__res_xhigh_po_1p41 L=10 mult=1 m=1
+XC1 VN VP sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+.ends
+
+
+* expanding symbol: bias/current_mirror_channel.sym # of pins=6
+** sym_path: /home/simon/code/caravel_tia/xschem/bias/current_mirror_channel.sym
+** sch_path: /home/simon/code/caravel_tia/xschem/bias/current_mirror_channel.sch
+.subckt current_mirror_channel VP TIA_I_Bias1 A_Out_I_Bias TIA_I_Bias2 I_in_channel VN
+*.ipin I_in_channel
+*.iopin VP
+*.opin TIA_I_Bias2
+*.iopin VN
+*.opin TIA_I_Bias1
+*.opin A_Out_I_Bias
+XM1 I_in_channel I_in_channel net1 VN sky130_fd_pr__nfet_01v8 L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=2 m=2
+XM2 net5 net4 VP VP sky130_fd_pr__pfet_01v8 L=1 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4*4 m=4*4
+XM3 net1 I_in_channel VN VN sky130_fd_pr__nfet_01v8 L=1 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=2*4 m=2*4
+XM4 net4 net4 net5 VP sky130_fd_pr__pfet_01v8 L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4 m=4
+XC8 net4 VP sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+XM5 net4 I_in_channel net2 VN sky130_fd_pr__nfet_01v8 L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=2 m=2
+XM6 TIA_I_Bias2 I_in_channel net3 VN sky130_fd_pr__nfet_01v8 L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=2 m=2
+XM7 net2 I_in_channel VN VN sky130_fd_pr__nfet_01v8 L=1 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=2*4 m=2*4
+XM8 net3 I_in_channel VN VN sky130_fd_pr__nfet_01v8 L=1 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=2*4 m=2*4
+XM13 net6 net4 VP VP sky130_fd_pr__pfet_01v8 L=1 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4*4*10 m=4*4*10
+XM14 TIA_I_Bias1 net4 net6 VP sky130_fd_pr__pfet_01v8 L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=4*10 m=4*10
+XM9 net7 net4 VP VP sky130_fd_pr__pfet_01v8 L=1 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=4*4*12 m=4*4*12
+XM10 A_Out_I_Bias net4 net7 VP sky130_fd_pr__pfet_01v8 L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=4*12 m=4*12
+XC4 net4 VP sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+XC3 VN I_in_channel sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+XC6 VN I_in_channel sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+XC1 VN net4 sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+XC2 VN VP sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+.ends
+
+
+* expanding symbol: tia/tia_rgc_core.sym # of pins=8
+** sym_path: /home/simon/code/caravel_tia/xschem/tia/tia_rgc_core.sym
+** sch_path: /home/simon/code/caravel_tia/xschem/tia/tia_rgc_core.sch
+.subckt tia_rgc_core VP Out_2 Out_ref Out_1 Disable_TIA Input I_Bias1 VN
+*.iopin VN
+*.iopin VP
+*.opin Out_2
+*.ipin I_Bias1
+*.ipin Input
+*.ipin Disable_TIA
+*.opin Out_1
+*.opin Out_ref
+XM5 VM5D I_Bias1 VN VN sky130_fd_pr__nfet_01v8 L=1 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=6 m=6
+XM6 VM6D I_Bias1 VN VN sky130_fd_pr__nfet_01v8 L=1.0 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=6 m=6
+XM7 Out_1 Input VM28D VN sky130_fd_pr__nfet_01v8_lvt L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=100 m=100
+XM8 Out_1 Input VP VP sky130_fd_pr__pfet_01v8 L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=60 m=60
+XM9 Input I_Bias1 VM5D VN sky130_fd_pr__nfet_01v8 L=0.15 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=12 m=12
+XM10 I_Bias1 I_Bias1 VM6D VN sky130_fd_pr__nfet_01v8 L=0.15 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=12 m=12
+XM23 I_Bias1 Disable_TIA VN VN sky130_fd_pr__nfet_01v8 L=0.15 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=5 m=5
+XM26 Disable_TIA_B Disable_TIA VN VN sky130_fd_pr__nfet_01v8 L=1 W=0.5 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=1 m=1
+XM27 Disable_TIA_B Disable_TIA VP VP sky130_fd_pr__pfet_01v8 L=1 W=0.5 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=1 m=1
+XM28 VM28D Disable_TIA_B VN VN sky130_fd_pr__nfet_01v8 L=0.15 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=100 m=100
+XM36 VM36D I_Bias1 VN VN sky130_fd_pr__nfet_01v8 L=1 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=6 m=6
+XM37 Out_ref VM39D VM40D VN sky130_fd_pr__nfet_01v8_lvt L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=100 m=100
+XM38 Out_ref VM39D VP VP sky130_fd_pr__pfet_01v8 L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=60 m=60
+XM39 VM39D I_Bias1 VM36D VN sky130_fd_pr__nfet_01v8 L=0.15 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=12 m=12
+XM40 VM40D Disable_TIA_B VN VN sky130_fd_pr__nfet_01v8 L=0.15 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=100 m=100
+XC20 VP VN sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+XM16 Out_2 VN VP VP sky130_fd_pr__pfet_01v8 L=0.5 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=10 m=10
+XM15 Out_2 Out_1 Input Input sky130_fd_pr__nfet_01v8_lvt L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=30 m=30
+XM31 VM31D Out_ref VM39D VM39D sky130_fd_pr__nfet_01v8_lvt L=0.2 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29'
++ as='int((nf+2)/2) * W/nf * 0.29' pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)'
++ nrd='0.29 / W' nrs='0.29 / W' sa=0 sb=0 sd=0 mult=30 m=30
+XM35 VM31D VN VP VP sky130_fd_pr__pfet_01v8 L=0.5 W=2 nf=1 ad='int((nf+1)/2) * W/nf * 0.29' as='int((nf+2)/2) * W/nf * 0.29'
++ pd='2*int((nf+1)/2) * (W/nf + 0.29)' ps='2*int((nf+2)/2) * (W/nf + 0.29)' nrd='0.29 / W' nrs='0.29 / W'
++ sa=0 sb=0 sd=0 mult=10 m=10
+XC7 VP VN sky130_fd_pr__cap_mim_m3_2 W=30 L=30 MF=1 m=1
+XC2 VP VM28D sky130_fd_pr__cap_mim_m3_2 W=20 L=30 MF=1 m=1
+XC5 VN I_Bias1 sky130_fd_pr__cap_mim_m3_1 W=15 L=12 MF=1 m=1
+XC4 VN I_Bias1 sky130_fd_pr__cap_mim_m3_1 W=15 L=12 MF=1 m=1
+XC1 VP VM40D sky130_fd_pr__cap_mim_m3_2 W=20 L=30 MF=1 m=1
+XC3 Disable_TIA_B VN VN sky130_fd_pr__cap_var_lvt W=2 L=5 VM=5
+.ends
+
+.end