"""GDS macro-hardening flow commands for the FABulous REPL.
Harden tiles and the fabric into GDS macros via the LibreLane flow.
"""
from decimal import Decimal
from pathlib import Path
from typing import Annotated, cast
import yaml
from cmd2 import with_annotated, with_category
from cmd2.annotated import Argument, Option
from loguru import logger
from fabulous.fabric_generator.gds_generator.steps.tile_area_opt import OptMode
from fabulous.fabulous_repl.command_set_base import (
CMD_FABRIC_FLOW,
ReplCommandSet,
)
from fabulous.fabulous_repl.helper import (
CommandPipeline,
)
from fabulous.fabulous_settings import get_context, is_pdk_config_set
def _require_directional_mode(
opt_mode: OptMode, implied: OptMode, flag: str
) -> OptMode:
"""Return the directional mode a ``--fix-*`` flag implies, or raise on conflict.
Parameters
----------
opt_mode : OptMode
The mode requested via ``--optimise`` (``NO_OPT`` when unset).
implied : OptMode
The directional mode the fix flag requires.
flag : str
The flag name, used for the error message.
Returns
-------
OptMode
``implied`` when compatible with ``opt_mode``.
Raises
------
ValueError
If ``opt_mode`` is an explicit mode other than ``implied``.
"""
if opt_mode in (OptMode.NO_OPT, implied):
return implied
raise ValueError(
f"{flag} is only valid with --optimise {implied.value}, not {opt_mode.value}."
)
def _resolve_directional_fix(
opt_mode: OptMode,
fix_width: Decimal | None,
fix_height: Decimal | None,
) -> tuple[OptMode, list[int | Decimal] | None]:
"""Resolve ``--optimise`` plus ``--fix-*`` into a mode and DIE_AREA override.
A fixed axis pins one side and minimises the other: ``--fix-width`` pairs with
``find_min_height`` and ``--fix-height`` with ``find_min_width``. The minimised
axis starts square; ``TileOptimisation`` re-seeds it from the synthesised cell
area, so only the fixed value needs to be supplied.
Parameters
----------
opt_mode : OptMode
The mode requested via ``--optimise``.
fix_width : Decimal | None
Locked tile width, if ``--fix-width`` was given.
fix_height : Decimal | None
Locked tile height, if ``--fix-height`` was given.
Returns
-------
tuple[OptMode, list[int | Decimal] | None]
The resolved optimisation mode and the DIE_AREA override, or ``None`` when
neither fix flag is set.
Raises
------
ValueError
If both fix flags are given, or a fix flag contradicts ``--optimise``.
"""
if fix_width is not None and fix_height is not None:
raise ValueError("Specify only one of --fix-width / --fix-height.")
if fix_width is not None:
mode = _require_directional_mode(
opt_mode, OptMode.FIND_MIN_HEIGHT, "--fix-width"
)
return mode, [0, 0, fix_width, fix_width]
if fix_height is not None:
mode = _require_directional_mode(
opt_mode, OptMode.FIND_MIN_WIDTH, "--fix-height"
)
return mode, [0, 0, fix_height, fix_height]
return opt_mode, None
[docs]
class MacroFlowCommandSet(ReplCommandSet):
"""Harden tiles and the fabric into GDS macros via the LibreLane flow."""
@with_category(CMD_FABRIC_FLOW)
@with_annotated
[docs]
def do_gen_tile_macro(
self,
tile: Annotated[
str,
Argument(
help_text="A tile",
completer=lambda self: [
tile.name for tile in self._cmd.fabulousAPI.getTiles()
],
),
],
optimise: Annotated[
OptMode,
Option(
"--optimise",
"-opt",
nargs="?",
const=OptMode.BALANCE,
help_text=(
"Optimize the GDS layout. Available modes: "
+ ", ".join(m.value for m in OptMode)
),
),
] = OptMode.NO_OPT,
override: Annotated[
Path | None,
Option(
"--override", help_text="Override config with a custom YAML config file"
),
] = None,
fix_width: Annotated[
Decimal | None,
Option(
"--fix-width",
metavar="WIDTH",
help_text=(
"Lock the tile width to WIDTH and minimise the height "
"(implies --optimise find_min_height)."
),
),
] = None,
fix_height: Annotated[
Decimal | None,
Option(
"--fix-height",
metavar="HEIGHT",
help_text=(
"Lock the tile height to HEIGHT and minimise the width "
"(implies --optimise find_min_width)."
),
),
] = None,
io_pin_config: Annotated[
Path | None,
Option(
"--io-pin-config",
help_text="Path to a custom IO pin config YAML file",
),
] = None,
) -> None:
"""Generate GDSII files for a specific tile.
This command generates GDSII files for the specified tile, allowing for the
physical representation of the tile to be created.
"""
repl = self._cmd
if not tile:
logger.error("Tile name must be specified")
return
if not is_pdk_config_set():
logger.error(
"PDK configuration is not set. Please set the PDK configuration to "
"generate tile macros."
)
return
try:
opt_mode, die_area_override = _resolve_directional_fix(
optimise, fix_width, fix_height
)
except ValueError as exc:
logger.error(str(exc))
return
custom_overrides: dict = {}
if override:
custom_overrides.update(yaml.safe_load(override.read_text()) or {})
if die_area_override is not None:
custom_overrides["FABULOUS_OPT_MODE"] = opt_mode
custom_overrides["DIE_AREA"] = die_area_override
tile_dir = repl.projectDir / "Tile" / tile
pin_order_file = tile_dir / f"{tile}_io_pin_order.yaml"
if not tile_dir.exists():
logger.error(f"Tile directory {tile_dir} does not exist")
return
if not io_pin_config:
if tile_obj := repl.fabulousAPI.getTile(tile):
repl.fabulousAPI.gen_io_pin_order_config(tile_obj, pin_order_file)
else:
super_tile = repl.fabulousAPI.getSuperTile(tile)
if super_tile is None:
logger.error(f"Tile {tile} not found in fabric definition")
return
repl.fabulousAPI.gen_io_pin_order_config(super_tile, pin_order_file)
else:
pin_order_file = io_pin_config.resolve()
repl.fabulousAPI.genTileMacro(
tile_dir,
pin_order_file,
tile_dir / "macro",
cast("str", get_context().pdk),
cast("Path", get_context().pdk_root),
optimisation=opt_mode,
base_config_path=repl.projectDir / "Tile" / "include" / "gds_config.yaml",
config_override_path=tile_dir / "gds_config.yaml",
custom_config_overrides=custom_overrides or None,
)
@with_annotated
@with_category(CMD_FABRIC_FLOW)
[docs]
def do_gen_all_tile_macros(
self,
parallel: Annotated[
bool,
Option("--parallel", "-p", help_text="Generate tile macros in parallel"),
] = False,
optimise: Annotated[
OptMode | None,
Option(
"--optimise",
"-opt",
nargs="?",
const=OptMode.BALANCE,
help_text=(
"Optimize the GDS layout of all tiles. Available modes: "
+ ", ".join(m.value for m in OptMode)
),
),
] = None,
) -> None:
"""Generate GDSII files for all tiles in the fabric."""
repl = self._cmd
commands = CommandPipeline(self._cmd)
for i in sorted(repl.all_tile):
if optimise:
commands.add_step(f"gen_tile_macro {i} --optimise {optimise.value}")
else:
commands.add_step(f"gen_tile_macro {i}")
if not parallel:
commands.execute()
else:
commands.execute_parallel()
@with_category(CMD_FABRIC_FLOW)
[docs]
def do_gen_fabric_macro(self, *_args: str) -> None:
"""Generate GDSII files for the entire fabric."""
repl = self._cmd
if not is_pdk_config_set():
logger.error(
"PDK configuration is not set. Please set the PDK configuration to "
"generate fabric macros."
)
return
tile_macro_root = repl.projectDir / "Tile"
tile_macro_paths: dict[str, Path] = {}
for tile_dir in tile_macro_root.iterdir():
if not tile_dir.is_dir():
continue
macro_dir = tile_dir / "macro" / "final_views"
if macro_dir.exists():
tile_macro_paths[tile_dir.name] = macro_dir
if not tile_macro_paths:
logger.error(
"No tile macro directories found. Generate tile GDS results first."
)
return
(repl.projectDir / "gds").mkdir(exist_ok=True)
(repl.projectDir / "Fabric" / "macro").mkdir(exist_ok=True)
repl.fabulousAPI.fabric_stitching(
tile_macro_paths,
repl.projectDir
/ "Fabric"
/ f"{repl.fabulousAPI.fabric.name}.{repl.extension}",
repl.projectDir / "Fabric" / "macro",
cast("str", get_context().pdk),
cast("Path", get_context().pdk_root),
base_config_path=repl.projectDir / "Fabric" / "gds_config.yaml",
)
@with_category(CMD_FABRIC_FLOW)
@with_annotated
[docs]
def do_run_FABulous_eFPGA_macro(
self,
tile_opt_info: Annotated[
str | None,
Option(
"--tile-opt-info",
help_text="Path to tile optimisation summary JSON to skip Step 1",
),
] = None,
nlp_only: Annotated[
bool,
Option(
"--nlp-only",
help_text="Run exploration and NLP only, skip recompilation",
),
] = False,
nlp_area_margin: Annotated[
float,
Option(
"--nlp-area-margin",
help_text="Area margin for NLP constraint (default: 0.05 = 5%%)",
),
] = 0.05,
) -> None:
"""Run the full FABulous eFPGA macro generation flow."""
repl = self._cmd
if not is_pdk_config_set():
logger.error(
"PDK configuration is not set. Please set the PDK configuration to "
"run the full FABulous eFPGA macro generation flow."
)
return
(repl.projectDir / "Fabric" / "macro").mkdir(exist_ok=True)
tile_opt_config = Path(tile_opt_info) if tile_opt_info else None
repl.fabulousAPI.full_fabric_automation(
repl.projectDir,
repl.projectDir / "Fabric" / "macro",
cast("str", get_context().pdk),
cast("Path", get_context().pdk_root),
base_config_path=repl.projectDir / "Fabric" / "gds_config.yaml",
tile_opt_config=tile_opt_config,
nlp_only=nlp_only,
nlp_area_margin=nlp_area_margin,
)