-
Notifications
You must be signed in to change notification settings - Fork 71
/
simulation.py
1234 lines (1091 loc) · 59.5 KB
/
simulation.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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
99
100
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
251
252
253
254
255
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
288
289
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
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# This code is part of KQCircuits
# Copyright (C) 2021 IQM Finland Oy
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with this program. If not, see
# https://www.gnu.org/licenses/gpl-3.0.html.
#
# The software distribution should follow IQM trademark policy for open-source software
# (meetiqm.com/developers/osstmpolicy). IQM welcomes contributions to the code. Please see our contribution agreements
# for individuals (meetiqm.com/developers/clas/individual) and organizations (meetiqm.com/developers/clas/organization).
import abc
import ast
from typing import List
import logging
from kqcircuits.defaults import default_faces
from kqcircuits.elements.airbridges.airbridge import Airbridge
from kqcircuits.elements.element import Element, resolve_face
from kqcircuits.elements.waveguide_composite import WaveguideComposite, Node
from kqcircuits.pya_resolver import pya
from kqcircuits.simulations.partition_region import PartitionRegion
from kqcircuits.simulations.port import Port, InternalPort, EdgePort
from kqcircuits.util.geometry_helper import (
region_with_merged_polygons,
region_with_merged_points,
merge_points_and_match_on_edges,
)
from kqcircuits.util.parameters import Param, pdt, add_parameters_from
from kqcircuits.simulations.export.util import find_edge_from_point_in_cell
from kqcircuits.simulations.export.util import get_enclosing_polygon
from kqcircuits.util.groundgrid import make_grid
from kqcircuits.junctions.sim import Sim
from kqcircuits.util.library_helper import load_libraries
simulation_layer_dict = dict()
load_libraries() # allows parameter overrides from defaults.py
def get_simulation_layer_by_name(layer_name):
"""Returns layer info of given name. If layer doesn't exist, a new layer is created.
New layers are created with data type = 0 and layer numbering starts from 1000."""
if layer_name not in simulation_layer_dict:
simulation_layer_dict[layer_name] = pya.LayerInfo(len(simulation_layer_dict) + 1000, 0, layer_name)
return simulation_layer_dict[layer_name]
@add_parameters_from(Element)
@add_parameters_from(Sim, "junction_total_length")
class Simulation:
"""Base class for simulation geometries.
Generally, this class is intended to be subclassed by a specific simulation implementation; the
implementation defines the simulation geometry and ports in `build`.
A convenience class method `Simulation.from_cell` is provided to create a Simulation from an
existing cell. In this case no ports will be added.
Basically, 3D layout is built of substrates, which are separated from each other by vacuum boxes, however, this rule
is modifiable by setting substrate and vacuum thicknesses to zero. In principle, one can stack faces on any of the
imaginable surface of a substrate. If substrate or vacuum thickness is set to zero, then there can be two
faces touching each other. Faces can be stacked on bottom or top surface of the substrates.
Number of substrate and vacuum boxes are determined with parameters face_stack and lower_box_height:
- If lower_box_height > 0, there will be a vacuum box below the lowest substrate, and the counting of faces will
start from the bottom surface of the lowest substrate. Otherwise, the lowest substrate will be below the lowest
vacuum box, and the counting of faces will start from the top surface of the lowest substrate.
- Length of face_stack list describes how many surfaces are taken into account in the simulation.
The terms in the face_stack indicate in which order the klayout faces are stacked in the 3D layout. Faces are
counted from the lowest substrate surface to the highest. One can also introduce face_stack as list of lists.
If a term in face_stack is a list, then all the faces given in the list are piled up on the corresponding surface
in the respective order. That means, the first term in the inner list indicates the face that is closest to the
surface of the substrate. One can use empty list in face_stack to leave certain surface
without metallization.
Heights of substrates (substrate_height) and vacuum boxes between surfaces (chip_distance) can be determined
individually from bottom to top or with single value. Any of the heights can be left zero, to indicate that there
is no vacuum between the substrates or substrate between the vacuum boxes. Also, the metal thickness (metal_height)
can be set to zero, but that means the metal layer is modelled as infinitely thin sheet. The insulator dielectric
can be set on any metal layer if non-zero dielectric_height is given.
"""
# The samples below show, how the layout is changed according to the parameters. Number of faces is unlimited:
#
# len(face_stack) = 1 len(face_stack) = 2 len(face_stack) = 2
# lower_box_height = 0 lower_box_height = 0 lower_box_height > 0
# |-------------------------| |-------------------------| |-------------------------|
# | | |/////////////////////////| | |
# | | |///substrate_height[1]///| | upper_box_height |
# | upper_box_height | |/////////////////////////| | |
# | | |----- face_stack[1] -----| |----- face_stack[1] -----|
# | | | | |/////////////////////////|
# |----- face_stack[0] -----| | chip_distance[0] | |///substrate_height[0]///|
# |/////////////////////////| | | |/////////////////////////|
# |/////////////////////////| |----- face_stack[0] -----| |----- face_stack[0] -----|
# |///substrate_height[0]///| |/////////////////////////| | |
# |/////////////////////////| |///substrate_height[0]///| | lower_box_height |
# |/////////////////////////| |/////////////////////////| | |
# |-------------------------| |-------------------------| |-------------------------|
LIBRARY_NAME = None # This is needed by some methods inherited from Element.
# Metadata associated with simulation
ports: List[Port]
# Parameters
box = Param(pdt.TypeShape, "Boundary box", pya.DBox(pya.DPoint(0, 0), pya.DPoint(10000, 10000)))
ground_grid_box = Param(pdt.TypeShape, "Border", pya.DBox(pya.DPoint(0, 0), pya.DPoint(10000, 10000)))
with_grid = Param(pdt.TypeBoolean, "Make ground plane grid", False)
name = Param(pdt.TypeString, "Name of the simulation", "Simulation")
use_ports = Param(pdt.TypeBoolean, "Turn off to disable all ports (for debugging)", True)
use_internal_ports = Param(pdt.TypeBoolean, "Use internal (lumped) ports. The alternative is wave ports.", True)
port_size = Param(
pdt.TypeDouble,
"Width and height of wave ports",
400.0,
unit="µm",
docstring="The port size can also be set as a list specifying the extensions from the center of "
"the port to left, right, down and up, respectively.",
)
upper_box_height = Param(pdt.TypeDouble, "Height of vacuum above top substrate", 1000.0, unit="µm")
lower_box_height = Param(
pdt.TypeDouble,
"Height of vacuum below bottom substrate",
0,
unit="µm",
docstring="Set > 0 to start face counting from substrate bottom layer.",
)
fixed_level_stackup = Param(pdt.TypeBoolean, "Use fixed level multi-face stack-up", True)
face_stack = Param(
pdt.TypeList,
"Face IDs on the substrate surfaces from bottom to top",
["1t1"],
docstring="The parameter can be set as list of lists to enable multi-face stack-up on substrate "
"surfaces. Set term to empty list to not have metal on the surface.",
)
substrate_height = Param(
pdt.TypeList,
"Height of the substrates",
[550.0, 375.0],
unit="[µm]",
docstring="The value can be scalar or list of scalars. Set as list to use individual "
"substrate heights from bottom to top.",
)
substrate_box = Param(
pdt.TypeList,
"x and y dimensions of substrates",
[None],
docstring="Set as a list of pya.DBox objects to give substrate dimensions individually from "
"bottom to top. If a value in the list is not pya.DBox object, the corresponding"
"substrate covers fully the general boundary box.",
)
substrate_material = Param(
pdt.TypeList,
"Material of the substrates.",
["silicon"],
docstring="Value can be string or list of strings. Use only keywords introduced in "
"material_dict. Set as list to use individual materials from bottom to top.",
)
material_dict = Param(
pdt.TypeString,
"Dictionary of dielectric materials",
"{'silicon': {'permittivity': 11.45}}",
docstring="Material property keywords follow Ansys Electromagnetics property names. "
"For example 'permittivity', 'dielectric_loss_tangent', etc.",
)
chip_distance = Param(
pdt.TypeList,
"Height of vacuum between two chips",
[8.0],
unit="[µm]",
docstring="The value can be scalar or list of scalars. Set as list to use individual chip "
"distances from bottom to top. The chip distances are measured between "
"the closest layers of the opposing chips.",
)
ground_metal_height = Param(
pdt.TypeDouble,
"Height of the grounded metal (in Xsection tool)",
0.2,
unit="µm",
docstring="Only used in Xsection tool and doesn't affect the 3D model",
)
signal_metal_height = Param(
pdt.TypeDouble,
"Height of the trace metal (in Xsection tool)",
0.2,
unit="µm",
docstring="Only used in Xsection tool and doesn't affect the 3D model",
)
airbridge_height = Param(pdt.TypeDouble, "Height of airbridges.", 3.4, unit="µm")
metal_height = Param(pdt.TypeList, "Height of metal sheet on each face.", [0.0], unit="µm")
dielectric_height = Param(pdt.TypeList, "Height of insulator dielectric on each face.", [0.0], unit="µm")
dielectric_material = Param(
pdt.TypeList,
"Material of insulator dielectric on each face.",
["silicon"],
unit="µm",
docstring="Use only keywords introduced in material_dict.",
)
waveguide_length = Param(
pdt.TypeDouble,
"Length of waveguide stubs or distance between couplers and waveguide " "turning point",
100,
unit="µm",
)
over_etching = Param(pdt.TypeDouble, "Expansion of metal gaps (negative to shrink the gaps).", 0, unit="μm")
vertical_over_etching = Param(pdt.TypeDouble, "Vertical over-etching into substrates at gaps.", 0, unit="μm")
hollow_tsv = Param(pdt.TypeBoolean, "Make TSVs hollow with vacuum inside and thin metal boundary.", False)
partition_regions = Param(
pdt.TypeString,
"Parameters of partition regions as list of dictionaries",
[],
docstring="See constructor of the PartitionRegion class for parameter definitions.",
)
tls_layer_thickness = Param(
pdt.TypeList, "Thickness of TLS interface layers (MA, MS, and SA, respectively)", [0.0], unit="µm"
)
tls_layer_material = Param(
pdt.TypeList,
"Materials of TLS interface layers (MA, MS, and SA, respectively)",
["vacuum", "silicon", "silicon"],
docstring="Use only keywords introduced in material_dict.",
)
tls_sheet_approximation = Param(pdt.TypeBoolean, "Approximate TLS interface layers as sheets", False)
minimum_point_spacing = Param(pdt.TypeDouble, "Tolerance for merging adjacent points in polygon", 0.01, unit="µm")
polygon_tolerance = Param(pdt.TypeDouble, "Tolerance for merging adjacent polygons in a layer", 0.004, unit="µm")
extra_json_data = Param(
pdt.TypeNone,
"Extra data in dict form to store in resulting JSON",
None,
docstring="This field may be used to store 'virtual' parameters useful for your simulations",
)
def __init__(self, layout, **kwargs):
"""Initialize a Simulation.
The initializer parses parameters, creates a top cell, and then calls `self.build` to create
the simulation geometry, followed by `self.create_simulation_layers` to process the geometry
so it is ready for exporting.
Args:
layout: the layout on which to create the simulation
Keyword arguments:
`**kwargs`:
Any parameter can be passed as a keyword argument.
In addition, `cell` can be passed as keyword argument. If `cell` is supplied, it will be
used as the top cell for the simulation. Otherwise, a new cell will be created. See
`Simulation.from_cell` for creating simulations from existing cells.
"""
self.refpoints = {}
if layout is None or not isinstance(layout, pya.Layout):
error_text = "Cannot create simulation with invalid or nil layout."
error = ValueError(error_text)
logging.error(error_text)
raise error
self.layout = layout
schema = type(self).get_schema()
# Apply kwargs or default value
# TODO: Validation? Could reuse the validation for Element
for parameter, item in schema.items():
if parameter in kwargs:
setattr(self, parameter, kwargs[parameter])
else:
setattr(self, parameter, item.default)
self.name = self.name.replace(" ", "").replace(",", "__") # no spaces or commas in filenames
self.ports = []
if "cell" in kwargs:
self.cell = kwargs["cell"]
else:
self.cell = layout.create_cell(self.name)
self.layers = dict()
self.build()
self.create_simulation_layers()
@classmethod
def from_cell(cls, cell, margin=300, grid_size=1, **kwargs):
"""Create a Simulation from an existing cell.
Arguments:
cell: existing top cell for the Simulation
margin: distance (μm) to expand the simulation box (ground plane) around the bounding
box of the cell If the `box` keyword argument is given, margin is ignored.
grid_size: size of the simulation box will be rounded to this resolution
If the `box` keyword argument is given, grid_size is ignored.
`**kwargs`: any simulation parameters passed
Returns:
Simulation instance
"""
extra_kwargs = {}
if "box" not in kwargs:
box = cell.dbbox().enlarge(margin, margin)
if grid_size > 0:
box.left = round(box.left / grid_size) * grid_size
box.right = round(box.right / grid_size) * grid_size
box.bottom = round(box.bottom / grid_size) * grid_size
box.top = round(box.top / grid_size) * grid_size
extra_kwargs["box"] = box
return cls(cell.layout(), cell=cell, **kwargs, **extra_kwargs)
@abc.abstractmethod
def build(self):
"""Build simulation geometry.
This method is to be overridden, and the overriding method should create the geometry to be
simulated and add any ports to `self.ports`.
"""
return
# Inherit specific methods from Element
get_schema = classmethod(Element.get_schema.__func__)
face = Element.face
insert_cell = Element.insert_cell
get_refpoints = Element.get_refpoints
add_element = Element.add_element
get_layer = Element.get_layer
pcell_params_by_name = Element.pcell_params_by_name
def face_stack_list_of_lists(self):
"""Return self.face_stack forced to be list of lists"""
return [f if isinstance(f, list) else [f] for f in self.face_stack]
def _face_box(self, i):
"""Return the boundary box for given face number.
Boundary box is the intersection of the global boundary box and the substrate x-y-dimension box.
"""
box = self.ith_value(self.substrate_box, (i + int(self.lower_box_height <= 0)) // 2)
return self.box & box if isinstance(box, pya.DBox) else self.box
@staticmethod
def ith_value(list_or_constant, i):
"""Helper function to return value from list or constant corresponding to the ordinal number i.
Too short lists are extended by duplicating the last value of the list.
"""
if isinstance(list_or_constant, list):
if i < len(list_or_constant):
return list_or_constant[i] # return ith term of the list
return list_or_constant[-1] # return last term of the list
return list_or_constant # return constant value
def face_z_levels(self):
"""Returns dictionary of z-levels. The dictionary can be used either with integer or string key values: Integer
keys return surface z-levels in ascending order (including domain boundary bottom and top). String keys
(key = face_id) return the three z-levels of the face (metal bottom, metal-dielectric interface, dielectric top)
The level z=0 is at lowest substrate top.
"""
# Build z-levels such that level z=0 is at very bottom. Z-transformation is applied later.
vacuum_at_bottom = self.lower_box_height > 0
z = self.lower_box_height if vacuum_at_bottom else float(self.ith_value(self.substrate_height, 0))
z_dict = dict()
z_list = [0.0]
z_trans = 0.0
stack = self.face_stack_list_of_lists()
for i, face_ids in enumerate(stack):
metal_heights = self.ith_value(self.metal_height, i)
dielectric_heights = self.ith_value(self.dielectric_height, i)
if bool(i % 2) == vacuum_at_bottom:
# faces on top of a substrate
z_list.append(z)
if i < 2:
z_trans = z # determine z-transformation
for j, face_id in enumerate(face_ids):
metal_z = z + float(self.ith_value(metal_heights, j))
dielectric_z = metal_z + float(self.ith_value(dielectric_heights, j))
z_dict[face_id] = [z, metal_z, dielectric_z]
z = dielectric_z
z += float(self.ith_value(self.chip_distance, i // 2)) if i < len(stack) - 1 else self.upper_box_height
else:
# faces on bottom of a substrate
for j, face_id in enumerate(face_ids[::-1]):
dielectric_z = z + float(self.ith_value(dielectric_heights, j))
metal_z = dielectric_z + float(self.ith_value(metal_heights, j))
z_dict[face_id] = [metal_z, dielectric_z, z]
z = metal_z
z_list.append(z)
z += float(self.ith_value(self.substrate_height, (i + 1) // 2))
z_list.append(z)
# Return combined dictionary such that level z=0 is at lowest substrate top
return {
**{k: z_list[k] - z_trans for k in range(-1, len(z_list))},
**{k: [x - z_trans for x in v] for k, v in z_dict.items()},
}
def region_from_layer(self, face_id, layer_name):
"""Returns a `Region` containing all geometry from a specified layer"""
face_layers = default_faces[face_id] if face_id in default_faces else dict()
if layer_name in face_layers:
return pya.Region(self.cell.begin_shapes_rec(self.layout.layer(face_layers[layer_name])))
return pya.Region()
def simplified_region(self, region, expansion=0.0):
"""Returns a region that is simplified by functions region_with_merged_polygons and region_with_merged_points.
More precisely:
- Merges polygons ignoring gaps that are smaller than self.polygon_tolerance
- Expands/shrinks region by amount given by 'expansion'
- In each polygon of the region, removes points that are closer to other points than self.minimum_point_spacing
"""
return region_with_merged_points(
region_with_merged_polygons(
region, tolerance=self.polygon_tolerance / self.layout.dbu, expansion=expansion / self.layout.dbu
),
tolerance=self.minimum_point_spacing / self.layout.dbu,
)
def insert_layer(self, layer_name, region, z0, z1, **params):
"""Adds layer parameters into 'self.layers' if region is non-empty."""
if not region.is_empty():
self.layers[layer_name] = {"region": region.dup(), "bottom": min(z0, z1), "top": max(z0, z1), **params}
def insert_stacked_up_layers(self, stack, z0):
"""Produces the layer stack-up and adds the layers into 'self.layers'.
Each layer is split into sub-layers by their z-level.
Args:
stack: list of layers in form of tuples containing (region, layer name, thickness, material)
z0: the base z-level for the layer stack-up
"""
levels = dict() # existing z-levels based on layers underneath (z-level as key and region as value)
for region, layer_name, thickness, material in stack:
if region.is_empty():
continue
# Split the layer into z-levels
region_levels = dict() # the layer region divided into z-levels
non_region_levels = dict() # the z-levels outside the layer region
sum_reg = pya.Region()
for z, reg in levels.items():
intersection = reg & region
if not intersection.is_empty():
region_levels[z] = intersection
subtraction = reg - region
if not subtraction.is_empty():
non_region_levels[z] = subtraction
sum_reg += reg
on_base_level = region - sum_reg
if not on_base_level.is_empty():
region_levels[round(z0, 12)] = on_base_level
# Create collective layer for klayout visualization if the layer is split
if len(region_levels) > 1:
self.cell.shapes(self.layout.layer(get_simulation_layer_by_name(layer_name))).insert(region)
# Apply parts of divided layers into self.layers
for i, (z, reg) in enumerate(sorted(region_levels.items())):
self.insert_layer(
f"{layer_name}_{i}" if len(region_levels) > 1 else layer_name,
reg,
z,
z + thickness,
material=material,
)
# Update existing z-levels dictionary
if thickness != 0.0:
levels = non_region_levels
for z, reg in region_levels.items():
top_z = round(z + thickness, 12)
if top_z in levels:
levels[top_z] += reg
else:
levels[top_z] = reg
def insert_layers_between_faces(self, i, opp_i, layer_name, **params):
"""Helper function to be used to produce indium bumps and TSVs"""
z = self.face_z_levels()
face_stack = self.face_stack_list_of_lists()
box = self._face_box(i)
if 0 <= opp_i < len(face_stack):
box = box & self._face_box(opp_i)
box_region = pya.Region(box.to_itype(self.layout.dbu))
sum_region = pya.Region()
for face_id in face_stack[i]:
region = self.simplified_region(self.region_from_layer(face_id, layer_name) & box_region)
if region.is_empty():
continue
sum_region += region
if 0 <= opp_i < len(face_stack):
for opp_id in face_stack[opp_i]:
common_region = region & self.simplified_region(
self.region_from_layer(opp_id, layer_name) & box_region
)
if common_region.is_empty():
continue
if f"{opp_id}_{face_id}_{layer_name}" not in self.layers: # if statement is to avoid duplicates
self.insert_layer(
f"{face_id}_{opp_id}_{layer_name}",
common_region,
z[face_id][1],
z[opp_id][1],
**params,
)
region -= common_region
if region.is_empty():
break
if not region.is_empty():
self.insert_layer(
face_id + "_" + layer_name,
region,
z[face_id][1],
z[opp_i + 1],
**params,
)
return sum_region
def create_simulation_layers(self):
"""Create the layers used for simulation export.
Based on any geometry defined on the relevant lithography layers.
This method is called from `__init__` after `build`, and should not be called directly.
Geometry is added to layers created specifically for simulation purposes. The layer numbers, z-levels,
thicknesses, materials, and other properties are stored in 'self.layers' parameter.
In the simulation-specific layers, all geometry has been merged and converted to simple polygons, that is,
polygons without holes.
"""
z = self.face_z_levels()
parts = [PartitionRegion(**(ast.literal_eval(r) if isinstance(r, str) else r)) for r in self.partition_regions]
for part in parts:
part.limit_box(z[0], z[-1], self.box, self.layout.dbu)
face_stack = self.face_stack_list_of_lists()
for i, face_ids in enumerate(face_stack):
sign = (-1) ** (i + int(self.lower_box_height > 0))
stack = []
dielectric_material = self.ith_value(self.dielectric_material, i)
# insert TSVs and indium bumps
tsv_params = {"edge_material": "pec"} if self.hollow_tsv else {"material": "pec"}
tsv_region = self.insert_layers_between_faces(i, i - sign, "through_silicon_via", **tsv_params)
bump_region = self.insert_layers_between_faces(i, i + sign, "indium_bump", material="pec")
ground_box_region = pya.Region(self._face_box(i).to_itype(self.layout.dbu))
for j, face_id in enumerate(face_ids):
metal_gap_region = self.region_from_layer(face_id, "base_metal_gap_wo_grid")
metal_add_region = self.region_from_layer(face_id, "base_metal_addition")
if self.over_etching >= 0:
lithography_region = self.simplified_region(metal_gap_region - metal_add_region, self.over_etching)
else:
lithography_region = ground_box_region - self.simplified_region(
ground_box_region - (metal_gap_region - metal_add_region), -self.over_etching
)
for port in self.ports:
if resolve_face(port.face, self.face_ids) == face_id and hasattr(port, "get_etch_polygon"):
lithography_region += pya.Region(port.get_etch_polygon().to_itype(self.layout.dbu))
if lithography_region.is_empty():
signal_region = pya.Region()
ground_region = ground_box_region.dup()
else:
signal_region = ground_box_region - lithography_region
# Find the ground plane and subtract it from the simulation area
# First, add all polygons touching any of the edges
ground_region = pya.Region()
for edge in ground_box_region.edges():
ground_region += signal_region.interacting(edge)
# Now, remove all edge polygons which are also a port
if self.use_ports:
for port in self.ports:
if resolve_face(port.face, self.face_ids) == face_id:
if hasattr(port, "ground_location"):
v_mps = port.signal_location - port.ground_location
v_mps = self.minimum_point_spacing * v_mps / v_mps.abs()
signal_loc = (port.signal_location + v_mps).to_itype(self.layout.dbu)
ground_region -= ground_region.interacting(pya.Edge(signal_loc, signal_loc))
ground_loc = (port.ground_location - v_mps).to_itype(self.layout.dbu)
ground_region += signal_region.interacting(pya.Edge(ground_loc, ground_loc))
else:
signal_loc = port.signal_location.to_itype(self.layout.dbu)
ground_region -= ground_region.interacting(pya.Edge(signal_loc, signal_loc))
ground_region.merge()
signal_region -= ground_region
dielectric_region = ground_box_region - self.simplified_region(
self.region_from_layer(face_id, "dielectric_etch")
)
# Create gap and etch regions and update metals
gap_region = ground_box_region - signal_region - ground_region # excluding ground grid
if self.with_grid:
ground_region -= self.ground_grid_region(face_id)
etch_region = ground_box_region - signal_region - ground_region # including ground grid
signal_region -= tsv_region
ground_region -= tsv_region
dielectric_region -= tsv_region
metal_region = signal_region + ground_region
for part in parts:
if part.face is not None and resolve_face(part.face, self.face_ids) == face_id:
part.limit_face(z[face_id][0], sign, metal_region, etch_region, self.layout.dbu)
# Insert signal, ground and dielectric layers
dielectric_thickness = z[face_id][2] - z[face_id][1]
if self.fixed_level_stackup:
# Use fixed level stack-up
self.insert_layer(face_id + "_signal", signal_region, z[face_id][0], z[face_id][1], material="pec")
self.insert_layer(face_id + "_ground", ground_region, z[face_id][0], z[face_id][1], material="pec")
if dielectric_thickness != 0.0:
self.insert_layer(
face_id + "_via",
ground_box_region - dielectric_region,
z[face_id][1],
z[face_id][2],
material="pec",
)
self.insert_layer(
face_id + "_dielectric",
ground_box_region,
z[face_id][0],
z[face_id][2],
material=self.ith_value(dielectric_material, j),
)
else:
# Use stack to produce drop-down stack-up
metal_thickness = z[face_id][1] - z[face_id][0]
stack.append((signal_region, face_id + "_signal", metal_thickness, "pec"))
stack.append((ground_region, face_id + "_ground", metal_thickness, "pec"))
if dielectric_thickness != 0.0:
stack.append(
(
dielectric_region,
face_id + "_dielectric",
dielectric_thickness,
self.ith_value(dielectric_material, j),
)
)
# Insert gap and etch layers only on the first face of the stack-up (no material)
if j == 0:
if self.vertical_over_etching > 0.0:
etch_z = z[face_id][0] - sign * self.vertical_over_etching
self.insert_layer(face_id + "_etch", etch_region, etch_z, z[face_id][0])
self.insert_layer(face_id + "_gap", gap_region, z[face_id][0], z[face_id][1])
# Insert airbridges
bridge_z = z[face_id][1] + sign * self.airbridge_height
ab_flyover_region = (
self.simplified_region(self.region_from_layer(face_id, "airbridge_flyover")) & ground_box_region
)
self.insert_layer(
face_id + "_airbridge_flyover",
ab_flyover_region,
bridge_z,
bridge_z,
material="pec",
)
ab_pads_region = (
self.simplified_region(self.region_from_layer(face_id, "airbridge_pads")) & ground_box_region
)
self.insert_layer(face_id + "_airbridge_pads", ab_pads_region, z[face_id][1], bridge_z, material="pec")
self.insert_stacked_up_layers(stack, z[i + 1])
# Rest of the features are not available with multilayer stack-up
if len(face_ids) != 1:
continue
face_id = face_ids[0]
# Insert TLS interface layers
for layer_num, layer_id in enumerate(["MA", "MS", "SA"]):
layer_name = face_id + "_layer" + layer_id
layer_z = [z[face_id][1], z[face_id][0], z[face_id][0] - sign * self.vertical_over_etching][layer_num]
thickness = float(self.ith_value(self.tls_layer_thickness, layer_num))
layer_top_z = layer_z + [sign, -sign, -sign][layer_num] * thickness
material = self.ith_value(self.tls_layer_material, layer_num)
if self.tls_sheet_approximation:
z_params = {"z0": layer_top_z, "z1": layer_top_z}
elif thickness != 0.0:
z_params = {"z0": layer_z, "z1": layer_top_z}
# Insert wall layer
if layer_z != z[face_id][0]:
wall_region = metal_region.sized(thickness / self.layout.dbu) & etch_region
self.insert_layer(
layer_name + "wall",
wall_region,
layer_z,
z[face_id][0],
material=material,
)
else:
continue
# Insert layer
layer_region = [
(
metal_region
if self.tls_sheet_approximation
else metal_region.sized(thickness / self.layout.dbu)
& (metal_region + etch_region - bump_region - ab_pads_region)
),
metal_region,
etch_region,
][layer_num]
self.insert_layer(layer_name, layer_region, material=material, **z_params)
# Insert substrates
for i in range(int(self.lower_box_height > 0), len(face_stack) + 1, 2):
self.insert_layer(
"substrate" if len(face_stack) - int(self.lower_box_height > 0) < 2 else f"substrate_{i // 2}",
pya.Region(self._face_box(i).to_itype(self.layout.dbu)),
z[i],
z[i + 1],
material=self.ith_value(self.substrate_material, i // 2),
subtract_keys=["_etch", "_through_silicon_via"],
)
# Insert vacuum
self.insert_layer(
"vacuum",
pya.Region(self.box.to_itype(self.layout.dbu)),
z[0],
z[-1],
material="vacuum",
)
self.produce_layers(parts)
# Eliminate gaps and overlaps caused by transformation to simple_polygon
merge_points_and_match_on_edges(self.cell, self.layout, [get_simulation_layer_by_name(n) for n in self.layers])
# Visualise parititon regions
for part in parts:
if part.visualise:
self.visualise_region(part.region, part.name, f"part_reg_{part.name}")
def produce_layers(self, parts):
"""Finalizes and partitions self.layers.
Metals and non-model objects are left without partitioning. We assume that these do not overlap.
Vacuum or dielectric objects are partitioned if parts is not empty. If these objects overlap, the smaller is
subtracted from larger.
Non-model objects are subtracted from vacuum or dielectric objects only if non-model object is mentioned in
subtract_keys of vacuum or dielectric object.
"""
layers = []
def can_modify(obj):
return obj.get("material", None) not in ["pec", None]
def are_separate(obj, tool):
"""Returns True if obj and tool do not overlap"""
if obj["bottom"] == obj["top"] and tool["bottom"] == tool["top"]:
if obj["bottom"] != tool["bottom"]:
return True
elif obj["top"] <= tool["bottom"] or tool["top"] <= obj["bottom"]:
return True
return tool["region"].overlapping(obj["region"]).is_empty()
def subtract(obj, lay):
"""Subtracts layers[lay] from obj."""
if lay in obj.get("subtract", set()):
return # already subtracted
tool = layers[lay]
if tool.get("material", None) is None and all(n not in tool["name"] for n in obj.get("subtract_keys", [])):
return # non-material tools are subtracted only if specified in subtract keys
if obj["bottom"] != obj["top"] and tool["bottom"] == tool["top"]:
return # do not subtract sheet from solid
if are_separate(obj, tool):
return # ignore separate objects
obj["subtract"] = obj.get("subtract", set()) | {lay}
def subtract_hard(obj, tool):
"""Subtracts tool from obj by modifying dimensions of obj. Returns True is successful.
Assumes that tool and obj overlap."""
subtract_diff = tool.get("subtract", set()) - obj.get("subtract", set())
if any(layers[s].get("material", None) is None and not are_separate(obj, layers[s]) for s in subtract_diff):
return False # can't apply hard subtract if tool has non-material subtractions that obj doesn't have
if obj["bottom"] < tool["bottom"]:
if tool["top"] < obj["top"]:
return False
if obj["region"].not_inside(tool["region"]).is_empty():
obj["top"] = tool["bottom"]
return True
return False
if tool["top"] < obj["top"]:
if obj["region"].not_inside(tool["region"]).is_empty():
obj["bottom"] = tool["top"]
return True
return False
if not can_modify(tool) and tool["region"].inside(obj["region"]).count() > 10 * obj["region"].count():
return False # avoid lateral hard subtract if it creates lots of holes (useful with lots of vias)
obj["region"] -= tool["region"]
return True
def exists(obj):
"""Hardens subtractions and returns True if geometry exists."""
obj["subtract"] = {s for s in obj.get("subtract", set()) if not are_separate(obj, layers[s])}
soft_subtract = {s for s in obj["subtract"] if not subtract_hard(obj, layers[s])}
while len(soft_subtract) < len(obj["subtract"]):
obj["subtract"] = {s for s in soft_subtract if not are_separate(obj, layers[s])}
soft_subtract = {s for s in obj["subtract"] if not subtract_hard(obj, layers[s])}
return not obj["region"].is_empty()
layer_list = [
{
"name": name,
"bottom": round(layer["bottom"], 12),
"top": round(layer["top"], 12),
**{n: v for n, v in layer.items() if n not in ["bottom", "top"]},
}
for name, layer in self.layers.items()
]
part_list = [
{
"name": part.name,
"bottom": round(part.z[0], 12),
"top": round(part.z[1], 12),
"region": part.region.dup(),
}
for part in parts
if part.face is None
]
for layer in sorted(layer_list, key=lambda x: (can_modify(x), x["top"] - x["bottom"], x["region"].area())):
if can_modify(layer):
# subtract layers that are added to layer_list
for i in range(len(layers)):
subtract(layer, i)
# partition the layer into sub-layers
for part in part_list:
if are_separate(layer, part):
continue # ignore separate objects
intersection = {
"bottom": max(layer["bottom"], part["bottom"]),
"top": min(layer["top"], part["top"]),
"region": layer["region"] & part["region"],
"material": layer.get("material", None),
"subtract_keys": layer.get("subtract_keys", []),
}
for s in layer.get("subtract", set()):
subtract(intersection, s)
if exists(intersection):
layers.append(intersection)
subtract(part, len(layers) - 1)
subtract(layer, len(layers) - 1)
intersection["name"] = (layer["name"] if "used" in part or exists(part) else "") + part["name"]
part["used"] = True
# add non-partitioned parts of the layer
if exists(layer):
layers.append(layer)
# produce self.layers from layers
self.layers = dict()
for layer in layers:
sim_layer = get_simulation_layer_by_name(layer["name"])
self.cell.shapes(self.layout.layer(sim_layer)).insert(layer["region"])
limit_region = pya.Region(self.box.to_itype(self.layout.dbu)).inside(layer["region"]).is_empty()
subtract = [layers[n]["name"] for n in sorted(layer.get("subtract", set()), reverse=True)]
self.layers[layer["name"]] = {
"z": round(layer["bottom"], 12),
"thickness": round(layer["top"] - layer["bottom"], 12),
**({"layer": sim_layer.layer} if limit_region else dict()),
**{k: v for k, v in layer.items() if k in ["material", "edge_material"] and v is not None},
**({"subtract": subtract} if subtract else dict()),
}
def ground_grid_region(self, face_id):
"""Returns region of ground grid for the given face id."""
grid_area = self.ground_grid_box * (1 / self.layout.dbu)
protection = self.simplified_region(self.region_from_layer(face_id, "ground_grid_avoidance"))
grid_mag_factor = 1
return make_grid(
grid_area,
protection,
grid_step=10 * (1 / self.layout.dbu) * grid_mag_factor,
grid_size=5 * (1 / self.layout.dbu) * grid_mag_factor,
)
def produce_waveguide_to_port(
self,
location,
towards,
port_nr,
side=None,
use_internal_ports=None,
waveguide_length=None,
term1=0,
turn_radius=None,
a=None,
b=None,
airbridge=False,
face=0,
etch_opposite_face=False,
**port_kwargs,
):
"""Create a waveguide connection from some `location` to a port, and add the corresponding port to
`simulation.ports`.
Arguments:
location (pya.DPoint): Point where the waveguide connects to the simulation
towards (pya.DPoint): Point that sets the direction of the waveguide.
The waveguide will start from `location` and go towards `towards`
port_nr (int): Port index for the simulation engine starting from 1
side (str): Indicate on which edge the waveguide is routed to, either `left`, `right`, `top` or `bottom`.
Ignored when use_internal_ports=True. If `None` then the edge is inferred from waveguide direction.
use_internal_ports: If True, a lumped port is placed at the end of straight waveguide segment. If False,
the waveguide is brought out to a wave port at the edge of the box, determined by `side`.
If the value is a string 'at_edge', then a lumped port will be placed next to the edge.
Defaults to the value of the `use_internal_ports` parameter.
waveguide_length (float, optional): length of the straight waveguide starting from `location` (μm).
Defaults to the value of the `waveguide_length` parameter.
term1 (float, optional): Termination gap (μm) at `location`. Default 0
turn_radius (float, optional): Turn radius of the waveguide. Defaults to the value of the `r` parameter.
a (float, optional): Center conductor width. Defaults to the value of the `a` parameter
b (float, optional): Conductor gap width. Defaults to the value of the `b` parameter
airbridge (bool, optional): if True, an airbridge will be inserted at `location`. Default False.
face: face to place waveguide and port on. Either 0 (default) or 1, for bottom or top face.
etch_opposite_face: If true, the metal on opposite face of the waveguide is etched away.
port_kwargs: keyword arguments passed for port
"""
waveguide_gap_extension = 1 # Extend gaps beyond waveguides into ground plane to define the ground port edge
if turn_radius is None:
turn_radius = self.r
if a is None:
a = self.a
if b is None:
b = self.b
if use_internal_ports is None:
use_internal_ports = self.use_internal_ports
if waveguide_length is None:
waveguide_length = self.waveguide_length
waveguide_a = a
waveguide_b = b
# First node may be an airbridge
if airbridge:
first_node = Node(location, Airbridge, a=a, b=b)
waveguide_a = Airbridge.bridge_width
waveguide_b = Airbridge.bridge_width / Element.a * Element.b
else:
first_node = Node(location)
d = towards - location
direction = d / d.length()
internal_port_length = a - 2 * self.over_etching # compensated with over etching
if use_internal_ports in [False, "at_edge"]: # edge port or internal port next to edge
if side is None:
side = (("left", "right"), ("bottom", "top"))[abs(d.x) < abs(d.y)][d.x + d.y > 0]
out_direction = {
"left": pya.DVector(-1, 0),
"right": pya.DVector(1, 0),
"top": pya.DVector(0, 1),
"bottom": pya.DVector(0, -1),
}[side]
turn_length = turn_radius * abs(out_direction.vprod(direction)) / (1 + out_direction.sprod(direction))
corner_point = location + (waveguide_length + turn_length) * direction
port_edge_point = {
"left": pya.DPoint(self.box.left, corner_point.y),
"right": pya.DPoint(self.box.right, corner_point.y),
"top": pya.DPoint(corner_point.x, self.box.top),
"bottom": pya.DPoint(corner_point.x, self.box.bottom),
}[side]
nodes = [
first_node,
Node(corner_point),
Node(port_edge_point),
]
if use_internal_ports == "at_edge":