diff --git a/.github/actions/prepare_nix_environment/action.yaml b/.github/actions/prepare_nix_environment/action.yaml index fcced3a45..e5bc0d087 100644 --- a/.github/actions/prepare_nix_environment/action.yaml +++ b/.github/actions/prepare_nix_environment/action.yaml @@ -47,6 +47,15 @@ runs: purge-last-accessed: 0 purge-primary-key: never + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + python-version: "3.12" + + - name: Generate librelane_plugin_fabulous side-package + shell: bash + run: uv sync --locked + - name: Build nix environment shell: bash run: nix develop --command true diff --git a/.github/workflows/librelane-plugin.yml b/.github/workflows/librelane-plugin.yml new file mode 100644 index 000000000..9aa6ec4e7 --- /dev/null +++ b/.github/workflows/librelane-plugin.yml @@ -0,0 +1,209 @@ +name: LibreLane Plugin Nightly + +on: + schedule: + # 03:00 UTC, offset from gds-flow-ci.yml (02:00) and dependency test (00:00) + - cron: "0 3 * * *" + workflow_dispatch: + pull_request: + +permissions: + contents: read + issues: write + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + PDK: ihp-sg13g2 + TILE_NAME: LUT4x8_ha + FABRIC_NAME: synthetic_lut4x8_ha_10x10 + TILE_BASE_CONFIG: tests/assets/librelane_plugin/tiles/classic/common/config.yaml + TILE_CONFIG: tests/assets/librelane_plugin/tiles/classic/LUT4x8_ha/config.yaml + FABRIC_CONFIG: tests/assets/librelane_plugin/fabrics/synthetic_lut4x8_ha_10x10/config.yaml + +jobs: + plugin-smoke: + name: Plugin import smoke test + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v6 + + - name: Install uv + uses: astral-sh/setup-uv@v6 + with: + python-version: "3.12" + + - name: Install FABulous (triggers build_hooks side-packages) + run: uv sync --locked + + - name: Confirm LibreLane auto-discovers the plugin and flows register + run: | + uv run python - <<'PY' + import librelane_plugin_fabulous + from librelane.flows import Flow + expected = { + "FABulousTileVerilogMacroFlow", + "FABulousTileVHDLMacroFlowClassic", + "FABulousFabricMacroFlow", + "FABulousFabricMacroFullFlow", + "FABulousTile", + "FABulousFabric", + } + registered = set(Flow.factory.list()) + missing = expected - registered + assert not missing, f"Missing FABulous flows: {missing}" + print("plugin file:", librelane_plugin_fabulous.__file__) + print("flows OK:", sorted(expected)) + PY + + tile-hardening: + name: Harden LUT4x8_ha via FABulousTile + runs-on: ubuntu-latest + needs: plugin-smoke + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/prepare_nix_environment + + - name: Run librelane on FABulousTile + run: | + nix develop --command bash -c " + set -euo pipefail + librelane \ + --pdk ${PDK} \ + --run-tag ci \ + --save-views-to tile_final_views \ + ${TILE_BASE_CONFIG} \ + ${TILE_CONFIG} \ + 2>&1 | tee tile_run.log + " + + - name: Verify tile final views exist + run: | + set -e + for sub in metrics.json gds lef nl pnl vh; do + if [ ! -e "tile_final_views/${sub}" ]; then + echo "FAIL: tile_final_views/${sub} missing" + ls -la tile_final_views/ || true + exit 1 + fi + done + echo "Tile final views OK" + ls tile_final_views/ + + - name: Package tile final views + if: always() + run: | + if [ -d tile_final_views ]; then + tar --zstd -cf tile-final-views.tar.zst tile_final_views + fi + if [ -f tile_run.log ]; then + tar --zstd -cf tile-run-log.tar.zst tile_run.log + fi + + - name: Upload tile final views + if: always() + uses: actions/upload-artifact@v4 + with: + name: tile-final-views-${{ env.PDK }} + path: tile-final-views.tar.zst + if-no-files-found: error + retention-days: 7 + + - name: Upload tile run log + if: always() + uses: actions/upload-artifact@v4 + with: + name: tile-run-log-${{ env.PDK }} + path: tile-run-log.tar.zst + if-no-files-found: warn + retention-days: 7 + + fabric-stitching: + name: Stitch synthetic_lut4x8_ha_10x10 via FABulousFabric + runs-on: ubuntu-latest + needs: tile-hardening + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v6 + + - uses: ./.github/actions/prepare_nix_environment + + - name: Download hardened tile final views + uses: actions/download-artifact@v4 + with: + name: tile-final-views-${{ env.PDK }} + + - name: Unpack tile final views + run: | + tar --zstd -xf tile-final-views.tar.zst + ls tile_final_views/ + + - name: Generate FABULOUS_TILE_MACROS overlay + run: | + TILE_MACRO_DIR="$(realpath tile_final_views)" + cat > tile_macros_overlay.yaml <&1 | tee fabric_run.log + " + + - name: Verify fabric final GDS exists + run: | + set -e + if ! ls fabric_final_views/gds/*.gds >/dev/null 2>&1; then + echo "FAIL: no fabric GDS produced" + ls -la fabric_final_views/ || true + exit 1 + fi + echo "Fabric GDS OK" + ls fabric_final_views/gds/ + + - name: Package fabric final views + if: always() + run: | + if [ -d fabric_final_views ]; then + tar --zstd -cf fabric-final-views.tar.zst fabric_final_views + fi + if [ -f fabric_run.log ]; then + tar --zstd -cf fabric-run-log.tar.zst fabric_run.log + fi + + - name: Upload fabric final views + if: always() + uses: actions/upload-artifact@v4 + with: + name: fabric-final-views-${{ env.PDK }} + path: fabric-final-views.tar.zst + if-no-files-found: error + retention-days: 7 + + - name: Upload fabric run log + if: always() + uses: actions/upload-artifact@v4 + with: + name: fabric-run-log-${{ env.PDK }} + path: fabric-run-log.tar.zst + if-no-files-found: warn + retention-days: 7 diff --git a/.gitignore b/.gitignore index 076008185..ecd118de1 100755 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ docker-image .codex .coverage* fabulous_nix +librelane_plugin_fabulous build # Documentation build artifacts (auto-generated in CI) diff --git a/build_hooks.py b/build_hooks.py index 538c9f261..a81a7d392 100644 --- a/build_hooks.py +++ b/build_hooks.py @@ -5,11 +5,93 @@ from setuptools.command.build_py import build_py +_UPSTREAM_PKG = "fabulous.fabric_generator.gds_generator" +_LIBRELANE_PLUGIN_SUBPKGS: tuple[str, ...] = ("steps", "flows") + + +def _render_librelane_plugin_init() -> str: + """Render the librelane_plugin_fabulous/__init__.py content.""" + subpkgs = ", ".join(repr(s) for s in _LIBRELANE_PLUGIN_SUBPKGS) + return f'''\ +"""LibreLane plugin: re-export of the FABulous GDS steps and flows. + +Ships as a side-package inside the FABulous-FPGA wheel. LibreLane +auto-discovers packages whose name matches ``librelane_plugin_*`` — importing +this package walks every submodule under +``{_UPSTREAM_PKG}.{{steps,flows}}`` so their +``@Step.factory.register()`` / ``@Flow.factory.register()`` decorators fire. + +``FABulousTile`` and ``FABulousFabric`` are additionally re-exported at the +package root as a drop-in replacement for ``mole99/librelane_plugin_fabulous``. + +Generated at build time by ``build_hooks.BuildPyWithFabulousNix`` — do not +edit by hand. The source of truth for every Step and Flow lives in +``{_UPSTREAM_PKG}``. +""" + +import importlib as _importlib +import pkgutil as _pkgutil +import sys as _sys + +_DEFERRED_FLOWS = False + + +def _tile_optimisation_is_initialising() -> bool: + module = _sys.modules.get( + "{_UPSTREAM_PKG}.steps.tile_optimisation" + ) + return module is not None and not hasattr(module, "TileOptimisation") + + +def _register_submodules(include_flows: bool = True) -> None: + global _DEFERRED_FLOWS + for _sub in {subpkgs!s}: + if _sub == "flows" and not include_flows: + _DEFERRED_FLOWS = True + continue + _pkg = _importlib.import_module(f"{_UPSTREAM_PKG}.{{_sub}}") + for _info in _pkgutil.iter_modules(_pkg.__path__): + _importlib.import_module(f"{_UPSTREAM_PKG}.{{_sub}}.{{_info.name}}") + if include_flows: + _DEFERRED_FLOWS = False + + +def _finish_deferred_registration() -> None: + if _DEFERRED_FLOWS: + _register_submodules(include_flows=True) + + +_register_submodules(include_flows=not _tile_optimisation_is_initialising()) + +__all__ = ["FABulousTile", "FABulousFabric"] + +_REEXPORTS = {{ + "FABulousTile": ("{_UPSTREAM_PKG}.flows.plugin_tile_flow", "FABulousTile"), + "FABulousFabric": ("{_UPSTREAM_PKG}.flows.plugin_fabric_flow", "FABulousFabric"), +}} + + +def __getattr__(name: str) -> object: + """Lazy re-export. + + Resolved on first access rather than at package-import time to avoid a + circular import when LibreLane's plugin discovery fires while this package's + own flow modules are still being initialised. + """ + target = _REEXPORTS.get(name) + if target is None: + raise AttributeError(f"module {{__name__!r}} has no attribute {{name!r}}") + _finish_deferred_registration() + module_name, attr = target + return getattr(_importlib.import_module(module_name), attr) +''' + class BuildPyWithFabulousNix(build_py): - """Generate a build-only `fabulous_nix` package with required nix assets.""" + """Generate FABulous side-packages.""" - _PACKAGE_NAME = "fabulous_nix" + _NIX_PACKAGE_NAME = "fabulous_nix" + _LIBRELANE_PLUGIN_PACKAGE_NAME = "librelane_plugin_fabulous" _ASSET_MAP: tuple[tuple[str, str], ...] = ( ("flake.nix", "flake.nix"), @@ -26,12 +108,14 @@ class BuildPyWithFabulousNix(build_py): ) def run(self) -> None: + """Run package build and generate side packages.""" super().run() self._build_fabulous_nix_package() + self._build_librelane_plugin_fabulous_package() def _build_fabulous_nix_package(self) -> None: project_root = Path(__file__).resolve().parent - target_root = self._target_root(project_root) + target_root = self._target_root(project_root, self._NIX_PACKAGE_NAME) rmtree(target_root, ignore_errors=True) target_root.mkdir(parents=True, exist_ok=True) @@ -47,12 +131,24 @@ def _build_fabulous_nix_package(self) -> None: target_path.parent.mkdir(parents=True, exist_ok=True) copy2(source_path, target_path) - def _target_root(self, project_root: Path) -> Path: + def _build_librelane_plugin_fabulous_package(self) -> None: + """Generate librelane_plugin_fabulous/ as a thin re-export side-package.""" + project_root = Path(__file__).resolve().parent + target_root = self._target_root( + project_root, self._LIBRELANE_PLUGIN_PACKAGE_NAME + ) + rmtree(target_root, ignore_errors=True) + target_root.mkdir(parents=True, exist_ok=True) + (target_root / "__init__.py").write_text( + _render_librelane_plugin_init(), encoding="utf-8" + ) + + def _target_root(self, project_root: Path, package_name: str) -> Path: if getattr(self, "editable_mode", False): packages = list(self.distribution.packages or []) - if self._PACKAGE_NAME not in packages: - packages.append(self._PACKAGE_NAME) + if package_name not in packages: + packages.append(package_name) self.distribution.packages = packages - return project_root / self._PACKAGE_NAME + return project_root / package_name - return Path(self.build_lib) / self._PACKAGE_NAME + return Path(self.build_lib) / package_name diff --git a/fabulous/fabric_definition/define.py b/fabulous/fabric_definition/define.py index 58e39d7af..048f09d69 100644 --- a/fabulous/fabric_definition/define.py +++ b/fabulous/fabric_definition/define.py @@ -6,6 +6,7 @@ from decimal import Decimal from enum import Enum, StrEnum +from functools import total_ordering from typing import NamedTuple @@ -25,23 +26,34 @@ class IO(Enum): NULL = "NULL" +@total_ordering class Direction(Enum): """Enumeration for wire and port directions in the fabric. - Defines the directional flow of wires and ports: + Members are declared in canonical order (NORTH, EAST, SOUTH, WEST, JUMP) + and can be sorted. + + The directional flow of wires and ports: - NORTH: Northward direction - - SOUTH: Southward direction - EAST: Eastward direction + - SOUTH: Southward direction - WEST: Westward direction - JUMP: Local connections within a tile """ NORTH = "NORTH" - SOUTH = "SOUTH" EAST = "EAST" + SOUTH = "SOUTH" WEST = "WEST" JUMP = "JUMP" + def __lt__(self, other: "Direction") -> bool: + """Order by canonical definition order (NORTH < EAST < SOUTH < WEST < JUMP).""" + if not isinstance(other, Direction): + return NotImplemented + members = list(type(self)) + return members.index(self) < members.index(other) + class Side(StrEnum): """Enumeration for tile sides and placement. diff --git a/fabulous/fabric_generator/gds_generator/define.py b/fabulous/fabric_generator/gds_generator/define.py new file mode 100644 index 000000000..e21a20902 --- /dev/null +++ b/fabulous/fabric_generator/gds_generator/define.py @@ -0,0 +1,27 @@ +"""Shared tile optimisation mode definitions.""" + +from enum import StrEnum + + +class OptMode(StrEnum): + """Optimisation modes for tile size finding.""" + + FIND_MIN_WIDTH = "find_min_width" + FIND_MIN_HEIGHT = "find_min_height" + BALANCE = "balance" + LARGE = "large" + NO_OPT = "no_opt" + + @classmethod + def _missing_(cls, value: object) -> "OptMode": + """Look up an OptMode member case-insensitively.""" + if isinstance(value, str): + value_lower = value.lower() + for member in cls: + if member.value == value_lower: + return member + + if value is None: + return cls.NO_OPT + + raise ValueError(f"{value!r} is not a valid {cls.__name__}") diff --git a/fabulous/fabric_generator/gds_generator/flows/full_fabric_flow.py b/fabulous/fabric_generator/gds_generator/flows/full_fabric_flow.py index afc2146a2..029c175b0 100644 --- a/fabulous/fabric_generator/gds_generator/flows/full_fabric_flow.py +++ b/fabulous/fabric_generator/gds_generator/flows/full_fabric_flow.py @@ -218,7 +218,11 @@ def _init_compile(self, fabric: Fabric, proj_dir: Path) -> None: opt_modes, fabric.get_all_unique_tiles() ): io_config_path: Path = tile_type.tileDir.parent / "io_pin_order.yaml" - generate_IO_pin_order_config(fabric, tile_type, io_config_path) + generate_IO_pin_order_config( + tile_type, + io_config_path, + fabric=fabric, + ) base_config_path: Path = ( proj_dir / "Tile" / "include" / "gds_config.yaml" ) diff --git a/fabulous/fabric_generator/gds_generator/flows/plugin_fabric_flow.py b/fabulous/fabric_generator/gds_generator/flows/plugin_fabric_flow.py new file mode 100644 index 000000000..15a695b24 --- /dev/null +++ b/fabulous/fabric_generator/gds_generator/flows/plugin_fabric_flow.py @@ -0,0 +1,140 @@ +"""Config-driven LibreLane plugin adapter for the FABulous fabric flow.""" + +from pathlib import Path + +from librelane.config.variable import Variable +from librelane.flows.flow import Flow, FlowException + +from fabulous.fabric_generator.code_generator.code_generator_Verilog import ( + VerilogCodeGenerator, +) +from fabulous.fabric_generator.gds_generator.flows.fabric_macro_flow import ( + FABulousFabricMacroFlow, + _build_macros, + _collect_fabric_verilog, +) +from fabulous.fabric_generator.gen_fabric.gen_fabric import generateFabric +from fabulous.fabric_generator.parser.parse_csv import parseFabricCSV + + +def _discover_tile_macros( + tile_names: list[str], tile_library_paths: list[Path] +) -> dict[str, Path]: + """Locate each tile's most recent ``RUN_*/final`` directory. + + Walks each ``FABULOUS_TILE_LIBRARY`` root, looks for ``//runs/``, + and picks the most recently modified ``RUN_*/final`` directory. Tiles that + cannot be resolved are omitted; the caller is responsible for surfacing + the error. + """ + discovered: dict[str, Path] = {} + for tile in tile_names: + candidates: list[Path] = [] + for lib in tile_library_paths: + runs = Path(lib) / tile / "runs" + if not runs.is_dir(): + continue + candidates.extend(p for p in runs.glob("RUN_*/final") if p.is_dir()) + if not candidates: + continue + discovered[tile] = max(candidates, key=lambda p: p.stat().st_mtime) + return discovered + + +@Flow.factory.register() +class FABulousFabric(FABulousFabricMacroFlow): + """Drop-in replacement for ``librelane_plugin_fabulous.FABulousFabric``.""" + + config_vars = FABulousFabricMacroFlow.config_vars + [ + Variable( + "FABULOUS_FABRIC_CONFIG", + list[Path], + "Path to the fabric CSV describing the tile map, parameters, and " + "per-tile CSV locations.", + ), + Variable( + "FABULOUS_TILE_LIBRARY", + list[Path], + "List of paths to the tile library roots.", + ), + Variable( + "FABULOUS_TILE_MACROS", + dict[str, Path] | None, + "Mapping of tile name to a previously-hardened macro output " + "directory (containing ``metrics.json``, ``gds/``, ``lef/``, " + "``vh/``, ``nl/``, ``pnl/``, ``spef/``). When omitted, each tile " + "name in the fabric CSV is auto-resolved by scanning the " + "``FABULOUS_TILE_LIBRARY`` paths for a ``/runs/RUN_*/final`` " + "directory and picking the most recently-modified one.", + default=None, + ), + ] + + def __init__( + self, + config: object = None, + *, + name: str | None = None, + design_dir: str | None = None, + pdk: str | None = None, + pdk_root: str | None = None, + **kwargs: object, + ) -> None: + # Skip FABulousFabricMacroFlow.__init__: plugin invocations receive a + # plain LibreLane config and prepare fabric/macros here. + super(FABulousFabricMacroFlow, self).__init__( + config, + name=name, + design_dir=design_dir, + pdk=pdk, + pdk_root=pdk_root, + **kwargs, + ) + + fabric_csv_entries = self.config["FABULOUS_FABRIC_CONFIG"] + if not fabric_csv_entries: + raise FlowException("FABULOUS_FABRIC_CONFIG is empty") + fabric_csv = Path(fabric_csv_entries[0]) + if not fabric_csv.is_file(): + raise FlowException(f"FABULOUS_FABRIC_CONFIG={fabric_csv} does not exist") + self.fabric = parseFabricCSV(fabric_csv) + design_name = self.config.get("DESIGN_NAME") + if design_name is not None: + self.fabric.name = design_name + + writer = VerilogCodeGenerator() + writer.outFileName = fabric_csv.parent / "fabric.v" + generateFabric(writer, self.fabric) + + # todo: use yaml instead of csv + tile_macros_cfg = self.config.get("FABULOUS_TILE_MACROS") or {} + tile_macro_dirs: dict[str, Path] = { + name: Path(path) for name, path in dict(tile_macros_cfg).items() + } + if not tile_macro_dirs: + tile_lib_paths = [ + Path(p) for p in self.config.get("FABULOUS_TILE_LIBRARY") or [] + ] + tile_names = [ + t.name for t in self.fabric.tileDic.values() if not t.partOfSuperTile + ] + tile_macro_dirs = _discover_tile_macros(tile_names, tile_lib_paths) + if not tile_macro_dirs: + raise FlowException( + "FABULOUS_TILE_MACROS is empty and no RUN_*/final directories " + "were found under FABULOUS_TILE_LIBRARY. Either harden the " + "tile macros first or supply FABULOUS_TILE_MACROS explicitly." + ) + + self.macros, self.tile_sizes = _build_macros(tile_macro_dirs) + + if self.config.get("DESIGN_NAME") is None: + self.config = self.config.copy(DESIGN_NAME=self.fabric.name) + + if "VERILOG_FILES" not in self.config or not self.config["VERILOG_FILES"]: + fabric_verilog = _collect_fabric_verilog( + fabric_csv.parent, self.fabric.name + ) + self.config = self.config.copy( + VERILOG_FILES=[str(p) for p in fabric_verilog] + ) diff --git a/fabulous/fabric_generator/gds_generator/flows/plugin_tile_flow.py b/fabulous/fabric_generator/gds_generator/flows/plugin_tile_flow.py new file mode 100644 index 000000000..4dc318ea4 --- /dev/null +++ b/fabulous/fabric_generator/gds_generator/flows/plugin_tile_flow.py @@ -0,0 +1,289 @@ +"""Config-driven LibreLane plugin adapter for the FABulous tile flow.""" + +from decimal import Decimal +from pathlib import Path +from typing import Literal + +from librelane.common import GenericDict +from librelane.config.variable import Variable +from librelane.flows.flow import Flow, FlowException +from librelane.flows.sequential import SequentialFlow +from librelane.state.state import State +from librelane.steps.step import Step + +from fabulous.fabric_definition.define import ConfigBitMode, MultiplexerStyle, Side +from fabulous.fabric_definition.supertile import SuperTile +from fabulous.fabric_definition.tile import Tile +from fabulous.fabric_generator.code_generator.code_generator_Verilog import ( + VerilogCodeGenerator, +) +from fabulous.fabric_generator.gds_generator.define import OptMode +from fabulous.fabric_generator.gds_generator.flows.tile_macro_flow import ( + FABulousTileVerilogMacroFlow, +) +from fabulous.fabric_generator.gds_generator.gen_io_pin_config_yaml import ( + generate_IO_pin_order_config, +) +from fabulous.fabric_generator.gds_generator.helper import ( + get_offset, + get_pitch, + get_routing_obstructions, + round_die_area, +) +from fabulous.fabric_generator.gen_fabric.gen_configmem import generateConfigMem +from fabulous.fabric_generator.gen_fabric.gen_switchmatrix import genTileSwitchMatrix +from fabulous.fabric_generator.gen_fabric.gen_tile import ( + generateSuperTile, + generateTile, +) +from fabulous.fabric_generator.parser.parse_csv import parseSupertilesCSV, parseTilesCSV +from fabulous.fabulous_settings import get_context, init_context + + +@Flow.factory.register() +class FABulousTile(SequentialFlow): + """Drop-in replacement for ``librelane_plugin_fabulous.FABulousTile``.""" + + Steps = FABulousTileVerilogMacroFlow.Steps + + config_vars = FABulousTileVerilogMacroFlow.config_vars + [ + Variable( + "FABULOUS_TILE_DIR", + list[Path], + "Path to the tile directory containing the tile CSV " + "(``.csv``) and its associated Verilog sources. " + "Declared as ``list[Path]`` so LibreLane's ``dir::.`` / ``refg::`` " + "resolvers validate cleanly; only the first element is used.", + ), + Variable( + "FABULOUS_EXTERNAL_SIDE", + Literal["N", "E", "S", "W"] | None, + "The side of the macro at which the external pins are placed. " + "The pin-ordering YAML is generated from the tile's position in " + "the parent fabric when available, so this value is a fallback for " + "standalone plugin tile runs.", + ), + Variable( + "FABULOUS_SUPERTILE", + bool | None, + "If true, ``FABULOUS_TILE_DIR`` refers to a super tile and the " + "flow generates a super-tile wrapper in addition to the " + "per-subtile Verilog.", + default=False, + ), + ] + + gating_config_vars = FABulousTileVerilogMacroFlow.gating_config_vars + + def run( + self, + initial_state: State, + **kwargs: object, + ) -> tuple[State, list[Step]]: + """Prepare FABulous inputs, then run the tile macro pipeline.""" + tile_dir_entries = self.config["FABULOUS_TILE_DIR"] + if not tile_dir_entries: + raise FlowException("FABULOUS_TILE_DIR is empty") + tile_dir = Path(tile_dir_entries[0]).resolve() + if not tile_dir.is_dir(): + raise FlowException(f"FABULOUS_TILE_DIR={tile_dir} is not a directory") + + init_context(api_mode=True) + + tile_name = self.config.get("DESIGN_NAME") or tile_dir.name + is_supertile = bool(self.config.get("FABULOUS_SUPERTILE", False)) + + tile = _parse_plugin_tile(tile_dir, tile_name, is_supertile) + writer = VerilogCodeGenerator() + _emit_tile_verilog(writer, tile, tile_dir) + + pin_yaml = Path(self.run_dir) / f"{tile_name}_io_pin_order.yaml" + external_side_value = self.config.get("FABULOUS_EXTERNAL_SIDE") + try: + external_port_side = ( + Side.SOUTH if external_side_value is None else Side(external_side_value) + ) + except ValueError as exc: + raise FlowException( + f"Invalid FABULOUS_EXTERNAL_SIDE={external_side_value!r}" + ) from exc + generate_IO_pin_order_config( + tile, + pin_yaml, + external_port_side=external_port_side, + ) + + file_list = [str(f) for f in self.config.get("VERILOG_FILES", []) or []] + if isinstance(tile, SuperTile): + concrete_tiles = list(tile.tiles) + generated_files = [tile_dir / f"{tile.name}.v"] + else: + concrete_tiles = [tile] + generated_files = [] + + for concrete_tile in concrete_tiles: + concrete_tile_dir = concrete_tile.tileDir.parent + generated_files.extend( + [ + concrete_tile_dir / f"{concrete_tile.name}.v", + concrete_tile_dir / f"{concrete_tile.name}_switch_matrix.v", + concrete_tile_dir / f"{concrete_tile.name}_ConfigMem.v", + ] + ) + for bel in concrete_tile.bels: + file_list.append(str(bel.src)) + + file_list.extend(str(f) for f in generated_files if f.exists()) + file_list = list(dict.fromkeys(file_list)) + if models_pack := get_context().models_pack: + file_list.append(str(models_pack.resolve())) + + if isinstance(tile, SuperTile): + logical_width = tile.max_width + logical_height = tile.max_height + else: + logical_width = 1 + logical_height = 1 + + self.config = self.config.copy( + DESIGN_NAME=tile_name, + VERILOG_FILES=file_list, + FABULOUS_IO_PIN_ORDER_CFG=str(pin_yaml), + FABULOUS_TILE_LOGICAL_WIDTH=logical_width, + FABULOUS_TILE_LOGICAL_HEIGHT=logical_height, + FABULOUS_OPT_MODE=OptMode.NO_OPT, + ) + + self.config = _apply_tile_die_area_config(self.config, tile) + self.config = round_die_area(self.config) + + if ( + "ROUTING_OBSTRUCTIONS" not in self.config + or self.config["ROUTING_OBSTRUCTIONS"] is None + ) and self.config["ROUTING_OBSTRUCTIONS"] is not False: + self.config = self.config.copy( + ROUTING_OBSTRUCTIONS=get_routing_obstructions(self.config) + ) + + return super().run(initial_state, **kwargs) + + +def _emit_tile_verilog( + writer: VerilogCodeGenerator, + tile: Tile | SuperTile, + tile_dir: Path, +) -> None: + """Generate switch-matrix, config-mem, and tile Verilog into ``tile_dir``.""" + if isinstance(tile, SuperTile): + for sub_tile in tile.tiles: + sub_dir = sub_tile.tileDir.parent + _emit_regular_tile_verilog(writer, sub_tile, sub_dir) + writer.outFileName = tile_dir / f"{tile.name}.v" + generateSuperTile( + writer, + tile, + disable_user_clk=True, + config_bit_mode=ConfigBitMode.FRAME_BASED, + ) + return + + _emit_regular_tile_verilog(writer, tile, tile_dir) + + +def _emit_regular_tile_verilog( + writer: VerilogCodeGenerator, + tile: Tile, + tile_dir: Path, +) -> None: + """Generate Verilog artefacts for one concrete tile.""" + switch_matrix_debug_signal = get_context().switch_matrix_debug_signal + + writer.outFileName = tile_dir / f"{tile.name}_switch_matrix.v" + genTileSwitchMatrix( + writer, + tile, + switch_matrix_debug_signal, + config_bit_mode=ConfigBitMode.FRAME_BASED, + multiplexer_style=MultiplexerStyle.CUSTOM, + default_pip_delay=80, + ) + writer.outFileName = tile_dir / f"{tile.name}_ConfigMem.v" + generateConfigMem(writer, tile, tile_dir / f"{tile.name}_ConfigMem.csv") + writer.outFileName = tile_dir / f"{tile.name}.v" + generateTile( + writer, + tile, + disable_user_clk=True, + config_bit_mode=ConfigBitMode.FRAME_BASED, + ) + + +def _apply_tile_die_area_config( + config: GenericDict[str, object], + tile_type: Tile | SuperTile, +) -> GenericDict[str, object]: + """Populate plugin tile ``DIE_AREA`` using patchable local helper imports.""" + x_pitch, y_pitch = get_pitch(config) + get_offset(config) + min_x, min_y = tile_type.get_min_die_area( + x_pitch, + y_pitch, + config.get("IO_PIN_V_THINKNESS_MULT", Decimal(1)), + config.get("IO_PIN_H_THINKNESS_MULT", Decimal(1)), + x_pitch, + y_pitch, + ) + + if config["FABULOUS_IGNORE_DEFAULT_DIE_AREA"] or config.get("DIE_AREA") is None: + return config.copy(DIE_AREA=(0, 0, min_x, min_y)) + + _, _, width, height = config["DIE_AREA"] + if width < min_x or height < min_y: + raise FlowException( + f"DIE_AREA ({width}, {height}) is smaller than the " + f"minimum required area ({min_x}, {min_y}) for the " + f"tile {tile_type.name}. Please update the DIE_AREA " + ) + return config + + +def _parse_plugin_tile( + tile_dir: Path, + tile_name: str, + is_supertile: bool, +) -> Tile | SuperTile: + """Parse the plugin tile directory without constructing a Fabric.""" + tile_csv = tile_dir / f"{tile_name}.csv" + if not tile_csv.exists(): + raise FlowException(f"Tile CSV {tile_csv} does not exist") + + if not is_supertile: + tiles, _ = parseTilesCSV(tile_csv) + for tile in tiles: + if tile.name == tile_name: + return tile + raise FlowException(f"Tile {tile_name!r} not found in {tile_csv}") + + tile_dic: dict[str, Tile] = {} + subtile_names: list[str] = [] + with tile_csv.open("r", encoding="utf-8") as f: + f.readline() + for raw in f: + line = raw.strip() + if "EndSuperTILE" in line: + break + for cell in line.split(","): + name = cell.strip() + if name: + subtile_names.append(name) + + for subtile_name in subtile_names: + subtile_csv = tile_dir / subtile_name / f"{subtile_name}.csv" + tiles, _ = parseTilesCSV(subtile_csv) + tile_dic.update({tile.name: tile for tile in tiles}) + + supertiles = parseSupertilesCSV(tile_csv, tile_dic) + for supertile in supertiles: + if supertile.name == tile_name: + return supertile + raise FlowException(f"SuperTile {tile_name!r} not found in {tile_csv}") diff --git a/fabulous/fabric_generator/gds_generator/script/fabric_io_place.py b/fabulous/fabric_generator/gds_generator/script/fabric_io_place.py index 41d9a130a..bc2fc8ac3 100644 --- a/fabulous/fabric_generator/gds_generator/script/fabric_io_place.py +++ b/fabulous/fabric_generator/gds_generator/script/fabric_io_place.py @@ -1,204 +1,61 @@ -"""Place I/O pins on the die edge based on mTerm positions.""" - -import logging -import math -from decimal import Decimal +"""Place fabric-level signal BPins by stamping connected ITerm mTerm geometry.""" import click import odb # type: ignore[import] -from librelane.logging.logger import warn +from librelane.logging.logger import info, warn from librelane.scripts.odbpy.reader import click_odb -from fabulous.fabric_generator.gds_generator.script.odb_protocol import ( - OdbReaderLike, - odbRectLike, -) +from fabulous.fabric_generator.gds_generator.script.odb_protocol import OdbReaderLike @click.command() -@click.option( - "-v", - "--ver-length", - default=None, - type=float, - help="Length for pins with N/S orientations in microns.", -) -@click.option( - "-h", - "--hor-length", - default=None, - type=float, - help="Length for pins with E/S orientations in microns.", -) -@click.option( - "-V", - "--ver-layer", - required=True, - help="Name of metal layer to place vertical pins on.", -) -@click.option( - "-H", - "--hor-layer", - required=True, - help="Name of metal layer to place horizontal pins on.", -) -@click.option( - "--hor-extension", - default=0, - type=float, - help="Extension for horizontal pins in microns.", -) -@click.option( - "--ver-extension", - default=0, - type=float, - help="Extension for vertical pins in microns.", -) -@click.option( - "--ver-width-mult", default=2, type=float, help="Multiplier for vertical pins." -) -@click.option( - "--hor-width-mult", default=2, type=float, help="Multiplier for horizontal pins." -) -@click.option( - "--verbose/--no-verbose", - default=False, - help="Enable verbose (DEBUG) logging output.", -) @click_odb -def io_place( - reader: OdbReaderLike, - ver_layer: str, - hor_layer: str, - ver_width_mult: float, - hor_width_mult: float, - hor_length: float | None, - ver_length: float | None, - hor_extension: float, - ver_extension: float, - verbose: bool, -) -> None: - """Place each BTerm's BPin on the die edge corresponding to the mTerm's position. - - Determines the side by checking where the mTerm is positioned relative to the master - tile center. If the mTerm is on the north side of the master, place the BPin on the - north edge of the die, and so on. Falls back to distance-based placement if mTerm - information is unavailable. - """ - if verbose: - logging.getLogger().setLevel(logging.DEBUG) - - micron_in_units = reader.dbunits - - h_extension = int(micron_in_units * hor_extension) - v_extension = int(micron_in_units * ver_extension) - - if h_extension < 0: - h_extension = 0 - - if v_extension < 0: - v_extension = 0 - - h_layer = reader.tech.findLayer(hor_layer) - v_layer = reader.tech.findLayer(ver_layer) - - h_width = int(Decimal(hor_width_mult) * h_layer.getWidth()) - v_width = int(Decimal(ver_width_mult) * v_layer.getWidth()) - - if hor_length is not None: - h_length = int(micron_in_units * hor_length) - else: - h_length = max( - int( - math.ceil( - h_layer.getArea() * micron_in_units * micron_in_units / h_width - ) - ), - h_width, - ) - - if ver_length is not None: - v_length = int(micron_in_units * ver_length) - else: - v_length = max( - int( - math.ceil( - v_layer.getArea() * micron_in_units * micron_in_units / v_width - ) - ), - v_width, - ) - - # Die area - die = reader.block.getDieArea() - llx, lly, urx, ury = die.xMin(), die.yMin(), die.xMax(), die.yMax() - - bterms = [ - bterm - for bterm in reader.block.getBTerms() - if bterm.getSigType() not in ["POWER", "GROUND"] - ] +def io_place(reader: OdbReaderLike) -> None: + """Stamp signal BTerm's BPin with the geometry of its driving/sinking ITerms.""" + stamped = 0 + deleted = 0 + for bterm in list(reader.block.getBTerms()): + if bterm.getSigType() in ("POWER", "GROUND"): + continue - for bterm in bterms: net = bterm.getNet() - iterms = net.getITerms() - if not iterms: + if net is None: + continue + + existing = bterm.getBPins() + if existing: warn( - f"Net {net.getName()} has no ITerms for BTerm " - f"{bterm.getName()}; skipping" + f"BTerm {bterm.getName()} already has {len(existing)} BPin(s); " + "leaving them in place." ) continue - iterm = iterms[0] - ibox: odbRectLike = iterm.getBBox() - cx = ibox.xCenter() - cy = ibox.yCenter() - - # Get mTerm bbox to determine which side of the master tile it's on - mterm = iterm.getMTerm() - master = mterm.getMaster() - # Use mTerm bbox position relative to master to determine side - side = None - # Get the first mPin's geometry bbox - mterm_bbox: odbRectLike = mterm.getBBox() - - if mterm_bbox.xMin() == 0: - side = "WEST" - if mterm_bbox.xMax() == master.getWidth(): - side = "EAST" - if mterm_bbox.yMin() == 0: - side = "SOUTH" - if mterm_bbox.yMax() == master.getHeight(): - side = "NORTH" - - # Prepare or reuse BPin - pins = bterm.getBPins() - if len(pins) > 0: - warn(f"{bterm.getName()} already has shapes. Modifying existing shape.") - assert len(pins) == 1 - pin_bpin = pins[0] - else: - pin_bpin = odb.dbBPin_create(bterm) - pin_bpin.setPlacementStatus("PLACED") + iterms = list(net.getITerms()) + if not iterms: + odb.dbBTerm.destroy(bterm) + if not net.getITerms() and not net.getBTerms(): + odb.dbNet.destroy(net) + deleted += 1 + continue - if side in ("NORTH", "SOUTH"): - # Vertical pin on top/bottom, align X to ITerm center - rect = odb.Rect(0, 0, int(v_width), int(v_length + v_extension)) - # Compute edge Y position - y = ury - int(v_length) if side == "NORTH" else lly - int(v_extension) - # Clamp X inside die for the body width - x = int(max(llx, min(cx - v_width // 2, urx - v_width))) - rect.moveTo(x, int(y)) - odb.dbBox_create(pin_bpin, v_layer, *rect.ll(), *rect.ur()) - else: - # Horizontal pin on left/right, align Y to ITerm center - rect = odb.Rect(0, 0, int(h_length + h_extension), int(h_width)) - # Compute edge X position - x = urx - int(h_length) if side == "EAST" else llx - int(h_extension) - # Clamp Y inside die for the body width - y = int(max(lly, min(cy - h_width // 2, ury - h_width))) - rect.moveTo(int(x), y) - odb.dbBox_create(pin_bpin, h_layer, *rect.ll(), *rect.ur()) + bpin = odb.dbBPin_create(bterm) + for iterm in iterms: + inst = iterm.getInst() + inst_x, inst_y = inst.getLocation() + for mpin in iterm.getMTerm().getMPins(): + for db in mpin.getGeometry(): + odb.dbBox_create( + bpin, + db.getTechLayer(), + inst_x + db.xMin(), + inst_y + db.yMin(), + inst_x + db.xMax(), + inst_y + db.yMax(), + ) + bpin.setPlacementStatus("FIRM") + stamped += 1 + + info(f"Stamped {stamped} signal BPins; deleted {deleted} orphan BTerms.") if __name__ == "__main__": diff --git a/fabulous/fabric_generator/gds_generator/steps/condition_magic_drc.py b/fabulous/fabric_generator/gds_generator/steps/condition_magic_drc.py index 078d9f4b3..0e4234ffb 100644 --- a/fabulous/fabric_generator/gds_generator/steps/condition_magic_drc.py +++ b/fabulous/fabric_generator/gds_generator/steps/condition_magic_drc.py @@ -12,7 +12,7 @@ class ConditionalMagicDRC(DRC): """Run Magic DRC if klayout DRC errors are found.""" - id = "Condition.MagicDRC" + id = "Magic.DRC" name = "Magic DRC Check" long_name = "KLayout DRC Check with Conditional Flow Control" diff --git a/fabulous/fabric_generator/gds_generator/steps/fabric_IO_placement.py b/fabulous/fabric_generator/gds_generator/steps/fabric_IO_placement.py index 535168711..26227c5b0 100644 --- a/fabulous/fabric_generator/gds_generator/steps/fabric_IO_placement.py +++ b/fabulous/fabric_generator/gds_generator/steps/fabric_IO_placement.py @@ -3,9 +3,6 @@ from importlib import resources from librelane.state.state import State -from librelane.steps.common_variables import ( - io_layer_variables, -) from librelane.steps.odb import OdbpyStep from librelane.steps.step import ( MetricsUpdate, @@ -16,20 +13,12 @@ @Step.factory.register() class FABulousFabricIOPlacement(OdbpyStep): - """Place I/O pins using a custom script. This is the fabric-level version. - - This step uses a custom Python script to place I/O pins according to the macro pin - coordinates. This is intended for use in the stitching flow to place top level macro - I/Os. This step will just line up to the master driver terminals and does not care - if the pin placement is pitch aligned. - """ + """Stamp fabric-level signal BPins onto their driving/sinking macro pins.""" id = "Odb.FABulousFabricIOPlacement" name = "FABulous fabric I/O Placement" long_name = "FABulous fabric I/O Pin Placement Script" - config_vars = io_layer_variables - def get_script_path(self) -> str: """Get the path to the I/O placement script.""" return str( @@ -37,36 +26,6 @@ def get_script_path(self) -> str: / "fabric_io_place.py" ) - def get_command(self) -> list[str]: - """Get the command to run the I/O placement script.""" - length_args = [] - if self.config["IO_PIN_V_LENGTH"] is not None: - length_args += ["--ver-length", self.config["IO_PIN_V_LENGTH"]] - if self.config["IO_PIN_H_LENGTH"] is not None: - length_args += ["--hor-length", self.config["IO_PIN_H_LENGTH"]] - - return ( - super().get_command() - + [ - "--hor-layer", - self.config["IO_PIN_H_LAYER"], - "--ver-layer", - self.config["IO_PIN_V_LAYER"], - "--hor-width-mult", - str(self.config["IO_PIN_H_THICKNESS_MULT"]), - "--ver-width-mult", - str(self.config["IO_PIN_V_THICKNESS_MULT"]), - "--hor-extension", - str(self.config["IO_PIN_H_EXTENSION"]), - "--ver-extension", - str(self.config["IO_PIN_V_EXTENSION"]), - ] - + length_args - ) - def run(self, state_in: State, **kwargs: dict) -> tuple[ViewsUpdate, MetricsUpdate]: - """Place I/O pins using a custom script. - - This is the fabric-level version. - """ + """Place I/O pins using the upstream-compatible stamping strategy.""" return super().run(state_in, **kwargs) diff --git a/fabulous/fabric_generator/gds_generator/steps/global_tile_opitmisation.py b/fabulous/fabric_generator/gds_generator/steps/global_tile_opitmisation.py index b22cb7bf7..807876741 100644 --- a/fabulous/fabric_generator/gds_generator/steps/global_tile_opitmisation.py +++ b/fabulous/fabric_generator/gds_generator/steps/global_tile_opitmisation.py @@ -22,8 +22,8 @@ from pymoo.termination.max_gen import MaximumGenerationTermination from fabulous.fabric_definition.fabric import Fabric +from fabulous.fabric_generator.gds_generator.define import OptMode from fabulous.fabric_generator.gds_generator.helper import round_up_decimal -from fabulous.fabric_generator.gds_generator.steps.tile_optimisation import OptMode if TYPE_CHECKING: from fabulous.fabric_definition.tile import Tile diff --git a/fabulous/fabric_generator/gds_generator/steps/tile_IO_placement.py b/fabulous/fabric_generator/gds_generator/steps/tile_IO_placement.py index beb35504d..342282755 100644 --- a/fabulous/fabric_generator/gds_generator/steps/tile_IO_placement.py +++ b/fabulous/fabric_generator/gds_generator/steps/tile_IO_placement.py @@ -1,7 +1,7 @@ """Custom IO placement step for FABulous tiles.""" from importlib import resources -from typing import Literal +from typing import Literal, Optional from librelane.common.types import Path from librelane.config.variable import Variable @@ -36,8 +36,9 @@ class FABulousTileIOPlacement(OdbpyStep): config_vars = io_layer_variables + [ Variable( "FABULOUS_IO_PIN_ORDER_CFG", - Path, + Optional[Path], # noqa: UP045 librelane issue "Path to a custom pin configuration file.", + default=None, ), Variable( "ERRORS_ON_UNMATCHED_IO", @@ -69,11 +70,17 @@ def get_command(self) -> list[str]: if self.config["IO_PIN_H_LENGTH"] is not None: length_args += ["--hor-length", self.config["IO_PIN_H_LENGTH"]] + io_pin_order_cfg = self.config["FABULOUS_IO_PIN_ORDER_CFG"] + if io_pin_order_cfg is None: + raise ValueError( + "FABULOUS_IO_PIN_ORDER_CFG must be set before IO placement" + ) + return ( super().get_command() + [ "--config", - self.config["FABULOUS_IO_PIN_ORDER_CFG"], + io_pin_order_cfg, "--hor-layer", self.config["IO_PIN_H_LAYER"], "--ver-layer", diff --git a/fabulous/fabric_generator/gds_generator/steps/tile_optimisation.py b/fabulous/fabric_generator/gds_generator/steps/tile_optimisation.py index edec67b4f..b46ca1b4c 100644 --- a/fabulous/fabric_generator/gds_generator/steps/tile_optimisation.py +++ b/fabulous/fabric_generator/gds_generator/steps/tile_optimisation.py @@ -1,7 +1,6 @@ """Tile size optimisation step for FABulous fabric generator.""" from decimal import Decimal -from enum import StrEnum from typing import cast from librelane.config.variable import Variable @@ -13,6 +12,7 @@ from librelane.steps import openroad as OpenROAD from librelane.steps.step import MetricsUpdate, Step, ViewsUpdate +from fabulous.fabric_generator.gds_generator.define import OptMode from fabulous.fabric_generator.gds_generator.helper import ( get_pitch, get_routing_obstructions, @@ -28,31 +28,6 @@ ) from fabulous.fabric_generator.gds_generator.steps.while_step import WhileStep - -class OptMode(StrEnum): - """Optimisation modes for tile size finding.""" - - FIND_MIN_WIDTH = "find_min_width" - FIND_MIN_HEIGHT = "find_min_height" - BALANCE = "balance" - LARGE = "large" - NO_OPT = "no_opt" - - @classmethod - def _missing_(cls, value: object) -> "OptMode": - """Look up an OptMode member case-insensitively.""" - if isinstance(value, str): - value_lower = value.lower() - for member in cls: - if member.value == value_lower: - return member - - if value is None: - return cls.NO_OPT - - raise ValueError(f"{value!r} is not a valid {cls.__name__}") - - var = [ Variable( "FABULOUS_OPTIMISATION_WIDTH_STEP_COUNT", @@ -184,6 +159,10 @@ def pre_iteration_callback(self, pre_iteration: State) -> State: """Pre iteration callback.""" if self.config["FABULOUS_OPT_MODE"] == OptMode.NO_OPT: self.config = self.config.copy(DRT_OPT_ITERS=64) + self.config = self.config.copy(ROUTING_OBSTRUCTIONS=None) + self.config = self.config.copy( + ROUTING_OBSTRUCTIONS=get_routing_obstructions(self.config) + ) return pre_iteration die_area_raw: tuple[Decimal, Decimal, Decimal, Decimal] = self.config.get( "DIE_AREA", None diff --git a/fabulous/fabric_generator/gen_fabric/gen_fabric.py b/fabulous/fabric_generator/gen_fabric/gen_fabric.py index 749bc5a86..989bdcc4d 100644 --- a/fabulous/fabric_generator/gen_fabric/gen_fabric.py +++ b/fabulous/fabric_generator/gen_fabric/gen_fabric.py @@ -95,6 +95,7 @@ def generateFabric(writer: CodeGenerator, fabric: Fabric) -> None: f"{Path(writer.outFileName).parent.parent}/Tile/{t}/{t}.vhdl" ) added.add(t) + else: writer.addComponentDeclarationForFile( f"{Path(writer.outFileName).parent.parent}/Tile/{name}/{name}.vhdl" @@ -228,97 +229,91 @@ def generateFabric(writer: CodeGenerator, fabric: Fabric) -> None: # use the offset to find all the related tile input, output signal # if is a normal tile then the offset is (0, 0) for i, j in tileLocationOffset: + here = fabric.tile[y + j][x + i] + in_super = here.partOfSuperTile + + def _local_names( + ports: list, _i: int = i, _j: int = j, in_super: bool = in_super + ) -> list[str]: + """Return local port names.""" + return ( + [f"Tile_X{_i}Y{_j}_{p.name}" for p in ports] + if in_super + else [p.name for p in ports] + ) + # input connection from north side of the south tile - if ( - 0 <= y + 1 < len(fabric.tile) - and fabric.tile[y + j + 1][x + i] is not None - and (x + i, y + j + 1) not in superTileLoc - ): - if fabric.tile[y + j][x + i].partOfSuperTile: - northPorts = [ - f"Tile_X{i}Y{j}_{p.name}" - for p in fabric.tile[y + j][x + i].getNorthPorts(IO.INPUT) + # (NORTH-direction wires entering this tile from south fabric neighbour) + south_neighbor_internal = (x + i, y + j + 1) in superTileLoc + if not south_neighbor_internal: + northPorts = _local_names(here.getNorthPorts(IO.INPUT)) + if ( + 0 <= y + 1 < len(fabric.tile) + and fabric.tile[y + j + 1][x + i] is not None + ): + northInput = [ + f"Tile_X{x + i}Y{y + j + 1}_{p.name}" + for p in fabric.tile[y + j + 1][x + i].getNorthPorts( + IO.OUTPUT + ) ] + portsPairs += list(zip(northPorts, northInput, strict=False)) else: - northPorts = [ - i.name - for i in fabric.tile[y + j][x + i].getNorthPorts(IO.INPUT) - ] - - northInput = [ - f"Tile_X{x + i}Y{y + j + 1}_{p.name}" - for p in fabric.tile[y + j + 1][x + i].getNorthPorts(IO.OUTPUT) - ] - portsPairs += list(zip(northPorts, northInput, strict=False)) + portsPairs += [(p, "") for p in northPorts] # input connection from east side of the west tile - if ( - 0 <= x - 1 < len(fabric.tile[0]) - and fabric.tile[y + j][x + i - 1] is not None - and (x + i - 1, y + j) not in superTileLoc - ): - if fabric.tile[y + j][x + i].partOfSuperTile: - eastPorts = [ - f"Tile_X{i}Y{j}_{p.name}" - for p in fabric.tile[y + j][x + i].getEastPorts(IO.INPUT) + west_neighbor_internal = (x + i - 1, y + j) in superTileLoc + if not west_neighbor_internal: + eastPorts = _local_names(here.getEastPorts(IO.INPUT)) + if ( + 0 <= x - 1 < len(fabric.tile[0]) + and fabric.tile[y + j][x + i - 1] is not None + ): + eastInput = [ + f"Tile_X{x + i - 1}Y{y + j}_{p.name}" + for p in fabric.tile[y + j][x + i - 1].getEastPorts( + IO.OUTPUT + ) ] + portsPairs += list(zip(eastPorts, eastInput, strict=False)) else: - eastPorts = [ - i.name - for i in fabric.tile[y + j][x + i].getEastPorts(IO.INPUT) - ] - - eastInput = [ - f"Tile_X{x + i - 1}Y{y + j}_{p.name}" - for p in fabric.tile[y + j][x + i - 1].getEastPorts(IO.OUTPUT) - ] - portsPairs += list(zip(eastPorts, eastInput, strict=False)) + portsPairs += [(p, "") for p in eastPorts] # input connection from south side of the north tile - if ( - 0 <= y - 1 < len(fabric.tile) - and fabric.tile[y + j - 1][x + i] is not None - and (x + i, y + j - 1) not in superTileLoc - ): - if fabric.tile[y + j][x + i].partOfSuperTile: - southPorts = [ - f"Tile_X{i}Y{j}_{p.name}" - for p in fabric.tile[y + j][x + i].getSouthPorts(IO.INPUT) + north_neighbor_internal = (x + i, y + j - 1) in superTileLoc + if not north_neighbor_internal: + southPorts = _local_names(here.getSouthPorts(IO.INPUT)) + if ( + 0 <= y - 1 < len(fabric.tile) + and fabric.tile[y + j - 1][x + i] is not None + ): + southInput = [ + f"Tile_X{x + i}Y{y + j - 1}_{p.name}" + for p in fabric.tile[y + j - 1][x + i].getSouthPorts( + IO.OUTPUT + ) ] + portsPairs += list(zip(southPorts, southInput, strict=False)) else: - southPorts = [ - i.name - for i in fabric.tile[y + j][x + i].getSouthPorts(IO.INPUT) - ] - - southInput = [ - f"Tile_X{x + i}Y{y + j - 1}_{p.name}" - for p in fabric.tile[y + j - 1][x + i].getSouthPorts(IO.OUTPUT) - ] - portsPairs += list(zip(southPorts, southInput, strict=False)) + portsPairs += [(p, "") for p in southPorts] # input connection from west side of the east tile - if ( - 0 <= x + 1 < len(fabric.tile[0]) - and fabric.tile[y + j][x + i + 1] is not None - and (x + i + 1, y + j) not in superTileLoc - ): - if fabric.tile[y + j][x + i].partOfSuperTile: - westPorts = [ - f"Tile_X{i}Y{j}_{p.name}" - for p in fabric.tile[y + j][x + i].getWestPorts(IO.INPUT) + east_neighbor_internal = (x + i + 1, y + j) in superTileLoc + if not east_neighbor_internal: + westPorts = _local_names(here.getWestPorts(IO.INPUT)) + if ( + 0 <= x + 1 < len(fabric.tile[0]) + and fabric.tile[y + j][x + i + 1] is not None + ): + westInput = [ + f"Tile_X{x + i + 1}Y{y + j}_{p.name}" + for p in fabric.tile[y + j][x + i + 1].getWestPorts( + IO.OUTPUT + ) ] + portsPairs += list(zip(westPorts, westInput, strict=False)) else: - westPorts = [ - i.name - for i in fabric.tile[y + j][x + i].getWestPorts(IO.INPUT) - ] - - westInput = [ - f"Tile_X{x + i + 1}Y{y + j}_{p.name}" - for p in fabric.tile[y + j][x + i + 1].getWestPorts(IO.OUTPUT) - ] - portsPairs += list(zip(westPorts, westInput, strict=False)) + portsPairs += [(p, "") for p in westPorts] # output signal name is same as the output port name if superTile: @@ -534,12 +529,12 @@ def generateFabric(writer: CodeGenerator, fabric: Fabric) -> None: emulateParamPairs.append( ("Emulate_Bitstream", f"`Tile_X{x}Y{y}_Emulate_Bitstream") ) - writer.addInstantiation( compName=name, compInsName=f"Tile_X{x}Y{y}_{name}", portsPairs=portsPairs, emulateParamPairs=emulateParamPairs, + add_keep=True, ) writer.addDesignDescriptionEnd() writer.writeToFile() diff --git a/fabulous/fabric_generator/parser/parse_csv.py b/fabulous/fabric_generator/parser/parse_csv.py index 06852d2fb..1a95fdcbd 100644 --- a/fabulous/fabric_generator/parser/parse_csv.py +++ b/fabulous/fabric_generator/parser/parse_csv.py @@ -405,6 +405,9 @@ def parseTilesCSV(fileName: Path) -> tuple[list[Tile], list[tuple[str, str]]]: if muxSize >= 2: configBit += (muxSize - 1).bit_length() + # sorting to en + ports.sort(key=lambda p: p.wireDirection) + new_tiles.append( Tile( name=tileName, diff --git a/pyproject.toml b/pyproject.toml index 5ac7280c5..ceb57a21c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -111,6 +111,7 @@ exclude = [ "scripts", ".claude", "fabulous_nix", + "librelane_plugin_fabulous", ".codex", "ttsim*", ] @@ -143,6 +144,7 @@ omit = [ "docs/*", "build_hooks.py", "fabulous_nix", + "librelane_plugin_fabulous", ] show_missing = true skip_covered = true diff --git a/tests/assets/librelane_plugin/README.md b/tests/assets/librelane_plugin/README.md new file mode 100644 index 000000000..985562279 --- /dev/null +++ b/tests/assets/librelane_plugin/README.md @@ -0,0 +1,49 @@ +# LibreLane plugin nightly assets + +Vendored assets used by `.github/workflows/librelane-plugin.yml` to drive +end-to-end runs of `FABulousTile` and `FABulousFabric` (the LibreLane plugin +entry points re-exported from `librelane_plugin_fabulous`). + +## Layout + +The directory tree mirrors the upstream `mole99/fabulous-tiles` repository so +the relative paths inside the vendored CSV/YAML files (`../common/`, +`../../../primitives/...`, `../../../models_pack.v`) resolve unmodified. + +``` +tests/assets/librelane_plugin/ +├── README.md # this file +├── models_pack.v # top-level Verilog +├── custom.v # top-level Verilog +├── primitives/ # BEL libraries +│ └── FABULOUS_LC/fabulous/FABULOUS_LC.v +├── tiles/classic/ # tile library +│ ├── common/ # shared per-direction wiring +│ │ ├── Base.csv +│ │ └── Base.list +│ └── LUT4x8_ha/ # the one tile we harden +│ ├── LUT4x8_ha.csv +│ ├── LUT4x8_ha_switch_matrix.list +│ ├── config.yaml # FABulousTile flow config +│ └── README_old.md +└── fabrics/ + └── synthetic_lut4x8_ha_10x10/ + ├── synthetic_lut4x8_ha_10x10.csv # 10×10 grid, all LUT4x8_ha + └── config.yaml # FABulousFabric flow config +``` + +## Sources + +| Path | Upstream repo | Commit pinned | +| --------------------------- | ------------------------------------------------------------ | ------------------------------------------ | +| `tiles/`, `primitives/`, `models_pack.v`, `custom.v` | https://github.com/mole99/fabulous-tiles | `964c1ab38a4e0a85c190999fbba7dc2fa7aa667c` | +| `fabrics/synthetic_lut4x8_ha_10x10/` (synthetic, derived from upstream layout) | https://github.com/mole99/fabulous-fabrics | `bb5d98490fbc99f1f0662f072d3819b7a9b2d663` | + +The fabric is **synthetic** rather than vendored verbatim: the upstream +`classic_fabric_10x10` references 16 distinct tile types, but the nightly +hardens only `LUT4x8_ha`, so the fabric grid is filled entirely with that one +tile to exercise the stitching code path. + +When the rest of the upstream tile library is hardenable in CI, this directory +should be replaced with a runtime clone of `mole99/fabulous-tiles` and +`mole99/fabulous-fabrics` and the synthetic fabric removed. diff --git a/tests/assets/librelane_plugin/fabrics/synthetic_lut4x8_ha_10x10/config.yaml b/tests/assets/librelane_plugin/fabrics/synthetic_lut4x8_ha_10x10/config.yaml new file mode 100644 index 000000000..9bda961e9 --- /dev/null +++ b/tests/assets/librelane_plugin/fabrics/synthetic_lut4x8_ha_10x10/config.yaml @@ -0,0 +1,33 @@ +meta: + version: 2 + flow: FABulousFabric + +GRT_ANTENNA_REPAIR_ITERS: 0 +DRT_ANTENNA_REPAIR_ITERS: 0 + +DESIGN_NAME: synthetic_lut4x8_ha_10x10 +VERILOG_FILES: [] + +MAGIC_EXT_UNIQUE: notopports + +GRT_ALLOW_CONGESTION: true + +# Boundary tile instances are emitted with `.PORT()` empty connections for +# inputs that point off-fabric (no neighbour to drive them). Tell the +# disconnected-pins checker this is intentional for the LUT4x8_ha module. +IGNORE_DISCONNECTED_MODULES: + - LUT4x8_ha + +FABULOUS_FABRIC_CONFIG: dir::synthetic_lut4x8_ha_10x10.csv +FABULOUS_TILE_LIBRARY: dir::../../tiles/classic/ + +FABULOUS_TILE_SPACING: [ 0, 0 ] +FABULOUS_HALO_SPACING: [ 0, 0, 0, 0 ] + +IO_PIN_H_LENGTH: null +IO_PIN_V_LENGTH: null + +PRIMARY_GDSII_STREAMOUT_TOOL: klayout + +pdk::ihp-sg13*: + RT_MAX_LAYER: TopMetal1 diff --git a/tests/assets/librelane_plugin/fabrics/synthetic_lut4x8_ha_10x10/synthetic_lut4x8_ha_10x10.csv b/tests/assets/librelane_plugin/fabrics/synthetic_lut4x8_ha_10x10/synthetic_lut4x8_ha_10x10.csv new file mode 100644 index 000000000..af540dd64 --- /dev/null +++ b/tests/assets/librelane_plugin/fabrics/synthetic_lut4x8_ha_10x10/synthetic_lut4x8_ha_10x10.csv @@ -0,0 +1,23 @@ +FabricBegin,,,,,,,,, +NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL +NULL,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,NULL +NULL,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,NULL +NULL,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,NULL +NULL,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,NULL +NULL,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,NULL +NULL,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,NULL +NULL,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,NULL +NULL,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,LUT4x8_ha,NULL +NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL,NULL +FabricEnd,,,,,,,,, +,,,,,,,,, +ParametersBegin,,,,,,,,, +ConfigBitMode,frame_based,,,,,,,, +GenerateDelayInSwitchMatrix,80,,,,,,,, +MultiplexerStyle,custom,,,,,,,, +SuperTileEnable,FALSE,,,,,,,, +DisableUserCLK,TRUE,,,,,,,, +,,,,,,,,, +Tile,../../tiles/classic/LUT4x8_ha/LUT4x8_ha.csv,,,,,,,, +,,,,,,,,, +ParametersEnd,,,,,,,,, diff --git a/tests/assets/librelane_plugin/models_pack.v b/tests/assets/librelane_plugin/models_pack.v new file mode 120000 index 000000000..baf58bd04 --- /dev/null +++ b/tests/assets/librelane_plugin/models_pack.v @@ -0,0 +1 @@ +../../../fabulous/fabric_files/FABulous_project_template_verilog/Fabric/models_pack.v \ No newline at end of file diff --git a/tests/assets/librelane_plugin/primitives/FABULOUS_LC/fabulous/FABULOUS_LC.v b/tests/assets/librelane_plugin/primitives/FABULOUS_LC/fabulous/FABULOUS_LC.v new file mode 100644 index 000000000..7b757b141 --- /dev/null +++ b/tests/assets/librelane_plugin/primitives/FABULOUS_LC/fabulous/FABULOUS_LC.v @@ -0,0 +1,137 @@ +// Copyright 2021 University of Manchester +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CoNDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +module LUTK #( + parameter K=4, + parameter LUT_ENTRIES = 2**K +)( + input [K-1:0] I, + input [LUT_ENTRIES-1:0] INIT, + output O +); + + generate + + if (K == 1) begin : LUT1 + assign O = I ? INIT[1] : INIT[0]; + end else begin : split_LUT + wire lower_O, upper_O; + + parameter LUT_ENTRIES_HALF = 2**(K-1); + + LUTK #( + .K (K-1) + ) LUTK_lower ( + .I (I[K-2:0]), + .INIT (INIT[LUT_ENTRIES_HALF-1:0]), + .O (lower_O) + ); + + LUTK #( + .K (K-1) + ) LUTK_upper ( + .I (I[K-2:0]), + .INIT (INIT[LUT_ENTRIES-1:LUT_ENTRIES_HALF]), + .O (upper_O) + ); + + assign O = I[K-1] ? upper_O : lower_O; + end + + endgenerate + +endmodule + +// Note: nextpnr also has support for ASYNC_SR and NEG_CLK +(*FABulous, BelMap, + INIT=0, + INIT_1=1, + INIT_2=2, + INIT_3=3, + INIT_4=4, + INIT_5=5, + INIT_6=6, + INIT_7=7, + INIT_8=8, + INIT_9=9, + INIT_10=10, + INIT_11=11, + INIT_12=12, + INIT_13=13, + INIT_14=14, + INIT_15=15, + FF=16, + I0mux=17, + SET_NORESET=18 +*) +module FABULOUS_LC #( + // #LUT inputs + parameter K=4, + parameter LUT_ENTRIES = 2**K, + + // ConfigBits has to be adjusted manually (we don't use an arithmetic parser for the value) + parameter N_CONFIG_BITS = LUT_ENTRIES + 3 +)( + input [K-1:0] I, // Vector for I0, I1, I2, I3 ... + output O, // Single output for LUT result + input Ci, // Carry chain input + output Co, // Carry chain output + input SR, // Shared reset + input EN, // Shared enable + input CLK, // Sahred clock + + (* FABulous, GLOBAL *) input [N_CONFIG_BITS-1:0] ConfigBits // Config bits as vector +); + + wire [LUT_ENTRIES-1 : 0] LUT_values; + wire [K-1 : 0] LUT_index; + wire LUT_out; + reg LUT_flop; + wire I0mux; // normal input '0', or carry input '1' + wire c_out_mux, c_I0mux, c_reset_value; // extra configuration bits + + assign LUT_values = ConfigBits[LUT_ENTRIES-1:0]; + assign c_out_mux = ConfigBits[LUT_ENTRIES+0]; + assign c_I0mux = ConfigBits[LUT_ENTRIES+1]; + assign c_reset_value = ConfigBits[LUT_ENTRIES+2]; + + assign I0mux = c_I0mux ? Ci : I[0]; + + assign LUT_index = {I[K-1:1], I0mux}; + + // The look-up table + LUTK #( + .K (4) + ) LUT4 ( + .I (LUT_index), + .INIT (LUT_values), + .O (LUT_out) + ); + + assign O = c_out_mux ? LUT_flop : LUT_out; + + // iCE40 like carry chain (as this is supported in Yosys; would normally go for fractured LUT) + assign Co = (Ci & I[1]) | (Ci & I[2]) | (I[1] & I[2]); + + always @ (posedge CLK) begin + if (SR) begin + LUT_flop <= c_reset_value; + end else begin + if (EN) begin + LUT_flop <= LUT_out; + end + end + end + +endmodule diff --git a/tests/assets/librelane_plugin/tiles/classic/LUT4x8_ha/LUT4x8_ha.csv b/tests/assets/librelane_plugin/tiles/classic/LUT4x8_ha/LUT4x8_ha.csv new file mode 100644 index 000000000..d18b3818c --- /dev/null +++ b/tests/assets/librelane_plugin/tiles/classic/LUT4x8_ha/LUT4x8_ha.csv @@ -0,0 +1,20 @@ +TILE,LUT4x8_ha,,,,,#carry out +#direction,source_name,X-offset,Y-offset,destination_name,wires, +NORTH,N_GBUF_BEG,0,-1,N_GBUF_END,4,# global buffers +INCLUDE,../common/Base.csv,,,,, +JUMP,GCLK_BEG,0,0,GCLK_END,1, +JUMP,GSR_BEG,0,0,GSR_END,1, +JUMP,GEN_BEG,0,0,GEN_END,1, +NORTH,CO,0,-1,CI,1,# carry +JUMP,J_SR_BEG,0,0,J_SR_END,1, +JUMP,J_EN_BEG,0,0,J_EN_END,1, +BEL,../../../primitives/FABULOUS_LC/fabulous/FABULOUS_LC.v,LA_,,,, +BEL,../../../primitives/FABULOUS_LC/fabulous/FABULOUS_LC.v,LB_,,,, +BEL,../../../primitives/FABULOUS_LC/fabulous/FABULOUS_LC.v,LC_,,,, +BEL,../../../primitives/FABULOUS_LC/fabulous/FABULOUS_LC.v,LD_,,,, +BEL,../../../primitives/FABULOUS_LC/fabulous/FABULOUS_LC.v,LE_,,,, +BEL,../../../primitives/FABULOUS_LC/fabulous/FABULOUS_LC.v,LF_,,,, +BEL,../../../primitives/FABULOUS_LC/fabulous/FABULOUS_LC.v,LG_,,,, +BEL,../../../primitives/FABULOUS_LC/fabulous/FABULOUS_LC.v,LH_,,,, +MATRIX,./LUT4x8_ha_switch_matrix.list,,,,, +EndTILE,,,,,, diff --git a/tests/assets/librelane_plugin/tiles/classic/LUT4x8_ha/LUT4x8_ha_switch_matrix.list b/tests/assets/librelane_plugin/tiles/classic/LUT4x8_ha/LUT4x8_ha_switch_matrix.list new file mode 100644 index 000000000..b89cc91ab --- /dev/null +++ b/tests/assets/librelane_plugin/tiles/classic/LUT4x8_ha/LUT4x8_ha_switch_matrix.list @@ -0,0 +1,299 @@ +# LUT4x8_ha +INCLUDE, ../common/Base.list + +# Global nets +N_GBUF_BEG[0|1|2|3],N_GBUF_END[0|1|2|3] + +GCLK_BEG[0|0|0|0],N_GBUF_END[0|1|2|3] +GSR_BEG[0|0|0|0],N_GBUF_END[0|1|2|3] +GEN_BEG[0|0|0|0],N_GBUF_END[0|1|2|3] + +# the actual LUT input multiplexer +L[A|B]_I0,[J2MID_ABa_END0|J2MID_ABa_END0] +L[A|B]_I1,[J2MID_ABa_END1|J2MID_ABa_END1] +L[A|B]_I2,[J2MID_ABa_END2|J2MID_ABa_END2] +L[A|B]_I3,[J2MID_ABa_END3|J2MID_ABa_END3] +L[C|D]_I0,[J2MID_CDa_END0|J2MID_CDa_END0] +L[C|D]_I1,[J2MID_CDa_END1|J2MID_CDa_END1] +L[C|D]_I2,[J2MID_CDa_END2|J2MID_CDa_END2] +L[C|D]_I3,[J2MID_CDa_END3|J2MID_CDa_END3] +L[E|F]_I0,[J2MID_EFa_END0|J2MID_EFa_END0] +L[E|F]_I1,[J2MID_EFa_END1|J2MID_EFa_END1] +L[E|F]_I2,[J2MID_EFa_END2|J2MID_EFa_END2] +L[E|F]_I3,[J2MID_EFa_END3|J2MID_EFa_END3] +L[G|H]_I0,[J2MID_GHa_END0|J2MID_GHa_END0] +L[G|H]_I1,[J2MID_GHa_END1|J2MID_GHa_END1] +L[G|H]_I2,[J2MID_GHa_END2|J2MID_GHa_END2] +L[G|H]_I3,[J2MID_GHa_END3|J2MID_GHa_END3] + +L[A|B]_I0,[J2MID_ABb_END0|J2MID_ABb_END0] +L[A|B]_I1,[J2MID_ABb_END1|J2MID_ABb_END1] +L[A|B]_I2,[J2MID_ABb_END2|J2MID_ABb_END2] +L[A|B]_I3,[J2MID_ABb_END3|J2MID_ABb_END3] +L[C|D]_I0,[J2MID_CDb_END0|J2MID_CDb_END0] +L[C|D]_I1,[J2MID_CDb_END1|J2MID_CDb_END1] +L[C|D]_I2,[J2MID_CDb_END2|J2MID_CDb_END2] +L[C|D]_I3,[J2MID_CDb_END3|J2MID_CDb_END3] +L[E|F]_I0,[J2MID_EFb_END0|J2MID_EFb_END0] +L[E|F]_I1,[J2MID_EFb_END1|J2MID_EFb_END1] +L[E|F]_I2,[J2MID_EFb_END2|J2MID_EFb_END2] +L[E|F]_I3,[J2MID_EFb_END3|J2MID_EFb_END3] +L[G|H]_I0,[J2MID_GHb_END0|J2MID_GHb_END0] +L[G|H]_I1,[J2MID_GHb_END1|J2MID_GHb_END1] +L[G|H]_I2,[J2MID_GHb_END2|J2MID_GHb_END2] +L[G|H]_I3,[J2MID_GHb_END3|J2MID_GHb_END3] + +L[A|B]_I0,[J2END_AB_END0|J2END_AB_END0] +L[A|B]_I1,[J2END_AB_END1|J2END_AB_END1] +L[A|B]_I2,[J2END_AB_END2|J2END_AB_END2] +L[A|B]_I3,[J2END_AB_END3|J2END_AB_END3] +L[C|D]_I0,[J2END_CD_END0|J2END_CD_END0] +L[C|D]_I1,[J2END_CD_END1|J2END_CD_END1] +L[C|D]_I2,[J2END_CD_END2|J2END_CD_END2] +L[C|D]_I3,[J2END_CD_END3|J2END_CD_END3] +L[E|F]_I0,[J2END_EF_END0|J2END_EF_END0] +L[E|F]_I1,[J2END_EF_END1|J2END_EF_END1] +L[E|F]_I2,[J2END_EF_END2|J2END_EF_END2] +L[E|F]_I3,[J2END_EF_END3|J2END_EF_END3] +L[G|H]_I0,[J2END_GH_END0|J2END_GH_END0] +L[G|H]_I1,[J2END_GH_END1|J2END_GH_END1] +L[G|H]_I2,[J2END_GH_END2|J2END_GH_END2] +L[G|H]_I3,[J2END_GH_END3|J2END_GH_END3] + +L[A|B]_I0,[J_l_AB_END0|J_l_AB_END0] +L[A|B]_I1,[J_l_AB_END1|J_l_AB_END1] +L[A|B]_I2,[J_l_AB_END2|J_l_AB_END2] +L[A|B]_I3,[J_l_AB_END3|J_l_AB_END3] +L[C|D]_I0,[J_l_CD_END0|J_l_CD_END0] +L[C|D]_I1,[J_l_CD_END1|J_l_CD_END1] +L[C|D]_I2,[J_l_CD_END2|J_l_CD_END2] +L[C|D]_I3,[J_l_CD_END3|J_l_CD_END3] +L[E|F]_I0,[J_l_EF_END0|J_l_EF_END0] +L[E|F]_I1,[J_l_EF_END1|J_l_EF_END1] +L[E|F]_I2,[J_l_EF_END2|J_l_EF_END2] +L[E|F]_I3,[J_l_EF_END3|J_l_EF_END3] +L[G|H]_I0,[J_l_GH_END0|J_l_GH_END0] +L[G|H]_I1,[J_l_GH_END1|J_l_GH_END1] +L[G|H]_I2,[J_l_GH_END2|J_l_GH_END2] +L[G|H]_I3,[J_l_GH_END3|J_l_GH_END3] + +# LUT output routing + +# The double wires (which have a mid connection) will get highest connectivity +# this is why we connect all LUT outputs to them as well as some more wires for local routing +# 16:1 muxes each (we specify them in two rounds of 8-driver port sets) +JN2BEG[0|0|0|0|0|0|0|0],[E1END3|LB_O|LC_O|LD_O|LE_O|LF_O|LG_O|LH_O] +JN2BEG[1|1|1|1|1|1|1|1],[LA_O|E1END0|LC_O|LD_O|LE_O|LF_O|LG_O|LH_O] +JN2BEG[2|2|2|2|2|2|2|2],[LA_O|LB_O|E1END1|LD_O|LE_O|LF_O|LG_O|LH_O] +JN2BEG[3|3|3|3|3|3|3|3],[LA_O|LB_O|LC_O|E1END2|LE_O|LF_O|LG_O|LH_O] +JN2BEG[4|4|4|4|4|4|4|4],[LA_O|LB_O|LC_O|LD_O|W1END3|LF_O|LG_O|LH_O] +JN2BEG[5|5|5|5|5|5|5|5],[LA_O|LB_O|LC_O|LD_O|LE_O|W1END0|LG_O|LH_O] +JN2BEG[6|6|6|6|6|6|6|6],[LA_O|LB_O|LC_O|LD_O|LE_O|LF_O|W1END1|LH_O] +JN2BEG[7|7|7|7|7|7|7|7],[LA_O|LB_O|LC_O|LD_O|LE_O|LF_O|LG_O|W1END2] + +JE2BEG[0|0|0|0|0|0|0|0],[N1END3|LB_O|LC_O|LD_O|LE_O|LF_O|LG_O|LH_O] +JE2BEG[1|1|1|1|1|1|1|1],[LA_O|N1END0|LC_O|LD_O|LE_O|LF_O|LG_O|LH_O] +JE2BEG[2|2|2|2|2|2|2|2],[LA_O|LB_O|N1END1|LD_O|LE_O|LF_O|LG_O|LH_O] +JE2BEG[3|3|3|3|3|3|3|3],[LA_O|LB_O|LC_O|N1END2|LE_O|LF_O|LG_O|LH_O] +JE2BEG[4|4|4|4|4|4|4|4],[LA_O|LB_O|LC_O|LD_O|S1END3|LF_O|LG_O|LH_O] +JE2BEG[5|5|5|5|5|5|5|5],[LA_O|LB_O|LC_O|LD_O|LE_O|S1END0|LG_O|LH_O] +JE2BEG[6|6|6|6|6|6|6|6],[LA_O|LB_O|LC_O|LD_O|LE_O|LF_O|S1END1|LH_O] +JE2BEG[7|7|7|7|7|7|7|7],[LA_O|LB_O|LC_O|LD_O|LE_O|LF_O|LG_O|S1END2] + +JS2BEG[0|0|0|0|0|0|0|0],[E1END3|LB_O|LC_O|LD_O|LE_O|LF_O|LG_O|LH_O] +JS2BEG[1|1|1|1|1|1|1|1],[LA_O|E1END0|LC_O|LD_O|LE_O|LF_O|LG_O|LH_O] +JS2BEG[2|2|2|2|2|2|2|2],[LA_O|LB_O|E1END1|LD_O|LE_O|LF_O|LG_O|LH_O] +JS2BEG[3|3|3|3|3|3|3|3],[LA_O|LB_O|LC_O|E1END2|LE_O|LF_O|LG_O|LH_O] +JS2BEG[4|4|4|4|4|4|4|4],[LA_O|LB_O|LC_O|LD_O|W1END3|LF_O|LG_O|LH_O] +JS2BEG[5|5|5|5|5|5|5|5],[LA_O|LB_O|LC_O|LD_O|LE_O|W1END0|LG_O|LH_O] +JS2BEG[6|6|6|6|6|6|6|6],[LA_O|LB_O|LC_O|LD_O|LE_O|LF_O|W1END1|LH_O] +JS2BEG[7|7|7|7|7|7|7|7],[LA_O|LB_O|LC_O|LD_O|LE_O|LF_O|LG_O|W1END2] + +JW2BEG[0|0|0|0|0|0|0|0],[N1END3|LB_O|LC_O|LD_O|LE_O|LF_O|LG_O|LH_O] +JW2BEG[1|1|1|1|1|1|1|1],[LA_O|N1END0|LC_O|LD_O|LE_O|LF_O|LG_O|LH_O] +JW2BEG[2|2|2|2|2|2|2|2],[LA_O|LB_O|N1END1|LD_O|LE_O|LF_O|LG_O|LH_O] +JW2BEG[3|3|3|3|3|3|3|3],[LA_O|LB_O|LC_O|N1END2|LE_O|LF_O|LG_O|LH_O] +JW2BEG[4|4|4|4|4|4|4|4],[LA_O|LB_O|LC_O|LD_O|S1END3|LF_O|LG_O|LH_O] +JW2BEG[5|5|5|5|5|5|5|5],[LA_O|LB_O|LC_O|LD_O|LE_O|S1END0|LG_O|LH_O] +JW2BEG[6|6|6|6|6|6|6|6],[LA_O|LB_O|LC_O|LD_O|LE_O|LF_O|S1END1|LH_O] +JW2BEG[7|7|7|7|7|7|7|7],[LA_O|LB_O|LC_O|LD_O|LE_O|LF_O|LG_O|S1END2] + +# +JN2BEG[0|0|0|0|0|0|0|0],[N1END1|N2END1|E2END1|SS4END1|W2END1|W6END1|E6END1|N4END1] +JN2BEG[1|1|1|1|1|1|1|1],[N1END2|N2END2|E2END2|S2END2|W2END2|W6END0|E6END0|N4END2] +JN2BEG[2|2|2|2|2|2|2|2],[N1END3|N2END3|E2END3|S2END3|W2END3|WW4END1|E6END1|N4END3] +JN2BEG[3|3|3|3|3|3|3|3],[N1END0|N2END4|E2END4|S2END4|W2END4|W6END0|E6END0|N4END0] +JN2BEG[4|4|4|4|4|4|4|4],[W2END5|N2END5|E2END5|S2END5|N1END1|E1END1|S1END1|W1END1] +JN2BEG[5|5|5|5|5|5|5|5],[W2END6|N2END6|E2END6|S2END6|N1END2|E1END2|S1END2|W1END2] +JN2BEG[6|6|6|6|6|6|6|6],[W2END7|N2END7|E2END7|S2END7|N1END3|E1END3|S1END3|W1END3] +JN2BEG[7|7|7|7|7|7|7|7],[W2END0|N2END0|EE4END0|S2END0|N1END0|E1END0|S1END0|W1END0] + +JE2BEG[0|0|0|0|0|0|0|0],[N1END1|N2END1|EE4END1|S2END1|W2END1|W6END1|E6END1|N4END1] +JE2BEG[1|1|1|1|1|1|1|1],[N1END2|N2END2|E2END2|S2END2|W2END2|WW4END3|E6END0|N4END2] +JE2BEG[2|2|2|2|2|2|2|2],[N1END3|N2END3|E2END3|S2END3|W2END3|W6END1|E6END1|N4END3] +JE2BEG[3|3|3|3|3|3|3|3],[N1END0|N2END4|E2END4|S2END4|W2END4|W6END0|E6END0|N4END0] +JE2BEG[4|4|4|4|4|4|4|4],[W2END5|N2END5|E2END5|S2END5|N1END1|E1END1|S1END1|W1END1] +JE2BEG[5|5|5|5|5|5|5|5],[W2END6|N2END6|E2END6|S2END6|N1END2|E1END2|S1END2|W1END2] +JE2BEG[6|6|6|6|6|6|6|6],[W2END7|N2END7|E2END7|S2END7|N1END3|E1END3|S1END3|W1END3] +JE2BEG[7|7|7|7|7|7|7|7],[W2END0|N2END0|E2END0|SS4END0|N1END0|E1END0|S1END0|WW4END0] + +JS2BEG[0|0|0|0|0|0|0|0],[N1END1|NN4END1|E2END1|S2END1|W2END1|W6END1|E6END1|S4END1] +JS2BEG[1|1|1|1|1|1|1|1],[N1END2|NN4END2|EE4END2|SS4END2|W2END2|W6END0|E6END0|S4END2] +JS2BEG[2|2|2|2|2|2|2|2],[N1END3|NN4END3|E2END3|S2END3|W2END3|W6END1|E6END1|S4END3] +JS2BEG[3|3|3|3|3|3|3|3],[N1END0|N2END4|E2END4|S2END4|W2END4|WW4END2|E6END0|S4END0] +JS2BEG[4|4|4|4|4|4|4|4],[W2END5|N2END5|E2END5|S2END5|N1END1|E1END1|S1END1|W1END1] +JS2BEG[5|5|5|5|5|5|5|5],[W2END6|N2END6|E2END6|S2END6|N1END2|E1END2|S1END2|W1END2] +JS2BEG[6|6|6|6|6|6|6|6],[W2END7|N2END7|E2END7|S2END7|N1END3|E1END3|S1END3|W1END3] +JS2BEG[7|7|7|7|7|7|7|7],[W2END0|N2END0|E2END0|S2END0|N1END0|E1END0|S1END0|W1END0] + +# I uncommented the following wires as they implement the input operand routing +# I use a stop-over (JE2BEG) which is used to recycle this multiplexer not only to drive the wire (E2BEG) but to drive other things +# So we have to check which other things this is and pick one of the inputs + +JW2BEG[0|0|0|0|0|0|0|0],[N1END1|N2END1|E2END1|S2END1|W2END1|W6END1|E6END1|S4END1] +JW2BEG[1|1|1|1|1|1|1|1],[N1END2|N2END2|E2END2|S2END2|W2END2|W6END0|E6END0|S4END2] +JW2BEG[2|2|2|2|2|2|2|2],[N1END3|N2END3|EE4END3|SS4END3|W2END3|W6END1|E6END1|S4END3] +JW2BEG[3|3|3|3|3|3|3|3],[N1END0|N2END4|E2END4|S2END4|W2END4|WW4END2|E6END0|S4END0] +JW2BEG[4|4|4|4|4|4|4|4],[W2END5|N2END5|E2END5|S2END5|N1END1|E1END1|S1END1|W1END1] +JW2BEG[5|5|5|5|5|5|5|5],[W2END6|N2END6|E2END6|S2END6|N1END2|E1END2|S1END2|W1END2] +JW2BEG[6|6|6|6|6|6|6|6],[W2END7|N2END7|E2END7|S2END7|N1END3|E1END3|S1END3|W1END3] +JW2BEG[7|7|7|7|7|7|7|7],[W2END0|NN4END0|E2END0|S2END0|N1END0|E1END0|S1END0|W1END0] + +# connection from the double jump wires (stopovers) to the actual double wires +# original connection: +# [N|E|S|W]2BEG[0|1|2|3|4|5|6|7],J[N|E|S|W]2END[0|1|2|3|4|5|6|7] +# the same but without the west routing/connection +[N|E|S]2BEG[0|1|2|3|4|5|6|7],J[N|E|S]2END[0|1|2|3|4|5|6|7] +# We split the double west channel into "normal routing" (indices[1|2|5|6]) and "operand routing" (indices[0|3|4|7]) +# the "normal routing" is using a JUMP stop-over in order to allow this multiplexer to drive the E2BEG wire and to serve as a source for other inputs +W2BEG[1|2|5|6],JW2END[1|2|5|6] +W2BEG[0|3|4|7],JW2END[0|3|4|7] +# the other wires, "operand routing", get just extended; this is like the routing used for a ReCoBus +##W2BEG[0|3|4|7],W2END[0|3|4|7] +# The operand (uses 4 double west wires) hard-wired routing: +# important: while it is normally a good idea to twist wire indexes to prevent linear combinations (and increase graph entropy) we don't want this for the operands. +# this allows it to relocate modules because in each slot, we will find the same operands bits. +# The exact index allocation was not chosen random! +# We wanted to have something where the 4 operand double end wires (W2END[0|3|4|7]) can ALL connect directly to all 8 LUT inputs. +# This includes the requirement that routing has to be conflict free, which is an issue as we route into the LUT through StopOvers (JUMPs) +# This is a little like solving a Sudoku puzzle +# This routing scheme is useful for simple Boolean functions +# Foreseen mapping: + # J2END_AB_BEG J_l_AB_BEG +# LUT_Input | J2END_x_BEG | J_1_x_BEG | used_index +# ----------|-------------|-----------|-------------------------------------------------- +# AB 0 | W2END6 | W2END3* | 3 +# AB 1 | W2END2 | W2END7* | 7 +# AB 2 | W2END4* | W6END1 | 4 +# AB 3 | W2END0* | JW2END1 | 0 +# ----------|-------------|-----------|-------------------------------------------------- +# CD 0 | W2END6 | W2END3* | 3 +# CD 1 | W2END2 | W2END7* | 7 +# CD 2 | W2END4* | JS2END2 | 4 +# CD 3 | W2END0* | W6END0 | 0 +# ----------|-------------|-----------|-------------------------------------------------- +# EF 0 | W2END7* | W2END3 | 7 +# EF 1 | W2END3* | JE2END3 | 3 +# EF 2 | W2END5 | W2END4* | 4 +# EF 3 | W2END1 | W2END0* | 0 +# ----------|-------------|-----------|-------------------------------------------------- +# GH 0 | W2END7* | JN2END4 | 7 +# GH 1 | W2END3* | W2END2 | 3 +# GH 2 | W2END5 | W2END4* | 4 +# GH 3 | W2END1 | W2END0* | 0 +# ----------|-------------|-----------|-------------------------------------------------- + +# single wires +N1BEG[0|0|0|0],[J_l_CD_END1|LC_O|JW2END3|J2MID_CDb_END3] +N1BEG[1|1|1|1],[J_l_EF_END2|LD_O|JW2END0|J2MID_EFb_END0] +N1BEG[2|2|2|2],[J_l_GH_END3|LE_O|JW2END1|J2MID_GHb_END1] +N1BEG[3|3|3|3],[J_l_AB_END0|LF_O|JW2END2|J2MID_ABb_END2] + +E1BEG[0|0|0|0],[J_l_CD_END1|LD_O|JN2END3|J2MID_CDb_END3] +E1BEG[1|1|1|1],[J_l_EF_END2|LE_O|JN2END0|J2MID_EFb_END0] +E1BEG[2|2|2|2],[J_l_GH_END3|LF_O|JN2END1|J2MID_GHb_END1] +E1BEG[3|3|3|3],[J_l_AB_END0|LG_O|JN2END2|J2MID_ABb_END2] + +S1BEG[0|0|0|0],[J_l_CD_END1|LE_O|JE2END3|J2MID_CDb_END3] +S1BEG[1|1|1|1],[J_l_EF_END2|LF_O|JE2END0|J2MID_EFb_END0] +S1BEG[2|2|2|2],[J_l_GH_END3|LG_O|JE2END1|J2MID_GHb_END1] +S1BEG[3|3|3|3],[J_l_AB_END0|LH_O|JE2END2|J2MID_ABb_END2] + +W1BEG[0|0|0|0],[J_l_CD_END1|LF_O|JS2END3|J2MID_CDb_END3] +W1BEG[1|1|1|1],[J_l_EF_END2|LG_O|JS2END0|J2MID_EFb_END0] +W1BEG[2|2|2|2],[J_l_GH_END3|LH_O|JS2END1|J2MID_GHb_END1] +W1BEG[3|3|3|3],[J_l_AB_END0|LA_O|JS2END2|J2MID_ABb_END2] + +# quad wires (we only have 4 of them in each vertical direction) +N4BEG[0|0|0|0],[N4END1|N2END2|E6END1|LE_O] +N4BEG[1|1|1|1],[N4END2|N2END3|E6END0|LF_O] +N4BEG[2|2|2|2],[N4END3|N2END0|W6END1|LG_O] +N4BEG[3|3|3|3],[N4END0|N2END1|W6END0|LH_O] + +S4BEG[0|0|0|0],[S4END1|S2END2|E6END1|LA_O] +S4BEG[1|1|1|1],[S4END2|S2END3|E6END0|LB_O] +S4BEG[2|2|2|2],[S4END3|S2END0|W6END1|LC_O] +S4BEG[3|3|3|3],[S4END0|S2END1|W6END0|LD_O] + +# hex wires (we only have 2 of them in each vertical direction) +E6BEG[0|0|0|0|0|0|0|0],[LA_O|LB_O|LC_O|LD_O|LE_O|LF_O|LG_O|LH_O] +E6BEG[1|1|1|1|1|1|1|1],[LA_O|LB_O|LC_O|LD_O|LE_O|LF_O|LG_O|LH_O] +E6BEG[0|0|0|0|0|0|0|0],[J2MID_ABb_END1|J2MID_CDb_END1|J2MID_EFb_END1|J2MID_GHb_END1|E1END3|W1END3|N1END3|S1END3] +E6BEG[1|1|1|1|1|1|1|1],[J2MID_ABa_END2|J2MID_CDa_END2|J2MID_EFa_END2|J2MID_GHa_END2|E1END2|W1END2|N1END2|S1END2] + +W6BEG[0|0|0|0|0|0|0|0],[LA_O|LB_O|LC_O|LD_O|LE_O|LF_O|LG_O|LH_O] +W6BEG[1|1|1|1|1|1|1|1],[LA_O|LB_O|LC_O|LD_O|LE_O|LF_O|LG_O|LH_O] +W6BEG[0|0|0|0|0|0|0|0],[J2MID_ABb_END1|J2MID_CDb_END1|J2MID_EFb_END1|J2MID_GHb_END1|E1END3|W1END3|N1END3|S1END3] +W6BEG[1|1|1|1|1|1|1|1],[J2MID_ABa_END2|J2MID_CDa_END2|J2MID_EFa_END2|J2MID_GHa_END2|E1END2|W1END2|N1END2|S1END2] + +EE4BEG[0|0|0|0|0|0|0|0],[J2MID_ABb_END1|J2MID_CDb_END1|LF_O|LG_O|J2END_GH_END0|N1END2|S1END2|E1END2] +EE4BEG[1|1|1|1|1|1|1|1],[J2MID_ABa_END2|J2MID_CDa_END2|LA_O|LH_O|J2END_EF_END0|N1END3|S1END3|E1END3] +EE4BEG[2|2|2|2|2|2|2|2],[J2MID_EFb_END1|J2MID_GHb_END1|LB_O|LC_O|J2END_CD_END0|N1END0|S1END0|E1END0] +EE4BEG[3|3|3|3|3|3|3|3],[J2MID_EFa_END2|J2MID_GHa_END2|LD_O|LE_O|J2END_AB_END0|N1END1|S1END1|E1END1] + +WW4BEG[0|0|0|0|0|0|0|0],[J2MID_ABb_END1|J2MID_CDb_END1|LF_O|LG_O|J2END_GH_END2|N1END2|S1END2|W1END2] +WW4BEG[1|1|1|1|1|1|1|1],[J2MID_ABa_END2|J2MID_CDa_END2|LA_O|LH_O|J2END_EF_END2|N1END3|S1END3|W1END3] +WW4BEG[2|2|2|2|2|2|2|2],[J2MID_EFb_END1|J2MID_GHb_END1|LB_O|LC_O|J2END_CD_END2|N1END0|S1END0|W1END0] +WW4BEG[3|3|3|3|3|3|3|3],[J2MID_EFa_END2|J2MID_GHa_END2|LD_O|LE_O|J2END_AB_END2|N1END1|S1END1|W1END1] + +NN4BEG[0|0|0|0|0|0|0|0],[J2MID_ABb_END1|J2MID_CDb_END1|LF_O|LG_O|J2END_GH_END1|E1END2|W1END2|N1END2] +NN4BEG[1|1|1|1|1|1|1|1],[J2MID_ABa_END2|J2MID_CDa_END2|LA_O|LH_O|J2END_EF_END1|E1END3|W1END3|N1END3] +NN4BEG[2|2|2|2|2|2|2|2],[J2MID_EFb_END1|J2MID_GHb_END1|LB_O|LC_O|J2END_CD_END1|E1END0|W1END0|N1END0] +NN4BEG[3|3|3|3|3|3|3|3],[J2MID_EFa_END2|J2MID_GHa_END2|LD_O|LE_O|J2END_AB_END1|E1END1|W1END1|N1END1] + +SS4BEG[0|0|0|0|0|0|0|0],[J2MID_ABb_END1|J2MID_CDb_END1|LF_O|LG_O|J2END_GH_END3|E1END2|W1END2|N1END2] +SS4BEG[1|1|1|1|1|1|1|1],[J2MID_ABa_END2|J2MID_CDa_END2|LA_O|LH_O|J2END_EF_END3|E1END3|W1END3|N1END3] +SS4BEG[2|2|2|2|2|2|2|2],[J2MID_EFb_END1|J2MID_GHb_END1|LB_O|LC_O|J2END_CD_END3|E1END0|W1END0|N1END0] +SS4BEG[3|3|3|3|3|3|3|3],[J2MID_EFa_END2|J2MID_GHa_END2|LD_O|LE_O|J2END_AB_END3|E1END1|W1END1|N1END1] + +# Carry chain CI -> LA_CI-LA_CO -> LB_CI-LB_CO -> ... -> LH_CI-LH_CO -> CO +LA_Ci,CI0 +L[B|C|D|E|F|G|H]_Ci,L[A|B|C|D|E|F|G]_Co +CO0,LH_Co + +# Clock assignment +L[A|B|C|D|E|F|G|H]_CLK,[GCLK_END0|GCLK_END0|GCLK_END0|GCLK_END0|GCLK_END0|GCLK_END0|GCLK_END0|GCLK_END0] + +# reset and enable input multiplexers +J_SR_BEG[0|0|0|0|0|0|0|0|0],[J2MID_ABb_END0|J2MID_CDb_END0|J2MID_EFb_END0|J2MID_GHa_END0|JN2END1|JE2END1|JS2END1|JW2END1|GSR_END0] +J_EN_BEG[0|0|0|0|0|0|0|0|0],[J2MID_ABb_END3|J2MID_CDb_END3|J2MID_EFb_END3|J2MID_GHa_END3|JN2END2|JE2END2|JS2END2|JW2END2|GEN_END0] + +# connect reset jump wire with LUTs +L[A|A]_SR,[J_SR_END0|GND0] +L[B|B]_SR,[J_SR_END0|GND0] +L[C|C]_SR,[J_SR_END0|GND0] +L[D|D]_SR,[J_SR_END0|GND0] +L[E|E]_SR,[J_SR_END0|GND0] +L[F|F]_SR,[J_SR_END0|GND0] +L[G|G]_SR,[J_SR_END0|GND0] +L[H|H]_SR,[J_SR_END0|GND0] + +# connect enable jump wire with LUTs +L[A|A]_EN,[J_EN_END0|VCC0] +L[B|B]_EN,[J_EN_END0|VCC0] +L[C|C]_EN,[J_EN_END0|VCC0] +L[D|D]_EN,[J_EN_END0|VCC0] +L[E|E]_EN,[J_EN_END0|VCC0] +L[F|F]_EN,[J_EN_END0|VCC0] +L[G|G]_EN,[J_EN_END0|VCC0] +L[H|H]_EN,[J_EN_END0|VCC0] diff --git a/tests/assets/librelane_plugin/tiles/classic/LUT4x8_ha/config.yaml b/tests/assets/librelane_plugin/tiles/classic/LUT4x8_ha/config.yaml new file mode 100644 index 000000000..1c28f9bb2 --- /dev/null +++ b/tests/assets/librelane_plugin/tiles/classic/LUT4x8_ha/config.yaml @@ -0,0 +1,24 @@ +meta: + version: 2 + flow: FABulousTile + +FABULOUS_TILE_DIR: dir::. + +# Basics +DESIGN_NAME: LUT4x8_ha +VERILOG_FILES: + - dir::../../../models_pack.v + +DIE_AREA: [ 0, 0, 250, 250 ] + +# Disable timing driven placement +# as it would resize the buffers +PL_TIMING_DRIVEN: false + +# "AREA 0" "AREA 1" "AREA 2" "AREA 3" +SYNTH_STRATEGY: "AREA 2" + +# Clock +CLOCK_PERIOD: 20 + +#CLOCK_PORT: N_GBUF_END[0] diff --git a/tests/assets/librelane_plugin/tiles/classic/common/Base.csv b/tests/assets/librelane_plugin/tiles/classic/common/Base.csv new file mode 100644 index 000000000..96bc8d741 --- /dev/null +++ b/tests/assets/librelane_plugin/tiles/classic/common/Base.csv @@ -0,0 +1,43 @@ +#direction,source_name,X-offset,Y-offset,destination_name,wires,,,,,,,,,,,,, +NORTH,N1BEG,0,-1,N1END,4,,,,,,,,,,,,, +NORTH,N2BEG,0,-1,N2MID,8,,,,,,,,,,,,, +NORTH,N2BEGb,0,-1,N2END,8,,,,,,,,,,,,, +NORTH,N4BEG,0,-4,N4END,4,,,,,,,,,,,,, +NORTH,NN4BEG,0,-4,NN4END,4,,,,,,,,,,,,, +EAST,E1BEG,1,0,E1END,4,,,,,,,,,,,,, +EAST,E2BEG,1,0,E2MID,8,,,,,,,,,,,,, +EAST,E2BEGb,1,0,E2END,8,,,,,,,,,,,,, +EAST,EE4BEG,4,0,EE4END,4,,,,,,,,,,,,, +EAST,E6BEG,6,0,E6END,2,,,,,,,,,,,,, +SOUTH,S1BEG,0,1,S1END,4,,,,,,,,,,,,, +SOUTH,S2BEG,0,1,S2MID,8,,,,,,,,,,,,, +SOUTH,S2BEGb,0,1,S2END,8,,,,,,,,,,,,, +SOUTH,S4BEG,0,4,S4END,4,,,,,,,,,,,,, +SOUTH,SS4BEG,0,4,SS4END,4,,,,,,,,,,,,, +WEST,W1BEG,-1,0,W1END,4,,,,,,,,,,,,, +WEST,W2BEG,-1,0,W2MID,8,,,,,,,,,,,,, +WEST,W2BEGb,-1,0,W2END,8,,,,,,,,,,,,, +WEST,WW4BEG,-4,0,WW4END,4,,,,,,,,,,,,, +WEST,W6BEG,-6,0,W6END,2,,,,,,,,,,,,, +JUMP,J2MID_ABa_BEG,0,0,J2MID_ABa_END,4,,,,,,,,,,,,, +JUMP,J2MID_CDa_BEG,0,0,J2MID_CDa_END,4,,,,,,,,,,,,, +JUMP,J2MID_EFa_BEG,0,0,J2MID_EFa_END,4,,,,,,,,,,,,, +JUMP,J2MID_GHa_BEG,0,0,J2MID_GHa_END,4,,,,,,,,,,,,, +JUMP,J2MID_ABb_BEG,0,0,J2MID_ABb_END,4,,,,,,,,,,,,, +JUMP,J2MID_CDb_BEG,0,0,J2MID_CDb_END,4,,,,,,,,,,,,, +JUMP,J2MID_EFb_BEG,0,0,J2MID_EFb_END,4,,,,,,,,,,,,, +JUMP,J2MID_GHb_BEG,0,0,J2MID_GHb_END,4,,,,,,,,,,,,, +JUMP,J2END_AB_BEG,0,0,J2END_AB_END,4,,,,,,,,,,,,, +JUMP,J2END_CD_BEG,0,0,J2END_CD_END,4,,,,,,,,,,,,, +JUMP,J2END_EF_BEG,0,0,J2END_EF_END,4,,,,,,,,,,,,, +JUMP,J2END_GH_BEG,0,0,J2END_GH_END,4,,,,,,,,,,,,, +JUMP,JN2BEG,0,0,JN2END,8,,,,,,,,,,,,, +JUMP,JE2BEG,0,0,JE2END,8,,,,,,,,,,,,, +JUMP,JS2BEG,0,0,JS2END,8,,,,,,,,,,,,, +JUMP,JW2BEG,0,0,JW2END,8,,,,,,,,,,,,, +JUMP,J_l_AB_BEG,0,0,J_l_AB_END,4,,,,,,,,,,,,, +JUMP,J_l_CD_BEG,0,0,J_l_CD_END,4,,,,,,,,,,,,, +JUMP,J_l_EF_BEG,0,0,J_l_EF_END,4,,,,,,,,,,,,, +JUMP,J_l_GH_BEG,0,0,J_l_GH_END,4,,,,,,,,,,,,, +JUMP,NULL,0,0,GND,1,,,,,,,,,,,,, +JUMP,NULL,0,0,VCC,1,,,,,,,,,,,,, diff --git a/tests/assets/librelane_plugin/tiles/classic/common/Base.list b/tests/assets/librelane_plugin/tiles/classic/common/Base.list new file mode 100644 index 000000000..e9c7467ec --- /dev/null +++ b/tests/assets/librelane_plugin/tiles/classic/common/Base.list @@ -0,0 +1,75 @@ +# Base switch matrix list file +# double with MID cascade : [N,E,S,W]2BEG --- [N,E,S,W]2MID -> [N,E,S,W]2BEGb --- [N,E,S,W]2END (just routing) +[N|E|S|W]2BEGb[0|1|2|3|4|5|6|7],[N|E|S|W]2MID[0|1|2|3|4|5|6|7] + +# shared double MID jump wires +J2MID_ABa_BEG[0|0|0|0],[JN2END3|N2MID6|S2MID6|W2MID6] +J2MID_ABa_BEG[1|1|1|1],[E2MID2|JE2END3|S2MID2|W2MID2] +J2MID_ABa_BEG[2|2|2|2],[E2MID4|N2MID4|JS2END3|W2MID4] +J2MID_ABa_BEG[3|3|3|3],[E2MID0|N2MID0|S2MID0|JW2END3] +J2MID_CDa_BEG[0|0|0|0],[E2MID6|JN2END4|S2MID6|W2MID6] +J2MID_CDa_BEG[1|1|1|1],[E2MID2|N2MID2|JE2END4|W2MID2] +J2MID_CDa_BEG[2|2|2|2],[E2MID4|N2MID4|S2MID4|JS2END4] +J2MID_CDa_BEG[3|3|3|3],[JW2END4|N2MID0|S2MID0|W2MID0] +J2MID_EFa_BEG[0|0|0|0],[E2MID6|N2MID6|JN2END5|W2MID6] +J2MID_EFa_BEG[1|1|1|1],[E2MID2|N2MID2|S2MID2|JE2END5] +J2MID_EFa_BEG[2|2|2|2],[JS2END5|N2MID4|S2MID4|W2MID4] +J2MID_EFa_BEG[3|3|3|3],[E2MID0|JW2END5|S2MID0|W2MID0] +J2MID_GHa_BEG[0|0|0|0],[E2MID6|N2MID6|S2MID6|JN2END6] +J2MID_GHa_BEG[1|1|1|1],[JE2END6|N2MID2|S2MID2|W2MID2] +J2MID_GHa_BEG[2|2|2|2],[E2MID4|JS2END6|S2MID4|W2MID4] +J2MID_GHa_BEG[3|3|3|3],[E2MID0|N2MID0|JW2END6|W2MID0] + +J2MID_ABb_BEG[0|0|0|0],[E2MID7|N2MID7|S2MID7|W2MID7] +J2MID_ABb_BEG[1|1|1|1],[E2MID3|N2MID3|S2MID3|W2MID3] +J2MID_ABb_BEG[2|2|2|2],[E2MID5|N2MID5|S2MID5|W2MID5] +J2MID_ABb_BEG[3|3|3|3],[E2MID1|N2MID1|S2MID1|W2MID1] +J2MID_CDb_BEG[0|0|0|0],[E2MID7|N2MID7|S2MID7|W2MID7] +J2MID_CDb_BEG[1|1|1|1],[E2MID3|N2MID3|S2MID3|W2MID3] +J2MID_CDb_BEG[2|2|2|2],[E2MID5|N2MID5|S2MID5|W2MID5] +J2MID_CDb_BEG[3|3|3|3],[E2MID1|N2MID1|S2MID1|W2MID1] +J2MID_EFb_BEG[0|0|0|0],[E2MID7|N2MID7|S2MID7|W2MID7] +J2MID_EFb_BEG[1|1|1|1],[E2MID3|N2MID3|S2MID3|W2MID3] +J2MID_EFb_BEG[2|2|2|2],[E2MID5|N2MID5|S2MID5|W2MID5] +J2MID_EFb_BEG[3|3|3|3],[E2MID1|N2MID1|S2MID1|W2MID1] +J2MID_GHb_BEG[0|0|0|0],[E2MID7|N2MID7|S2MID7|W2MID7] +J2MID_GHb_BEG[1|1|1|1],[E2MID3|N2MID3|S2MID3|W2MID3] +J2MID_GHb_BEG[2|2|2|2],[E2MID5|N2MID5|S2MID5|W2MID5] +J2MID_GHb_BEG[3|3|3|3],[E2MID1|N2MID1|S2MID1|W2MID1] + + +# shared double END jump wires +J2END_AB_BEG[0|0|0|0],[E2END6|N2END6|SS4END3|W2END6] +J2END_AB_BEG[1|1|1|1],[E2END2|NN4END0|S2END2|W2END2] +J2END_AB_BEG[2|2|2|2],[EE4END0|N2END4|S2END4|W2END4] +J2END_AB_BEG[3|3|3|3],[E2END0|N2END0|S2END0|WW4END3] +J2END_CD_BEG[0|0|0|0],[E2END6|NN4END3|S2END6|W2END6] +J2END_CD_BEG[1|1|1|1],[E2END2|N2END2|S2END2|WW4END2] +J2END_CD_BEG[2|2|2|2],[E2END4|N2END4|SS4END2|W2END4] +J2END_CD_BEG[3|3|3|3],[EE4END1|N2END0|S2END0|W2END0] +J2END_EF_BEG[0|0|0|0],[EE4END2|N2END7|S2END7|W2END7] +J2END_EF_BEG[1|1|1|1],[E2END3|N2END3|S2END3|WW4END1] +J2END_EF_BEG[2|2|2|2],[E2END5|N2END5|SS4END1|W2END5] +J2END_EF_BEG[3|3|3|3],[E2END1|NN4END2|S2END1|W2END1] +J2END_GH_BEG[0|0|0|0],[E2END7|N2END7|S2END7|WW4END0] +J2END_GH_BEG[1|1|1|1],[E2END3|N2END3|SS4END0|W2END3] +J2END_GH_BEG[2|2|2|2],[E2END5|NN4END1|S2END5|W2END5] +J2END_GH_BEG[3|3|3|3],[EE4END3|N2END1|S2END1|W2END1] + +# shared double END jump wires I shared that with the flop outputs +J_l_AB_BEG[0|0|0|0],[JN2END1|NN4END3|S4END3|WW4END0] +J_l_AB_BEG[1|1|1|1],[EE4END2|JE2END1|S4END2|W2END7] +J_l_AB_BEG[2|2|2|2],[E6END1|N4END1|JS2END1|W6END1] +J_l_AB_BEG[3|3|3|3],[E6END0|N4END0|S4END0|JW2END1] +J_l_CD_BEG[0|0|0|0],[E2END3|JN2END2|SS4END3|WW4END2] +J_l_CD_BEG[1|1|1|1],[E2END2|N4END2|JE2END2|W2END7] +J_l_CD_BEG[2|2|2|2],[EE4END1|NN4END1|S4END1|JS2END2] +J_l_CD_BEG[3|3|3|3],[JW2END2|N4END0|SS4END0|W6END0] +J_l_EF_BEG[0|0|0|0],[E2END3|N4END3|JN2END3|W2END3] +J_l_EF_BEG[1|1|1|1],[E2END2|NN4END2|S4END2|JE2END3] +J_l_EF_BEG[2|2|2|2],[JS2END3|N4END1|SS4END1|W2END4] +J_l_EF_BEG[3|3|3|3],[EE4END3|JW2END3|S4END0|WW4END1] +J_l_GH_BEG[0|0|0|0],[EE4END0|N4END3|S4END3|JN2END4] +J_l_GH_BEG[1|1|1|1],[JE2END4|N4END2|SS4END2|W2END2] +J_l_GH_BEG[2|2|2|2],[E6END1|JS2END4|S4END1|WW4END3] +J_l_GH_BEG[3|3|3|3],[E6END0|NN4END0|JW2END4|W2END0] diff --git a/tests/assets/librelane_plugin/tiles/classic/common/config.yaml b/tests/assets/librelane_plugin/tiles/classic/common/config.yaml new file mode 100644 index 000000000..947977936 --- /dev/null +++ b/tests/assets/librelane_plugin/tiles/classic/common/config.yaml @@ -0,0 +1,93 @@ +DIE_AREA: [ 0, 0, 245, 245 ] + +SYNTH_STRATEGY: "AREA 2" + +IO_PIN_V_EXTENSION: 0.0 +IO_PIN_H_EXTENSION: 0.0 +IO_PIN_H_THICKNESS_MULT: 2.0 +IO_PIN_V_THICKNESS_MULT: 2.0 +FP_SIZING: "absolute" + +IO_PIN_H_LENGTH: null +IO_PIN_V_LENGTH: null + +RUN_HEURISTIC_DIODE_INSERTION: False +HEURISTIC_ANTENNA_THRESHOLD: 90 + +CTS_SINK_CLUSTERING_SIZE: 25 +CTS_SINK_CLUSTERING_MAX_DIAMETER: 50 +CTS_SINK_CLUSTERING_ENABLE: False + +MAGIC_NO_EXT_UNIQUE: False + +DESIGN_REPAIR_BUFFER_INPUT_PORTS: False +DESIGN_REPAIR_BUFFER_OUTPUT_PORTS: True + +PL_TIMING_DRIVEN: False + +CLOCK_PERIOD: 20.0 +CLOCK_PORT: "UserCLK" + +GRT_ANTENNA_REPAIR_ITERS: 10 +GRT_ALLOW_CONGESTION: true +RUN_ANTENNA_REPAIR: True + +PDN_MULTILAYER: false + +BOTTOM_MARGIN_MULT: 1 +TOP_MARGIN_MULT: 1 +LEFT_MARGIN_MULT: 6 +RIGHT_MARGIN_MULT: 6 + +PDN_SKIPTRIM: True + +DIODE_ON_PORTS: "in" + +pdk::sky130A: + #Floor planning + IO_PIN_H_LAYER: "met3" + IO_PIN_V_LAYER: "met2" + + # Routing + RT_MAX_LAYER: met4 + + # PDN + PDN_VWIDTH: 1.6 + PDN_VSPACING: 3.7 + PDN_VPITCH: 30 + PDN_VOFFSET: 5 + + PDN_HWIDTH: 1.6 + PDN_HSPACING: 3.7 + PDN_HPITCH: 30 + PDN_HOFFSET: 5 + +pdk::ihp-sg13g2: + #Floor planning + IO_PIN_H_LAYER: "Metal3" + IO_PIN_V_LAYER: "Metal2" + + # Routing + RT_MAX_LAYER: TopMetal1 + + DIODE_CELL: "sg13g2_antennanp/A" + + # PDN + PDN_VWIDTH: 2.2 + PDN_VSPACING: 4.0 + PDN_VPITCH: 75.6 + PDN_VOFFSET: 13.6 + + PDN_HWIDTH: 2.0 + PDN_HSPACING: 4.0 + PDN_HPITCH: 75.6 + PDN_HOFFSET: 13.57 + +pdk::gf180mcuD: + # Routing + RT_MAX_LAYER: Metal4 + + # PDN + PDN_VPITCH: 100 + + SYNTH_LATCH_MAP: "dir::../../../include/gf180mcuD_latchmap.v" diff --git a/tests/gds_flow_test/flow_test/test_fabric_macro_flow.py b/tests/gds_flow_test/flow_test/test_fabric_macro_flow.py index 9a98f1c79..0c61ad6d5 100644 --- a/tests/gds_flow_test/flow_test/test_fabric_macro_flow.py +++ b/tests/gds_flow_test/flow_test/test_fabric_macro_flow.py @@ -505,6 +505,87 @@ def test_flow_has_fabulous_spef_corners_config(self) -> None: config_names: list[str] = [var.name for var in configs] assert "FABULOUS_SPEF_CORNERS" in config_names + +class TestSpacingVariableTypes: + """Type-system checks for the Union-typed spacing variables. + + The spacing variables accept either a scalar (applied to both axes) or a + tuple (per-axis). Real example projects use *both* shapes: + - ``tt-fabulous-ihp-26a/fabrics/tiny_fabric_9x5/config.yaml`` uses + ``FABULOUS_TILE_SPACING: 0`` (scalar). + - ``tests/assets/librelane_plugin/.../config.yaml`` uses ``[0, 0]`` (tuple). + + YAML-loaded values reach LibreLane as bare ``int``/``str``/``list``, so + ``Variable.compile`` is invoked with ``permissive_typing=True``. These + tests assert both shapes survive that pipeline and produce the + Decimal-typed shapes that ``run()``'s normalization expects. + """ + + @staticmethod + def _compile_var(name: str, value: object) -> object: + """Compile a config var the same way LibreLane does for YAML inputs.""" + from librelane.common import GenericDict + + var = next(v for v in configs if v.name == name) + _, compiled = var.compile( + GenericDict({name: value}), + warning_list_ref=[], + permissive_typing=True, + ) + return compiled + + def test_tile_spacing_accepts_scalar(self) -> None: + v = self._compile_var("FABULOUS_TILE_SPACING", 5) + assert v == Decimal(5) + assert isinstance(v, Decimal) + + def test_tile_spacing_accepts_2_tuple(self) -> None: + v = self._compile_var("FABULOUS_TILE_SPACING", [3, 7]) + assert v == (Decimal(3), Decimal(7)) + assert isinstance(v, tuple) + assert all(isinstance(x, Decimal) for x in v) + + def test_tile_spacing_default_compiles(self) -> None: + """Default ``(0, 0)`` must satisfy its own type.""" + from librelane.common import GenericDict + + var = next(v for v in configs if v.name == "FABULOUS_TILE_SPACING") + # Empty input → falls back to default; compile must not raise. + _, v = var.compile(GenericDict({}), warning_list_ref=[], permissive_typing=True) + assert v == (Decimal(0), Decimal(0)) + + def test_halo_spacing_accepts_scalar(self) -> None: + v = self._compile_var("FABULOUS_HALO_SPACING", 4) + assert v == Decimal(4) + assert isinstance(v, Decimal) + + def test_halo_spacing_accepts_4_tuple(self) -> None: + v = self._compile_var("FABULOUS_HALO_SPACING", [1, 2, 3, 4]) + assert v == (Decimal(1), Decimal(2), Decimal(3), Decimal(4)) + assert isinstance(v, tuple) + assert all(isinstance(x, Decimal) for x in v) + + def test_halo_spacing_default_compiles(self) -> None: + from librelane.common import GenericDict + + var = next(v for v in configs if v.name == "FABULOUS_HALO_SPACING") + _, v = var.compile(GenericDict({}), warning_list_ref=[], permissive_typing=True) + assert v == (Decimal(0), Decimal(0), Decimal(0), Decimal(0)) + + def test_tile_spacing_rejects_wrong_arity_tuple(self) -> None: + """A 3-tuple must be rejected — only scalar or 2-tuple are valid.""" + with pytest.raises(ValueError, match="FABULOUS_TILE_SPACING"): + self._compile_var("FABULOUS_TILE_SPACING", [1, 2, 3]) + + def test_halo_spacing_rejects_wrong_arity_tuple(self) -> None: + """A 2-tuple must be rejected for halo — only scalar or 4-tuple.""" + with pytest.raises(ValueError, match="FABULOUS_HALO_SPACING"): + self._compile_var("FABULOUS_HALO_SPACING", [1, 2]) + + +class TestFlowSubstitutionsAndAttributes: + """Tests for flow substitutions and class-level attributes.""" + def test_io_placement_substitution(self) -> None: """Test IO placement substitution.""" from fabulous.fabric_generator.gds_generator.steps.fabric_IO_placement import ( diff --git a/tests/gds_flow_test/flow_test/test_plugin_fabric_flow.py b/tests/gds_flow_test/flow_test/test_plugin_fabric_flow.py new file mode 100644 index 000000000..5a9298bf3 --- /dev/null +++ b/tests/gds_flow_test/flow_test/test_plugin_fabric_flow.py @@ -0,0 +1,334 @@ +"""Tests for the ``FABulousFabric`` LibreLane-plugin adapter. + +These tests verify the *adapting* layer: how ``FABulousFabric``'s overridden +``__init__`` turns plugin-level config (a fabric CSV path + a mapping of tile +name → pre-hardened macro directory) into the ``self.fabric`` / ``self.macros`` +/ ``self.tile_sizes`` fields that the underlying +:class:`FABulousFabricMacroFlow` expects. +""" + +# ruff: noqa: SLF001 + +import json +from decimal import Decimal +from pathlib import Path + +import pytest +from librelane.flows.flow import Flow, FlowException +from pytest_mock import MockerFixture + +from fabulous.fabric_generator.gds_generator.flows import plugin_fabric_flow +from fabulous.fabric_generator.gds_generator.flows.fabric_macro_flow import ( + FABulousFabricMacroFlow, +) +from fabulous.fabric_generator.gds_generator.flows.plugin_fabric_flow import ( + FABulousFabric, + _build_macros, + _collect_fabric_verilog, +) + + +def _write_macro_dir( + root: Path, name: str, width: str = "100", height: str = "100" +) -> Path: + """Build a minimal macro-output tree as the fabric flow expects.""" + macro_dir: Path = root / name + (macro_dir / "gds").mkdir(parents=True) + (macro_dir / "lef").mkdir(parents=True) + (macro_dir / "vh").mkdir(parents=True) + (macro_dir / "nl").mkdir(parents=True) + (macro_dir / "pnl").mkdir(parents=True) + (macro_dir / "gds" / f"{name}.gds").write_bytes(b"") + (macro_dir / "lef" / f"{name}.lef").write_text("", encoding="utf-8") + (macro_dir / "vh" / f"{name}.vh").write_text("", encoding="utf-8") + (macro_dir / "nl" / f"{name}.nl.v").write_text("", encoding="utf-8") + (macro_dir / "pnl" / f"{name}.pnl.v").write_text("", encoding="utf-8") + (macro_dir / "metrics.json").write_text( + json.dumps({"design__die__bbox": f"0 0 {width} {height}"}), + encoding="utf-8", + ) + return macro_dir + + +class TestFABulousFabricSchema: + """Schema-level assertions for the plugin wrapper.""" + + def test_registered_in_flow_factory(self) -> None: + assert Flow.factory.get("FABulousFabric") is FABulousFabric + + def test_exposes_plugin_config_vars(self) -> None: + names: set[str] = {v.name for v in FABulousFabric.config_vars} + assert { + "FABULOUS_FABRIC_CONFIG", + "FABULOUS_TILE_LIBRARY", + "FABULOUS_TILE_MACROS", + } <= names + + def test_inherits_underlying_config_vars(self) -> None: + plugin_names: set[str] = {v.name for v in FABulousFabric.config_vars} + underlying_names: set[str] = { + v.name for v in FABulousFabricMacroFlow.config_vars + } + assert underlying_names <= plugin_names + + def test_subclass_of_underlying_flow(self) -> None: + """Wrapper inherits ``run()`` / step substitutions from the real flow.""" + assert issubclass(FABulousFabric, FABulousFabricMacroFlow) + + def test_fabulous_fabric_config_accepts_dir_resolver_list( + self, tmp_path: Path + ) -> None: + """``FABULOUS_FABRIC_CONFIG`` must validate when set to ``dir::...``. + + LibreLane rewrites ``dir::file.csv`` to ``refg::$DESIGN_DIR/file.csv`` + and the refg resolver always produces a ``list[str]`` (see + ``librelane/config/preprocessor.py``). If the Variable is typed as a + scalar ``Path`` the type validator crashes with + ``TypeError: argument should be ... not 'list'`` before ``run()`` ever + executes, making ``dir::`` unusable for this variable. + """ + from librelane.common import GenericDict + + var = next( + v for v in FABulousFabric.config_vars if v.name == "FABULOUS_FABRIC_CONFIG" + ) + fabric_csv = tmp_path / "fabric.csv" + fabric_csv.write_text("", encoding="utf-8") + resolved_list: list[str] = [str(fabric_csv)] + mutable = GenericDict({"FABULOUS_FABRIC_CONFIG": resolved_list}) + _, value = var.compile(mutable, warning_list_ref=[]) + + # Concrete fabric path must be recoverable from whatever type we accept. + fabric_path = Path(value[0]) if isinstance(value, list) else Path(value) + assert fabric_path == fabric_csv + + +class TestBuildMacros: + """``_build_macros`` turns a tile-name → macro-dir mapping into Macros.""" + + def test_builds_macro_from_complete_directory_tree(self, tmp_path: Path) -> None: + macro_dir: Path = _write_macro_dir(tmp_path, "LUT4AB", "150", "200") + + macros, tile_sizes = _build_macros({"LUT4AB": macro_dir}) + + assert set(macros) == {"LUT4AB"} + macro = macros["LUT4AB"] + assert [p.name for p in macro.gds] == ["LUT4AB.gds"] + assert macro.lef == [str(macro_dir / "lef" / "LUT4AB.lef")] + assert macro.vh == [str(macro_dir / "vh" / "LUT4AB.vh")] + assert macro.nl == [str(macro_dir / "nl" / "LUT4AB.nl.v")] + assert macro.pnl == [str(macro_dir / "pnl" / "LUT4AB.pnl.v")] + assert macro.spef == {} + # Size comes from metrics.json "design__die__bbox" + assert tile_sizes["LUT4AB"] == (Decimal(150), Decimal(200)) + + def test_reads_width_height_from_metrics_bbox(self, tmp_path: Path) -> None: + """Bbox is ``x1 y1 x2 y2``; we take the last two as width/height.""" + macro_dir: Path = _write_macro_dir(tmp_path, "T", "42.5", "17.125") + + _, tile_sizes = _build_macros({"T": macro_dir}) + + assert tile_sizes["T"] == (Decimal("42.5"), Decimal("17.125")) + + def test_collects_spef_per_corner(self, tmp_path: Path) -> None: + macro_dir: Path = _write_macro_dir(tmp_path, "LUT4AB") + spef_root: Path = macro_dir / "spef" + (spef_root / "nom_tt_025C_1v80").mkdir(parents=True) + (spef_root / "min_ff_n40C_1v95").mkdir(parents=True) + nom_file = spef_root / "nom_tt_025C_1v80" / "LUT4AB.spef" + min_file = spef_root / "min_ff_n40C_1v95" / "LUT4AB.spef" + nom_file.write_text("", encoding="utf-8") + min_file.write_text("", encoding="utf-8") + + macros, _ = _build_macros({"LUT4AB": macro_dir}) + + assert set(macros["LUT4AB"].spef) == {"nom_tt_025C_1v80", "min_ff_n40C_1v95"} + assert macros["LUT4AB"].spef["nom_tt_025C_1v80"] == [nom_file] + assert macros["LUT4AB"].spef["min_ff_n40C_1v95"] == [min_file] + + def test_missing_spef_dir_yields_empty_spef(self, tmp_path: Path) -> None: + macro_dir: Path = _write_macro_dir(tmp_path, "LUT4AB") + + macros, _ = _build_macros({"LUT4AB": macro_dir}) + + assert macros["LUT4AB"].spef == {} + + def test_raises_when_metrics_json_missing(self, tmp_path: Path) -> None: + macro_dir: Path = _write_macro_dir(tmp_path, "LUT4AB") + (macro_dir / "metrics.json").unlink() + + with pytest.raises(FlowException, match="metrics.json not found"): + _build_macros({"LUT4AB": macro_dir}) + + def test_raises_when_die_bbox_missing(self, tmp_path: Path) -> None: + macro_dir: Path = _write_macro_dir(tmp_path, "LUT4AB") + (macro_dir / "metrics.json").write_text("{}", encoding="utf-8") + + with pytest.raises(FlowException, match="missing design__die__bbox"): + _build_macros({"LUT4AB": macro_dir}) + + def test_builds_multiple_tiles(self, tmp_path: Path) -> None: + a = _write_macro_dir(tmp_path, "A", "100", "100") + b = _write_macro_dir(tmp_path, "B", "200", "150") + + macros, tile_sizes = _build_macros({"A": a, "B": b}) + + assert set(macros) == {"A", "B"} + assert tile_sizes == { + "A": (Decimal(100), Decimal(100)), + "B": (Decimal(200), Decimal(150)), + } + + +class TestCollectFabricVerilog: + """``_collect_fabric_verilog`` finds the fabric-level ``*.v`` files.""" + + def test_finds_fabric_name_prefixed_verilog(self, tmp_path: Path) -> None: + target: Path = tmp_path / "MyFab.v" + target.write_text("", encoding="utf-8") + + result: list[Path] = _collect_fabric_verilog(tmp_path, "MyFab") + + assert result[0].resolve() == target.resolve() + + def test_finds_generic_fabric_v(self, tmp_path: Path) -> None: + target: Path = tmp_path / "fabric.v" + target.write_text("", encoding="utf-8") + + result: list[Path] = _collect_fabric_verilog(tmp_path, "MyFab") + + assert any(p.resolve() == target.resolve() for p in result) + + def test_dedupes_overlapping_candidates(self, tmp_path: Path) -> None: + """A file matching multiple globs should appear only once.""" + # ``{fabric_name}.v`` is also returned by the ``*.v`` glob. + (tmp_path / "MyFab.v").write_text("", encoding="utf-8") + (tmp_path / "other.v").write_text("", encoding="utf-8") + + result: list[Path] = _collect_fabric_verilog(tmp_path, "MyFab") + + assert len(result) == len(set(result)) + + def test_raises_when_no_verilog_found(self, tmp_path: Path) -> None: + with pytest.raises(FlowException, match="No fabric Verilog found"): + _collect_fabric_verilog(tmp_path, "MyFab") + + +@pytest.mark.usefixtures("mock_config_load") +class TestFABulousFabricInitAdapter: + """Integration test for ``FABulousFabric.__init__`` adapter logic. + + Mocks ``parseFabricCSV`` so we do not depend on the CSV parser schema, + and verifies the wrapper builds ``fabric``, ``macros``, ``tile_sizes`` + from config variables and populates derived keys. + """ + + def test_init_builds_fabric_macros_and_sizes( + self, mocker: MockerFixture, tmp_path: Path + ) -> None: + fabric_csv: Path = tmp_path / "fabric.csv" + fabric_csv.write_text("", encoding="utf-8") + macro_dir: Path = _write_macro_dir(tmp_path, "LUT4AB", "120", "80") + (tmp_path / "MyFab.v").write_text("", encoding="utf-8") + + mock_fabric = mocker.MagicMock() + mock_fabric.name = "MyFab" + mocker.patch.object( + plugin_fabric_flow, "parseFabricCSV", return_value=mock_fabric + ) + mocker.patch.object(plugin_fabric_flow, "generateFabric") + + flow = FABulousFabric( + config={ + "FABULOUS_FABRIC_CONFIG": [str(fabric_csv)], + "FABULOUS_TILE_LIBRARY": str(tmp_path), + "FABULOUS_TILE_MACROS": {"LUT4AB": str(macro_dir)}, + "DESIGN_DIR": str(tmp_path), + }, + design_dir=str(tmp_path), + pdk="sky130A", + pdk_root=str(tmp_path / "pdk"), + ) + + assert flow.fabric is mock_fabric + assert set(flow.macros) == {"LUT4AB"} + assert flow.tile_sizes["LUT4AB"] == (Decimal(120), Decimal(80)) + # DESIGN_NAME defaulted from fabric.name because config omitted it. + assert flow.config["DESIGN_NAME"] == "MyFab" + # VERILOG_FILES was populated by the fabric-Verilog collector. + assert any(str(p).endswith("MyFab.v") for p in flow.config["VERILOG_FILES"]) + + def test_init_respects_user_supplied_design_name( + self, mocker: MockerFixture, tmp_path: Path + ) -> None: + fabric_csv: Path = tmp_path / "fabric.csv" + fabric_csv.write_text("", encoding="utf-8") + macro_dir: Path = _write_macro_dir(tmp_path, "LUT4AB") + (tmp_path / "MyFab.v").write_text("", encoding="utf-8") + + mock_fabric = mocker.MagicMock() + mock_fabric.name = "MyFab" + mocker.patch.object( + plugin_fabric_flow, "parseFabricCSV", return_value=mock_fabric + ) + mocker.patch.object(plugin_fabric_flow, "generateFabric") + + flow = FABulousFabric( + config={ + "DESIGN_NAME": "CustomName", + "FABULOUS_FABRIC_CONFIG": [str(fabric_csv)], + "FABULOUS_TILE_LIBRARY": str(tmp_path), + "FABULOUS_TILE_MACROS": {"LUT4AB": str(macro_dir)}, + "DESIGN_DIR": str(tmp_path), + }, + design_dir=str(tmp_path), + pdk="sky130A", + pdk_root=str(tmp_path / "pdk"), + ) + + assert flow.config["DESIGN_NAME"] == "CustomName" + + def test_init_raises_on_missing_fabric_csv( + self, mocker: MockerFixture, tmp_path: Path + ) -> None: + macro_dir: Path = _write_macro_dir(tmp_path, "LUT4AB") + mocker.patch.object(plugin_fabric_flow, "parseFabricCSV") + + with pytest.raises(FlowException, match="does not exist"): + FABulousFabric( + config={ + "FABULOUS_FABRIC_CONFIG": [str(tmp_path / "missing.csv")], + "FABULOUS_TILE_LIBRARY": str(tmp_path), + "FABULOUS_TILE_MACROS": {"LUT4AB": str(macro_dir)}, + "DESIGN_DIR": str(tmp_path), + }, + design_dir=str(tmp_path), + pdk="sky130A", + pdk_root=str(tmp_path / "pdk"), + ) + + def test_init_raises_on_empty_tile_macros( + self, mocker: MockerFixture, tmp_path: Path + ) -> None: + fabric_csv: Path = tmp_path / "fabric.csv" + fabric_csv.write_text("", encoding="utf-8") + + mock_fabric = mocker.MagicMock() + mock_fabric.name = "MyFab" + mocker.patch.object( + plugin_fabric_flow, "parseFabricCSV", return_value=mock_fabric + ) + mocker.patch.object(plugin_fabric_flow, "generateFabric") + + with pytest.raises(FlowException, match="FABULOUS_TILE_MACROS is empty"): + FABulousFabric( + config={ + "FABULOUS_FABRIC_CONFIG": [str(fabric_csv)], + "FABULOUS_TILE_LIBRARY": str(tmp_path), + "FABULOUS_TILE_MACROS": {}, + "DESIGN_DIR": str(tmp_path), + }, + design_dir=str(tmp_path), + pdk="sky130A", + pdk_root=str(tmp_path / "pdk"), + ) diff --git a/tests/gds_flow_test/flow_test/test_plugin_tile_flow.py b/tests/gds_flow_test/flow_test/test_plugin_tile_flow.py new file mode 100644 index 000000000..94b0c90e7 --- /dev/null +++ b/tests/gds_flow_test/flow_test/test_plugin_tile_flow.py @@ -0,0 +1,512 @@ +"""Tests for the ``FABulousTile`` LibreLane-plugin adapter. + +These tests verify the *adapting* layer: how ``FABulousTile`` translates +plugin-level config variables and tile-directory inputs into what the +underlying :class:`FABulousTileVerilogMacroFlow` pipeline expects. They do not +exercise the real LibreLane pipeline (that is covered elsewhere). +""" + +# ruff: noqa: SLF001 + +from decimal import Decimal +from pathlib import Path +from unittest.mock import MagicMock + +import pytest +from librelane.flows.flow import Flow, FlowException +from pytest_mock import MockerFixture + +from fabulous.fabric_generator.gds_generator.flows import plugin_tile_flow +from fabulous.fabric_generator.gds_generator.flows.plugin_tile_flow import ( + FABulousTile, + _emit_tile_verilog, +) +from fabulous.fabric_generator.gds_generator.flows.tile_macro_flow import ( + FABulousTileVerilogMacroFlow, +) + + +class TestFABulousTileSchema: + """Schema-level assertions for the plugin wrapper.""" + + def test_registered_in_flow_factory(self) -> None: + """LibreLane must be able to resolve the flow by name.""" + assert Flow.factory.get("FABulousTile") is FABulousTile + + def test_exposes_plugin_config_vars(self) -> None: + """The three plugin-level variables must be declared.""" + names: set[str] = {v.name for v in FABulousTile.config_vars} + assert { + "FABULOUS_TILE_DIR", + "FABULOUS_EXTERNAL_SIDE", + "FABULOUS_SUPERTILE", + } <= names + + def test_inherits_underlying_config_vars(self) -> None: + """Underlying flow's config vars must still be declared so LibreLane validates + the full schema against a single class.""" + plugin_names: set[str] = {v.name for v in FABulousTile.config_vars} + underlying_names: set[str] = { + v.name for v in FABulousTileVerilogMacroFlow.config_vars + } + assert underlying_names <= plugin_names + + def test_inherits_steps_from_underlying_flow(self) -> None: + """``Steps`` matches the underlying flow (SequentialFlow may copy).""" + assert FABulousTile.Steps == FABulousTileVerilogMacroFlow.Steps + + def test_inherits_gating_config_vars(self) -> None: + """Gating vars come from the underlying flow unchanged.""" + assert ( + FABulousTile.gating_config_vars + == FABulousTileVerilogMacroFlow.gating_config_vars + ) + + def test_fabulous_supertile_default_false(self) -> None: + """Default for FABULOUS_SUPERTILE is ``False`` so users can omit it.""" + var = next( + v for v in FABulousTile.config_vars if v.name == "FABULOUS_SUPERTILE" + ) + assert var.default is False + + def test_fabulous_io_pin_order_cfg_is_optional_on_step(self) -> None: + """``FABULOUS_IO_PIN_ORDER_CFG`` must be optional on the step. + + The FABulous IO placer is fully automated: the plugin generates the + pin-order YAML inside ``run()`` and users of the plugin should never + have to set this variable by hand. If the step declares the variable + as required without a default, LibreLane's ``Config.load`` rejects + any mole99-style ``config.yaml`` before ``run()`` can supply a real + value. + """ + from fabulous.fabric_generator.gds_generator.steps.tile_IO_placement import ( + FABulousTileIOPlacement, + ) + + var = next( + v + for v in FABulousTileIOPlacement.config_vars + if v.name == "FABULOUS_IO_PIN_ORDER_CFG" + ) + type_str = str(var.type) + allows_none = "None" in type_str or "Optional" in type_str + has_default = var.default is not None or allows_none + assert allows_none or has_default, ( + f"FABULOUS_IO_PIN_ORDER_CFG must be optional (type={var.type!r}, " + f"default={var.default!r}) — the automated placer generates it." + ) + + def test_fabulous_tile_dir_accepts_dir_resolver_list(self, tmp_path: Path) -> None: + """``FABULOUS_TILE_DIR`` must validate when set to ``dir::...``. + + LibreLane rewrites ``dir::.`` to ``refg::$DESIGN_DIR/.`` and the refg + resolver always produces a ``list[str]`` (see + ``librelane/config/preprocessor.py``). If the Variable is typed as a + scalar ``Path`` the type validator crashes with + ``TypeError: argument should be ... not 'list'`` before ``run()`` ever + executes, making ``dir::.`` unusable for this variable. + """ + from librelane.common import GenericDict + + var = next(v for v in FABulousTile.config_vars if v.name == "FABULOUS_TILE_DIR") + resolved_list: list[str] = [str(tmp_path)] + mutable = GenericDict({"FABULOUS_TILE_DIR": resolved_list}) + _, value = var.compile(mutable, warning_list_ref=[]) + + # Concrete tile_dir must be recoverable from whatever type we accept. + tile_dir_path = Path(value[0]) if isinstance(value, list) else Path(value) + assert tile_dir_path == tmp_path + + +class TestEmitTileVerilog: + """``_emit_tile_verilog`` drives the direct tile generators.""" + + @pytest.fixture + def mock_writer(self, mocker: MockerFixture) -> MagicMock: + return mocker.MagicMock() + + def test_regular_tile_emits_switch_matrix_config_mem_and_tile( + self, mock_writer: MagicMock, mocker: MockerFixture, tmp_path: Path + ) -> None: + from fabulous.fabric_definition.tile import Tile + + tile_dir: Path = tmp_path / "LUT4AB" + tile_dir.mkdir() + mock_tile: MagicMock = mocker.MagicMock(spec=Tile) + mock_tile.name = "LUT4AB" + + actual_paths: list[Path] = [] + gen_sm = mocker.patch.object(plugin_tile_flow, "genTileSwitchMatrix") + gen_sm.side_effect = lambda *_args, **_kwargs: actual_paths.append( + mock_writer.outFileName + ) + gen_cm = mocker.patch.object(plugin_tile_flow, "generateConfigMem") + gen_cm.side_effect = lambda *_args, **_kwargs: actual_paths.append( + mock_writer.outFileName + ) + gen_tile = mocker.patch.object(plugin_tile_flow, "generateTile") + gen_tile.side_effect = lambda *_args, **_kwargs: actual_paths.append( + mock_writer.outFileName + ) + mocker.patch.object( + plugin_tile_flow, + "get_context", + return_value=mocker.MagicMock(switch_matrix_debug_signal=False), + ) + + _emit_tile_verilog(mock_writer, mock_tile, tile_dir) + + expected: list[Path] = [ + tile_dir / "LUT4AB_switch_matrix.v", + tile_dir / "LUT4AB_ConfigMem.v", + tile_dir / "LUT4AB.v", + ] + assert actual_paths == expected + gen_sm.assert_called_once() + gen_cm.assert_called_once_with( + mock_writer, mock_tile, tile_dir / "LUT4AB_ConfigMem.csv" + ) + gen_tile.assert_called_once() + + def test_supertile_emits_per_subtile_then_wrapper( + self, mock_writer: MagicMock, mocker: MockerFixture, tmp_path: Path + ) -> None: + from fabulous.fabric_definition.supertile import SuperTile + from fabulous.fabric_definition.tile import Tile + + tile_dir: Path = tmp_path / "DSP" + tile_dir.mkdir() + top_dir = tile_dir / "DSP_top" + bot_dir = tile_dir / "DSP_bot" + top_dir.mkdir() + bot_dir.mkdir() + top_tile: MagicMock = mocker.MagicMock(spec=Tile) + top_tile.name = "DSP_top" + top_tile.tileDir = top_dir / "DSP_top.csv" + bot_tile: MagicMock = mocker.MagicMock(spec=Tile) + bot_tile.name = "DSP_bot" + bot_tile.tileDir = bot_dir / "DSP_bot.csv" + mock_supertile: MagicMock = mocker.MagicMock(spec=SuperTile) + mock_supertile.name = "DSP" + mock_supertile.tiles = [top_tile, bot_tile] + + emit_regular = mocker.patch.object( + plugin_tile_flow, + "_emit_regular_tile_verilog", + ) + gen_super = mocker.patch.object(plugin_tile_flow, "generateSuperTile") + + _emit_tile_verilog(mock_writer, mock_supertile, tile_dir) + + assert [call.args[2] for call in emit_regular.call_args_list] == [ + top_dir, + bot_dir, + ] + assert mock_writer.outFileName == tile_dir / "DSP.v" + gen_super.assert_called_once() + + +@pytest.mark.usefixtures("mock_config_load") +class TestFABulousTileRunAdapter: + """Integration-style test of ``FABulousTile.run()`` with heavy mocking. + + Verifies the adapter ordering: the tile is parsed, tile Verilog is emitted, + the IO pin YAML is generated, and the config + is populated with the keys the downstream pipeline expects before the + inherited ``SequentialFlow.run`` is invoked. + """ + + @pytest.fixture + def project_tree(self, tmp_path: Path) -> dict[str, Path]: + project: Path = tmp_path / "proj" + tile_dir: Path = project / "Tile" / "LUT4AB" + tile_dir.mkdir(parents=True) + # Existing Verilog discovered via ``**/*.v`` glob on the project Tile dir. + (tile_dir.parent / "shared.v").write_text("", encoding="utf-8") + return {"project": project, "tile_dir": tile_dir} + + def test_run_populates_config_and_delegates( + self, + mocker: MockerFixture, + tmp_path: Path, + project_tree: dict[str, Path], + ) -> None: + from decimal import Decimal + + from librelane.config.config import Config + + from fabulous.fabric_definition.tile import Tile + + tile_dir: Path = project_tree["tile_dir"] + mock_tile: MagicMock = mocker.MagicMock(spec=Tile) + mock_tile.get_min_die_area.return_value = (Decimal(10), Decimal(10)) + mock_tile.name = "LUT4AB" + mock_tile.tileDir = tile_dir / "LUT4AB.csv" + mock_tile.bels = [] + mock_tile.globalConfigBits = 0 + + init_ctx = mocker.patch.object(plugin_tile_flow, "init_context") + mocker.patch.object( + plugin_tile_flow, + "get_context", + return_value=mocker.MagicMock(models_pack=None), + ) + mocker.patch.object(plugin_tile_flow, "VerilogCodeGenerator") + parse_tile = mocker.patch.object( + plugin_tile_flow, "_parse_plugin_tile", return_value=mock_tile + ) + emit_verilog = mocker.patch.object(plugin_tile_flow, "_emit_tile_verilog") + gen_pin_yaml = mocker.patch.object( + plugin_tile_flow, "generate_IO_pin_order_config" + ) + mocker.patch.object( + plugin_tile_flow, "get_pitch", return_value=(Decimal(1), Decimal(1)) + ) + mocker.patch.object(plugin_tile_flow, "get_offset") + mocker.patch.object( + plugin_tile_flow, "get_routing_obstructions", return_value=[] + ) + mocker.patch.object( + plugin_tile_flow, + "round_die_area", + side_effect=lambda cfg: cfg, + ) + + flow = FABulousTile( + config={ + "DESIGN_NAME": "LUT4AB", + "FABULOUS_TILE_DIR": [str(tile_dir)], + "DESIGN_DIR": str(tile_dir), + }, + design_dir=str(tile_dir), + pdk="sky130A", + pdk_root=str(tmp_path / "pdk"), + ) + + flow.run_dir = str(tmp_path / "run") + Path(flow.run_dir).mkdir() + # Stub out the underlying ``SequentialFlow.run`` so we only assert on + # the adapter's config manipulation. + sentinel_state = mocker.MagicMock() + super_run = mocker.patch( + "fabulous.fabric_generator.gds_generator.flows.plugin_tile_flow.SequentialFlow.run", + return_value=(sentinel_state, []), + ) + + state, steps = flow.run(initial_state=mocker.MagicMock()) + + assert (state, steps) == (sentinel_state, []) + # init_context is called in api_mode — no project dir required. + init_ctx.assert_called_once_with(api_mode=True) + parse_tile.assert_called_once_with(tile_dir, "LUT4AB", False) + emit_verilog.assert_called_once() + # Pin YAML should be generated below run_dir. + assert gen_pin_yaml.call_count == 1 + assert gen_pin_yaml.call_args.args[:2] == ( + mock_tile, + Path(flow.run_dir) / "LUT4AB_io_pin_order.yaml", + ) + # Adapter must set the downstream keys. + assert flow.config["DESIGN_NAME"] == "LUT4AB" + assert flow.config["FABULOUS_TILE_LOGICAL_WIDTH"] == 1 + assert flow.config["FABULOUS_TILE_LOGICAL_HEIGHT"] == 1 + assert str(flow.config["FABULOUS_IO_PIN_ORDER_CFG"]).endswith( + "LUT4AB_io_pin_order.yaml" + ) + assert isinstance(flow.config, Config) + super_run.assert_called_once() + + def test_run_raises_on_bad_tile_dir( + self, mocker: MockerFixture, tmp_path: Path + ) -> None: + flow = FABulousTile( + config={ + "DESIGN_NAME": "LUT4AB", + "FABULOUS_TILE_DIR": [str(tmp_path / "does_not_exist")], + "DESIGN_DIR": str(tmp_path), + }, + design_dir=str(tmp_path), + pdk="sky130A", + pdk_root=str(tmp_path / "pdk"), + ) + with pytest.raises(FlowException, match="is not a directory"): + flow.run(initial_state=mocker.MagicMock()) + + def test_run_uses_get_super_tile_when_supertile_flag_set( + self, + mocker: MockerFixture, + tmp_path: Path, + project_tree: dict[str, Path], + ) -> None: + from decimal import Decimal + + from fabulous.fabric_definition.supertile import SuperTile + + tile_dir: Path = project_tree["tile_dir"] + (tile_dir / "DSP_top").mkdir() + + mock_tile: MagicMock = mocker.MagicMock(spec=SuperTile) + mock_tile.max_width = 4 + mock_tile.max_height = 2 + mock_tile.get_min_die_area.return_value = (Decimal(10), Decimal(10)) + mock_tile.name = "LUT4AB" + mock_tile.tiles = [] + mock_tile.bels = [] + + mocker.patch.object(plugin_tile_flow, "init_context") + mocker.patch.object( + plugin_tile_flow, + "get_context", + return_value=mocker.MagicMock(models_pack=None), + ) + mocker.patch.object(plugin_tile_flow, "VerilogCodeGenerator") + parse_tile = mocker.patch.object( + plugin_tile_flow, "_parse_plugin_tile", return_value=mock_tile + ) + mocker.patch.object(plugin_tile_flow, "_emit_tile_verilog") + mocker.patch.object(plugin_tile_flow, "generate_IO_pin_order_config") + mocker.patch.object( + plugin_tile_flow, "get_pitch", return_value=(Decimal(1), Decimal(1)) + ) + mocker.patch.object(plugin_tile_flow, "get_offset") + mocker.patch.object( + plugin_tile_flow, "get_routing_obstructions", return_value=[] + ) + mocker.patch.object( + plugin_tile_flow, "round_die_area", side_effect=lambda cfg: cfg + ) + mocker.patch( + "fabulous.fabric_generator.gds_generator.flows.plugin_tile_flow.SequentialFlow.run", + return_value=(mocker.MagicMock(), []), + ) + + flow = FABulousTile( + config={ + "DESIGN_NAME": "LUT4AB", + "FABULOUS_TILE_DIR": [str(tile_dir)], + "FABULOUS_SUPERTILE": True, + "DESIGN_DIR": str(tile_dir), + }, + design_dir=str(tile_dir), + pdk="sky130A", + pdk_root=str(tmp_path / "pdk"), + ) + + flow.run_dir = str(tmp_path / "run2") + Path(flow.run_dir).mkdir() + flow.run(initial_state=mocker.MagicMock()) + + parse_tile.assert_called_once_with(tile_dir, "LUT4AB", True) + # Supertile logical dimensions must be taken from the tile itself. + assert flow.config["FABULOUS_TILE_LOGICAL_WIDTH"] == 4 + assert flow.config["FABULOUS_TILE_LOGICAL_HEIGHT"] == 2 + + +SYNTHETIC_TILE_NAME = "PLUGIN_TEST_TILE" + + +def _build_synthetic_tile(parent: Path) -> Path: + """Build a minimal valid plugin-tile workspace under ``parent``. + + Produces ``//.csv`` and the matching + ``_switch_matrix.list``. The trailing comma on each line keeps the + ``temp[6]`` lookup in :func:`parseTilesCSV` safe. + """ + name = SYNTHETIC_TILE_NAME + tile_dir = parent / name + tile_dir.mkdir() + (tile_dir / f"{name}.csv").write_text( + f"TILE,{name},\n" + "NORTH,NULL,0,-1,N1END,4,\n" + "SOUTH,S1BEG,0,1,NULL,4,\n" + f"MATRIX,./{name}_switch_matrix.list,\n" + "EndTILE,\n", + encoding="utf-8", + ) + (tile_dir / f"{name}_switch_matrix.list").write_text( + "S1BEG[0|1|2|3],N1END[3|2|1|0]\n", + encoding="utf-8", + ) + return tile_dir + + +@pytest.mark.usefixtures("mock_config_load") +class TestFABulousTileEndToEnd: + """End-to-end exercise of ``FABulousTile.run()`` against a synthetic tile. + + Unlike :class:`TestFABulousTileRunAdapter`, this class does not mock the + plugin's prep work (CSV parsing, RTL emission, pin-YAML generation): it + runs the real generators on disk. Only the PDK-reading helpers and the + inherited ``SequentialFlow.run`` are stubbed, so we cover regressions in + the plugin's actual code path without needing a PDK install or EDA tools. + + The tile workspace is generated programmatically inside ``tmp_path`` so + the test does not depend on any in-tree demo plugin tile. + """ + + @pytest.fixture + def tile_workspace(self, tmp_path: Path) -> Path: + return _build_synthetic_tile(tmp_path) + + def test_run_emits_real_rtl_and_pin_yaml( + self, + tile_workspace: Path, + tmp_path: Path, + mocker: MockerFixture, + ) -> None: + """Run the plugin against a synthetic tile and verify on-disk artifacts. + + Only PDK-touching helpers are stubbed; the generators, parser, and + pin-YAML producer all execute. This is the test that would have + caught the ``FABulous_API.fabric`` AttributeError surfaced by the + librelane CLI smoke run. + """ + # Stub PDK readers; plugin computes a fake DIE_AREA from these. + mocker.patch.object( + plugin_tile_flow, "get_pitch", return_value=(Decimal(1), Decimal(1)) + ) + mocker.patch.object(plugin_tile_flow, "get_offset") + mocker.patch.object( + plugin_tile_flow, "get_routing_obstructions", return_value=[] + ) + mocker.patch.object(plugin_tile_flow, "round_die_area", side_effect=lambda c: c) + # Don't actually run the LibreLane SequentialFlow steps. + mocker.patch( + "fabulous.fabric_generator.gds_generator.flows.plugin_tile_flow.SequentialFlow.run", + return_value=(mocker.MagicMock(), []), + ) + + name = SYNTHETIC_TILE_NAME + flow = FABulousTile( + config={ + "DESIGN_NAME": name, + "FABULOUS_TILE_DIR": [str(tile_workspace)], + "VERILOG_FILES": [], + "DESIGN_DIR": str(tile_workspace), + }, + design_dir=str(tile_workspace), + pdk="sky130A", + pdk_root=str(tmp_path / "pdk"), + ) + flow.run_dir = str(tmp_path / "run") + Path(flow.run_dir).mkdir() + + flow.run(initial_state=mocker.MagicMock()) + + # The plugin must have re-emitted the per-tile RTL artifacts... + assert (tile_workspace / f"{name}.v").exists() + assert (tile_workspace / f"{name}_switch_matrix.v").exists() + # ...bootstrapped the .list switch-matrix into a .csv... + assert (tile_workspace / f"{name}_switch_matrix.csv").exists() + # ...and produced the IO pin-order YAML in the run directory. + pin_yaml = Path(flow.run_dir) / f"{name}_io_pin_order.yaml" + assert pin_yaml.exists() + # And the downstream-facing config keys must be set. + assert flow.config["DESIGN_NAME"] == name + assert flow.config["FABULOUS_IO_PIN_ORDER_CFG"] == str(pin_yaml) + assert flow.config["FABULOUS_TILE_LOGICAL_WIDTH"] == 1 + assert flow.config["FABULOUS_TILE_LOGICAL_HEIGHT"] == 1 + # Generated RTL must be in VERILOG_FILES so downstream synth picks it up. + verilog_files = [str(p) for p in flow.config["VERILOG_FILES"]] + assert any(f"{name}.v" in p for p in verilog_files) + assert any(f"{name}_switch_matrix.v" in p for p in verilog_files) diff --git a/tests/gds_flow_test/script_test/conftest.py b/tests/gds_flow_test/script_test/conftest.py index 6b9a2427f..6fd9472fd 100644 --- a/tests/gds_flow_test/script_test/conftest.py +++ b/tests/gds_flow_test/script_test/conftest.py @@ -608,12 +608,60 @@ def getHeight(self) -> int: # noqa: D401 return self._h +class MockGeom: + """Mock ODB geometry box (mPin shape).""" + + def __init__(self, layer: object, x1: int, y1: int, x2: int, y2: int) -> None: + self._layer, self._x1, self._y1, self._x2, self._y2 = layer, x1, y1, x2, y2 + + def getTechLayer(self) -> object: # noqa: D401, N802 + return self._layer + + def xMin(self) -> int: # noqa: D401, N802 + return self._x1 + + def yMin(self) -> int: # noqa: D401, N802 + return self._y1 + + def xMax(self) -> int: # noqa: D401, N802 + return self._x2 + + def yMax(self) -> int: # noqa: D401, N802 + return self._y2 + + +class MockMPin: + """Mock ODB master pin (geometry container).""" + + def __init__(self, geometry: list[MockGeom]) -> None: + self._g = geometry + + def getGeometry(self) -> list[MockGeom]: # noqa: D401, N802 + return self._g + + +class MockInst: + """Mock ODB instance with a placement location.""" + + def __init__(self, x: int = 0, y: int = 0) -> None: + self._x, self._y = x, y + + def getLocation(self) -> tuple[int, int]: # noqa: D401, N802 + return (self._x, self._y) + + class MockMTerm: """Mock ODB master terminal object.""" - def __init__(self, bbox: MockRect, master: MockMaster) -> None: + def __init__( + self, + bbox: MockRect, + master: MockMaster, + mpins: list[MockMPin] | None = None, + ) -> None: self._bbox = bbox self._master = master + self._mpins = mpins or [] def getBBox(self) -> MockRect: # noqa: D401 return self._bbox @@ -621,13 +669,22 @@ def getBBox(self) -> MockRect: # noqa: D401 def getMaster(self) -> MockMaster: # noqa: D401 return self._master + def getMPins(self) -> list[MockMPin]: # noqa: D401, N802 + return self._mpins + class MockITerm: """Mock ODB instance terminal object.""" - def __init__(self, bbox: MockRect, mterm: MockMTerm) -> None: + def __init__( + self, + bbox: MockRect, + mterm: MockMTerm, + inst: MockInst | None = None, + ) -> None: self._bbox = bbox self._mterm = mterm + self._inst = inst or MockInst(0, 0) def getBBox(self) -> MockRect: # noqa: D401 return self._bbox @@ -635,6 +692,9 @@ def getBBox(self) -> MockRect: # noqa: D401 def getMTerm(self) -> MockMTerm: # noqa: D401 return self._mterm + def getInst(self) -> MockInst: # noqa: D401, N802 + return self._inst + class MockNetIoPlace: """Mock ODB net object for IO place tests.""" @@ -642,6 +702,7 @@ class MockNetIoPlace: def __init__(self, name: str, iterms: list[MockITerm]) -> None: self._name = name self._iterms = iterms + self._bterms: list[MockBTermIoPlace] = [] def getName(self) -> str: # noqa: D401 return self._name @@ -649,25 +710,34 @@ def getName(self) -> str: # noqa: D401 def getITerms(self) -> list[MockITerm]: # noqa: D401 return self._iterms + def getBTerms(self) -> list["MockBTermIoPlace"]: # noqa: D401, N802 + return self._bterms + class MockBTermIoPlace: """Mock ODB boundary term for IO place tests.""" - def __init__(self, name: str, net: MockNetIoPlace) -> None: + def __init__( + self, + name: str, + net: MockNetIoPlace | None, + sig_type: str = "SIGNAL", + ) -> None: self._name = name self._net = net + self._sig_type = sig_type self._bpins: list[MockBPinIoPlace] = [] def getName(self) -> str: # noqa: D401 return self._name def getSigType(self) -> str: # noqa: D401 - return "SIGNAL" + return self._sig_type def getBPins(self) -> list[MockBPinIoPlace]: # noqa: D401 - return self._bpins + return list(self._bpins) - def getNet(self) -> MockNetIoPlace: # noqa: D401 + def getNet(self) -> MockNetIoPlace | None: # noqa: D401 return self._net def _add_bpin(self, bpin: MockBPinIoPlace) -> None: @@ -738,19 +808,41 @@ def dbBPin_create(bterm: MockBTermIoPlace) -> MockBPinIoPlace: return bpin def dbBox_create( - bpin: MockBPinIoPlace, layer: MockLayer, x1: int, y1: int, x2: int, y2: int + bpin: MockBPinIoPlace, + layer: object, + x1: int, + y1: int, + x2: int, + y2: int, ) -> None: # Record for box_recorder (infrastructure tests) box_recorder(bpin, layer, x1, y1, x2, y2) # Record for pin_placement_recorder (behavior tests) + layer_name = layer.getName() if hasattr(layer, "getName") else str(layer) if isinstance(bpin, MockBPinIoPlace) and bpin.bterm_name: - layer_name = layer.getName() pin_placement_recorder.placements.append( (bpin.bterm_name, layer_name, x1, y1, x2, y2) ) + destroyed_bterms: list[MockBTermIoPlace] = [] + destroyed_nets: list[MockNetIoPlace] = [] + + class _DbBTerm: + @staticmethod + def destroy(b: MockBTermIoPlace) -> None: + destroyed_bterms.append(b) + + class _DbNet: + @staticmethod + def destroy(n: MockNetIoPlace) -> None: + destroyed_nets.append(n) + return SimpleNamespace( Rect=MockRect, dbBPin_create=dbBPin_create, dbBox_create=dbBox_create, + dbBTerm=_DbBTerm, + dbNet=_DbNet, + destroyed_bterms=destroyed_bterms, + destroyed_nets=destroyed_nets, ) diff --git a/tests/gds_flow_test/script_test/test_fabric_io_place.py b/tests/gds_flow_test/script_test/test_fabric_io_place.py index d9bc55c53..3e7945cea 100644 --- a/tests/gds_flow_test/script_test/test_fabric_io_place.py +++ b/tests/gds_flow_test/script_test/test_fabric_io_place.py @@ -1,10 +1,4 @@ -"""Integration tests for fabric_io_place - calls the actual io_place function. - -This test file calls the actual io_place() function from fabric_io_place.py -by mocking only the external dependencies (odb, OdbReader) at the module level. - -This tests the real production code, not a reimplementation. -""" +"""Tests for fabric_io_place: stamps BPin geometry from connected ITerms.""" import contextlib from types import SimpleNamespace @@ -12,207 +6,129 @@ import pytest from conftest import ( MockBlockIoPlace, + MockBPinIoPlace, MockBTermIoPlace, MockDie, + MockGeom, + MockInst, MockITerm, - MockLayer, MockMaster, + MockMPin, MockMTerm, MockNetIoPlace, MockReaderIoPlace, MockRect, MockTechIoPlace, - PinPlacementRecorder, ) @pytest.fixture def _io_place_setup( mock_odb_io_place: SimpleNamespace, monkeypatch: pytest.MonkeyPatch -) -> None: # noqa: ANN001, ANN202 - """Setup io_place with mocked OdbReader and odb module.""" - +) -> None: + """Wire the fake ODB module into fabric_io_place for the test.""" from fabulous.fabric_generator.gds_generator.script import fabric_io_place - # Patch odb module using monkeypatch monkeypatch.setattr(fabric_io_place, "odb", mock_odb_io_place) -def _call_io_place( - reader: MockReaderIoPlace, - monkeypatch: pytest.MonkeyPatch, - **kwargs: object, -) -> None: - """Call the actual io_place function with mocked dependencies.""" +def _call_io_place(reader: MockReaderIoPlace, monkeypatch: pytest.MonkeyPatch) -> None: from librelane.scripts.odbpy.reader import OdbReader from fabulous.fabric_generator.gds_generator.script import fabric_io_place - # Mock OdbReader to return our mock reader - def mock_odbreader_init(self: object, *_args: object, **_: object) -> None: + def _init(self: object, *_a: object, **_k: object) -> None: for attr in dir(reader): - if not attr.startswith("_"): - with contextlib.suppress(AttributeError): - setattr(self, attr, getattr(reader, attr)) - - monkeypatch.setattr(OdbReader, "__init__", mock_odbreader_init) - io_place_func = fabric_io_place.io_place - - # Get the actual function from Click command - actual_func = ( - io_place_func.callback if hasattr(io_place_func, "callback") else io_place_func - ) - - # Call with parameters - actual_func( - input_db="dummy.odb", - input_lefs=[], - config_path=None, - reader=reader, - ver_layer=str(kwargs.get("ver_layer", "V")), - hor_layer=str(kwargs.get("hor_layer", "H")), - ver_width_mult=float(kwargs.get("ver_width_mult", 2.0)), # type: ignore - hor_width_mult=float(kwargs.get("hor_width_mult", 2.0)), # type: ignore - hor_length=kwargs.get("hor_length"), - ver_length=kwargs.get("ver_length"), - hor_extension=float(kwargs.get("hor_extension", 0.0)), # type: ignore - ver_extension=float(kwargs.get("ver_extension", 0.0)), # type: ignore - verbose=bool(kwargs.get("verbose", False)), - ) + if attr.startswith("_"): + continue + with contextlib.suppress(AttributeError): + setattr(self, attr, getattr(reader, attr)) + + monkeypatch.setattr(OdbReader, "__init__", _init) + fn = fabric_io_place.io_place + actual = fn.callback if hasattr(fn, "callback") else fn + actual(input_db="x.odb", input_lefs=[], config_path=None, reader=reader) + + +def _make_iterm(inst_x: int, inst_y: int, geoms: list[MockGeom]) -> MockITerm: + master = MockMaster(100, 100) + bbox = MockRect(0, 0, 0, 0) + mterm = MockMTerm(bbox, master, mpins=[MockMPin(geoms)]) + return MockITerm(bbox, mterm, inst=MockInst(inst_x, inst_y)) @pytest.mark.usefixtures("_io_place_setup") -def test_io_place_north_side_placement( - pin_placement_recorder: PinPlacementRecorder, +def test_stamps_bpin_geometry_from_iterm( monkeypatch: pytest.MonkeyPatch, ) -> None: - """Test pin placement on NORTH side - validates coordinates and layer selection.""" - h_layer = MockLayer(width=50, area=10000, name="H") - v_layer = MockLayer(width=50, area=10000, name="V") - tech = MockTechIoPlace(h_layer, v_layer) - die = MockDie(0, 0, 1000, 1000) + """BPin gets one box per mPin geometry, shifted by the instance location.""" + geom = MockGeom("Metal2", 10, 20, 30, 40) + iterm = _make_iterm(1000, 2000, [geom]) + net = MockNetIoPlace("sig", [iterm]) + bterm = MockBTermIoPlace("sig", net) - master = MockMaster(100, 100) - mterm_bbox = MockRect(25, 100, 50, 0) - iterm_bbox = MockRect(400, 400, 100, 100) - mterm = MockMTerm(mterm_bbox, master) - iterm = MockITerm(iterm_bbox, mterm) - net = MockNetIoPlace("north_pin", [iterm]) - bterm = MockBTermIoPlace("north_pin", net) - - block = MockBlockIoPlace(die, [bterm]) - reader = MockReaderIoPlace(100.0, tech, block) - - _call_io_place( - reader, - monkeypatch, - ver_layer="V", - hor_layer="H", - ver_width_mult=2.0, - hor_width_mult=2.0, - hor_length=None, - ver_length=None, - hor_extension=0.0, - ver_extension=0.0, - verbose=False, - ) - - assert len(pin_placement_recorder.placements) == 1 - name, layer, x1, y1, x2, y2 = pin_placement_recorder.placements[0] - - # Verify pin name and layer selection - assert name == "north_pin" - assert layer == "V", "NORTH side should use vertical layer" - - # Verify Y coordinates - pin should extend to die boundary - assert y2 == 1000, f"Pin should extend to die yMax (1000), got {y2}" - - # Verify X coordinates - pin should be centered on iterm - # iterm_bbox is at (400, 400) with width 100, so center is at 450 - # Pin width = layer_width (50) * width_mult (2.0) = 100 - # So pin should span from center - width/2 to center + width/2 - assert x1 == 400, f"Pin x1 should be 400 (iterm x), got {x1}" - - # Verify pin width is correct (using width multiplier) - expected_width = v_layer.getWidth() * 2.0 # width_mult = 2.0 - actual_width = x2 - x1 - assert actual_width == expected_width, ( - f"Pin width should be {expected_width}, got {actual_width}" - ) + block = MockBlockIoPlace(MockDie(0, 0, 1000, 1000), [bterm]) + reader = MockReaderIoPlace(100.0, MockTechIoPlace(None, None), block) + + _call_io_place(reader, monkeypatch) + + pins = bterm.getBPins() + assert len(pins) == 1 + pin = pins[0] + assert pin.status == "FIRM" + assert pin.bterm_name == "sig" @pytest.mark.usefixtures("_io_place_setup") -def test_io_place_all_four_sides( - pin_placement_recorder: PinPlacementRecorder, +def test_skips_power_and_ground( monkeypatch: pytest.MonkeyPatch, ) -> None: - """Test that pins can be placed on all four sides with correct coordinates.""" - h_layer = MockLayer(width=50, area=10000, name="H") - v_layer = MockLayer(width=50, area=10000, name="V") - tech = MockTechIoPlace(h_layer, v_layer) - die = MockDie(0, 0, 1000, 1000) + """POWER/GROUND BTerms must not be touched.""" + pwr = MockBTermIoPlace("VPWR", None, sig_type="POWER") + gnd = MockBTermIoPlace("VGND", None, sig_type="GROUND") - master = MockMaster(100, 100) + block = MockBlockIoPlace(MockDie(0, 0, 100, 100), [pwr, gnd]) + reader = MockReaderIoPlace(100.0, MockTechIoPlace(None, None), block) + + _call_io_place(reader, monkeypatch) + + assert pwr.getBPins() == [] + assert gnd.getBPins() == [] + + +@pytest.mark.usefixtures("_io_place_setup") +def test_destroys_orphan_bterm_with_no_iterms( + mock_odb_io_place: SimpleNamespace, monkeypatch: pytest.MonkeyPatch +) -> None: + """A signal BTerm whose net has no ITerms is destroyed (and so is the net).""" + net = MockNetIoPlace("orphan", []) + bterm = MockBTermIoPlace("orphan", net) + + block = MockBlockIoPlace(MockDie(0, 0, 100, 100), [bterm]) + reader = MockReaderIoPlace(100.0, MockTechIoPlace(None, None), block) + + _call_io_place(reader, monkeypatch) + + assert bterm in mock_odb_io_place.destroyed_bterms + assert net in mock_odb_io_place.destroyed_nets + + +@pytest.mark.usefixtures("_io_place_setup") +def test_leaves_existing_bpins_alone( + monkeypatch: pytest.MonkeyPatch, +) -> None: + """If a BTerm already has a BPin, io_place skips it without re-stamping.""" + geom = MockGeom("Metal2", 0, 0, 10, 10) + iterm = _make_iterm(0, 0, [geom]) + net = MockNetIoPlace("sig", [iterm]) + bterm = MockBTermIoPlace("sig", net) + pre_existing = MockBPinIoPlace(bterm.getName()) + bterm._add_bpin(pre_existing) # noqa: SLF001 + + block = MockBlockIoPlace(MockDie(0, 0, 100, 100), [bterm]) + reader = MockReaderIoPlace(100.0, MockTechIoPlace(None, None), block) + + _call_io_place(reader, monkeypatch) - bterms = [] - for side, bbox in [ - ("north", MockRect(25, 100, 50, 0)), - ("south", MockRect(25, 0, 50, 0)), - ("east", MockRect(100, 25, 0, 50)), - ("west", MockRect(0, 25, 0, 50)), - ]: - iterm_bbox = MockRect(400, 400, 100, 100) - mterm = MockMTerm(bbox, master) - iterm = MockITerm(iterm_bbox, mterm) - net = MockNetIoPlace(f"{side}_pin", [iterm]) - bterm = MockBTermIoPlace(f"{side}_pin", net) - bterms.append(bterm) - - block = MockBlockIoPlace(die, bterms) - reader = MockReaderIoPlace(100.0, tech, block) - - _call_io_place( - reader, - monkeypatch, - ver_layer="V", - hor_layer="H", - ver_width_mult=2.0, - hor_width_mult=2.0, - hor_length=None, - ver_length=None, - hor_extension=0.0, - ver_extension=0.0, - verbose=False, - ) - - assert len(pin_placement_recorder.placements) == 4 - - placements_by_name = { - name: (layer, x1, y1, x2, y2) - for name, layer, x1, y1, x2, y2 in pin_placement_recorder.placements - } - - # Verify layer selection based on side - assert placements_by_name["north_pin"][0] == "V", "North should use vertical layer" - assert placements_by_name["south_pin"][0] == "V", "South should use vertical layer" - assert placements_by_name["east_pin"][0] == "H", "East should use horizontal layer" - assert placements_by_name["west_pin"][0] == "H", "West should use horizontal layer" - - # Verify boundary coordinates for each side - # North pin should extend to y=1000 (die yMax) - north_y2 = placements_by_name["north_pin"][4] - assert north_y2 == 1000, ( - f"North pin should extend to die yMax (1000), got {north_y2}" - ) - - # South pin should extend to y=0 (die yMin) - south_y1 = placements_by_name["south_pin"][2] - assert south_y1 == 0, f"South pin should extend to die yMin (0), got {south_y1}" - - # East pin should extend to x=1000 (die xMax) - east_x2 = placements_by_name["east_pin"][3] - assert east_x2 == 1000, f"East pin should extend to die xMax (1000), got {east_x2}" - - # West pin should extend to x=0 (die xMin) - west_x1 = placements_by_name["west_pin"][1] - assert west_x1 == 0, f"West pin should extend to die xMin (0), got {west_x1}" + # Still exactly one BPin, no new one was created. + assert bterm.getBPins() == [pre_existing]