Skip to content

Generic

generic

Generic routing functions which are independent of the potential use.

ManhattanRoute pydantic-model

Bases: BaseModel

Optical route containing a connection between two ports.

Attrs

backbone: backbone points start_port: port at the first instance denoting the start of the route end_port: port at the last instance denoting the end of the route instances: list of the instances in order from start to end of the route n_bend90: number of bends used length: length of the route without the bends length_straights: length of the straight_factory elements

Fields:

Source code in kfactory/routing/generic.py
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
class ManhattanRoute(BaseModel, arbitrary_types_allowed=True):
    """Optical route containing a connection between two ports.

    Attrs:
        backbone: backbone points
        start_port: port at the first instance denoting the start of the route
        end_port: port at the last instance denoting the end of the route
        instances: list of the instances in order from start to end of the route
        n_bend90: number of bends used
        length: length of the route without the bends
        length_straights: length of the straight_factory elements
    """

    backbone: list[kdb.Point]
    start_port: Port
    end_port: Port
    instances: list[Instance] = Field(default_factory=list)
    n_bend90: int = 0
    n_taper: int = 0
    bend90_radius: dbu = 0
    taper_length: dbu = 0
    """Length of backbone without the bends."""
    length_straights: dbu = 0
    polygons: dict[kdb.LayerInfo, list[kdb.Polygon]] = Field(default_factory=dict)
    length_function: LengthFunction = Field(default_factory=get_length_from_area)

    @property
    def length_backbone(self) -> dbu:
        """Length of the backbone in dbu."""
        length = 0
        p_old = self.backbone[0]
        for p in self.backbone[1:]:
            length += int((p - p_old).length())
            p_old = p
        return length

    @property
    def length(self) -> int | float:
        return self.length_function(self)

length_backbone property

length_backbone: dbu

Length of the backbone in dbu.

taper_length pydantic-field

taper_length: dbu = 0

Length of backbone without the bends.

PlacerFunction

Bases: Protocol

A placer function. Used to place Instances given a path.

Source code in kfactory/routing/generic.py
44
45
46
47
48
49
50
51
52
53
54
55
56
57
class PlacerFunction(Protocol):
    """A placer function. Used to place Instances given a path."""

    def __call__(
        self,
        c: KCell,
        p1: Port,
        p2: Port,
        pts: Sequence[kdb.Point],
        route_width: int | None = None,
        **kwargs: Any,
    ) -> ManhattanRoute:
        """Implementation of the function."""
        ...

__call__

__call__(
    c: KCell,
    p1: Port,
    p2: Port,
    pts: Sequence[Point],
    route_width: int | None = None,
    **kwargs: Any,
) -> ManhattanRoute

Implementation of the function.

Source code in kfactory/routing/generic.py
47
48
49
50
51
52
53
54
55
56
57
def __call__(
    self,
    c: KCell,
    p1: Port,
    p2: Port,
    pts: Sequence[kdb.Point],
    route_width: int | None = None,
    **kwargs: Any,
) -> ManhattanRoute:
    """Implementation of the function."""
    ...

check_collisions

check_collisions(
    c: KCell,
    start_ports: Sequence[BasePort],
    end_ports: Sequence[BasePort],
    routers: Sequence[ManhattanRouter],
    routes: Sequence[ManhattanRoute],
    on_collision: Literal["error", "show_error"]
    | None = "show_error",
    collision_check_layers: Sequence[LayerInfo]
    | None = None,
) -> None

Checks for collisions given manhattan routes.

Parameters:

Name Type Description Default
c KCell

The KCell to check.

required
start_ports Sequence[BasePort]

Ports from which the routes are supposed to start.

required
end_ports Sequence[BasePort]

Ports where the routes are supposed to end.

required
routers Sequence[ManhattanRouter]

The ManhattanRouters that constructed the routes.

required
routes Sequence[ManhattanRoute]

The ManhatnnaRoutes which were used by the placer.

required
on_collision Literal['error', 'show_error'] | None

What to do on error. Can either do nothing (None), throw an error ("error"), or throw an error and open the cell with report in Klayout ("show_error").

'show_error'
collision_check_layers Sequence[LayerInfo] | None

Sequence of layers which should be checked for overlaps to determine error. If not defined, all layers occurring in ports will be used.

None
Source code in kfactory/routing/generic.py
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
def check_collisions(
    c: KCell,
    start_ports: Sequence[BasePort],
    end_ports: Sequence[BasePort],
    routers: Sequence[ManhattanRouter],
    routes: Sequence[ManhattanRoute],
    on_collision: Literal["error", "show_error"] | None = "show_error",
    collision_check_layers: Sequence[kdb.LayerInfo] | None = None,
) -> None:
    """Checks for collisions given manhattan routes.

    Args:
        c: The KCell to check.
        start_ports: Ports from which the routes are supposed to start.
        end_ports: Ports where the routes are supposed to end.
        routers: The ManhattanRouters that constructed the routes.
        routes: The ManhatnnaRoutes which were used by the placer.
        on_collision: What to do on error. Can either do nothing (None),
            throw an error ("error"), or throw an error and open the
            cell with report in Klayout ("show_error").
        collision_check_layers: Sequence of layers which should be checked for
            overlaps to determine error. If not defined, all layers occurring in
            ports will be used.
    """
    if on_collision is None:
        return
    collision_edges: dict[str, kdb.Edges] = {}
    inter_route_collisions = kdb.Edges()
    all_router_edges = kdb.Edges()
    for i, (ps, pe, router) in enumerate(
        zip(start_ports, end_ports, routers, strict=False)
    ):
        edges_, router_edges = router.collisions(log_errors=None)
        if not edges_.is_empty():
            collision_edges[f"{ps.name} - {pe.name} (index: {i})"] = edges_
        inter_route_collision = all_router_edges.interacting(router_edges)
        if not inter_route_collision.is_empty():
            inter_route_collisions.join_with(inter_route_collision)
        all_router_edges.join_with(router_edges)

    if collision_edges or not inter_route_collisions.is_empty():
        if collision_check_layers is None:
            collision_check_layers = list(
                {p.cross_section.main_layer for p in start_ports}
            )
        dbu = c.kcl.dbu
        db = rdb.ReportDatabase("Routing Errors")
        cat = db.create_category("Manhattan Routing Collisions")
        c.name = c.kcl.future_cell_name or c.name
        cell = db.create_cell(c.name)
        for name, edges in collision_edges.items():
            item = db.create_item(cell, cat)
            item.add_value(name)
            for edge in edges.each():
                item.add_value(edge.to_dtype(dbu))
        insts = [inst for route in routes for inst in route.instances]
        shapes: dict[kdb.LayerInfo, list[kdb.Region]] = defaultdict(list)
        for route in routes:
            for layer, _shapes in route.polygons.items():
                shapes[layer].append(kdb.Region(_shapes))
        layer_cats: dict[kdb.LayerInfo, rdb.RdbCategory] = {}

        def layer_cat(layer_info: kdb.LayerInfo) -> rdb.RdbCategory:
            if layer_info not in layer_cats:
                layer_cats[layer_info] = db.category_by_path(
                    layer_info.to_s()
                ) or db.create_category(layer_info.to_s())
            return layer_cats[layer_info]

        any_layer_collision = False

        for layer_info in collision_check_layers:
            shapes_regions = shapes[layer_info]
            layer_ = c.kcl.layout.layer(layer_info)
            error_region_instances = kdb.Region()
            error_region_shapes = kdb.Region()
            inst_regions: dict[int, kdb.Region] = {}
            inst_region = kdb.Region()
            shape_region = kdb.Region()
            for r in shapes_regions:
                if not (shape_region & r).is_empty():
                    error_region_shapes.insert(shape_region & r)
                shape_region.insert(r)
            for i, inst in enumerate(insts):
                inst_region_ = kdb.Region(inst.bbox(layer_))
                if not (inst_region & inst_region_).is_empty():
                    # if inst_shapes is None:
                    inst_shapes = kdb.Region()
                    shape_it = c.begin_shapes_rec_overlapping(layer_, inst.bbox(layer_))
                    shape_it.select_cells([inst.cell.cell_index()])
                    shape_it.min_depth = 1
                    for _it in shape_it.each():
                        if _it.path()[0].inst() == inst.instance:
                            inst_shapes.insert(
                                _it.shape().polygon.transformed(_it.trans())
                            )
                    for j, _reg in inst_regions.items():
                        if _reg & inst_region_:
                            reg = kdb.Region()
                            shape_it = c.begin_shapes_rec_touching(
                                layer_, (_reg & inst_region_).bbox()
                            )
                            shape_it.select_cells([insts[j].cell.cell_index()])
                            shape_it.min_depth = 1
                            for _it in shape_it.each():
                                if _it.path()[0].inst() == insts[j].instance:
                                    reg.insert(
                                        _it.shape().polygon.transformed(_it.trans())
                                    )

                            error_region_instances.insert(reg & inst_shapes)
                inst_region += inst_region_
                inst_regions[i] = inst_region_

            if not error_region_shapes.is_empty():
                any_layer_collision = True
                if on_collision == "error":
                    continue
                cat = layer_cat(layer_info)
                sc = db.category_by_path(
                    f"{cat.path()}.RoutingErrors"
                ) or db.create_category(layer_cat(layer_info), "RoutingErrors")
                for poly in error_region_shapes.merge().each():
                    it = db.create_item(cell, sc)
                    it.add_value("Route shapes overlapping with other shapes")
                    it.add_value(c.kcl.to_um(poly.downcast()))
            if not error_region_instances.is_empty():
                any_layer_collision = True
                if on_collision == "error":
                    continue
                cat = layer_cat(layer_info)
                sc = db.category_by_path(
                    f"{cat.path()}.RoutingErrors"
                ) or db.create_category(layer_cat(layer_info), "RoutingErrors")
                for poly in error_region_instances.merge().each():
                    it = db.create_item(cell, sc)
                    it.add_value("Route instances overlapping with other instances")
                    it.add_value(c.kcl.to_um(poly.downcast()))

        if any_layer_collision:
            match on_collision:
                case "show_error":
                    c.show(lyrdb=db)
                    raise RuntimeError(
                        f"Routing collision in {c.kcl.future_cell_name or c.name}"
                    )
                case "error":
                    raise RuntimeError(
                        f"Routing collision in {c.kcl.future_cell_name or c.name}"
                    )

get_radius

get_radius(ports: Sequence[ProtoPort[Any]]) -> dbu

Calculates a radius between two ports.

This can be used to determine the radius of two bend ports.

Parameters:

Name Type Description Default
ports Sequence[ProtoPort[Any]]

A sequence of exactly two ports.

required

Returns:

Type Description
dbu

Radius in dbu.

Raises:

Type Description
ValueError

Radius cannot be determined

Source code in kfactory/routing/generic.py
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
def get_radius(ports: Sequence[ProtoPort[Any]]) -> dbu:
    """Calculates a radius between two ports.

    This can be used to determine the radius of two bend ports.

    Args:
        ports: A sequence of exactly two ports.

    Returns:
        Radius in dbu.

    Raises:
        ValueError: Radius cannot be determined
    """
    ports_ = tuple(p.to_itype() for p in ports)
    if len(ports_) != PORTS_FOR_RADIUS:
        raise ValueError(
            "Cannot determine the maximal radius of a bend with more than two ports."
        )
    p1, p2 = ports_
    if p1.angle == p2.angle:
        return int((p1.trans.disp - p2.trans.disp).length())
    p = kdb.Point(1, 0)
    e1 = kdb.Edge(p1.trans.disp.to_p(), p1.trans * p)
    e2 = kdb.Edge(p2.trans.disp.to_p(), p2.trans * p)

    center = e1.cut_point(e2)
    if center is None:
        raise ValueError("Could not determine the radius. Something went very wrong.")
    return int(
        max((p1.trans.disp - center).length(), (p2.trans.disp - center).length())
    )

route_bundle

route_bundle(
    *,
    c: KCell,
    start_ports: list[BasePort],
    end_ports: list[BasePort],
    route_width: dbu | list[dbu] | None = None,
    on_collision: Literal["error", "show_error"]
    | None = "show_error",
    on_placer_error: Literal["error", "show_error"]
    | None = "show_error",
    collision_check_layers: Sequence[LayerInfo]
    | None = None,
    routing_function: ManhattanBundleRoutingFunction = route_smart,
    routing_kwargs: dict[str, Any] | None = None,
    placer_function: PlacerFunction,
    placer_kwargs: dict[str, Any] | None = None,
    constraints: Sequence[Constraint] | None = None,
    starts: dbu
    | list[dbu]
    | list[Step]
    | list[list[Step]]
    | None = None,
    ends: dbu
    | list[dbu]
    | list[Step]
    | list[list[Step]]
    | None = None,
    start_angles: int | list[int] | None = None,
    end_angles: int | list[int] | None = None,
    route_debug: RouteDebug | None = None,
    route_name: str | None = None,
) -> list[ManhattanRoute]

Route a bundle from starting ports to end_ports.

Waypoints will create a front which will create ports in a 1D array. If waypoints are a transformation it will be like a point with a direction. If multiple points are passed, the direction will be invfered. For orientation of 0 degrees it will create the following front for 4 ports:

      │
      │
      │
      p1 ->
      │
      │
      │


      │
      │
      │
      p2 ->
      │
      │
      │
  ___\waypoint
     /
      │
      │
      │
      p3 ->
      │
      │
      │


      │
      │
      │
      p4 ->
      │
      │
      │

Parameters:

Name Type Description Default
c KCell

Cell to place the route in.

required
start_ports list[BasePort]

List of start ports.

required
end_ports list[BasePort]

List of end ports.

required
route_width dbu | list[dbu] | None

Width of the route. If None, the width of the ports is used.

None
sort_ports

Automatically sort ports.

required
on_collision Literal['error', 'show_error'] | None

Define what to do on routing collision. Default behaviour is to open send the layout of c to klive and open an error lyrdb with the collisions. "error" will simply raise an error. None will ignore any error.

'show_error'
on_placer_error Literal['error', 'show_error'] | None

If a placing of the components fails, use the strategy above to handle the error. show_error will visualize it in klayout with the intended route along the already placed parts of c. Error will just throw an error. None will ignore the error.

'show_error'
collision_check_layers Sequence[LayerInfo] | None

Layers to check for actual errors if manhattan routes detect potential collisions.

None
routing_function ManhattanBundleRoutingFunction

Function to place the routes. Must return a corresponding list of OpticalManhattan routes. Must accept the following protocol:

routing_function(
    c: KCell, p1: Port, p2: Port, pts: list[Point], **placer_kwargs
)

route_smart
routing_kwargs dict[str, Any] | None

Additional kwargs passed to the placer_function.

None
placer_function PlacerFunction

Function to place the routes. Must return a corresponding list of OpticalManhattan routes. Must accept the following protocol:

placer_function(
    c: KCell, p1: Port, p2: Port, pts: list[Point], **placer_kwargs
)

required
placer_kwargs dict[str, Any] | None

Additional kwargs passed to the placer_function.

None
constraints Sequence[Constraint] | None

Routing constraints to enforce after routing but before placement. Each constraint's enforce method is called with the routers and routing kwargs (e.g. separation, bend90_radius).

None
starts dbu | list[dbu] | list[Step] | list[list[Step]] | None

List of steps to use on each starting port or all of them.

None
ends dbu | list[dbu] | list[Step] | list[list[Step]] | None

List of steps to use on each end port or all of them.

None
start_angles int | list[int] | None

Overwrite the port orientation of all start_ports together (single value) or each one (list of values which is as long as start_ports).

None
end_angles int | list[int] | None

Overwrite the port orientation of all start_ports together (single value) or each one (list of values which is as long as end_ports).

None

Returns:

Type Description
list[ManhattanRoute]

List of ManattanRoutes containing the instances of the route.

Raises:

Type Description
PlacerError

Something went wrong and the resulting route of the placer function is not manhattan or the elements cannot be fitted.

ValueError

Ports or places or args are misconfigured.

Source code in kfactory/routing/generic.py
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
def route_bundle(
    *,
    c: KCell,
    start_ports: list[BasePort],
    end_ports: list[BasePort],
    route_width: dbu | list[dbu] | None = None,
    on_collision: Literal["error", "show_error"] | None = "show_error",
    on_placer_error: Literal["error", "show_error"] | None = "show_error",
    collision_check_layers: Sequence[kdb.LayerInfo] | None = None,
    routing_function: ManhattanBundleRoutingFunction = route_smart,
    routing_kwargs: dict[str, Any] | None = None,
    placer_function: PlacerFunction,
    placer_kwargs: dict[str, Any] | None = None,
    constraints: Sequence[Constraint] | None = None,
    starts: dbu | list[dbu] | list[Step] | list[list[Step]] | None = None,
    ends: dbu | list[dbu] | list[Step] | list[list[Step]] | None = None,
    start_angles: int | list[int] | None = None,
    end_angles: int | list[int] | None = None,
    route_debug: RouteDebug | None = None,
    route_name: str | None = None,
) -> list[ManhattanRoute]:
    r"""Route a bundle from starting ports to end_ports.

    Waypoints will create a front which will create ports in a 1D array. If waypoints
    are a transformation it will be like a point with a direction. If multiple points
    are passed, the direction will be invfered.
    For orientation of 0 degrees it will create the following front for 4 ports:

    ```



          p1 ->








          p2 ->



      ___\waypoint
         /



          p3 ->








          p4 ->



    ```

    Args:
        c: Cell to place the route in.
        start_ports: List of start ports.
        end_ports: List of end ports.
        route_width: Width of the route. If None, the width of the ports is used.
        sort_ports: Automatically sort ports.
        on_collision: Define what to do on routing collision. Default behaviour is to
            open send the layout of c to klive and open an error lyrdb with the
            collisions. "error" will simply raise an error. None will ignore any error.
        on_placer_error: If a placing of the components fails, use the strategy above to
            handle the error. show_error will visualize it in klayout with the intended
            route along the already placed parts of c. Error will just throw an error.
            None will ignore the error.
        collision_check_layers: Layers to check for actual errors if manhattan routes
            detect potential collisions.
        routing_function: Function to place the routes. Must return a corresponding list
            of OpticalManhattan routes.
            Must accept the following protocol:
            ```
            routing_function(
                c: KCell, p1: Port, p2: Port, pts: list[Point], **placer_kwargs
            )
            ```
        routing_kwargs: Additional kwargs passed to the placer_function.
        placer_function: Function to place the routes. Must return a corresponding list
            of OpticalManhattan routes.
            Must accept the following protocol:
            ```
            placer_function(
                c: KCell, p1: Port, p2: Port, pts: list[Point], **placer_kwargs
            )
            ```
        placer_kwargs: Additional kwargs passed to the placer_function.
        constraints: Routing constraints to enforce after routing but before placement.
            Each constraint's `enforce` method is called with the routers and routing
            kwargs (e.g. separation, bend90_radius).
        starts: List of steps to use on each starting port or all of them.
        ends: List of steps to use on each end port or all of them.
        start_angles: Overwrite the port orientation of all start_ports together
            (single value) or each one (list of values which is as long as start_ports).
        end_angles: Overwrite the port orientation of all start_ports together
            (single value) or each one (list of values which is as long as end_ports).

    Returns:
        List of ManattanRoutes containing the instances of the route.

    Raises:
        PlacerError: Something went wrong and the resulting route of the placer function
            is not manhattan or the elements cannot be fitted.
        ValueError: Ports or places or args are misconfigured.
    """
    if ends is None:
        ends = []
    if starts is None:
        starts = []
    if placer_kwargs is None:
        placer_kwargs = {}
    if routing_kwargs is None:
        routing_kwargs = {"bbox_routing": "minimal"}
    if route_debug is not None:
        routing_kwargs["route_debug"] = route_debug
    if not start_ports:
        return []
    if not (len(start_ports) == len(end_ports)):
        raise ValueError(
            "For bundle routing the input port list must have"
            " the same size as the end ports and be the same length."
        )
    length = len(start_ports)
    if starts is None or starts == []:
        starts = [[]] * length
    elif isinstance(starts, int):
        starts = [[Straight(dist=starts)] for _ in range(length)]
    elif isinstance(starts, list):
        if _is_steps_list(starts):
            starts = [starts for _ in range(len(start_ports))]
        else:
            starts = cast("list[int]", starts)
            starts = [[Straight(dist=s) for s in starts]] * len(start_ports)
    if ends is None or ends == []:
        ends = [[]] * length
    elif isinstance(ends, int):
        ends = [[Straight(dist=ends)] for _ in range(length)]
    elif isinstance(ends, list):
        if _is_steps_list(ends):
            ends = [ends for _ in range(len(end_ports))]
        else:
            ends = cast("list[int]", ends)
            ends = [[Straight(dist=e) for e in ends]] * len(end_ports)

    if start_angles is not None:
        if isinstance(start_angles, int):
            start_ports = [
                p.transformed(post_trans=kdb.Trans(start_angles - p.get_trans().angle))
                for p in start_ports
            ]
        else:
            if not len(start_angles) == len(start_ports):
                raise ValueError(
                    "If more than one end port should be rotated,"
                    " a rotation for all ports must be provided."
                )
            start_ports = [
                p.transformed(post_trans=kdb.Trans(a - p.get_trans().angle))
                for a, p in zip(start_angles, start_ports, strict=False)
            ]

    if end_angles is not None:
        if isinstance(end_angles, int):
            end_ports = [
                p.transformed(post_trans=kdb.Trans(end_angles - p.get_trans().angle))
                for p in end_ports
            ]
        else:
            if not len(end_angles) == len(end_ports):
                raise ValueError(
                    "If more than one end port should be rotated,"
                    " a rotation for all ports must be provided."
                )
            end_ports = [
                p.transformed(post_trans=kdb.Trans(a - p.get_trans().angle))
                for a, p in zip(end_angles, start_ports, strict=False)
            ]

    if route_width:
        if isinstance(route_width, int):
            widths = [route_width] * len(start_ports)
        else:
            widths = route_width
    else:
        widths = [p.cross_section.width for p in start_ports]

    routers = routing_function(
        start_ports=start_ports,
        end_ports=end_ports,
        widths=widths,
        starts=starts,
        ends=ends,
        **routing_kwargs,
    )

    if not routers:
        return []

    start_mapping = {sp.get_trans(): sp for sp in start_ports}
    end_mapping = {ep.get_trans(): ep for ep in end_ports}
    routes: list[ManhattanRoute] = []
    start_ports = []
    end_ports = []

    for router in routers:
        sp = start_mapping[router.start_transformation]
        ep = end_mapping[router.end_transformation]
        start_ports.append(sp)
        end_ports.append(ep)

    if constraints:
        for constraint in constraints:
            constraint.enforce(
                c=c,
                routers=routers,
                route_name=route_name,
            )
    placer_errors: list[Exception] = []
    error_routes: list[tuple[BasePort, BasePort, list[kdb.Point], int]] = []
    for router, ps, pe in zip(routers, start_ports, end_ports, strict=False):
        try:
            route = placer_function(
                c,
                Port(base=ps),
                Port(base=pe),
                router.start.pts,
                **placer_kwargs,
            )
            routes.append(route)
        except Exception as e:
            placer_errors.append(e)
            error_routes.append((ps, pe, router.start.pts, router.width))
    if placer_errors and on_placer_error == "show_error":
        db = rdb.ReportDatabase("Route Placing Errors")
        c.name = c.kcl.future_cell_name or c.name
        cell = db.create_cell(c.name)
        for error, (ps, pe, pts, width) in zip(
            placer_errors, error_routes, strict=False
        ):
            cat = db.create_category(f"{ps.name} - {pe.name}")
            it = db.create_item(cell=cell, category=cat)
            it.add_value(
                f"Error while trying to place route from {ps.name} to {pe.name} at"
                f" points (dbu): {pts}"
            )
            it.add_value(f"Exception: {error}")
            path = kdb.Path(pts, width or ps.cross_section.width)
            it.add_value(c.kcl.to_um(path.polygon()))
        c.show(lyrdb=db)
    if placer_errors and on_placer_error is not None:
        for error in placer_errors:
            logger.error(error)
        if c.name.startswith("Unnamed_"):
            c.name = c.kcl.future_cell_name or c.name
        raise PlacerError(
            "Failed to place routes for bundle routing from "
            f"{[p.name for p in start_ports]} to {[p.name for p in end_ports]}"
        )

    check_collisions(
        c=c,
        start_ports=start_ports,
        end_ports=end_ports,
        on_collision=on_collision,
        collision_check_layers=collision_check_layers,
        routers=routers,
        routes=routes,
    )
    if constraints:
        for constraint in constraints:
            constraint._routes[route_name] = routes
    return routes