Skip to content

Device terminal defined with several polygons connects to only one of them #2525

Closed

Description

@martinjankoehler

@klayoutmatthias found another one whilst working on kpex…

When a GenericDeviceExtractor defines a terminal with several unconnected polygons, the terminal is connected to the net of only one of them. The other nets lose the terminal, without a warning. define_terminal keeps all polygons, but NetlistExtractor::make_device_abstract_connections (src/db/db/dbNetlistExtractor.cc) keeps one cluster per terminal: each set_cluster_id_for_terminal call overwrites the previous one.

import klayout.db as kdb


class Extractor(kdb.GenericDeviceExtractor):
    def setup(self):
        self.name = 'DEV'
        self.define_layer('MK', 'marker')
        self.define_layer('A', 'terminal A')
        device_class = kdb.DeviceClass()
        device_class.add_terminal(kdb.DeviceTerminalDefinition('A'))
        self.register_device_class(device_class)

    def get_connectivity(self, layout, layers):
        conn = kdb.Connectivity()
        conn.connect(layers[0], layers[1])
        return conn

    def extract_devices(self, layer_geometry):
        marker, a = layer_geometry
        device = self.create_device()
        for p in a.each():
            self.define_terminal(device, self.device_class().terminal_id('A'), 1, p)


layout = kdb.Layout()
top = layout.create_cell('TOP')
mk, a, m1 = (layout.layer(n, 0) for n in (1, 2, 3))
top.shapes(mk).insert(kdb.Box(0, 0, 1000, 1000))
for x in (100, 500):  # two polygons of terminal A, each under a wire of its own
    top.shapes(a).insert(kdb.Box(x, 100, x + 100, 200))
    top.shapes(m1).insert(kdb.Box(x - 50, 50, x + 150, 250))

l2n = kdb.LayoutToNetlist(kdb.RecursiveShapeIterator(layout, top, []))
r_mk, r_a, r_m1 = (l2n.make_layer(li) for li in (mk, a, m1))
l2n.extract_devices(Extractor(), {'MK': r_mk, 'A': r_a})
l2n.connect(r_a)
l2n.connect(r_m1)
l2n.connect(r_a, r_m1)
l2n.extract_netlist()
for net in l2n.netlist().top_circuit().each_net():
    print(net.expanded_name(), [str(p.bbox()) for p in l2n.shapes_of_net(net, r_m1).each()],
          [t.terminal_def().name for t in net.each_terminal()])

Output, with only the second wire on terminal A:

$1 ['(50,50;250,250)'] []
$2 ['(450,50;650,250)'] ['A']

Expected: both wires on the net of A, or at least a warning.

Seen with the sg13g2 LVS deck of IHP-Open-PDK, which defines the emitter E of an npn13G2 with Nx > 1 with one polygon per emitter. LVS passes there because metal joins the emitters, but parasitic extraction puts E on one emitter only (iic-jku/klayout-pex#364).

KLayout 0.30.12 (Python module), the code is the same on master

Activity

  1. klayoutmatthias commented on Oct 29, 2026

    @klayoutmatthias
    Collaborator

    Hi @martinjankoehler,

    Nice testcase, thanks (or maybe thanks to Claude :) ).

    A small remark first: inside "get_connectivity" you should also include self-interactions, since otherwise devices with a split marker shape are not considered as one device:

    conn.connect(layers[0], layers[0])
    conn.connect(layers[1], layers[1])
    

    However, I'm afraid what you brought up is a complex topic.

    The important piece of information here is that the feature targets bipolar devices. So let's consider "marker" to be collector, base is skipped for simplicity and I think it goes over the whole marker area, and "A" is actually the emitter. You layout is then essentially a multi-emitter device. The issue seen here comes from the fact, that your device has basically two emitter terminals while the class only has one. The problem is not about polygons not being attached, but about which terminal to attach them.

    There are basically three ways to look at this situation and all require a different solution:

    1. The terminals are alternatives: This means, you can attach to any of them and the device is somehow supposed to make a connection between them internally. From that perspective, the current behavior is a bug, since you connect to both of them. However, if I allow that, it could mask a potential wiring problem: you could make a connection through the device - i.a. running a wire to one terminal and continuing the wire from the other. Formally that would be a connection, since both wires connect to the same terminal, but physically that may be a pretty bad one, as the internal connection may go through wells or diffusion areas. We have soft connections now to detect such problems, so basically I can implement the behavior now. I'll take care of that here.

    2. However, if your intention is to have the device always connected at both emitters, then the approach is not the right one. Instead you should treat the emitter areas as separate devices (pull the create_device call inside the loop over the polygons of a) and establish a device combination rule that connects these parallel devices. This gives you the opportunity to add up the finger parameters ("nx", "ny", whatever ...) and check that in LVS. There is a pitfall however: as you don't want to combine parallel devices from different collection regions, you have to "stamp" them: create a new parameter - i.e. "id", assign it a unique integer for all devices created inside the same loop and then combine only those devices with the same id. This case is also called a "must-connect" terminal, because the requirement in the end is to make a connection on every terminal, hence shorting them.

    3. As a third and most complex option, you want that device to be extracted as a multi-emitter transistor with two emitter terminals. There is no out-of-the box solution right now. The device classes have a fixed number of terminals and a device extractor is associated with a single class. So no variable number of terminals. A solution is to proceed similar to case 2., but not combine the devices. Instead, replace parallel devices by multi-emitter devices with emitter-count specific classes in a post-extraction sweep of the netlist. Another option is to have multiple device classes available in one extractor. That would be an extension. If this option is required, let's discuss the implementation path separately.

    I'll see what I can do about case 1. now.

    Matthias

  2. added this to the 0.30.13 milestone on Oct 29, 2026
  3. linked a pull request that will close this issueBugfix/issue 2525 #2527on Oct 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions