Coverage for klayout_pex/magic/magic_log_analyzer.py: 68%
117 statements
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-06 13:14 +0000
« prev ^ index » next coverage.py v7.17.2, created at 2026-11-06 13:14 +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.klayout.output_files import write_output_file
39from klayout_pex.log import debug
40from klayout_pex.magic.magic_ext_file_parser import parse_magic_pex_run
41from klayout_pex.magic.magic_ext_data_structures import MagicPEXRun, CellExtData
42from klayout_pex.util.argparse_helpers import add_argument_with_former_name, warn_deprecated_options
43from klayout_pex.util.stdio import use_utf8_stdio
45PROGRAM_NAME = "magic_log_analyzer"
47# MAGIC has no position for the substrate node and writes its own infinity,
48# (1 << 30) - 7, in place of one. Scaled to database units that is far outside
49# the coordinate range of a kdb.Box.
50MAGIC_INFINITY = (1 << 30) - 7
53def is_magic_marker(*coordinates: float) -> bool:
54 return any(abs(c) >= MAGIC_INFINITY for c in coordinates)
57class MagicLogAnalyzer:
58 def __init__(self,
59 magic_pex_run: MagicPEXRun,
60 report: rdb.ReportDatabase,
61 dbu: float):
62 self.magic_pex_run = magic_pex_run
63 self.report = report
64 self.magic_category = self.report.create_category('MAGIC Extraction')
65 self.dbu = dbu
67 def analyze(self):
68 for cell, cell_data in self.magic_pex_run.cells.items():
69 self.analyze_cell(cell=cell, cell_data=cell_data)
71 def analyze_cell(self,
72 cell: str,
73 cell_data: CellExtData):
74 rdb_cell = self.report.create_cell(name=cell)
75 ports_cat = self.report.create_category(parent=self.magic_category, name='Ports')
76 nodes_cat = self.report.create_category(parent=self.magic_category, name='Nodes')
77 devices_cat = self.report.create_category(parent=self.magic_category, name='Devices')
78 rnodes_cat = self.report.create_category(parent=self.magic_category, name='Resistor Nodes')
79 resistors_cat = self.report.create_category(parent=self.magic_category, name='Resistors')
81 dbu_to_um = 200.0
83 def scaled(value: float) -> float:
84 return value / dbu_to_um / self.dbu
86 def box_for_point_dbu(x: float, y: float) -> kdb.Box:
87 return kdb.Box(x, y, x + 20, y + 20)
89 # Everything the cell does place, so that a marker position can be
90 # reported clamped into it rather than dropped.
91 cell_box = kdb.Box()
92 for p in cell_data.ext_data.ports:
93 if not is_magic_marker(p.x_bot, p.y_bot, p.x_top, p.y_top):
94 cell_box += kdb.Box(scaled(p.x_bot), scaled(p.y_bot),
95 scaled(p.x_top), scaled(p.y_top))
96 for n in cell_data.ext_data.nodes:
97 if not is_magic_marker(n.x_bot, n.y_bot):
98 cell_box += box_for_point_dbu(scaled(n.x_bot), scaled(n.y_bot))
99 for d in cell_data.ext_data.devices:
100 if not is_magic_marker(d.x_bot, d.y_bot, d.x_top, d.y_top):
101 cell_box += kdb.Box(scaled(d.x_bot), scaled(d.y_bot),
102 scaled(d.x_top), scaled(d.y_top))
104 def clamped(x: float, y: float) -> Optional[Tuple[float, float]]:
105 """
106 The point in database units, or the closest one inside the cell if
107 MAGIC gave a marker instead of a position. None when there is
108 nothing to clamp into, which is the only case worth dropping.
109 """
110 if not is_magic_marker(x, y):
111 return scaled(x), scaled(y)
112 if cell_box.empty():
113 debug(f"Cell {cell}: node at MAGIC's marker position skipped, "
114 f"the cell places no other geometry to clamp it into")
115 return None
116 return (min(max(scaled(x), cell_box.left), cell_box.right),
117 min(max(scaled(y), cell_box.bottom), cell_box.top))
119 for p in cell_data.ext_data.ports:
120 port_cat = self.report.create_category(parent=ports_cat, name=f"{p.net} ({p.layer})")
121 shapes = kdb.Shapes()
122 shapes.insert(kdb.Box(scaled(p.x_bot), scaled(p.y_bot),
123 scaled(p.x_top), scaled(p.y_top)))
124 self.report.create_items(cell_id=rdb_cell.rdb_id(), category_id=port_cat.rdb_id(),
125 trans=kdb.CplxTrans(mag=self.dbu), shapes=shapes)
127 for n in cell_data.ext_data.nodes:
128 point = clamped(n.x_bot, n.y_bot)
129 if point is None:
130 continue
131 node_cat = self.report.create_category(parent=nodes_cat, name=f"{n.net} ({n.layer})")
132 shapes = kdb.Shapes()
133 shapes.insert(box_for_point_dbu(*point))
134 self.report.create_items(cell_id=rdb_cell.rdb_id(), category_id=node_cat.rdb_id(),
135 trans=kdb.CplxTrans(mag=self.dbu), shapes=shapes)
137 for d in cell_data.ext_data.devices:
138 device_cat = self.report.create_category(parent=devices_cat,
139 name=f"Type={d.device_type} Model={d.model}")
140 shapes = kdb.Shapes()
141 shapes.insert(kdb.Box(scaled(d.x_bot), scaled(d.y_bot),
142 scaled(d.x_top), scaled(d.y_top)))
143 self.report.create_items(cell_id=rdb_cell.rdb_id(), category_id=device_cat.rdb_id(),
144 trans=kdb.CplxTrans(mag=self.dbu), shapes=shapes)
146 if cell_data.res_ext_data is not None:
147 for n in cell_data.res_ext_data.rnodes:
148 point = clamped(n.x_bot, n.y_bot)
149 if point is None:
150 continue
151 rnode_cat = self.report.create_category(parent=rnodes_cat,
152 name=n.name)
153 shapes = kdb.Shapes()
154 shapes.insert(box_for_point_dbu(*point))
155 self.report.create_items(cell_id=rdb_cell.rdb_id(), category_id=rnode_cat.rdb_id(),
156 trans=kdb.CplxTrans(mag=self.dbu), shapes=shapes)
158 for idx, r in enumerate(cell_data.res_ext_data.resistors):
159 shapes = kdb.Shapes()
160 for n in cell_data.res_ext_data.rnodes_by_name(r.node1) + \
161 cell_data.res_ext_data.rnodes_by_name(r.node2):
162 point = clamped(n.x_bot, n.y_bot)
163 if point is not None:
164 shapes.insert(box_for_point_dbu(*point))
165 if shapes.is_empty():
166 continue
167 res_cat = self.report.create_category(parent=resistors_cat,
168 name=f"#{idx} {r.node1}↔︎{r.node2} = {r.value_ohm} Ω")
169 self.report.create_items(cell_id=rdb_cell.rdb_id(), category_id=res_cat.rdb_id(),
170 trans=kdb.CplxTrans(mag=self.dbu), shapes=shapes)
173class ArgumentValidationError(Exception):
174 pass
177def _parse_args(arg_list: List[str] = None) -> argparse.Namespace:
178 main_parser = argparse.ArgumentParser(description=f"{PROGRAM_NAME}: "
179 f"Tool to create KLayout RDB for magic runs",
180 add_help=False,
181 formatter_class=RichHelpFormatter)
183 add_argument_with_former_name(main_parser, "--magic-log-dir", "--magic_log_dir", "-m",
184 dest="magic_log_dir_path", required=True,
185 help="Input magic log directory path")
187 add_argument_with_former_name(main_parser, "--out-rdb", "--out", "-o",
188 dest="output_rdb_path", default=None,
189 help="Path of the report database (default is the input directory / "
190 "'report.rdb.gz')")
192 if arg_list is None:
193 arg_list = sys.argv[1:]
194 args = main_parser.parse_args(arg_list)
196 if not os.path.isdir(args.magic_log_dir_path):
197 raise ArgumentValidationError(f"Intput magic log directory does not exist at '{args.magic_log_dir_path}'")
199 if args.output_rdb_path is None:
200 os.path.join(args.magic_log_dir_path, 'report.rdb.gz')
202 return args
205def main():
206 use_utf8_stdio()
207 args = _parse_args()
208 warn_deprecated_options(args)
209 report = rdb.ReportDatabase('')
211 magic_pex_run = parse_magic_pex_run(Path(args.magic_log_dir_path))
213 c = MagicLogAnalyzer(magic_pex_run=magic_pex_run,
214 report=report,
215 dbu=1e-3)
216 c.analyze()
217 write_output_file(report.save, args.output_rdb_path)
220if __name__ == "__main__":
221 main()