Working on extracting metadata from found files.
diff --git a/scripts/python-skywater-pdk/collect_metadata.py b/scripts/python-skywater-pdk/collect_metadata.py
new file mode 100755
index 0000000..99a77da
--- /dev/null
+++ b/scripts/python-skywater-pdk/collect_metadata.py
@@ -0,0 +1,94 @@
+#!/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
+
+import os
+import pprint
+import sys
+import traceback
+
+from skywater_pdk import base, corners, drives
+
+
+def process(cellpath):
+    assert os.path.exists(cellpath), cellpath
+    assert os.path.isdir(cellpath), cellpath
+
+    files = [
+        (f, os.path.abspath(os.path.join(cellpath, f)))
+        for f in os.listdir(cellpath)]
+    files.sort()
+
+    dcell, fname = base.parse_pathname(cellpath)
+    assert isinstance(dcell, base.Cell), (cellpath, dcell, fname)
+    assert fname is None, (cellpath, dcell, fname)
+    extensions = set()
+    dcorners = set()
+    ddrives = set()
+    errors = []
+    for fname, fpath in files:
+        print("Processing:", fname)
+        if fname in ('README.rst',):
+            continue
+        try:
+            fcell, fextra, fext = base.parse_filename(fpath)
+        except Exception as e:
+            traceback.print_exc()
+            errors.append(e)
+        assert isinstance(fcell, base.Cell), (fpath, fcell, fextra, ext)
+
+        extensions.add(fext)
+
+        assert fcell.library == dcell.library, (fcell, dcell)
+        if not fextra:
+            continue
+
+        try:
+            fcorner = corners.parse_filename(fextra)
+        except Exception as e:
+            traceback.print_exc()
+            errors.append(e)
+
+        try:
+            assert fcell.name.startswith(dcell.name), (fcell, dcell)
+            fdrive = fcell.name[len(dcell.name):]
+
+            ddrives.add(drives.parse_drive(fdrive))
+        except Exception as e:
+            traceback.print_exc()
+            errors.append(e)
+
+        dcorners.add(fcorner)
+
+    dcorners = list(sorted(dcorners))
+    ddrives = list(sorted(ddrives))
+
+    print()
+    print(cellpath)
+    print('-'*75)
+    print('Cell:', dcell)
+    print('Cell drives:', ddrives)
+    print('Cell corners:')
+    pprint.pprint(dcorners)
+    print('File types:', extensions)
+    if errors:
+        raise ValueError("\n".join(errors))
+
+
+def main(args):
+    for a in args:
+        print()
+        print()
+        process(os.path.abspath(a))
+
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv[1:]))
diff --git a/scripts/python-skywater-pdk/skywater_pdk/base.py b/scripts/python-skywater-pdk/skywater_pdk/base.py
index 08096a6..0b2e205 100644
--- a/scripts/python-skywater-pdk/skywater_pdk/base.py
+++ b/scripts/python-skywater-pdk/skywater_pdk/base.py
@@ -16,6 +16,8 @@
 from enum import Enum
 from typing import Optional, Union, Tuple
 
+from .utils import comparable_to_none
+
 
 LibraryOrCell = Union['Library', 'Cell']
 
@@ -151,7 +153,7 @@
 
     >>> t = list(parse_filename('sky130_fd_io__top_ground_padonlyv2__tt_1p80V_3p30V_3p30V_25C.wrap.lib'))
     >>> t.pop(0)
-    Cell(name='top_ground_padonlyv2', library=Library(node=LibraryNode.SKY130, source=LibrarySource('fd'), type=LibraryType.io, name=None, version=None))
+    Cell(name='top_ground_padonlyv2', library=Library(node=LibraryNode.SKY130, source=LibrarySource('fd'), type=LibraryType.io, name='', version=None))
     >>> t.pop(0)
     'tt_1p80V_3p30V_3p30V_25C'
     >>> t.pop(0)
@@ -166,15 +168,22 @@
 
     >>> t = list(parse_filename('sky130_fd_io/v0.1.0/sky130_fd_io__top_powerhv_hvc_wpad__tt_1p80V_3p30V_100C.wrap.json'))
     >>> t.pop(0)
-    Cell(name='top_powerhv_hvc_wpad', library=Library(node=LibraryNode.SKY130, source=LibrarySource('fd'), type=LibraryType.io, name=None, version=LibraryVersion(milestone=0, major=1, minor=0, commits=0, hash='')))
+    Cell(name='top_powerhv_hvc_wpad', library=Library(node=LibraryNode.SKY130, source=LibrarySource('fd'), type=LibraryType.io, name='', version=LibraryVersion(milestone=0, major=1, minor=0, commits=0, hash='')))
     >>> from skywater_pdk.corners import parse_filename as pf_corners
     >>> pf_corners(t.pop(0))
-    Corner(volts=[1.8, 3.3], temps=[100], flags=[], types=[CornerType.t, CornerType.t])
+    Corner(corner=(CornerType.t, CornerType.t), volts=(1.8, 3.3), temps=(100,), flags=None)
     >>> t.pop(0)
     'wrap.json'
 
     >>> parse_filename('libraries/sky130_fd_io/v0.2.1/cells/analog_pad/sky130_fd_io-analog_pad.blackbox.v')[0]
-    Library(node=LibraryNode.SKY130, source=LibrarySource('fd'), type=LibraryType.io, name=None, version=LibraryVersion(milestone=0, major=2, minor=1, commits=0, hash=''))
+    Cell(name='analog_pad', library=Library(node=LibraryNode.SKY130, source=LibrarySource('fd'), type=LibraryType.io, name='', version=LibraryVersion(milestone=0, major=2, minor=1, commits=0, hash='')))
+
+    >>> t = list(parse_filename('skywater-pdk/libraries/sky130_fd_sc_hd/v0.0.1/cells/a2111o/sky130_fd_sc_hd__a2111o.blackbox.v'))
+    >>> t.pop(0)
+    Cell(name='a2111o', library=Library(node=LibraryNode.SKY130, source=LibrarySource('fd'), type=LibraryType.sc, name='hd', version=LibraryVersion(milestone=0, major=0, minor=1, commits=0, hash='')))
+    >>> assert t.pop(0) is None
+    >>> t.pop(0)
+    'blackbox.v'
 
     """
     dirname, filename = os.path.split(pathname)
@@ -201,18 +210,20 @@
 
     # Parse the actual filename
     bits = basename.split(SEPERATOR, 3)
-    if len(bits) in (1, 2):
+    if len(bits) in (1,):
         library = Library.parse(bits.pop(0))
         extra = ""
         if bits:
             extra = bits.pop(0)
         if version:
             library.version = version
-    elif len(bits) == 3:
+    elif len(bits) in (2, 3):
         library = Cell.parse(bits[0]+SEPERATOR+bits[1])
         if version:
             library.library.version = version
-        extra = bits[2]
+        extra = None
+        if len(bits) > 2:
+            extra = bits[2]
     else:
         raise NotImplementedError()
 
@@ -221,7 +232,7 @@
 
 SEPERATOR = "__"
 
-
+@comparable_to_none
 @dataclass_json
 @dataclass(order=True, frozen=True)
 class LibraryVersion:
@@ -251,7 +262,7 @@
     True
     >>> v0 < v2
     True
-    >>> l = [v1a, v2, v3, v1b, v0, v2]
+    >>> l = [v1a, v2, v3, None, v1b, v0, v2]
     >>> l.sort()
     >>> [i.fullname for i in l]
     ['0.0.0', '0.0.0-4-g123abc', '0.0.0-10-g123abc', '0.0.2', '0.0.2', '0.2.0']
@@ -260,8 +271,8 @@
     major: int = 0
     minor: int = 0
 
-    commits: Optional[int] = 0
-    hash: Optional[str] = ''
+    commits: int = 0
+    hash: str = ''
 
     @classmethod
     def parse(cls, s):
@@ -362,6 +373,7 @@
         return self.value
 
 
+@comparable_to_none
 @dataclass_json
 @dataclass
 class Library:
@@ -385,13 +397,17 @@
     >>> l.source.fullname
     "Unknown source: 'rrr'"
 
+    >>> l1 = Library.parse("sky130_fd_sc_hd")
+    >>> l2 = Library.parse("sky130_fd_sc_hdll")
+    >>> l = [l2, None, l1]
+    >>> l.sort()
 
     """
 
     node: LibraryNode
     source: LibrarySource
     type: LibraryType
-    name: Optional[str] = None
+    name: str = ''
     version: Optional[LibraryVersion] = None
 
     @property
diff --git a/scripts/python-skywater-pdk/skywater_pdk/corners.py b/scripts/python-skywater-pdk/skywater_pdk/corners.py
index f1ad75a..c46dba1 100644
--- a/scripts/python-skywater-pdk/skywater_pdk/corners.py
+++ b/scripts/python-skywater-pdk/skywater_pdk/corners.py
@@ -15,9 +15,11 @@
 from enum import Flag
 from dataclasses import dataclass
 from dataclasses_json import dataclass_json
-from typing import List, Optional
+from typing import Tuple, Optional
 
 from . import base
+from .utils import OrderedFlag
+from .utils import comparable_to_none
 
 
 CornerTypeMappings = {}
@@ -31,14 +33,14 @@
 CornerTypeMappings["ws"] = "ss"
 
 
-class CornerType(Flag):
+class CornerType(OrderedFlag):
     """
     >>> CornerType.parse('t')
     CornerType.t
     >>> CornerType.parse('tt')
     [CornerType.t, CornerType.t]
     >>> CornerType.parse('wp')
-    [CornerType.f, CornerType.s]
+    [CornerType.f, CornerType.f]
     """
     t = 'Typical'  # all  nominal (typical) values
     f = 'Fast'     # fast, that is, values that make transistors run faster
@@ -67,36 +69,43 @@
         return self.value
 
 
-class CornerFlag(Flag):
+class CornerFlag(OrderedFlag):
     nointpr = 'No internal power'
     lv = 'Low voltage'
     ccsnoise = 'Composite Current Source Noise'
-    ns5 = '5 nanoseconds'
     pwr = 'Power'
 
     @classmethod
     def parse(cls, s):
-        if s == "5ns":
-            return cls.ns5
-        elif hasattr(cls, s):
+        if hasattr(cls, s):
             return getattr(cls, s)
         else:
             raise TypeError("Unknown CornerFlags: {}".format(s))
 
     def __repr__(self):
-        return 'CornerType.'+self.name
+        return 'CornerFlag.'+self.name
 
     def __str__(self):
         return self.value
 
 
+@comparable_to_none
+class OptionalTuple(tuple):
+    pass
+
+
+@comparable_to_none
 @dataclass_json
-@dataclass
+@dataclass(frozen=True, order=True)
 class Corner:
-    volts: List[float]
-    temps: List[int]
-    flags: List[CornerFlag]
-    types: Optional[List[CornerType]] = None
+    corner: Tuple[CornerType, CornerType]
+    volts: Tuple[float, ...]
+    temps: Tuple[int, ...]
+    flags: Optional[Tuple[CornerFlag, ...]] = None
+
+    def __post_init__(self):
+        if self.flags:
+            object.__setattr__(self, 'flags', OptionalTuple(self.flags))
 
 
 VOLTS_REGEX = re.compile('([0-9]p[0-9]+)V')
@@ -105,49 +114,49 @@
     """Extract corner information from a filename.
 
     >>> parse_filename('tt_1p80V_3p30V_3p30V_25C')
-    Corner(volts=[1.8, 3.3, 3.3], temps=[25], flags=[], types=[CornerType.t, CornerType.t])
+    Corner(corner=(CornerType.t, CornerType.t), volts=(1.8, 3.3, 3.3), temps=(25,), flags=None)
 
     >>> parse_filename('sky130_fd_io__top_ground_padonlyv2__tt_1p80V_3p30V_3p30V_25C.wrap.lib')
-    Corner(volts=[1.8, 3.3, 3.3], temps=[25], flags=[], types=[CornerType.t, CornerType.t])
+    Corner(corner=(CornerType.t, CornerType.t), volts=(1.8, 3.3, 3.3), temps=(25,), flags=None)
 
     >>> parse_filename('sky130_fd_sc_ms__tt_1p80V_100C.wrap.json')
-    Corner(volts=[1.8], temps=[100], flags=[], types=[CornerType.t, CornerType.t])
+    Corner(corner=(CornerType.t, CornerType.t), volts=(1.8,), temps=(100,), flags=None)
 
     >>> parse_filename('sky130_fd_sc_ms__tt_1p80V_100C.wrap.lib')
-    Corner(volts=[1.8], temps=[100], flags=[], types=[CornerType.t, CornerType.t])
+    Corner(corner=(CornerType.t, CornerType.t), volts=(1.8,), temps=(100,), flags=None)
 
     >>> parse_filename('sky130_fd_sc_ms__tt_1p80V_25C_ccsnoise.wrap.json')
-    Corner(volts=[1.8], temps=[25], flags=[CornerType.ccsnoise], types=[CornerType.t, CornerType.t])
-
-    >>> parse_filename('sky130_fd_sc_ms__wp_1p56V_n40C_5ns.wrap.json')
-    Corner(volts=[1.56], temps=[-40], flags=[CornerType.ns5], types=[CornerType.f, CornerType.s])
+    Corner(corner=(CornerType.t, CornerType.t), volts=(1.8,), temps=(25,), flags=(CornerFlag.ccsnoise,))
 
     >>> parse_filename('sky130_fd_sc_ms__wp_1p65V_n40C.wrap.json')
-    Corner(volts=[1.65], temps=[-40], flags=[], types=[CornerType.f, CornerType.s])
+    Corner(corner=(CornerType.f, CornerType.f), volts=(1.65,), temps=(-40,), flags=None)
 
     >>> parse_filename('sky130_fd_sc_ms__wp_1p95V_85C_pwr.wrap.lib')
-    Corner(volts=[1.95], temps=[85], flags=[CornerType.pwr], types=[CornerType.f, CornerType.s])
+    Corner(corner=(CornerType.f, CornerType.f), volts=(1.95,), temps=(85,), flags=(CornerFlag.pwr,))
 
     >>> parse_filename('sky130_fd_sc_ms__wp_1p95V_n40C_ccsnoise.wrap.json')
-    Corner(volts=[1.95], temps=[-40], flags=[CornerType.ccsnoise], types=[CornerType.f, CornerType.s])
+    Corner(corner=(CornerType.f, CornerType.f), volts=(1.95,), temps=(-40,), flags=(CornerFlag.ccsnoise,))
 
     >>> parse_filename('sky130_fd_sc_ms__wp_1p95V_n40C_pwr.wrap.lib')
-    Corner(volts=[1.95], temps=[-40], flags=[CornerType.pwr], types=[CornerType.f, CornerType.s])
+    Corner(corner=(CornerType.f, CornerType.f), volts=(1.95,), temps=(-40,), flags=(CornerFlag.pwr,))
+
+    >>> parse_filename('sky130_fd_sc_hd__a2111o_4__ss_1p76V_n40C.cell.json')
+    Corner(corner=(CornerType.s, CornerType.s), volts=(1.76,), temps=(-40,), flags=None)
 
     """
-    pathname = pathname.replace('-', base.SEPERATOR) # FIXME: !!!
-
     if base.SEPERATOR in pathname:
-        _, extra, extension = base.parse_filename(pathname)
+        cell, extra, extension = base.parse_filename(pathname)
     else:
+        cell = None
         extra = pathname
         extension = ''
 
-    if extension not in ('', 'lib', 'wrap.lib', 'wrap.json'):
+    if extension not in ('', 'lib', 'cell.lib', 'cell.json', 'wrap.lib', 'wrap.json'):
         raise ValueError('Not possible to extract corners from: {!r}'.format(extension))
 
     if not extra:
-        raise ValueError('No corners found in: {!r}'.format(pathname))
+        extra = cell.name
+        cell = None
 
     kw = {}
     kw['flags'] = []
@@ -165,11 +174,17 @@
             assert b.endswith('C'), b
             kw['temps'].append(int(b[:-1].replace('n', '-')))
         else:
-            if 'types' not in kw:
-                kw['types'] = CornerType.parse(b)
+            if 'corner' not in kw:
+                kw['corner'] = CornerType.parse(b)
             else:
                 kw['flags'].append(CornerFlag.parse(b))
 
+    for k, v in kw.items():
+        kw[k] = tuple(v)
+
+    if not kw['flags']:
+        del kw['flags']
+
     return Corner(**kw)
 
 
diff --git a/scripts/python-skywater-pdk/skywater_pdk/drives.py b/scripts/python-skywater-pdk/skywater_pdk/drives.py
index 165ee45..1b6b5ac 100644
--- a/scripts/python-skywater-pdk/skywater_pdk/drives.py
+++ b/scripts/python-skywater-pdk/skywater_pdk/drives.py
@@ -17,6 +17,10 @@
 from dataclasses_json import dataclass_json
 
 
+def parse_drive(s):
+    return DriveStrength.from_suffix(s)
+
+
 class InvalidSuffixError(ValueError):
     def __init__(self, s):
         ValueError.__init__(self, "Invalid suffix: {}".format(s.strip()))
diff --git a/scripts/python-skywater-pdk/skywater_pdk/utils.py b/scripts/python-skywater-pdk/skywater_pdk/utils.py
new file mode 100644
index 0000000..d3752b2
--- /dev/null
+++ b/scripts/python-skywater-pdk/skywater_pdk/utils.py
@@ -0,0 +1,170 @@
+#!/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
+
+import dataclasses
+import random
+import sys
+
+from dataclasses import dataclass
+from enum import Flag
+from typing import Optional, Tuple, Any
+
+
+def comparable_to_none(cls):
+    """
+
+    >>> @comparable_to_none
+    ... @dataclass(order=True)
+    ... class A:
+    ...     a: int = 0
+    >>> @comparable_to_none
+    ... @dataclass(order=True)
+    ... class B:
+    ...     b: Optional[A] = None
+    >>> b0 = B()
+    >>> repr(b0)
+    'B(b=None)'
+    >>> str(b0)
+    'B(b=None)'
+    >>> b1 = B(A())
+    >>> repr(b1)
+    'B(b=A(a=0))'
+    >>> str(b1)
+    'B(b=A(a=0))'
+    >>> b2 = B(A(2))
+    >>> repr(b2)
+    'B(b=A(a=2))'
+    >>> str(b2)
+    'B(b=A(a=2))'
+    >>> l = [b0, b1, b2, None]
+    >>> for i in range(0, 3):
+    ...     random.shuffle(l)
+    ...     l.sort()
+    ...     print(l)
+    [None, B(b=None), B(b=A(a=0)), B(b=A(a=2))]
+    [None, B(b=None), B(b=A(a=0)), B(b=A(a=2))]
+    [None, B(b=None), B(b=A(a=0)), B(b=A(a=2))]
+
+    """
+    class ComparableToNoneVersion(cls):
+        def __ge__(self, other):
+            if other is None:
+                return True
+            return super().__ge__(other)
+        def __gt__(self, other):
+            if other is None:
+                return True
+            return super().__gt__(other)
+        def __le__(self, other):
+            if other is None:
+                return False
+            return super().__le__(other)
+        def __lt__(self, other):
+            if other is None:
+                return False
+            return super().__lt__(other)
+        def __eq__(self, other):
+            if other is None:
+                return False
+            return super().__eq__(other)
+        def __hash__(self):
+            return super().__hash__()
+        def __repr__(self):
+            s = super().__repr__()
+            return s.replace('comparable_to_none.<locals>.ComparableToNoneVersion', cls.__name__)
+
+    return ComparableToNoneVersion
+
+
+def _is_optional_type(t):
+    """
+    >>> _is_optional_type(Optional[int])
+    True
+    >>> _is_optional_type(Optional[Tuple])
+    True
+    >>> _is_optional_type(Any)
+    False
+    """
+    return hasattr(t, "__args__") and len(t.__args__) == 2 and t.__args__[-1] is type(None)
+
+
+def _get_the_optional_type(t):
+    """
+    >>> _get_the_optional_type(Optional[int])
+    <class 'int'>
+    >>> _get_the_optional_type(Optional[Tuple])
+    typing.Tuple
+    >>> class A:
+    ...     pass
+    >>> _get_the_optional_type(Optional[A])
+    <class '__main__.A'>
+    >>> _get_type_name(_get_the_optional_type(Optional[A]))
+    'A'
+    """
+    assert _is_optional_type(t), t
+    return t.__args__[0]
+
+
+def _get_type_name(ot):
+    """
+    >>> _get_type_name(int)
+    'int'
+    >>> _get_type_name(Tuple)
+    'Tuple'
+    >>> _get_type_name(Optional[Tuple])
+    'typing.Union[typing.Tuple, NoneType]'
+    """
+    if hasattr(ot, "_name") and ot._name:
+        return ot._name
+    elif hasattr(ot, "__name__") and ot.__name__:
+        return ot.__name__
+    else:
+        return str(ot)
+
+
+class OrderedFlag(Flag):
+    def __ge__(self, other):
+        if other is None:
+            return True
+        if self.__class__ is other.__class__:
+            return self.value >= other.value
+        return NotImplemented
+    def __gt__(self, other):
+        if other is None:
+            return True
+        if self.__class__ is other.__class__:
+            return self.value > other.value
+        return NotImplemented
+    def __le__(self, other):
+        if other is None:
+            return False
+        if self.__class__ is other.__class__:
+            return self.value <= other.value
+        return NotImplemented
+    def __lt__(self, other):
+        if other is None:
+            return False
+        if self.__class__ is other.__class__:
+            return self.value < other.value
+        return NotImplemented
+    def __eq__(self, other):
+        if other is None:
+            return False
+        if self.__class__ is other.__class__:
+            return self.value == other.value
+        return NotImplemented
+    def __hash__(self):
+        return hash(self._name_)
+
+
+if __name__ == "__main__":
+    import doctest
+    doctest.testmod()