Source code for fabulous.fabric_cad.timing_model.hdlnx.sdfnx.timing_graph

"""SDF Timing Graph Generation Module.

This module provides functionality to parse SDF files and generate timing directed
graphs using NetworkX.
"""

from pathlib import Path

import networkx as nx
from sdf_timing import sdfparse

from fabulous.fabric_cad.timing_model.models import (
    Component,
    DelayType,
    SDFCellType,
    SDFGobject,
)


def _as_float(value: float | None, default: float = 0.0) -> float:
    """Convert `value` to float, treating None/missing as `default`."""
    if value is None:
        return default
    return float(value)


[docs] def delay_type(delay_paths: dict, kind: DelayType = DelayType.MAX_ALL) -> float: """Determine the delay value from a delay dictionary. Based on the specified type. In the SDF format, delays can be specified for different conditions (fast, slow, nominal). For example, a delay dictionary might look like this: delay_paths{ "fast": {"min": 1.0, "avg": None, "max": 2.0}, "slow": {"min": 3.0, "avg": None, "max": 4.0}, "nominal": {"min": 2.0, "avg": None, "max": 3.0} } which will be in the SDF as: ((1.0::2.0) (3.0::4.0)) for fast and slow, and (2.0::3.0) for nominal. Parameters ---------- delay_paths : dict A dictionary containing delay information. kind : DelayType The type of delay to extract. Options include: DelayType.MIN_ALL, DelayType.MAX_ALL, DelayType.AVG_ALL, DelayType.AVG_FAST, DelayType.AVG_SLOW, DelayType.MAX_FAST, DelayType.MAX_SLOW, DelayType.MIN_FAST, DelayType.MIN_SLOW. Returns ------- float The calculated delay value. Raises ------ ValueError If an unknown delay type is specified. """ nominal = delay_paths.get("nominal") if isinstance(nominal, dict) and ("min" in nominal or "max" in nominal): nmin = _as_float(nominal.get("min")) nmax = _as_float(nominal.get("max")) return max(nmin, nmax) fast = delay_paths.get("fast", {}) or {} slow = delay_paths.get("slow", {}) or {} fast_min: float = _as_float(fast.get("min")) fast_max: float = _as_float(fast.get("max")) slow_min: float = _as_float(slow.get("min")) slow_max: float = _as_float(slow.get("max")) match kind: case DelayType.MIN_ALL: return min(fast_min, fast_max, slow_min, slow_max) case DelayType.MAX_ALL: return max(fast_min, fast_max, slow_min, slow_max) case DelayType.AVG_ALL: return (fast_min + fast_max + slow_min + slow_max) / 4.0 case DelayType.AVG_FAST: return (fast_min + fast_max) / 2.0 case DelayType.AVG_SLOW: return (slow_min + slow_max) / 2.0 case DelayType.MAX_FAST: return max(fast_min, fast_max) case DelayType.MAX_SLOW: return max(slow_min, slow_max) case DelayType.MIN_FAST: return min(fast_min, fast_max) case DelayType.MIN_SLOW: return min(slow_min, slow_max) case _: raise ValueError(f"Unknown delay type: {kind!r}")
[docs] def split_instance_pin(name: str, hier_sep: str) -> tuple[str, str]: """Separate instance and pin from a hierarchical name. Split a hierarchical name into instance and pin parts based on the separator. For example, given the name "_2988_/Q" and separator "/", it returns ("_2988_", "Q"). Parameters ---------- name : str The hierarchical name to split. hier_sep : str The separator used in the hierarchical name. Returns ------- tuple[str, str] A tuple containing the instance and pin names. """ inst, _sep, pin = name.rpartition(hier_sep) return inst, pin
[docs] def parse_sdf(sdf_file: Path, delay_type_str: DelayType) -> SDFGobject: """Parse the SDF file to extract INTERCONNECT and IOPATH components. Parse the SDF file to extract INTERCONNECT and IOPATH components with their delays. Also extracts header information, cell names, and instance-component mappings. But IOPATHs and INTERCONNECTS are used to build the timing graph. Timing checks (hold, setup, reset, recover, width) and other components are stored in the instances dictionary. Parameters ---------- sdf_file : Path Path to the SDF file. delay_type_str : DelayType The type of delay to extract (e.g., DelayType.MAX_ALL). Returns ------- SDFGobject An SDFGobject containing the parsed SDF data, including header information, cell names, instance-component mappings, and lists of IOPATH and INTERCONNECT components. """ sdf_data: dict = sdfparse.parse(sdf_file.read_text()) header_info: dict = sdf_data.get("header", {}) io_paths: list[Component] = [] interconnects: list[Component] = [] cells: list[str] = list(sdf_data.get("cells", {}).keys()) instances: dict[str, list[Component]] = {} hier_sep: str = header_info.get("divider", "/") for cell_name, cell_data in sdf_data["cells"].items(): for instance_name, instance_data in cell_data.items(): if instance_name is not None: instances[instance_name] = [] for component, component_data in instance_data.items(): inst_pin_from: tuple[str, str] = split_instance_pin( component_data["from_pin"], hier_sep ) inst_pin_to: tuple[str, str] = split_instance_pin( component_data["to_pin"], hier_sep ) single_delay: float = delay_type( component_data["delay_paths"], delay_type_str ) one_inst: bool = inst_pin_from[0] == inst_pin_to[0] # IOPATH is a combinational path that can change the output # of a cell based on changes to the input. if component_data["type"] == "iopath": io_paths.append( Component( c_type=SDFCellType.IOPATH, cell_name=cell_name, connection_string=component, from_cell_instance=instance_name, to_cell_instance=instance_name, from_cell_pin=component_data["from_pin"], to_cell_pin=component_data["to_pin"], delay=single_delay, delay_paths=component_data["delay_paths"], is_one_cell_instance=True, is_timing_check=component_data["is_timing_check"], is_timing_env=component_data["is_timing_env"], is_absolute=component_data["is_absolute"], is_incremental=component_data["is_incremental"], is_cond=component_data["is_cond"], cond_equation=component_data["cond_equation"], from_pin_edge=component_data["from_pin_edge"], to_pin_edge=component_data["to_pin_edge"], ) ) # Since SDF does not model for a FF a path from D -> Q as IOPATH # only CLK -> Q is IOPATH, since the D -> Q path is not combinational # but sequential. Swap pins and model D --(delay 0)--> CLK --> Q # beacuse CLK always controls the output Q. if component_data["type"] in ("setup", "hold"): io_paths.append( Component( c_type=SDFCellType.IOPATH, cell_name=cell_name, connection_string=str(component).split("_", 1)[-1], from_cell_instance=instance_name, to_cell_instance=instance_name, from_cell_pin=component_data["to_pin"], to_cell_pin=component_data["from_pin"], delay=0.0, delay_paths=None, is_one_cell_instance=True, is_timing_check=component_data["is_timing_check"], is_timing_env=component_data["is_timing_env"], is_absolute=component_data["is_absolute"], is_incremental=component_data["is_incremental"], is_cond=component_data["is_cond"], cond_equation=component_data["cond_equation"], from_pin_edge=None, to_pin_edge=None, ) ) # INTERCONNECT is a path that connects two different cell instances, # which can be combinational or sequential. if component_data["type"] == "interconnect": interconnects.append( Component( c_type=SDFCellType.INTERCONNECT, cell_name=cell_name, connection_string=component, from_cell_instance=inst_pin_from[0], to_cell_instance=inst_pin_to[0], from_cell_pin=inst_pin_from[1], to_cell_pin=inst_pin_to[1], delay=single_delay, delay_paths=component_data["delay_paths"], is_one_cell_instance=one_inst, is_timing_check=component_data["is_timing_check"], is_timing_env=component_data["is_timing_env"], is_absolute=component_data["is_absolute"], is_incremental=component_data["is_incremental"], is_cond=component_data["is_cond"], cond_equation=component_data["cond_equation"], from_pin_edge=component_data["from_pin_edge"], to_pin_edge=component_data["to_pin_edge"], ) ) # Other components include timing checks (hold, setup, reset, # recover, width) and other types of paths. if component_data["type"] != "interconnect": instances[instance_name].append( Component( c_type=SDFCellType(component_data["type"].upper()), cell_name=cell_name, connection_string=component, from_cell_instance=instance_name, to_cell_instance=instance_name, from_cell_pin=component_data["from_pin"], to_cell_pin=component_data["to_pin"], delay=single_delay, delay_paths=component_data["delay_paths"], is_one_cell_instance=True, is_timing_check=component_data["is_timing_check"], is_timing_env=component_data["is_timing_env"], is_absolute=component_data["is_absolute"], is_incremental=component_data["is_incremental"], is_cond=component_data["is_cond"], cond_equation=component_data["cond_equation"], from_pin_edge=component_data["from_pin_edge"], to_pin_edge=component_data["to_pin_edge"], ) ) # io_paths, interconnects, header_info, sdf_data, cells, instances return SDFGobject( nx_graph=nx.DiGraph(), hier_sep=hier_sep, header_info=header_info, sdf_data=sdf_data, cells=cells, instances=instances, io_paths=io_paths, interconnects=interconnects, )
[docs] def gen_timing_digraph(sdf_file: Path, delay_type_str: DelayType) -> SDFGobject: """Generate a timing directed networkx graph (DiGraph) from the SDF file. Also extracts header information, cell names, and instance-component mappings. But IOPATHs and INTERCONNECTS are used to build the timing graph. Timing checks (hold, setup, reset, recover, width) and other components are stored in the instances dictionary. Parameters ---------- sdf_file : Path Path to the SDF file. delay_type_str : DelayType The type of delay to extract (e.g., DelayType.MAX_ALL). Returns ------- SDFGobject An SDFGobject containing the generated timing graph, header information, cell names, instance-component mappings, and lists of IOPATH and INTERCONNECT components. """ sdf_gobject: SDFGobject = parse_sdf(sdf_file, delay_type_str) def node(inst: str, pin: str) -> str: """Create a node name from instance and pin. It uses the hierarchical separator. """ return f"{inst}{sdf_gobject.hier_sep}{pin}".removeprefix(sdf_gobject.hier_sep) # Includes both IOPATHs and INTERCONNECTS, but not timing checks # or other components. components: list[Component] = sdf_gobject.io_paths + sdf_gobject.interconnects for comp in components: sdf_gobject.nx_graph.add_edge( node(comp.from_cell_instance, comp.from_cell_pin), node(comp.to_cell_instance, comp.to_cell_pin), weight=comp.delay, component=comp, ) return sdf_gobject