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