From 6c947065ec482da9cec8be73c96cd27e6748201a Mon Sep 17 00:00:00 2001 From: emd9494 Date: Wed, 1 Jul 2026 21:21:44 -0400 Subject: [PATCH 1/7] Modify lib_parser to ignore comments --- scripts/lib_parser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/lib_parser.py b/scripts/lib_parser.py index bdd068b..73fd258 100755 --- a/scripts/lib_parser.py +++ b/scripts/lib_parser.py @@ -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 = [] @@ -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({ @@ -207,7 +207,7 @@ 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 @@ -215,7 +215,7 @@ def parse_lib_power(lib_path: str, pg_pin: str = "VDD") -> Dict: 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"): From 512e08b197cfddf150168a9267e2aac8cddb48df Mon Sep 17 00:00:00 2001 From: emd9494 Date: Thu, 9 Jul 2026 11:02:39 -0400 Subject: [PATCH 2/7] Add mandatory backend selection to config file --- scripts/main.py | 65 ++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/scripts/main.py b/scripts/main.py index 14ee5d0..7ddba13 100755 --- a/scripts/main.py +++ b/scripts/main.py @@ -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 @@ -300,9 +309,9 @@ 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 @@ -310,7 +319,9 @@ class Config: 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" # --------------------------------------------------------------------------- @@ -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: @@ -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")) @@ -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, @@ -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, ) @@ -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" From c071b2e8fa311d740855508ca766d7d3161f5b94 Mon Sep 17 00:00:00 2001 From: emd9494 Date: Thu, 9 Jul 2026 11:20:05 -0400 Subject: [PATCH 3/7] Add _measure_inv_cap_ngspice as alternative to Spectre --- scripts/tx.py | 88 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/scripts/tx.py b/scripts/tx.py index 6f56d88..1491ee4 100755 --- a/scripts/tx.py +++ b/scripts/tx.py @@ -57,6 +57,7 @@ class TxNetlistResult: load_pF: float # external Liberate load (RX device cap when channel embedded) cap_in_pF: float # measured unit inverter input cap (pF) cap_in_source: str # "spice" or "analytical_fallback" + backend: str = "liberate" # "liberate" or "charlib" channel_rc_integrated: bool = False # True → channel RC is inside txip.scs; Liberate # TX switching power already includes channel energy # and TX delay covers full TX+channel propagation. @@ -225,6 +226,93 @@ def _measure_inv_cap_spice( return cap_pF +def _measure_inv_cap_ngspice( + lib_path: str, + lib_corner: str, + w_n_um: float, + w_p_um: float, + l_um: float, + nf: int, + vdd: float, + nmos_name: str, + pmos_name: str, + work_dir: str, + ngspice_exe: str = "ngspice", + temp: float = 25, + spec: Optional[DeviceSpec] = None, + model_include_format: str = "spice_lib", +) -> Optional[float]: + """ + Measure unit inverter input capacitance via ngspice batch simulation. + + Uses the same Q/V method as _measure_inv_cap_spice(). Runs ngspice -b + and parses .meas results from stdout. Returns capacitance in pF, or + None if the simulation fails or the result cannot be parsed. + """ + cap_dir = os.path.join(work_dir, "inverter_cap") + os.makedirs(cap_dir, exist_ok=True) + filename = "inverter_capacitance.sp" + + if spec is None: + spec = DeviceSpec() + + model_header = _gen_model_sp(lib_path, lib_corner, None, + fmt=model_include_format).rstrip() + # ngspice does not understand the Spectre 'simulator lang' directive + model_header = re.sub(r'^\s*simulator\s+lang\s*=.*$', '', model_header, + flags=re.MULTILINE) + + if spec.is_finfet: + nfin_n = spec.w_to_nfin(w_n_um) + nfin_p = spec.w_to_nfin(w_p_um) + l_nm = spec.l_nm() + nmos_inst = f'xnm1 out in 0 0 {nmos_name} L={l_nm}n nfin={nfin_n}' + pmos_inst = f'xpm1 out in VDD VDD {pmos_name} L={l_nm}n nfin={nfin_p}' + else: + nf_tok = f' nf={nf}' if spec.include_nf else '' + nmos_inst = f'xnm1 out in 0 0 {nmos_name} w={w_n_um}u l={l_um}u{nf_tok}' + pmos_inst = f'xpm1 out in VDD VDD {pmos_name} w={w_p_um}u l={l_um}u{nf_tok}' + + netlist = f"""\ +Inverter Input Capacitance Measurement - Q/V Method +{model_header} +.option temp={temp} +VDD VDD 0 DC {vdd} +VIN in_source 0 PULSE(0 {vdd} 200p 50p 50p 500p 1000p) +VMEAS in_source in 0 +{nmos_inst} +{pmos_inst} +CL out 0 1f +.tran 0.1p 1500p +.measure tran q_rise_edge integ i(VMEAS) from=200p to=300p +.measure tran c_in_rise param='abs(q_rise_edge)/{vdd}' +.measure tran q_fall_edge integ i(VMEAS) from=700p to=800p +.measure tran c_in_fall param='abs(q_fall_edge)/{vdd}' +.measure tran c_in_avg param='(c_in_rise+c_in_fall)/2' +.end +""" + + netlist_path = os.path.join(cap_dir, filename) + with open(netlist_path, "w") as f: + f.write(netlist) + + result = subprocess.run( + [ngspice_exe, "-b", filename], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + cwd=cap_dir, + ) + if result.returncode != 0: + return None + + output = result.stdout.decode("utf-8", errors="replace") + for line in output.splitlines(): + m = re.match(r'\s*c_in_avg\s*=\s*([\d.eE+\-]+)', line, re.IGNORECASE) + if m: + return float(m.group(1)) * 1e12 + return None + + # --------------------------------------------------------------------------- # Inverter chain sizing # --------------------------------------------------------------------------- From e45f7afa955b27a3628be6ccc4c990cbf8456ba8 Mon Sep 17 00:00:00 2001 From: emd9494 Date: Thu, 9 Jul 2026 11:24:27 -0400 Subject: [PATCH 4/7] Add CharLib spice and YAML file generators --- scripts/tx.py | 138 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) diff --git a/scripts/tx.py b/scripts/tx.py index 1491ee4..8f1e345 100755 --- a/scripts/tx.py +++ b/scripts/tx.py @@ -1076,6 +1076,144 @@ def _pin_block(pins, indent=8, cols=4): """ +# --------------------------------------------------------------------------- +# CharLib file generators +# --------------------------------------------------------------------------- + +def _gen_txip_sp( + lane_count: int, + inv_sizes: List[Tuple[float, float]], # [(w_n_um, w_p_um), ...] per stage + use_eq: bool, + R_eq_ohm: float, + C_eq_fF: float, + l_um: float, + nmos_name: str, + pmos_name: str, + nf: int, + spec: Optional[DeviceSpec] = None, + w_max_um: float = 900.0, + nf_auto: bool = False, +) -> str: + """ + Generate a plain SPICE netlist for the TX cell (no channel RC). + + Produces two subckts: + .subckt tx IN VDD VSS TXPAD — unit driver + optional EQ network + .subckt txip IN_0…N-1 PAD_0…N-1 VDD VSS — N-lane wrapper + + When nf_auto=True, gate fingers are split to ~1μm per finger so that the + BSIM4 gate resistance (rshg * W_finger / (3*L)) stays below ~3ohm, avoiding + numerical stiffness in transient simulations (CharLib backend only). + """ + if spec is None: + spec = DeviceSpec() + + num_stages = len(inv_sizes) + lines = ["* TX unit driver + N-lane wrapper — generated by CLIPGen", + "* simulator lang=spice", + ""] + lines.append(".subckt tx IN VDD VSS TXPAD") + + for s, (w_n, w_p) in enumerate(inv_sizes): + in_node = "IN" if s == 0 else f"out{s}" + out_node = f"out{s + 1}" if s < num_stages - 1 else "eq_in" + if nf_auto and not spec.is_finfet: + nf_n = max(1, math.ceil(w_n)) + nf_p = max(1, math.ceil(w_p)) + _append_mos(lines, "n", s + 1, out_node, in_node, w_n / nf_n, l_um, + nf_n, nmos_name, w_max_um, spec=spec) + _append_mos(lines, "p", s + 1, out_node, in_node, w_p / nf_p, l_um, + nf_p, pmos_name, w_max_um, spec=spec) + else: + _append_mos(lines, "n", s + 1, out_node, in_node, w_n, l_um, nf, + nmos_name, w_max_um, spec=spec) + _append_mos(lines, "p", s + 1, out_node, in_node, w_p, l_um, nf, + pmos_name, w_max_um, spec=spec) + + if use_eq: + lines.append(f"Req eq_in TXPAD {R_eq_ohm:.4f}") + lines.append(f"Ceq TXPAD VSS {C_eq_fF:.4f}f") + else: + lines.append("Req eq_in TXPAD 0.001") + lines.append("Ceq TXPAD VSS 1e-30") + + lines += [".ends tx", ""] + + in_pins = [f"IN_{i}" for i in range(lane_count)] + pad_pins = [f"PAD_{i}" for i in range(lane_count)] + port_str = " ".join(in_pins) + " " + " ".join(pad_pins) + " VDD VSS" + lines.append(f".subckt txip {port_str}") + for i in range(lane_count): + lines.append(f"xtx{i} IN_{i} VDD VSS PAD_{i} tx") + lines += [".ends txip", ""] + + return "\n".join(lines) + + +def _gen_charlib_yaml( + lib_name: str, + results_dir: str, + netlist_path: str, + model_path: str, + input_slews_ns: list, + output_loads_pF: list, + vdd: float, + temp: float, + lane_count: int, +) -> str: + """Generate a CharLib YAML configuration for the txip cell.""" + in_pins = [f"IN_{i}" for i in range(lane_count)] + pad_pins = [f"PAD_{i}" for i in range(lane_count)] + funcs = [f"PAD_{i} = IN_{i}" for i in range(lane_count)] + + slew_str = "[" + ", ".join(str(s) for s in input_slews_ns) + "]" + load_str = "[" + ", ".join(str(l) for l in output_loads_pF) + "]" + in_str = "[" + ", ".join(in_pins) + "]" + out_str = "[" + ", ".join(pad_pins) + "]" + func_str = "[" + ", ".join(f'"{f}"' for f in funcs) + "]" + + return f"""\ +settings: + lib_name: {lib_name} + results_dir: {results_dir} + units: + leakage_power: nW + energy: pJ + named_nodes: + primary_power: + name: VDD + voltage: {vdd} + primary_ground: + name: VSS + voltage: 0.0 + nwell: + name: VNW + voltage: {vdd} + pwell: + name: VPW + voltage: 0.0 + temperature: {temp} + cell_defaults: + netlist: {netlist_path} + models: + - {model_path} + data_slews: {slew_str} + loads: {load_str} + plots: none + simulation: + backend: ngspice-shared + combinational_leakage_procedure: combinational_leakage + combinational_dynamic_power_procedure: combinational_dynamic_power + debug: false + +cells: + txip: + inputs: {in_str} + outputs: {out_str} + functions: {func_str} +""" + + # --------------------------------------------------------------------------- # TX-only (TX+EQ, no channel) Liberate run # --------------------------------------------------------------------------- From 1bd60593e615281b0f447d035967de7168dbec83 Mon Sep 17 00:00:00 2001 From: emd9494 Date: Thu, 9 Jul 2026 11:35:31 -0400 Subject: [PATCH 5/7] Add cell leakage power parsing from Liberty file --- scripts/lib_parser.py | 49 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/scripts/lib_parser.py b/scripts/lib_parser.py index 73fd258..d969416 100755 --- a/scripts/lib_parser.py +++ b/scripts/lib_parser.py @@ -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 : `` 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 From a0dcd0fbc89a41c4bda2acd738196c9bf6a4d998 Mon Sep 17 00:00:00 2001 From: emd9494 Date: Thu, 9 Jul 2026 11:38:26 -0400 Subject: [PATCH 6/7] Add backend and charlib fields to config.json --- config.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/config.json b/config.json index af769ec..a437b38 100755 --- a/config.json +++ b/config.json @@ -61,6 +61,7 @@ "max_parallel": 32, "pareto_selection": "all" }, + "backend": "liberate", "liberate": { "slew_lower_rise": 0.2, "slew_upper_rise": 0.8, @@ -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] } } From 4f4c9b2fd6e11361ec6b14c2741e1d251ba56b40 Mon Sep 17 00:00:00 2001 From: emd9494 Date: Thu, 9 Jul 2026 11:40:05 -0400 Subject: [PATCH 7/7] Support metrics reported from CharLib Liberty output --- scripts/get_metrics.py | 63 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 6 deletions(-) diff --git a/scripts/get_metrics.py b/scripts/get_metrics.py index e9adb56..631a4d0 100755 --- a/scripts/get_metrics.py +++ b/scripts/get_metrics.py @@ -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]) @@ -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)