Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ print(f"Path delay: {delays[0].nominal.max}")

## CLI Reference

The `sdf-toolkit` command provides 19 subcommands for comprehensive SDF manipulation:
The `sdf-toolkit` command provides 20 subcommands for comprehensive SDF manipulation:

### File I/O

Expand Down Expand Up @@ -148,6 +148,7 @@ The `sdf-toolkit` command provides 19 subcommands for comprehensive SDF manipula
| `merge` | Combine multiple SDF files |
| `normalize` | Convert all delays to target timescale |
| `annotate` | Generate Verilog specify blocks |
| `port-to-interconnect` | Rewrite PORT delays as INTERCONNECT using a netlist |

### Visualization

Expand Down Expand Up @@ -256,6 +257,12 @@ sdf-toolkit normalize design.sdf --target 1ns --format sdf > design_ns.sdf
# Annotate Verilog with specify blocks
sdf-toolkit annotate design.sdf cells.v -o annotated_cells.v

# Rewrite PORT (sink-only) delays as INTERCONNECT (source -> sink) delays.
# The driver of each PORT load is recovered from the gate-level netlist via
# Yosys, so back-ends that emit PORT (e.g. Libero) become consumable by tools
# that only understand INTERCONNECT.
sdf-toolkit port-to-interconnect design.sdf netlist.v -o design_interconnect.sdf

# Decompose unknown delay
sdf-toolkit decompose \
--total '{"nominal": {"min": null, "avg": null, "max": 5.0}}' \
Expand Down
60 changes: 58 additions & 2 deletions src/sdf_toolkit/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,54 @@ def annotate(
typer.echo(f"Written to {output}")


@app.command(name="port-to-interconnect")
def port_to_interconnect_cmd(
sdf_file: Annotated[
Path,
typer.Argument(help="Path to the SDF file containing PORT delays."),
],
verilog_file: Annotated[
Path,
typer.Argument(help="Path to the gate-level Verilog netlist."),
],
output: Annotated[
Path | None,
typer.Option("--output", "-o", help="Output file (default: stdout)."),
] = None,
timescale: Annotated[
str,
typer.Option("--timescale", "-t", help="Timescale when the SDF sets none."),
] = "1ps",
top_module: Annotated[
str | None,
typer.Option("--top", help="Top module name (default: SDF DESIGN)."),
] = None,
) -> None:
"""Rewrite PORT (sink-only) delays as INTERCONNECT (source->sink) delays.

The driver of each PORT load is recovered from the Verilog netlist via Yosys.
"""
from sdf_toolkit.io.annotate import parse_yosys_json, run_yosys
from sdf_toolkit.transform.interconnect import (
DriverResolutionError,
port_to_interconnect,
)

sdf = _load_sdf(sdf_file)
design = parse_yosys_json(run_yosys(verilog_file))
try:
result = port_to_interconnect(sdf, design, top_module=top_module)
except DriverResolutionError as exc:
raise typer.BadParameter(str(exc)) from exc

text = sdf_emit(result, timescale=result.header.timescale or timescale)
if output is not None:
output.write_text(text)
typer.echo(f"Written to {output}")
else:
typer.echo(text)


@app.command()
def normalize(
sdf_file: Annotated[
Expand Down Expand Up @@ -731,7 +779,10 @@ def query_cmd(

sdf = _load_sdf(sdf_file)

entry_types = [EntryType(e) for e in entry_type] if entry_type else None
try:
entry_types = [EntryType(e) for e in entry_type] if entry_type else None
except ValueError as exc:
raise typer.BadParameter(str(exc), param_hint="--entry-type") from exc

result = query(
sdf,
Expand Down Expand Up @@ -863,10 +914,15 @@ def merge_cmd(
"""Merge two or more SDF files into one."""
from sdf_toolkit.transform.merge import ConflictStrategy, merge

try:
conflict_strategy = ConflictStrategy(strategy)
except ValueError as exc:
raise typer.BadParameter(str(exc), param_hint="--strategy") from exc

sdf_files = [_load_sdf(f) for f in files]
result = merge(
sdf_files,
strategy=ConflictStrategy(strategy),
strategy=conflict_strategy,
target_timescale=target_timescale,
)

Expand Down
33 changes: 31 additions & 2 deletions src/sdf_toolkit/core/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,37 @@
from typing import Any, Literal


class EntryType(StrEnum):
"""Types of SDF timing entries."""
class CaseInsensitiveStrEnum(StrEnum):
"""A ``StrEnum`` whose value lookup ignores case.

SDF and Verilog keywords are conventionally written upper case (``IOPATH``,
``PORT``) while the member values here are lower case. This lets
``EntryType("IOPATH")`` resolve instead of raising, so CLI options that take
these names work regardless of the case the user types.
"""

@classmethod
def _missing_(cls, value: object) -> "CaseInsensitiveStrEnum | None":
"""Resolve a value case-insensitively, or return None if unknown."""
if isinstance(value, str):
lowered = value.lower()
for member in cls:
if member.value == lowered:
return member
return None


class EntryType(CaseInsensitiveStrEnum):
"""Types of SDF timing entries.

Member values are lower case but lookup is case-insensitive, so the
upper-case spellings used in SDF files resolve directly:

>>> EntryType("IOPATH") is EntryType.IOPATH
True
>>> EntryType("Port") is EntryType.PORT
True
"""

PORT = "port"
INTERCONNECT = "interconnect"
Expand Down
7 changes: 7 additions & 0 deletions src/sdf_toolkit/transform/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
"""Transform modules for SDF timing data manipulation."""

from sdf_toolkit.transform.interconnect import (
DriverResolutionError,
port_to_interconnect,
)
from sdf_toolkit.transform.merge import ConflictStrategy, merge
from sdf_toolkit.transform.normalize import normalize_delays

__all__ = [
# merge
"ConflictStrategy",
# interconnect
"DriverResolutionError",
"merge",
# normalize
"normalize_delays",
"port_to_interconnect",
]
Loading