Coverage for tests/magic/magic_log_analyzer_test.py: 100%
65 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#
2# --------------------------------------------------------------------------------
3# SPDX-FileCopyrightText: 2024-2026 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/iic-jku/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#
25from __future__ import annotations
27import os
28from pathlib import Path
29import tempfile
30from typing import *
31import unittest
33import allure
34import klayout.db as kdb
35import klayout.rdb as rdb
37from klayout_pex.magic.magic_ext_file_parser import parse_magic_pex_run
38from klayout_pex.magic.magic_log_analyzer import MagicLogAnalyzer
40CELL = 'r_wire_voltage_divider_li1'
42HEADER = """timestamp 0
43version 8.3
44tech sky130A
45style ngspice()
46scale 1000 1 500000
47port "B" 1 1970 -15 2000 15 li
48port "A" 2 0 -15 30 15 li
49port "C" 3 985 185 1015 215 li
50"""
52# MAGIC writes the node's resistance and capacitance before its position, and
53# the capacitance is not an integer.
54NODE = 'node "B" 939 968.643 1970 -15 li 0 0 0 0\n'
56# MAGIC places the substrate node at its own infinity marker, 2^30 - 7. Scaled
57# to DBU that is far outside the 32 bit range of a kdb.Box coordinate.
58SUBSTRATE = 'substrate "VSUBS" 0 0 -1073741817 -1073741817 space 0 0 0 0\n'
61@allure.parent_suite('Unit Tests')
62@allure.tag('MAGIC', 'Log Analyzer')
63class Test(unittest.TestCase):
64 @staticmethod
65 def analyze(ext_file_content: str) -> rdb.ReportDatabase:
66 with tempfile.TemporaryDirectory() as run_dir:
67 with open(os.path.join(run_dir, f"{CELL}.ext"), 'w') as f:
68 f.write(ext_file_content)
69 report = rdb.ReportDatabase('')
70 MagicLogAnalyzer(magic_pex_run=parse_magic_pex_run(Path(run_dir)),
71 report=report,
72 dbu=0.001).analyze()
73 return report
75 @staticmethod
76 def items_by_category_path(report: rdb.ReportDatabase) -> List[Tuple[str, Any]]:
77 """Every reported item, paired with the path of its category."""
78 path_by_id: Dict[int, str] = {}
80 def collect(categories: Any, prefix: str):
81 for category in categories:
82 path = f"{prefix}/{category.name()}"
83 path_by_id[category.rdb_id()] = path
84 collect(category.each_sub_category(), path)
86 collect(report.each_category(), '')
87 return [(path_by_id[item.category_id()], item) for item in report.each_item()]
89 @staticmethod
90 def categories_with_items(report: rdb.ReportDatabase) -> Dict[str, int]:
91 """The item count per category, keyed on the category's path."""
92 counts: Dict[str, int] = {}
93 for path, _ in Test.items_by_category_path(report):
94 counts[path] = counts.get(path, 0) + 1
95 return counts
97 @staticmethod
98 def item_boxes(report: rdb.ReportDatabase) -> Dict[str, List[kdb.DBox]]:
99 """The bounding box of every reported shape, keyed on its category."""
100 boxes: Dict[str, List[kdb.DBox]] = {}
101 for path, item in Test.items_by_category_path(report):
102 for value in item.each_value():
103 boxes.setdefault(path, []).append(value.polygon().bbox())
104 return boxes
106 def test_ports_are_reported(self):
107 report = self.analyze(HEADER)
108 assert self.categories_with_items(report) == {
109 '/MAGIC Extraction/Ports/A (li)': 1,
110 '/MAGIC Extraction/Ports/B (li)': 1,
111 '/MAGIC Extraction/Ports/C (li)': 1,
112 }
114 def test_node_capacitance_may_be_fractional(self):
115 report = self.analyze(HEADER + NODE)
116 assert self.categories_with_items(report).get('/MAGIC Extraction/Nodes/B (li)') == 1
118 def test_substrate_node_at_magic_infinity_is_clamped_into_the_cell(self):
119 # The marker position describes no geometry and cannot become a box of
120 # its own, but the node is still worth reporting — at the closest point
121 # of what the cell does place.
122 report = self.analyze(HEADER + NODE + SUBSTRATE)
123 assert self.categories_with_items(report) == {
124 '/MAGIC Extraction/Ports/A (li)': 1,
125 '/MAGIC Extraction/Ports/B (li)': 1,
126 '/MAGIC Extraction/Ports/C (li)': 1,
127 '/MAGIC Extraction/Nodes/B (li)': 1,
128 '/MAGIC Extraction/Nodes/VSUBS (space)': 1,
129 }
131 boxes = self.item_boxes(report)
132 substrate = boxes.pop('/MAGIC Extraction/Nodes/VSUBS (space)')
133 cell_box = kdb.DBox()
134 for placed in boxes.values():
135 for box in placed:
136 cell_box += box
137 assert cell_box.contains(substrate[0].p1)