Coverage for tests/rcx25/rcx25_test_helpers.py: 100%
87 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-02 23:32 +0000
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-02 23:32 +0000
1#
2# --------------------------------------------------------------------------------
3# SPDX-FileCopyrightText: 2024-2025 Martin Jan Köhler and Harald Pretl
4# Johannes Kepler University, Institute for Integrated Circuits.
5#
6# This file is part of KPEX
7# (see https://github.com/martinjankoehler/klayout-pex).
8#
9# This program is free software: you can redistribute it and/or modify
10# it under the terms of the GNU General Public License as published by
11# the Free Software Foundation, either version 3 of the License, or
12# (at your option) any later version.
13#
14# This program is distributed in the hope that it will be useful,
15# but WITHOUT ANY WARRANTY; without even the implied warranty of
16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17# GNU General Public License for more details.
18#
19# You should have received a copy of the GNU General Public License
20# along with this program. If not, see <http://www.gnu.org/licenses/>.
21# SPDX-License-Identifier: GPL-3.0-or-later
22# --------------------------------------------------------------------------------
23#
24from __future__ import annotations
26from dataclasses import dataclass
27from enum import StrEnum
28import glob
29import io
30import json
31import os
32import tempfile
33from typing import *
35import allure
36import csv_diff
38import klayout.db as kdb
39import klayout.lay as klay
41from klayout_pex.kpex_cli import KpexCLI
42from klayout_pex.rcx25.extraction_results import CellExtractionResults
43from klayout_pex.rcx25.pex_mode import PEXMode
46class PDKName(StrEnum):
47 SKY130A = 'sky130A'
48 IHP_SG13G2 = 'ihp-sg13g2'
49 GF180MCUD = 'gf180mcuD'
52@dataclass
53class PDKTestConfig:
54 name: PDKName
56 @property
57 def kpex_pdk_dir(self) -> str:
58 return os.path.realpath(os.path.join(__file__, '..', '..', '..',
59 'pdk', self.name, 'libs.tech', 'kpex'))
61 @property
62 def test_designs_dir(self) -> str:
63 return os.path.realpath(os.path.join(__file__, '..', '..', '..',
64 'testdata', 'designs', self.name))
66 @property
67 def lyt_path(self) -> str:
68 lyt_paths = glob.glob(os.path.join(self.kpex_pdk_dir, '*.lyt'))
69 assert len(lyt_paths) == 1, f"Expected one KLayout technology file in {self.kpex_pdk_dir}"
70 return os.path.abspath(lyt_paths[0])
72 def load_kdb_technology(self) -> kdb.Technology:
73 kdb.Technology.clear_technologies()
74 tech = kdb.Technology.create_technology(self.name)
75 tech.load(self.lyt_path)
76 return tech
78 def gds_path(self, *path_components) -> str:
79 return os.path.join(self.test_designs_dir, *path_components)
82@dataclass
83class RCX25Extraction:
84 pdk: PDKTestConfig
85 pex_mode: PEXMode
86 blackbox: bool
88 def save_layout_preview(self, gds_path: str, output_png_path: str):
89 self.pdk.load_kdb_technology()
90 lv = klay.LayoutView()
91 lv.load_layout(gds_path)
92 lv.max_hier()
93 lv.set_config('background-color', '#000000')
94 lv.set_config('bitmap-oversampling', '1')
95 lv.set_config('default-font-size', '4')
96 lv.set_config('default-text-size', '0.1')
97 lv.save_image_with_options(
98 output_png_path,
99 width=4096, height=2160
100 # ,
101 # linewidth=2,
102 # resolution=0.25 # 4x as large fonts
103 )
105 def run_rcx25d_single_cell(self, *path_components) -> Tuple[CellExtractionResults, CSVPath, PNGPath]:
106 gds_path = self.pdk.gds_path(*path_components)
108 preview_png_path = tempfile.mktemp(prefix=f"layout_preview_", suffix=".png")
109 self.save_layout_preview(gds_path, preview_png_path)
110 output_dir_path = os.path.realpath(os.path.join(__file__, '..', '..', '..', f"output_{self.pdk.name}"))
111 cli = KpexCLI()
112 cli.main(['main',
113 '--pdk', self.pdk.name,
114 '--mode', self.pex_mode,
115 '--blackbox', 'y' if self.blackbox else 'n',
116 '--gds', gds_path,
117 '--out_dir', output_dir_path,
118 '--2.5D',
119 '--halo', '10000',
120 '--scale', 'n'])
121 assert cli.rcx25_extraction_results is not None
122 assert len(cli.rcx25_extraction_results.cell_extraction_results) == 1 # assume single cell test
123 results = list(cli.rcx25_extraction_results.cell_extraction_results.values())[0]
124 assert results.cell_name == path_components[-1][:-len('.gds.gz')]
125 return results, cli.rcx25_extracted_csv_path, preview_png_path
127 def written_device_lines(self, *path_components, prefixes: str = 'XDMQ') -> List[str]:
128 """
129 Extracts the single cell and returns the lines of the written SPICE netlist with these prefixes
130 (by default not the capacitors and resistors KPEX adds), with their continuation lines joined
131 """
132 _, csv_path, _ = self.run_rcx25d_single_cell(*path_components)
133 with open(f"{os.path.splitext(csv_path)[0]}.spice") as f:
134 lines = f.read().replace('\n+', '').splitlines()
135 return [l for l in lines if l and l[0] in prefixes]
137 def assert_expected_matches_obtained(self,
138 *path_components,
139 expected_csv_content: str) -> CellExtractionResults:
140 result, csv, preview_png = self.run_rcx25d_single_cell(*path_components)
141 allure.attach.file(csv, name='pex_obtained.csv', attachment_type=allure.attachment_type.CSV)
142 allure.attach.file(preview_png, name='📸 layout_preview.png', attachment_type=allure.attachment_type.PNG)
143 expected_csv = csv_diff.load_csv(io.StringIO(expected_csv_content), key='Device')
144 with open(csv, 'r') as f:
145 obtained_csv = csv_diff.load_csv(f, key='Device')
146 diff = csv_diff.compare(expected_csv, obtained_csv, show_unchanged=False)
147 human_diff = csv_diff.human_text(
148 diff, current=obtained_csv, extras=(('Net1','{Net1}'),('Net2','{Net2}'))
149 )
150 allure.attach(expected_csv_content, name='pex_expected.csv', attachment_type=allure.attachment_type.CSV)
151 allure.attach(json.dumps(diff, sort_keys=True, indent=' ').encode("utf8"),
152 name='pex_diff.json', attachment_type=allure.attachment_type.JSON)
153 allure.attach(human_diff.encode("utf8"), name='‼️ pex_diff.txt', attachment_type=allure.attachment_type.TEXT)
154 # assert diff['added'] == []
155 # assert diff['removed'] == []
156 # assert diff['changed'] == []
157 # assert diff['columns_added'] == []
158 # assert diff['columns_removed'] == []
159 assert human_diff == '', 'Diff detected'
160 return result