Coverage for tests/rcx25/rcx25_test_helpers.py: 100%
78 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-31 20:14 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-31 20:14 +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 io
29import json
30import os
31import tempfile
32from typing import *
34import allure
35import csv_diff
37import klayout.db as kdb
38import klayout.lay as klay
40from klayout_pex.kpex_cli import KpexCLI
41from klayout_pex.rcx25.extraction_results import CellExtractionResults
42from klayout_pex.rcx25.pex_mode import PEXMode
45class PDKName(StrEnum):
46 SKY130A = 'sky130A'
47 IHP_SG13G2 = 'ihp_sg13g2'
50@dataclass
51class PDKTestConfig:
52 name: PDKName
54 @property
55 def kpex_pdk_dir(self) -> str:
56 return os.path.realpath(os.path.join(__file__, '..', '..', '..',
57 'pdk', self.name, 'libs.tech', 'kpex'))
59 @property
60 def test_designs_dir(self) -> str:
61 return os.path.realpath(os.path.join(__file__, '..', '..', '..',
62 'testdata', 'designs', self.name))
64 @property
65 def lyt_path(self) -> str:
66 return os.path.abspath(os.path.join(self.kpex_pdk_dir, 'sky130A.lyt'))
68 def load_kdb_technology(self) -> kdb.Technology:
69 kdb.Technology.clear_technologies()
70 tech = kdb.Technology.create_technology('sky130A')
71 tech.load(self.lyt_path)
72 return tech
74 def gds_path(self, *path_components) -> str:
75 return os.path.join(self.test_designs_dir, *path_components)
78@dataclass
79class RCX25Extraction:
80 pdk: PDKTestConfig
81 pex_mode: PEXMode
82 blackbox: bool
84 def save_layout_preview(self, gds_path: str, output_png_path: str):
85 self.pdk.load_kdb_technology()
86 lv = klay.LayoutView()
87 lv.load_layout(gds_path)
88 lv.max_hier()
89 lv.set_config('background-color', '#000000')
90 lv.set_config('bitmap-oversampling', '1')
91 lv.set_config('default-font-size', '4')
92 lv.set_config('default-text-size', '0.1')
93 lv.save_image_with_options(
94 output_png_path,
95 width=4096, height=2160
96 # ,
97 # linewidth=2,
98 # resolution=0.25 # 4x as large fonts
99 )
101 def run_rcx25d_single_cell(self, *path_components) -> Tuple[CellExtractionResults, CSVPath, PNGPath]:
102 gds_path = self.pdk.gds_path(*path_components)
104 preview_png_path = tempfile.mktemp(prefix=f"layout_preview_", suffix=".png")
105 self.save_layout_preview(gds_path, preview_png_path)
106 output_dir_path = os.path.realpath(os.path.join(__file__, '..', '..', '..', f"output_{self.pdk.name}"))
107 cli = KpexCLI()
108 cli.main(['main',
109 '--pdk', self.pdk.name,
110 '--mode', self.pex_mode,
111 '--blackbox', 'y' if self.blackbox else 'n',
112 '--gds', gds_path,
113 '--out_dir', output_dir_path,
114 '--2.5D',
115 '--halo', '10000',
116 '--scale', 'n'])
117 assert cli.rcx25_extraction_results is not None
118 assert len(cli.rcx25_extraction_results.cell_extraction_results) == 1 # assume single cell test
119 results = list(cli.rcx25_extraction_results.cell_extraction_results.values())[0]
120 assert results.cell_name == path_components[-1][:-len('.gds.gz')]
121 return results, cli.rcx25_extracted_csv_path, preview_png_path
123 def assert_expected_matches_obtained(self,
124 *path_components,
125 expected_csv_content: str) -> CellExtractionResults:
126 result, csv, preview_png = self.run_rcx25d_single_cell(*path_components)
127 allure.attach.file(csv, name='pex_obtained.csv', attachment_type=allure.attachment_type.CSV)
128 allure.attach.file(preview_png, name='📸 layout_preview.png', attachment_type=allure.attachment_type.PNG)
129 expected_csv = csv_diff.load_csv(io.StringIO(expected_csv_content), key='Device')
130 with open(csv, 'r') as f:
131 obtained_csv = csv_diff.load_csv(f, key='Device')
132 diff = csv_diff.compare(expected_csv, obtained_csv, show_unchanged=False)
133 human_diff = csv_diff.human_text(
134 diff, current=obtained_csv, extras=(('Net1','{Net1}'),('Net2','{Net2}'))
135 )
136 allure.attach(expected_csv_content, name='pex_expected.csv', attachment_type=allure.attachment_type.CSV)
137 allure.attach(json.dumps(diff, sort_keys=True, indent=' ').encode("utf8"),
138 name='pex_diff.json', attachment_type=allure.attachment_type.JSON)
139 allure.attach(human_diff.encode("utf8"), name='‼️ pex_diff.txt', attachment_type=allure.attachment_type.TEXT)
140 # assert diff['added'] == []
141 # assert diff['removed'] == []
142 # assert diff['changed'] == []
143 # assert diff['columns_added'] == []
144 # assert diff['columns_removed'] == []
145 assert human_diff == '', 'Diff detected'
146 return result