Coverage for klayout_pex/magic/magic_log_analyzer.py: 61%

114 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-17 19:08 +0000

1#! /usr/bin/env python3 

2# 

3# -------------------------------------------------------------------------------- 

4# SPDX-FileCopyrightText: 2024-2025 Martin Jan Köhler and Harald Pretl 

5# Johannes Kepler University, Institute for Integrated Circuits. 

6# 

7# This file is part of KPEX  

8# (see https://github.com/iic-jku/klayout-pex). 

9# 

10# This program is free software: you can redistribute it and/or modify 

11# it under the terms of the GNU General Public License as published by 

12# the Free Software Foundation, either version 3 of the License, or 

13# (at your option) any later version. 

14# 

15# This program is distributed in the hope that it will be useful, 

16# but WITHOUT ANY WARRANTY; without even the implied warranty of 

17# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

18# GNU General Public License for more details. 

19# 

20# You should have received a copy of the GNU General Public License 

21# along with this program. If not, see <http://www.gnu.org/licenses/>. 

22# SPDX-License-Identifier: GPL-3.0-or-later 

23# -------------------------------------------------------------------------------- 

24# 

25 

26import argparse 

27import os 

28from pathlib import Path 

29import re 

30import sys 

31from typing import * 

32 

33from rich_argparse import RichHelpFormatter 

34 

35import klayout.db as kdb 

36import klayout.rdb as rdb 

37 

38from klayout_pex.log import debug 

39from klayout_pex.magic.magic_ext_file_parser import parse_magic_pex_run 

40from klayout_pex.magic.magic_ext_data_structures import MagicPEXRun, CellExtData 

41 

42PROGRAM_NAME = "magic_log_analyzer" 

43 

44# MAGIC has no position for the substrate node and writes its own infinity, 

45# (1 << 30) - 7, in place of one. Scaled to database units that is far outside 

46# the coordinate range of a kdb.Box. 

47MAGIC_INFINITY = (1 << 30) - 7 

48 

49 

50def is_magic_marker(*coordinates: float) -> bool: 

51 return any(abs(c) >= MAGIC_INFINITY for c in coordinates) 

52 

53 

54class MagicLogAnalyzer: 

55 def __init__(self, 

56 magic_pex_run: MagicPEXRun, 

57 report: rdb.ReportDatabase, 

58 dbu: float): 

59 self.magic_pex_run = magic_pex_run 

60 self.report = report 

61 self.magic_category = self.report.create_category('MAGIC Extraction') 

62 self.dbu = dbu 

63 

64 def analyze(self): 

65 for cell, cell_data in self.magic_pex_run.cells.items(): 

66 self.analyze_cell(cell=cell, cell_data=cell_data) 

67 

68 def analyze_cell(self, 

69 cell: str, 

70 cell_data: CellExtData): 

71 rdb_cell = self.report.create_cell(name=cell) 

72 ports_cat = self.report.create_category(parent=self.magic_category, name='Ports') 

73 nodes_cat = self.report.create_category(parent=self.magic_category, name='Nodes') 

74 devices_cat = self.report.create_category(parent=self.magic_category, name='Devices') 

75 rnodes_cat = self.report.create_category(parent=self.magic_category, name='Resistor Nodes') 

76 resistors_cat = self.report.create_category(parent=self.magic_category, name='Resistors') 

77 

78 dbu_to_um = 200.0 

79 

80 def scaled(value: float) -> float: 

81 return value / dbu_to_um / self.dbu 

82 

83 def box_for_point_dbu(x: float, y: float) -> kdb.Box: 

84 return kdb.Box(x, y, x + 20, y + 20) 

85 

86 # Everything the cell does place, so that a marker position can be 

87 # reported clamped into it rather than dropped. 

88 cell_box = kdb.Box() 

89 for p in cell_data.ext_data.ports: 

90 if not is_magic_marker(p.x_bot, p.y_bot, p.x_top, p.y_top): 

91 cell_box += kdb.Box(scaled(p.x_bot), scaled(p.y_bot), 

92 scaled(p.x_top), scaled(p.y_top)) 

93 for n in cell_data.ext_data.nodes: 

94 if not is_magic_marker(n.x_bot, n.y_bot): 

95 cell_box += box_for_point_dbu(scaled(n.x_bot), scaled(n.y_bot)) 

96 for d in cell_data.ext_data.devices: 

97 if not is_magic_marker(d.x_bot, d.y_bot, d.x_top, d.y_top): 

98 cell_box += kdb.Box(scaled(d.x_bot), scaled(d.y_bot), 

99 scaled(d.x_top), scaled(d.y_top)) 

100 

101 def clamped(x: float, y: float) -> Optional[Tuple[float, float]]: 

102 """ 

103 The point in database units, or the closest one inside the cell if 

104 MAGIC gave a marker instead of a position. None when there is 

105 nothing to clamp into, which is the only case worth dropping. 

106 """ 

107 if not is_magic_marker(x, y): 

108 return scaled(x), scaled(y) 

109 if cell_box.empty(): 

110 debug(f"Cell {cell}: node at MAGIC's marker position skipped, " 

111 f"the cell places no other geometry to clamp it into") 

112 return None 

113 return (min(max(scaled(x), cell_box.left), cell_box.right), 

114 min(max(scaled(y), cell_box.bottom), cell_box.top)) 

115 

116 for p in cell_data.ext_data.ports: 

117 port_cat = self.report.create_category(parent=ports_cat, name=f"{p.net} ({p.layer})") 

118 shapes = kdb.Shapes() 

119 shapes.insert(kdb.Box(scaled(p.x_bot), scaled(p.y_bot), 

120 scaled(p.x_top), scaled(p.y_top))) 

121 self.report.create_items(cell_id=rdb_cell.rdb_id(), category_id=port_cat.rdb_id(), 

122 trans=kdb.CplxTrans(mag=self.dbu), shapes=shapes) 

123 

124 for n in cell_data.ext_data.nodes: 

125 point = clamped(n.x_bot, n.y_bot) 

126 if point is None: 

127 continue 

128 node_cat = self.report.create_category(parent=nodes_cat, name=f"{n.net} ({n.layer})") 

129 shapes = kdb.Shapes() 

130 shapes.insert(box_for_point_dbu(*point)) 

131 self.report.create_items(cell_id=rdb_cell.rdb_id(), category_id=node_cat.rdb_id(), 

132 trans=kdb.CplxTrans(mag=self.dbu), shapes=shapes) 

133 

134 for d in cell_data.ext_data.devices: 

135 device_cat = self.report.create_category(parent=devices_cat, 

136 name=f"Type={d.device_type} Model={d.model}") 

137 shapes = kdb.Shapes() 

138 shapes.insert(kdb.Box(scaled(d.x_bot), scaled(d.y_bot), 

139 scaled(d.x_top), scaled(d.y_top))) 

140 self.report.create_items(cell_id=rdb_cell.rdb_id(), category_id=device_cat.rdb_id(), 

141 trans=kdb.CplxTrans(mag=self.dbu), shapes=shapes) 

142 

143 if cell_data.res_ext_data is not None: 

144 for n in cell_data.res_ext_data.rnodes: 

145 point = clamped(n.x_bot, n.y_bot) 

146 if point is None: 

147 continue 

148 rnode_cat = self.report.create_category(parent=rnodes_cat, 

149 name=n.name) 

150 shapes = kdb.Shapes() 

151 shapes.insert(box_for_point_dbu(*point)) 

152 self.report.create_items(cell_id=rdb_cell.rdb_id(), category_id=rnode_cat.rdb_id(), 

153 trans=kdb.CplxTrans(mag=self.dbu), shapes=shapes) 

154 

155 for idx, r in enumerate(cell_data.res_ext_data.resistors): 

156 shapes = kdb.Shapes() 

157 for n in cell_data.res_ext_data.rnodes_by_name(r.node1) + \ 

158 cell_data.res_ext_data.rnodes_by_name(r.node2): 

159 point = clamped(n.x_bot, n.y_bot) 

160 if point is not None: 

161 shapes.insert(box_for_point_dbu(*point)) 

162 if shapes.is_empty(): 

163 continue 

164 res_cat = self.report.create_category(parent=resistors_cat, 

165 name=f"#{idx} {r.node1}↔︎{r.node2} = {r.value_ohm} Ω") 

166 self.report.create_items(cell_id=rdb_cell.rdb_id(), category_id=res_cat.rdb_id(), 

167 trans=kdb.CplxTrans(mag=self.dbu), shapes=shapes) 

168 

169 

170class ArgumentValidationError(Exception): 

171 pass 

172 

173 

174def _parse_args(arg_list: List[str] = None) -> argparse.Namespace: 

175 main_parser = argparse.ArgumentParser(description=f"{PROGRAM_NAME}: " 

176 f"Tool to create KLayout RDB for magic runs", 

177 add_help=False, 

178 formatter_class=RichHelpFormatter) 

179 

180 main_parser.add_argument("--magic_log_dir", "-m", 

181 dest="magic_log_dir_path", required=True, 

182 help="Input magic log directory path") 

183 

184 main_parser.add_argument("--out", "-o", 

185 dest="output_rdb_path", default=None, 

186 help="Magic log directory path (default is input directory / 'report.rdb.gz')") 

187 

188 if arg_list is None: 

189 arg_list = sys.argv[1:] 

190 args = main_parser.parse_args(arg_list) 

191 

192 if not os.path.isdir(args.magic_log_dir_path): 

193 raise ArgumentValidationError(f"Intput magic log directory does not exist at '{args.magic_log_dir_path}'") 

194 

195 if args.output_rdb_path is None: 

196 os.path.join(args.magic_log_dir_path, 'report.rdb.gz') 

197 

198 return args 

199 

200 

201def main(): 

202 args = _parse_args() 

203 report = rdb.ReportDatabase('') 

204 

205 magic_pex_run = parse_magic_pex_run(Path(args.magic_log_dir_path)) 

206 

207 c = MagicLogAnalyzer(magic_pex_run=magic_pex_run, 

208 report=report, 

209 dbu=1e-3) 

210 c.analyze() 

211 report.save(args.output_rdb_path) 

212 

213 

214if __name__ == "__main__": 

215 main()