diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..e7f1a97 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "python.testing.pytestArgs": [ + "Tetris" + ], + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true +} \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index e580050..696b2fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,15 +4,18 @@ [workspace] members = [ + # "rules21", "gds21", "layout21", "layout21converters", "layout21protos", "layout21raw", - "layout21tetris", + # "layout21tetris", "layout21utils", + "layout21wgpu", "lef21", ] +resolver = "2" # Inherited Package Attributes # Thanks https://doc.rust-lang.org/cargo/reference/workspaces.html#the-package-table! diff --git a/Tetris/.gitignore b/Tetris/.gitignore new file mode 100644 index 0000000..b176143 --- /dev/null +++ b/Tetris/.gitignore @@ -0,0 +1,152 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintainted in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ \ No newline at end of file diff --git a/Tetris/LICENSE b/Tetris/LICENSE new file mode 100644 index 0000000..2a2ea6c --- /dev/null +++ b/Tetris/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2021, Dan Fritchman +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/Tetris/readme.md b/Tetris/readme.md new file mode 100644 index 0000000..6bac663 --- /dev/null +++ b/Tetris/readme.md @@ -0,0 +1,5 @@ + +# Tetris + +Gridded Semi-Custom Integrated Circuit Layout + diff --git a/Tetris/setup.py b/Tetris/setup.py new file mode 100644 index 0000000..4d4555c --- /dev/null +++ b/Tetris/setup.py @@ -0,0 +1,34 @@ +""" +# Setup Script + +Derived from the setuptools sample project at +https://github.com/pypa/sampleproject/blob/main/setup.py + +""" + +# Always prefer setuptools over distutils +from setuptools import setup, find_packages +import pathlib + +here = pathlib.Path(__file__).parent.resolve() + +# Get the long description from the README file +long_description = (here / "readme.md").read_text(encoding="utf-8") + +_VLSIR_VERSION = "1.0.0.dev0" + +setup( + name="tetris", + version=_VLSIR_VERSION, + description="Gridded Semi-Custom Integrated Circuit Layout", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/dan-fritchman/Layout21", + author="Dan Fritchman", + packages=find_packages(), + python_requires=">=3.7, <4", + install_requires=[f"vlsir=={_VLSIR_VERSION}", "numpy==1.21.5"], + extras_require={ + "dev": ["pytest==5.2", "coverage", "pytest-cov", "black==19.10b0", "twine"] + }, +) diff --git a/Tetris/tests/__init__.py b/Tetris/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Tetris/tests/empty.yaml b/Tetris/tests/empty.yaml new file mode 100644 index 0000000..b99215e --- /dev/null +++ b/Tetris/tests/empty.yaml @@ -0,0 +1,13 @@ +--- +domain: demolib +cells: + - name: democell + layout: + name: democell + outline: + x: [ 100 ] + y: [ 10 ] + metals: 0 + instances: [] + assignments: [] + cuts: [] \ No newline at end of file diff --git a/Tetris/tests/insts.yaml b/Tetris/tests/insts.yaml new file mode 100644 index 0000000..2a0e769 --- /dev/null +++ b/Tetris/tests/insts.yaml @@ -0,0 +1,32 @@ +--- +domain: demoinst +cells: + - name: unit + layout: + name: unit + outline: + x: [5] + y: [1] + metals: 2 + instances: [] + assignments: [] + cuts: [] + - name: democell + layout: + name: democell + outline: + x: [100] + y: [10] + metals: 5 + instances: + - name: unit1 + cell: + to: + Local: unit + loc: + place: + Abs: { x: 10, y: 1 } + reflect_horiz: false + reflect_vert: false + assignments: [] + cuts: [] diff --git a/Tetris/tests/stacks.rs b/Tetris/tests/stacks.rs new file mode 100644 index 0000000..fdaae66 --- /dev/null +++ b/Tetris/tests/stacks.rs @@ -0,0 +1,187 @@ +//! +//! # Test Sample [Stack]s +//! + +// Local imports +use crate::raw::{self, Dir, LayoutResult, Units}; +use crate::stack::*; +use crate::tracks::*; +use crate::utils::Ptr; +use crate::validate::ValidStack; + +/// # Sample Stacks +/// Namespace for commonly re-used [Stack]s for testing. +pub struct SampleStacks; + +impl SampleStacks { + /// As nearly empty a [Stack] as possible, while being raw-exportable. + /// Includes: + /// * A `boundary_layer` + /// * [raw::Layers] containing solely that boundary layer + /// * No metals or via layers + /// Generally useful for placement activities, particularly among [Instace]s. + pub fn empty() -> LayoutResult { + let mut rawlayers = raw::Layers::default(); + let boundary_layer = Some(rawlayers.add(raw::Layer::from_pairs( + 0, + &[(0, raw::LayerPurpose::Outline)], + )?)); + let stack = Stack { + units: Units::default(), + boundary_layer, + prim: PrimitiveLayer::new((100, 100).into()), + metals: Vec::new(), // No metal layers + vias: Vec::new(), // No vias + rawlayers: Some(Ptr::new(rawlayers)), + }; + Ok(stack.validate()?) + } + + /// Real(istic) PDK [Stack] + pub fn pdka() -> LayoutResult { + let mut rawlayers = raw::Layers::default(); + // Shorthands for the common purpose-numbers + let metal_purps = [ + (255, raw::LayerPurpose::Obstruction), + (20, raw::LayerPurpose::Drawing), + (5, raw::LayerPurpose::Label), + (16, raw::LayerPurpose::Pin), + ]; + let via_purps = [ + (255, raw::LayerPurpose::Obstruction), + (44, raw::LayerPurpose::Drawing), + (5, raw::LayerPurpose::Label), + (16, raw::LayerPurpose::Pin), + ]; + // Add a few base-layers that we are used in imported/ primitive cells, but not in our stack + rawlayers.add(raw::Layer::new(64, "nwell").add_pairs(&metal_purps)?); + rawlayers.add(raw::Layer::new(67, "li1").add_pairs(&metal_purps)?); + // Create the test stack + let stack = Stack { + units: Units::Nano, + boundary_layer: Some(rawlayers.add(raw::Layer::from_pairs( + 236, + &[(0, raw::LayerPurpose::Outline)], + )?)), + prim: PrimitiveLayer { + pitches: (460, 2720).into(), + }, + metals: vec![ + MetalLayer { + name: "met1".into(), + entries: vec![ + TrackSpec::gnd(480), + TrackSpec::repeat(vec![TrackEntry::gap(200), TrackEntry::sig(140)], 6), + TrackSpec::gap(200), + TrackSpec::pwr(480), + ], + dir: Dir::Horiz, + offset: (-240).into(), + cutsize: (250).into(), + overlap: (480).into(), + raw: Some(rawlayers.add(raw::Layer::from_pairs(68, &metal_purps)?)), + flip: FlipMode::EveryOther, + prim: PrimitiveMode::Split, + }, + MetalLayer { + name: "met2".into(), + entries: vec![TrackSpec::sig(140), TrackSpec::gap(320)], + dir: Dir::Vert, + cutsize: (250).into(), + offset: (-70).into(), + overlap: (0).into(), + raw: Some(rawlayers.add(raw::Layer::from_pairs(69, &metal_purps)?)), + flip: FlipMode::None, + prim: PrimitiveMode::Stack, + }, + MetalLayer { + name: "met3".into(), + entries: vec![ + TrackSpec::gnd(480), + TrackSpec::repeat(vec![TrackEntry::gap(200), TrackEntry::sig(140)], 6), + TrackSpec::gap(200), + TrackSpec::pwr(480), + ], + dir: Dir::Horiz, + offset: (-240).into(), + cutsize: (250).into(), + overlap: (480).into(), + raw: Some(rawlayers.add(raw::Layer::from_pairs(70, &metal_purps)?)), + flip: FlipMode::EveryOther, + prim: PrimitiveMode::Stack, + }, + MetalLayer { + name: "met4".into(), + entries: vec![ + TrackSpec::gnd(510), + TrackSpec::repeat(vec![TrackEntry::gap(410), TrackEntry::sig(50)], 8), + TrackSpec::gap(410), + TrackSpec::pwr(510), + ], + dir: Dir::Vert, + cutsize: (250).into(), + offset: (-255).into(), + overlap: (510).into(), + raw: Some(rawlayers.add(raw::Layer::from_pairs(71, &metal_purps)?)), + flip: FlipMode::EveryOther, + prim: PrimitiveMode::Stack, + }, + MetalLayer { + name: "met5".into(), + entries: vec![ + TrackSpec::gnd(480), + TrackSpec::repeat(vec![TrackEntry::gap(200), TrackEntry::sig(140)], 6), + TrackSpec::gap(200), + TrackSpec::pwr(480), + ], + dir: Dir::Horiz, + offset: (-240).into(), + cutsize: (250).into(), + overlap: (480).into(), + raw: Some(rawlayers.add(raw::Layer::from_pairs(72, &metal_purps)?)), + flip: FlipMode::EveryOther, + prim: PrimitiveMode::Stack, + }, + ], + vias: vec![ + ViaLayer { + name: "mcon".into(), + size: (240, 240).into(), + bot: ViaTarget::Primitive, + top: ViaTarget::Metal(0), + raw: Some(rawlayers.add(raw::Layer::from_pairs(67, &via_purps)?)), + }, + ViaLayer { + name: "via1".into(), + size: (240, 240).into(), + bot: 0.into(), + top: 1.into(), + raw: Some(rawlayers.add(raw::Layer::from_pairs(68, &via_purps)?)), + }, + ViaLayer { + name: "via2".into(), + size: (240, 240).into(), + bot: 1.into(), + top: 2.into(), + raw: Some(rawlayers.add(raw::Layer::from_pairs(69, &via_purps)?)), + }, + ViaLayer { + name: "via3".into(), + size: (240, 240).into(), + bot: 2.into(), + top: 3.into(), + raw: Some(rawlayers.add(raw::Layer::from_pairs(70, &via_purps)?)), + }, + ViaLayer { + name: "via4".into(), + size: (240, 240).into(), + bot: 3.into(), + top: 4.into(), + raw: Some(rawlayers.add(raw::Layer::from_pairs(71, &via_purps)?)), + }, + ], + rawlayers: Some(Ptr::new(rawlayers)), + }; + Ok(stack.validate()?) + } +} diff --git a/Tetris/tests/test_placer.py b/Tetris/tests/test_placer.py new file mode 100644 index 0000000..88abc38 --- /dev/null +++ b/Tetris/tests/test_placer.py @@ -0,0 +1,327 @@ +from dataclasses import dataclass +from typing import Optional + +import tetris +from tetris import Align, AlignSide +from tetris import Abstract, Port, PortKind +from tetris import Library +from tetris import Instance, Reflect +from tetris import Layout +from tetris import Outline +from tetris import Cell +from tetris import Place, Placeable, RelAssign, RelativePlace, Side, AbsPlace +from tetris import SepBy, Separation + +# Non-public imports +from tetris.placer import Placer + +# from .tests import exports, SampleStacks + + +def exports(lib: Library, stack: Optional["Stack"] = None) -> None: + ... # FIXME! + + +class SampleStacks: + @classmethod + def empty(cls) -> None: + return None + + @classmethod + def pdka(cls) -> None: + return None + + +def test_place1() -> None: + # Most basic smoke-test + Placer.place(Library("plib"), SampleStacks.empty()) + + +def test_place2() -> None: + # Initial test of relative placement + + lib = Library("test_place2") + # Create a unit cell which we'll instantiate a few times + unit = Layout("unit", 0, Outline.rect(3, 7)) + unit = Cell(name="unit", layout=unit) + unit = lib.add_cell(unit) + # Create an initial instance + i0 = Instance( + name="i0", + of=unit, + loc=AbsPlace.xy(47, 51), + reflect=Reflect.default(), + ) + # Create the parent cell which instantiates it + parent = Layout("parent", 0, Outline.rect(100, 100)) + i0 = parent.add_instance(i0) + # Create another Instance, placed relative to `i0` + i1 = Instance( + name="i1", + of=unit, + loc=RelativePlace( + to=i0, + side=Side.Right, + align=AlignSide(Side.Bottom), + sep=Separation.zero(), + ), + reflect=Reflect.default(), + ) + i1 = parent.add_instance(i1) + parent = Cell(name="parent", layout=parent) + parent = lib.add_cell(parent) + + # The real code-under-test: run placement + (lib, stack) = Placer.place(lib, SampleStacks.empty()) + + # Checks on results + assert len(lib.cells) == 2 + assert lib.cells["unit"] is unit + assert lib.cells["parent"] is parent + # Now check the locations + parent_layout = parent.layout + assert parent_layout.instances[0].loc == AbsPlace.xy(47, 51) + assert parent_layout.instances[1].loc.resolved == AbsPlace.xy(50, 51) + exports(lib, stack) + + +def test_place3() -> None: + # Test each relative side and alignment + + # Get the sample data + sample_lib = SampleLib.get() + ibig = sample_lib.ibig + big = sample_lib.big + lil = sample_lib.lil + lib = sample_lib.lib + parent = sample_lib.parent + + lib.name = "test_place3" + relto = ibig + + # Relative-placement-adder closure + def add_inst(name: str, side: Side, align: Side) -> Instance: + i = Instance( + name=name, + of=lil, + loc=RelativePlace( + to=relto, + side=side, + align=AlignSide(align), + sep=Separation.zero(), + ), + reflect=Reflect.default(), + ) + return parent.add_instance(i) + + # Add a bunch of em + i1 = add_inst("i1", Side.Left, Side.Bottom) + i2 = add_inst("i2", Side.Right, Side.Bottom) + i3 = add_inst("i3", Side.Bottom, Side.Left) + i4 = add_inst("i4", Side.Bottom, Side.Right) + i5 = add_inst("i5", Side.Left, Side.Top) + i6 = add_inst("i6", Side.Right, Side.Top) + i7 = add_inst("i7", Side.Top, Side.Left) + i8 = add_inst("i8", Side.Top, Side.Right) + + # Add `parent` to the library + parent = lib.add_cell(Cell(name="parent", layout=parent)) + + # The real code under test: run placement + (lib, stack) = Placer.place(lib, SampleStacks.pdka()) + + # And test the placed results + bigbox = ibig.boundbox() + ibox = i1.boundbox() + assert ibox.side(Side.Right) == bigbox.side(Side.Left) + assert ibox.side(Side.Bottom) == bigbox.side(Side.Bottom) + ibox = i2.boundbox() + assert ibox.side(Side.Left) == bigbox.side(Side.Right) + assert ibox.side(Side.Bottom) == bigbox.side(Side.Bottom) + ibox = i3.boundbox() + assert ibox.side(Side.Top) == bigbox.side(Side.Bottom) + assert ibox.side(Side.Left) == bigbox.side(Side.Left) + ibox = i4.boundbox() + assert ibox.side(Side.Top) == bigbox.side(Side.Bottom) + assert ibox.side(Side.Right) == bigbox.side(Side.Right) + ibox = i5.boundbox() + assert ibox.side(Side.Right) == bigbox.side(Side.Left) + assert ibox.side(Side.Top) == bigbox.side(Side.Top) + ibox = i6.boundbox() + assert ibox.side(Side.Left) == bigbox.side(Side.Right) + assert ibox.side(Side.Top) == bigbox.side(Side.Top) + ibox = i7.boundbox() + assert ibox.side(Side.Bottom) == bigbox.side(Side.Top) + assert ibox.side(Side.Left) == bigbox.side(Side.Left) + ibox = i8.boundbox() + assert ibox.side(Side.Bottom) == bigbox.side(Side.Top) + assert ibox.side(Side.Right) == bigbox.side(Side.Right) + exports(lib, stack) + + +# def test_place4() -> None : +# # Test size-of separation + +# # Get the sample data +# sample_lib = SampleLib.get() +# ibig = sample_lib.ibig +# big = sample_lib.big +# lil = sample_lib.lil +# lib = sample_lib.lib +# parent = sample_lib.parent + +# lib.name = "test_place4" + +# # Relative-placement-adder closure +# add_inst = |name: str, side, sep| { +# i = Instance( +# name: name, +# cell: lil, +# loc: RelativePlace(RelativePlace( +# to: ibig, +# side, +# align: AlignSide(side.cw_90()), # Leave out `align` as an arg, set one-turn CW of `side` +# sep, +# ), +# reflect_horiz: False, +# reflect_vert: False, +# ) +# parent.add_instance(i) +# } +# # Add a bunch of em +# sep_x = Separation.x(SepBy.SizeOf(lil)) +# i1 = add_inst("i1", Side.Left, sep_x) +# i2 = add_inst("i2", Side.Right, sep_x) +# sep_y = Separation.y(SepBy.SizeOf(lil)) +# i3 = add_inst("i3", Side.Bottom, sep_y) +# i4 = add_inst("i4", Side.Top, sep_y) +# # Add `parent` to the library +# _parent = lib.add_cell(parent) + +# # The real code under test: run placement +# (lib, stack) = Placer.place(lib, SampleStacks.pdka()) + +# # And test the placed results +# lilsize = lil.boundbox_size() +# bigbox = ibig.boundbox() +# ibox = i1.boundbox() +# assert ibox.side(Side.Top), bigbox.side(Side.Top)) +# assert ibox.side(Side.Right), bigbox.side(Side.Left) - lilsize.x) +# ibox = i2.boundbox() +# assert ibox.side(Side.Bottom), bigbox.side(Side.Bottom)) +# assert ibox.side(Side.Left), bigbox.side(Side.Right) + lilsize.x) +# ibox = i3.boundbox() +# assert ibox.side(Side.Left), bigbox.side(Side.Left)) +# assert ibox.side(Side.Top), bigbox.side(Side.Bottom) - lilsize.y) +# ibox = i4.boundbox() +# assert ibox.side(Side.Right), bigbox.side(Side.Right)) +# assert ibox.side(Side.Bottom), bigbox.side(Side.Top) + lilsize.y) + +# exports(lib, stack) + + +# def test_place5() -> None : +# # Test separation by units + +# # Get the sample data +# sample_lib = SampleLib.get() +# ibig = sample_lib.ibig +# big = sample_lib.big +# lil = sample_lib.lil +# lib = sample_lib.lib +# parent = sample_lib.parent + +# lib.name = "test_place5" + +# # Relative-placement-adder closure +# add_inst = |name: str, side, sep| { +# i = Instance { +# name: name, +# cell: lil, +# loc: RelativePlace(RelativePlace { +# to: ibig, +# side, +# align: AlignSide(side.ccw_90()), # Leave out `align` as an arg, set one-turn CCW of `side` +# sep, +# }), +# reflect_horiz: False, +# reflect_vert: False, +# } +# parent.add_instance(i) +# } +# # Add a bunch of em +# dx = PrimPitches(Dir.Horiz, 1) +# sep_x = Separation.x(SepBy.UnitSpeced(dx)) +# i1 = add_inst("i1", Side.Left, sep_x) +# i2 = add_inst("i2", Side.Right, sep_x) +# dy = PrimPitches(Dir.Vert, 5) +# sep_y = Separation.y(SepBy.UnitSpeced(dy)) +# i3 = add_inst("i3", Side.Bottom, sep_y) +# i4 = add_inst("i4", Side.Top, sep_y) +# # Add `parent` to the library +# _parent = lib.add_cell(parent) + +# # The real code under test: run placement +# (lib, stack) = Placer.place(lib, SampleStacks.pdka()) + +# # And test the placed results +# bigbox = ibig.boundbox() +# ibox = i1.boundbox() +# assert ibox.side(Side.Bottom), bigbox.side(Side.Bottom)) +# assert ibox.side(Side.Right), bigbox.side(Side.Left) - dx) +# ibox = i2.boundbox() +# assert ibox.side(Side.Top), bigbox.side(Side.Top)) +# assert ibox.side(Side.Left), bigbox.side(Side.Right) + dx) +# ibox = i3.boundbox() +# assert ibox.side(Side.Right), bigbox.side(Side.Right)) +# assert ibox.side(Side.Top), bigbox.side(Side.Bottom) - dy) +# ibox = i4.boundbox() +# assert ibox.side(Side.Left), bigbox.side(Side.Left)) +# assert ibox.side(Side.Bottom), bigbox.side(Side.Top) + dy) + +# exports(lib, stack) + + +@dataclass +class SampleLib: + # The sample library used in several tests, + # including attribute-references to key instances and cells. + + lib: Library + big: Cell + ibig: Instance + lil: Cell + parent: Layout + + @classmethod + def get(cls) -> "SampleLib": + """# Get a sample library with test cells `big`, `lil`, and `parent`. + # Designed for adding instances of `lil` relative to `big` all around `parent`.""" + + lib = Library("_rename_me_plz_") + # Create a big center cell + big = Layout("big", 1, Outline.rect(11, 12)) + big = lib.add_cell(Cell(name="big", layout=big)) + # Create the parent cell which instantiates it + parent = Layout("parent", 3, Outline.rect(40, 35)) + # Create an initial instance + ibig = Instance( + name="ibig", + of=big, + loc=AbsPlace.xy(16, 15), + reflect=Reflect.default(), + ) + ibig = parent.add_instance(ibig) + # Create a unit cell which we'll instantiate a few times around `ibig` + lil = Cell("lil") + lil.layout = Layout("lil", 1, Outline.rect(2, 1)) + lil.abs = Abstract(name="lil", metals=1, outline=Outline.rect(2, 1), ports=[]) + lil = lib.add_cell(lil) + + return SampleLib( + lib, + big, + ibig, + lil, + parent, + ) diff --git a/Tetris/tests/test_ro.py b/Tetris/tests/test_ro.py new file mode 100644 index 0000000..dfaeb74 --- /dev/null +++ b/Tetris/tests/test_ro.py @@ -0,0 +1,468 @@ +""" +# +# Tests of one of our very favorite circuits to place in this framework! +# +""" +import pytest +from typing import Optional, Callable + +# Local imports +import tetris as t + +# Test-locals +# from . import exports, resource, stacks.SampleStacks + +# Create an abs unit-cell +def abstract_unit_cell(_lib: t.Library) -> t.Cell: + return abstract_unit() + + +# Create an abs unit-cell +def abstract_unit() -> t.abstract.Abstract: + unitsize = (18, 1) + + unit = t.abstract.Abstract( + name="Wrapper", + metals=1, + outline=t.Outline.rect(unitsize[0], unitsize[1]), + ports=[ + t.abstract.Port( + name="en", + kind=t.abstract.ZTopEdge( + track=2, + side=t.abstract.Side.BottomOrLeft, + into=(5, t.RelZ.Above), + ), + ), + t.abstract.Port( + name="inp", + kind=t.abstract.ZTopEdge( + track=3, + side=t.abstract.Side.TopOrRight, + into=(11, t.RelZ.Above), + ), + ), + t.abstract.Port( + name="out", + kind=t.abstract.ZTopEdge( + track=5, + side=t.abstract.Side.TopOrRight, + into=(11, t.RelZ.Above), + ), + ), + ], + ) + return unit + + +# RO, absolute-placement edition +def ro_abs(unit: t.Cell) -> t.Layout: + unitsize = (18, 1) + + # Create an initially empty layout + ro = t.Layout( + "RO", # name + 4, # metals + t.Outline.rect(130, 7), # outline + ) + m2xpitch = 36 + m2botcut = 5 + m2topcut = 7 * m2botcut + 1 + + # For each column + for x in range(3): + m2track = m2xpitch * (x + 1) - 4 + m2entrack = (x * m2xpitch) + m2xpitch / 2 + + # For each row + for y in range(3): + loc = ((2 * x + 1) * unitsize[0], (2 * (y + 1)) * unitsize[1]) + inst = t.Instance( + name=f"inst{x}{y}", + of=unit, + loc=loc, + reflect=t.Reflect(horiz=False, vert=True), + ) + ro.add_instance(inst) + + # Assign the input + m1track = y * 12 + 9 + m3track = m1track + x + ro.net(f"dly{x}").at(1, m2track, m1track, t.RelZ.Below).at( + 2, m3track, m2track, t.RelZ.Below + ) + if x != 0: + # Cut M3 to the *right* of the input + ro.cut(2, m3track, m2track + 1, t.RelZ.Below) + else: + # Cut M3 to the *left* of the input + ro.cut(2, m3track, m2track - 1, t.RelZ.Below) + + # Assign the output + m3track = m1track + ((x + 1) % 3) + m1track = y * 12 + 11 + ro.net(f"dly{((x + 1) % 3)}").at(1, m2track + 2, m1track, t.RelZ.Below).at( + 2, m3track, m2track + 2, t.RelZ.Below + ) + if x != 2: + # Cut M3 to the *left* of the output + ro.cut(2, m3track, m2track + 1, t.RelZ.Below) + else: + # Cut M3 to the *right* of the output + ro.cut(2, m3track, m2track + 3, t.RelZ.Below) + + # Assign the enable + m1track = y * 12 + 8 + m2track = m2entrack + y + ro.net(f"en{x}{y}").at(1, m2track, m1track, t.RelZ.Below) + ro.cut(1, m2track, m1track + 1, t.RelZ.Below) # Cut just above + + # Make top & bottom M2 cuts + ro.cut(1, m2track, m2botcut, t.RelZ.Below) + ro.cut(1, m2track, m2topcut, t.RelZ.Below) + ro.cut(1, m2track + 2, m2botcut, t.RelZ.Below) + ro.cut(1, m2track + 2, m2topcut, t.RelZ.Below) + + return ro + + +def ro_rel(unit: t.Cell) -> t.Layout: + """# RO, relative-placement edition""" + + unitsize = (18, 1) + + # Create an initially empty layout + ro = t.Layout(name="RO", metals=4, outline=t.Outline.rect(130, 7)) + m2xpitch = 36 + m2botcut = 5 + m2topcut = 7 * m2botcut + 1 + + # Next-location tracker + next_loc: t.Place = t.AbsPlace(xy=t.Xy.new(unitsize[0], 2 * unitsize[1])) + + # For each column + for x in range(3): + m2track = m2xpitch * (x + 1) - 4 + m2entrack = (x * m2xpitch) + m2xpitch / 2 + bottom_inst: Optional[t.Instance] = None + + # For each row + for y in range(3): + inst = t.Instance( + name=f"inst{x}{y}", + of=unit, + loc=next_loc, + reflect=t.Reflect(horiz=False, vert=True), + ) + inst = ro.add_instance(inst) + if y == 0: + bottom_inst = inst + + # FIXME! relative net-assignment locations + # # Assign an input M2, at the center of its pin + # assn = Placeable.Assign(Ptr.new(RelAssign + # net: format!("dly", x), + # loc: RelativePlace + # to: Placeable.Port + # inst: inst, + # port: "inp", + # , + # side: Side.Right, + # align: Align.Center, + # sep: Separation.by_z(1), + # , + # )) + # ro.places.push(assn) + + # # Assign an output M2, at the right-edge of the instance + # assn = Placeable.Assign(Ptr.new(RelAssign + # net: format!("dly", ((x + 1) % 3)), + # loc: RelativePlace + # to: Placeable.Port + # inst: inst, + # port: "out", + # , + # side: Side.Right, + # align: Align.Side(Side.Right), + # sep: Separation.by_z(1), + # , + # )) + # ro.places.push(assn) + + if y == 2: + # Top of a row. Place to the right of its bottom instance. + next_loc = t.RelativePlace( + to=bottom_inst, + side=t.Side.Right, + align=t.AlignSide( + t.Side.Bottom + ), # Top or Bottom both work just as well here + sep=t.Separation.by_x(t.PrimPitches.x(unitsize[0])), + ) + + else: + # Place above the most-recent instance. + next_loc = t.RelativePlace( + to=inst, + side=t.Side.Top, + align=t.AlignSide( + t.Side.Left + ), # Left or Right both work just as well here + sep=t.Separation.by_y(t.PrimPitches.y(unitsize[1])), + ) + + # Assign the input + m1track = y * 12 + 9 + m3track = m1track + x + ro.net(f"dly{x}").at(2, m3track, m2track, t.RelZ.Below) + + if x != 0: + # Cut M3 to the *right* of the input + ro.cut(2, m3track, m2track + 1, t.RelZ.Below) + else: + # Cut M3 to the *left* of the input + ro.cut(2, m3track, m2track - 1, t.RelZ.Below) + + # Assign the output + m3track = m1track + ((x + 1) % 3) + + ro.net(f"dly{ (x + 1) % 3}").at(2, m3track, m2track + 2, t.RelZ.Below) + + if x != 2: + # Cut M3 to the *left* of the output + ro.cut(2, m3track, m2track + 1, t.RelZ.Below) + else: + # Cut M3 to the *right* of the output + ro.cut(2, m3track, m2track + 3, t.RelZ.Below) + + # Assign the enable + m1track = y * 12 + 8 + m2track = m2entrack + y + ro.net(f"en{x}{y}").at(1, m2track, m1track, t.RelZ.Below) + ro.cut(1, m2track, m1track + 1, t.RelZ.Below) # Cut just above + + # Make top & bottom M2 cuts + ro.cut(1, m2track, m2botcut, t.RelZ.Below) + ro.cut(1, m2track, m2topcut, t.RelZ.Below) + ro.cut(1, m2track + 2, m2botcut, t.RelZ.Below) + ro.cut(1, m2track + 2, m2topcut, t.RelZ.Below) + + return ro + + +def ro_array(unit: t.Cell) -> t.Layout: + """# RO, array-placement edition""" + + unitsize = (18, 1) + + # Create an initially empty layout + ro = t.Layout.new( + "RO", # name + 4, # metals + t.Outline.rect(130, 7), # outline + ) + m2xpitch = 36 + m2botcut = 5 + m2topcut = 7 * m2botcut + 1 + + # Create the main array of t.Instances + array_inst = t.Instance( + name="array_inst", + unit=t.Array( + name="col", + count=3, + sep=t.Separation.by_y(t.PrimPitches.y(2)), + unit=t.Array( + name="row", + unit=unit, + count=3, + sep=t.Separation.by_x(t.PrimPitches.x(36)), # FIXME! + ), + reflect=t.Reflect(vert=True, horiz=False), + ), + loc=(unitsize[0], 6 * unitsize[1]), + ) + ro.add_instance(array_inst) + + # # Now do all of the metal-layer stuff: assignments and cuts + # # This part remains in absolute coordinates + + # # For each column + # for x in 0..3 + # m2track = (m2xpitch * (x + 1) - 4) + # m2entrack = (x * m2xpitch) + m2xpitch / 2 + + # # For each row + # for y in 0..3 + # /* + # # Remaining TODO here: + # * Assignments, for input, output, and enable of each instance + # * Cuts + # group = Group.new("") + # instptr = group.add(t.Instance + # name: "inst", + # cell: unit, + # loc: Place.origin(), + # reflect_horiz: False, + # reflect_vert: True, + # ) + # a1 = group.add(AssignPlace + # net: format!("dly", x), + # at: RelSomething( + # inst, + # "out", # + # Separation.by_z(1), # One layer up + # Align.Center, # Align on pin-center + # ), + # ) + # AssignSomething + # net: format!("dly", x), + # at: RelSomething( + # a1, # Relative to the last assignment + # Separation.by_z(2), # Now *two* layers up + # ), + + # */ + # # Assign the input + # m1track = (y * 12 + 9) + # m3track = m1track + x + # ro.net(format!("dly", x)) + # .at(1, m2track, m1track,t.RelZ.Below) + # .at(2, m3track, m2track,t.RelZ.Below) + # if x != 0 + # # Cut M3 to the *right* of the input + # ro.cut(2, m3track, m2track + 1,t.RelZ.Below) + # else + # # Cut M3 to the *left* of the input + # ro.cut(2, m3track, m2track - 1,t.RelZ.Below) + + # # Assign the output + # m3track = m1track + ((x + 1) % 3) + # m1track = (y * 12 + 11) + # ro.net(format!("dly", ((x + 1) % 3))) + # .at(1, m2track + 2, m1track,t.RelZ.Below) + # .at(2, m3track, m2track + 2,t.RelZ.Below) + # if x != 2 + # # Cut M3 to the *left* of the output + # ro.cut(2, m3track, m2track + 1,t.RelZ.Below) + # else + # # Cut M3 to the *right* of the output + # ro.cut(2, m3track, m2track + 3,t.RelZ.Below) + + # # Assign the enable + # m1track = (y * 12 + 8) + # m2track = (m2entrack + y) + # ro.net(format!("en", x, y)) + # .at(1, m2track, m1track,t.RelZ.Below) + # ro.cut(1, m2track, m1track + 1,t.RelZ.Below) # Cut just above + + # # Make top & bottom M2 cuts + # ro.cut(1, m2track, m2botcut,t.RelZ.Below) + # ro.cut(1, m2track, m2topcut,t.RelZ.Below) + # ro.cut(1, m2track + 2, m2botcut,t.RelZ.Below) + # ro.cut(1, m2track + 2, m2topcut,t.RelZ.Below) + + return ro + + +# # Test importing and wrapping an existing GDSII into a [Library]/[t.Cell] + +# def wrap_gds() -> None: +# lib =t.Library("wrap_gds") +# _wrap_gds( lib) +# exports(lib, SampleStacks.pdka()) + +# # Most internal implementation of the `wrap_gds` test +# def _wrap_gds(lib: t.Library) ->t.LayoutResult> +# # Import a [GdsLibrary] to a [raw.Library] +# gds_fname = resource("ginv.gds") +# gds = raw.gds.gds21.GdsLibrary.load(&gds_fname) + +# stack = SampleStacks.pdka() + +# rawlib = raw.Library.from_gds(&gds, Some(Ptr.clone(&stack.rawlayers.unwrap()))) +# assert_eq!(rawlib.cells.len(), 1) +# # Get a [Ptr] to the first (and only) cell +# cell = rawlib.cells.first().unwrap() + +# # Add tracking of our dependence on the [raw.Library] +# rawlibptr = lib.add_rawlib(rawlib) +# # Create a [t.Cell] from the [raw.Library]'s sole cell +# unitsize = (18, 1) +# wrapped = RawLayoutPtr +# outline:t.Outline.rect(unitsize[0], unitsize[1]), # outline +# metals: 1, +# lib: rawlibptr, +# cell, + +# wrapped = lib.cells.insert(wrapped) + +# # Create a wrapper cell +# wrapper =t.Layout.new( +# "Wrapper", # name +# 1, # metals +# t.Outline.rect(unitsize[0], unitsize[1]), # outline +# ) +# wrapper.add_instance(t.Instance +# name: "wrapped", +# cell: wrapped, +# loc: (0, 0), +# reflect_horiz: False, +# reflect_vert: False, +# ) +# # Convert the layout to a [t.Cell] +# wrapper: t.Cell = wrapper +# # And add an [Abstract] view +# wrapper.abs = Some(abstract_unit()) +# # Finally add the wrapper [t.Cell] to our [Library], and return a pointer to it. +# wrapper = lib.cells.insert(wrapper) +# return wrapper + + +def _ro_test( + libname: str, + unitfn: Callable, # def (t.Library) -> t.Cell, + wrapfn: Callable, # def (t.Cell) -> t.Layout, +) -> None: + """# Runner for each of these RO tests. + Accepts function-arguments for the unit-cell and wrapper-cell factories.""" + + lib = t.Library(libname) + unit = unitfn(lib) # Create the unit cell + ro = wrapfn(unit) # Create the RO layout + ro_cell = t.Cell(name="RO", layout=ro) # Wrap it in a Cell + lib.add_cell(ro_cell) # And add it to the Library + exports(lib, SampleStacks.pdka()) # And export everything to our handful of formats + + +""" +# Execute a bunch of combinations, each as a separate test +""" + + +@pytest.mark.xfail +def test_ro_wrap_gds_abs() -> None: + return _ro_test("RoWrapGdsAbs", _wrap_gds, ro_abs) + + +@pytest.mark.xfail +def test_ro_wrap_gds_rel() -> None: + return _ro_test("RoWrapGdsRel", _wrap_gds, ro_rel) + + +@pytest.mark.xfail +def test_ro_wrap_gds_array() -> None: + return _ro_test("RoWrapGdsArray", _wrap_gds, ro_array) + + +def test_ro_abs_abs() -> None: + return _ro_test("RoAbsAbs", abstract_unit_cell, ro_abs) + + +def test_ro_abs_rel() -> None: + return _ro_test("RoAbsRel", abstract_unit_cell, ro_rel) + + +@pytest.mark.xfail +def test_ro_abs_array() -> None: + return _ro_test("RoAbsArray", abstract_unit_cell, ro_array) diff --git a/Tetris/tests/test_tetris.py b/Tetris/tests/test_tetris.py new file mode 100644 index 0000000..be7f98e --- /dev/null +++ b/Tetris/tests/test_tetris.py @@ -0,0 +1,10 @@ +""" +# Unit Tests +""" + +import pytest +import tetris + + +def test_version(): + assert tetris.__version__ == "1.0.0.dev0" diff --git a/Tetris/tests/test_tetris1.py b/Tetris/tests/test_tetris1.py new file mode 100644 index 0000000..13d1aa1 --- /dev/null +++ b/Tetris/tests/test_tetris1.py @@ -0,0 +1,231 @@ +# +# # Unit Tests +# + +from typing import Optional + +# Local imports +from tetris.cell import Cell +from tetris.instance import Instance +from tetris.layout import Layout +from tetris.library import Library +from tetris.outline import Outline +from tetris.relz import RelZ +from tetris.stack import * + +# from tetris import conv +# from tetris.tracks import * +# from tetris.validate import ValidStack + + +def exports(lib: Library, stack: Optional["Stack"] = None) -> None: + ... # FIXME! + + +# Create an empty cell +def test_empty_cell() -> None: + c = Layout( + name="EmptyCell", + metals=5, + outline=Outline.rect(50, 5), + instances=list(), + assignments=list(), + cuts=list(), + ) + lib = Library("EmptyCellLib") + lib.cells[c.name] = Cell.from_views(c.name, [c]) + exports(lib, SampleStacks.pdka()) + + +# Create a layout-implementation +def test_create_layout() -> None: + Layout( + name="HereGoes", + metals=4, + outline=Outline.rect(50, 5), + instances=list(), + assignments=[ + Assign( + net="clk", + at=TrackCross.from_relz(1, 0, 1, RelZ.Above), + ) + ], + cuts=list(), + ) + + +# Create a library +def test_create_lib1() -> None: + lib = Library("lib1") + layout = Layout( + name="HereGoes", + metals=3, + outline=Outline.rect(50, 5), + instances=list(), + assignments=[ + Assign( + net="clk", + at=TrackCross.from_relz(1, 4, 2, RelZ.Below), + ) + ], + cuts=[ + TrackCross.from_relz(0, 1, 1, RelZ.Above), + TrackCross.from_relz(0, 1, 3, RelZ.Above), + TrackCross.from_relz(0, 1, 5, RelZ.Above), + TrackCross.from_relz(1, 1, 1, RelZ.Below), + TrackCross.from_relz(1, 1, 3, RelZ.Below), + TrackCross.from_relz(1, 1, 5, RelZ.Below), + ], + ) + lib.cells[layout.name] = Cell.from_views(layout.name, [layout]) + exports(lib, SampleStacks.pdka()) + + +# # Create a cell with instances +# def test_create_lib2() -> None : +# lib = Library("lib2") +# c2 = Layout("IsInst", 2, Outline.rect(100, 10)) +# c2 = lib.cells.insert(c2) + +# lib.cells.insert(Layout : +# name: "HasInst", +# metals: 4, +# outline: Outline.rect(200, 20), +# instances: [Instance : +# inst_name: "inst1", +# cell: c2, +# loc: (20, 2), +# reflect_horiz: false, +# reflect_vert: false, +# ] +# , +# assignments: [Assign : +# net: "clk", +# at: TrackCross.from_relz(1, 1, 1, RelZ.Above), +# ], +# cuts: list(), +# places: list(), +# ) +# exports(lib, SampleStacks.pdka()) + + +# # Create an abstract layout, with its variety of supported port types +# def test_create_abstract() -> None : +# outline = Outline.rect(11, 11) +# ports = [ +# abs.Port : +# name: "edge_bot", +# kind: abs.PortKind.Edge : +# layer: 2, +# track: 2, +# side: abs.Side.BottomOrLeft, +# , +# , +# abs.Port : +# name: "edge_top", +# kind: abs.PortKind.Edge : +# layer: 2, +# track: 4, +# side: abs.Side.TopOrRight, +# , +# , +# abs.Port : +# name: "edge_left", +# kind: abs.PortKind.Edge : +# layer: 1, +# track: 1, +# side: abs.Side.BottomOrLeft, +# , +# , +# abs.Port : +# name: "edge_right", +# kind: abs.PortKind.Edge : +# layer: 1, +# track: 5, +# side: abs.Side.TopOrRight, +# , +# , +# ] +# abs.Abstract : +# name: "abstrack", +# outline, +# metals: 4, +# ports, + + +# # Create a cell with abstract instances +# def test_create_lib3() -> None : +# lib = Library("lib3") + +# c2 = lib.cells.insert(abs.Abstract : +# name: "IsAbs", +# metals: 1, +# outline: Outline.rect(100, 10), +# ports: list(), +# ) + +# lib.cells.insert(Layout : +# name: "HasAbss", +# metals: 4, +# outline: Outline.rect(500, 50), +# instances: [ +# Instance : +# inst_name: "inst1", +# cell: c2.clone(), +# loc: (0, 0), +# reflect_horiz: false, +# reflect_vert: false, +# , +# Instance : +# inst_name: "inst2", +# cell: c2.clone(), +# loc: (200, 20), +# reflect_horiz: false, +# reflect_vert: false, +# , +# Instance : +# inst_name: "inst4", +# cell: c2.clone(), +# loc: (400, 40), +# reflect_horiz: false, +# reflect_vert: false, +# , +# ] +# , +# assignments: list(), +# cuts: list(), +# places: list(), +# ) +# exports(lib, SampleStacks.pdka()) + +# # Helper function. Export [Library] `lib` in several formats. +# def exports(lib: Library, stack: ValidStack) -> None : +# # Serializable formats will generally be written as YAML. +# from . import utils.SerializationFormat.Yaml + +# rawlib = conv.raw.RawExporter.convert(lib, stack) +# rawlib = rawlib.read() + +# # Export to ProtoBuf, save as YAML and binary +# protolib = rawlib.to_proto() +# Yaml.save( +# protolib, +# resource(format!(":.proto.yaml", protolib.domain)), +# ) + +# raw.proto.proto.save( +# protolib, +# resource(format!(":.proto.bin", protolib.domain)), +# ) + + +# # Export to GDSII +# gds = rawlib.to_gds() +# Yaml.save(gds, resource(format!(":.gds.yaml", gds.name))) + +# gds.save(resource(format!(":.gds", gds.name))) + + +# # Grab the full path of resource-file `fname` +# def resource(rname: str) -> str : +# format!(":/resources/:", env!("CARGO_MANIFEST_DIR"), rname) diff --git a/Tetris/tetris/__init__.py b/Tetris/tetris/__init__.py new file mode 100644 index 0000000..b476a30 --- /dev/null +++ b/Tetris/tetris/__init__.py @@ -0,0 +1,29 @@ +__version__ = "1.0.0.dev0" + + +from .coords import * +from .outline import * +from .relz import * +from .stack import * +from .track_spec import * +from .abstract import * +from .layout import * +from .cell import * +from .library import * +from .bundle import * +from .bbox import * +from .instance import * +from .placement import * +from .array import * +from .group import * +from .instantiable import * +from .reflect import * +from .align import * +from .separation import * + +# from .layer_period import * +# from .tracks import * + +# from .placer import * +# from .validate import * +# from .conv import * diff --git a/Tetris/tetris/abstract.py b/Tetris/tetris/abstract.py new file mode 100644 index 0000000..9cb9a8a --- /dev/null +++ b/Tetris/tetris/abstract.py @@ -0,0 +1,113 @@ +# +# # Abstract Layout Module +# +# [Abstract] layouts describe a [Cell]'s outline and physical interface, without exposing implementation details. +# [Cell]-[Abstract]s primarily comprise their outlines and pins. +# Outlines follow the same "Tetris-Shapes" as `layout21.tetris` layout cells, including the requirements for a uniform z-axis. +# Internal layers are "fully blocked", in that parent layouts may not route through them. +# In legacy layout systems this would be akin to including blockages of the same shape as [Outline] on each layer. +# +# Sadly the english-spelled name "abstract" is reserved as a potential +# [future Rust keyword](https:#doc.rust-lang.org/reference/keywords.html#reserved-keywords), +# and is hence avoided as an identifier throughout Layout21. +# + +from enum import Enum, auto +from typing import List, Union, Optional, Tuple + +from pydantic.dataclasses import dataclass + +# Local imports +from .outline import Outline +from .relz import RelZ + +# A location (track intersection) on our top z-axis layer +@dataclass +class TopLoc: + # Track Index + track: int + # Intersecting Track Index + at: int + # Whether `at` refers to the track-indices above or below + relz: RelZ + + +# # Port Side Enumeration +# +# Note there are only two such sides: the "origin-side" [BottomOrLeft] and the "width-side" [TopOrRight]. +# Each [Layer]'s orientation ([Dir]) dictates between bottom/left and top/right. +# Also note the requirements on [Outline] shapes ensure each track has a unique left/right or top/bottom pair of edges. +# +@dataclass +class Side(Enum): + BottomOrLeft = auto() + TopOrRight = auto() + + +# Ports which connect on x/y outline edges +@dataclass +class Edge: + layer: int + track: int + side: Side + + +# Ports accessible from bot top *and* top-layer edges +# Note their `layer` field is implicitly defined as the cell's `metals`. +@dataclass +class ZTopEdge: + # Track Index + track: int + # Side + side: Side + # Location into which the pin extends inward + into: Tuple[int, RelZ] + + +# Ports which are internal to the cell outline, +# but connect from above in the z-stack. +# These can be assigned at several locations across their track, +# and are presumed to be internally-connected between such locations. +@dataclass +class ZTopInner: + # Locations + locs: List[TopLoc] + + +# Abstract-Layout Port Inner Detail +# +# All location and "geometric" information per Port is stored here, +# among a few enumerated variants. +# +# Ports may either connect on x/y edges, or on the top (in the z-axis) layer. +PortKind = Union[Edge, ZTopInner, ZTopEdge] + + +# Abstract-Layout Port +@dataclass +class Port: + # Port/ Signal Name + name: str + # Physical Info + kind: PortKind + + +# Abstract-Layout +@dataclass +class Abstract: + # Cell Name + name: str + # Outline in "Tetris-Shapes" + outline: Outline + # Number of Metal Layers Used + metals: int + # Ports + ports: List[Port] + + # Retrieve a reference to a port by name. + # Returns `None` if no port with that name exists. + def port(self, name: str) -> Optional[Port]: + for port in self.ports: + if port.name == name: + return port + return None diff --git a/Tetris/tetris/align.py b/Tetris/tetris/align.py new file mode 100644 index 0000000..17fee01 --- /dev/null +++ b/Tetris/tetris/align.py @@ -0,0 +1,27 @@ +from typing import Tuple, Union + +from pydantic.dataclasses import dataclass + +from .side import Side + + +@dataclass +class AlignSide: + # Side-to-side alignment + side: Side + + +@dataclass +class AlignCenter: + # Center-aligned + ... # No data + + +@dataclass +class AlignPorts: + # Port-to-port alignment + ports: Tuple[str, str] + + +# Alignment types +Align = Union[AlignSide, AlignCenter, AlignPorts] diff --git a/Tetris/tetris/array.py b/Tetris/tetris/array.py new file mode 100644 index 0000000..7faa5b6 --- /dev/null +++ b/Tetris/tetris/array.py @@ -0,0 +1,41 @@ +# +# # Layout Arrays +# +# Uniformly-spaced repetitions of [Arrayable] elements. +# + +from dataclasses import dataclass, field + +# Local imports +from .reflect import Reflect +from .coords import PrimPitches, Xy +from .separation import Separation + +# from .instantiable import Instantiable + + +@dataclass +class Array: + """# Array + A Uniform-Spaced Array of Identical [`Instantiable`] Elements""" + + # Array Name + name: str + + # Unit to be Arrayed + unit: "Instantiable" + + # Number of elements + count: int + + # Separation between elements + # FIXME: whether to include the size of the element or not + sep: Separation + + # Reflection state of the unit element + reflect: Reflect = field(default_factory=Reflect.default) + + # Size of the Array's rectangular `boundbox` i.e. the zero-origin `boundbox` of its `cell`. + def boundbox_size(self) -> Xy[PrimPitches]: + raise NotImplementedError # FIXME! + _unit = self.unit.boundbox_size() diff --git a/Tetris/tetris/bbox.py b/Tetris/tetris/bbox.py new file mode 100644 index 0000000..5604512 --- /dev/null +++ b/Tetris/tetris/bbox.py @@ -0,0 +1,59 @@ +# +# # Rectangular Bounding Boxes +# + +from typing import TypeVar, Generic, Tuple + +from pydantic.generics import GenericModel + +# Local imports +from .coords import Xy +from .side import Side + + +T = TypeVar("T") + + +class BoundBox(GenericModel, Generic[T]): + """# Rectangular Bounding Box""" + + mins: Xy[T] # Minimum (x,y) coordinates + maxs: Xy[T] # Maximum (x,y) coordinates + + @property + def top(self) -> T: + return self.maxs.y + + @property + def bot(self) -> T: + return self.mins.y + + @property + def bottom(self) -> T: + return self.mins.y + + @property + def left(self) -> T: + return self.mins.x + + @property + def right(self) -> T: + return self.maxs.x + + def side(self, side: Side) -> T: + """# Retrieve our coordinate at [Side] `side`.""" + if side == Side.Left: + return self.mins.x + if side == Side.Right: + return self.maxs.x + if side == Side.Bottom: + return self.mins.y + if side == Side.Top: + return self.maxs.y + raise ValueError(f"Invalid side: {side}") + + def from_xy(xs: Tuple[T, T], ys: Tuple[T, T]) -> "BoundBox": + """# Create a new [BoundBox] from potentially unordered pairs of x and y coordinates.""" + (x0, x1) = (xs[0], xs[1]) if xs[0] < xs[1] else (xs[1], xs[0]) + (y0, y1) = (ys[0], ys[1]) if ys[0] < ys[1] else (ys[1], ys[0]) + return BoundBox(Xy(x0, y0), Xy(x1, y1)) diff --git a/Tetris/tetris/bundle.py b/Tetris/tetris/bundle.py new file mode 100644 index 0000000..a5d7b56 --- /dev/null +++ b/Tetris/tetris/bundle.py @@ -0,0 +1,39 @@ +# +# # Interfaces Module +# +# Describing Cells in terms of their IO Interfaces +# + +from typing import List +from enum import Enum, auto + +from pydantic.dataclasses import dataclass + + +class PortKind(Enum): + # Flat Scalar Port, e.g. `clk` + Scalar = auto() + # Array-Based Port, e.g. `data[31:0]` + Array = auto() + # Instance of a Hierarchical Bundle + Bundle = auto() + + +# # Port +# +# Logical port, as in a netlist or HDL description. +# Includes scalar, vector (bus), and bundle-valued ports. +# Does not include physical/ geometric information. +# +@dataclass +class Port: + # Port Name + name: str + # Port Type Content + kind: PortKind + + +@dataclass +class Bundle: + name: str + ports: List[Port] diff --git a/Tetris/tetris/cell.py b/Tetris/tetris/cell.py new file mode 100644 index 0000000..679ed2a --- /dev/null +++ b/Tetris/tetris/cell.py @@ -0,0 +1,120 @@ +# +# # Cell Definition +# +# Defines the [Cell] type, which represents a multi-viewed piece of reusable hardware. +# [Cell]s can, and generally do, have one or more associated "views", +# including [Abstract]s, [Layout], interface definitions, and/or "raw" layouts. +# + + +from typing import List, Set, Dict, Type, Union, Optional +from dataclasses import dataclass + +# Local imports +from .index import Index +from .coords import PrimPitches, Xy +from .layout import Layout +from .outline import Outline +from .error import LayoutError +from .bundle import Bundle +from .abstract import Abstract + +# "Pointer" to a raw (lib, cell) combination. +# Wraps with basic [Outline] and `metals` information to enable bounded placement. +@dataclass +class RawLayoutPtr: + ... # FIXME! handle `raw` stuff + + # # Outline shape, counted in x and y pitches of `stack` + # outline: Outline + # # Number of Metal Layers Used + # metals: int + # # Pointer to the raw Library + # lib: Ptr + # # Pointer to the raw Cell + # cell: Ptr + + +# # Cell View Enumeration +# All of the ways in which a Cell is represented +CellView = Union[ + Bundle, + Abstract, + Layout, + RawLayoutPtr, +] + +# Collection of the Views describing a Cell +@dataclass +class Cell: + # Cell Name + name: str + # Interface + interface: Optional[Bundle] = None + # Layout Abstract + abs: Optional[Abstract] = None + # Layout Implementation + layout: Optional[Layout] = None + + # # Raw Layout + # # FIXME: this should probably move "up" a level + # # so that cells are either defined as `raw` or `tetris` implementations + # # but not both + # raw: Option + + # Add [CellView] `view` to our appropriate type-based field. + def add_view(self, view: CellView): + if isinstance(view, Layout): + self.layout = view + elif isinstance(view, Abstract): + self.abs = view + elif isinstance(view, Bundle): + self.interface = view + else: + raise TypeError(f"Invalid Cell View {view} of type {type(view)}") + + # Create from a list of [CellView]s and a name. + def from_views(name: str, views: List[CellView]) -> "Cell": + myself = Cell(name=name) + for view in views: + myself.add_view(view) + return myself + + # Return whichever view highest-prioritorily dictates the outline + def outline(self) -> Outline: + # We take the "most abstract" view for the outline + # (although if there are more than one, they better be the same... + # FIXME: this should be a validation step.) + # Overall this method probably should move to a "validated" cell in which each view is assured consistent. + if self.abs is not None: + return self.abs.outline + elif self.layout is not None: + return self.layout.outline + raise LayoutError( + "Failed to retrieve outline of cell : with no abstract or implementation", + ) + + # Size of the [Cell]'s rectangular `boundbox`. + def boundbox_size(self) -> Xy[PrimPitches]: + outline = self.outline() + return Xy.new(outline.xmax(), outline.ymax()) + + # Return whichever view highest-prioritorily dictates the top-layer + def metals(self) -> int: + # FIXME: same commentary as `outline` above + if self.abs is not None: + return self.abs.metals + elif self.layout is not None: + return self.layout.metals + raise LayoutError( + "Failed to retrieve metal-layers of cell : with no abstract or implementation", + ) + + # Get the cell's top metal layer (numer). + # Returns `None` if no metal layers are used. + def top_metal(self) -> Optional[Index]: + metals = self.metals() + if metals == 0: + return None + else: + return Index(metals - 1) diff --git a/Tetris/tetris/conv/layer_period.py b/Tetris/tetris/conv/layer_period.py new file mode 100644 index 0000000..2e994d3 --- /dev/null +++ b/Tetris/tetris/conv/layer_period.py @@ -0,0 +1,154 @@ +from typing import List + +from pydantic.dataclasses import dataclass + +# Local imports +from .coords import DbUnits +from .track_spec import TrackCross, TrackRef, TrackSpec, TrackType +from .tracks import Track, TrackData + + +@dataclass +class LayerPeriodData: + signals: List[TrackData] + rails: List[TrackData] + + # Convert this [Layer]'s track-info into a [LayerPeriodData] + def to_layer_period_data(self) -> "LayerPeriodData": + period = LayerPeriodData.default() + cursor = self.offset + for e in self.entries(): + d = e.width + # FIXME! + # match e.ttype : + # TrackType.Gap => (), + # TrackType.Rail(_railkind) => : + # period.rails.push(TrackData : + # ttype: e.ttype, + # index: period.rails.len(), + # dir: self.dir, + # start: cursor, + # width: d, + # ) + + # TrackType.Signal => : + # period.signals.push(TrackData : + # ttype: e.ttype, + # index: period.signals.len(), + # dir: self.dir, + # start: cursor, + # width: d, + # ) + cursor += d + + return period + + # Convert this [Layer]'s track-info into a [LayerPeriod] + def to_layer_period( + self, + index: int, + stop: DbUnits, + ) -> LayerPeriod: + stop = stop.into() + period = LayerPeriod.default() + period.index = index + cursor = self.offset + (self.pitch() * index) + entries = self.entries() + if self.flip == FlipMode.EveryOther: + iterator = reversed(entries) + else: + iterator = entries + + for e in iterator: + d = e.width + # FIXME! + # match e.ttype : + # TrackType.Gap => (), + # TrackType.Rail(railkind) => : + # period.rails.push( + # Track : + # data: TrackData : + # ttype: e.ttype, + # index: period.rails.len(), + # dir: self.dir, + # start: cursor, + # width: d, + # , + # segments: vec![TrackSegment : + # tp: TrackSegmentType.Rail(railkind), + # start: 0.into(), + # stop, + # ], + + # .validate(), + # ) + + # TrackType.Signal => : + # period.signals.push( + # Track : + # data: TrackData : + # ttype: e.ttype, + # index: period.signals.len(), + # dir: self.dir, + # start: cursor, + # width: d, + # , + # segments: vec![TrackSegment : + # tp: TrackSegmentType.Wire : src: None , + # start: 0.into(), + # stop, + # ], + + # .validate(), + # ) + cursor += d + + return period + + +# Transformed single period of [Track]s on a [Layer] +# Splits track-info between signals and rails. +# Stores each as a [Track] struct, which moves to a (start, width) size-format, +# and includes a vector of track-segments for cutting and assigning nets. +@dataclass +class LayerPeriod: + index: int + signals: List[Track] + rails: List[Track] + + # Shift the period by `dist` in its periodic direction + def offset(self, dist: DbUnits) -> None: + for t in self.rails: + t.data.start += dist + + for t in self.signals: + t.data.start += dist + + # Set the stop position for all [Track]s to `stop` + def stop(self, stop: DbUnits) -> None: + for t in self.rails: + t.stop(stop) + + for t in self.signals: + t.stop(stop) + + # Cut all [Track]s from `start` to `stop`, + def cut( + self, + start: DbUnits, + stop: DbUnits, + src: TrackCross, + ) -> None: + for t in self.rails: + t.cut(start, stop, src) + + for t in self.signals: + t.cut(start, stop, src) + + # Block all [Track]s from `start` to `stop`, + def block(self, start: DbUnits, stop: DbUnits, src: "Instance") -> None: + for t in self.rails: + t.block(start, stop, src) + + for t in self.signals: + t.block(start, stop, src) diff --git a/Tetris/tetris/conv/mod.rs b/Tetris/tetris/conv/mod.rs new file mode 100644 index 0000000..23ba02c --- /dev/null +++ b/Tetris/tetris/conv/mod.rs @@ -0,0 +1,6 @@ +//! +//! Conversion Modules +//! + +pub mod proto; +pub mod raw; diff --git a/Tetris/tetris/conv/proto.rs b/Tetris/tetris/conv/proto.rs new file mode 100644 index 0000000..23c03b2 --- /dev/null +++ b/Tetris/tetris/conv/proto.rs @@ -0,0 +1,605 @@ +//! +//! # ProtoBuf Import & Export +//! + +// Std-Lib +use std::collections::HashMap; +use std::convert::{TryFrom, TryInto}; + +// Local imports +use crate::{ + // FIXME: some of these should come from `validate` + abs::{Abstract, Port}, + cell::Cell, + coords::{HasUnits, PrimPitches, Xy}, + instance::Instance, + layout::Layout, + library::Library, + outline::Outline, + placement::Place, + raw::{Dir, LayoutError, LayoutResult}, + stack::{Assign, RelZ}, + tracks::{TrackCross, TrackRef}, + utils::{DepOrder, DepOrderer, ErrorContext, ErrorHelper, Ptr}, +}; +// Proto-crate imports and aliases +use layout21protos as proto; +use proto::raw as rawproto; +use proto::tetris as tproto; + +/// # ProtoBuf Exporter +#[derive(Debug)] +pub struct ProtoExporter<'lib> { + lib: &'lib Library, // Source [Library] + ctx: Vec, // Error Stack +} +impl<'lib> ProtoExporter<'lib> { + pub fn export(lib: &'lib Library) -> LayoutResult { + Self { + lib, + ctx: Vec::new(), + } + .export_lib() + } + /// Internal implementation method. Convert all, starting from our top-level [Library]. + fn export_lib(&mut self) -> LayoutResult { + self.ctx.push(ErrorContext::Library(self.lib.name.clone())); + // Create a new [tproto::Library] + let mut plib = tproto::Library::default(); + + // Set its library name + plib.domain = self.lib.name.clone(); + // And convert each of our cells + for cell in CellOrder::order(&self.lib.cells)?.iter() { + let pcell = self.export_cell(&*cell.read()?)?; + plib.cells.push(pcell); + } + self.ctx.pop(); + Ok(plib) + } + /// Convert a [Cell] to a [tproto::Cell] cell-definition + fn export_cell(&mut self, cell: &Cell) -> LayoutResult { + self.ctx.push(ErrorContext::Cell(cell.name.clone())); + let mut pcell = tproto::Cell::default(); + pcell.name = cell.name.clone(); + + if let Some(ref lay) = cell.layout { + pcell.layout = Some(self.export_layout(lay)?); + } + if let Some(ref a) = cell.abs { + pcell.r#abstract = Some(self.export_abstract(a)?); + } + self.ctx.pop(); + Ok(pcell) + } + /// Convert a [Abstract] + fn export_abstract(&mut self, abs: &Abstract) -> LayoutResult { + self.ctx.push(ErrorContext::Abstract); + // Create the new [tproto::Abstract] + let mut pabs = tproto::Abstract::default(); + // Convert our name + pabs.name = abs.name.clone(); + // Convert its ports + for port in abs.ports.iter() { + let pport = self.export_abstract_port(&port, abs.metals)?; + pabs.ports.push(pport); + } + // Convert its outline + pabs.outline = Some(self.export_outline(&abs.outline, abs.metals)?); + // And we're done - pop the context and return + self.ctx.pop(); + Ok(pabs) + } + /// Export an [Outline] + fn export_outline( + &mut self, + outline: &Outline, + metals: usize, + ) -> LayoutResult { + let x = self.export_dimensions(outline.x.as_slice())?; + let y = self.export_dimensions(outline.y.as_slice())?; + let metals = i64::try_from(metals)?; + Ok(tproto::Outline { x, y, metals }) + } + /// Export an abstract's [Port] + fn export_abstract_port( + &mut self, + port: &Port, + metals: usize, + ) -> LayoutResult { + use crate::abs::{PortKind, Side}; + let mut pport = tproto::AbstractPort::default(); + pport.net = port.name.clone(); + + use tproto::abstract_port::{EdgePort, Kind, PortSide, ZTopEdgePort, ZTopInner}; + let kind = match &port.kind { + PortKind::Edge { layer, track, side } => { + let track = Some(tproto::TrackRef { + layer: i64::try_from(*layer)?, + track: i64::try_from(*track)?, + }); + let side = match side { + Side::BottomOrLeft => PortSide::BottomOrLeft, + Side::TopOrRight => PortSide::TopOrRight, + }; + let side = i32::from(side); + Kind::Edge(EdgePort { track, side }) + } + PortKind::ZTopEdge { track, side, into } => { + let track = i64::try_from(*track)?; + let into = { + let layer = match into.1 { + RelZ::Above => metals + 1, + RelZ::Below => metals - 1, + }; + Some(tproto::TrackRef { + layer: i64::try_from(layer)?, + track: i64::try_from(into.0)?, + }) + }; + let side = match side { + Side::BottomOrLeft => PortSide::BottomOrLeft, + Side::TopOrRight => PortSide::TopOrRight, + }; + let side = i32::from(side); + + Kind::ZtopEdge(ZTopEdgePort { track, side, into }) + } + PortKind::ZTopInner { locs: _ } => todo!(), + }; + pport.kind = Some(kind); + Ok(pport) + } + /// Export a [Layout] to a [tproto::Layout] cell-implementation + fn export_layout(&mut self, layout: &Layout) -> LayoutResult { + self.ctx.push(ErrorContext::Impl); + // Create the empty/default [tproto::Layout] + let mut playout = tproto::Layout::default(); + // Convert our name + playout.name = layout.name.clone(); + playout.outline = Some(self.export_outline(&layout.outline, layout.metals)?); + // Convert each [Instance] + for ptr in layout.instances.iter() { + let inst = ptr.read()?; + playout.instances.push(self.export_instance(&*inst)?); + } + // Convert each [Assign] + for assn in &layout.assignments { + playout.assignments.push(self.export_assignment(assn)?); + } + // Convert each [Cut] + for cut in &layout.cuts { + playout.cuts.push(self.export_track_cross(cut)?); + } + self.ctx.pop(); + Ok(playout) + } + /// Convert an [Instance] to a [tproto::Instance] + fn export_instance(&mut self, inst: &Instance) -> LayoutResult { + let cell = inst.cell.read()?; + let abs_loc = inst.loc.abs()?; // FIXME: asserts the instance has an absolute location, for now + let loc = self.export_point(&abs_loc)?; + + // This is where the auto-generated proto-`oneof`s get fun. + // proto::tetris::Place <= this is a struct + // proto::tetris::place <= this is a `mod`, lower-case named after that struct + // proto::tetris::place::Place <= this is an enum, upper-case named after that `mod` + // Got it? + let loc = Some(proto::tetris::Place { + place: Some(proto::tetris::place::Place::Abs(loc)), + }); + + Ok(tproto::Instance { + name: inst.inst_name.clone(), + cell: Some(proto::utils::Reference { + to: Some(proto::utils::reference::To::Local(cell.name.clone())), + }), + reflect_vert: inst.reflect_vert, + reflect_horiz: inst.reflect_horiz, + loc, + }) + } + /// Export an [Assign] to a [tproto::Assign] + fn export_assignment(&mut self, assn: &Assign) -> LayoutResult { + let mut passn = tproto::Assign::default(); + passn.net = assn.net.clone(); + passn.at = Some(self.export_track_cross(&assn.at)?); + Ok(passn) + } + /// Export a [TrackCross] + fn export_track_cross(&mut self, cross: &TrackCross) -> LayoutResult { + let track = Some(self.export_track_ref(&cross.track)?); + let cross = Some(self.export_track_ref(&cross.cross)?); + let pcross = tproto::TrackCross { track, cross }; + Ok(pcross) + } + /// Export a [TrackRef] + fn export_track_ref(&mut self, track: &TrackRef) -> LayoutResult { + let layer = i64::try_from(track.layer)?; + let track = i64::try_from(track.track)?; + Ok(tproto::TrackRef { layer, track }) + } + /// Export a list of [HasUnit] dimensioned distance-values to + fn export_dimensions(&mut self, p: &[T]) -> LayoutResult> { + let mut rv = Vec::with_capacity(p.len()); + for val in p { + rv.push(self.export_dimension(val)?); + } + Ok(rv) + } + /// Export a [HasUnit] dimensioned distance-value to `i64` + fn export_dimension(&mut self, p: &T) -> LayoutResult { + // Convert to no-unit integers, and then to i64 + Ok(i64::try_from(p.raw())?) + } + /// Export a [Point] + fn export_point(&mut self, p: &Xy) -> LayoutResult { + let p = p.raw(); // Convert to no-unit integers + let x = i64::try_from(p.x)?; // Convert into i64 + let y = i64::try_from(p.y)?; + Ok(rawproto::Point::new(x, y)) // And send back a proto-Point + } +} // impl ProtoExporter +impl ErrorHelper for ProtoExporter<'_> { + type Error = LayoutError; + fn err(&self, msg: impl Into) -> LayoutError { + LayoutError::Export { + message: msg.into(), + stack: self.ctx.clone(), + } + } +} + +/// Empty struct for implementing the [DepOrder] trait for library [Cell]s +struct CellOrder; +impl DepOrder for CellOrder { + type Item = Ptr; + type Error = LayoutError; + + /// Process [Cell]-pointer `item` + /// Follow its `instances` list, pushing their `cell` to the stack + fn process(item: &Ptr, orderer: &mut DepOrderer) -> LayoutResult<()> { + let cell = item.read()?; + if let Some(layout) = &cell.layout { + // Push all instances first + for ptr in layout.instances.iter() { + let inst = ptr.read()?; + orderer.push(&inst.cell)?; + } + } + Ok(()) + } + fn fail() -> Result<(), Self::Error> { + LayoutError::fail("Cell ordering error") + } +} + +/// # ProtoBuf Library Importer +#[derive(Debug, Default)] +pub struct ProtoLibImporter { + ctx: Vec, // Error Stack + cell_map: HashMap>, // Proto cell-name => [Cell] +} +impl ProtoLibImporter { + pub fn import(plib: &tproto::Library) -> LayoutResult { + // Run the main import-implementation method + Self::default().import_lib(&plib) + } + /// Internal implementation method. Convert the top-level library. + fn import_lib(&mut self, plib: &tproto::Library) -> LayoutResult { + let name = plib.domain.clone(); + self.ctx.push(ErrorContext::Library(name.clone())); + // Give our library its name + let mut lib = Library::new(name); + // And convert each of its `cells` + for cell in &plib.cells { + let name = cell.name.clone(); + let cell = self.import_cell(cell)?; + let cellkey = lib.cells.insert(cell); + self.cell_map.insert(name, cellkey); + } + Ok(lib) + } + /// Import a [Cell] + fn import_cell(&mut self, pcell: &tproto::Cell) -> LayoutResult { + self.ctx.push(ErrorContext::Cell(pcell.name.clone())); + let mut cell = Cell::new(&pcell.name); + if let Some(ref lay) = pcell.layout { + cell.layout = Some(self.import_layout(lay)?); + } + if let Some(ref a) = pcell.r#abstract { + cell.abs = Some(self.import_abstract(a)?); + } + self.ctx.pop(); + Ok(cell) + } + /// Import an [Abstract] + fn import_abstract(&mut self, pabs: &tproto::Abstract) -> LayoutResult { + self.ctx.push(ErrorContext::Abstract); + + // Convert the abstract's [Outline] + let poutline = self.unwrap(pabs.outline.as_ref(), "Invalid Abstract with no Outline")?; + let (outline, metals) = self.import_outline(poutline)?; + // Create our in-memory [Abstract] + let mut abs = Abstract::new(&pabs.name, metals, outline); + // And convert each of its [Port]s + for pport in &pabs.ports { + abs.ports.push(self.import_abstract_port(pport)?); + } + + self.ctx.pop(); + Ok(abs) + } + /// Import an Abstract [Port] + fn import_abstract_port(&mut self, _pport: &tproto::AbstractPort) -> LayoutResult { + todo!() // FIXME! + } + /// Import an [Outline] + fn import_outline(&mut self, poutline: &tproto::Outline) -> LayoutResult<(Outline, usize)> { + let x = self.import_prim_pitches_list(poutline.x.as_slice(), Dir::Horiz)?; + let y = self.import_prim_pitches_list(poutline.y.as_slice(), Dir::Vert)?; + let metals = usize::try_from(poutline.metals)?; + Ok((Outline::from_prim_pitches(x, y)?, metals)) + } + /// Import an [Assign] + fn import_assignment(&mut self, passn: &tproto::Assign) -> LayoutResult { + let at = self.unwrap(passn.at.as_ref(), "Invalid proto Assign with no location ")?; + let at = self.import_track_cross(at)?; + let assn = Assign::new(passn.net.clone(), at); + Ok(assn) + } + /// Import a [TrackCross] + fn import_track_cross(&mut self, pcross: &tproto::TrackCross) -> LayoutResult { + // Create a [TrackCross], always using `pcross.top` as the primary track, and `pcross.bot` as its intersection. + // First unwrap the not-really-optional `track` and `cross` fields + let track = self.unwrap(pcross.track.as_ref(), "Invalid TrackCross missing `top`")?; + let cross = self.unwrap(pcross.cross.as_ref(), "Invalid TrackCross missing `top`")?; + let track = self.import_track_ref(track)?; + let cross = self.import_track_ref(cross)?; + // And create the intersection + let cross = TrackCross::new(track, cross); + Ok(cross) + } + /// Import a [TrackRef] + fn import_track_ref(&mut self, pref: &tproto::TrackRef) -> LayoutResult { + let layer = usize::try_from(pref.layer)?; + let track = usize::try_from(pref.track)?; + Ok(TrackRef { layer, track }) + } + /// Import a [Layout] + fn import_layout(&mut self, playout: &tproto::Layout) -> LayoutResult { + self.ctx.push(ErrorContext::Impl); + let name = playout.name.clone(); + + let poutline = self.unwrap( + playout.outline.as_ref(), + format!("Invalid tproto::Instance with no Outline: {}", playout.name), + )?; + + let (outline, metals) = self.import_outline(poutline)?; + let mut layout = Layout::new(name, metals, outline); + + for inst in &playout.instances { + layout.instances.push(self.import_instance(inst)?); + } + for s in &playout.assignments { + layout.assignments.push(self.import_assignment(s)?); + } + for txt in &playout.cuts { + layout.cuts.push(self.import_track_cross(txt)?); + } + self.ctx.pop(); + Ok(layout) + } + /// Import an [Instance] + fn import_instance(&mut self, pinst: &tproto::Instance) -> LayoutResult> { + let inst_name = pinst.name.clone(); + self.ctx.push(ErrorContext::Instance(inst_name.clone())); + + // Look up the cell-pointer, which must be imported by now, or we fail + let cell = self.import_reference(&pinst)?; + + // Mostly wind through protobuf-generated structures' layers of [Option]s + let loc = self.unwrap( + pinst.loc.as_ref(), + format!("Invalid tproto::Instance with no Location: {}", pinst.name), + )?; + let loc = self.unwrap( + loc.place.as_ref(), + format!("Invalid tproto::Instance with no Location: {}", pinst.name), + )?; + use tproto::place::Place::{Abs, Rel}; + let loc = match loc { + Abs(ref p) => self.import_xy_prim_pitches(p)?, + Rel(_) => self.fail("Proto-imports of relative placements are not (yet) supported")?, + }; + let loc = Place::Abs(loc); + + let inst = Instance { + inst_name, + cell, + loc, + reflect_horiz: pinst.reflect_horiz, + reflect_vert: pinst.reflect_vert, + }; + let inst = Ptr::new(inst); + self.ctx.pop(); + Ok(inst) + } + /// Import a proto-defined pointer, AKA [tproto::Reference] + fn import_reference(&mut self, pinst: &tproto::Instance) -> LayoutResult> { + // Mostly wind through protobuf-generated structures' layers of [Option]s + let pref = self.unwrap( + pinst.cell.as_ref(), + format!("Invalid tproto::Instance with null Cell: {}", pinst.name), + )?; + let pref_to = self.unwrap( + pref.to.as_ref(), + format!("Invalid tproto::Instance with null Cell: {}", pinst.name), + )?; + use proto::utils::reference::To::{External, Local}; + let cellname: &str = match pref_to { + Local(ref name) => Ok(name), + External(_) => self.fail("Import of external proto-references not supported"), + }?; + // Now look that up in our hashmap + let cellptr = self.unwrap( + self.cell_map.get(cellname), + format!("Instance tproto::Instance of undefined cell {}", cellname), + )?; + Ok(cellptr.clone()) + } + /// Import a [tproto::Point] designed to be interpreted as [PrimPitches] + fn import_xy_prim_pitches(&mut self, pt: &rawproto::Point) -> LayoutResult> { + let x = PrimPitches::x(pt.x.try_into()?); + let y = PrimPitches::y(pt.y.try_into()?); + Ok(Xy::new(x, y)) + } + /// Import a list of primitive-pitch dimensions + fn import_prim_pitches_list( + &mut self, + pts: &[i64], + dir: Dir, + ) -> LayoutResult> { + let mut rv = Vec::with_capacity(pts.len()); + for pt in pts { + rv.push(self.import_prim_pitches(*pt, dir)?); + } + Ok(rv) + } + /// Import a [tproto::Point] designed to be interpreted as [PrimPitches] + fn import_prim_pitches(&mut self, pt: i64, dir: Dir) -> LayoutResult { + Ok(PrimPitches::new(dir, pt.try_into()?)) + } +} // impl ProtoLibImporter + +impl ErrorHelper for ProtoLibImporter { + type Error = LayoutError; + fn err(&self, msg: impl Into) -> LayoutError { + LayoutError::Import { + message: msg.into(), + stack: self.ctx.clone(), + } + } +} +#[cfg(test)] +mod tests { + use super::*; + use crate::utils::SerializationFormat::Yaml; + + #[test] + fn proto_roundtrip1() -> LayoutResult<()> { + // Proto-export + let lib = Library::new("proto_rt1"); + let plib = ProtoExporter::export(&lib)?; + assert_eq!(plib.domain, "proto_rt1"); + assert_eq!(plib.cells, Vec::new()); + assert_eq!(plib.author, None); + + // Import it back + let lib2 = ProtoLibImporter::import(&plib)?; + assert_eq!(lib2.name, "proto_rt1"); + assert_eq!(lib2.cells.len(), 0); + assert_eq!(lib2.rawlibs.len(), 0); + + Ok(()) + } + + #[test] + fn proto_roundtrip2() -> LayoutResult<()> { + // Proto round-trip, round 2, with some content + let mut lib = Library::new("proto_rt2"); + let mut cell = Cell::new("proto_rt2_cell"); + cell.layout = Some(Layout::new("proto_rt2_cell", 0, Outline::rect(1, 1)?)); + cell.abs = Some(Abstract::new("proto_rt2_cell", 0, Outline::rect(1, 1)?)); + lib.cells.add(cell); + + let plib = ProtoExporter::export(&lib)?; + assert_eq!(plib.domain, "proto_rt2"); + assert_eq!(plib.author, None); + assert_eq!(plib.cells.len(), 1); + let pcell = &plib.cells[0]; + let playout = pcell.layout.as_ref().unwrap(); + assert_eq!(playout.name, "proto_rt2_cell"); + let playout_outline = playout.outline.as_ref().unwrap(); + assert_eq!(playout_outline.x, vec![1]); + assert_eq!(playout_outline.y, vec![1]); + let pabs = pcell.r#abstract.as_ref().unwrap(); + assert_eq!(pabs.name, "proto_rt2_cell"); + assert_eq!(pabs.ports.len(), 0); + let pabs_outline = pabs.outline.as_ref().unwrap(); + assert_eq!(pabs_outline.x, vec![1]); + assert_eq!(pabs_outline.y, vec![1]); + assert_eq!(pabs_outline, playout_outline); + + // Import it back + let lib2 = ProtoLibImporter::import(&plib)?; + assert_eq!(lib2.name, "proto_rt2"); + assert_eq!(lib2.rawlibs.len(), 0); + assert_eq!(lib2.cells.len(), 1); + let cell2 = &lib2.cells[0].read()?; + let layout2 = cell2.layout.as_ref().unwrap(); + assert_eq!(layout2.name, "proto_rt2_cell"); + assert_eq!(layout2.outline, Outline::rect(1, 1)?); + assert_eq!(layout2.metals, 0); + assert_eq!(layout2.instances.len(), 0); + assert_eq!(layout2.assignments.len(), 0); + assert_eq!(layout2.cuts.len(), 0); + let abs2 = cell2.abs.as_ref().unwrap(); + assert_eq!(abs2.name, "proto_rt2_cell"); + assert_eq!(abs2.ports.len(), 0); + + // Yaml.save(&plib, "proto_rt2.yaml")?; + Ok(()) + } + + #[test] + fn proto_yaml1() -> LayoutResult<()> { + // Proto export, then YAML export + let lib = Library::new("proto_yaml1"); + let plib = ProtoExporter::export(&lib)?; + assert_eq!(plib.domain, "proto_yaml1"); + assert_eq!(plib.cells, Vec::new()); + assert_eq!(plib.author, None); + + // Yaml.save(&plib, "proto_yaml1.yaml")?; + Ok(()) + } + #[test] + fn proto_yaml2() -> LayoutResult<()> { + // Import from YAML + let yaml = r#" +--- +domain: proto_rt2 +cells: + - name: proto_rt2_cell + abstract: + name: proto_rt2_cell + outline: + x: [ 1 ] + y: [ 1 ] + metals: 0 + ports: [] + layout: + name: proto_rt2_cell + outline: + x: [ 1 ] + y: [ 1 ] + metals: 0 + instances: [] + assignments: [] + cuts: [] +"#; + let plib: tproto::Library = Yaml.from_str(yaml)?; + let lib = ProtoLibImporter::import(&plib)?; + + // let lib = Library::new("proto_yaml1"); + // let plib = ProtoExporter::export(&lib)?; + // assert_eq!(plib.domain, "proto_yaml1"); + // assert_eq!(plib.cells, Vec::new()); + // assert_eq!(plib.author, None); + + // Yaml.save(&plib, "proto_yaml2.yaml")?; + Ok(()) + } +} diff --git a/Tetris/tetris/conv/raw.rs b/Tetris/tetris/conv/raw.rs new file mode 100644 index 0000000..bfeb49b --- /dev/null +++ b/Tetris/tetris/conv/raw.rs @@ -0,0 +1,851 @@ +//! +//! # Raw-Layout Conversion Module +//! + +// Std-lib +use std::collections::HashMap; +use std::convert::TryFrom; +use std::fmt::Debug; + +// Crates.io +use slotmap::{new_key_type, SlotMap}; + +// Local imports +use crate::{ + abs, cell, + coords::{DbUnits, HasUnits, PrimPitches, UnitSpeced, Xy}, + instance::Instance, + layout::Layout, + library::Library, + outline::Outline, + raw::{self, Dir, LayoutError, LayoutResult, Point}, + stack::{LayerPeriod, RelZ}, + tracks::{Track, TrackCross, TrackSegmentType}, + utils::{ErrorContext, ErrorHelper, Ptr, PtrList}, + validate, +}; + +// Create key-types for each internal type stored in [SlotMap]s +new_key_type! { + /// Keys for [ValidAssign] entries + pub struct AssignKey; +} +/// A short-lived Cell, largely organized by layer +#[derive(Debug, Clone)] +struct TempCell<'lib> { + /// Reference to the source [Cell] + cell: &'lib Layout, + /// Reference to the source [Library] + lib: &'lib Library, + /// Instances and references to their definitions + instances: PtrList, + /// Cuts, arranged by Layer + cuts: Vec>, + /// Validated Assignments + assignments: SlotMap, + /// Assignments, arranged by Layer + top_assns: Vec>, + /// Assignments, arranged by Layer + bot_assns: Vec>, +} +/// Temporary arrangement of data for a [Layer] within a [Cell] +#[derive(Debug, Clone)] +struct TempCellLayer<'lib> { + /// Reference to the validated metal layer + layer: &'lib validate::ValidMetalLayer, + /// Reference to the parent cell + cell: &'lib TempCell<'lib>, + /// Instances which intersect with this layer and period + instances: PtrList, + /// Pitch per layer-period + pitch: DbUnits, + /// Number of layer-periods + nperiods: usize, + /// Spanning distance in the layer's "infinite" dimension + span: DbUnits, +} + +/// Short-Lived structure of the stuff relevant for converting a single LayerPeriod, +/// on a single Layer, in a single Cell. +#[derive(Debug, Clone)] +struct TempPeriod<'lib> { + periodnum: usize, + cell: &'lib TempCell<'lib>, + layer: &'lib TempCellLayer<'lib>, + /// Instance Blockages + blockages: Vec<(PrimPitches, PrimPitches, Ptr)>, + cuts: Vec<&'lib TrackCross>, + top_assns: Vec, + bot_assns: Vec, +} +/// # Converter from [Library] and constituent elements to [raw::Library] +#[derive(Debug)] +pub struct RawExporter { + /// Source [Library] + lib: Library, + /// Source (validated) [Stack] + stack: validate::ValidStack, + /// HashMap from source [Cell] to exported [raw::Cell], + /// largely for lookup during conversion of [Instance]s + rawcells: HashMap, Ptr>, + /// Context stack, largely for error reporting + ctx: Vec, +} +impl<'lib> RawExporter { + /// Convert the combination of a [Library] `lib` and [Stack] `stack` to a [raw::Library]. + /// Both `lib` and `stack` are consumed in the process. + pub fn convert(lib: Library, stack: validate::ValidStack) -> LayoutResult> { + // Put the combination through absolute-placement + use crate::placer::Placer; + let (lib, stack) = Placer::place(lib, stack)?; + + // Run the [Library] through validation + validate::LibValidator::new(&stack).validate_lib(&lib)?; + + let mut myself = Self { + lib, + stack, + rawcells: HashMap::new(), + ctx: Vec::new(), + }; + myself.export_stack()?; + myself.export_lib() + } + /// "Convert" our [Stack]. Really just checks a few properties are valid. + fn export_stack(&mut self) -> LayoutResult<()> { + // Require our [Stack] specify both: + // (a) set of [raw::Layers], and + // (b) a boundary layer + // Both these fields are frequently `unwrap`ed hereafter. + if !self.stack.rawlayers.is_some() { + return self.fail("Raw export failed: no [raw::Layers] specified"); + } + if !self.stack.boundary_layer.is_some() { + return self.fail("Raw export failed: no `boundary_layer` specified"); + } + Ok(()) + } + /// Convert everything in our [Library] + fn export_lib(&mut self) -> LayoutResult> { + self.ctx.push(ErrorContext::Library(self.lib.name.clone())); + // Get our starter raw-lib, either anew or from any we've imported + let rawlibptr = if self.lib.rawlibs.len() == 0 { + // Create a new [raw::Library] + let mut rawlib = raw::Library::new( + &self.lib.name, + self.stack.units, + Some(Ptr::clone(self.stack.rawlayers.as_ref().unwrap())), + ); + Ok(Ptr::new(rawlib)) + } else if self.lib.rawlibs.len() == 1 { + // Pop the sole raw-library, and use it as a starting point + let rawlibptr = self.lib.rawlibs.pop().unwrap(); + let mut rawlib = rawlibptr.write()?; + rawlib.name = self.lib.name.to_string(); + if rawlib.units != self.stack.units { + // Check that the units match, or fail + return self.fail(format!( + "NotImplemented: varying units between raw and tetris libraries: {:?} vs {:?}", + rawlib.units, self.stack.units, + )); + } + drop(rawlib); + Ok(rawlibptr) + } else { + // Multiple raw-libraries will require some merging + // Probably not difficult, but not done yet either. + self.fail("NotImplemented: with multiple [raw::Library") + }?; + { + // Get write-access to the raw-lib + let mut rawlib = rawlibptr.write()?; + // Convert each defined [Cell] to a [raw::Cell] + for srcptr in self.lib.dep_order() { + let rawptr = self.export_cell(&*srcptr.read()?, &mut rawlib.cells)?; + self.rawcells.insert(srcptr.clone(), rawptr); + } + } // Ends `rawlib` write-access scope + self.ctx.pop(); + Ok(rawlibptr) + } + /// Convert a [Cell] to a [raw::Cell] and add to `rawcells`. + /// FIXME: In reality only one of the cell-views is converted, + /// generally the "most specific" available view. + fn export_cell( + &mut self, + cell: &cell::Cell, + rawcells: &mut PtrList, + ) -> LayoutResult> { + if let Some(ref x) = cell.raw { + // Raw definitions store the cell-pointer + // Just return a copy of it and *don't* add it to `rawcells` + // First check for validity, i.e. lack of alternate definitions + if cell.abs.is_some() || cell.layout.is_some() { + // FIXME: move this to validation stages + return self.fail(format!( + "Cell {} has an invalid combination of raw-definition and tetris-definition", + cell.name, + )); + } + return Ok(x.cell.clone()); + } + if cell.abs.is_none() && cell.layout.is_none() { + // FIXME: move this to validation stages + return self.fail(format!( + "Cell {} has no abstract nor implementation", + cell.name, + )); + } + + // Create the raw-cell + let mut rawcell = raw::Cell::new(&cell.name.to_string()); + // And create each defined view + if let Some(ref x) = cell.layout { + rawcell.layout = Some(self.export_layout_impl(x)?); + } + if let Some(ref x) = cell.abs { + rawcell.abs = Some(self.export_abstract(x)?); + } + // Add it to `rawcells`, and return the pointer that comes back + Ok(rawcells.add(rawcell)) + } + /// Convert to a raw layout cell + fn export_layout_impl(&self, layout: &Layout) -> LayoutResult { + if layout.outline.x.len() > 1 { + return Err(LayoutError::Str( + "Non-rectangular outline; conversions not supported (yet)".into(), + )); + }; + let mut elems: Vec = Vec::new(); + // Re-organize the cell into the format most helpful here + let temp_cell = self.temp_cell(layout)?; + // Convert a layer at a time, starting from bottom + for layernum in 0..layout.metals { + // Organize the cell/layer combo into temporary conversion format + let temp_layer = self.temp_cell_layer(&temp_cell, self.stack.metal(layernum)?)?; + // Convert each "layer period" one at a time + for periodnum in 0..temp_layer.nperiods { + // Again, re-organize into the relevant objects for this "layer period" + let temp_period = self.temp_cell_layer_period(&temp_layer, periodnum)?; + // And finally start doing stuff! + elems.extend(self.export_cell_layer_period(&temp_period)?); + } + } + // Convert our [Outline] into a polygon + elems.push(self.export_outline(&layout.outline)?); + // Convert our [Instance]s + let insts = layout + .instances + .iter() + .map(|ptr| { + let inst = ptr.read()?; + self.export_instance(&*inst) + }) + .collect::, _>>()?; + // Aaaand create our new [raw::Cell] + Ok(raw::Layout { + name: layout.name.clone(), + insts, + elems, + ..Default::default() + }) + } + /// Convert an [Instance] to a [raw::Instance] + fn export_instance(&self, inst: &Instance) -> LayoutResult { + // Get the raw-cell pointer from our mapping. + // Note this requires dependent cells be converted first, depth-wise. + let rawkey = self.unwrap( + self.rawcells.get(&inst.cell), + format!("Internal Error Exporting Instance {}", inst.inst_name), + )?; + // Convert its orientation + let (reflect_vert, angle) = match (inst.reflect_vert, inst.reflect_horiz) { + (false, false) => (false, None), // Default orientation + (true, false) => (true, None), // Flip vertically + (false, true) => (true, Some(180.)), // Flip horiz via vert-flip and rotation + (true, true) => (false, Some(180.)), // Flip both by rotation + }; + // Primarily scale the location of each instance by our pitches + Ok(raw::Instance { + inst_name: inst.inst_name.clone(), + cell: rawkey.clone(), + loc: self.export_xy(inst.loc.abs()?).into(), + reflect_vert, + angle, + }) + } + /// Create a [TempCell], organizing [Cell] data in more-convenient fashion for conversion + fn temp_cell<'a>(&'a self, layout: &'a Layout) -> LayoutResult> { + // Collect references to its instances + let instances = layout.instances.clone(); + // Validate `cuts`, and arrange them by layer + let mut cuts: Vec> = vec![vec![]; layout.metals]; + for cut in layout.cuts.iter() { + validate::LibValidator::new(&self.stack).validate_track_cross(cut)?; + cuts[cut.track.layer].push(&cut); + // FIXME: cell validation should also check that this lies within our outline. probably do this earlier + } + + // Validate all the cell's assignments, and arrange references by layer + let mut bot_assns = vec![vec![]; layout.metals]; + let mut top_assns = vec![vec![]; layout.metals]; + let mut assignments = SlotMap::with_key(); + for assn in layout.assignments.iter() { + // Validate the assignment + let v = validate::LibValidator::new(&self.stack).validate_assign(assn)?; + let bot = v.bot.layer; + let top = v.top.layer; + + // Check both layers exist in our stack + // (This also returns the layer, which we ignore.) + self.stack.metal(bot)?; + self.stack.metal(top)?; + + let k = assignments.insert(v); + bot_assns[bot].push(k); + top_assns[top].push(k); + } + // And create our (temporary) cell data! + Ok(TempCell { + cell: layout, + lib: &self.lib, + instances, + assignments, + top_assns, + bot_assns, + cuts, + }) + } + /// Convert a single row/col (period) on a single layer in a single Cell. + fn export_cell_layer_period( + &self, + temp_period: &TempPeriod, + ) -> LayoutResult> { + let mut elems: Vec = Vec::new(); + let layer = temp_period.layer.layer; // FIXME! Can't love this name. + + // Create the layer-period object we'll manipulate most of the way + let mut layer_period = temp_period + .layer + .layer + .spec + .to_layer_period(temp_period.periodnum, temp_period.layer.span.0)?; + // Insert blockages on each track + for (n1, n2, inst_ptr) in temp_period.blockages.iter() { + // Convert primitive-pitch-based blockages to db units + let start = self.db_units(*n1); + let stop = self.db_units(*n2); + let res = layer_period.block(start, stop, &inst_ptr); + self.ok( + res, + format!( + "Could not insert blockage on Layer {:?}, period {} from {:?} to {:?}", + layer, temp_period.periodnum, start, stop + ), + )?; + } + // Place all relevant cuts + let nsig = layer_period.signals.len(); + for cut in temp_period.cuts.iter() { + // Cut the assigned track + let track = &mut layer_period.signals[cut.track.track % nsig]; + let cut_loc = self.track_cross_xy(cut)?; + let dist = cut_loc[layer.spec.dir]; + let res = track.cut( + dist - layer.spec.cutsize / 2, // start + dist + layer.spec.cutsize / 2, // stop + cut, // src + ); + self.ok( + res, + format!("Could not make track-cut {:?} in {:?}", cut, temp_period), + )?; + } + // Handle Net Assignments + // Start with those for which we're the lower of the two layers. + // These will also be where we add vias. + let mut via_opt = None; + for assn_id in temp_period.bot_assns.iter() { + // Note that while `via_layer` is identical over every iteration of this loop, it may not exist if we never enter the loop. + // So, retrieve it from the `stack` on our first iteration. + if via_opt.is_none() { + via_opt = Some(self.stack.via_from(layer.index)?); + } + let via_layer = via_opt.as_ref().unwrap(); + + let assn = self.unwrap( + temp_period.cell.assignments.get(*assn_id), + "Internal error: invalid assignment", + )?; + self.assign_track(layer, &mut layer_period, assn, false)?; + let assn_loc = self.track_cross_xy(&assn.src.at)?; + // Create the via element + let e = raw::Element { + net: Some(assn.src.net.clone()), + layer: via_layer.raw.unwrap(), + purpose: raw::LayerPurpose::Drawing, + inner: raw::Shape::Rect(raw::Rect { + p0: self.export_point( + assn_loc.x - via_layer.size.x / 2, + assn_loc.y - via_layer.size.y / 2, + ), + p1: self.export_point( + assn_loc.x + via_layer.size.x / 2, + assn_loc.y + via_layer.size.y / 2, + ), + }), + }; + elems.push(e); + } + + // Assign all the segments for which we're the top layer + for assn_id in temp_period.top_assns.iter() { + let assn = self.unwrap( + temp_period.cell.assignments.get(*assn_id), + "Internal error: invalid assignment", + )?; + self.assign_track(layer, &mut layer_period, assn, true)?; + } + + // Convert all TrackSegments to raw Elements + for t in layer_period.rails.iter() { + elems.extend(self.export_track(t, &layer)?); + } + for t in layer_period.signals.iter() { + elems.extend(self.export_track(t, &layer)?); + } + Ok(elems) + } + /// Set the net corresponding to `assn` on layer `layer`. + /// + /// The type signature, particularly lifetimes, aren't pretty. + /// <'f> is the short "function lifetime" of the argument references + pub fn assign_track<'f>( + &self, + layer: &'f validate::ValidMetalLayer, + layer_period: &'f mut LayerPeriod<'lib>, + assn: &'lib validate::ValidAssign, + top: bool, // Boolean indication of whether to assign `top` or `bot`. FIXME: not our favorite. + ) -> LayoutResult<()> { + // Grab a (mutable) reference to the assigned track + let nsig = layer_period.signals.len(); + let track = if top { assn.top.track } else { assn.bot.track }; + let track = &mut layer_period.signals[track % nsig]; + // And set the net at the assignment's location + let assn_loc = self.track_cross_xy(&assn.src.at)?; + let res = track.set_net(assn_loc[layer.spec.dir], &assn.src); + self.ok(res, "Error Assigning Track")?; + Ok(()) + } + /// Convert a [Abstract] into raw form. + pub fn export_abstract(&mut self, abs: &abs::Abstract) -> LayoutResult { + self.ctx.push(ErrorContext::Abstract); + + // Create the outline-element, and grab a copy of its inner shape + let outline = self.export_outline(&abs.outline)?; + let outline_shape = outline.inner.clone(); + // Create the raw abstract + let mut rawabs = raw::Abstract::new(&abs.name, outline); + + // Draw a blockage on each layer, equal to the shape of the outline + for layerindex in 0..abs.metals { + let layerkey = self.stack.metal(layerindex)?.raw.unwrap(); + let blk = vec![outline_shape.clone()]; + rawabs.blockages.insert(layerkey, blk); + } + + // Create shapes for each port + for port in abs.ports.iter() { + let rawport = self.export_abstract_port(abs, port)?; + rawabs.ports.push(rawport); + } + // And return the [raw::Abstract] + self.ctx.pop(); + Ok(rawabs) + } + /// Convert an [abs::Port] into raw form. + pub fn export_abstract_port( + &mut self, + abs: &abs::Abstract, + port: &abs::Port, + ) -> LayoutResult { + use abs::PortKind::{Edge, ZTopEdge, ZTopInner}; + + let (layerkey, shape): (raw::LayerKey, raw::Shape) = match &port.kind { + Edge { + layer: layer_index, + track, + side, + } => { + let layer = &self.stack.metal(*layer_index)?.spec; + // First get the "infinite dimension" coordinate from the edge + let infdims: (DbUnits, DbUnits) = match side { + abs::Side::BottomOrLeft => (DbUnits(0), DbUnits(100)), + abs::Side::TopOrRight => { + // FIXME: this assumes rectangular outlines; will take some more work for polygons. + let outside = self.db_units(abs.outline.max(layer.dir)); + (outside - DbUnits(100), outside) + } + }; + // Now get the "periodic dimension" from our layer-center + let perdims: (DbUnits, DbUnits) = self.track_span(*layer_index, *track)?; + // Presuming we're horizontal, points are here: + let mut pts = [Xy::new(infdims.0, perdims.0), Xy::new(infdims.1, perdims.1)]; + // And if vertical, just transpose them + if layer.dir == Dir::Vert { + pts[0] = pts[0].transpose(); + pts[1] = pts[1].transpose(); + } + ( + self.stack.metal(*layer_index)?.raw.unwrap(), + raw::Shape::Rect(raw::Rect { + p0: self.export_xy(&pts[0]), + p1: self.export_xy(&pts[1]), + }), + ) + } + ZTopEdge { track, side, into } => { + let top_metal = if abs.metals == 0 { + self.fail("Abs Port with no metal layers") + } else { + Ok(abs.metals - 1) + }?; + let layer = &self.stack.metal(top_metal)?.spec; + let other_layer_index = match into.1 { + RelZ::Above => top_metal + 1, + RelZ::Below => top_metal - 1, + }; + let other_layer = self.stack.metal(other_layer_index)?; + let other_layer_center = other_layer.center(into.0)?; + // First get the "infinite dimension" coordinate from the edge + let infdims: (DbUnits, DbUnits) = match side { + abs::Side::BottomOrLeft => (DbUnits(0), other_layer_center), + abs::Side::TopOrRight => { + // FIXME: this assumes rectangular outlines; will take some more work for polygons. + let outside = self.db_units(abs.outline.max(layer.dir)); + (other_layer_center, outside) + } + }; + // Now get the "periodic dimension" from our layer-center + let perdims: (DbUnits, DbUnits) = self.track_span(top_metal, *track)?; + // Presuming we're horizontal, points are here: + let mut pts = [Xy::new(infdims.0, perdims.0), Xy::new(infdims.1, perdims.1)]; + // And if vertical, just transpose them + if layer.dir == Dir::Vert { + pts[0] = pts[0].transpose(); + pts[1] = pts[1].transpose(); + } + ( + self.stack.metal(top_metal)?.raw.unwrap(), + raw::Shape::Rect(raw::Rect { + p0: self.export_xy(&pts[0]), + p1: self.export_xy(&pts[1]), + }), + ) + } + ZTopInner { .. } => todo!(), + }; + let mut shapes = HashMap::new(); + shapes.insert(layerkey, vec![shape]); + let rawport = raw::AbstractPort { + net: port.name.clone(), + shapes, + }; + Ok(rawport) + } + /// Get the positions spanning track number `track` on layer number `layer` + fn track_span( + &self, + layer_index: usize, + track_index: usize, + ) -> LayoutResult<(DbUnits, DbUnits)> { + let layer = self.stack.metal(layer_index)?; + layer.span(track_index) + } + /// Convert an [Outline] to a [raw::Shape] + fn outline_shape(&self, outline: &Outline) -> LayoutResult { + // FIXME: always uses `Poly`, because some proto-schemas insist on it as the most general. + // Probably move that conversion down-stack, keep either `Poly` or `Rect` on `layout21::raw::Abstract`. + + // if outline.x.len() == 1 { + // // Rectangular + // let p0 = Point::new(0, 0); + // let xp = self.db_units(outline.x[0]).raw(); + // let yp = self.db_units(outline.y[0]).raw(); + // let p1 = Point::new(xp, yp); + // return Ok(raw::Shape::Rect { p0, p1 }); + // } + // Polygon + // Create an array of Outline-Points + let mut pts = vec![Point { x: 0, y: 0 }]; + let mut xp: isize; + let mut yp: isize = 0; + for i in 0..outline.x.len() { + xp = self.db_units(outline.x[i]).raw(); + pts.push(Point::new(xp, yp)); + yp = self.db_units(outline.y[i]).raw(); + pts.push(Point::new(xp, yp)); + } + // Add the final implied Point at (x, y[-1]) + pts.push(Point::new(0, yp)); + Ok(raw::Shape::Polygon(raw::Polygon { points: pts })) + } + /// Convert an [Outline] to a [raw::Element] polygon + pub fn export_outline(&self, outline: &Outline) -> LayoutResult { + // Create the outline shape + let shape = self.outline_shape(outline)?; + // And create the [raw::Element] + Ok(raw::Element { + net: None, + layer: self.stack.boundary_layer.unwrap(), + purpose: raw::LayerPurpose::Outline, + inner: shape, + }) + } + /// Convert a [Track]-full of [TrackSegment]s to a vector of [raw::Element] rectangles + fn export_track( + &self, + track: &Track, + layer: &validate::ValidMetalLayer, + ) -> LayoutResult> { + let mut elems = Vec::new(); + for seg in &track.segments { + // Convert wires and rails, skip blockages and cuts + use TrackSegmentType::*; + let net: Option = match seg.tp { + Wire { src } => src.map(|src| src.net.clone()), + Rail(rk) => Some(rk.to_string()), + Cut { .. } | Blockage { .. } => continue, + }; + // Convert the inner shape + let inner = match track.data.dir { + Dir::Horiz => raw::Shape::Rect(raw::Rect { + p0: self.export_point(seg.start, track.data.start), + p1: self.export_point(seg.stop, track.data.start + track.data.width), + }), + Dir::Vert => raw::Shape::Rect(raw::Rect { + p0: self.export_point(track.data.start, seg.start), + p1: self.export_point(track.data.start + track.data.width, seg.stop), + }), + }; + // And pack it up as a [raw::Element] + let e = raw::Element { + net, + layer: self.stack.metal(layer.index)?.raw.unwrap(), + purpose: raw::LayerPurpose::Drawing, + inner, + }; + elems.push(e); + } + Ok(elems) + } + /// Create a [TempCellLayer] for the intersection of `temp_cell` and `layer` + fn temp_cell_layer<'a>( + &self, + temp_cell: &'a TempCell, + layer: &'a validate::ValidMetalLayer, + ) -> LayoutResult> { + // Sort out which of the cell's [Instance]s come up to this layer + let mut instances = Vec::with_capacity(temp_cell.instances.len()); + for ptr in temp_cell.instances.iter() { + let inst = ptr.read()?; + let cell = inst.cell.read()?; + if cell.metals()? > layer.index { + instances.push(ptr.clone()); + } + } + // And convert it to a [PtrList] + let instances = PtrList::from_ptrs(instances); + + // Sort out which direction we're working across + let cell = temp_cell.cell; + // Convert to database units + let x = self.db_units(cell.outline.x[0]); // FIXME: rectangles implied here + let y = self.db_units(cell.outline.y[0]); + let (span, breadth) = match layer.spec.dir { + Dir::Horiz => (x, y), + Dir::Vert => (y, x), + }; + + // FIXME: move to `validate` stage + if (breadth % layer.pitch) != 0 { + return self.fail(format!( + "{} has invalid dimension on {}: {:?}, must be multiple of {:?}", + cell.name, layer.spec.name, breadth, layer.pitch, + )); + } + let nperiods = usize::try_from(breadth / layer.pitch).unwrap(); // FIXME: errors + Ok(TempCellLayer { + layer, + cell: temp_cell, + instances, + nperiods, + pitch: layer.pitch, + span, + }) + } + /// Create the [TempPeriod] at the intersection of `temp_layer` and `periodnum` + fn temp_cell_layer_period<'a>( + &self, + temp_layer: &'a TempCellLayer, + periodnum: usize, + ) -> LayoutResult> { + let cell = temp_layer.cell; + let layer = temp_layer.layer; + let dir = layer.spec.dir; + + // For each row, decide which instances intersect + // Convert these into blockage-areas for the tracks + let mut blockages = Vec::with_capacity(temp_layer.instances.len()); + for ptr in temp_layer.instances.iter() { + let inst = &*ptr.read()?; + if self.instance_intersects(inst, layer, periodnum)? { + // Create the blockage + let cell = inst.cell.read()?; + let start = inst.loc.abs()?[dir]; + let stop = start + cell.outline()?.max(dir); + blockages.push((start, stop, ptr.clone())); + } + } + + // Grab indices of the relevant tracks for this period + let nsig = temp_layer.layer.period_data.signals.len(); + let relevant_track_nums = (periodnum * nsig, (periodnum + 1) * nsig); + // Filter cuts down to those in this period + let cuts: Vec<&TrackCross> = cell.cuts[temp_layer.layer.index] + .iter() + .filter(|cut| { + cut.track.track >= relevant_track_nums.0 && cut.track.track < relevant_track_nums.1 + }) + .map(|r| *r) + .collect(); + // Filter assignments down to those in this period + let top_assns = cell.top_assns[temp_layer.layer.index] + .iter() + .filter(|id| { + let assn = cell + .assignments + .get(**id) + .ok_or(LayoutError::from("Internal error: invalid assignment")) + .unwrap(); + assn.top.track >= relevant_track_nums.0 && assn.top.track < relevant_track_nums.1 + }) + .copied() + .collect(); + let bot_assns = cell.bot_assns[temp_layer.layer.index] + .iter() + .filter(|id| { + let assn = cell + .assignments + .get(**id) + .ok_or(LayoutError::from("Internal error: invalid assignment")) + .unwrap(); + assn.bot.track >= relevant_track_nums.0 && assn.bot.track < relevant_track_nums.1 + }) + .copied() + .collect(); + + Ok(TempPeriod { + periodnum, + cell, + layer: temp_layer, + blockages, + cuts, + top_assns, + bot_assns, + }) + } + /// Boolean indication of whether `inst` intersects `layer` at `periodnum` + /// FIXME: rectangular only for now + fn instance_intersects( + &self, + inst: &Instance, + layer: &validate::ValidMetalLayer, + periodnum: usize, + ) -> LayoutResult { + // Grab the layer's *periodic* direction + let dir = !layer.spec.dir; + // Get its starting location in that dimension + let inst_start = self.db_units(inst.loc.abs()?[dir]); + // Sort out whether it's been reflected in this direction + let reflected = match dir { + Dir::Horiz => inst.reflect_horiz, + Dir::Vert => inst.reflect_vert, + }; + // Grab the span of the cell-outline + let span = { + let cell = inst.cell.read()?; + self.db_units(cell.outline()?.max(dir)) + }; + // And sort out the span of the [Instance], from its cell-outline and reflection + let (inst_min, inst_max) = if !reflected { + (inst_start, inst_start + span) + } else { + (inst_start - span, inst_start) + }; + // And return the boolean intersection. "Touching" edge-to-edge is *not* considered an intersection. + Ok(inst_max > layer.pitch * periodnum && inst_min < layer.pitch * (periodnum + 1)) + } + /// Convert any [UnitSpeced]-convertible distances into [DbUnits] + fn db_units(&self, pt: impl Into) -> DbUnits { + let pt: UnitSpeced = pt.into(); + match pt { + UnitSpeced::DbUnits(u) => u, // Return as-is + UnitSpeced::PrimPitches(p) => { + // Multiply by the primitive pitch in `pt`s direction + let pitch = self.stack.prim.pitches[p.dir]; + (p.num * pitch.raw()).into() + } + UnitSpeced::LayerPitches(_p) => { + // LayerPitches are always in the layer's "periodic" dimension + todo!() + } + } + } + /// Convert an [Xy] into a [raw::Point] + fn export_xy>(&self, xy: &Xy) -> raw::Point { + let x = self.db_units(xy.x); + let y = self.db_units(xy.y); + self.export_point(x, y) + } + /// Convert a two-tuple of [DbUnits] into a [raw::Point] + fn export_point(&self, x: DbUnits, y: DbUnits) -> raw::Point { + raw::Point::new(x.0, y.0) + } + /// Convert a [TrackCross] into an (x,y) ([Xy]) coordinate in [DbUnits] + fn track_cross_xy(&self, i: &TrackCross) -> LayoutResult> { + // Find the (x,y) center of our track, initially assuming it runs vertically + let x = self.stack.metal(i.track.layer)?.center(i.track.track)?; + let y = self.stack.metal(i.cross.layer)?.center(i.cross.track)?; + + // And transpose if it's actually horizontal + let mut xy = Xy::new(x, y); + if self.stack.metal(i.track.layer)?.spec.dir == Dir::Horiz { + xy = xy.transpose(); + } + Ok(xy) + } +} +impl ErrorHelper for RawExporter { + type Error = LayoutError; + fn err(&self, msg: impl Into) -> LayoutError { + LayoutError::Export { + message: msg.into(), + stack: self.ctx.clone(), + } + } + fn ok( + &self, + res: Result, + msg: impl Into, + ) -> Result { + match res { + Ok(t) => Ok(t), + Err(e) => Err(LayoutError::Conversion { + message: msg.into(), + err: Box::new(e), + stack: self.ctx.clone(), + }), + } + } +} diff --git a/Tetris/tetris/conv/tracks.py b/Tetris/tetris/conv/tracks.py new file mode 100644 index 0000000..271218e --- /dev/null +++ b/Tetris/tetris/conv/tracks.py @@ -0,0 +1,233 @@ + +from enum import Enum, auto +from typing import List, Union, Optional + +from pydantic.dataclasses import dataclass + +# Local imports +from .coords import DbUnits, Dir +from .error import LayoutError +from .stack import Assign +from .relz import RelZ +# from .instance import Instance + + +@dataclass +class TrackSegmentType: + ... # FIXME! +# enum TrackSegmentType<'lib> : +# Cut : src: 'lib TrackCross , +# Blockage : src: Ptr , +# Wire : src: Optional<'lib Assign> , +# Rail(RailKind), + +# # Segments of un-split, single-net wire on a [Track] +@dataclass +class TrackSegment: + # Segment-Type + tp: TrackSegmentType + # Start Location, in [Stack]'s `units` + start: DbUnits + # End/Stop Location, in [Stack]'s `units` + stop: DbUnits + + +TrackConflict = Union[Assign, TrackCross, Instance] + + + + + +enum TrackError : + OutOfBounds(DbUnits), + Overlap(DbUnits, DbUnits), + Conflict(TrackConflict, TrackConflict), + CutConflict(TrackConflict, TrackCross), + BlockageConflict(TrackConflict, Ptr), + +type TrackResult = Result + + # Display a [TrackError] + def fmt(self, f: std.fmt.Formatter) -> std.fmt.Result : + match self : + TrackError.OutOfBounds(stop) => write!(f, "Track Out of Bounds: ::", stop), + TrackError.Overlap(p0, p1) => : + write!(f, "Overlapping Track cuts at: ::, ::", p0, p1) + + TrackError.CutConflict(t0, t1) => : + write!(f, "Conflicting Track-Cuts at: ::, ::", t0, t1) + + TrackError.BlockageConflict(t0, t1) => : + write!( + f, + "Conflicting Instance Blockages: \n * :\n * ::\n", + t0, t1 + ) + + TrackError.Conflict(t0, t1) => : + write!(f, "Conflict Between: \n * :\n * ::\n", t0, t1) + + + + + + # Display a [TrackError] + def fmt(self, f: std.fmt.Formatter) -> std.fmt.Result : + std.fmt.Debug.fmt(self, f) + + + + + def into(self) -> LayoutError : + LayoutError.Boxed(Box(self)) + + + + +@dataclass +class TrackData : + # Track Type (Rail, Signal) + ttype: TrackType + # Track Index + index: int + # Direction + dir: Dir + # Starting-point in off-dir axis + start: DbUnits + # Track width + width: DbUnits + +# # Track +# +# An "instantiated" track, including: +# * Track-long data in a [TrackData], and +# * A set of [TrackSegment]s +@dataclass +class Track: + # Track-long data + data: TrackData + # Set of wire-segments in positional order + segments: List[TrackSegment] + + # Verify a (generally just-created) [Track] is valid + def validate(self) -> "Track" : + if self.data.width < DbUnits(0) : + raise LayoutError("Negative Track Width") + return self + + # Set the net of the track-segment at `at` to `net` + def set_net(self, at: DbUnits, assn: Assign) -> None : + # First find the segment to be modified + seg = None + for s in self.segments.iter_mut() : + if s.start > at : + break + + if s.start <= at s.stop >= at : + seg = Some(s) + break + + + match seg : + None => Err(TrackError.OutOfBounds(at)), + Some(seg) => match seg.tp : + TrackSegmentType.Rail(_) => unreachable!(), + TrackSegmentType.Cut : .. => Err(TrackError.Conflict( + # Error: trying to assign a net onto a Cut. + TrackConflict.Assign(assn.clone()), + TrackConflict.from(seg.tp.clone()), + )), + TrackSegmentType.Blockage : .. => : + # FIXME: sort out the desired behaviour here. + # Vias above ZTop instance-pins generally land in this case. + # We could check for their locations Or just it go. + Ok(()) + + TrackSegmentType.Wire : ref src, .. => : + # The good case - assignment succeeds. + src.replace(assn) + Ok(()) + + # Insert a cut or blockage corresponding to `blockage`. + def cut_or_block( + self, + start: DbUnits, + stop: DbUnits, + tp: TrackSegmentType, + ) -> None: + # First bounds-check against the end of our segments, which are the end of the cell + if stop > self.segments.last().unwrap().stop : + return Err(TrackError.OutOfBounds(stop)) + + # Find the segment where the blockage starts + segidx = self + .segments + .iter_mut() + .position(|seg| seg.stop > start) + .ok_or(TrackError.OutOfBounds(start)) + .clone() + seg = self.segments[segidx] + # Check for conflicts, and get a copy of our segment-type as we will likely insert a similar segment + tpcopy = match seg.tp : + TrackSegmentType.Blockage : ref src => : + return Err(TrackError.BlockageConflict( + TrackConflict.from(tp), + src.clone(), + )) + + TrackSegmentType.Cut : src => : + return Err(TrackError.CutConflict( + TrackConflict.from(tp), + src.clone(), + )) + + TrackSegmentType.Wire : .. => seg.tp.clone(), + TrackSegmentType.Rail(_) => seg.tp.clone(), + + # Make sure the cut only effects one segment, or fail + if seg.stop < stop : + # FIXME this should really be the *next* segment, borrow checking fight + return Err(TrackError.Overlap(seg.stop, stop)) + + + # All clear time to cut it. + # In the more-common case in which the cut-end and segment-end *do not* coincide, create and insert a new segment. + to_be_inserted: List<(int, TrackSegment)> = List() + to_be_inserted.push((segidx + 1, TrackSegment : start, stop, tp )) + if seg.stop != stop : + newseg = TrackSegment : + tp: tpcopy, + start: stop, + stop: seg.stop, + + to_be_inserted.push((segidx + 2, newseg)) + + # Update the existing segment (and importantly, drop its mutable borrow) + seg.stop = start + for (idx, seg) in to_be_inserted : + self.segments.insert(idx, seg) + + Ok(()) + + # Insert a blockage from `start` to `stop`. + # Fails if the region is not a contiguous wire segment. + # def block(self, start: DbUnits, stop: DbUnits, src: Instance) -> None: + # return self.cut_or_block(start, stop, TrackSegmentType.Blockage (src)) + # + # Cut from `start` to `stop`. + # Fails if the region is not a contiguous wire segment. + def cut( + self, + start: DbUnits, + stop: DbUnits, + src: TrackCross, + ) -> None : + self.cut_or_block(start, stop, TrackSegmentType.Cut (src)) + + # Set the stop position for our last [TrackSegment] to `stop` + def stop(self, stop: DbUnits) -> None: + if self.segments.len() == 0 : + raise LayoutError("Error Stopping Track") + + idx = self.segments.len() - 1 + self.segments[idx].stop = stop diff --git a/Tetris/tetris/coords.py b/Tetris/tetris/coords.py new file mode 100644 index 0000000..24db87c --- /dev/null +++ b/Tetris/tetris/coords.py @@ -0,0 +1,300 @@ +# +# # Tetris Coordinate System(s) +# + +from enum import Enum, auto +from typing import List, Dict, Optional, Union, TypeVar, Tuple, Generic + +from pydantic.generics import GenericModel +from pydantic.dataclasses import dataclass + +# Local Imports +from .index import Index + + +class Dir(Enum): + """Enumerated 2-D Directions""" + + Horiz = "horiz" + Vert = "vert" + + def other(self) -> "Dir": + if self == Dir.Horiz: + return Dir.Vert + if self == Dir.Vert: + return Dir.Horiz + raise ValueError + + +@dataclass +class DbUnits: + """Distance Specified in Database Units""" + + num: int + + +# impl HasUnits for DbUnits { +# # Every so often we need the raw number, fine. Use sparingly. +# #[inline(always)] +# def raw(self) -> int { +# self.0 +# } +# } +# impl std.ops.Div for DbUnits { +# type Output = int; +# def div(self, rhs: DbUnits) -> Self.Output { +# self.raw() / rhs.raw() +# } +# } +# impl std.ops.Div for DbUnits { +# type Output = Self; +# def div(self, rhs: int) -> Self.Output { +# Self(self.raw() / rhs) +# } +# } +# impl std.ops.Rem for DbUnits { +# type Output = int; +# def rem(self, rhs: DbUnits) -> Self.Output { +# self.raw().rem(rhs.raw()) +# } +# } +# impl std.ops.Mul for DbUnits { +# type Output = Self; +# def mul(self, rhs: int) -> Self.Output { +# Self(self.0 * rhs) +# } +# } +# impl std.ops.Mul for DbUnits { +# type Output = Self; +# def mul(self, rhs: usize) -> Self.Output { +# Self(int.try_from(rhs).unwrap() * self.0) +# } +# } + + +@dataclass +class PrimPitches: + """Distance in Primitive-Pitches, in Either X/Y Direction""" + + dir: Dir + num: int + + @staticmethod + def new(dir: Dir, num: int) -> "PrimPitches": + # Create a new [PrimPitches] + return PrimPitches(dir, num) + + # Create a [PrimPitches] in the `x` direction + def x(num: int) -> "PrimPitches": + return PrimPitches(Dir.Horiz, num) + + # Create a [PrimPitches] in the `y` direction + def y(num: int) -> "PrimPitches": + return PrimPitches(Dir.Vert, num) + + # Create a new [PrimPitches] with opposite sign of `self.num` + def negate(self) -> "PrimPitches": + return PrimPitches(self.dir, -self.num) + + def __add__(self, other: "PrimPitches") -> "PrimPitches": + if not isinstance(other, PrimPitches): + return NotImplemented + if self.dir != other.dir: + raise ValueError( + "Invalid attempt to add opposite-direction {} and {}".format( + self, other + ) + ) + return PrimPitches(self.dir, self.num + other.num) + + def __sub__(self, other: "PrimPitches") -> "PrimPitches": + if not isinstance(other, PrimPitches): + return NotImplemented + if self.dir != other.dir: + raise ValueError( + "Invalid attempt to add opposite-direction {} and {}".format( + self, other + ) + ) + return PrimPitches(self.dir, self.num - other.num) + + +# # Numeric operations between primitive-pitch values. +# # Generally panic if operating on two [PrimPitches] with different directions. +# impl std.ops.Add for PrimPitches { +# type Output = PrimPitches; +# def add(self, rhs: Self) -> Self.Output { +# if self.dir != rhs.dir { +# panic!( +# "Invalid attempt to add opposite-direction {:?} and {:?}", +# self, rhs +# ); +# } +# Self { +# dir: self.dir, +# num: self.num + rhs.num, +# } +# } +# } +# impl std.ops.AddAssign for PrimPitches { +# def add_assign( self, rhs: Self) { +# *self = *self + rhs; +# } +# } +# impl std.ops.Sub for PrimPitches { +# type Output = PrimPitches; +# def sub(self, rhs: Self) -> Self.Output { +# if self.dir != rhs.dir { +# panic!( +# "Invalid attempt to add opposite-direction {:?} and {:?}", +# self, rhs +# ); +# } +# Self { +# dir: self.dir, +# num: self.num - rhs.num, +# } +# } +# } +# impl std.ops.SubAssign for PrimPitches { +# def sub_assign( self, rhs: Self) { +# *self = *self - rhs; +# } +# } +# # Numeric operations between primitive-pitch values and regular numerics. +# impl std.ops.Mul for PrimPitches { +# type Output = Self; +# def mul(self, rhs: int) -> Self.Output { +# Self(self.dir, self.num * rhs) +# } +# } +# impl std.ops.MulAssign for PrimPitches { +# def mul_assign( self, rhs: int) { +# self.num = self.num * rhs; +# } +# } +# impl std.ops.Mul for PrimPitches { +# type Output = Self; +# def mul(self, rhs: usize) -> Self.Output { +# Self(self.dir, self.num * int.try_from(rhs).unwrap()) +# } +# } +# impl std.ops.MulAssign for PrimPitches { +# def mul_assign( self, rhs: usize) { +# self.num = self.num * int.try_from(rhs).unwrap(); +# } +# } + + +@dataclass +class LayerPitches: + """Distance in Pitches on a Particular Layer""" + + layer: Index + num: int + + # Consume self, returning the underlying [usize] layer-index and [int] number. + def into_inner(self) -> Tuple[Index, int]: + return (self.layer, self.num) + + +# # Numeric operations between pitch-values and regular numerics. +# impl std.ops.Mul for LayerPitches { +# type Output = Self; +# def mul(self, rhs: int) -> Self.Output { +# Self(self.layer, self.num * rhs) +# } +# } +# impl std.ops.MulAssign for LayerPitches { +# def mul_assign( self, rhs: int) { +# self.num = self.num * rhs; +# } +# } +# impl std.ops.Mul for LayerPitches { +# type Output = Self; +# def mul(self, rhs: usize) -> Self.Output { +# Self(self.layer, self.num * int.try_from(rhs).unwrap()) +# } +# } +# impl std.ops.MulAssign for LayerPitches { +# def mul_assign( self, rhs: usize) { +# self.num = self.num * int.try_from(rhs).unwrap(); +# } +# } + + +class UnitType(Enum): + # Paired "type" zero-data enum for [UnitSpeced] + DbUnits = auto() + PrimPitches = auto() + LayerPitches = auto() + + +T = TypeVar("T") + + +class Xy(GenericModel, Generic[T]): + """X-Y Cartesian Pair""" + + x: T + y: T + + @staticmethod + def new(x: T, y: T) -> "Xy": + return Xy(x=x, y=y) + + def transpose(self) -> "Xy": + # Create a new [Xy] with transposed coordinates. + Xy(self.y, self.x) + + def dir(self, dir_: Dir) -> T: + """Get the dimension in direction `dir` + Also available via square-bracket access through `__getitem__`.""" + if dir_ == Dir.Horiz: + return self.x + if dir_ == Dir.Vert: + return self.y + raise ValueError + + def __getitem__(self, dir_: Dir) -> T: + """Square bracket access via [Dir]""" + if not isinstance(dir_, Dir): + return NotImplemented + return self.dir(dir_) + + +# impl From<(int, int)> for Xy { +# def from(tup: (int, int)) -> Self { +# Self { +# x: tup.0.into(), +# y: tup.1.into(), +# } +# } +# } +# impl From<(int, int)> for Xy { +# def from(tup: (int, int)) -> Self { +# Self( +# PrimPitches { +# dir: Dir.Horiz, +# num: tup.0.into(), +# }, +# PrimPitches { +# dir: Dir.Vert, +# num: tup.1.into(), +# }, +# ) +# } +# } + + +# # Unit-Specified Distances Enumeration +# +# Much of the confusion in a multi-coordinate system such as this +# lies in keeping track of which numbers are in which units. +# +# There are three generally useful units of measure here: +# * DB Units generally correspond to physical length quantities, e.g. nanometers +# * Primitive pitches +# * Per-layer pitches, parameterized by a metal-layer index +# +UnitSpeced = Union[DbUnits, PrimPitches, LayerPitches] diff --git a/Tetris/tetris/error.py b/Tetris/tetris/error.py new file mode 100644 index 0000000..e471fb1 --- /dev/null +++ b/Tetris/tetris/error.py @@ -0,0 +1,7 @@ +""" +# Layout Error Type +""" + + +class LayoutError(Exception): + ... # FIXME! Add some real data diff --git a/Tetris/tetris/group.py b/Tetris/tetris/group.py new file mode 100644 index 0000000..cd1ed6b --- /dev/null +++ b/Tetris/tetris/group.py @@ -0,0 +1,27 @@ +# +# # Layout Element Groups +# +# The primary [Group] type is a set of named, located elements +# which can be placed and moved together. +# + +from typing import List + +from pydantic.dataclasses import dataclass + +# Local imports +from .coords import PrimPitches, Xy +from .instance import Instance + + +# Named group of placeable elements +@dataclass +class Group: + # Group Name + name: str + # Constituent Elements + elements: List[Instance] + + # Size of the Instance's rectangular `boundbox`, i.e. the zero-origin `boundbox` of its `cell`. + def boundbox_size(self) -> Xy[PrimPitches]: + raise NotImplementedError diff --git a/Tetris/tetris/index.py b/Tetris/tetris/index.py new file mode 100644 index 0000000..3469777 --- /dev/null +++ b/Tetris/tetris/index.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass + + +from pydantic.dataclasses import dataclass + + +@dataclass +class Index: + inner: int diff --git a/Tetris/tetris/instance.py b/Tetris/tetris/instance.py new file mode 100644 index 0000000..70522c8 --- /dev/null +++ b/Tetris/tetris/instance.py @@ -0,0 +1,74 @@ +# +# # Instance Structures +# +# Located, oriented instances of other cells or similar reusable layout objects. +# + +from dataclasses import dataclass, field + +# Local imports +from .coords import PrimPitches, Xy, Dir +from .bbox import BoundBox +from .reflect import Reflect + +# from .placement import Place +# from .instantiable import Instantiable + + +@dataclass # Note this is a std-lib dataclass, not pydantic +class Instance: + """Instance of another Cell, Group, or Array""" + + # Instance Name + name: str + + # Target `Cell`, `Group`, `Array`, or other `Instantiable` + of: "Instantiable" + + # Location of the Instance origin + # This origin-position holds regardless of either `reflect` field. + # If specified in absolute coordinates location-units are [PrimPitches]. + loc: "Place" = field() + + # Reflect + reflect: Reflect = field(default_factory=Reflect.default) + + # Boolean indication of whether this Instance is reflected in direction `dir` + def reflected(self, dir_: Dir) -> bool: + return self.reflect.reflected(dir_) + + # Size of the Instance's rectangular `boundbox`, i.e. the zero-origin `boundbox` of its `cell`. + def boundbox_size(self) -> "Xy[PrimPitches]": + return self.of.boundbox_size() + + def __repr__(self): + return f"Instance(name=:self.name, cell=:self.of.name, loc=:self.loc)" + + def boundbox(self) -> BoundBox[PrimPitches]: + """# Retrieve this Instance's bounding rectangle, specified in [PrimPitches]. + # Instance location must be resolved to absolute coordinates, or this method will fail.""" + from .placement import AbsPlace, RelativePlace + + if isinstance(self.loc, RelativePlace): + if self.loc.resolved is None: + msg = f"Instance location must be resolved to absolute coordinates" + raise RuntimeError(msg) + abs_loc = self.loc.resolved + elif isinstance(self.loc, AbsPlace): + abs_loc = self.loc + else: + raise TypeError + + outline = self.of.outline() + + if self.reflect.horiz: + (x0, x1) = (abs_loc.x - outline.xmax(), abs_loc.x) + else: + (x0, x1) = (abs_loc.x, abs_loc.x + outline.xmax()) + + if self.reflect.vert: + (y0, y1) = (abs_loc.y - outline.ymax(), abs_loc.y) + else: + (y0, y1) = (abs_loc.y, abs_loc.y + outline.ymax()) + + return BoundBox(mins=Xy(x=x0, y=y0), maxs=Xy(x=x1, y=y1)) diff --git a/Tetris/tetris/instantiable.py b/Tetris/tetris/instantiable.py new file mode 100644 index 0000000..dbb747b --- /dev/null +++ b/Tetris/tetris/instantiable.py @@ -0,0 +1,9 @@ +from typing import Union + +from .cell import Cell +from .array import Array +from .group import Group + +# Instantiable Types Union +# Primarily used as the `of` target of each `Instance` +Instantiable = Union[Cell, Array, Group] diff --git a/Tetris/tetris/layout.py b/Tetris/tetris/layout.py new file mode 100644 index 0000000..fda9ad7 --- /dev/null +++ b/Tetris/tetris/layout.py @@ -0,0 +1,65 @@ +# +# # Layout-Cell Definitions +# +# Physical implementations of tetris [Cell]s. +# +from typing import List +from dataclasses import field + +from pydantic.dataclasses import dataclass + +# Local imports +from .outline import Outline +from .stack import Assign +from .relz import RelZ +from .track_spec import TrackCross +from .instance import Instance + + +@dataclass +class Layout: + """ + # Layout Cell Implementation + A combination of hierarchical instances and net-assignments to tracks. + """ + + # Cell Name + name: str + # Number of Metal Layers Used + metals: int + # Outline shape counted in x and y pitches of `stack` + outline: Outline + + # Instances of other layout objects (cells, arrays, etc.) + instances: List[Instance] = field(default_factory=list) + # Net-to-track assignments + assignments: List[Assign] = field(default_factory=list) + # Track cuts + cuts: List[TrackCross] = field(default_factory=list) + + def add_instance(self, instance: Instance) -> Instance: + self.instances.append(instance) + return instance + + # Assign a net at the given coordinates. + def assign( + self, + net: str, + layer: int, + track: int, + at: int, + relz: RelZ, + ) -> None: + at = TrackCross.from_relz(layer, track, at, relz) + self.assignments.append(Assign(net, at)) + + # Add a cut at the specified coordinates. + def cut(self, layer: int, track: int, at: int, relz: RelZ): + cut = TrackCross.from_relz(layer, track, at, relz) + self.cuts.append(cut) + + # Get a temporary handle for net assignments + def net(self, net: str) -> "NetHandle": + from .net_handle import NetHandle + + return NetHandle(name=net, parent=self) diff --git a/Tetris/tetris/library.py b/Tetris/tetris/library.py new file mode 100644 index 0000000..01beec2 --- /dev/null +++ b/Tetris/tetris/library.py @@ -0,0 +1,81 @@ +# +# # Layout Library Module +# + +from typing import List, Set, Dict +from dataclasses import field + +from pydantic.dataclasses import dataclass + +from .cell import Cell + + +@dataclass +class Library: + """ + # # Layout Library + # + # A combination of cell definitions, sub-libraries, and metadata + # + """ + + name: str # Library Name + cells: Dict[str, Cell] = field(default_factory=dict) + # Cell Definitions + + # FIXME: `raw` stuff + # # [raw.Library] Definitions + # rawlibs: List[raw.Library] + # # Export to a [raw.Library] + # def to_raw(self, stack: validate.ValidStack) -> LayoutResult> : + # conv.raw.RawExporter.convert(self, stack) + # # Add a [raw.Library] + # def add_rawlib(self, rawlib: raw.Library) -> Ptr : + # self.rawlibs.add(rawlib) + + def add_cell(self, cell: Cell) -> Cell: + """# Add a [Cell]""" + if not isinstance(cell, Cell): + raise TypeError + if cell.name in self.cells: + raise RuntimeError + self.cells[cell.name] = cell + return cell + + # Create an ordered list in which dependent cells follow their dependencies. + def dep_order(self) -> List[Cell]: + return DepOrder.order(self) + + +# # Dependency-Orderer +# Creates an ordered list in which dependent cells follow their dependencies. +@dataclass +class DepOrder: + lib: Library + order: List[Cell] = field(default_factory=list) + done: Set[str] = field(default_factory=set) + pending: Set[str] = field(default_factory=set) + + def order(lib: Library) -> List[Cell]: + myself = DepOrder(lib=lib, order=[], done=set(), pending=set()) + for cell in lib.cells.values(): + myself.process(cell) + return myself.order + + def process(self, cell: Cell): + # If the Cell hasn't already been visited, depth-first search it + if cell.name in self.done: + return # Already done + if cell.name in self.pending: + raise RuntimeError(f"Cycle in cell dependencies: {cell.name}") + self.pending.add(cell.name) + + # If the cell has an implementation, visit its [Instance]s before inserting it + if cell.layout is not None: + for inst in cell.layout.instances: + self.process(inst.of) + + # And insert the cell (pointer) itself + self.pending.remove(cell.name) + self.done.add(cell.name) + self.order.append(cell) diff --git a/Tetris/tetris/net_handle.py b/Tetris/tetris/net_handle.py new file mode 100644 index 0000000..c259553 --- /dev/null +++ b/Tetris/tetris/net_handle.py @@ -0,0 +1,23 @@ +from pydantic.dataclasses import dataclass + +# Local imports +from .layout import Layout +from .relz import RelZ + +# # Net Handle +# +# A short-term handle for chaining multiple assignments to a net +# Typically used as: `mycell.net("name").at(/* args */).at(/* more args */)` +# Takes an exclusive reference to its parent [Layout], +# so generally must be dropped quickly to avoid locking it up. +# +@dataclass +class NetHandle: + name: str + parent: Layout + + # Assign our net at the given coordinates. + # Consumes and returns `self` to enable chaining. + def at(self, layer: int, track: int, at: int, relz: RelZ) -> "NetHandle": + self.parent.assign(self.name, layer, track, at, relz) + return self diff --git a/Tetris/tetris/outline.py b/Tetris/tetris/outline.py new file mode 100644 index 0000000..fe28aef --- /dev/null +++ b/Tetris/tetris/outline.py @@ -0,0 +1,86 @@ +# +# # Tetris-Cell Outlines +# + +from typing import List + +from pydantic.dataclasses import dataclass + +from .coords import PrimPitches, Dir +from .error import LayoutError + +# # Block Outline +# +# All block-outlines are "tetris shaped" rectilinear polygons, and are `layout21.tetris`'s namesake. +# +# These boundaries are closed, consist solely of 90-degree rectangular turns, +# and are specified by a counter-clockwise set of points. +# "Holes" such as the shapes "O" and "8" and "divots" such as the shapes "U" and "H" are not supported. +# +# Two equal-length vectors `x` and `y` describe an Outline's points. +# Counter-clockwise-ness and divot-free-ness requires that: +# * (a) `x` values are monotonically non-increasing, and +# * (b) `y` values are monotonically non-decreasing +# +# Such an outline has vertices in Cartesian space at: +# `[(0,0), (x[0], 0), (x[0], y[0]), (x[1], y[0]), ... , (0, y[-1]), (0,0)]` +# With the first point at the origin, the final point at (0, y[-1]), and its connection back to the origin all implied. +# +# Example: a rectangular Outline would requires single entry for each of `x` and `y`, +# at the rectangle's vertex opposite the origin in both axes. +# +@dataclass +class Outline: + x: List[PrimPitches] + y: List[PrimPitches] + + # Outline constructor from primitive-pitches + def from_prim_pitches(x: List[PrimPitches], y: List[PrimPitches]) -> "Outline": + # Check that x and y are of compatible lengths + if len(x) < 1 or len(x) != len(y): + raise LayoutError("Invalid zero-length Outline dimensions") + + # Check for: + # * Correct directions + # * all non-negative values + for k in range(len(x)): + if x[k].dir != Dir.Horiz or y[k].dir != Dir.Vert: + raise LayoutError("Invalid Outline direction(s)") + + if x[k].num < 0 or y[k].num < 0: + raise LayoutError("Invalid Outline with negative coordinate(s)") + + # Check for: + # * x non-increasing-ness, + # * y for non-decreasing-ness + for k in range(len(x), 1): + if x[k].num > x[k - 1].num: + raise LayoutError("Invalid Outline with non-increasing x-coordinates") + + if y[k].num < y[k - 1].num: + raise LayoutError("Invalid Outline with non-decreasing y-coordinates") + + return Outline(x, y) + + @staticmethod + def rect(x: int, y: int) -> "Outline": + """Create a new rectangular outline of dimenions `x` by `y`""" + return Outline([PrimPitches(Dir.Horiz, x)], [PrimPitches(Dir.Vert, y)]) + + # Maximum x-coordinate + # (Which is also always the *first* x-coordinate) + def xmax(self) -> PrimPitches: + return self.x[0] + + # Maximum y-coordinate + # (Which is also always the *last* y-coordinate) + def ymax(self) -> PrimPitches: + return self.y[len(self.y) - 1] + + # Maximum coordinate in [Dir] `dir` + def max(self, dir_: Dir) -> PrimPitches: + if dir_ == Dir.Horiz: + return self.xmax() + if dir_ == Dir.Vert: + return self.ymax() + raise ValueError diff --git a/Tetris/tetris/placement.py b/Tetris/tetris/placement.py new file mode 100644 index 0000000..382c1ad --- /dev/null +++ b/Tetris/tetris/placement.py @@ -0,0 +1,92 @@ +# +# # Layout21 Placement Module +# + +from typing import Union, Any, Optional + +from pydantic.dataclasses import dataclass + +# Local imports +from .coords import PrimPitches, Xy +from .instance import Instance +from .side import Side +from .align import Align +from .separation import Separation + + +# # Relatively-Placed Assignment +# FIXME: merge back in with absoutely-placed [Assign] +@dataclass +class RelAssign: + net: str + loc: "RelativePlace" + + +@dataclass +class Port: + """Reference to the Location of a Port""" + + inst: Instance + port: str + + +# Place-able types union +Placeable = Union[Instance, RelAssign, Port] + + +# Get the location of the placeable +def loc(self: Placeable) -> "Place": + if isinstance(self, Instance): + return self.loc + if isinstance(self, (Port, RelAssign)): + raise NotImplementedError # FIXME! + raise TypeError + + +@dataclass +class AbsPlace: + """Absolute-Valued Placement, in Primitive Pitches""" + + xy: Xy ## FIXME: [PrimPitches] + + @staticmethod + def origin() -> "AbsPlace": + return AbsPlace(xy=Xy.origin()) + + @property + def x(self) -> PrimPitches: + return self.xy.x + + @property + def y(self) -> PrimPitches: + return self.xy.y + + @staticmethod + def xy(x: int, y: int) -> "AbsPlace": + return AbsPlace(xy=Xy(x=PrimPitches.x(x), y=PrimPitches.y(y))) + + +# # Relative Placement +@dataclass +class RelativePlace: + # Placement is relative `to` this + to: Any ## FIXME: Placeable + # Placed on this `side` of `to` + side: Side + # Aligned to this aspect of `to` + align: Align + # Separation between the placement and the `to` + sep: Separation + + resolved: Optional[AbsPlace] = None + + +# # Placement Union +# +# Includes absolute and relative placements. +# +# Absolute placements are in `Self.AbsType` units. +# Relative placements use the [RelativePlace] struct, +# which can be specified relative to any other [Placeable] object. +# +Place = Union[AbsPlace, RelativePlace] diff --git a/Tetris/tetris/placer.py b/Tetris/tetris/placer.py new file mode 100644 index 0000000..071ef63 --- /dev/null +++ b/Tetris/tetris/placer.py @@ -0,0 +1,357 @@ +""" +# Tetris Placer + +Converts potentially relatively-placed attributes to absolute positions. +""" + +# Std-Lib Imports +from typing import List, Tuple, Union, Optional, Set +from dataclasses import field + +# PyPi Imports +from dataclasses import dataclass + +# Local imports +from .cell import Cell +from .instance import Instance +from .reflect import Reflect +from .bbox import BoundBox +from .layout import Layout +from .coords import Dir, PrimPitches, UnitSpeced, Xy +from .library import Library +from .placement import Placeable, RelativePlace, AbsPlace, Side +from .separation import SepBy +from .stack import ValidStack +from .abstract import Abstract +from .align import Align, AlignSide +from .library import Library +from .stack import ValidStack + +StackEntry = Union[Library, Cell, Layout, Abstract, Instance] # FIXME: maybe more + +# # Placer +# Converts all potentially-relatively-placed attributes to absolute positions. +@dataclass +class Placer: + lib: Library + stack: Optional[ValidStack] + ctx: List[StackEntry] + + # [Placer] public API entrypoint. + # Modify and return [Library] `lib`, converting all [RelativePlace]s to absolute locations. + @classmethod + def place(cls, lib: Library, stack: ValidStack) -> Tuple[Library, ValidStack]: + this = cls( + lib, + stack, + ctx=[], + ) + this.place_lib() + return (this.lib, this.stack) + + # Primary internal implementation method. Update placements for [Library] `self.lib`. + def place_lib(self) -> None: + self.ctx.append(self.lib) + # Iterate over all the library's cells, updating their instance-placements. + for cell in self.lib.dep_order(): + self.ctx.append(cell) + if cell.layout is not None: + self.place_layout(cell.layout) + self.ctx.pop() + self.ctx.pop() + + # Update placements for [Layout] `layout` + def place_layout(self, layout: Layout) -> None: + self.ctx.append(layout) + + # Move `instances` and `places` into one vector of [Placeable]s + places: List[Placeable] = layout.instances + # places.extend(layout.places) + layout.places = [] + + # Iterate over `places` in dependency order, updating any relative-places to absolute. + ordered = PlaceOrder.get_ordered(places) + for place in ordered: + if isinstance(place, Instance): + inst = place + if isinstance(inst.loc, RelativePlace): + # Convert to an absolute location + inst.loc.resolved = self.resolve_instance_place(inst) + + # Add the now-absolute-placed inst to the `instances` list + # FIXME: do this or nah? + # layout.instances.append(inst_ptr.clone()) + + # if isinstance(place, Array): + # array_inst = ptr.write() + # if Place.Rel(ref rel) = array_inst.loc { + # # Convert to an absolute location + # abs = self.resolve_array_place(*array_inst, rel) + # array_inst.loc = Place.Abs(abs) + # } + # # And flatten its instances + # children = self.flatten_array_inst(*array_inst) + # children = children.into_iter().map(|i| Ptr.new(i)) + # layout.instances.extend(children) + # } + # Placeable.Assign(ref ptr) => { + # assn = ptr.read() + # abs: TrackCross = self.resolve_assign_place(assn.loc) + # new_assn = stack.Assign { + # net: assn.net.clone(), + # at: abs, + # } + # layout.assignments.append(new_assn) + # } + # Placeable.Group(_) => unimplemented!(), + # Placeable.Port { .. } => (), # Nothing to do, at least until hitting something that *depends* on the Port location + + self.ctx.pop() + + # Flatten an [ArrayInstance] to a vector of Cell Instances. + # Instance location must be absolute by call-time. + # def flatten_array_inst( + # self, + # array_inst: ArrayInstance, + # ) -> LayoutResult> { + # # Read the child-Instances from the underlying [Array] definition + # children = { + # array = array_inst.array.read() + # self.flatten_array(*array, array_inst.name) + # } + # # Get its initial location + # loc = array_inst.loc.abs() + # # Translate each child to our location and reflection + # for child in children.iter_mut() { + # childloc = child.loc.abs_mut() + # if array_inst.reflect_horiz { + # # Reflect horizontally + # childloc.x *= -1_isize + # child.reflect_horiz = !child.reflect_horiz + # } + # if array_inst.reflect_vert { + # # Reflect vertically + # childloc.y *= -1_isize + # child.reflect_vert = !child.reflect_vert + # } + # # Translate its location + # *childloc += *loc + + # Flatten an [Array] to a vector of Cell Instances. + # def flatten_array(self, array: Array, prefix: str) -> LayoutResult> { + # insts = Vec.with_capacity(array.count) + + # # Get the separations in each dimension + # xsep = match array.sep.x { + # None => PrimPitches.x(0), + # Some(SepBy.UnitSpeced(u)) => { + # match u { + # UnitSpeced.PrimPitches(p) => p.clone(), + # _ => unimplemented!(), # TODO: other units + # } + # } + # Some(SepBy.SizeOf(_)) => unimplemented!(), + # } + # ysep = match array.sep.y { + # None => PrimPitches.y(0), + # Some(SepBy.UnitSpeced(u)) => { + # match u { + # UnitSpeced.PrimPitches(p) => p.clone(), + # _ => unimplemented!(), # TODO: other units + # } + # } + # Some(SepBy.SizeOf(_)) => unimplemented!(), + # } + # sep = Xy.new(xsep, ysep) + + # # Initialize our location to the array-origin + # loc = (0, 0).into() + # for i in 0..array.count { + # match array.unit { + # Arrayable.Instance(cell) => { + # i = Instance { + # inst_name: format!("{}[{}]", prefix, i), # `arrayname[i]` + # cell: cell.clone(), + # loc: Place.Abs(loc), + # reflect_horiz: false, + # reflect_vert: false, + # } + # insts.append(i) + # } + # Arrayable.Array(arr) => { + # # Create a new [ArrayInstance] at the current location, + # # largely for sake of reusing `flatten_array_inst` + # # to get its located, flattened children. + # i = ArrayInstance { + # name: format!("{}[{}]", prefix, i), # `arrayname[i]` + # array: arr.clone(), + # loc: Place.Abs(loc), + # reflect_horiz: false, + # reflect_vert: false, + # } + # # (Potentially recursively) flatten that short-lived [ArrayInstance] + # children = self.flatten_array_inst(i) + # # And add its children to ours + # insts.extend(children) + # } + # Arrayable.Group(_arr) => unimplemented!(), + # } + # # Increment the location by our (two-dimensional) increment. + # loc += sep + + # return insts + + # Resolve a location of [ArrayInstance] `inst` relative to its [RelativePlace] `rel`. + # def resolve_array_place( + # self, + # _inst: ArrayInstance, + # _rel: RelativePlace, + # ) -> LayoutResult> { + # # FIXME: this should just need the same stuff as `resolve_instance_place`, + # # once we have a consolidated version of [Instance] that covers Arrays. + # todo!() + # } + def resolve_instance_place(self, inst: Instance) -> AbsPlace: + """# Resolve a location of [Instance] `inst` relative to its [RelativePlace] `rel`.""" + self.ctx.append(inst) + + # Get the relative-to instance's bounding box + if not isinstance(inst.loc, RelativePlace): + raise RuntimeError("Expected RelativePlace") + place_relative_to_this_bbox = inst.loc.to.boundbox() + + # The coordinate axes here are referred to as `side`, corresponding to `inst.loc.side`, and `align`, corresponding to `inst.loc.align`. + # Mapping these back to (x,y) happens at the very end. + # FIXME: checks that `side` and `align` are orthogonal should come earlier + + # Collect the two sides of the placed instance dictated by `inst.loc.to` + left = right = top = bottom = None + + if inst.loc.side == Side.Left: + right = place_relative_to_this_bbox.left + elif inst.loc.side == Side.Right: + left = place_relative_to_this_bbox.right + elif inst.loc.side == Side.Top: + bottom = place_relative_to_this_bbox.top + elif inst.loc.side == Side.Bottom: + top = place_relative_to_this_bbox.bottom + else: + raise ValueError + + if inst.loc.align.side == Side.Left: + left = place_relative_to_this_bbox.left + elif inst.loc.align.side == Side.Right: + right = place_relative_to_this_bbox.right + elif inst.loc.align.side == Side.Top: + top = place_relative_to_this_bbox.top + elif inst.loc.align.side == Side.Bottom: + bottom = place_relative_to_this_bbox.bottom + else: + raise ValueError + + origin = something(inst=inst, top=top, bottom=bottom, left=left, right=right) + return AbsPlace(origin) + + +def something( + inst: Instance, + top: Optional[PrimPitches], + bottom: Optional[PrimPitches], + left: Optional[PrimPitches], + right: Optional[PrimPitches], +) -> Xy: + # What we know at this point: + # * The cell's bounding box + # * *Either* the top or bottom edge of the instance + # * *Either* the right or left edge of the instance + # * The instance's reflection state + # Now find its origin + ... + cell_size = inst.boundbox_size() + + if top is None: + top = bottom + cell_size.y + if bottom is None: + bottom = top - cell_size.y + if left is None: + left = right - cell_size.x + if right is None: + right = left + cell_size.x + + bbox = BoundBox(mins=Xy(x=left, y=bottom), maxs=Xy(x=right, y=top)) + return get_origin_something(bbox, inst.reflect) + + +def get_origin_something(bbox: BoundBox, reflect: Reflect) -> Xy: + """# Get the origin of an object with bounding box `bbox` and reflection-state `reflect`.""" + x = bbox.right if reflect.horiz else bbox.left + y = bbox.top if reflect.vert else bbox.bottom + return Xy(x=x, y=y) + + +@dataclass +class PlaceOrder: + placeables: List[Placeable] + order: List[Placeable] = field(default_factory=list) + pending: Set[str] = field(default_factory=set) + done: Set[str] = field(default_factory=set) + + @classmethod + def get_ordered(cls, placeables: List[Placeable]) -> List[Placeable]: + order = cls(placeables) + for p in placeables: + order.push(p) + return order.order + + def push(self, item: Placeable): + # Depth-first search dependent Instance placements + if item.name in self.done: + return # Already processed + + # Check for cycles, indicated if `item` is in the pending-set, i.e. an open recursive stack-frame. + if item.name in self.pending: + self.fail() + + self.pending.add(item.name) + # Process the Item, dependencies first + self.process(item) + # Check that `item` hasn't (somehow) been removed from the pending-set + self.pending.remove(item.name) + + # And insert the Item itself + self.done.add(item.name) + self.order.append(item) + + def process(self, item: Placeable) -> None: + # Process [Instance]-pointer `item` + if isinstance(item, Instance): + if isinstance(item.loc, RelativePlace): + self.push(item.loc.to) # Visit the dependency first + elif isinstance(item.loc, AbsPlace): + return # Nothing to do + else: + raise TypeError(f"Unexpected location type {item.loc}") + else: + raise TypeError # FIXME! + + # Placeable.Array(ref p) => { + # if Place.Rel(rel) = p.read().loc { + # self.order.append(rel.to) # Visit the dependency first + # } + # } + # Placeable.Group(ref p) => { + # if Place.Rel(rel) = p.read().loc { + # self.order.append(rel.to) # Visit the dependency first + # } + # } + # Placeable.Assign(a) => { + # # Always relative, append it unconditionally + # a = a.read() + # self.order.append(a.loc.to) + # } + # Placeable.Port { ref inst, .. } => { + # if Place.Rel(rel) = inst.read().loc { + # self.order.append(rel.to) # Visit the dependency first + + def fail(self, msg: Optional[str] = None): + raise RuntimeError(msg or "") diff --git a/Tetris/tetris/reflect.py b/Tetris/tetris/reflect.py new file mode 100644 index 0000000..c442e08 --- /dev/null +++ b/Tetris/tetris/reflect.py @@ -0,0 +1,30 @@ +from pydantic.dataclasses import dataclass + +# Local imports +from .coords import PrimPitches, Xy, Dir +from .bbox import BoundBox + + +@dataclass +class Reflect: + """# Reflection-State""" + + horiz: bool # Reflected horizontally + vert: bool # Reflected vertically + + @staticmethod + def default() -> "Reflect": + """The default, non-reflected state.""" + return Reflect(horiz=False, vert=False) + + def reflected(self, dir_: Dir) -> bool: + """Boolean indication of whether reflected in direction `dir`.""" + if dir_ == Dir.Horiz: + return self.horiz + if dir_ == Dir.Vert: + return self.vert + raise ValueError(f"Invalid direction: {dir_}") + + def __getitem__(self, dir_: Dir) -> bool: + """Square bracket access. Boolean indication of whether reflected in direction `dir`.""" + return self.reflected(dir_) diff --git a/Tetris/tetris/relz.py b/Tetris/tetris/relz.py new file mode 100644 index 0000000..5801989 --- /dev/null +++ b/Tetris/tetris/relz.py @@ -0,0 +1,15 @@ +from enum import Enum, auto + + +class RelZ(Enum): + """Relative Z-Axis Reference to one Layer `Above` or `Below` another""" + + Above = auto() + Below = auto() + + def other(self) -> "RelZ": + if self == RelZ.Above: + return RelZ.Below + if self == RelZ.Below: + return RelZ.Above + raise ValueError diff --git a/Tetris/tetris/separation.py b/Tetris/tetris/separation.py new file mode 100644 index 0000000..90f5c7e --- /dev/null +++ b/Tetris/tetris/separation.py @@ -0,0 +1,53 @@ +from typing import Union, List, Optional + +from pydantic.dataclasses import dataclass + +# Local imports +from .coords import UnitSpeced, Dir + +# from .instantiable import Instantiable + + +@dataclass +class SizeOf: + """# The size of `of`.""" + + ... # FIXME! + # of: "Instantiable" + + +# Enumerated means of specifying x-y relative-placement separation +SepBy = Union[UnitSpeced, SizeOf] + + +@dataclass +class Separation: + """# Three-dimensional separation""" + + x: Optional[SepBy] = None + y: Optional[SepBy] = None + z: Optional[int] = None + + @staticmethod + def zero() -> "Separation": + return Separation() + + @staticmethod + def by_x(x: SepBy) -> "Separation": + return Separation(x=x) + + @staticmethod + def by_y(y: SepBy) -> "Separation": + return Separation(y=y) + + @staticmethod + def by_z(z: int) -> "Separation": + return Separation(z=z) + + # Get the separation in direction `dir` + def dir(self, dir_: Dir) -> Optional[SepBy]: + if dir_ == Dir.Horiz: + return self.x + if dir_ == Dir.Vert: + return self.y + raise ValueError diff --git a/Tetris/tetris/side.py b/Tetris/tetris/side.py new file mode 100644 index 0000000..720289c --- /dev/null +++ b/Tetris/tetris/side.py @@ -0,0 +1,32 @@ +from enum import Enum, auto + + +class Side(Enum): + Top = auto() + Bottom = auto() + Left = auto() + Right = auto() + + # Get the side rotated 90 degrees clockwise + def cw_90(self) -> "Side": + if self == Side.Top: + return Side.Right + if self == Side.Right: + return Side.Bottom + if self == Side.Bottom: + return Side.Left + if self == Side.Left: + return Side.Top + raise ValueError + + # Get the side rotated 90 degrees counter-clockwise + def ccw_90(self) -> "Side": + if self == Side.Top: + return Side.Left + if self == Side.Left: + return Side.Bottom + if self == Side.Bottom: + return Side.Right + if self == Side.Right: + return Side.Top + raise ValueError diff --git a/Tetris/tetris/stack.py b/Tetris/tetris/stack.py new file mode 100644 index 0000000..a3b6cb1 --- /dev/null +++ b/Tetris/tetris/stack.py @@ -0,0 +1,149 @@ +from enum import Enum, auto +from typing import List, Optional +from dataclasses import dataclass + +from pydantic.dataclasses import dataclass + +# Local imports +from .index import Index +from .units import Units +from .coords import DbUnits, Xy, Dir +from .track_spec import TrackCross, TrackSpec, TrackEntry, Repeat + + +# # Via Targets +# +# Enumerates the things vias can "go between". +# Generally either a numbered metal layer, or the primitive base-layers. +# +# Values stored in the `Metal` variant are treated as indicies into `Stack.metals`, +# i.e. `Metal(0)` is the first metal layer defined in the stack. +ViaTarget = Optional[Index] + +# # Via / Insulator Layer Between Metals +@dataclass +class ViaLayer: + # Layer name + name: str + # Top of the two layers connected by this layer + top: ViaTarget + # Bottom of the two layers connected by this layer + bot: ViaTarget + # Via size + size: Xy[DbUnits] + + # FIXME: `raw` stuff + # # Stream-out layer numbers + # raw: Option + + +# Assignment of a net onto a track-intersection +@dataclass +class Assign: + # Net Name + net: str + # Track Intersection Location + at: TrackCross + + +# Indication of whether a layer flips in its periodic axis with every period, +# as most standard-cell-style logic gates do. +class FlipMode(Enum): + EveryOther = auto() + NoFlip = auto() + + +# Indication of whether a layer is owned by, partially included in, or external to the primitive blocks +class PrimitiveMode(Enum): + # Owned by Primitives + Prim = auto() + # Partially split between Primitives and Stack + Split = auto() + # Owned by the Stack + Stack = auto() + + +# Description of the primitive-level cells in a [Stack] +@dataclass +class PrimitiveLayer: + pitches: Xy[DbUnits] + + +# # MetalLayer +# +# Metal layer in a [Stack] +# Each layer is effectively infinite-spanning in one dimension, and periodic in the other. +# Layers with `dir=Dir::Horiz` extend to infinity in x, and repeat in y, and vice-versa. +# +@dataclass +class MetalLayer: + # Layer Name + name: str + # Direction Enumeration (Horizontal/ Vertical) + dir: Dir + # Default size of wire-cuts + cutsize: DbUnits + # Track Size Type Entries + entries: List[TrackSpec] + # Offset in our periodic dimension + offset: DbUnits + # Overlap between periods + overlap: DbUnits + # Setting for period-by-period flipping + flip: FlipMode + # Primitive-layer relationship + prim: PrimitiveMode + + # FIXME: handling `raw` stuff + # # [raw::Layer] for exports + # raw: Option + + # Sum up this [Layer]'s pitch + def pitch(self) -> DbUnits: + return sum(self.flatten()) - self.overlap + + # Flatten our [Entry]s into a vector + # Removes any nested patterns + def flatten(self) -> List[TrackEntry]: + v: List[TrackEntry] = list() + for e in self.entries: + if isinstance(e, Repeat): + for _ in range(e.nrep): + for ee in e.entries: + v.push(ee) + elif isinstance(e, TrackEntry): + v.push(e) + else: + raise TypeError + return v + + +# # Stack +# +# The z-stack, primarily including metal, via, and primitive layers +@dataclass +class Stack: + # Measurement units + units: Units + # Primitive Layer + prim: PrimitiveLayer + # Set of metal layers + metals: List[MetalLayer] + # Set of via layers + vias: List[ViaLayer] + + # FIXME: how to handle `raw` thing + # # [raw::Layer] Mappings + # rawlayers: Option> + # # Layer used for cell outlines/ boundaries + # boundary_layer: Optional + + # Run validation, creating a [ValidStack] + def validate(self) -> "ValidStack": + from .validate import validate_stack + + return validate_stack(self) + + +# FIXME: these will probably be the same type +ValidStack = Stack diff --git a/Tetris/tetris/track_spec.py b/Tetris/tetris/track_spec.py new file mode 100644 index 0000000..6e9121a --- /dev/null +++ b/Tetris/tetris/track_spec.py @@ -0,0 +1,129 @@ +from enum import Enum, auto +from typing import List, Union, Optional + +from pydantic.dataclasses import dataclass + +# Local imports +from .coords import DbUnits, Dir +from .error import LayoutError +from .relz import RelZ + + +# # Track Reference +# +# Integer-pair representing a pointer to a [Layer] and track-index. +# +@dataclass +class TrackRef: + layer: int # Layer Index + track: int # Track Index + + +# # Track Crossing +# +# Located intersection between opposite-direction [Layer]s in [Track]-Space +# +@dataclass +class TrackCross: + # "Primary" [Track] being referred to + track: TrackRef + # Intersecting "secondary" track + cross: TrackRef + + # Create from four [int], representing the two (layer-index, track-index) pairs. + def from_parts(layer1: int, index1: int, layer2: int, index2: int) -> "TrackCross": + return TrackCross( + track=TrackRef(layer1, index1), + cross=TrackRef(layer2, index2), + ) + + # Create from a (layer-index, track-index) pair and a [RelZ] + def from_relz(layer: int, track: int, at: int, relz: RelZ) -> "TrackCross": + layer2 = layer + 1 if relz == RelZ.Above else layer - 1 + track = TrackRef(layer, track) + cross = TrackRef( + layer=layer2, + track=at, + ) + + return TrackCross(track, cross) + + +class TrackType(Enum): + Gap = auto() + Signal = auto() + Pwr = auto() + Gnd = auto() + + def to_string(self) -> str: + if self == TrackEntry.Pwr: + return "VDD" + if self == TrackEntry.Gnd: + return "VSS" + raise ValueError + + +@dataclass +class TrackEntry: + ttype: TrackType + width: DbUnits + + # # Helper method: create of [TrackEntry] of [TrackType] [TrackType.Gap] + # def gap(width: impl Into) -> Self { + # TrackEntry { + # width: width.into(), + # ttype: TrackType.Gap, + # } + # } + # # Helper method: create of [TrackEntry] of [TrackType] [TrackType.Signal] + # def sig(width: impl Into) -> Self { + # TrackEntry { + # width: width.into(), + # ttype: TrackType.Signal, + + +# An array of layout `Entries`, repeated `nrep` times +@dataclass +class Repeat: + entries: List[TrackEntry] + nrep: int + + +# # Track "Specification" Entry +# +# Either a single entry, or repitition thereof. +# +TrackSpec = Union[TrackEntry, Repeat] + +# def gap(width: impl Into) -> Self { +# Self.Entry(TrackEntry { +# width: width.into(), +# ttype: TrackType.Gap, +# }) +# } +# def sig(width: impl Into) -> Self { +# Self.Entry(TrackEntry { +# width: width.into(), +# ttype: TrackType.Signal, +# }) +# } +# def rail(width: impl Into, rk: RailKind) -> Self { +# Self.Entry(TrackEntry { +# width: width.into(), +# ttype: TrackType.Rail(rk), +# }) +# } +# def pwr(width: impl Into) -> Self { +# Self.Entry(TrackEntry { +# width: width.into(), +# ttype: TrackType.Rail(RailKind.Pwr), +# }) +# } +# def gnd(width: impl Into) -> Self { +# Self.Entry(TrackEntry { +# width: width.into(), +# ttype: TrackType.Rail(RailKind.Gnd), +# }) +# } +# def repeat(e: impl Into>, nrep: int) -> Self { +# Self.Repeat(Repeat(e, nrep)) diff --git a/Tetris/tetris/units.py b/Tetris/tetris/units.py new file mode 100644 index 0000000..376c79d --- /dev/null +++ b/Tetris/tetris/units.py @@ -0,0 +1,6 @@ +from pydantic.dataclasses import dataclass + + +@dataclass +class Units: + ... # FIXME! diff --git a/Tetris/tetris/validate.rs b/Tetris/tetris/validate.rs new file mode 100644 index 0000000..7e77433 --- /dev/null +++ b/Tetris/tetris/validate.rs @@ -0,0 +1,375 @@ +//! +//! # Tetris Validators +//! +//! Integrity checks for [Stack]s, [Library]s, and the like. +//! + +// Std-Lib Imports +use std::convert::TryFrom; + +// Local imports +use crate::{ + abs::Abstract, + cell::Cell, + coords::{DbUnits, HasUnits}, + instance::Instance, + layout::Layout, + library::Library, + raw::{self, LayoutError, LayoutResult, Units}, + stack::{Assign, LayerPeriodData, MetalLayer, PrimitiveLayer, Stack}, + stack::{PrimitiveMode, ViaLayer, ViaTarget}, + tracks::{TrackCross, TrackRef}, + utils::{ErrorHelper, Ptr}, +}; + +/// Helper-function for asserting all sorts of boolean conditions, returning [LayoutResult] and enabling the question-mark operator. +pub fn assert(b: bool) -> LayoutResult<()> { + match b { + true => Ok(()), + false => LayoutError::fail("Assertion Failed"), + } +} +#[derive(Debug)] +pub struct StackValidator; +impl ErrorHelper for StackValidator { + type Error = LayoutError; + /// Errors are string-valued [LayoutError::String]s. + fn err(&self, msg: impl Into) -> Self::Error { + LayoutError::msg(msg) + } +} +/// Validate a [Stack], returning a [ValidStack] in its place. +pub fn validate_stack(stack: Stack) -> LayoutResult { + // Create a [StackValidator] instance, and use its internal instance method. + StackValidator.validate_stack(stack) +} +impl StackValidator { + /// Internal implementation of [validate_stack]. + fn validate_stack(&mut self, stack: Stack) -> LayoutResult { + let Stack { + units, + boundary_layer, + vias, + metals, + prim, + rawlayers, + .. + } = stack; + // Validate the primitive layer + self.assert( + prim.pitches.x.raw() > 0, + "Invalid zero or negative Primitive pitch", + )?; + self.assert( + prim.pitches.y.raw() > 0, + "Invalid zero or negative Primitive pitch", + )?; + + // Validate each metal layer + let mut valid_metals = Vec::new(); + for (num, layer) in metals.into_iter().enumerate() { + valid_metals.push(self.validate_metal(layer, num, &prim)?); + } + // Calculate pitches as the *least-common multiple* of same-direction layers below each layer + let mut pitches = vec![DbUnits(0); valid_metals.len()]; + for (num, metal) in valid_metals.iter().enumerate() { + let mut pitch = prim.pitches[!metal.spec.dir]; + for nn in 0..num + 1 { + if valid_metals[nn].spec.dir == metal.spec.dir { + pitch = num_integer::lcm(pitch.raw(), valid_metals[nn].pitch.raw()).into(); + } + } + pitches[num] = pitch; + } + // FIXME: add checks on [ViaLayer]s + // Stack checks out! Return its derived data + Ok(ValidStack { + units, + vias, + pitches, + metals: valid_metals, + prim, + rawlayers, + boundary_layer, + }) + } + /// Perform validation on a [Layer], return a corresponding [ValidMetalLayer] + pub fn validate_metal<'prim>( + &mut self, + layer: MetalLayer, + index: usize, + prim: &'prim PrimitiveLayer, + ) -> LayoutResult { + // Check for non-zero widths of all entries + for entry in layer.entries().iter() { + self.assert( + entry.width.raw() > 0, + format!( + "Invalid non-positive entry on {:?}: {:?}", + layer, entry.width + ), + )?; + } + let pitch = layer.pitch(); + self.assert( + pitch.raw() > 0, + format!( + "Invalid layer with non-positive pitch={}: {:?}", + pitch.raw(), + layer + ), + )?; + // Check for fit on the primitive grid, if the layer is in primitives + match layer.prim { + PrimitiveMode::Split | PrimitiveMode::Prim => { + let prim_pitch = prim.pitches[!layer.dir]; + self.assert(pitch % prim_pitch == 0, format!("Invalid layer {:?} shared with Primitives is not an integer multiple of the primitive pitch in the {:?} direction", layer, !layer.dir))?; + } + PrimitiveMode::Stack => (), + } + // Convert to a prototype [LayerPeriod] + // This is frequently used for calculating track locations + let period_data = layer.to_layer_period_data()?; + Ok(ValidMetalLayer { + raw: layer.raw.clone(), + spec: layer, + index, + period_data, + pitch, + }) + } +} + +/// Derived data for a [Stack], after it has gone through some validation steps. +#[derive(Debug)] +pub struct ValidStack { + /// Measurement units + pub units: Units, + /// Primitive layer + pub prim: PrimitiveLayer, + /// Set of via layers + pub vias: Vec, + /// Metal Layers + metals: Vec, + /// Pitches per metal layer, one each for those in `stack` + pub pitches: Vec, + + /// [raw::Layer] Mappings + pub rawlayers: Option>, + /// Layer used for cell outlines/ boundaries + pub boundary_layer: Option, +} +impl ValidStack { + /// Get Metal-Layer number `idx`. Returns `None` if `idx` is out of bounds. + pub fn metal(&self, idx: usize) -> LayoutResult<&ValidMetalLayer> { + if idx >= self.metals.len() { + LayoutError::fail(format!("Invalid metal index {}", idx)) + } else { + Ok(&self.metals[idx]) + } + } + /// Get the via-layer whose bottom "target" is metal-layer `idx`. + pub fn via_from(&self, idx: usize) -> LayoutResult<&ViaLayer> { + for via_layer in self.vias.iter() { + if let ViaTarget::Metal(k) = via_layer.bot { + if k == idx { + return Ok(via_layer); + } + } + } + LayoutError::fail(format!("Requiring undefined via from metal layer {}", idx)) + } + /// Get Via-Layer number `idx`. Returns an error if `idx` is out of bounds. + pub fn via(&self, idx: usize) -> LayoutResult<&ViaLayer> { + if idx >= self.vias.len() { + LayoutError::fail(format!("Invalid via index {}", idx)) + } else { + Ok(&self.vias[idx]) + } + } +} +#[derive(Debug)] +pub struct ValidMetalLayer { + /// Original Layer Spec + pub spec: MetalLayer, + + // Derived data + /// Index in layers array + pub index: usize, + /// Derived single-period template + pub period_data: LayerPeriodData, + /// Pitch in db-units + pub pitch: DbUnits, + /// Raw layer-key + pub raw: Option, +} +impl ValidMetalLayer { + /// Get the track-index at [DbUnits] `dist` + pub fn track_index(&self, dist: DbUnits) -> LayoutResult { + // FIXME: this, particularly the `position` call, grabs the first track that ends *after* `dist`. + // It could end up more helpful to do "closest" if `dist` is in-between two, + // or have some alignment options. + let npitches = dist / self.pitch; + let remainder = DbUnits(dist % self.pitch); + let mut index = usize::try_from(npitches)? * self.period_data.signals.len(); + + index += self + .period_data + .signals + .iter() + .position(|sig| sig.start + sig.width > remainder) + .unwrap(); + Ok(index) + } + /// Get the center-coordinate of signal-track `idx`, in our periodic dimension + pub fn center(&self, idx: usize) -> LayoutResult { + // FIXME: incorrect for asymmetric tracks via `FlipMode` turned on! + let len = self.period_data.signals.len(); + let track = &self.period_data.signals[idx % len]; + let mut cursor = self.pitch * (idx / len); + cursor += track.start + track.width / 2; + Ok(cursor) + } + /// Get the spanning-coordinates of signal-track `idx`, in our periodic dimension + pub fn span(&self, idx: usize) -> LayoutResult<(DbUnits, DbUnits)> { + let len = self.period_data.signals.len(); + let track = &self.period_data.signals[idx % len]; + let cursor = self.pitch * (idx / len) + track.start; + Ok((cursor, cursor + track.width)) + } +} +/// Validate [Library] `lib`. Requires a valid `stack`. +pub fn validate_lib(lib: &Library, stack: &ValidStack) -> LayoutResult<()> { + LibValidator::new(stack).validate_lib(lib) +} +/// # Library Validator +pub struct LibValidator<'stk> { + pub stack: &'stk ValidStack, +} +impl<'stk> LibValidator<'stk> { + pub(crate) fn new(stack: &'stk ValidStack) -> Self { + Self { stack } + } + pub(crate) fn validate_lib(&mut self, lib: &Library) -> LayoutResult<()> { + self.assert(lib.name.len() > 0, "Library name is empty")?; + for cellptr in lib.cells.iter() { + let mut cell = cellptr.write()?; + self.validate_cell(&mut *cell)?; + } + // FIXME: validate raw-content + Ok(()) + } + pub(crate) fn validate_cell(&mut self, cell: &mut Cell) -> LayoutResult<()> { + // FIXME: add checks on `metals`, `outline` + self.assert(cell.name.len() > 0, "Cell name is empty")?; + if let Some(ref mut abs) = cell.abs { + self.assert( + abs.name == cell.name, + format!( + "Cell name mismatch between Abstract {} and Cell {}", + abs.name, cell.name + ), + )?; + + self.validate_abstract(abs)?; + } + if let Some(ref mut layout) = cell.layout { + self.assert( + layout.name == cell.name, + format!( + "Cell name mismatch between Layout {} and Cell {}", + layout.name, cell.name + ), + )?; + self.validate_layout(layout)?; + } + // FIXME: validate any raw and circuit content + Ok(()) + } + pub(crate) fn validate_abstract(&mut self, _abs: &Abstract) -> LayoutResult<()> { + Ok(()) // FIXME! + } + pub(crate) fn validate_layout(&mut self, layout: &Layout) -> LayoutResult<()> { + for instptr in layout.instances.iter() { + let inst = instptr.read()?; + self.validate_instance(&*inst)?; + } + for cut in layout.cuts.iter() { + self.validate_track_cross(cut)?; + } + for assn in layout.assignments.iter() { + self.validate_assign(assn)?; + } + self.assert( + layout.places.len() == 0, + "Internal Error: Layout being validated without first being Placed ", + )?; + Ok(()) + } + pub(crate) fn validate_instance(&mut self, _inst: &Instance) -> LayoutResult<()> { + Ok(()) // FIXME! + } + pub(crate) fn validate_assign(&mut self, assn: &Assign) -> LayoutResult { + // Net "validation": just empty-string checking, at least for now + self.assert( + assn.net.len() > 0, + format!("Invalid zero-length net assigned at {:?}", assn.at), + )?; + // Validate the track-cross location + let i = &assn.at; + self.validate_track_cross(i)?; + // Arrange the two by top/bottom + let (top, bot) = if i.track.layer == i.cross.layer + 1 { + (i.track, i.cross) + } else if i.track.layer == i.cross.layer - 1 { + (i.cross, i.track) + } else { + return self.fail(format!("Invalid Assign on non-adjacent layers: {:?}", assn)); + }; + Ok(ValidAssign { + top, + bot, + src: assn.clone(), + }) + } + pub(crate) fn validate_track_cross(&mut self, i: &TrackCross) -> LayoutResult<()> { + // Validate both [TrackRef]s + self.validate_track_ref(&i.track)?; + self.validate_track_ref(&i.cross)?; + // Verify that the two are in opposite directions + if self.stack.metal(i.track.layer)?.spec.dir == self.stack.metal(i.cross.layer)?.spec.dir { + self.fail(format!( + "TrackCross {:?} and {:?} are in the same direction", + i.track, i.cross + ))?; + } + Ok(()) + } + pub(crate) fn validate_track_ref(&mut self, i: &TrackRef) -> LayoutResult<()> { + // Check that we won't reach outside the stack + self.assert( + i.layer < self.stack.metals.len(), + format!("Invalid TrackRef outside Stack: {:?}", i), + )?; + Ok(()) + } +} +impl ErrorHelper for LibValidator<'_> { + type Error = LayoutError; + /// Errors are string-valued [LayoutError::String]s. + fn err(&self, msg: impl Into) -> Self::Error { + LayoutError::msg(msg) + } +} + +/// # Validated Assignment +/// +/// Track-intersection including the invariant that `top` is one layer above `bot`, +/// such that the a via can be drawn between the two. +/// +#[derive(Debug, Clone)] +pub struct ValidAssign { + pub src: Assign, + pub top: TrackRef, + pub bot: TrackRef, +} diff --git a/gds21/src/tests.rs b/gds21/src/tests.rs index ea46379..a7a6f2a 100644 --- a/gds21/src/tests.rs +++ b/gds21/src/tests.rs @@ -209,7 +209,6 @@ fn empty_lib_to_yaml() -> GdsResult<()> { Ok(()) } #[test] -#[ignore] // https://github.com/dan-fritchman/Layout21/issues/33 fn empty_lib_to_toml() -> GdsResult<()> { let lib = empty_lib(); Toml.save(&lib, &resource("empty.gds.toml")) diff --git a/layout21raw/src/data.rs b/layout21raw/src/data.rs index 0f31b58..930e3b4 100644 --- a/layout21raw/src/data.rs +++ b/layout21raw/src/data.rs @@ -165,11 +165,11 @@ impl Layers { } LayoutError::fail("No more layer numbers available") } - /// Get a reference to the [LayerKey] for layer-number `num` + /// Get the [LayerKey] for layer-number `num` pub fn keynum(&self, num: i16) -> Option { self.nums.get(&num).map(|x| x.clone()) } - /// Get a reference to the [LayerKey] layer-name `name` + /// Get the [LayerKey] for layer-name `name` pub fn keyname(&self, name: impl Into) -> Option { self.names.get(&name.into()).map(|x| x.clone()) } @@ -402,11 +402,13 @@ pub struct Library { } impl Library { /// Create a new and empty Library - pub fn new(name: impl Into, units: Units) -> Self { + pub fn new(name: impl Into, units: Units, layers: Option>) -> Self { + let layers = layers.unwrap_or_default(); Self { name: name.into(), units, - ..Default::default() + layers, + cells: PtrList::new(), } } } @@ -505,6 +507,13 @@ pub struct Layout { pub annotations: Vec, } impl Layout { + /// Create an empty [Layout] with the given `name` + pub fn new(name: impl Into) -> Self { + Self { + name: name.into(), + ..Default::default() + } + } /// Create a rectangular [BoundBox] surrounding all elements in the [Layout]. pub fn bbox(&self) -> BoundBox { let mut bbox = BoundBox::empty(); @@ -585,6 +594,21 @@ pub struct Element { /// Shape pub inner: Shape, } +impl Element { + pub fn new( + net: Option>, + layer: LayerKey, + purpose: LayerPurpose, + inner: impl Into, + ) -> Self { + Self { + net: net.map(|s| s.into()), + layer, + purpose, + inner: inner.into(), + } + } +} /// Location, orientation, and angular rotation for an [Instance] /// Note these fields exist "flat" in [Instance] as well, diff --git a/layout21raw/src/gds.rs b/layout21raw/src/gds.rs index 98c5b5d..c9019d2 100644 --- a/layout21raw/src/gds.rs +++ b/layout21raw/src/gds.rs @@ -228,14 +228,13 @@ impl<'lib> GdsExporter<'lib> { purpose: &LayerPurpose, ) -> LayoutResult { let layers = self.lib.layers.read()?; - let layer = layers.get(*layer).unwrapper( + let layer = layers.get(*layer).or_handle( self, format!("Layer {:?} Not Defined in Library {}", layer, self.lib.name), - )?; - let xtype = layer - .num(purpose) - .unwrapper( - self, + )?; + let xtype = self + .unwrap( + layer.num(purpose), format!("LayerPurpose Not Defined for {:?}, {:?}", layer, purpose), )? .clone(); @@ -810,7 +809,7 @@ impl GdsImporter { let cell = self .cell_map .get(&sref.name) - .unwrapper(self, format!("Instance of invalid cell {}", cname))?; + .or_handle(self, format!("Instance of invalid cell {}", cname))?; let cell = Ptr::clone(cell); // Convert its location @@ -862,7 +861,7 @@ impl GdsImporter { let cell = self .cell_map .get(&aref.name) - .unwrapper(self, format!("Instance Array of invalid cell {}", cname))?; + .or_handle(self, format!("Instance Array of invalid cell {}", cname))?; let cell = Ptr::clone(cell); // Convert its three (x,y) coordinates diff --git a/layout21raw/src/geom.rs b/layout21raw/src/geom.rs index 841e3a2..be1d73f 100644 --- a/layout21raw/src/geom.rs +++ b/layout21raw/src/geom.rs @@ -67,6 +67,11 @@ impl Point { } } } +impl From<(Int, Int)> for Point { + fn from(vals: (Int, Int)) -> Self { + Self::new(vals.0, vals.1) + } +} /// Direction Enumeration #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] pub enum Dir { @@ -100,6 +105,35 @@ pub struct Path { pub points: Vec, pub width: usize, } +impl Path { + pub fn new(points: Vec, width: usize) -> Self { + Self { points, width } + } + /// Convert a Manhattan path into a vector of rectangles. + /// Returns `None` if the path is not Manhattan. + pub fn rects(&self) -> Option> { + let (points, width) = (&self.points, self.width); + let width = Int::try_from(width).unwrap(); // FIXME: probably store these signed, check them on creation + let mut rects: Vec = Vec::with_capacity(self.points.len()); + for k in 0..points.len() - 1 { + let rect = if points[k].x == points[k + 1].x { + Rect { + p0: Point::new(points[k].x - width / 2, points[k].y), + p1: Point::new(points[k].x + width / 2, points[k + 1].y), + } + } else if points[k].y == points[k + 1].y { + Rect { + p0: Point::new(points[k].x, points[k].y - width / 2), + p1: Point::new(points[k + 1].x, points[k].y + width / 2), + } + } else { + return None; // Non-Manhattan Path + }; + rects.push(rect); + } + Some(rects) + } +} /// # Polygon /// /// Closed n-sided polygon with arbitrary number of vertices. @@ -112,6 +146,50 @@ pub struct Path { pub struct Polygon { pub points: Vec, } +impl Polygon { + pub fn new(points: Vec) -> Self { + Self { points } + } +} +/// Polygon Builder +/// +/// Exposes a `move_by` interface for relative polygon construction. +/// The `build` method consumes each [PolygonBuilder] +/// and returns a [Polygon] in its place. +/// +/// FIXME: add a similar `PathBuilder`. +/// +#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct PolygonBuilder { + points: Vec, +} +impl PolygonBuilder { + /// Create a new [PolygonBuilder] from the origin. + pub fn new() -> Self { + Self { + points: vec![Point::new(0, 0)], + } + } + /// Create a new [PolygonBuilder] from a starting [Point]. + pub fn start_at(start: impl Into) -> Self { + Self { + points: vec![start.into()], + } + } + /// Move the "current point" by (x,y). + /// Adds a [Point] to the eventual [Polygon]. + pub fn move_by(&mut self, x: Int, y: Int) { + let last = self.points.last().unwrap(); + let new = last.shift(&Point::new(x, y)); + self.points.push(new); + } + /// Build to a [Polygon]. Consumes `self`. + pub fn build(self) -> Polygon { + Polygon { + points: self.points, + } + } +} /// # Rectangle /// /// Axis-aligned rectangle, specified by two opposite corners. @@ -122,6 +200,13 @@ pub struct Rect { pub p1: Point, } impl Rect { + /// Create a new [Rect] from two opposite corners + pub fn new(p0: impl Into, p1: impl Into) -> Self { + Self { + p0: p0.into(), + p1: p1.into(), + } + } /// Calculate our center-point pub fn center(&self) -> Point { Point::new((self.p0.x + self.p1.x) / 2, (self.p0.y + self.p1.y) / 2) @@ -173,6 +258,8 @@ pub trait ShapeTrait { fn contains(&self, pt: &Point) -> bool; /// Convert to a [Polygon], our most general of shapes fn to_poly(&self) -> Polygon; + /// Convert to a [Shape] enum, consuming `self` in the process + fn to_shape(self) -> Shape; } impl ShapeTrait for Rect { @@ -218,6 +305,10 @@ impl ShapeTrait for Rect { ], } } + /// Convert to a [Shape] enum, consuming `self` in the process + fn to_shape(self) -> Shape { + Shape::Rect(self) + } } impl ShapeTrait for Polygon { /// Retrieve our "origin", or first [Point] @@ -292,6 +383,10 @@ impl ShapeTrait for Polygon { fn to_poly(&self) -> Polygon { self.clone() } + /// Convert to a [Shape] enum, consuming `self` in the process + fn to_shape(self) -> Shape { + Shape::Polygon(self) + } } impl ShapeTrait for Path { /// Retrieve our "origin", or first [Point] @@ -314,37 +409,23 @@ impl ShapeTrait for Path { /// Boolean indication of whether the [Shape] contains [Point] `pt`. /// Containment is *inclusive* for all [Shape] types. /// [Point]s on their boundary, which generally include all points specifying the shape itself, are regarded throughout as "inside" the shape. + /// + /// Note checks for [`Path`] are valid solely for Manhattan paths, i.e. those with segments solely running vertically or horizontally. fn contains(&self, pt: &Point) -> bool { - // Break into segments, and check for intersection with each - // Probably not the most efficient way to do this, but a start. - // Only "Manhattan paths", i.e. those with segments solely running vertically or horizontally, are supported. // FIXME: even with this method, there are some small pieces at corners which we'll miss. // Whether these are relevant in real life, tbd. - let (points, width) = (&self.points, self.width); - let width = Int::try_from(width).unwrap(); // FIXME: probably store these signed, check them on creation - for k in 0..points.len() - 1 { - let rect = if points[k].x == points[k + 1].x { - Rect { - p0: Point::new(points[k].x - width / 2, points[k].y), - p1: Point::new(points[k].x + width / 2, points[k + 1].y), - } - } else if points[k].y == points[k + 1].y { - Rect { - p0: Point::new(points[k].x, points[k].y - width / 2), - p1: Point::new(points[k + 1].x, points[k].y + width / 2), - } - } else { - unimplemented!("Unsupported Non-Manhattan Path") - }; - if rect.contains(pt) { - return true; - } + match self.rects() { + None => false, // FIXME! non-Manhattan paths + Some(rects) => rects.iter().any(|r| r.contains(pt)), } - false } fn to_poly(&self) -> Polygon { unimplemented!("Path::to_poly") } + /// Convert to a [Shape] enum, consuming `self` in the process + fn to_shape(self) -> Shape { + Shape::Path(self) + } } /// # Matrix-Vector Transformation diff --git a/layout21raw/src/lef.rs b/layout21raw/src/lef.rs index 1fce12b..1e68ad5 100644 --- a/layout21raw/src/lef.rs +++ b/layout21raw/src/lef.rs @@ -125,7 +125,7 @@ impl<'lib> LefExporter<'lib> { let layers = self.lib.layers.read()?; let name = layers .get_name(layerkey) - .unwrapper(self, format!("Invalid un-named layer for LEF export"))?; + .or_handle(self, format!("Invalid un-named layer for LEF export"))?; Ok(name.to_string()) } /// Export a [Shape] to a [lef21::LefGeometry] @@ -253,7 +253,7 @@ impl LefImporter { // Create an outline-rectangle. let outline = { // Grab a [Point] from the `size` field - let lefsize = lefmacro.size.as_ref().unwrapper(self, "Missing LEF size")?; + let lefsize = lefmacro.size.as_ref().or_handle(self, "Missing LEF size")?; let lefsize = lef21::LefPoint::new(lefsize.0, lefsize.1); let Point { x, y } = self.import_point(&lefsize)?; @@ -408,7 +408,7 @@ impl LefImporter { // And *our* paths are integer-width'ed, so they better have a zero-valued fractional part. let width = layer .width - .unwrapper(self, "Invalid LEF Path with no Width")?; + .or_handle(self, "Invalid LEF Path with no Width")?; let width = self.import_dist(&width)?; let width = usize::try_from(width)?; // Convert each of the Points diff --git a/layout21raw/src/proto.rs b/layout21raw/src/proto.rs index 999b154..0dd8f50 100644 --- a/layout21raw/src/proto.rs +++ b/layout21raw/src/proto.rs @@ -303,13 +303,13 @@ impl<'lib> ProtoExporter<'lib> { purpose: &LayerPurpose, ) -> LayoutResult { let layers = self.lib.layers.read()?; - let layer = layers.get(*layer).unwrapper( + let layer = layers.get(*layer).or_handle( self, format!("Layer {:?} Not Defined in Library {}", layer, self.lib.name), )?; let purpose = layer .num(purpose) - .unwrapper( + .or_handle( self, format!("LayerPurpose Not Defined for {:?}, {:?}", layer, purpose), )? @@ -606,11 +606,11 @@ impl ProtoImporter { /// Import a proto-defined pointer, AKA [proto::Reference] fn import_reference(&mut self, pinst: &proto::Instance) -> LayoutResult> { // Mostly wind through protobuf-generated structures' layers of [Option]s - let pref = pinst.cell.as_ref().unwrapper( + let pref = pinst.cell.as_ref().or_handle( self, format!("Invalid proto::Instance with null Cell: {}", pinst.name), )?; - let pref_to = pref.to.as_ref().unwrapper( + let pref_to = pref.to.as_ref().or_handle( self, format!("Invalid proto::Instance with null Cell: {}", pinst.name), )?; @@ -620,7 +620,7 @@ impl ProtoImporter { External(_) => self.fail("Import of external proto-references not supported"), }?; // Now look that up in our hashmap - let cellkey = self.cell_map.get(cellname).unwrapper( + let cellkey = self.cell_map.get(cellname).or_handle( self, format!("Instance proto::Instance of undefined cell {}", cellname), )?; @@ -633,7 +633,7 @@ impl ProtoImporter { // Look up the cell-pointer, which must be imported by now, or we fail let cell = self.import_reference(&pinst)?; // Unwrap the [Option] over (not really optional) location `origin_location` - let origin_location = pinst.origin_location.as_ref().unwrapper( + let origin_location = pinst.origin_location.as_ref().or_handle( self, format!("Invalid proto::Instance with no Location: {}", pinst.name), )?; @@ -731,7 +731,7 @@ impl Layers { #[test] fn proto1() -> LayoutResult<()> { // Round-trip through Layout21::Raw -> ProtoBuf -> Layout21::Raw - let mut lib = Library::new("prt_lib", Units::Nano); + let mut lib = Library::new("prt_lib", Units::Nano, None); let (layer, purpose) = { let mut layers = lib.layers.write()?; layers.get_or_insert(0, 0)? diff --git a/layout21raw/src/tests.rs b/layout21raw/src/tests.rs index 67c3fa8..f7882f0 100644 --- a/layout21raw/src/tests.rs +++ b/layout21raw/src/tests.rs @@ -3,6 +3,7 @@ //! use super::*; +use crate::utils::Ptr; #[test] fn point() { @@ -13,32 +14,36 @@ fn point() { /// Create a [Layers] used by a number of tests pub fn layers() -> LayoutResult { + use LayerPurpose::{Drawing, Label, Outline}; let mut layers = Layers::default(); // Add the outline/ boundary layer - layers.add(Layer::new(236, "boundary").add_pairs(&[(0, LayerPurpose::Outline)])?); + layers.add(Layer::new(236, "boundary").add_pairs(&[(0, Outline)])?); // Create metal layers - let metal_purps = [(20, LayerPurpose::Drawing), (5, LayerPurpose::Label)]; + let metal_purps = [(20, Drawing), (5, Label)]; layers.add(Layer::new(68, "met1").add_pairs(&metal_purps)?); layers.add(Layer::new(69, "met2").add_pairs(&metal_purps)?); layers.add(Layer::new(70, "met3").add_pairs(&metal_purps)?); layers.add(Layer::new(71, "met4").add_pairs(&metal_purps)?); + // Create the via layers // Note that while these use the same *GDS* layer number, they are separate [Layer] objects here. // (Because, well, they really are.) - let via_purps = [(44, LayerPurpose::Drawing)]; + let via_purps = [(44, Drawing)]; layers.add(Layer::new(67, "mcon").add_pairs(&via_purps)?); layers.add(Layer::new(68, "via").add_pairs(&via_purps)?); layers.add(Layer::new(69, "via2").add_pairs(&via_purps)?); layers.add(Layer::new(70, "via3").add_pairs(&via_purps)?); layers.add(Layer::new(71, "via4").add_pairs(&via_purps)?); - // Add a base-layer - layers.add( - Layer::new(64, "nwell") - .add_pairs(&[(44, LayerPurpose::Drawing), (5, LayerPurpose::Label)])?, - ); + // Add base-layers + // FIXME: check these datatype numbers! + layers.add(Layer::new(64, "nwell").add_pairs(&[(20, Drawing), (5, Label)])?); + layers.add(Layer::new(65, "diff").add_pairs(&[(20, Drawing), (5, Label)])?); + layers.add(Layer::new(66, "poly").add_pairs(&[(20, Drawing), (5, Label)])?); + layers.add(Layer::new(67, "li1").add_pairs(&[(20, Drawing), (5, Label)])?); + Ok(layers) } #[test] @@ -60,6 +65,151 @@ fn test_layers() -> LayoutResult<()> { Ok(()) } +#[test] +fn polygon_builder() -> LayoutResult<()> { + let mut b = PolygonBuilder::start_at((0, 0)); + + // Create an "H" shape + b.move_by(1, 0); + b.move_by(0, 1); + b.move_by(1, 0); + b.move_by(0, -1); + b.move_by(1, 0); + b.move_by(0, 3); + b.move_by(-1, 0); + b.move_by(0, -1); + b.move_by(-1, 0); + b.move_by(0, 1); + b.move_by(-1, 0); + + // Convert to a polygon + let p = b.build(); + assert_eq!( + p.points, + vec![ + Point { x: 0, y: 0 }, + Point { x: 1, y: 0 }, + Point { x: 1, y: 1 }, + Point { x: 2, y: 1 }, + Point { x: 2, y: 0 }, + Point { x: 3, y: 0 }, + Point { x: 3, y: 3 }, + Point { x: 2, y: 3 }, + Point { x: 2, y: 2 }, + Point { x: 1, y: 2 }, + Point { x: 1, y: 3 }, + Point { x: 0, y: 3 }, + ] + ); + + Ok(()) +} + +/// Test [Layout] creation and adding a few elements +#[test] +fn create_layout() -> LayoutResult<()> { + use LayerPurpose::Drawing; + let layers = layers()?; + let met1 = layers.keyname("met1").unwrap(); + let mut lay = Layout::new("test_layout_creation"); + lay.elems.push(Element::new( + Some("vss"), + met1, + Drawing, + Rect::new(Point::new(0, 0), Point::new(10, 10)), + )); + lay.elems.push(Element { + net: Some("vdd".into()), + layer: met1, + purpose: Drawing, + inner: Shape::Rect(Rect::new(Point::new(0, 10), Point::new(10, 10))), + }); + lay.elems.push(Element { + net: Some("something_else".into()), + layer: met1, + purpose: Drawing, + inner: Shape::Polygon(Polygon::new(vec![ + Point::new(0, 10), + Point::new(10, 10), + Point::new(10, 0), + Point::new(0, 0), + ])), + }); + + // FIXME: more specific content checks + + Ok(()) +} + +/// Make a tutorial-grade inverter! +#[test] +#[cfg(all(feature = "gds", feature = "proto"))] +fn create_inverter() -> LayoutResult<()> { + use LayerPurpose::Drawing; + let layers = layers()?; + + let met1 = layers.keyname("met1").unwrap(); + let diff = layers.keyname("diff").unwrap(); + let poly = layers.keyname("poly").unwrap(); + let nwell = layers.keyname("nwell").unwrap(); + let li1 = layers.keyname("li1").unwrap(); + + let mut layout = Layout::new("inv"); + + layout.elems.push(Element { + net: None, + layer: diff, + purpose: Drawing, + inner: Rect::new(Point::new(0, 400), Point::new(500, 500)).into(), + }); + layout.elems.push(Element { + net: None, + layer: diff, + purpose: Drawing, + inner: Rect::new(Point::new(0, 0), Point::new(500, 100)).into(), + }); + layout.elems.push(Element { + net: Some("VSS".into()), + layer: met1, + purpose: Drawing, + inner: Rect::new(Point::new(-100, -100), Point::new(600, 100)).into(), + }); + layout.elems.push(Element { + net: Some("VDD".into()), + layer: met1, + purpose: Drawing, + inner: Rect::new(Point::new(-100, 400), Point::new(600, 600)).into(), + }); + layout.elems.push(Element { + net: Some("inp".into()), + layer: poly, + purpose: Drawing, + inner: Rect::new(Point::new(300, 0), Point::new(400, 600)).into(), + }); + layout.elems.push(Element { + net: Some("VDD".into()), + layer: nwell, + purpose: Drawing, + inner: Rect::new(Point::new(-100, 300), Point::new(600, 600)).into(), + }); + layout.elems.push(Element { + net: Some("VDD".into()), + layer: li1, + purpose: Drawing, + inner: Path::new(vec![Point::new(-100, 300), Point::new(600, 600)], 50).into(), + }); + + // FIXME: add the rest of the content! + + let mut lib = Library::new("invlib", Units::Nano, Some(Ptr::new(layers))); + lib.cells.insert(layout); + + let gds = lib.to_gds()?; + gds.save("invlib.gds")?; + let proto = lib.to_proto()?; + // FIXME: write that to disk too + Ok(()) +} /// Take a trip through GDSII -> Layout21::Raw -> ProtoBuf #[cfg(all(feature = "gds", feature = "proto"))] diff --git a/layout21tetris/src/conv/raw.rs b/layout21tetris/src/conv/raw.rs index a51ae21..8af5164 100644 --- a/layout21tetris/src/conv/raw.rs +++ b/layout21tetris/src/conv/raw.rs @@ -131,8 +131,11 @@ impl<'lib> RawExporter { // Get our starter raw-lib, either anew or from any we've imported let rawlibptr = if self.lib.rawlibs.len() == 0 { // Create a new [raw::Library] - let mut rawlib = raw::Library::new(&self.lib.name, self.stack.units); - rawlib.layers = Ptr::clone(self.stack.rawlayers.as_ref().unwrap()); + let mut rawlib = raw::Library::new( + &self.lib.name, + self.stack.units, + Some(Ptr::clone(self.stack.rawlayers.as_ref().unwrap())), + ); Ok(Ptr::new(rawlib)) } else if self.lib.rawlibs.len() == 1 { // Pop the sole raw-library, and use it as a starting point @@ -332,7 +335,7 @@ impl<'lib> RawExporter { let start = self.db_units(*n1); let stop = self.db_units(*n2); // And insert the blockage - layer_period.block(start, stop, &inst_ptr).unwrapper( + layer_period.block(start, stop, &inst_ptr).or_handle( self, format!( "Could not insert blockage on Layer {:?}, period {} from {:?} to {:?}", @@ -353,7 +356,7 @@ impl<'lib> RawExporter { dist + layer.spec.cutsize / 2, // stop cut, // src ) - .unwrapper( + .or_handle( self, format!("Could not make track-cut {:?} in {:?}", cut, temp_period), )?; @@ -432,7 +435,7 @@ impl<'lib> RawExporter { let assn_loc = self.track_cross_xy(&assn.src.at)?; let res = track .set_net(assn_loc[layer.spec.dir], &assn.src) - .unwrapper(self, "Error Assigning Track")?; + .or_handle(self, "Error Assigning Track")?; Ok(()) } /// Convert a [Abstract] into raw form. diff --git a/layout21utils/Cargo.toml b/layout21utils/Cargo.toml index 4f53d9f..10d9fcf 100644 --- a/layout21utils/Cargo.toml +++ b/layout21utils/Cargo.toml @@ -25,4 +25,4 @@ serde_derive = "1.0.88" serde_json = "1.0" serde_yaml = "0.8" textwrap = "0.14.2" -toml = "0.5.10" +toml = "0.6" diff --git a/layout21utils/src/error.rs b/layout21utils/src/error.rs index 80f8288..be6cb2f 100644 --- a/layout21utils/src/error.rs +++ b/layout21utils/src/error.rs @@ -19,11 +19,11 @@ //! /// Demo of using the [`Unwrapper`] trait on [`Option`]s and [`Result`]s. //! fn fun(&self) -> Result<(), String> { //! // Unwrap an [`Option`] -//! Some(5).unwrapper(self, "Option failed!")?; +//! Some(5).or_handle(self, "Option failed!")?; //! //! // Unwrap a [`Result`] //! let r: Result<(), String> = Ok(()); -//! r.unwrapper(self, "Result failed!") +//! r.or_handle(self, "Result failed!") //! } //! } //! ``` @@ -46,6 +46,16 @@ pub trait ErrorHelper { fn fail(&self, msg: impl Into) -> Result { Err(self.err(msg)) } + /// Wrap an existing error, with a paired message, into our [`Error`] type. + /// "Optional" method which must be user-implemented to be used. + fn wrap_err( + &self, + _err: impl std::error::Error, + _msg: impl Into, + ) -> Result { + // Default implementation fails. Implement to use! + unimplemented!() + } /// Unwrap the [Option] `opt` if it is [Some], and return our error if not. fn unwrap(&self, opt: Option, msg: impl Into) -> Result { match opt { @@ -53,13 +63,22 @@ pub trait ErrorHelper { None => self.fail(msg), } } - /// Assert a boolean condition. Returns through `self.fail` if it is not satisfied. + /// Assert boolean condition `b`. Returns through `self.fail` if not. fn assert(&self, b: bool, msg: impl Into) -> Result<(), Self::Error> { match b { true => Ok(()), false => self.fail(msg), } } + /// Unwrap the [Result] `res`. Return through our failure method if it is [Err]. + /// Optional method, but must be implemented to be (usefully) called. + /// The default implementation simply returns an error via `self.fail`. + fn ok(&self, _res: Result, msg: impl Into) -> Result + where + E: std::error::Error + 'static, + { + self.fail(msg) // Default version always fails. + } } /// @@ -68,7 +87,7 @@ pub trait ErrorHelper { /// Trait for post-fix application of [`ErrorHelper`] handling, /// during the particularly common cases of unwrapping [`Option`]s and [`Result`]s. /// -/// Sole method `unwrapper` takes an [`ErrorHelper`] and string-convertible error-message as arguments, +/// Sole method `or_handle` takes an [`ErrorHelper`] and string-convertible error-message as arguments, /// and returns a [`Result`] of the [`ErrorHelper`]'s associated error type. /// /// Example: @@ -78,33 +97,33 @@ pub trait ErrorHelper { /// /// fn example(h: &impl ErrorHelper) -> Result<(), String> { /// // Unwrap an [`Option`] -/// Some(5).unwrapper(h, "Option failed!")?; +/// Some(5).or_handle(h, "Option failed!")?; /// /// // Unwrap a [`Result`] /// let r: Result<(), String> = Ok(()); -/// r.unwrapper(h, "Result failed!") +/// r.or_handle(h, "Result failed!") /// } /// ``` /// /// The typical usage of [`Unwrapper`] is not to implement it for new types, -/// but to just import the trait and use it on the standard library [`Option`] and [`Result`] types. -/// And while not required, said usages are generally expected to be -/// in the context of a type that implements [`ErrorHelper`]. +/// but to just import the trait and use it on the standard library [`Option`] and [`Result`] types. +/// And while not required, said usages are generally expected to be in the +/// implementations of [`ErrorHelper`] types. /// pub trait Unwrapper { type Ok; - fn unwrapper(self, helper: &H, msg: impl Into) -> Result + fn or_handle(self, helper: &H, msg: impl Into) -> Result where H: ErrorHelper; } /// # Unwrapper for [`Option`] /// -/// Performs an action similar to [`Option.unwrap`], but routing failures to the paired [`ErrorHelper`] failure-handler rather than panicking. +/// Performs an action similar to [`Option.unwrap`], mapping to the paired [`ErrorHelper`] error. /// impl Unwrapper for Option { type Ok = T; - fn unwrapper(self, helper: &H, msg: impl Into) -> Result + fn or_handle(self, helper: &H, msg: impl Into) -> Result where H: ErrorHelper, { @@ -117,15 +136,15 @@ impl Unwrapper for Option { /// # Unwrapper for [`Result`] /// -/// Performs an action similar to [`Result.unwrap`], but routing failures to the paired [`ErrorHelper`] failure-handler rather than panicking. +/// Performs an action similar to [`Result.unwrap`], mapping to the paired [`ErrorHelper`] error. /// impl Unwrapper for Result { type Ok = T; - fn unwrapper( + fn or_handle( self, helper: &H, msg: impl Into, - ) -> Result<::Ok, H::Error> + ) -> Result< as Unwrapper>::Ok, H::Error> where H: ErrorHelper, { diff --git a/layout21wgpu/Cargo.toml b/layout21wgpu/Cargo.toml new file mode 100644 index 0000000..1f0d484 --- /dev/null +++ b/layout21wgpu/Cargo.toml @@ -0,0 +1,41 @@ +[package] +description = "Layout21 WGPU" +name = "layout21wgpu" + +# Shared layout21 attributes +authors.workspace = true +categories.workspace = true +documentation.workspace = true +edition.workspace = true +exclude.workspace = true +homepage.workspace = true +include.workspace = true +keywords.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +version.workspace = true +workspace = "../" + +[dependencies] +# Local workspace dependencies +layout21protos = {path = "../layout21protos", version = "3.0.0-pre.3"} +layout21raw = {path = "../layout21raw", version = "3.0.0-pre.3"} +layout21utils = {path = "../layout21utils", version = "3.0.0-pre.3"} + +# Crates.io +wgpu = "0.15" +derive_builder = "0.9" +derive_more = "0.99.16" +num-integer = "0.1" +num-traits = "0.2" +serde = {version = "1.0", features = ["derive"]} +serde_derive = "1.0.88" +slotmap = {version = "1.0", features = ["serde"]} +winit = "0.27.5" +rand = "0.8.5" +log = "0.4.17" +bytemuck = { version = "1.4", features = ["derive"] } +env_logger = "0.10.0" +pollster = "0.2.5" +lyon = "1.0.1" diff --git a/layout21wgpu/src/buffers.rs b/layout21wgpu/src/buffers.rs new file mode 100644 index 0000000..aec0fb7 --- /dev/null +++ b/layout21wgpu/src/buffers.rs @@ -0,0 +1,5 @@ + +use lyon::tessellation::geometry_builder::VertexBuffers; +use crate::Vertex; + +pub type Buffers = VertexBuffers; diff --git a/layout21wgpu/src/color.rs b/layout21wgpu/src/color.rs new file mode 100644 index 0000000..c358330 --- /dev/null +++ b/layout21wgpu/src/color.rs @@ -0,0 +1,29 @@ +use bytemuck::{Pod, Zeroable}; + +#[repr(C)] +#[derive(Copy, Clone, Debug, Pod, Zeroable)] +pub struct Color(pub [f32; 3]); + +pub const COLORS: [Color; 7] = [ + Color([1.0, 0.0, 0.0]), // red + Color([0.0, 1.0, 0.0]), // green + Color([0.0, 0.0, 1.0]), // blue + Color([1.0, 1.0, 0.0]), // + Color([1.0, 0.0, 1.0]), // + Color([0.0, 1.0, 1.0]), // + Color([1.0, 1.0, 1.0]), // white +]; +#[derive(Debug)] +pub(crate) struct ColorWheel { + index: usize, +} +impl ColorWheel { + pub fn new() -> Self { + Self { index: 0 } + } + pub fn next(&mut self) -> Color { + let color = COLORS[self.index]; + self.index = (self.index + 1) % COLORS.len(); + color + } +} diff --git a/layout21wgpu/src/gpu.rs b/layout21wgpu/src/gpu.rs new file mode 100644 index 0000000..ce5ee78 --- /dev/null +++ b/layout21wgpu/src/gpu.rs @@ -0,0 +1,194 @@ +//! +//! # GPU Stuff +//! +//! All the WGPU machinery lives here. +//! Many, many terms of art fly around GPU world; get used to it. +//! + +use wgpu::util::DeviceExt; +use winit::window::Window; + +// Local Imports +use crate::{Buffers, Vertex}; + +/// # GPU Stuff +/// +/// *Microooo-processors*. Idunno what they are, you dunno what they are, CASH. +/// +pub struct GpuStuff { + pub surface: wgpu::Surface, + pub device: wgpu::Device, + pub queue: wgpu::Queue, + pub config: wgpu::SurfaceConfiguration, + pub size: winit::dpi::PhysicalSize, + pub pipeline: wgpu::RenderPipeline, + pub vertex_buffer: wgpu::Buffer, + pub index_buffer: wgpu::Buffer, +} +impl GpuStuff { + /// Create new [`GpuStuff`]. + /// + /// Much of the machinery and terminology lives here in the configuration phase. + /// Once this gets done, things really do calm down to just the vertex and index buffers, + /// and writing triangles to them. + /// + pub async fn new(window: &Window, buffers: &Buffers) -> Self { + let size = window.inner_size(); + let backends = wgpu::util::backend_bits_from_env().unwrap_or_else(wgpu::Backends::all); + let instance = wgpu::Instance::new(wgpu::InstanceDescriptor { + backends, + dx12_shader_compiler: wgpu::Dx12Compiler::default(), + }); + let surface = unsafe { instance.create_surface(window) }.unwrap(); + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + compatible_surface: Some(&surface), + force_fallback_adapter: false, + }) + .await + .unwrap(); + + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + label: None, + features: wgpu::Features::empty(), + limits: wgpu::Limits::default(), + }, + None, // Trace path + ) + .await + .unwrap(); + let swapchain_capabilities = surface.get_capabilities(&adapter); + let swapchain_format = swapchain_capabilities.formats[0]; + + let config = wgpu::SurfaceConfiguration { + usage: wgpu::TextureUsages::RENDER_ATTACHMENT, + format: swapchain_format, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Fifo, + alpha_mode: wgpu::CompositeAlphaMode::Auto, + view_formats: Vec::new(), + }; + surface.configure(&device, &config); + + let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { + label: Some("Shader"), + source: wgpu::ShaderSource::Wgsl(include_str!("shader.wgsl").into()), + }); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: Some("Render Pipeline Layout"), + bind_group_layouts: &[], + push_constant_ranges: &[], + }); + + let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: Some("Render Pipeline"), + layout: Some(&pipeline_layout), + vertex: wgpu::VertexState { + module: &shader, + entry_point: "vs_main", + buffers: &[Vertex::desc()], + }, + fragment: Some(wgpu::FragmentState { + module: &shader, + entry_point: "fs_main", + targets: &[Some(wgpu::ColorTargetState { + format: config.format, + blend: Some(wgpu::BlendState::ALPHA_BLENDING), + write_mask: wgpu::ColorWrites::ALL, + })], + }), + primitive: wgpu::PrimitiveState { + topology: wgpu::PrimitiveTopology::TriangleList, + strip_index_format: None, + ..Default::default() + }, + depth_stencil: None, + multisample: wgpu::MultisampleState::default(), + multiview: None, + }); + + let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Vertex Buffer"), + contents: bytemuck::cast_slice(&buffers.vertices), + usage: wgpu::BufferUsages::VERTEX, + }); + let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some("Index Buffer"), + contents: bytemuck::cast_slice(&buffers.indices), + usage: wgpu::BufferUsages::INDEX, + }); + + Self { + surface, + device, + queue, + config, + size, + pipeline, + vertex_buffer, + index_buffer, + } + } + + pub fn resize(&mut self, new_size: winit::dpi::PhysicalSize) { + if new_size.width > 0 && new_size.height > 0 { + self.size = new_size; + self.config.width = new_size.width; + self.config.height = new_size.height; + self.surface.configure(&self.device, &self.config); + } + } + + /// Render the current frame + pub fn render(&self, buffers: &Buffers) -> Result<(), wgpu::SurfaceError> { + let output = self.surface.get_current_texture()?; + let view = output + .texture + .create_view(&wgpu::TextureViewDescriptor::default()); + + let mut encoder = self + .device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { + label: Some("Render Encoder"), + }); + + { + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + label: Some("Render Pass"), + color_attachments: &[Some(wgpu::RenderPassColorAttachment { + view: &view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color { + r: 0.2, + g: 0.247, + b: 0.314, + a: 1.0, + }), + store: true, + }, + })], + depth_stencil_attachment: None, + }); + + render_pass.set_pipeline(&self.pipeline); + render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); + render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint16); + render_pass.draw_indexed( + 0..buffers.indices.len() as u32, // indices + 0, // base_vertex + 0..1, // instances + ); + } + + self.queue.submit(std::iter::once(encoder.finish())); + output.present(); + + Ok(()) + } +} diff --git a/layout21wgpu/src/layout.rs b/layout21wgpu/src/layout.rs new file mode 100644 index 0000000..faafca3 --- /dev/null +++ b/layout21wgpu/src/layout.rs @@ -0,0 +1,221 @@ +use std::collections::HashMap; + +use lyon::{ + geom::Box2D, + math::{point, Transform}, + path::polygon::Polygon, + tessellation::{ + geometry_builder::{BuffersBuilder, VertexBuffers}, + FillOptions, FillTessellator, FillVertex, FillVertexConstructor, + }, +}; + +use layout21protos::{self, conv as proto_converters}; +use layout21raw as raw; +use layout21utils::Ptr; + +// Local imports +use crate::{Color, ColorWheel, Vertex}; + +/// +/// # Tessellate `layout_display` to triangles ready to render on the GPU. +/// +pub fn tessellate(layout_display: &LayoutDisplay, size: &Size) -> VertexBuffers { + let cell1 = layout_display.cell.read().unwrap(); + let layout = cell1.layout.as_ref().unwrap(); + let bbox = &layout_display.bbox; + let layer_colors = &layout_display.layer_colors; + + let screen_transform = fit(&bbox, size); + let transform = &screen_transform.transform; + + let mut tessellator = FillTessellator::new(); + let mut buffers: VertexBuffers = VertexBuffers::new(); + + // Closures to convert between lyon and layout21 shapes, applying the transform + let get_lyon_point = |p: &raw::Point| transform.transform_point(point(p.x as f32, p.y as f32)); + let get_lyon_rect = |r: &raw::Rect| Box2D::new(get_lyon_point(&r.p0), get_lyon_point(&r.p1)); + + for elem in &layout.elems { + let color = layer_colors.get(&elem.layer).unwrap().clone(); + + let shape = &elem.inner; + match shape { + raw::Shape::Rect(r) => { + tessellator + .tessellate_rectangle( + &get_lyon_rect(r), + &FillOptions::DEFAULT, + &mut BuffersBuilder::new(&mut buffers, WithColor::new(color)), + ) + .unwrap(); + } + raw::Shape::Path(p) => { + // Operate on manhattan paths only, for now + // If the path is non-manhattan, we'll just skip it + let path_rects = p.rects(); + if path_rects.is_none() { + continue; + } + let path_rects = path_rects.unwrap(); + + // Tessellate each rectangle in the path + for r in path_rects.iter() { + let lyon_rect = get_lyon_rect(r); + tessellator + .tessellate_rectangle( + &lyon_rect, + &FillOptions::DEFAULT, + &mut BuffersBuilder::new(&mut buffers, WithColor::new(color)), + ) + .unwrap(); + } + } + raw::Shape::Polygon(p) => { + let points: Vec<_> = p.points.iter().map(|p| get_lyon_point(p)).collect(); + let lyon_polygon: Polygon<_> = Polygon { + points: &points, + closed: true, + }; + tessellator + .tessellate_polygon( + lyon_polygon, + &FillOptions::DEFAULT, + &mut BuffersBuilder::new(&mut buffers, WithColor::new(color)), + ) + .unwrap(); + } + } + } + + buffers +} + +/// Screen/ window size +#[derive(Debug)] +pub struct Size { + pub width: T, + pub height: T, +} +/// Get the transform to fit the bounding box in the screen +fn fit(bbox: &raw::BoundBox, size: &Size) -> ScreenTransformState { + let xspan = (bbox.p1.x - bbox.p0.x) as f64; + let yspan = (bbox.p1.y - bbox.p0.y) as f64; + + // Sort out which dimension to scale to + let zoom = if yspan * size.width as f64 > xspan * size.height as f64 { + 2.0 / yspan // scale to height + } else { + 2.0 / xspan // scale to width + }; + let zoom = (0.9 * zoom) as f32; // leave a bit of padding + + // Get the center of the bounding box + let xmid = (bbox.p1.x + bbox.p0.x) as f32 / 2.0; + let ymid = (bbox.p1.y + bbox.p0.y) as f32 / 2.0; + + // Provide a panning coordinate which scales/ zooms it into GPU coordinates + let pan = (-zoom * xmid, -zoom * ymid); + + ScreenTransformState::new(zoom, pan) +} + +/// The state of zooming and panning the screen +#[derive(Debug)] +pub struct ScreenTransformState { + pub zoom: f32, + pub pan: (f32, f32), + pub transform: Transform, +} +impl ScreenTransformState { + pub fn new(zoom: f32, pan: (f32, f32)) -> Self { + Self { + zoom, + pan, + transform: Transform::identity() + .pre_translate((pan.0, pan.1).into()) + .pre_scale(zoom, zoom), + } + } + pub fn identity() -> Self { + Self { + zoom: 1.0, + pan: (0.0, 0.0), + transform: Transform::identity(), + } + } + pub fn update(&mut self, zoom: f32, pan: (f32, f32)) { + self.zoom = zoom; + self.pan = pan; + self.transform = Transform::identity() + .pre_translate((pan.0, pan.1).into()) + .pre_scale(zoom, zoom); + } +} + +#[derive(Debug)] +pub struct LayoutDisplay { + // Source layout data + pub lib: raw::Library, + pub cell: Ptr, + + // Derived at load time + pub bbox: raw::BoundBox, + pub layer_colors: HashMap, +} +impl LayoutDisplay { + pub fn from_proto() -> Self { + let proto_lib: layout21protos::Library = + proto_converters::open(&resource("sky130_fd_sc_hd__dfxtp_1.pb")).unwrap(); + let rawlib = raw::Library::from_proto(proto_lib, None).unwrap(); + let cell = rawlib.cells[0].clone(); + Self::build(rawlib, cell) + } + pub fn build(rawlib: raw::Library, cell: Ptr) -> Self { + let cell1 = cell.read().unwrap(); + let layout = cell1.layout.as_ref().unwrap(); + + let bbox: raw::BoundBox = layout.bbox(); + + let mut layer_colors: HashMap = HashMap::new(); + let mut color_wheel = ColorWheel::new(); + for elem in &layout.elems { + layer_colors + .entry(elem.layer) + .or_insert_with(|| color_wheel.next()); + } + + Self { + lib: rawlib, + cell: cell.clone(), + bbox, + layer_colors, + } + } +} + +pub struct WithColor { + pub color: Color, +} +impl WithColor { + fn new(color: Color) -> Self { + Self { color } + } +} +impl FillVertexConstructor for WithColor { + fn new_vertex(&mut self, vertex: FillVertex) -> Vertex { + Vertex { + position: vertex.position().to_array(), + color: self.color.clone(), + } + } +} + +/// Grab the full path of resource-file `fname` +fn resource(rname: &str) -> String { + format!( + "{}/../layout21converters/resources/{}", + env!("CARGO_MANIFEST_DIR"), + rname + ) +} diff --git a/layout21wgpu/src/lib.rs b/layout21wgpu/src/lib.rs new file mode 100644 index 0000000..49abf67 --- /dev/null +++ b/layout21wgpu/src/lib.rs @@ -0,0 +1,23 @@ +//! +//! # Layout21 WGPU +//! + +// Internal modules +mod color; +use crate::color::{Color, ColorWheel}; + +mod gpu; +use crate::gpu::GpuStuff; + +mod vertex; +use crate::vertex::Vertex; + +mod buffers; +use crate::buffers::Buffers; + +mod layout; +use crate::layout::{tessellate, LayoutDisplay, Size}; + +// Primary public export: the run function +mod run; +pub use crate::run::run; diff --git a/layout21wgpu/src/main.rs b/layout21wgpu/src/main.rs new file mode 100644 index 0000000..b05b64e --- /dev/null +++ b/layout21wgpu/src/main.rs @@ -0,0 +1,5 @@ +use layout21wgpu::run; + +fn main() { + run() +} diff --git a/layout21wgpu/src/run.rs b/layout21wgpu/src/run.rs new file mode 100644 index 0000000..fea2ca2 --- /dev/null +++ b/layout21wgpu/src/run.rs @@ -0,0 +1,93 @@ +use log::error; +use winit::{ + event::*, + event_loop::{ControlFlow, EventLoop}, + window::{Window, WindowBuilder}, +}; + +// Local imports +use crate::{tessellate, GpuStuff, LayoutDisplay, Size, Buffers}; + +/// # Application State +pub struct State { + gpu: GpuStuff, + layout: LayoutDisplay, + buffers: Buffers, +} +impl State { + async fn new(window: &Window) -> Self { + let layout = LayoutDisplay::from_proto(); + let size = window.inner_size(); + let size: Size = Size { + width: size.width, + height: size.height, + }; + let buffers = tessellate(&layout, &size); + let gpu = GpuStuff::new(window, &buffers).await; + Self { + gpu, + layout, + buffers, + } + } +} + +#[allow(unused_variables)] +fn handle_input(event: &WindowEvent) -> bool { + false +} + +pub fn run() { + env_logger::init(); + + let event_loop = EventLoop::new(); + let window = WindowBuilder::new().build(&event_loop).unwrap(); + window.set_title(&*format!("{}", "Layout21 Viewer")); + let mut state = pollster::block_on(State::new(&window)); + + error!("START!!!"); + + event_loop.run(move |event, _, control_flow| match event { + Event::WindowEvent { + ref event, + window_id, + } if window_id == window.id() => { + if !handle_input(event) { + match event { + WindowEvent::CloseRequested + | WindowEvent::KeyboardInput { + input: + KeyboardInput { + state: ElementState::Pressed, + virtual_keycode: Some(VirtualKeyCode::Escape), + .. + }, + .. + } => *control_flow = ControlFlow::Exit, + WindowEvent::Resized(physical_size) => { + state.gpu.resize(*physical_size); + window.request_redraw(); + } + WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { + state.gpu.resize(**new_inner_size); + } + _ => {} + } + } + } + Event::RedrawRequested(_) => { + error!("REDRAW!!!"); + match state.gpu.render(&state.buffers) { + Ok(_) => {} + Err(wgpu::SurfaceError::Lost) => state.gpu.resize(state.gpu.size), + Err(wgpu::SurfaceError::OutOfMemory) => *control_flow = ControlFlow::Exit, + Err(e) => eprintln!("{:?}", e), + } + } + Event::MainEventsCleared => { + // error!("REQUESTING!!!"); + // window.request_redraw(); + } + _ => {} + }); +} diff --git a/layout21wgpu/src/shader.wgsl b/layout21wgpu/src/shader.wgsl new file mode 100644 index 0000000..5da115f --- /dev/null +++ b/layout21wgpu/src/shader.wgsl @@ -0,0 +1,22 @@ +struct VertexInput { + @location(0) pos: vec2, + @location(1) color: vec3, +}; + +struct VertexOutput { + @builtin(position) position: vec4, + @location(0) color: vec4, +}; + +@vertex +fn vs_main(in: VertexInput) -> VertexOutput { + var out: VertexOutput; + out.color = vec4(in.color, 0.3); + out.position = vec4(in.pos, 0.0, 1.0); + return out; +} + +@fragment +fn fs_main(in: VertexOutput) -> @location(0) vec4 { + return in.color; +} \ No newline at end of file diff --git a/layout21wgpu/src/vertex.rs b/layout21wgpu/src/vertex.rs new file mode 100644 index 0000000..59cde91 --- /dev/null +++ b/layout21wgpu/src/vertex.rs @@ -0,0 +1,23 @@ +use bytemuck::{Pod, Zeroable}; + +// Local imports +use crate::Color; + +#[repr(C)] +#[derive(Copy, Clone, Debug, Pod, Zeroable)] +pub struct Vertex { + pub position: [f32; 2], + pub color: Color, +} +impl Vertex { + pub const ATTRIBUTES: [wgpu::VertexAttribute; 2] = + wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x3]; + + pub fn desc<'a>() -> wgpu::VertexBufferLayout<'a> { + wgpu::VertexBufferLayout { + array_stride: std::mem::size_of::() as wgpu::BufferAddress, + step_mode: wgpu::VertexStepMode::Vertex, + attributes: &Self::ATTRIBUTES, + } + } +} diff --git a/rules21/Cargo.toml b/rules21/Cargo.toml new file mode 100644 index 0000000..a2a5db0 --- /dev/null +++ b/rules21/Cargo.toml @@ -0,0 +1,29 @@ +[package] +description = "Layout Design Rules Schema and Parser" +name = "rules21" +# Shared layout21 attributes +authors = ["Dan Fritchman "] +edition = "2018" +exclude = ["resources"] +license = "BSD-3-Clause" +repository = "https://github.com/dan-fritchman/Layout21" +version = "0.2.1" +workspace = "../" + +[dependencies] +# Local workspace dependencies +layout21raw = {path = "../layout21raw", version = "3.0.0-pre.3"} +layout21utils = {path = "../layout21utils", version = "3.0.0-pre.3"} + +# External dependencies +slotmap = {version = "1.0", features = ["serde"]} +# derive_builder = "0.9.0" +# enum_dispatch = "0.3.1" +# num-derive = "0.3" +# num-traits = "0.2" +# rust_decimal = {version = "1.14.3"} +# serde = {version = "1.0", features = ["derive"]} +# serde_derive = "1.0.88" +# derive_more = "0.99.16" +# once_cell = "1.8.0" +# fstrings = "0.2.3" diff --git a/rules21/readme.md b/rules21/readme.md new file mode 100644 index 0000000..e69de29 diff --git a/rules21/readme.tpl b/rules21/readme.tpl new file mode 100644 index 0000000..c70acf8 --- /dev/null +++ b/rules21/readme.tpl @@ -0,0 +1,7 @@ + +# Rules21 + +DRC and LVS rule-definitions + +{{readme}} + diff --git a/rules21/src/data.rs b/rules21/src/data.rs new file mode 100644 index 0000000..f772dd5 --- /dev/null +++ b/rules21/src/data.rs @@ -0,0 +1,279 @@ +//! +//! # Layout Rules Data Model +//! +//! Defines the core types for connectivity and validity of raw-geometry-style layout. +//! + +// Std-library imports +use std::collections::{HashMap, HashSet}; + +// Crates.io +use slotmap::{new_key_type, SlotMap}; + +// Local Imports +use layout21raw as raw; +use layout21utils as utils; +use utils::ptr::{Ptr, PtrList}; + +// Create key-types for each internal type stored in [SlotMap]s +new_key_type! { + pub struct ElementKey; +} + +/// # Rules for Circuit-Extraction from a Layout +/// +/// As typically performed as part of a layout-vs-schematic check, +/// or as the early stages of a parasitic extraction. +/// +pub struct CircuitExtractionRules { + pub layers: Vec, + pub devices: PtrList, + pub connect_rules: Vec, +} + +pub struct LayerElements { + pub layer: Ptr, + pub elements: Vec, +} +#[derive(Debug, Default)] +struct FlatLayout { + pub net_names: HashSet, + pub by_net: HashMap>, + pub by_layer: HashMap<(raw::LayerKey, raw::LayerPurpose), Vec>, + pub elements: SlotMap, +} +impl FlatLayout { + pub fn from_raw(layout: &raw::Layout) -> Self { + let mut this = Self::default(); + + // Flatten any hierarchy in the input layout, creating a flat vector of shape-elements + let elements = layout.flatten().unwrap(); + for element in elements { + // Add the element to our slot-mapped set, and get a key for it + let ekey = this.elements.insert(element); + let element = &this.elements[ekey]; + + // If the element has a labeled net, add it to the by-net set + if let Some(ref net_name) = element.net { + this.net_names.insert(net_name.to_string()); + this.by_net + .entry(net_name.to_string()) + .or_insert_with(Vec::new) + .push(ekey); + } + // And add it to the by-layer set + this.by_layer + .entry((element.layer, element.purpose.clone())) + .or_insert_with(Vec::new) + .push(ekey); + } + this + } +} + +fn extract_instances( + layout: &raw::Layout, + rules: &CircuitExtractionRules, +) -> Result, Vec>, ()> { + // Flatten `layout`, replacing all layout-instances with raw shapes. + // This also gives us ownership of the flattened shapes. + let layout = FlatLayout::from_raw(layout); + + // Associate each shape in `layout` with one of `rules` layers, or fail + // map_some_layer_thing(layout_layers, rules.layers); + + // For each device-definition in `rules`, search each shape in `layout` for a match + let mut instances: HashMap, Vec> = HashMap::new(); + for device in rules.devices.iter() { + let device_instances = find_device_instances(&layout, &device.read().unwrap(), rules)?; + instances.insert(device.clone(), device_instances); + } + Ok(instances) +} +/// Find all instances of `device` in `layout` +fn find_device_instances( + layout: &FlatLayout, + device: &Device, + rules: &CircuitExtractionRules, +) -> Result, ()> { + let mut instances: Vec = Vec::new(); + // Many devices have several ports on the same layer, e.g. MOS source and drain. + // Get a unique hash-set of all relevant layers for intersection. + let mut port_layers = HashSet::new(); + for port in &device.ports { + port_layers.insert(port.layer.clone()); + } + // // Check each polygon in the layout on the device's ID layer + let id_layer = &device.id_layer; + for id_layer_elem_key in layout.by_layer[id_layer].iter() { + // Each ID-layer shape defines, or at least probably intends to define, an instance of this device. + // Check for intersecting shapes in each of its port-layers. + let id_layer_elem = &layout.elements[*id_layer_elem_key]; + if let Some(instance) = + is_this_an_instance(layout, device, &port_layers, id_layer_elem, rules)? + { + instances.push(instance); + } + } + Ok(instances) +} +/// Extract whether `id_layer_elem` creates an instance of `device` +fn is_this_an_instance( + layout: &FlatLayout, + device: &Device, + port_layers: &HashSet<(raw::LayerKey, raw::LayerPurpose)>, + id_layer_elem: &raw::Element, + rules: &CircuitExtractionRules, +) -> Result, ()> { + let mut intersecting_port_layer_elems: HashMap< + (raw::LayerKey, raw::LayerPurpose), + Vec, + > = HashMap::new(); + let mut nhits = 0; + for port_layer in port_layers { + for port_layer_elem_key in layout.by_layer[port_layer].iter() { + let port_layer_elem = &layout.elements[*port_layer_elem_key]; + if port_layer_elem.inner.intersects(&id_layer_elem.inner) { + intersecting_port_layer_elems + .entry(port_layer.clone()) + .or_insert_with(Vec::new) + .push(*port_layer_elem_key); + nhits += 1; + } + todo!(); + } + } + // Now sort out whether that set of intersections covers all of `device`s ports + // Easy case: if we found fewer hits than ports, we can't be an instance + if nhits < device.ports.len() { + return Ok(None); + } + todo!() +} +pub struct PrimaryLayer { + pub name: String, + pub desc: String, + pub id: u64, + pub spec: LayerMapSpec, +} + +pub struct LayerMapSpec { + pub layernum: u32, + pub datatype: u32, +} + +pub enum LayerData { + Primary(PrimaryLayer), + Derived(DerivedLayer), +} +pub struct Layer { + pub data: LayerData, + pub connect_rules: Vec, +} + +/// # Derived Layout Layer +/// +/// Produced by a set of geometric expressions applied to other [Layer]s. +/// +pub struct DerivedLayer { + pub name: String, + pub desc: String, + pub id: u64, + pub expr: GeomOp, +} + +/// # Enumerated Geometric Operators +/// The primary evaluation-mechanism for derived layers +pub enum GeomOp { + BinaryOp(BinaryOp), +} + +/// # Binary Geometric Operation +/// +/// Produces a [DerivedLayer] as a function of two other [Layer]s. +/// +pub struct BinaryOp { + pub op: BinaryOperator, + pub lhs: Ptr, + pub rhs: Ptr, +} + +/// # Enumerated Binary Geometric Operations +pub enum BinaryOperator { + Union, + Intersection, + Difference, + Xor, + /// Selects all shapes from [Layer] `lhs` that touch or are coincident with shapes in [Layer] `rhs`. + Interact, +} + +pub struct Device { + pub name: String, + pub desc: String, + pub id: u64, + /// Key-layer identifying the device + pub id_layer: (raw::LayerKey, raw::LayerPurpose), // FIXME: Ptr, + /// Port Layers + pub ports: Vec, + /// Additional, optional layers, which generally dictate device flavors or parameters + pub optional_layers: Vec>, + /// Set of parameters and evaluation functions + pub params: Vec, + /// Set of port-symmetries, i.e. ports that can be arbitrarily swapped. + pub symmetries: Symmetries, +} + +/// # Enumerated Types of Layout-Extractable [Device] +pub enum DeviceKind { + Mos(MosKind), + Bipolar(BipolarKind), + Diode, + Capacitor, + Resistor, +} + +pub enum MosKind { + Nmos, + Pmos, +} +pub enum BipolarKind { + Npn, + Pnp, +} + +/// # Extracted Device Port +/// +pub struct Port { + pub name: String, + pub layer: (raw::LayerKey, raw::LayerPurpose), // FIXME: Ptr, +} + +pub struct Symmetries; + +pub struct ParamDeclaration { + pub name: String, + pub desc: String, +} + +/// # Extracted Instance of a [Device] +pub struct Instance { + pub name: String, + pub id: u64, + pub device: Ptr, + pub shapes: Vec>, + pub ports: HashMap>, + pub params: HashMap, +} +pub struct Shape; +pub struct ParamValue; + +/// # Connection Rule to a [Layer] +/// +/// Stored as attributes of each [Layer], indicating which [Layer]s are connected to it. +/// Optionally specifies a third `through` [Layer], typically a via, +/// which if specified is required to coincide with *both* `from` and `to` to form a connection. +pub struct ConnectRule { + pub to: Ptr, + pub through: Option>, +} diff --git a/rules21/src/lib.rs b/rules21/src/lib.rs new file mode 100644 index 0000000..d84e1b1 --- /dev/null +++ b/rules21/src/lib.rs @@ -0,0 +1,13 @@ +//! +//! # Rules21 +//! +//! Layout design rules for design-rule correctness (DRC), +//! layout-versus-schematic (LVS), and similar tools. +//! + +pub mod data; +pub use data::*; + +pub mod read; +pub use read::*; + diff --git a/rules21/src/read.rs b/rules21/src/read.rs new file mode 100644 index 0000000..e69de29