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

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# 

24 

25from __future__ import annotations 

26 

27import os 

28from pathlib import Path 

29import tempfile 

30from typing import * 

31import unittest 

32 

33import allure 

34import klayout.db as kdb 

35import klayout.rdb as rdb 

36 

37from klayout_pex.magic.magic_ext_file_parser import parse_magic_pex_run 

38from klayout_pex.magic.magic_log_analyzer import MagicLogAnalyzer 

39 

40CELL = 'r_wire_voltage_divider_li1' 

41 

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""" 

51 

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' 

55 

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' 

59 

60 

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 

74 

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] = {} 

79 

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) 

85 

86 collect(report.each_category(), '') 

87 return [(path_by_id[item.category_id()], item) for item in report.each_item()] 

88 

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 

96 

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 

105 

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 } 

113 

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 

117 

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 } 

130 

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)