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
« 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#
26import argparse
27import os
28from pathlib import Path
29import re
30import sys
31from typing import *
33from rich_argparse import RichHelpFormatter
35import klayout.db as kdb
36import klayout.rdb as rdb
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
42PROGRAM_NAME = "magic_log_analyzer"
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
50def is_magic_marker(*coordinates: float) -> bool:
51 return any(abs(c) >= MAGIC_INFINITY for c in coordinates)
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
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)
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')
78 dbu_to_um = 200.0
80 def scaled(value: float) -> float:
81 return value / dbu_to_um / self.dbu
83 def box_for_point_dbu(x: float, y: float) -> kdb.Box:
84 return kdb.Box(x, y, x + 20, y + 20)
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))
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))
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)
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)
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)
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)
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)
170class ArgumentValidationError(Exception):
171 pass
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)
180 main_parser.add_argument("--magic_log_dir", "-m",
181 dest="magic_log_dir_path", required=True,
182 help="Input magic log directory path")
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')")
188 if arg_list is None:
189 arg_list = sys.argv[1:]
190 args = main_parser.parse_args(arg_list)
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}'")
195 if args.output_rdb_path is None:
196 os.path.join(args.magic_log_dir_path, 'report.rdb.gz')
198 return args
201def main():
202 args = _parse_args()
203 report = rdb.ReportDatabase('')
205 magic_pex_run = parse_magic_pex_run(Path(args.magic_log_dir_path))
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)
214if __name__ == "__main__":
215 main()