# Using Custom Scripts to Postprocess Simulation Results ```{warning} Custom scripts are still a WIP and the function signature of `postprocess` might still change! ``` This tutorial shows how to use custom Python scripts to postprocess simulation results of the `ngspice` tool. This feature can be used to extract certain parameters from simulation results. For example, it could be used to calculate the INL, DNL, offset error and gain error of a DAC. An example of this can be seen in [sky130_ef_ip__rdac3v_8bit](https://github.com/RTimothyEdwards/sky130_ef_ip__rdac3v_8bit). For a step-by-step guide on how to set up custom scripts, please see below. ## Specifying Custom Scripts in the Datasheet At present, only the `ngspice` tool allows postprocessing of the simulation results. Please have a look at its arguments at {doc}`../reference_manual/tools`. To add a custom script for postprocessing, you simply need to specify the `script` and `script_variables` arguments. ```yaml tool: ngspice: template: voltage_output.sch collate: iterations format: ascii suffix: .data variables: [null, vout] script: inl.py script_variables: [inl] ``` The `script` argument defines the name of the Python script that is called for each of the simulation results. The `script_variables` argument defines the results generated by the script. In this case, the script generates a single variable called `dnl`. The variables can then be used in the `spec` or `plot` section as if they came from the simulation run itself. The script needs to be placed in the `scripts` folder of CACE. CACE expects the custom script to provide a function called `postprocess`. The function signature of `postprocess` is as follows: ```Python def postprocess(results: dict[str, list], conditions: dict[str, Any]) -> dict[str, list]: ``` The `results` argument contains the results of a single simulation run. Since `variables: [null, vout]` is set in the datasheet, `results` contains a single key `vout` with a list of values (`results['vout'] = [..., ...]`). The `conditions` argument contains the conditions for this simulation run. For example, it contains a key `temperature` with the value for this simulation run, e.g. `27` (`conditions['temperature'] = 27`). If you collate simulation results based on a condition, then this condition will contain a list of values instead of a single value. For a Monte Carlo simulation that is collated with the `iterations` condition, `conditions['iterations']` contains a list of all the iterations for all collated simulations (`conditions['iterations'] = [1, 2, 3...]`). The return value of `postprocess` is another dictionary that must contain the keys specified in the `script_variables` argument in the datasheet. In this case `{'inl': [..., ...]}` is returned, where the list can also contain only a single value. To get a feel for the data being passed to your script, you can simply print `results` and `conditions` and return an empty dictionary. (Note that `script_variables` in the datasheet must be an empty list as this script does not return any results.) ```Python def postprocess(results: dict[str, list], conditions: dict[str, Any]) -> dict[str, list]: print(f'results: {results}') print(f'conditions: {conditions}') return {} ``` To notify CACE about any issues, simply raise an exception in your script: ```Python raise Exception('This is an exception') ``` For a real example, see the contents of `inl.py`: ```Python def postprocess(results: dict[str, list], conditions: dict[str, Any]) -> dict[str, list]: print(f'results: {results}') print(f'conditions: {conditions}') # INL calculation: # x is the digital value b7:0 converted to an integer # V(x) is the original value in RESULT: The voltage output of the DAC # under the given set of conditions. # ALSB = (Vhigh - Vlow) / 256 (ideal voltage step size per LSB) # # INL(x) = (RESULT - (x * ALSB + Vlow)) / ALSB (in units of LSB) # # NOTE that this DAC is based on 256 steps with the highest step being # 1 lsb below Vhigh. Vhigh = float(conditions['Vhigh']) Vlow = float(conditions['Vlow']) x = float(conditions['b']) alsb = (Vhigh - Vlow) / 256 inl = [] # Iterate over MC results for vout in results['vout']: inl.append((vout - (x * alsb + Vlow)) / alsb) return {'inl': inl} ``` For a simulation run the result might look like this in the summary: ``` Parameter Tool Result Min Limit Min Value Typ Target Typ Value Max Limit Max Value Status ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ INL ngspice inl -2 lsb 0.998 lsb 0 lsb 1.136 lsb 2 lsb 3.995 lsb Fail ❌ ```