diff --git a/environment.yml b/environment.yml index ee13527ba..a6e586111 100644 --- a/environment.yml +++ b/environment.yml @@ -17,9 +17,11 @@ name: skywater-pdk-scripts channels: - symbiflow - defaults +- conda-forge dependencies: - python=3.8 - pip +- ngspice # Packages installed from PyPI - pip: - -r file:requirements.txt diff --git a/requirements.txt b/requirements.txt index fe8a2c1b2..58a4641f4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,5 +4,8 @@ flake8 # previews. rst_include +# the PySpice module for rendering FET bins plots +PySpice + # The Python API for the SkyWater PDK. -e scripts/python-skywater-pdk diff --git a/scripts/python-skywater-pdk/skywater_pdk/base.py b/scripts/python-skywater-pdk/skywater_pdk/base.py index bcfc7e845..bbc4d0079 100644 --- a/scripts/python-skywater-pdk/skywater_pdk/base.py +++ b/scripts/python-skywater-pdk/skywater_pdk/base.py @@ -22,12 +22,16 @@ from dataclasses import dataclass from dataclasses_json import dataclass_json from enum import Enum -from typing import Optional, Union, Tuple +from typing import Optional, Union, Tuple, List +from collections import namedtuple +from pathlib import Path +import csv from .utils import comparable_to_none from .utils import dataclass_json_passthru_config as dj_pass_cfg +FETBin = namedtuple('FETBin', ['device', 'bin', 'w', 'l']) LibraryOrCell = Union['Library', 'Cell'] @@ -545,6 +549,24 @@ def fullname(self): self)) return "{}__{}".format(self.library.fullname, self.name) + @classmethod + def parse_bins(cls, binsfile) -> List[FETBin]: + """ + Parse bins.csv file. + + Parameters + ---------- + binsfile: path to the bins.csv file + """ + with open(binsfile, 'r') as f: + reader = csv.reader(f) + next(reader) + res = [] + for line in reader: + res.append(FETBin(line[0], int(line[1]), float(line[2]), float(line[3]))) + return res + + @classmethod def parse(cls, s): kw = {} @@ -555,7 +577,6 @@ def parse(cls, s): return cls(**kw) - if __name__ == "__main__": import doctest doctest.testmod() diff --git a/scripts/python-skywater-pdk/skywater_pdk/simulation/__init__.py b/scripts/python-skywater-pdk/skywater_pdk/simulation/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/python-skywater-pdk/skywater_pdk/simulation/analog/__init__.py b/scripts/python-skywater-pdk/skywater_pdk/simulation/analog/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/scripts/python-skywater-pdk/skywater_pdk/simulation/analog/fet.py b/scripts/python-skywater-pdk/skywater_pdk/simulation/analog/fet.py new file mode 100644 index 000000000..c197d2157 --- /dev/null +++ b/scripts/python-skywater-pdk/skywater_pdk/simulation/analog/fet.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright 2020 SkyWater PDK Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + + +"""Module for creating FET characteristics plots from bins.csv files. + +This module allows simulating FET cells and creating: + +* Id/W vs gm/Id +* fT vs gm/Id +* gm/gds vs gm/Id +* gm/Id vs Vgg + +plots based on different FET length and width values from bins.csv file. +""" + +import PySpice.Logging.Logging as Logging +from PySpice.Spice.Netlist import Circuit +from PySpice.Unit import u_V +import matplotlib.pyplot as plt +from pathlib import Path +import csv +from collections import defaultdict +import os +import sys + +sys.path.insert(0, os.path.abspath(__file__ + '/../../../../')) + +from skywater_pdk.base import Cell + +logger = Logging.setup_logging() + + +def create_test_circuit(fet_type, fet_L, fet_W, corner_path): + """ + Creates a simple test circuit that contains only given FET. + + Parameters + ---------- + fet_type: name of the FET model + fet_L: FET length + fet_W: FET width + corner_path: + path to the spice corner file with model definitions and parameters + """ + c = Circuit('gm_id') + c.include(corner_path) + + # create the circuit + c.V('gg', 1, c.gnd, 0@u_V) + c.V('dd', 2, c.gnd, 1.8@u_V) + c.X( + 'M1', fet_type, 2, 1, c.gnd, c.gnd, L=fet_L, W=fet_W, ad="'W*0.29'", + pd="'2*(W+0.29)'", as_="'W*0.29'", ps="'2*(W+0.29)'", nrd="'0.29/W'", + nrs="'0.29/W'", sa=0, sb=0, sd=0, nf=1, mult=1 + ) + return c + + +def run_sim(c, iparam, fet_W): + """ + Runs simulation for a given circuit with FET cell. + + Parameters + ---------- + c: circuit to simulate + iparam: + template string for creating internal parameter names (gm, id, gds, + cgg) for a given FET + fet_W: FET width + """ + sim = c.simulator() + sim.save_internal_parameters( + iparam % 'gm', iparam % 'id', iparam % 'gds', iparam % 'cgg' + ) + + try: + # run the dc simulation + an = sim.dc(Vgg=slice(0, 1.8, 0.01)) + + # calculate needed values..need as_ndarray() since most of these have + # None as the unit and that causes an error + gm_id = (an.internal_parameters[iparam % 'gm'].as_ndarray() / + an.internal_parameters[iparam % 'id'].as_ndarray()) + ft = (an.internal_parameters[iparam % 'gm'].as_ndarray() / + an.internal_parameters[iparam % 'cgg'].as_ndarray()) + id_W = (an.internal_parameters[iparam % 'id'].as_ndarray() / fet_W) + gm_gds = (an.internal_parameters[iparam % 'gm'].as_ndarray() / + an.internal_parameters[iparam % 'gds'].as_ndarray()) + except Exception: + sim.ngspice.destroy('all') + sim.ngspice.reset() + raise + + sim.ngspice.destroy('all') + sim.ngspice.reset() + + return gm_id, ft, id_W, gm_gds, an.nodes['v-sweep'] + + +def init_plots(fet_name, W): + """ + Initializes plots for FET bins simulation. + + Parameters + ---------- + fet_name: name of the current FET cell + W: FET width + """ + figs = [plt.figure(), plt.figure(), plt.figure(), plt.figure()] + plots = [f.subplots() for f in figs] + figs[0].suptitle(f'{fet_name} Id/W vs gm/Id (W = {W})') + plots[0].set_xlabel("gm/Id") + plots[0].set_ylabel("Id/W") + plots[0].grid(True) + figs[1].suptitle(f'{fet_name} fT vs gm/Id (W = {W})') + plots[1].set_xlabel("gm/Id") + plots[1].set_ylabel("f_T") + plots[1].grid(True) + figs[2].suptitle(f'{fet_name} gm/gds vs gm/Id (W = {W})') + plots[2].set_xlabel("gm/Id") + plots[2].set_ylabel("gm/gds") + plots[2].grid(True) + figs[3].suptitle(f'{fet_name} gm/Id vs Vgg (W = {W})') + plots[3].set_xlabel("Vgg") + plots[3].set_ylabel("gm/Id") + plots[3].grid(True) + return figs, plots + + +def gen_plots(gm_id, id_W, ft, gm_gds, vsweep, fet_W, fet_L, plots): + """ + Generates plot lines for FET bins simulation parameters. + + Parameters + ---------- + gm_id: gm/Id values + id_W: Id/W values + ft: f_T values + gm_gds: gm/gds values + vsweep: v-sweep values + fet_W: FET width + fet_L: FET length + plots: plots on which plot lines should be drawn + """ + # plot some interesting things + plots[0].plot(gm_id, id_W, label=f'W {fet_W} x L {fet_L}') + plots[1].plot(gm_id, ft, label=f'W {fet_W} x L {fet_L}') + plots[2].plot(gm_id, gm_gds, label=f'W {fet_W} x L {fet_L}') + plots[3].plot(vsweep, gm_id, label=f'W {fet_W} x L {fet_L}') + + +def close_plots(figures): + """ + Closes plots. + """ + for figure in figures: + plt.close(figure) + + +def generate_fet_plots( + corner_path, + bins_csv, + outdir, + outprefix, + only_W=None, + ext='svg'): + """ + Generates FET bins plots. + + Parameters + ---------- + corner_path: Path to FET model definitions and parameters + bins_csv: Path to the CSV file with bin parameters and FET model names + outdir: Directory where outputs should be stored + outprefix: Prefix for the output plot images + only_W: List of FET widths for which the plots should be generated + ext: extension for plot files + """ + print(f'[generate_fet_plots] {corner_path} {bins_csv}' + + f'{outdir} {outprefix} {only_W}') + + bins = Cell.parse_bins(bins_csv) + + bins_by_W = defaultdict(list) + # group bins by W + for fetbin in bins: + bins_by_W[(fetbin.device, fetbin.w)].append(fetbin) + + Ws = [key for key in bins_by_W.keys() + if only_W is None or key[1] in only_W] + + for fet_type, W in Ws: + if outprefix is None: + outprefix = fet_type + print(f'======> {fet_type}: {W}') + iparam = f'@m.xm1.m{fet_type}[%s]' + # fet_W and fet_L values here are only for initialization, they are + # later changed in the for loop + c = create_test_circuit(fet_type, 0.15, 1, corner_path) + + figures, plots = init_plots(fet_type, W) + try: + for fetbin in bins_by_W[(fet_type, W)]: + if only_W is not None and fetbin.w not in only_W: + continue + c.element('XM1').parameters['W'] = fetbin.w + c.element('XM1').parameters['L'] = fetbin.l + gm_id, ft, id_W, gm_gds, vsweep = run_sim(c, iparam, fetbin.w) + gen_plots(gm_id, id_W, ft, gm_gds, vsweep, fetbin.w, fetbin.l, plots) + except Exception: + close_plots(figures) + raise + + figtitles = ['Id_w', 'fT', 'gm_gds', 'gm_id'] + for figure, name in zip(figures, figtitles): + lg = figure.legend( + bbox_to_anchor=(1.05, 1), + loc='upper left' + ) + figure.tight_layout() + figure.savefig( + Path(outdir) / ( + outprefix + + f'_{fet_type}_{name}_W{str(W).replace(".", "_")}.{ext}'), + bbox_extra_artists=(lg,), + bbox_inches='tight' + ) + close_plots(figures) + + +def main(argv): + import argparse + + parser = argparse.ArgumentParser( + prog=argv[0], + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + 'corner_path', + help='Path to corner SPICE file containing FET definition', + type=Path + ) + parser.add_argument( + 'bins_csv', + help='Path to CSV file with fet_type, bin, fet_W and fet_L parameters', + type=Path + ) + parser.add_argument( + 'outdir', + help='Path to the directory to save the plots to', + type=Path + ) + parser.add_argument( + '--outprefix', + help='The prefix to add to plot file names' + ) + parser.add_argument( + '--only-w', + help='Simulate the FET only for a given fet_W values', + nargs='+', + type=float + ) + parser.add_argument( + '--ext', + help='The image extension to use for figures', + default='svg' + ) + args = parser.parse_args(argv[1:]) + + generate_fet_plots( + args.corner_path, + args.bins_csv, + args.outdir, + args.outprefix, + args.only_w, + args.ext) + + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff --git a/scripts/python-skywater-pdk/skywater_pdk/simulation/analog/generate-fet-bins-plots.py b/scripts/python-skywater-pdk/skywater_pdk/simulation/analog/generate-fet-bins-plots.py new file mode 100644 index 000000000..391de39c5 --- /dev/null +++ b/scripts/python-skywater-pdk/skywater_pdk/simulation/analog/generate-fet-bins-plots.py @@ -0,0 +1,131 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# Copyright 2020 SkyWater PDK Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + + +"""Creates plots for FET characteristics for FET cells in Skywater libraries. + +This script scans for FET cells in the Skywater PDK libraries and generates +the FET cells using methods from the fet submodule. +""" + + +import argparse +from pathlib import Path +import sys +import contextlib +import traceback +import errno + +from fet import generate_fet_plots + + +def main(argv): + parser = argparse.ArgumentParser( + prog=argv[0], + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter + ) + parser.add_argument( + 'libraries_dir', + help='Path to the libraries directory of skywater-pdk', + type=Path + ) + parser.add_argument( + 'corner_file', + help='Path to the corner SPICE file', + type=Path + ) + parser.add_argument( + 'output_dir', + help='Path to the output directory', + type=Path + ) + parser.add_argument( + '--libname', + help='Library name to generate the Symbolator diagrams for', + type=str + ) + parser.add_argument( + '--version', + help='Version for which the Symbolator diagrams should be generated', + type=str + ) + parser.add_argument( + '--create-dirs', + help='Create directories for output when not present', + action='store_true' + ) + parser.add_argument( + '--failed-inputs', + help=('Path to an output file which will store all input filenames ' + + 'for which ngspice failed to simulate' + ), + type=Path + ) + + args = parser.parse_args(argv[1:]) + + fetbins = list(args.libraries_dir.rglob('*fet*bins.csv')) + + nc = contextlib.nullcontext() + + with open(args.failed_inputs, 'w') if args.failed_inputs else nc as err: + for fetbin in fetbins: + outdir = (args.output_dir / + fetbin.parent.resolve() + .relative_to(args.libraries_dir.resolve())) + library = outdir.relative_to(args.output_dir).parts[0] + ver = outdir.relative_to(args.output_dir).parts[1] + if args.libname and args.libname != library: + continue + if args.version and args.version != ver: + continue + print(f'===> {str(fetbin)}') + try: + if not outdir.exists(): + if args.create_dirs: + outdir.mkdir(parents=True) + else: + print(f'The output directory {str(outdir)} is missing') + print('Run the script with --create-dirs') + return errno.ENOENT + + prefix = fetbin.name.replace('.bins.csv', '') + generate_fet_plots( + args.corner_file, + fetbin, + outdir, + f'{prefix}_', + ext='sim.svg' + ) + except (Exception, IndexError): + print( + f'Failed to generate FET plot for {str(fetbin)}', + file=sys.stderr + ) + traceback.print_exc() + if err: + err.write(f'{fetbin}\n') + + print('Finished generating FET plots') + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv))