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
2 changes: 1 addition & 1 deletion chipfoundry_cli/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
"""ChipFoundry CLI package: Automate project submission to SFTP."""
__version__ = "2.7.1"
__version__ = "2.7.2"
58 changes: 47 additions & 11 deletions chipfoundry_cli/preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@
LINT_FAILED = "failed"
LINT_SKIPPED = "skipped"

AREA_SOURCE_DIE = "die_area"
AREA_SOURCE_SYNTHESIS = "synthesis"

_ELABORATE_ONLY_STRATEGIES = {
STRATEGY_HIER_MACRO_FIRST,
STRATEGY_HIER_MACRO_FIRST_LEGACY,
Expand Down Expand Up @@ -105,6 +108,30 @@ def die_area_mm2(config: Dict[str, Any]) -> Optional[Decimal]:
return (Decimal(str(um2)) / Decimal("1000000")).quantize(Decimal("0.0001"))


def floorplan_area_is_fixed(config: Dict[str, Any]) -> bool:
"""True when DIE_AREA is the macro footprint LibreLane will use.

``FP_SIZING=relative`` sizes the die from synthesized cells. Absolute
sizing, and a DIE_AREA with no FP_SIZING, is a fixed footprint.
"""
sizing = config.get("FP_SIZING")
if isinstance(sizing, str) and sizing.strip().lower() == "relative":
return False
return die_area_mm2(config) is not None


def _apply_die_area(row: Dict[str, Any], config: Dict[str, Any]) -> None:
"""Record DIE_AREA. A fixed footprint is not replaced by synthesis later."""
area = die_area_mm2(config)
if area is None:
return
if floorplan_area_is_fixed(config):
row["estimated_area_mm2"] = str(area)
row["area_source"] = AREA_SOURCE_DIE
elif row.get("estimated_area_mm2") is None:
row["estimated_area_mm2"] = str(area)


def _stem_from_view_path(value: Any) -> Optional[str]:
if not isinstance(value, str) or not value.strip():
return None
Expand Down Expand Up @@ -262,6 +289,7 @@ def scan_project(project_root: Path) -> Dict[str, Any]:
"source": "cli_preview",
"config_path": row["config_path"],
"estimated_area_mm2": None,
"area_source": None,
"gate_count": None,
"instances": [],
"integration_strategy": None,
Expand All @@ -283,7 +311,7 @@ def scan_project(project_root: Path) -> Dict[str, Any]:
else:
parent = design_name.strip()

area = die_area_mm2(config)
is_new = parent not in macros_by_name
parent_row = macros_by_name.setdefault(
parent,
{
Expand All @@ -292,19 +320,23 @@ def scan_project(project_root: Path) -> Dict[str, Any]:
"is_top_level": False,
"source": "cli_preview",
"config_path": row["config_path"],
"estimated_area_mm2": str(area) if area is not None else None,
"estimated_area_mm2": None,
"area_source": None,
"gate_count": None,
"instances": [],
"integration_strategy": None,
"lint_status": None,
},
)
if parent_row["config_path"] != row["config_path"]:
if is_new or not parent_row.get("config_path") or parent_row["config_path"] == row["config_path"]:
if not is_new and not parent_row.get("config_path"):
parent_row["config_path"] = row["config_path"]
parent_row["notes"] = f"from {row['config_path']}"
_apply_die_area(parent_row, config)
else:
notes.append(
f"Multiple configs declare DESIGN_NAME {parent!r}; using {parent_row['config_path']}"
)
elif area is not None and parent_row["estimated_area_mm2"] is None:
parent_row["estimated_area_mm2"] = str(area)

macro_section = config.get("MACROS")
if isinstance(macro_section, dict):
Expand All @@ -330,6 +362,7 @@ def scan_project(project_root: Path) -> Dict[str, Any]:
"source": "cli_preview",
"config_path": None,
"estimated_area_mm2": None,
"area_source": None,
"gate_count": None,
"instances": [],
"integration_strategy": STRATEGY_HARD_IP,
Expand Down Expand Up @@ -362,6 +395,7 @@ def scan_project(project_root: Path) -> Dict[str, Any]:
"source": "cli_preview",
"config_path": None,
"estimated_area_mm2": None,
"area_source": None,
"gate_count": None,
"instances": [],
"integration_strategy": STRATEGY_HARD_IP,
Expand Down Expand Up @@ -471,12 +505,13 @@ def apply_synth_metrics(
macro["lint_status"] = LINT_FAILED
if strategy_uses_gate_count(strategy):
macro["gate_count"] = metrics["gate_count"]
area = metrics.get("estimated_area_mm2")
if area is not None:
macro["estimated_area_mm2"] = str(area)
elif macro.get("estimated_area_mm2") is None and metrics.get("estimated_area_mm2") is not None:
# Keep DIE_AREA / catalog area; do not treat Yosys cells as gates.
pass
# Cell area is the sum of synthesized gates. A fixed DIE_AREA is the
# footprint OpenLane will actually build.
if macro.get("area_source") != AREA_SOURCE_DIE:
area = metrics.get("estimated_area_mm2")
if area is not None:
macro["estimated_area_mm2"] = str(area)
macro["area_source"] = AREA_SOURCE_SYNTHESIS


def leaf_summary(macros: Sequence[Dict[str, Any]]) -> Dict[str, Any]:
Expand Down Expand Up @@ -675,6 +710,7 @@ def build_preview_payload(
"source": "cli_preview",
"config_path": row.get("config_path"),
"estimated_area_mm2": row.get("estimated_area_mm2"),
"area_source": row.get("area_source"),
"gate_count": row.get("gate_count"),
"instances": row.get("instances") or [],
"integration_strategy": row.get("integration_strategy"),
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "chipfoundry-cli"
version = "2.7.1"
version = "2.7.2"
description = "CLI tool to automate ChipFoundry project submission to SFTP server"
authors = ["ChipFoundry <marwan.abbas@chipfoundry.io>"]
readme = "README.md"
Expand Down
86 changes: 86 additions & 0 deletions tests/test_preview_command.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ def test_scan_hierarchy_and_ipm(tmp_path: Path):
{
"DESIGN_NAME": "user_proj_example",
"VERILOG_FILES": ["dir::../../verilog/rtl/user_proj_example.v"],
"FP_SIZING": "absolute",
"DIE_AREA": [0, 0, 2800, 1760],
}
),
Expand All @@ -131,6 +132,10 @@ def test_scan_hierarchy_and_ipm(tmp_path: Path):
assert by_name["user_proj_example"]["instances"] == [
{"parent_macro": "user_project_wrapper", "count": 1}
]
assert by_name["user_proj_example"]["estimated_area_mm2"] == "4.9280"
assert by_name["user_proj_example"]["area_source"] == "die_area"
assert by_name["user_project_wrapper"]["estimated_area_mm2"] == "10.2784"
assert by_name["user_project_wrapper"]["area_source"] == "die_area"
assert any(item["name"] == "CF_SRAM_1024x32" for item in scan["marketplace_ip"])


Expand Down Expand Up @@ -192,9 +197,24 @@ def test_parse_synth_metrics_and_skip_gates_for_wrapper(tmp_path: Path):
"integration_strategy": STRATEGY_FLATTENED_RTL,
"gate_count": None,
"estimated_area_mm2": None,
"area_source": None,
}
apply_synth_metrics(leaf, metrics, lint_errors=0)
assert leaf["gate_count"] == 1234
assert leaf["estimated_area_mm2"] == "0.05"
assert leaf["area_source"] == "synthesis"

fixed = {
"name": "user_proj_example",
"integration_strategy": STRATEGY_FLATTENED_RTL,
"gate_count": None,
"estimated_area_mm2": "4.9280",
"area_source": "die_area",
}
apply_synth_metrics(fixed, metrics, lint_errors=0)
assert fixed["gate_count"] == 1234
assert fixed["estimated_area_mm2"] == "4.9280"
assert fixed["area_source"] == "die_area"


def test_leaf_summary_partial_metrics():
Expand Down Expand Up @@ -227,6 +247,72 @@ def test_leaf_summary_partial_metrics():
assert summary["missing_gate_count_macro_count"] == 1


def test_scan_applies_die_area_when_wrapper_config_is_seen_first(tmp_path: Path):
_init_project(tmp_path)
_write(
tmp_path / "openlane" / "a_wrapper" / "config.json",
json.dumps(
{
"DESIGN_NAME": "a_wrapper",
"SYNTH_ELABORATE_ONLY": True,
"MACROS": {
"user_proj_example": {
"gds": ["dir::../../gds/user_proj_example.gds"],
"lef": ["dir::../../lef/user_proj_example.lef"],
"instances": {"mprj": {}},
}
},
}
),
)
_write(
tmp_path / "openlane" / "user_proj_example" / "config.json",
json.dumps(
{
"DESIGN_NAME": "user_proj_example",
"VERILOG_FILES": ["a.v"],
"FP_SIZING": "absolute",
"DIE_AREA": "0 0 2800 1760",
}
),
)
scan = scan_project(tmp_path)
by_name = {row["name"]: row for row in scan["macros"]}
assert by_name["user_proj_example"]["config_path"].endswith(
"openlane/user_proj_example/config.json"
)
assert by_name["user_proj_example"]["estimated_area_mm2"] == "4.9280"
assert by_name["user_proj_example"]["area_source"] == "die_area"
assert not any("Multiple configs declare" in note for note in scan["notes"])


def test_relative_floorplan_area_is_replaced_by_synthesis(tmp_path: Path):
_init_project(tmp_path)
_write(
tmp_path / "openlane" / "user_proj_example" / "config.json",
json.dumps(
{
"DESIGN_NAME": "user_proj_example",
"VERILOG_FILES": ["a.v"],
"FP_SIZING": "relative",
"DIE_AREA": [0, 0, 2800, 1760],
}
),
)
scan = scan_project(tmp_path)
macro = scan["macros"][0]
assert macro["estimated_area_mm2"] == "4.9280"
assert macro["area_source"] is None
apply_synth_metrics(
macro,
{"gate_count": 40, "estimated_area_mm2": 0.02},
lint_errors=0,
)
assert macro["gate_count"] == 40
assert macro["estimated_area_mm2"] == "0.02"
assert macro["area_source"] == "synthesis"


def test_preview_skip_synth_no_push(tmp_path: Path):
_init_project(tmp_path)
_write(
Expand Down
Loading