Coverage for tests/klayout/pex25d_builder_test.py: 100%
129 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 types import SimpleNamespace
26import unittest
28import allure
29import klayout.db as kdb
31from klayout_pex.klayout.pex25d_builder import PEX25DBuilder, BuildError
32from klayout_pex.pex25d.protobuf import pex25d_terminal_pb2
33from klayout_pex.pex25d.reader import read_pex25d_text
34from klayout_pex.pex25d.resolver import resolve
35from klayout_pex.pex25d.validator import validate
36from klayout_pex.pex25d.writer import write_pex25d_text
37from klayout_pex.tech_info import TechInfo
38import klayout_pex_protobuf.kpex.tech.tech_pb2 as tech_pb2
39import klayout_pex_protobuf.kpex.tech.process_stack_pb2 as stack_pb2
42class TerminalFixture:
43 def __init__(self):
44 self.netlist = kdb.Netlist()
45 self.circuit = kdb.Circuit()
46 self.circuit.name = 'test'
47 self.netlist.add(self.circuit)
48 self.net = self.circuit.create_net('N')
49 self.device_class = kdb.DeviceClassMOS4Transistor()
50 self.device_class.name = 'nfet'
51 self.netlist.add(self.device_class)
52 self.geometry = {(1, 0): kdb.Region(kdb.Box(0, 0, 100, 20))}
53 self.markers = {}
54 self.labels = kdb.Texts()
55 self.pins = kdb.Region(kdb.Box(0, 0, 100, 20))
56 tech = tech_pb2.Technology(name='test')
57 metal = tech.layers.add(name='met1')
58 metal.drw_gds_pair.layer = 1
59 metal.pin_gds_pair.layer = 1
60 metal.pin_gds_pair.datatype = 16
61 metal.label_gds_pair.layer = 1
62 metal.label_gds_pair.datatype = 5
63 stack = tech.process_stack
64 substrate = stack.layers.add(name='subs', layer_type=stack_pb2.ProcessStackInfo.LAYER_TYPE_SUBSTRATE)
65 substrate.substrate_layer.height = 0.1
66 substrate.substrate_layer.thickness = 0.2
67 metal = stack.layers.add(name='met1', layer_type=stack_pb2.ProcessStackInfo.LAYER_TYPE_METAL)
68 metal.metal_layer.z = 1.0
69 metal.metal_layer.thickness = 0.1
70 air = stack.layers.add(name='air', layer_type=stack_pb2.ProcessStackInfo.LAYER_TYPE_SIMPLE_DIELECTRIC)
71 air.simple_dielectric_layer.dielectric_k = 1.0
72 self.tech = TechInfo(tech, None)
73 self.context = SimpleNamespace(
74 dbu=0.001, top_circuit=self.circuit,
75 shapes_of_net=lambda gds_pair, net: self.geometry.get(gds_pair),
76 pins_of_layer=lambda pair: self.pins,
77 labels_of_layer=lambda pair: self.labels,
78 lvsdb=SimpleNamespace(
79 layer_name=lambda index: 'met1',
80 shapes_of_terminal=lambda ref: self.markers[ref.device().expanded_name()]))
82 def pin(self, label, x, y):
83 self.labels.insert(kdb.Text(label, kdb.Trans(x, y)))
85 def device(self, name, region):
86 device = self.circuit.create_device(self.device_class, name)
87 device.connect_terminal('G', self.net)
88 self.markers[name] = {1: region}
90 def build(self):
91 return PEX25DBuilder(self.context, self.tech, 'test').build()
94@allure.parent_suite('Unit Tests')
95@allure.tag('PEX25D', 'Builder')
96class Pex25DBuilderTerminalTest(unittest.TestCase):
97 def test_pin_uses_its_label_not_the_whole_pin_polygon(self):
98 fixture = TerminalFixture()
99 fixture.pin('A', 10, 10)
100 fixture.pin('B', 90, 10)
101 file = fixture.build()
102 assert [t.name for t in file.terminals] == ['pin:A', 'pin:B']
103 assert all(t.kind == pex25d_terminal_pb2().TERMINAL_KIND_PIN for t in file.terminals)
104 terminal = file.terminals[0]
105 assert terminal.conductor == 'N'
106 assert (terminal.region.lower_left.x, terminal.region.upper_right.x) == (90, 110)
107 assert file.shapes[0].box.upper_right.x == 1000
108 assert validate(file, strict=True).exit_code == 0
110 def test_pin_on_a_bent_wire_resolves(self):
111 fixture = TerminalFixture()
112 fixture.geometry[(1, 0)] = kdb.Region(kdb.Polygon([
113 kdb.Point(0, 0), kdb.Point(100, 0), kdb.Point(100, 20),
114 kdb.Point(20, 20), kdb.Point(20, 100), kdb.Point(0, 100)]))
115 fixture.pin('A', 10, 10)
116 file = fixture.build()
117 scene = resolve(read_pex25d_text(write_pex25d_text(file), '<generated>'))
118 terminal = scene.conductors[0].terminals[0]
119 assert len(terminal.boxes) == 1
120 assert terminal.boxes[0].upper_right.x - terminal.boxes[0].lower_left.x == 20
122 def test_repeated_labels_produce_unique_stable_names(self):
123 fixture = TerminalFixture()
124 fixture.pin('A', 10, 10)
125 fixture.pin('A', 90, 10)
126 names = [t.name for t in fixture.build().terminals]
127 assert names == [t.name for t in fixture.build().terminals]
128 assert len(names) == len(set(names)) == 2
130 def test_a_label_outside_the_pin_marker_is_ignored(self):
131 fixture = TerminalFixture()
132 fixture.pin('A', 200, 10)
133 assert not fixture.build().terminals
135 def test_a_pin_without_interconnect_reports_a_build_error(self):
136 fixture = TerminalFixture()
137 fixture.pin('A', 10, 10)
138 fixture.geometry[(1, 0)] = kdb.Region(kdb.Box(50, 0, 100, 20))
139 with self.assertRaisesRegex(BuildError, 'must select one conductor'):
140 fixture.build()
142 def test_a_device_terminal_keeps_all_its_disjoint_shapes_in_one_node(self):
143 fixture = TerminalFixture()
144 fixture.geometry[(1, 0)] = kdb.Region(kdb.Box(10, 0, 20, 100))
145 fixture.geometry[(1, 0)].insert(kdb.Box(40, 0, 50, 100))
146 marker = kdb.Region(kdb.Box(10, 40, 20, 50))
147 marker.insert(kdb.Box(40, 40, 50, 50))
148 fixture.device('M1', marker)
149 file = fixture.build()
150 assert len(file.terminals) == 1
151 terminal = file.terminals[0]
152 assert terminal.name == 'device:M1:G'
153 assert terminal.kind == pex25d_terminal_pb2().TERMINAL_KIND_DEVICE_TERMINAL
154 scene = resolve(file)
155 assert len(scene.conductors[0].terminals) == 1
156 assert len(scene.conductors[0].terminals[0].boxes) == 2
158 def test_an_abutting_device_gets_only_a_boundary_strip(self):
159 fixture = TerminalFixture()
160 fixture.device('M1', kdb.Region(kdb.Box(-20, 0, 0, 20)))
161 file = fixture.build()
162 terminal = resolve(file).conductors[0].terminals[0]
163 assert (terminal.boxes[0].lower_left.x, terminal.boxes[0].upper_right.x) == (0, 10)
165 def test_a_box_cannot_silently_short_interconnect_between_device_shapes(self):
166 fixture = TerminalFixture()
167 marker = kdb.Region(kdb.Box(10, 0, 20, 20))
168 marker.insert(kdb.Box(40, 0, 50, 20))
169 fixture.device('M1', marker)
170 with self.assertRaisesRegex(BuildError, 'shorting additional interconnect'):
171 fixture.build()
173 def test_device_terminal_names_cannot_collide_with_pin_names(self):
174 fixture = TerminalFixture()
175 fixture.pin('M1:G', 10, 10)
176 fixture.device('M1', kdb.Region(kdb.Box(50, 0, 60, 20)))
177 file = fixture.build()
178 assert len({t.name for t in file.terminals}) == 2
179 assert len(read_pex25d_text(write_pex25d_text(file), '<generated>').terminals) == 2