Skip to content
Draft
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
8 changes: 8 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
"max_parallel": 32,
"pareto_selection": "all"
},
"backend": "liberate",
"liberate": {
"slew_lower_rise": 0.2,
"slew_upper_rise": 0.8,
Expand All @@ -69,5 +70,12 @@
"input_slews_ns": [0.25, 0.75, 1.5],
"output_loads_pF": [0.0005, 0.001, 0.002, 0.004, 0.016, 0.064],
"tx_include_channel_rc": true
},
"charlib": {
"charlib_executable": "/Path/to/charlib",
"ngspice_executable": "/Path/to/ngspice",
"ngspice_library_path": "/Path/to/libngspice.so",
"input_slews_ns": [0.010938, 0.021875, 0.043750],
"output_loads_pF": [0.0005, 0.001, 0.002, 0.004, 0.016, 0.064]
}
}
63 changes: 57 additions & 6 deletions scripts/get_metrics.py
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,53 @@ def _parse_lib_transitions(lib_path: str, prefix: str, warnings: list) -> dict:
return result


def _extract_from_lib_charlib(lib_path: str, prefix: str, warnings: list) -> dict:
"""
Extract all metrics from a CharLib Liberty .lib file.

prefix : "tx" or "rx" — used as the key prefix.
Returns a dict with the same keys as _parse_tx_datasheet / _parse_rx_datasheet.
All metrics come from the .lib; there is no DATASHEET for CharLib runs.
"""
result = {}
if not os.path.exists(lib_path):
warnings.append(f"{prefix.upper()}: .lib file not found: {lib_path}")
return result

try:
timing = lib_parser.parse_lib_timing(lib_path)
result[f"{prefix}_delay_rr_avg_ns"] = timing.get("avg_cell_rise_ns", 0.0)
result[f"{prefix}_delay_ff_avg_ns"] = timing.get("avg_cell_fall_ns", 0.0)
result[f"{prefix}_slew_rr_avg_ns"] = timing.get("avg_rise_transition_ns", 0.0)
result[f"{prefix}_slew_ff_avg_ns"] = timing.get("avg_fall_transition_ns", 0.0)
except Exception as e:
warnings.append(f"{prefix.upper()}: failed to parse .lib timing: {e}")

try:
power = lib_parser.parse_lib_power(lib_path)
rise_vals = [v for arc in power.get("rise_power", [])
for row in arc.get("values", []) for v in row]
fall_vals = [v for arc in power.get("fall_power", [])
for row in arc.get("values", []) for v in row]
if rise_vals:
result[f"{prefix}_sw_rise_avg_pJ"] = _avg(rise_vals)
else:
warnings.append(f"{prefix.upper()}: rise_power not found in .lib")
if fall_vals:
result[f"{prefix}_sw_fall_avg_pJ"] = _avg(fall_vals)
else:
warnings.append(f"{prefix.upper()}: fall_power not found in .lib")
except Exception as e:
warnings.append(f"{prefix.upper()}: failed to parse .lib power: {e}")

try:
result[f"{prefix}_leak_avg_nW"] = lib_parser.parse_cell_leakage_power(lib_path)
except Exception as e:
warnings.append(f"{prefix.upper()}: failed to parse .lib leakage: {e}")

return result


def _parse_rx_lib_delay_min(lib_path: str, warnings: list) -> dict:
"""
Extract RX delay at minimum input slew and minimum output load (index [0][0])
Expand Down Expand Up @@ -480,19 +527,23 @@ def extract(cfg, ch_result, eq_result, term_result, tx_result, run_dir: str) ->
warnings = []

channel_rc_integrated = getattr(tx_result, 'channel_rc_integrated', False)
use_charlib = getattr(tx_result, 'backend', 'liberate') == "charlib"

tx_ds_path = os.path.join(run_dir, "tx", "DATASHEET", "txip.txt")
rx_ds_path = os.path.join(run_dir, "rx", "DATASHEET", "rxip.txt")

tx = _parse_tx_datasheet(tx_ds_path, warnings)
rx = _parse_rx_datasheet(rx_ds_path, warnings)

# --- Fill in transition times from .lib files ---
# The text datasheet typically omits output transition tables. The .lib
# file always contains rise_transition / fall_transition data.
tx_lib_path = os.path.join(run_dir, "tx", "LIBRARY", "txip_nldm.lib")
rx_lib_path = os.path.join(run_dir, "rx", "LIBRARY", "rxip_nldm.lib")

if use_charlib:
tx = _extract_from_lib_charlib(tx_lib_path, "tx", warnings)
rx = _extract_from_lib_charlib(rx_lib_path, "rx", warnings)
else:
tx = _parse_tx_datasheet(tx_ds_path, warnings)
rx = _parse_rx_datasheet(rx_ds_path, warnings)

# --- Fill in transition times from .lib files (Liberate only; CharLib path already
# populated via _extract_from_lib_charlib above) ---
if tx.get("tx_slew_rr_avg_ns", 0.0) == 0.0:
tx_lib_trans = _parse_lib_transitions(tx_lib_path, "tx", warnings)
tx.update(tx_lib_trans)
Expand Down
57 changes: 53 additions & 4 deletions scripts/lib_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ def parse_lib_timing(lib_path: str, cell_name: Optional[str] = None) -> Dict:
# balanced-brace matching. A simple non-greedy regex stops at the first
# inner nested '}' (e.g. the close of 'cell_rise'), truncating the block
# before 'rise_transition' / 'fall_transition' are reached.
timing_groups = _extract_balanced_blocks(content, r'timing\s*\(\)')
timing_groups = _extract_balanced_blocks(content, r'timing\s*\([^)]*\)')

for group_name in ("cell_rise", "cell_fall", "rise_transition", "fall_transition"):
all_values = []
Expand All @@ -122,7 +122,7 @@ def parse_lib_timing(lib_path: str, cell_name: Optional[str] = None) -> Dict:
values = _parse_values_block(block)

# Extract related_pin from timing group
rp_match = re.search(r'related_pin\s*:\s*"([^"]*)"', tg)
rp_match = re.search(r'related_pin\s*:\s*"?([^"\s;]+)"?', tg)
related_pin = rp_match.group(1) if rp_match else ""

result[group_name].append({
Expand Down Expand Up @@ -207,15 +207,15 @@ def parse_lib_power(lib_path: str, pg_pin: str = "VDD") -> Dict:
result: Dict[str, list] = {"rise_power": [], "fall_power": []}

# Find all internal_power () { ... } blocks
ip_blocks = _extract_balanced_blocks(content, r'internal_power\s*\(\)')
ip_blocks = _extract_balanced_blocks(content, r'internal_power\s*\([^)]*\)')

for ip in ip_blocks:
# Filter by related_pg_pin
pg_match = re.search(r'related_pg_pin\s*:\s*(\w+)', ip)
if pg_match and pg_match.group(1) != pg_pin:
continue

rp_match = re.search(r'related_pin\s*:\s*"([^"]*)"', ip)
rp_match = re.search(r'related_pin\s*:\s*"?([^"\s;]+)"?', ip)
related_pin = rp_match.group(1) if rp_match else ""

for group_name in ("rise_power", "fall_power"):
Expand Down Expand Up @@ -292,3 +292,52 @@ def parse_pin_capacitance(lib_path: str, pin_pattern: Optional[str] = None) -> D
caps[pin_name] = float(cap_match.group(1))

return caps


# ---------------------------------------------------------------------------
# Leakage power extraction
# ---------------------------------------------------------------------------

def parse_cell_leakage_power(lib_path: str, cell_name: Optional[str] = None) -> float:
"""
Parse leakage_power blocks from a Liberty .lib file. Not needed for Liberate since
leakage results are gleaned from the DATASHEET.txt. This function is needed for
CharLib since it reports leakage power only through the .lib output.

Parameters
----------
lib_path : str
Path to the .lib file.
cell_name : str, optional
If given, only parse leakage_power blocks inside the named cell block.
If None, parse all leakage_power blocks in the file.

Returns
-------
float
Average of all ``value : <float>`` entries found (in nW, library unit).
Returns 0.0 if no leakage_power blocks are found.
"""
with open(lib_path, "r") as f:
content = f.read()

if cell_name is not None:
# Find the matching cell block
cell_blocks = _extract_balanced_blocks(content, rf'cell\s*\(\s*{re.escape(cell_name)}\s*\)')
if not cell_blocks:
return 0.0
search_text = cell_blocks[0]
else:
search_text = content

lp_blocks = _extract_balanced_blocks(search_text, r'leakage_power\s*\([^)]*\)')
values = []
for block in lp_blocks:
m = re.search(r'\bvalue\s*:\s*([\d.eE+\-]+)', block)
if m:
try:
values.append(float(m.group(1)))
except ValueError:
pass

return sum(values) / len(values) if values else 0.0
65 changes: 59 additions & 6 deletions scripts/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,15 @@ class LiberateConfig:
tx_include_channel_rc: bool = False


@dataclass
class CharLibConfig:
charlib_executable: str # absolute path to the charlib binary
ngspice_executable: str # absolute path to the ngspice batch binary
ngspice_library_path: str # absolute path to libngspice.so (for charlib shared backend)
input_slews_ns: list
output_loads_pF: list


@dataclass
class OutputConfig:
base_dir: str
Expand Down Expand Up @@ -300,17 +309,19 @@ class Config:
termination: TerminationConfig
termination_hidden: TerminationHiddenConfig
rx: RxConfig
liberate: LiberateConfig
output: OutputConfig
sweep: SweepConfig
backend: str # "liberate" or "charlib"; no default - must be explicit in config
rx_sizing: Optional[RxSizingConfig] = None
tx_sizing: Optional[TxSizingConfig] = None
co_opt: Optional[CoOptConfig] = None
layout: Optional[LayoutConfig] = None
area_hidden: Optional[AreaHiddenConfig] = None
clocking: Optional[object] = None # clocking.ClockingConfig
clocking_hidden: Optional[object] = None # clocking.ClockingHiddenConfig
pdk_path: Optional[str] = None # resolved PDK config the entry pointed to
pdk_path: Optional[str] = None # resolved PDK config the entry pointed to
liberate: Optional[LiberateConfig] = None # required when backend == "liberate"
charlib: Optional[CharLibConfig] = None # required when backend == "charlib"


# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -345,7 +356,14 @@ def _deep_merge(base: dict, override: dict) -> dict:
# Path-valued keys are resolved relative to the file that DEFINES them (see
# _load_raw), so that e.g. a PDK config's "../templates/..." stays correct even
# when the entry point is the general root config.json.
_PATH_KEYS = (("process", "lib_path"), ("liberate", "template_dir"), ("output", "base_dir"))
_PATH_KEYS = (
("process", "lib_path"),
("liberate", "template_dir"),
("output", "base_dir"),
("charlib", "charlib_executable"),
("charlib", "ngspice_executable"),
("charlib", "ngspice_library_path"),
)


def _load_raw(path: str, pdk_override: str = None, is_entry: bool = True) -> dict:
Expand Down Expand Up @@ -619,6 +637,26 @@ def _resolve(path: str) -> str:
lane_width_frac = ah_raw.get("lane_width_frac"),
)

# Backend selection - required; no default
backend = raw.get("backend")
if backend is None:
raise ValueError(
f"{config_path} must specify \"backend\": set "
"\"backend\": \"liberate\" or \"backend\": \"charlib\" in config.json.")

# Liberate config - required when backend == "liberate"; may be absent/partial otherwise
_liberate_required = {"template_dir", "slew_lower_rise", "slew_upper_rise",
"slew_lower_fall", "slew_upper_fall", "input_slews_ns", "output_loads_pF"}
_liberate_raw = raw.get("liberate", {})
if _liberate_required.issubset(_liberate_raw.keys()):
liberate_cfg = LiberateConfig(**_liberate_raw)
else:
liberate_cfg = None

# CharLib config - required when backend == "charlib"; absent otherwise
_charlib_raw = raw.get("charlib")
charlib_cfg = CharLibConfig(**_charlib_raw) if _charlib_raw is not None else None

# Clocking — optional (defaults live in scripts/clocking.py)
clocking_cfg = clocking.load_clocking_config(raw.get("clocking"))
clocking_hidden_cfg = clocking.load_clocking_hidden(raw.get("clocking_hidden"))
Expand All @@ -645,9 +683,9 @@ def _resolve(path: str) -> str:
),
termination_hidden = term_hidden,
rx = rx_cfg,
liberate = LiberateConfig(**raw["liberate"]),
output = OutputConfig(**raw["output"]),
sweep = SweepConfig(**raw["sweep"]),
backend = backend,
rx_sizing = rx_sizing_cfg,
tx_sizing = tx_sizing_cfg,
co_opt = co_opt_cfg,
Expand All @@ -656,6 +694,8 @@ def _resolve(path: str) -> str:
clocking = clocking_cfg,
clocking_hidden = clocking_hidden_cfg,
pdk_path = pdk_path,
liberate = liberate_cfg,
charlib = charlib_cfg,
)


Expand Down Expand Up @@ -720,8 +760,21 @@ def validate_config(cfg: Config) -> None:
if not cfg.sweep.data_rate_Gbps:
errors.append("[sweep] data_rate_Gbps list is empty")

if not os.path.isdir(cfg.liberate.template_dir):
errors.append(f"[liberate] template_dir not found: {cfg.liberate.template_dir}")
if cfg.backend == "liberate":
if cfg.liberate is None:
errors.append("[liberate] backend selected but 'liberate' section is missing or incomplete in config")
elif not os.path.isdir(cfg.liberate.template_dir):
errors.append(f"[liberate] template_dir not found: {cfg.liberate.template_dir}")
elif cfg.backend == "charlib":
if cfg.charlib is None:
errors.append("[charlib] backend selected but 'charlib' section is missing or incomplete in config")
else:
if not os.path.isfile(cfg.charlib.charlib_executable):
errors.append(f"[charlib] charlib_executable not found: {cfg.charlib.charlib_executable}")
if not os.path.isfile(cfg.charlib.ngspice_library_path):
errors.append(f"[charlib] ngspice_library_path not found: {cfg.charlib.ngspice_library_path}")
else:
errors.append(f"[backend] must be 'liberate' or 'charlib', got {cfg.backend!r}")

if errors:
raise ValueError("Configuration validation failed:\n"
Expand Down
Loading