Merge branch 'main' into 86-cell_cross_index
diff --git a/.readthedocs.yml b/.readthedocs.yml
index bea9451..668fa38 100644
--- a/.readthedocs.yml
+++ b/.readthedocs.yml
@@ -31,6 +31,11 @@
submodules:
include:
- libraries/sky130_fd_io/latest
+ - libraries/sky130_fd_sc_hd/latest
+ - libraries/sky130_fd_sc_hdll/latest
+ - libraries/sky130_fd_sc_hs/latest
+ - libraries/sky130_fd_sc_ls/latest
+ - libraries/sky130_fd_sc_ms/latest
recursive: false
formats:
diff --git a/docs/_ext/skywater_pdk b/docs/_ext/skywater_pdk
new file mode 120000
index 0000000..c5fe58c
--- /dev/null
+++ b/docs/_ext/skywater_pdk
@@ -0,0 +1 @@
+../../scripts/python-skywater-pdk/skywater_pdk/
\ No newline at end of file
diff --git a/docs/conf.py b/docs/conf.py
index f5c16d7..625b88a 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -31,9 +31,9 @@
import docutils
import os
import re
-# import sys
+import sys
# sys.path.insert(0, os.path.abspath('.'))
-
+sys.path.insert(0, os.path.abspath('./_ext'))
# -- Project information -----------------------------------------------------
@@ -67,6 +67,8 @@
'sphinx.ext.todo',
'sphinxcontrib_hdl_diagrams',
'sphinxcontrib.bibtex',
+ 'skywater_pdk.cells.cross_index',
+ 'skywater_pdk.cells.generate.readme',
]
bibtex_default_style = 'plain'
@@ -410,3 +412,6 @@
app.add_role('lib', lib_role)
app.add_role('cell', cell_role)
app.add_role('model', cell_role)
+
+ app.emit("cells_generate_readme", 'contents/libraries/*/cells/*')
+
diff --git a/docs/contents/cell-index.rst b/docs/contents/cell-index.rst
new file mode 100644
index 0000000..448c981
--- /dev/null
+++ b/docs/contents/cell-index.rst
@@ -0,0 +1 @@
+.. cross_index:: libraries/*
diff --git a/docs/contents/libraries.rst b/docs/contents/libraries.rst
index b7b29de..d760ec6 100644
--- a/docs/contents/libraries.rst
+++ b/docs/contents/libraries.rst
@@ -146,3 +146,8 @@
libraries/sky130_ef_io/README
+.. toctree::
+ :maxdepth: 1
+ :name: Cells in libraries cross-index
+
+ cell-index
diff --git a/scripts/python-skywater-pdk/skywater_pdk/cells/cross_index.py b/scripts/python-skywater-pdk/skywater_pdk/cells/cross_index.py
new file mode 100755
index 0000000..fa9bf69
--- /dev/null
+++ b/scripts/python-skywater-pdk/skywater_pdk/cells/cross_index.py
@@ -0,0 +1,307 @@
+#!/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
+
+import argparse
+import json
+import os
+import pathlib
+import pprint
+import sys
+import textwrap
+from docutils import nodes
+from docutils.parsers.rst import Directive
+from docutils.statemachine import ViewList
+from sphinx.util.nodes import nested_parse_with_titles
+
+from typing import Tuple, List, Dict
+
+verbose = False
+
+# using a list-table here to allow for easier line breaks in description
+rst_header_line_char = '-'
+rst_header = 'Cells in libraries cross-index'
+rst_template ="""\
+{header_line}
+{header_underline}
+
+.. list-table::
+ :header-rows: 1
+
+ * - Cell name
+ - {lib_suffixes}
+ - Number of libraries
+{cell_list}
+"""
+
+cell_template = """\
+ * - {cell_name}
+ - {lib_suffixes_match}
+ - {lib_count}
+"""
+
+tab_entry = '\n - '
+
+def collect(library_dir) -> Tuple[str, List[str]]:
+ """Collect the available definitions for cells in a library
+
+ Parameters
+ ----------
+ library_dir: str or pathlib.Path
+ Path to a library.
+
+ Returns
+ -------
+ lib : str
+ Library name
+
+ cells : list of pathlib.Path
+ definition files for cells in the library.
+ """
+
+ if not isinstance(library_dir, pathlib.Path):
+ library_dir = pathlib.Path(library_dir)
+
+ libname = None
+ cells = set()
+
+ for p in library_dir.rglob("definition.json"):
+ if not p.is_file():
+ continue
+ define_data = json.load(open(p))
+ if not define_data['type'] == 'cell':
+ continue
+ cells.add(p)
+ if libname is None:
+ libname = define_data['library']
+
+ cells = list(sorted(cells))
+ if not len(cells):
+ raise FileNotFoundError("No cell definitions found")
+ assert len(libname) > 0
+ return libname, cells
+
+def get_cell_names(cells):
+ """Get cell names from definition filess
+
+ Parameters
+ ----------
+ cells: list of pathlib.Path
+ List of paths to JSON description files
+
+ Returns
+ -------
+ cell_list: list of str
+ List of cell names
+ """
+
+ cell_list = []
+
+ for cell in cells:
+ with open(str(cell), "r") as c:
+ cell_json = json.load(c)
+ cell_list.append( cell_json['name'] )
+ return cell_list
+
+
+def generate_crosstable (cells_lib, link_template=''):
+ """Generate the RST paragraph containing cell cross reference table
+
+ Parameters:
+ cells_lib: dictionary with list of libraries per cell name [dict]
+ link_template: cell README generic path (with {lib} and {cell} tags) [str]
+
+ Returns:
+ paragraph: Generated paragraph [str]
+
+ """
+
+ assert isinstance (cells_lib, dict)
+
+ paragraph = ""
+ cell_list = ""
+
+ lib_suffixes = set()
+ for v in cells_lib.values():
+ lib_suffixes.update( [lib.rpartition('_')[2] for lib in v] )
+ lib_suffixes = list(lib_suffixes)
+ lib_suffixes.sort()
+ #print (lib_suffixes)
+
+ for c in sorted(cells_lib):
+ ls = {} # dictionary of cell library shorts (suffixes)
+ for lib in cells_lib[c]:
+ ls [lib.rpartition('_')[2]] = lib
+ mark = ' :doc:`x <' + link_template + '>`' # lib match mark with link
+ suff_match = [ mark.format(cell=c,lib=ls[s]) if s in ls else '' for s in lib_suffixes ]
+ cell_list += cell_template.format(
+ cell_name = c,
+ lib_suffixes_match = tab_entry.join(suff_match),
+ lib_count = str (len(ls))
+ )
+
+ paragraph = rst_template.format(
+ header_line = rst_header,
+ header_underline = rst_header_line_char * len(rst_header),
+ lib_suffixes = tab_entry.join(lib_suffixes),
+ cell_list = cell_list
+ )
+ return paragraph
+
+
+def cells_in_libs (libpaths):
+ """Generate the RST paragraph containing cell cross reference table
+
+ Parameters:
+ libpaths: list of cell library paths [list of pathlib.Path]
+
+ Returns:
+ cells_lib: dictionary with list of libraries containing each cell name [dict]
+ """
+
+ lib_dirs = [pathlib.Path(d) for d in libpaths]
+ lib_dirs = [d for d in lib_dirs if d.is_dir()]
+ libs_toc = dict()
+
+ for lib in lib_dirs:
+ try:
+ libname, cells = collect(lib)
+ if verbose:
+ print(f"{lib} \tLibrary name: {libname}, found {len(cells)} cells")
+ libs_toc[libname] = get_cell_names(cells)
+ except FileNotFoundError:
+ if verbose:
+ print (f'{lib} \t- no cells found')
+
+ all_cells = set()
+ cells_lib = {}
+ for lib,cells in libs_toc.items():
+ all_cells.update(set(cells))
+ for c in cells:
+ cells_lib[c] = cells_lib.get(c, []) + [lib]
+
+ return cells_lib
+
+
+
+# --- Sphinx extension wrapper ---
+
+class CellCrossIndex(Directive):
+
+ required_arguments = 1
+ optional_arguments = 1
+ has_content = True
+
+ def run(self):
+ env = self.state.document.settings.env
+ dirname = env.docname.rpartition('/')[0]
+ arg = self.arguments[0]
+ arg = dirname + '/' + arg
+ output = dirname + '/' + self.arguments[1] if len(self.arguments)>2 else None
+
+ path = pathlib.Path(arg).expanduser()
+ parts = path.parts[1:] if path.is_absolute() else path.parts
+ paths = pathlib.Path(path.root).glob(str(pathlib.Path("").joinpath(*parts)))
+ paths = list(paths)
+ paths = [d.resolve() for d in paths if d.is_dir()]
+
+ cells_lib = cells_in_libs ( list(paths) )
+ celllink = self.arguments[0].replace('*','{lib}') + '/cells/{cell}/README'
+ paragraph = generate_crosstable (cells_lib,celllink)
+
+ if output is None: # dynamic output
+ # parse rst string to docutils nodes
+ rst = ViewList()
+ for i,line in enumerate(paragraph.split('\n')):
+ rst.append(line, "cell-index-tmp.rst", i+1)
+ node = nodes.section()
+ node.document = self.state.document
+ nested_parse_with_titles(self.state, rst, node)
+ return node.children
+ else: # file output
+ if not output.endswith('.rst'):
+ output += '.rst'
+ with open(str(output),'w') as f:
+ f.write(paragraph)
+ paragraph_node = nodes.paragraph()
+ return [paragraph_node]
+
+def setup(app):
+ app.add_directive("cross_index", CellCrossIndex)
+
+ return {
+ 'version': '0.1',
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }
+
+# --- stand alone, command line operation ---
+
+def main():
+ global verbose
+ parser = argparse.ArgumentParser()
+ alllibpath = '../../../libraries/*/latest'
+ celllink = 'libraries/{lib}/cells/{cell}/README'
+
+ parser.add_argument(
+ "-v",
+ "--verbose",
+ help="increase verbosity",
+ action="store_true"
+ )
+ parser.add_argument(
+ "--all_libs",
+ help="process all libs in "+alllibpath,
+ action="store_true")
+ parser.add_argument(
+ "libraries_dirs",
+ help="Paths to the library directories. Eg. " + alllibpath,
+ type=pathlib.Path,
+ nargs="*")
+ parser.add_argument(
+ "-o",
+ "--outfile",
+ help="Output file name",
+ type=pathlib.Path,
+ default=pathlib.Path('./cell-index.rst'))
+ parser.add_argument(
+ "-c",
+ "--celllink",
+ help="Specify cell link template. Default: '" + celllink +"'",
+ type=str,
+ default=celllink)
+
+ args = parser.parse_args()
+ verbose = args.verbose
+
+ if args.all_libs:
+ path = pathlib.Path(alllibpath).expanduser()
+ parts = path.parts[1:] if path.is_absolute() else path.parts
+ paths = pathlib.Path(path.root).glob(str(pathlib.Path("").joinpath(*parts)))
+ args.libraries_dirs = list(paths)
+
+
+ cells_lib = cells_in_libs (args.libraries_dirs)
+ par = generate_crosstable (cells_lib,args.celllink)
+
+ with open(str(args.outfile),'w') as f:
+ f.write(par)
+
+
+if __name__ == "__main__":
+ sys.exit(main())
diff --git a/scripts/python-skywater-pdk/skywater_pdk/cells/generate/readme.py b/scripts/python-skywater-pdk/skywater_pdk/cells/generate/readme.py
new file mode 100755
index 0000000..ff90d34
--- /dev/null
+++ b/scripts/python-skywater-pdk/skywater_pdk/cells/generate/readme.py
@@ -0,0 +1,235 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# Copyright 2020 The SkyWater PDK Authors.
+#
+# Use of this source code is governed by the Apache 2.0
+# license that can be found in the LICENSE file or at
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# SPDX-License-Identifier: Apache-2.0
+
+''' This is a prototype of cell documentation generation script.
+'''
+
+import csv
+import json
+import os
+import sys
+import argparse
+import pathlib
+import glob
+import subprocess
+import textwrap
+from docutils import nodes
+from docutils.parsers.rst import Directive
+from docutils.statemachine import ViewList
+from sphinx.util.nodes import nested_parse_with_titles
+
+verbose = False
+
+readme_template ="""\
+{header}
+
+**{description}**
+
+*This is a stub of cell description file*
+
+- **Cell name**: {name}
+- **Type**: {deftype}
+- **Verilog name**: {verilog_name}
+- **Library**: {library}
+- **Inputs**: {inputs}
+- **Outputs**: {outputs}
+
+{subheader_sym}
+
+.. list-table::
+
+ * - .. figure:: {symbol1}
+ -
+ - .. figure:: {symbol2}
+
+{subheader_sch}
+
+.. figure:: {schematic}
+ :align: center
+
+{subheader_gds}
+
+"""
+
+figure_template ="""
+
+.. figure:: {fig}
+ :align: center
+ :width: 50%
+
+ {name}
+"""
+
+def write_readme(cellpath, define_data):
+ ''' Generates README for a given cell.
+
+ Args:
+ cellpath - path to a cell [str of pathlib.Path]
+ define_data - cell data from json [dic]
+
+ '''
+ outpath = os.path.join(cellpath, 'README.rst')
+ prefix = define_data['file_prefix']
+ header = f':cell:`{prefix}`'
+ subheader_sym = header + ' symbols'
+ subheader_sch = header + ' schematic'
+ subheader_gds = header + ' GDSII layouts'
+
+ header += '\n' + '=' * len(header)
+ subheader_sym += '\n' + '-' * len(subheader_sym)
+ subheader_sch += '\n' + '-' * len(subheader_sch)
+ subheader_gds += '\n' + '-' * len(subheader_gds)
+
+
+ symbol1 = prefix + '.symbol.svg'
+ symbol2 = prefix + '.pp.symbol.svg'
+ schematic = prefix + '.schematic.svg'
+ inputs = []
+ outputs = []
+ for p in define_data['ports']:
+ try:
+ if p[0]=='signal' and p[2]=='input':
+ inputs.append(p[1])
+ if p[0]=='signal' and p[2]=='output':
+ outputs.append(p[1])
+ except:
+ pass
+ gdssvg = []
+ svglist = list(pathlib.Path(cellpath).glob('*.svg'))
+ for s in svglist:
+ gdsfile = pathlib.Path(os.path.join(cellpath, s.stem +'.gds'))
+ if gdsfile.is_file():
+ gdssvg.append(s)
+
+ with open(outpath, 'w') as f:
+ f.write (readme_template.format (
+ header = header,
+ subheader_sym = subheader_sym,
+ subheader_sch = subheader_sch,
+ subheader_gds = subheader_gds,
+ description = define_data['description'].rstrip('.'),
+ name = ':cell:`' + prefix +'`',
+ deftype = define_data['type'],
+ verilog_name = define_data['verilog_name'],
+ library = define_data['library'],
+ inputs = f'{len(inputs)} (' + ', '.join(inputs) + ')',
+ outputs = f'{len(outputs)} (' + ', '.join(outputs) + ')',
+ symbol1 = symbol1,
+ symbol2 = symbol2,
+ schematic = schematic,
+ ))
+ for gs in sorted(gdssvg):
+ f.write (figure_template.format (
+ fig = gs.name,
+ name = gs.stem
+ ))
+
+def process(cellpath):
+ ''' Processes cell indicated by path.
+ Opens cell definiton and calls further processing
+
+ Args:
+ cellpath - path to a cell [str of pathlib.Path]
+ '''
+ if verbose:
+ print()
+ print(cellpath)
+ define_json = os.path.join(cellpath, 'definition.json')
+ if not os.path.exists(define_json):
+ print("No definition.json in", cellpath)
+ assert os.path.exists(define_json), define_json
+ define_data = json.load(open(define_json))
+
+ if define_data['type'] == 'cell':
+ write_readme(cellpath, define_data)
+
+ return
+
+# --- Sphinx extension wrapper ----------------
+
+def GenerateCellReadme (app, cellpath):
+
+ print (f'GenerateCellReadme: generating files for {cellpath}')
+ path = pathlib.Path(cellpath).expanduser()
+ parts = path.parts[1:] if path.is_absolute() else path.parts
+ paths = pathlib.Path(path.root).glob(str(pathlib.Path("").joinpath(*parts)))
+ paths = list(paths)
+ cell_dirs = [d.resolve() for d in paths if d.is_dir()]
+
+ errors = 0
+ for d in cell_dirs:
+ try:
+ process(d)
+ except (AssertionError, FileNotFoundError, ChildProcessError) as ex:
+ print (f'GenerateCellReadme: {type(ex).__name__}')
+ print (f'{ex.args}')
+ errors +=1
+ print (f'GenerateCellReadme: {len(cell_dirs)} files processed, {errors} errors.')
+
+def setup(app):
+ app.add_event("cells_generate_readme")
+ app.connect('cells_generate_readme', GenerateCellReadme)
+
+ return {
+ 'version': '0.1',
+ 'parallel_read_safe': True,
+ 'parallel_write_safe': True,
+ }
+
+# ----------------------------------------------
+
+def main():
+ ''' Generates README.rst for cell.'''
+
+ prereq_txt = ''
+ output_txt = 'output:\n generates README.rst'
+ allcellpath = '../../../libraries/*/latest/cells/*'
+
+ parser = argparse.ArgumentParser(
+ description = main.__doc__,
+ epilog = prereq_txt +'\n\n'+ output_txt,
+ formatter_class=argparse.RawDescriptionHelpFormatter)
+ parser.add_argument(
+ "--all_libs",
+ help="process all cells in "+allcellpath,
+ action="store_true")
+ parser.add_argument(
+ "cell_dir",
+ help="path to the cell directory",
+ type=pathlib.Path,
+ nargs="*")
+
+ args = parser.parse_args()
+
+ if args.all_libs:
+ path = pathlib.Path(allcellpath).expanduser()
+ parts = path.parts[1:] if path.is_absolute() else path.parts
+ paths = pathlib.Path(path.root).glob(str(pathlib.Path("").joinpath(*parts)))
+ args.cell_dir = list(paths)
+
+ cell_dirs = [d.resolve() for d in args.cell_dir if d.is_dir()]
+
+ errors = 0
+ for d in cell_dirs:
+ try:
+ process(d)
+ except KeyboardInterrupt:
+ sys.exit(1)
+ except (AssertionError, FileNotFoundError, ChildProcessError) as ex:
+ print (f'Error: {type(ex).__name__}')
+ print (f'{ex.args}')
+ errors +=1
+ print (f'\n{len(cell_dirs)} files processed, {errors} errors.')
+ return 0 if errors else 1
+
+if __name__ == "__main__":
+ sys.exit(main())
+