pyedb 0.38.0__py3-none-any.whl → 0.39.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyedb might be problematic. Click here for more details.

Files changed (205) hide show
  1. pyedb/__init__.py +1 -1
  2. pyedb/common/nets.py +53 -139
  3. pyedb/configuration/cfg_components.py +1 -1
  4. pyedb/configuration/cfg_general.py +4 -2
  5. pyedb/configuration/cfg_modeler.py +1 -1
  6. pyedb/configuration/cfg_package_definition.py +1 -1
  7. pyedb/configuration/cfg_padstacks.py +1 -1
  8. pyedb/configuration/cfg_ports_sources.py +56 -23
  9. pyedb/configuration/configuration.py +18 -1
  10. pyedb/dotnet/{application → database}/Variables.py +21 -21
  11. pyedb/dotnet/{edb_core → database}/cell/connectable.py +5 -5
  12. pyedb/dotnet/{edb_core → database}/cell/hierarchy/component.py +11 -11
  13. pyedb/dotnet/{edb_core → database}/cell/hierarchy/hierarchy_obj.py +1 -1
  14. pyedb/dotnet/{edb_core → database}/cell/hierarchy/model.py +1 -1
  15. pyedb/dotnet/{edb_core → database}/cell/layout.py +17 -17
  16. pyedb/dotnet/{edb_core → database}/cell/layout_obj.py +3 -3
  17. pyedb/dotnet/{edb_core → database}/cell/primitive/bondwire.py +1 -1
  18. pyedb/dotnet/{edb_core → database}/cell/primitive/path.py +4 -4
  19. pyedb/dotnet/{edb_core → database}/cell/primitive/primitive.py +14 -14
  20. pyedb/dotnet/{edb_core → database}/cell/terminal/bundle_terminal.py +2 -2
  21. pyedb/dotnet/{edb_core → database}/cell/terminal/edge_terminal.py +4 -4
  22. pyedb/dotnet/{edb_core → database}/cell/terminal/padstack_instance_terminal.py +2 -2
  23. pyedb/dotnet/{edb_core → database}/cell/terminal/pingroup_terminal.py +2 -2
  24. pyedb/dotnet/{edb_core → database}/cell/terminal/point_terminal.py +2 -2
  25. pyedb/dotnet/{edb_core → database}/cell/terminal/terminal.py +11 -11
  26. pyedb/dotnet/{edb_core → database}/cell/voltage_regulator.py +2 -2
  27. pyedb/dotnet/{edb_core → database}/components.py +101 -124
  28. pyedb/dotnet/{edb_core → database}/definition/component_def.py +5 -5
  29. pyedb/dotnet/{edb_core → database}/definition/component_model.py +1 -1
  30. pyedb/dotnet/{edb_core → database}/definition/definition_obj.py +1 -1
  31. pyedb/dotnet/{edb_core → database}/definition/definitions.py +2 -2
  32. pyedb/dotnet/{edb_core → database}/definition/package_def.py +4 -4
  33. pyedb/dotnet/{edb_core → database}/dotnet/database.py +8 -8
  34. pyedb/dotnet/{edb_core → database}/dotnet/primitive.py +9 -9
  35. pyedb/dotnet/{edb_core → database}/edb_data/control_file.py +12 -12
  36. pyedb/dotnet/{edb_core → database}/edb_data/hfss_extent_info.py +7 -7
  37. pyedb/dotnet/{edb_core → database}/edb_data/nets_data.py +10 -13
  38. pyedb/dotnet/{edb_core → database}/edb_data/padstacks_data.py +16 -16
  39. pyedb/dotnet/{edb_core → database}/edb_data/ports.py +4 -4
  40. pyedb/dotnet/{edb_core → database}/edb_data/primitives_data.py +5 -5
  41. pyedb/dotnet/{edb_core → database}/edb_data/raptor_x_simulation_setup_data.py +4 -4
  42. pyedb/dotnet/{edb_core → database}/edb_data/simulation_configuration.py +10 -10
  43. pyedb/dotnet/{edb_core → database}/edb_data/sources.py +4 -4
  44. pyedb/dotnet/{edb_core → database}/edb_data/variables.py +1 -1
  45. pyedb/dotnet/{edb_core → database}/geometry/polygon_data.py +4 -4
  46. pyedb/dotnet/{edb_core → database}/hfss.py +8 -8
  47. pyedb/dotnet/{edb_core → database}/layout_obj_instance.py +1 -1
  48. pyedb/dotnet/{edb_core → database}/layout_validation.py +2 -2
  49. pyedb/dotnet/{edb_core → database}/materials.py +23 -8
  50. pyedb/dotnet/{edb_core → database}/modeler.py +27 -27
  51. pyedb/dotnet/{edb_core → database}/net_class.py +8 -8
  52. pyedb/dotnet/{edb_core → database}/nets.py +12 -12
  53. pyedb/dotnet/{edb_core → database}/padstack.py +15 -15
  54. pyedb/dotnet/{edb_core → database}/sim_setup_data/data/mesh_operation.py +1 -1
  55. pyedb/dotnet/{edb_core → database}/sim_setup_data/data/settings.py +3 -3
  56. pyedb/dotnet/{edb_core → database}/sim_setup_data/data/sim_setup_info.py +2 -2
  57. pyedb/dotnet/{edb_core → database}/sim_setup_data/data/simulation_settings.py +1 -1
  58. pyedb/dotnet/{edb_core → database}/sim_setup_data/data/siw_dc_ir_settings.py +1 -1
  59. pyedb/dotnet/{edb_core → database}/sim_setup_data/data/sweep_data.py +1 -1
  60. pyedb/dotnet/{edb_core → database}/siwave.py +10 -10
  61. pyedb/dotnet/{edb_core → database}/stackup.py +12 -12
  62. pyedb/dotnet/{edb_core → database}/utilities/hfss_simulation_setup.py +15 -15
  63. pyedb/dotnet/{edb_core → database}/utilities/obj_base.py +1 -1
  64. pyedb/dotnet/{edb_core → database}/utilities/simulation_setup.py +3 -3
  65. pyedb/dotnet/{edb_core → database}/utilities/siwave_simulation_setup.py +6 -6
  66. pyedb/dotnet/edb.py +117 -112
  67. pyedb/generic/design_types.py +26 -19
  68. pyedb/generic/general_methods.py +1 -1
  69. pyedb/generic/plot.py +0 -2
  70. pyedb/grpc/database/__init__.py +1 -0
  71. pyedb/grpc/database/components.py +2354 -0
  72. pyedb/grpc/database/control_file.py +1277 -0
  73. pyedb/grpc/database/definition/component_def.py +218 -0
  74. pyedb/grpc/database/definition/component_model.py +39 -0
  75. pyedb/grpc/database/definition/component_pin.py +32 -0
  76. pyedb/grpc/database/definition/materials.py +1207 -0
  77. pyedb/grpc/database/definition/n_port_component_model.py +34 -0
  78. pyedb/grpc/database/definition/package_def.py +227 -0
  79. pyedb/grpc/database/definition/padstack_def.py +842 -0
  80. pyedb/grpc/database/definitions.py +70 -0
  81. pyedb/grpc/database/general.py +43 -0
  82. pyedb/grpc/database/geometry/__init__.py +0 -0
  83. pyedb/grpc/database/geometry/arc_data.py +93 -0
  84. pyedb/grpc/database/geometry/point_3d_data.py +79 -0
  85. pyedb/grpc/database/geometry/point_data.py +30 -0
  86. pyedb/grpc/database/geometry/polygon_data.py +133 -0
  87. pyedb/grpc/database/hfss.py +1279 -0
  88. pyedb/grpc/database/hierarchy/__init__.py +0 -0
  89. pyedb/grpc/database/hierarchy/component.py +1301 -0
  90. pyedb/grpc/database/hierarchy/model.py +31 -0
  91. pyedb/grpc/database/hierarchy/netlist_model.py +30 -0
  92. pyedb/grpc/database/hierarchy/pin_pair_model.py +128 -0
  93. pyedb/grpc/database/hierarchy/pingroup.py +245 -0
  94. pyedb/grpc/database/hierarchy/s_parameter_model.py +33 -0
  95. pyedb/grpc/database/hierarchy/spice_model.py +48 -0
  96. pyedb/grpc/database/layers/__init__.py +0 -0
  97. pyedb/grpc/database/layers/layer.py +57 -0
  98. pyedb/grpc/database/layers/stackup_layer.py +410 -0
  99. pyedb/grpc/database/layout/__init__.py +0 -0
  100. pyedb/grpc/database/layout/cell.py +30 -0
  101. pyedb/grpc/database/layout/layout.py +196 -0
  102. pyedb/grpc/database/layout/voltage_regulator.py +149 -0
  103. pyedb/grpc/database/layout_validation.py +319 -0
  104. pyedb/grpc/database/modeler.py +1468 -0
  105. pyedb/grpc/database/net/__init__.py +0 -0
  106. pyedb/grpc/database/net/differential_pair.py +138 -0
  107. pyedb/grpc/database/net/extended_net.py +340 -0
  108. pyedb/grpc/database/net/net.py +198 -0
  109. pyedb/grpc/database/net/net_class.py +93 -0
  110. pyedb/grpc/database/nets.py +633 -0
  111. pyedb/grpc/database/padstacks.py +1500 -0
  112. pyedb/grpc/database/ports/__init__.py +0 -0
  113. pyedb/grpc/database/ports/ports.py +396 -0
  114. pyedb/grpc/database/primitive/__init__.py +3 -0
  115. pyedb/grpc/database/primitive/bondwire.py +181 -0
  116. pyedb/grpc/database/primitive/circle.py +75 -0
  117. pyedb/grpc/database/primitive/padstack_instance.py +1116 -0
  118. pyedb/grpc/database/primitive/path.py +346 -0
  119. pyedb/grpc/database/primitive/polygon.py +276 -0
  120. pyedb/grpc/database/primitive/primitive.py +739 -0
  121. pyedb/grpc/database/primitive/rectangle.py +146 -0
  122. pyedb/grpc/database/simulation_setup/__init__.py +0 -0
  123. pyedb/grpc/database/simulation_setup/adaptive_frequency.py +33 -0
  124. pyedb/grpc/database/simulation_setup/hfss_advanced_meshing_settings.py +32 -0
  125. pyedb/grpc/database/simulation_setup/hfss_advanced_settings.py +59 -0
  126. pyedb/grpc/database/simulation_setup/hfss_dcr_settings.py +35 -0
  127. pyedb/grpc/database/simulation_setup/hfss_general_settings.py +61 -0
  128. pyedb/grpc/database/simulation_setup/hfss_settings_options.py +78 -0
  129. pyedb/grpc/database/simulation_setup/hfss_simulation_settings.py +118 -0
  130. pyedb/grpc/database/simulation_setup/hfss_simulation_setup.py +355 -0
  131. pyedb/grpc/database/simulation_setup/hfss_solver_settings.py +34 -0
  132. pyedb/grpc/database/simulation_setup/mesh_operation.py +34 -0
  133. pyedb/grpc/database/simulation_setup/raptor_x_advanced_settings.py +34 -0
  134. pyedb/grpc/database/simulation_setup/raptor_x_general_settings.py +33 -0
  135. pyedb/grpc/database/simulation_setup/raptor_x_simulation_settings.py +64 -0
  136. pyedb/grpc/database/simulation_setup/raptor_x_simulation_setup.py +125 -0
  137. pyedb/grpc/database/simulation_setup/siwave_dcir_simulation_setup.py +34 -0
  138. pyedb/grpc/database/simulation_setup/siwave_simulation_setup.py +119 -0
  139. pyedb/grpc/database/simulation_setup/sweep_data.py +32 -0
  140. pyedb/grpc/database/siwave.py +1023 -0
  141. pyedb/grpc/database/source_excitations.py +2572 -0
  142. pyedb/grpc/database/stackup.py +2574 -0
  143. pyedb/grpc/database/terminal/__init__.py +0 -0
  144. pyedb/grpc/database/terminal/bundle_terminal.py +218 -0
  145. pyedb/grpc/database/terminal/edge_terminal.py +51 -0
  146. pyedb/grpc/database/terminal/padstack_instance_terminal.py +171 -0
  147. pyedb/grpc/database/terminal/pingroup_terminal.py +162 -0
  148. pyedb/grpc/database/terminal/point_terminal.py +99 -0
  149. pyedb/grpc/database/terminal/terminal.py +470 -0
  150. pyedb/grpc/database/utility/__init__.py +3 -0
  151. pyedb/grpc/database/utility/constants.py +25 -0
  152. pyedb/grpc/database/utility/heat_sink.py +124 -0
  153. pyedb/grpc/database/utility/hfss_extent_info.py +448 -0
  154. pyedb/grpc/database/utility/layout_statistics.py +277 -0
  155. pyedb/grpc/database/utility/rlc.py +80 -0
  156. pyedb/grpc/database/utility/simulation_configuration.py +3305 -0
  157. pyedb/grpc/database/utility/sources.py +388 -0
  158. pyedb/grpc/database/utility/sweep_data_distribution.py +83 -0
  159. pyedb/grpc/database/utility/xml_control_file.py +1277 -0
  160. pyedb/grpc/edb.py +4151 -0
  161. pyedb/grpc/edb_init.py +481 -0
  162. pyedb/grpc/rpc_session.py +177 -0
  163. pyedb/ipc2581/ecad/cad_data/assembly_drawing.py +3 -2
  164. pyedb/ipc2581/ecad/cad_data/feature.py +4 -3
  165. pyedb/ipc2581/ecad/cad_data/layer_feature.py +32 -20
  166. pyedb/ipc2581/ecad/cad_data/outline.py +3 -2
  167. pyedb/ipc2581/ecad/cad_data/package.py +4 -3
  168. pyedb/ipc2581/ecad/cad_data/path.py +82 -31
  169. pyedb/ipc2581/ecad/cad_data/polygon.py +122 -60
  170. pyedb/ipc2581/ecad/cad_data/profile.py +13 -12
  171. pyedb/ipc2581/ecad/cad_data/step.py +53 -21
  172. pyedb/ipc2581/ipc2581.py +47 -49
  173. pyedb/modeler/geometry_operators.py +1 -1
  174. {pyedb-0.38.0.dist-info → pyedb-0.39.0.dist-info}/METADATA +5 -2
  175. pyedb-0.39.0.dist-info/RECORD +288 -0
  176. pyedb-0.38.0.dist-info/RECORD +0 -195
  177. /pyedb/dotnet/{edb_core → database}/__init__.py +0 -0
  178. /pyedb/dotnet/{application → database/cell}/__init__.py +0 -0
  179. /pyedb/dotnet/{edb_core/cell → database/cell/hierarchy}/__init__.py +0 -0
  180. /pyedb/dotnet/{edb_core → database}/cell/hierarchy/netlist_model.py +0 -0
  181. /pyedb/dotnet/{edb_core → database}/cell/hierarchy/pin_pair_model.py +0 -0
  182. /pyedb/dotnet/{edb_core → database}/cell/hierarchy/s_parameter_model.py +0 -0
  183. /pyedb/dotnet/{edb_core → database}/cell/hierarchy/spice_model.py +0 -0
  184. /pyedb/dotnet/{edb_core → database}/cell/primitive/__init__.py +0 -0
  185. /pyedb/dotnet/{edb_core/cell/hierarchy → database/cell/terminal}/__init__.py +0 -0
  186. /pyedb/dotnet/{edb_core/cell/terminal → database/definition}/__init__.py +0 -0
  187. /pyedb/dotnet/{edb_core/definition → database/dotnet}/__init__.py +0 -0
  188. /pyedb/dotnet/{edb_core/dotnet → database/edb_data}/__init__.py +0 -0
  189. /pyedb/dotnet/{edb_core → database}/edb_data/design_options.py +0 -0
  190. /pyedb/dotnet/{edb_core → database}/edb_data/edbvalue.py +0 -0
  191. /pyedb/dotnet/{edb_core → database}/edb_data/layer_data.py +0 -0
  192. /pyedb/dotnet/{edb_core → database}/edb_data/utilities.py +0 -0
  193. /pyedb/dotnet/{edb_core → database}/general.py +0 -0
  194. /pyedb/dotnet/{edb_core/edb_data → database/geometry}/__init__.py +0 -0
  195. /pyedb/dotnet/{edb_core → database}/geometry/point_data.py +0 -0
  196. /pyedb/dotnet/{edb_core → database}/sim_setup_data/__init__.py +0 -0
  197. /pyedb/dotnet/{edb_core → database}/sim_setup_data/data/__init__.py +0 -0
  198. /pyedb/dotnet/{edb_core → database}/sim_setup_data/data/adaptive_frequency_data.py +0 -0
  199. /pyedb/dotnet/{edb_core/geometry → database/sim_setup_data/io}/__init__.py +0 -0
  200. /pyedb/dotnet/{edb_core → database}/sim_setup_data/io/siwave.py +0 -0
  201. /pyedb/dotnet/{edb_core → database}/utilities/__init__.py +0 -0
  202. /pyedb/dotnet/{edb_core → database}/utilities/heatsink.py +0 -0
  203. /pyedb/{dotnet/edb_core/sim_setup_data/io → grpc/database/definition}/__init__.py +0 -0
  204. {pyedb-0.38.0.dist-info → pyedb-0.39.0.dist-info}/LICENSE +0 -0
  205. {pyedb-0.38.0.dist-info → pyedb-0.39.0.dist-info}/WHEEL +0 -0
@@ -0,0 +1,2574 @@
1
+ # Copyright (C) 2023 - 2024 ANSYS, Inc. and/or its affiliates.
2
+ # SPDX-License-Identifier: MIT
3
+ #
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in all
13
+ # copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ # SOFTWARE.
22
+
23
+ """
24
+ This module contains the `EdbStackup` class.
25
+
26
+ """
27
+
28
+ from __future__ import absolute_import
29
+
30
+ from collections import OrderedDict
31
+ import json
32
+ import logging
33
+ import math
34
+ import warnings
35
+
36
+ from ansys.edb.core.definition.die_property import DieOrientation as GrpcDieOrientation
37
+ from ansys.edb.core.definition.solder_ball_property import (
38
+ SolderballPlacement as GrpcSolderballPlacement,
39
+ )
40
+ from ansys.edb.core.geometry.point3d_data import Point3DData as GrpcPoint3DData
41
+ from ansys.edb.core.hierarchy.cell_instance import CellInstance as GrpcCellInstance
42
+ from ansys.edb.core.hierarchy.component_group import ComponentType as GrpcComponentType
43
+ from ansys.edb.core.layer.layer import LayerType as GrpcLayerType
44
+ from ansys.edb.core.layer.layer import TopBottomAssociation as GrpcTopBottomAssociation
45
+ from ansys.edb.core.layer.layer_collection import (
46
+ LayerCollectionMode as GrpcLayerCollectionMode,
47
+ )
48
+ from ansys.edb.core.layer.layer_collection import LayerCollection as GrpcLayerCollection
49
+ from ansys.edb.core.layer.layer_collection import LayerTypeSet as GrpcLayerTypeSet
50
+ from ansys.edb.core.layer.stackup_layer import StackupLayer as GrpcStackupLayer
51
+ from ansys.edb.core.layout.mcad_model import McadModel as GrpcMcadModel
52
+ from ansys.edb.core.utility.transform3d import Transform3D as GrpcTransform3D
53
+ from ansys.edb.core.utility.value import Value as GrpcValue
54
+
55
+ from pyedb.generic.general_methods import ET, generate_unique_name
56
+ from pyedb.grpc.database.layers.layer import Layer
57
+ from pyedb.grpc.database.layers.stackup_layer import StackupLayer
58
+ from pyedb.misc.aedtlib_personalib_install import write_pretty_xml
59
+
60
+ colors = None
61
+ pd = None
62
+ np = None
63
+ try:
64
+ import matplotlib.colors as colors
65
+ except ImportError:
66
+ colors = None
67
+
68
+ try:
69
+ import numpy as np
70
+ except ImportError:
71
+ np = None
72
+
73
+ try:
74
+ import pandas as pd
75
+ except ImportError:
76
+ pd = None
77
+
78
+ logger = logging.getLogger(__name__)
79
+
80
+
81
+ class LayerCollection(GrpcLayerCollection):
82
+ """Layer collection."""
83
+
84
+ def __init__(self, pedb, edb_object):
85
+ super().__init__(edb_object.msg)
86
+ self._layer_collection = edb_object
87
+ self._pedb = pedb
88
+
89
+ def update_layout(self):
90
+ """Set layer collection into edb.
91
+
92
+ Parameters
93
+ ----------
94
+ stackup
95
+ """
96
+ self._pedb.layout.layer_collection = self
97
+
98
+ def add_layer_top(self, name, layer_type="signal", **kwargs):
99
+ """Add a layer on top of the stackup.
100
+
101
+ Parameters
102
+ ----------
103
+ name : str
104
+ Name of the layer.
105
+ layer_type: str, optional
106
+ Type of the layer. The default to ``"signal"``. Options are ``"signal"``, ``"dielectric"``
107
+ kwargs
108
+
109
+ Returns
110
+ -------
111
+
112
+ """
113
+ thickness = GrpcValue(0.0)
114
+ if "thickness" in kwargs:
115
+ thickness = GrpcValue(kwargs["thickness"])
116
+ elevation = GrpcValue(0.0)
117
+ _layer_type = GrpcLayerType.SIGNAL_LAYER
118
+ if layer_type.lower() == "dielectric":
119
+ _layer_type = GrpcLayerType.DIELECTRIC_LAYER
120
+ layer = GrpcStackupLayer.create(
121
+ name=name, layer_type=_layer_type, thickness=thickness, material="copper", elevation=elevation
122
+ )
123
+ return self._layer_collection.add_layer_top(layer)
124
+
125
+ def add_layer_bottom(self, name, layer_type="signal", **kwargs):
126
+ """Add a layer on bottom of the stackup.
127
+
128
+ Parameters
129
+ ----------
130
+ name : str
131
+ Name of the layer.
132
+ layer_type: str, optional
133
+ Type of the layer. The default to ``"signal"``. Options are ``"signal"``, ``"dielectric"``
134
+ kwargs
135
+
136
+ Returns
137
+ -------
138
+
139
+ """
140
+ thickness = GrpcValue(0.0)
141
+ if "thickness" in kwargs:
142
+ thickness = GrpcValue(kwargs["thickness"])
143
+ elevation = GrpcValue(0.0)
144
+ _layer_type = GrpcLayerType.SIGNAL_LAYER
145
+ if layer_type.lower() == "dielectric":
146
+ _layer_type = GrpcLayerType.DIELECTRIC_LAYER
147
+ layer = GrpcStackupLayer.create(
148
+ name=name, layer_type=_layer_type, thickness=thickness, material="copper", elevation=elevation
149
+ )
150
+ return self._layer_collection.add_layer_bottom(layer)
151
+
152
+ def add_layer_below(self, name, base_layer_name, layer_type="signal", **kwargs):
153
+ """Add a layer below a layer.
154
+
155
+ Parameters
156
+ ----------
157
+ name : str
158
+ Name of the layer.
159
+ base_layer_name: str
160
+ Name of the base layer.
161
+ layer_type: str, optional
162
+ Type of the layer. The default to ``"signal"``. Options are ``"signal"``, ``"dielectric"``
163
+ kwargs
164
+
165
+ Returns
166
+ -------
167
+
168
+ """
169
+ thickness = GrpcValue(0.0)
170
+ if "thickness" in kwargs:
171
+ thickness = GrpcValue(kwargs["thickness"])
172
+ elevation = GrpcValue(0.0)
173
+ _layer_type = GrpcLayerType.SIGNAL_LAYER
174
+ if layer_type.lower() == "dielectric":
175
+ _layer_type = GrpcLayerType.DIELECTRIC_LAYER
176
+ layer = GrpcStackupLayer.create(
177
+ name=name, layer_type=_layer_type, thickness=thickness, material="copper", elevation=elevation
178
+ )
179
+ return self._layer_collection.add_layer_below(layer, base_layer_name)
180
+
181
+ def add_layer_above(self, name, base_layer_name, layer_type="signal", **kwargs):
182
+ """Add a layer above a layer.
183
+
184
+ Parameters
185
+ ----------
186
+ name : str
187
+ Name of the layer.
188
+ base_layer_name: str
189
+ Name of the base layer.
190
+ layer_type: str, optional
191
+ Type of the layer. The default to ``"signal"``. Options are ``"signal"``, ``"dielectric"``
192
+ kwargs
193
+
194
+ Returns
195
+ -------
196
+
197
+ """
198
+ thickness = GrpcValue(0.0)
199
+ if "thickness" in kwargs:
200
+ thickness = GrpcValue(kwargs["thickness"])
201
+ elevation = GrpcValue(0.0)
202
+ _layer_type = GrpcLayerType.SIGNAL_LAYER
203
+ if layer_type.lower() == "dielectric":
204
+ _layer_type = GrpcLayerType.DIELECTRIC_LAYER
205
+ layer = GrpcStackupLayer.create(
206
+ name=name, layer_type=_layer_type, thickness=thickness, material="copper", elevation=elevation
207
+ )
208
+ return self._layer_collection.add_layer_above(layer, base_layer_name)
209
+
210
+ def add_document_layer(self, name, layer_type="user", **kwargs):
211
+ """Add a document layer.
212
+
213
+ Parameters
214
+ ----------
215
+ name : str
216
+ Name of the layer.
217
+ layer_type: str, optional
218
+ Type of the layer. The default is ``"user"``. Options are ``"user"``, ``"outline"``
219
+ kwargs
220
+
221
+ Returns
222
+ -------
223
+
224
+ """
225
+ added_layer = self.add_layer_top(name)
226
+ added_layer.type = GrpcLayerType.USER_LAYER
227
+ return added_layer
228
+
229
+ @property
230
+ def stackup_layers(self):
231
+ """Retrieve the dictionary of signal and dielectric layers."""
232
+ warnings.warn("Use new property :func:`layers` instead.", DeprecationWarning)
233
+ return self.layers
234
+
235
+ @property
236
+ def non_stackup_layers(self):
237
+ """Retrieve the dictionary of signal layers."""
238
+ return {
239
+ layer.name: Layer(self._pedb, layer) for layer in self.get_layers(GrpcLayerTypeSet.NON_STACKUP_LAYER_SET)
240
+ }
241
+
242
+ @property
243
+ def all_layers(self):
244
+ return {layer.name: Layer(self._pedb, layer) for layer in self.get_layers(GrpcLayerTypeSet.ALL_LAYER_SET)}
245
+
246
+ @property
247
+ def signal_layers(self):
248
+ return {
249
+ layer.name: StackupLayer(self._pedb, layer) for layer in self.get_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)
250
+ }
251
+
252
+ @property
253
+ def dielectric_layers(self):
254
+ return {
255
+ layer.name: StackupLayer(self._pedb, layer)
256
+ for layer in self.get_layers(GrpcLayerTypeSet.DIELECTRIC_LAYER_SET)
257
+ }
258
+
259
+ @property
260
+ def layers_by_id(self):
261
+ """Retrieve the list of layers with their ids."""
262
+ return [[obj.id, name] for name, obj in self.all_layers.items()]
263
+
264
+ @property
265
+ def layers(self):
266
+ """Retrieve the dictionary of layers.
267
+
268
+ Returns
269
+ -------
270
+ Dict[str, :class:`pyedb.grpc.database.edb_data.layer_data.LayerEdbClass`]
271
+ """
272
+ return {obj.name: StackupLayer(self._pedb, obj) for obj in self.get_layers(GrpcLayerTypeSet.STACKUP_LAYER_SET)}
273
+
274
+ def find_layer_by_name(self, name: str):
275
+ """Finds a layer with the given name.
276
+
277
+ . deprecated:: pyedb 0.29.0
278
+ Use :func:`find_by_name` instead.
279
+
280
+ """
281
+ warnings.warn(
282
+ "`find_layer_by_name` is deprecated and is now located here "
283
+ "`pyedb.grpc.core.excitations.find_by_name` instead.",
284
+ DeprecationWarning,
285
+ )
286
+ layer = self.find_by_name(name)
287
+ if layer.is_null:
288
+ raise ValueError(f"Layer with name '{name}' was not found.")
289
+ return layer
290
+
291
+
292
+ class Stackup(LayerCollection):
293
+ """Manages EDB methods for stackup."""
294
+
295
+ def __init__(self, pedb, edb_object=None):
296
+ super().__init__(pedb, edb_object)
297
+ self._pedb = pedb
298
+
299
+ @property
300
+ def _logger(self):
301
+ return self._pedb.logger
302
+
303
+ @property
304
+ def thickness(self):
305
+ """Retrieve Stackup thickness.
306
+
307
+ Returns
308
+ -------
309
+ float
310
+ Layout stackup thickness.
311
+
312
+ """
313
+ return self.get_layout_thickness()
314
+
315
+ @property
316
+ def num_layers(self):
317
+ """Retrieve the stackup layer number.
318
+
319
+ Returns
320
+ -------
321
+ int
322
+ layer number.
323
+
324
+ """
325
+ return len(list(self.layers.keys()))
326
+
327
+ def create_symmetric_stackup(
328
+ self,
329
+ layer_count,
330
+ inner_layer_thickness="17um",
331
+ outer_layer_thickness="50um",
332
+ dielectric_thickness="100um",
333
+ dielectric_material="FR4_epoxy",
334
+ soldermask=True,
335
+ soldermask_thickness="20um",
336
+ ): # pragma: no cover
337
+ """Create a symmetric stackup.
338
+
339
+ Parameters
340
+ ----------
341
+ layer_count : int
342
+ Number of layer count.
343
+ inner_layer_thickness : str, float, optional
344
+ Thickness of inner conductor layer.
345
+ outer_layer_thickness : str, float, optional
346
+ Thickness of outer conductor layer.
347
+ dielectric_thickness : str, float, optional
348
+ Thickness of dielectric layer.
349
+ dielectric_material : str, optional
350
+ Material of dielectric layer.
351
+ soldermask : bool, optional
352
+ Whether to create soldermask layers. The default is``True``.
353
+ soldermask_thickness : str, optional
354
+ Thickness of soldermask layer.
355
+
356
+ Returns
357
+ -------
358
+ bool
359
+ """
360
+ if not np:
361
+ self._pedb.logger.error("Numpy is needed. Please, install it first.")
362
+ return False
363
+ if not layer_count % 2 == 0:
364
+ return False
365
+
366
+ self.add_layer(
367
+ "BOT",
368
+ None,
369
+ material="copper",
370
+ thickness=outer_layer_thickness,
371
+ fillMaterial=dielectric_material,
372
+ )
373
+ self.add_layer(
374
+ "D" + str(int(layer_count / 2)),
375
+ None,
376
+ material="FR4_epoxy",
377
+ thickness=dielectric_thickness,
378
+ layer_type="dielectric",
379
+ fillMaterial=dielectric_material,
380
+ )
381
+ self.add_layer(
382
+ "TOP",
383
+ None,
384
+ material="copper",
385
+ thickness=outer_layer_thickness,
386
+ fillMaterial=dielectric_material,
387
+ )
388
+ if soldermask:
389
+ self.add_layer(
390
+ "SMT",
391
+ None,
392
+ material="SolderMask",
393
+ thickness=soldermask_thickness,
394
+ layer_type="dielectric",
395
+ fillMaterial=dielectric_material,
396
+ )
397
+ self.add_layer(
398
+ "SMB",
399
+ None,
400
+ material="SolderMask",
401
+ thickness=soldermask_thickness,
402
+ layer_type="dielectric",
403
+ fillMaterial=dielectric_material,
404
+ method="add_on_bottom",
405
+ )
406
+ self.layers["TOP"].dielectric_fill = "SolderMask"
407
+ self.layers["BOT"].dielectric_fill = "SolderMask"
408
+
409
+ for layer_num in np.arange(int(layer_count / 2), 1, -1):
410
+ # Generate upper half
411
+ self.add_layer(
412
+ "L" + str(layer_num),
413
+ "TOP",
414
+ material="copper",
415
+ thickness=inner_layer_thickness,
416
+ fillMaterial=dielectric_material,
417
+ method="insert_below",
418
+ )
419
+ self.add_layer(
420
+ "D" + str(layer_num - 1),
421
+ "TOP",
422
+ material=dielectric_material,
423
+ thickness=dielectric_thickness,
424
+ layer_type="dielectric",
425
+ fillMaterial=dielectric_material,
426
+ method="insert_below",
427
+ )
428
+
429
+ # Generate lower half
430
+ self.add_layer(
431
+ "L" + str(layer_count - layer_num + 1),
432
+ "BOT",
433
+ material="copper",
434
+ thickness=inner_layer_thickness,
435
+ fillMaterial=dielectric_material,
436
+ method="insert_above",
437
+ )
438
+ self.add_layer(
439
+ "D" + str(layer_count - layer_num + 1),
440
+ "BOT",
441
+ material=dielectric_material,
442
+ thickness=dielectric_thickness,
443
+ layer_type="dielectric",
444
+ fillMaterial=dielectric_material,
445
+ method="insert_above",
446
+ )
447
+ return True
448
+
449
+ @property
450
+ def mode(self):
451
+ """Stackup mode.
452
+
453
+ Returns
454
+ -------
455
+ int, str
456
+ Type of the stackup mode, where:
457
+
458
+ * 0 - Laminate
459
+ * 1 - Overlapping
460
+ * 2 - MultiZone
461
+ """
462
+ return super().mode.name.lower()
463
+
464
+ @mode.setter
465
+ def mode(self, value):
466
+ if value == 0 or value == GrpcLayerCollectionMode.LAMINATE or value == "laminate":
467
+ super(LayerCollection, self.__class__).mode.__set__(self, GrpcLayerCollectionMode.LAMINATE)
468
+ elif value == 1 or value == GrpcLayerCollectionMode.OVERLAPPING or value == "overlapping":
469
+ super(LayerCollection, self.__class__).mode.__set__(self, GrpcLayerCollectionMode.OVERLAPPING)
470
+ elif value == 2 or value == GrpcLayerCollectionMode.MULTIZONE or value == "multizone":
471
+ super(LayerCollection, self.__class__).mode.__set__(self, GrpcLayerCollectionMode.MULTIZONE)
472
+ self.update_layout()
473
+
474
+ def _set_layout_stackup(self, layer_clone, operation, base_layer=None, method=1):
475
+ """Internal method. Apply stackup change into EDB.
476
+
477
+ Parameters
478
+ ----------
479
+ layer_clone : :class:`dotnet.database.EDB_Data.EDBLayer`
480
+ operation : str
481
+ Options are ``"change_attribute"``, ``"change_name"``,``"change_position"``, ``"insert_below"``,
482
+ ``"insert_above"``, ``"add_on_top"``, ``"add_on_bottom"``, ``"non_stackup"``, ``"add_at_elevation"``.
483
+ base_layer : str, optional
484
+ Name of the base layer. The default value is ``None``.
485
+
486
+ Returns
487
+ -------
488
+
489
+ """
490
+ lc = self._pedb.layout.layer_collection
491
+ if operation in ["change_position", "change_attribute", "change_name"]:
492
+ _lc = GrpcLayerCollection.create()
493
+
494
+ layers = [i for i in lc.get_layers(GrpcLayerTypeSet.STACKUP_LAYER_SET)]
495
+ non_stackup = [i for i in lc.get_layers(GrpcLayerTypeSet.NON_STACKUP_LAYER_SET)]
496
+ _lc.mode = lc.mode
497
+ if lc.mode.name.lower() == "overlapping":
498
+ for layer in layers:
499
+ if layer.name == layer_clone.name or layer.name == base_layer:
500
+ _lc.add_stackup_layer_at_elevation(layer_clone)
501
+ else:
502
+ _lc.add_stackup_layer_at_elevation(layer)
503
+ else:
504
+ for layer in layers:
505
+ if layer.name == layer_clone.name or layer.name == base_layer:
506
+ _lc.add_layer_bottom(layer_clone)
507
+ else:
508
+ _lc.add_layer_bottom(layer)
509
+ for layer in non_stackup:
510
+ _lc.add_layer_bottom(layer)
511
+ elif operation == "insert_below":
512
+ lc.add_layer_below(layer_clone, base_layer)
513
+ elif operation == "insert_above":
514
+ lc.add_layer_above(layer_clone, base_layer)
515
+ elif operation == "add_on_top":
516
+ lc.add_layer_top(layer_clone)
517
+ elif operation == "add_on_bottom":
518
+ lc.add_layer_bottom(layer_clone)
519
+ elif operation == "add_at_elevation":
520
+ lc.add_stackup_layer_at_elevation(layer_clone)
521
+ elif operation == "non_stackup":
522
+ lc.add_layer_bottom(layer_clone)
523
+ self._pedb.layout.layer_collection = lc
524
+ return True
525
+
526
+ def _create_stackup_layer(self, layer_name, thickness, layer_type="signal", material="copper"):
527
+ if layer_type == "signal":
528
+ _layer_type = GrpcLayerType.SIGNAL_LAYER
529
+ else:
530
+ _layer_type = GrpcLayerType.DIELECTRIC_LAYER
531
+ material = "FR4_epoxy"
532
+ thickness = GrpcValue(thickness, self._pedb.active_db)
533
+ layer = StackupLayer.create(
534
+ name=layer_name,
535
+ layer_type=_layer_type,
536
+ thickness=thickness,
537
+ elevation=GrpcValue(0),
538
+ material=material,
539
+ )
540
+ return layer
541
+
542
+ def _create_nonstackup_layer(self, layer_name, layer_type):
543
+ if layer_type == "conducting": # pragma: no cover
544
+ _layer_type = GrpcLayerType.CONDUCTING_LAYER
545
+ elif layer_type == "airlines": # pragma: no cover
546
+ _layer_type = GrpcLayerType.AIRLINES_LAYER
547
+ elif layer_type == "error": # pragma: no cover
548
+ _layer_type = GrpcLayerType.ERRORS_LAYER
549
+ elif layer_type == "symbol": # pragma: no cover
550
+ _layer_type = GrpcLayerType.SYMBOL_LAYER
551
+ elif layer_type == "measure": # pragma: no cover
552
+ _layer_type = GrpcLayerType.MEASURE_LAYER
553
+ elif layer_type == "assembly": # pragma: no cover
554
+ _layer_type = GrpcLayerType.ASSEMBLY_LAYER
555
+ elif layer_type == "silkscreen": # pragma: no cover
556
+ _layer_type = GrpcLayerType.SILKSCREEN_LAYER
557
+ elif layer_type == "soldermask": # pragma: no cover
558
+ _layer_type = GrpcLayerType.SOLDER_MASK_LAYER
559
+ elif layer_type == "solderpaste": # pragma: no cover
560
+ _layer_type = GrpcLayerType.SOLDER_PASTE_LAYER
561
+ elif layer_type == "glue": # pragma: no cover
562
+ _layer_type = GrpcLayerType.GLUE_LAYER
563
+ elif layer_type == "wirebond": # pragma: no cover
564
+ _layer_type = GrpcLayerType.WIREBOND_LAYER
565
+ elif layer_type == "user": # pragma: no cover
566
+ _layer_type = GrpcLayerType.USER_LAYER
567
+ elif layer_type == "siwavehfsssolverregions": # pragma: no cover
568
+ _layer_type = GrpcLayerType.SIWAVE_HFSS_SOLVER_REGIONS
569
+ elif layer_type == "outline": # pragma: no cover
570
+ _layer_type = GrpcLayerType.OUTLINE_LAYER
571
+ elif layer_type == "postprocessing": # pragma: no cover
572
+ _layer_type = GrpcLayerType.POST_PROCESSING_LAYER
573
+ else: # pragma: no cover
574
+ _layer_type = GrpcLayerType.UNDEFINED_LAYER_TYPE
575
+
576
+ result = Layer.create(layer_name, _layer_type)
577
+ return result
578
+
579
+ def add_outline_layer(self, outline_name="Outline"):
580
+ """Add an outline layer named ``"Outline"`` if it is not present.
581
+
582
+ Returns
583
+ -------
584
+ bool
585
+ "True" if successful, ``False`` if failed.
586
+ """
587
+ return self.add_document_layer(name="Outline", layer_type="outline")
588
+
589
+ # TODO: Update optional argument material into material_name and fillMaterial into fill_material_name
590
+
591
+ def add_layer(
592
+ self,
593
+ layer_name,
594
+ base_layer=None,
595
+ method="add_on_top",
596
+ layer_type="signal",
597
+ material="copper",
598
+ fillMaterial="FR4_epoxy",
599
+ thickness="35um",
600
+ etch_factor=None,
601
+ is_negative=False,
602
+ enable_roughness=False,
603
+ elevation=None,
604
+ ):
605
+ """Insert a layer into stackup.
606
+
607
+ Parameters
608
+ ----------
609
+ layer_name : str
610
+ Name of the layer.
611
+ base_layer : str, optional
612
+ Name of the base layer.
613
+ method : str, optional
614
+ Where to insert the new layer. The default is ``"add_on_top"``. Options are ``"add_on_top"``,
615
+ ``"add_on_bottom"``, ``"insert_above"``, ``"insert_below"``, ``"add_at_elevation"``,.
616
+ layer_type : str, optional
617
+ Type of layer. The default is ``"signal"``. Options are ``"signal"``, ``"dielectric"``, ``"conducting"``,
618
+ ``"air_lines"``, ``"error"``, ``"symbol"``, ``"measure"``, ``"assembly"``, ``"silkscreen"``,
619
+ ``"solder_mask"``, ``"solder_paste"``, ``"glue"``, ``"wirebond"``, ``"hfss_region"``, ``"user"``.
620
+ material : str, optional
621
+ Material of the layer.
622
+ fillMaterial : str, optional
623
+ Fill material of the layer.
624
+ thickness : str, float, optional
625
+ Thickness of the layer.
626
+ etch_factor : int, float, optional
627
+ Etch factor of the layer.
628
+ is_negative : bool, optional
629
+ Whether the layer is negative.
630
+ enable_roughness : bool, optional
631
+ Whether roughness is enabled.
632
+ elevation : float, optional
633
+ Elevation of new layer. Only valid for Overlapping Stackup.
634
+
635
+ Returns
636
+ -------
637
+ :class:`pyedb.dotnet.database.edb_data.layer_data.LayerEdbClass`
638
+ """
639
+ if layer_name in self.layers:
640
+ logger.error("layer {} exists.".format(layer_name))
641
+ return False
642
+ if not material:
643
+ material = "copper" if layer_type == "signal" else "FR4_epoxy"
644
+ if not fillMaterial:
645
+ fillMaterial = "FR4_epoxy"
646
+
647
+ materials = self._pedb.materials
648
+ if material not in materials:
649
+ material_properties = self._pedb.materials.read_syslib_material(material)
650
+ if material_properties:
651
+ logger.info(f"Material {material} found in syslib. Adding it to aedb project.")
652
+ materials.add_material(material, **material_properties)
653
+ else:
654
+ logger.warning(f"Material {material} not found. Check the library and retry.")
655
+
656
+ if layer_type != "dielectric" and fillMaterial not in materials:
657
+ material_properties = self._pedb.materials.read_syslib_material(fillMaterial)
658
+ if material_properties:
659
+ logger.info(f"Material {fillMaterial} found in syslib. Adding it to aedb project.")
660
+ materials.add_material(fillMaterial, **material_properties)
661
+ else:
662
+ logger.warning(f"Material {fillMaterial} not found. Check the library and retry.")
663
+
664
+ if layer_type in ["signal", "dielectric"]:
665
+ new_layer = self._create_stackup_layer(layer_name, thickness, layer_type)
666
+ new_layer.set_material(material)
667
+ if layer_type != "dielectric":
668
+ new_layer.set_fill_material(fillMaterial)
669
+ new_layer.negative = is_negative
670
+ l1 = len(self.layers)
671
+ if method == "add_at_elevation" and elevation:
672
+ new_layer.lower_elevation = GrpcValue(elevation)
673
+ if etch_factor:
674
+ new_layer.etch_factor = etch_factor
675
+ if enable_roughness:
676
+ new_layer.roughness_enabled = True
677
+ self._set_layout_stackup(new_layer, method, base_layer)
678
+ if len(self.layers) == l1:
679
+ self._set_layout_stackup(new_layer, method, base_layer, method=2)
680
+ else:
681
+ new_layer = self._create_nonstackup_layer(layer_name, layer_type)
682
+ self._set_layout_stackup(new_layer, "non_stackup")
683
+ return self.layers[layer_name]
684
+
685
+ def remove_layer(self, name):
686
+ """Remove a layer from stackup.
687
+
688
+ Parameters
689
+ ----------
690
+ name : str
691
+ Name of the layer to remove.
692
+
693
+ Returns
694
+ -------
695
+
696
+ """
697
+ new_layer_collection = LayerCollection.create()
698
+ for lyr in self.layers:
699
+ if not (lyr.name == name):
700
+ new_layer_collection.add_layer_bottom(lyr)
701
+
702
+ self._pedb.layout.layer_collection = new_layer_collection
703
+ return True
704
+
705
+ def export(self, fpath, file_format="xml", include_material_with_layer=False):
706
+ """Export stackup definition to a CSV or JSON file.
707
+
708
+ Parameters
709
+ ----------
710
+ fpath : str
711
+ File path to csv or json file.
712
+ file_format : str, optional
713
+ Format of the file to export. The default is ``"csv"``. Options are ``"csv"``, ``"xlsx"``,
714
+ ``"json"``.
715
+ include_material_with_layer : bool, optional.
716
+ Whether to include the material definition inside layer ones. This parameter is only used
717
+ when a JSON file is exported. The default is ``False``, which keeps the material definition
718
+ section in the JSON file. If ``True``, the material definition is included inside the layer ones.
719
+
720
+ Examples
721
+ --------
722
+ >>> from pyedb import Edb
723
+ >>> edb = Edb()
724
+ >>> edb.stackup.export("stackup.xml")
725
+ """
726
+ if len(fpath.split(".")) == 1:
727
+ fpath = "{}.{}".format(fpath, file_format)
728
+
729
+ if fpath.endswith(".csv"):
730
+ return self._export_layer_stackup_to_csv_xlsx(fpath, file_format="csv")
731
+ elif fpath.endswith(".xlsx"):
732
+ return self._export_layer_stackup_to_csv_xlsx(fpath, file_format="xlsx")
733
+ elif fpath.endswith(".json"):
734
+ return self._export_layer_stackup_to_json(fpath, include_material_with_layer)
735
+ elif fpath.endswith(".xml"):
736
+ return self._export_xml(fpath)
737
+ else:
738
+ self._logger.warning("Layer stackup format is not supported. Skipping import.")
739
+ return False
740
+
741
+ def export_stackup(self, fpath, file_format="xml", include_material_with_layer=False):
742
+ """Export stackup definition to a CSV or JSON file.
743
+
744
+ .. deprecated:: 0.6.61
745
+ Use :func:`export` instead.
746
+
747
+ Parameters
748
+ ----------
749
+ fpath : str
750
+ File path to CSV or JSON file.
751
+ file_format : str, optional
752
+ Format of the file to export. The default is ``"csv"``. Options are ``"csv"``, ``"xlsx"``
753
+ and ``"json"``.
754
+ include_material_with_layer : bool, optional.
755
+ Whether to include the material definition inside layer objects. This parameter is only used
756
+ when a JSON file is exported. The default is ``False``, which keeps the material definition
757
+ section in the JSON file. If ``True``, the material definition is included inside the layer ones.
758
+
759
+ Examples
760
+ --------
761
+ >>> from pyedb import Edb
762
+ >>> edb = Edb()
763
+ >>> edb.stackup.export_stackup("stackup.xml")
764
+ """
765
+
766
+ self._logger.warning("Method export_stackup is deprecated. Use .export.")
767
+ return self.export(fpath, file_format=file_format, include_material_with_layer=include_material_with_layer)
768
+
769
+ def _export_layer_stackup_to_csv_xlsx(self, fpath=None, file_format=None):
770
+ if not pd:
771
+ self._pedb.logger.error("Pandas is needed. Please, install it first.")
772
+ return False
773
+
774
+ data = {
775
+ "Type": [],
776
+ "Material": [],
777
+ "Dielectric_Fill": [],
778
+ "Thickness": [],
779
+ }
780
+ idx = []
781
+ for lyr in self.layers.values():
782
+ idx.append(lyr.name)
783
+ data["Type"].append(lyr.type)
784
+ data["Material"].append(lyr.material)
785
+ data["Dielectric_Fill"].append(lyr.dielectric_fill)
786
+ data["Thickness"].append(lyr.thickness)
787
+ df = pd.DataFrame(data, index=idx, columns=["Type", "Material", "Dielectric_Fill", "Thickness"])
788
+ if file_format == "csv": # pragma: no cover
789
+ if not fpath.endswith(".csv"):
790
+ fpath = fpath + ".csv"
791
+ df.to_csv(fpath)
792
+ else: # pragma: no cover
793
+ if not fpath.endswith(".xlsx"): # pragma: no cover
794
+ fpath = fpath + ".xlsx"
795
+ df.to_excel(fpath)
796
+ return True
797
+
798
+ def _export_layer_stackup_to_json(self, output_file=None, include_material_with_layer=False):
799
+ if not include_material_with_layer:
800
+ material_out = {}
801
+ for material_name, material in self._pedb.materials.materials.items():
802
+ material_out[material_name] = material.to_dict()
803
+ layers_out = {}
804
+ for k, v in self.layers.items():
805
+ data = v._json_format()
806
+ # FIXME: Update the API to avoid providing following information to our users
807
+ del data["pedb"]
808
+ del data["edb_object"]
809
+ layers_out[k] = data
810
+ if v.material in self._pedb.materials.materials:
811
+ layer_material = self._pedb.materials.materials[v.material]
812
+ if not v.dielectric_fill:
813
+ dielectric_fill = False
814
+ else:
815
+ dielectric_fill = self._pedb.materials.materials[v.dielectric_fill]
816
+ if include_material_with_layer:
817
+ layers_out[k]["material"] = layer_material.to_dict()
818
+ if dielectric_fill:
819
+ layers_out[k]["dielectric_fill"] = dielectric_fill.to_dict()
820
+ if not include_material_with_layer:
821
+ stackup_out = {"materials": material_out, "layers": layers_out}
822
+ else:
823
+ stackup_out = {"layers": layers_out}
824
+ if output_file:
825
+ with open(output_file, "w") as write_file:
826
+ json.dump(stackup_out, write_file, indent=4)
827
+
828
+ return True
829
+ else:
830
+ return False
831
+
832
+ # TODO: This method might need some refactoring
833
+
834
+ def _import_layer_stackup(self, input_file=None):
835
+ if input_file:
836
+ f = open(input_file)
837
+ json_dict = json.load(f) # pragma: no cover
838
+ for k, v in json_dict.items():
839
+ if k == "materials":
840
+ for material in v.values():
841
+ material_name = material["name"]
842
+ del material["name"]
843
+ if material_name not in self._pedb.materials:
844
+ self._pedb.materials.add_material(material_name, **material)
845
+ else:
846
+ self._pedb.materials.update_material(material_name, material)
847
+ if k == "layers":
848
+ if len(list(v.values())) == len(list(self.layers.values())):
849
+ imported_layers_list = [l_dict["name"] for l_dict in list(v.values())]
850
+ layout_layer_list = list(self.layers.keys())
851
+ for layer_name in imported_layers_list:
852
+ layer_index = imported_layers_list.index(layer_name)
853
+ if layout_layer_list[layer_index] != layer_name:
854
+ self.layers[layout_layer_list[layer_index]].name = layer_name
855
+ prev_layer = None
856
+ for layer_name, layer in v.items():
857
+ if layer["name"] not in self.layers:
858
+ if not prev_layer:
859
+ self.add_layer(
860
+ layer_name,
861
+ method="add_on_top",
862
+ layer_type=layer["type"],
863
+ material=layer["material"],
864
+ fillMaterial=layer["dielectric_fill"],
865
+ thickness=layer["thickness"],
866
+ )
867
+ prev_layer = layer_name
868
+ else:
869
+ self.add_layer(
870
+ layer_name,
871
+ base_layer=layer_name,
872
+ method="insert_below",
873
+ layer_type=layer["type"],
874
+ material=layer["material"],
875
+ fillMaterial=layer["dielectric_fill"],
876
+ thickness=layer["thickness"],
877
+ )
878
+ prev_layer = layer_name
879
+ if layer_name in self.layers:
880
+ self.layers[layer["name"]]._load_layer(layer)
881
+ return True
882
+
883
+ def limits(self, only_metals=False):
884
+ """Retrieve stackup limits.
885
+
886
+ Parameters
887
+ ----------
888
+ only_metals : bool, optional
889
+ Whether to retrieve only metals. The default is ``False``.
890
+
891
+ Returns
892
+ -------
893
+ bool
894
+ ``True`` when successful, ``False`` when failed.
895
+ """
896
+ if only_metals:
897
+ input_layers = GrpcLayerTypeSet.SIGNAL_LAYER_SET
898
+ else:
899
+ input_layers = GrpcLayerTypeSet.STACKUP_LAYER_SET
900
+
901
+ res = self.get_top_bottom_stackup_layers(input_layers)
902
+ upper_layer = res[0]
903
+ upper_layer_top_elevationm = res[1]
904
+ lower_layer = res[2]
905
+ lower_layer_lower_elevation = res[3]
906
+ return upper_layer.name, upper_layer_top_elevationm, lower_layer.name, lower_layer_lower_elevation
907
+
908
+ def flip_design(self):
909
+ """Flip the current design of a layout.
910
+
911
+ Returns
912
+ -------
913
+ bool
914
+ ``True`` when succeed ``False`` if not.
915
+
916
+ Examples
917
+ --------
918
+ >>> edb = Edb(edbpath=targetfile, edbversion="2021.2")
919
+ >>> edb.stackup.flip_design()
920
+ >>> edb.save()
921
+ >>> edb.close_edb()
922
+ """
923
+ try:
924
+ lc = self._layer_collection
925
+ new_lc = LayerCollection.create()
926
+ new_lc.mode = lc.mode
927
+ max_elevation = 0.0
928
+ for layer in lc.get_layers(GrpcLayerTypeSet.STACKUP_LAYER_SET):
929
+ if "RadBox" not in layer.name: # Ignore RadBox
930
+ lower_elevation = layer.clone().lower_elevation.value * 1.0e6
931
+ upper_elevation = layer.Clone().upper_elevation.value * 1.0e6
932
+ max_elevation = max([max_elevation, lower_elevation, upper_elevation])
933
+
934
+ non_stackup_layers = []
935
+ for layer in lc.get_Layers():
936
+ cloned_layer = layer.clone()
937
+ if not cloned_layer.is_stackup_layer:
938
+ non_stackup_layers.append(cloned_layer)
939
+ continue
940
+ if "RadBox" not in cloned_layer.name and not cloned_layer.is_via_layer:
941
+ upper_elevation = cloned_layer.upper_elevation.value * 1.0e6
942
+ updated_lower_el = max_elevation - upper_elevation
943
+ val = GrpcValue(f"{updated_lower_el}um")
944
+ cloned_layer.lower_elevation = val
945
+ if cloned_layer.top_bottom_association == GrpcTopBottomAssociation.TOP_ASSOCIATED:
946
+ cloned_layer.top_bottom_association = GrpcTopBottomAssociation.BOTTOM_ASSOCIATED
947
+ else:
948
+ cloned_layer.top_bottom_association = GrpcTopBottomAssociation.TOP_BOTTOM_ASSOCIATION_COUNT
949
+ new_lc.add_stackup_layer_at_elevation(cloned_layer)
950
+
951
+ vialayers = [lay for lay in lc.get_layers(GrpcLayerTypeSet.STACKUP_LAYER_SET) if lay.clone().is_via_layer]
952
+ for layer in vialayers:
953
+ cloned_via_layer = layer.clone()
954
+ upper_ref_name = cloned_via_layer.get_ref_layer_name(True)
955
+ lower_ref_name = cloned_via_layer.get_ref_layer_name(False)
956
+ upper_ref = [lay for lay in lc.Layers(GrpcLayerTypeSet.ALL_LAYER_SET) if lay.name == upper_ref_name][0]
957
+ lower_ref = [lay for lay in lc.Layers(GrpcLayerTypeSet.ALL_LAYER_SET) if lay.name == lower_ref_name][0]
958
+ cloned_via_layer.set_ref_layer(lower_ref, True)
959
+ cloned_via_layer.set_ref_layer(upper_ref, False)
960
+ ref_layer_in_flipped_stackup = [
961
+ lay for lay in new_lc.get_layers(GrpcLayerTypeSet.ALL_LAYER_SET) if lay.name == upper_ref_name
962
+ ][0]
963
+ via_layer_lower_elevation = (
964
+ ref_layer_in_flipped_stackup.lower_elevation + ref_layer_in_flipped_stackup.thickness
965
+ )
966
+ cloned_via_layer.lower_elevation = via_layer_lower_elevation
967
+ new_lc.add_stackup_layer_at_elevation(cloned_via_layer)
968
+ new_lc.add_layers(non_stackup_layers)
969
+ self._pedb.layout.layer_collection = new_lc
970
+
971
+ for pyaedt_cmp in list(self._pedb.components.instances.values()):
972
+ cmp = pyaedt_cmp
973
+ cmp_type = cmp.type
974
+ cmp_prop = cmp.component_property
975
+ try:
976
+ if cmp_prop.solder_ball_property.placement == GrpcSolderballPlacement.ABOVE_PADSTACK:
977
+ sball_prop = cmp_prop.solder_ball_property
978
+ sball_prop.placement = GrpcSolderballPlacement.BELOW_PADSTACK
979
+ cmp_prop.solder_ball_property = sball_prop
980
+ elif cmp_prop.solder_ball_property.placement == GrpcSolderballPlacement.BELOW_PADSTACK:
981
+ sball_prop = cmp_prop.solder_ball_property
982
+ sball_prop.placement = GrpcSolderballPlacement.ABOVE_PADSTACK
983
+ cmp_prop.solder_ball_property = sball_prop
984
+ except:
985
+ pass
986
+ if cmp_type == GrpcComponentType.IC:
987
+ die_prop = cmp_prop.die_property
988
+ chip_orientation = die_prop.die_orientation
989
+ if chip_orientation == GrpcDieOrientation.CHIP_DOWN:
990
+ die_prop.die_orientation = GrpcDieOrientation.CHIP_UP
991
+ cmp_prop.die_property = die_prop
992
+ else:
993
+ die_prop.die_orientation = GrpcDieOrientation.CHIP_DOWN
994
+ cmp_prop.die_property = die_prop
995
+ cmp.component_property = cmp_prop
996
+
997
+ lay_list = new_lc.get_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)
998
+ for padstack in list(self._pedb.padstacks.instances.values()):
999
+ start_layer_id = [lay.id for lay in lay_list if lay.name == padstack.start_layer]
1000
+ stop_layer_id = [lay.id for lay in lay_list if lay.name == padstack.stop_layer]
1001
+ layer_map = padstack.get_layer_map()
1002
+ layer_map.set_mapping(stop_layer_id[0], start_layer_id[0])
1003
+ padstack.set_layer_map(layer_map)
1004
+ return True
1005
+ except:
1006
+ return False
1007
+
1008
+ def get_layout_thickness(self):
1009
+ """Return the layout thickness.
1010
+
1011
+ Returns
1012
+ -------
1013
+ float
1014
+ The thickness value.
1015
+ """
1016
+ layers = list(self.layers.values())
1017
+ layers.sort(key=lambda lay: lay.lower_elevation)
1018
+ thickness = 0
1019
+ if layers:
1020
+ top_layer = layers[-1]
1021
+ bottom_layer = layers[0]
1022
+ thickness = abs(top_layer.upper_elevation - bottom_layer.lower_elevation)
1023
+ return round(thickness, 7)
1024
+
1025
+ def _get_solder_height(self, layer_name):
1026
+ for _, val in self._pedb.components.instances.items():
1027
+ if val.solder_ball_height and val.placement_layer == layer_name:
1028
+ return val.solder_ball_height
1029
+ return 0
1030
+
1031
+ def _remove_solder_pec(self, layer_name):
1032
+ for _, val in self._pedb.components.instances.items():
1033
+ if val.solder_ball_height and val.placement_layer == layer_name:
1034
+ comp_prop = val.component_property
1035
+ port_property = comp_prop.port_property
1036
+ port_property.reference_size_auto = False
1037
+ port_property.reference_size = (GrpcValue(0.0), GrpcValue(0.0))
1038
+ comp_prop.port_property = port_property
1039
+ val.edbcomponent.component_property = comp_prop
1040
+
1041
+ def adjust_solder_dielectrics(self):
1042
+ """Adjust the stack-up by adding or modifying dielectric layers that contains Solder Balls.
1043
+ This method identifies the solder-ball height and adjust the dielectric thickness on top (or bottom) to fit
1044
+ the thickness in order to merge another layout.
1045
+
1046
+ Returns
1047
+ -------
1048
+ bool
1049
+ """
1050
+ for el, val in self._pedb.components.instances.items():
1051
+ if val.solder_ball_height:
1052
+ layer = val.placement_layer
1053
+ if layer == list(self.layers.keys())[0]:
1054
+ self.add_layer(
1055
+ "Bottom_air",
1056
+ base_layer=list(self.layers.keys())[-1],
1057
+ method="insert_below",
1058
+ material="air",
1059
+ thickness=val.solder_ball_height,
1060
+ layer_type="dielectric",
1061
+ )
1062
+ elif layer == list(self.layers.keys())[-1]:
1063
+ self.add_layer(
1064
+ "Top_Air",
1065
+ base_layer=layer,
1066
+ material="air",
1067
+ thickness=val.solder_ball_height,
1068
+ layer_type="dielectric",
1069
+ )
1070
+ elif layer == list(self.signal_layers.keys())[-1]:
1071
+ list(self.layers.values())[-1].thickness = val.solder_ball_height
1072
+
1073
+ elif layer == list(self.signal_layers.keys())[0]:
1074
+ list(self.layers.values())[0].thickness = val.solder_ball_height
1075
+ return True
1076
+
1077
+ def place_in_layout(
1078
+ self,
1079
+ edb,
1080
+ angle=0.0,
1081
+ offset_x=0.0,
1082
+ offset_y=0.0,
1083
+ flipped_stackup=True,
1084
+ place_on_top=True,
1085
+ ):
1086
+ """Place current Cell into another cell using layer placement method.
1087
+ Flip the current layer stackup of a layout if requested. Transform parameters currently not supported.
1088
+
1089
+ Parameters
1090
+ ----------
1091
+ edb : Edb
1092
+ Cell on which to place the current layout. If None the Cell will be applied on an empty new Cell.
1093
+ angle : double, optional
1094
+ The rotation angle applied on the design.
1095
+ offset_x : double, optional
1096
+ The x offset value.
1097
+ offset_y : double, optional
1098
+ The y offset value.
1099
+ flipped_stackup : bool, optional
1100
+ Either if the current layout is inverted.
1101
+ If `True` and place_on_top is `True` the stackup will be flipped before the merge.
1102
+ place_on_top : bool, optional
1103
+ Either if place the current layout on Top or Bottom of destination Layout.
1104
+
1105
+ Returns
1106
+ -------
1107
+ bool
1108
+ ``True`` when succeed ``False`` if not.
1109
+
1110
+ Examples
1111
+ --------
1112
+ >>> edb1 = Edb(edbpath=targetfile1, edbversion="2021.2")
1113
+ >>> edb2 = Edb(edbpath=targetfile2, edbversion="2021.2")
1114
+
1115
+ >>> hosting_cmp = edb1.components.get_component_by_name("U100")
1116
+ >>> mounted_cmp = edb2.components.get_component_by_name("BGA")
1117
+
1118
+ >>> vector, rotation, solder_ball_height = edb1.components.get_component_placement_vector(
1119
+ ... mounted_component=mounted_cmp,
1120
+ ... hosting_component=hosting_cmp,
1121
+ ... mounted_component_pin1="A12",
1122
+ ... mounted_component_pin2="A14",
1123
+ ... hosting_component_pin1="A12",
1124
+ ... hosting_component_pin2="A14")
1125
+ >>> edb2.stackup.place_in_layout(edb1.active_cell, angle=0.0, offset_x=vector[0],
1126
+ ... offset_y=vector[1], flipped_stackup=False, place_on_top=True,
1127
+ ... )
1128
+ """
1129
+ # if flipped_stackup and place_on_top or (not flipped_stackup and not place_on_top):
1130
+ self.adjust_solder_dielectrics()
1131
+ if not place_on_top:
1132
+ edb.stackup.flip_design()
1133
+ place_on_top = True
1134
+ if not flipped_stackup:
1135
+ self.flip_design()
1136
+ elif flipped_stackup:
1137
+ self.flip_design()
1138
+ edb_cell = edb.active_cell
1139
+ _angle = GrpcValue(angle * math.pi / 180.0)
1140
+ _offset_x = GrpcValue(offset_x)
1141
+ _offset_y = GrpcValue(offset_y)
1142
+
1143
+ if edb_cell.name not in self._pedb.cell_names:
1144
+ list_cells = self._pedb.copy_cells([edb_cell.api_object])
1145
+ edb_cell = list_cells[0]
1146
+ self._pedb.layout.cell.is_blackbox = True
1147
+ cell_inst2 = GrpcCellInstance.create(
1148
+ layout=edb_cell.layout, name=self._pedb.layout.cell.name, ref=self._pedb.active_layout
1149
+ )
1150
+ cell_trans = cell_inst2.transform
1151
+ cell_trans.rotation = _angle
1152
+ cell_trans.offset_x = _offset_x
1153
+ cell_trans.offset_y = _offset_y
1154
+ cell_trans.mirror = flipped_stackup
1155
+ cell_inst2.transform = cell_trans
1156
+ cell_inst2.solve_independent_preference = False
1157
+ stackup_target = edb_cell.layout.layer_collection
1158
+
1159
+ if place_on_top:
1160
+ cell_inst2.placement_layer = stackup_target.get_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)[0]
1161
+ else:
1162
+ cell_inst2.placement_layer = stackup_target.get_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)[-1]
1163
+ return True
1164
+
1165
+ def place_in_layout_3d_placement(
1166
+ self,
1167
+ edb,
1168
+ angle=0.0,
1169
+ offset_x=0.0,
1170
+ offset_y=0.0,
1171
+ flipped_stackup=True,
1172
+ place_on_top=True,
1173
+ solder_height=0,
1174
+ ):
1175
+ """Place current Cell into another cell using 3d placement method.
1176
+ Flip the current layer stackup of a layout if requested. Transform parameters currently not supported.
1177
+
1178
+ Parameters
1179
+ ----------
1180
+ edb : Edb
1181
+ Cell on which to place the current layout. If None the Cell will be applied on an empty new Cell.
1182
+ angle : double, optional
1183
+ The rotation angle applied on the design.
1184
+ offset_x : double, optional
1185
+ The x offset value.
1186
+ offset_y : double, optional
1187
+ The y offset value.
1188
+ flipped_stackup : bool, optional
1189
+ Either if the current layout is inverted.
1190
+ If `True` and place_on_top is `True` the stackup will be flipped before the merge.
1191
+ place_on_top : bool, optional
1192
+ Either if place the current layout on Top or Bottom of destination Layout.
1193
+ solder_height : float, optional
1194
+ Solder Ball or Bumps eight.
1195
+ This value will be added to the elevation to align the two layouts.
1196
+
1197
+ Returns
1198
+ -------
1199
+ bool
1200
+ ``True`` when succeed ``False`` if not.
1201
+
1202
+ Examples
1203
+ --------
1204
+ >>> edb1 = Edb(edbpath=targetfile1, edbversion="2021.2")
1205
+ >>> edb2 = Edb(edbpath=targetfile2, edbversion="2021.2")
1206
+ >>> hosting_cmp = edb1.components.get_component_by_name("U100")
1207
+ >>> mounted_cmp = edb2.components.get_component_by_name("BGA")
1208
+ >>> edb2.stackup.place_in_layout(edb1.active_cell, angle=0.0, offset_x="1mm",
1209
+ ... offset_y="2mm", flipped_stackup=False, place_on_top=True,
1210
+ ... )
1211
+ """
1212
+ _angle = angle * math.pi / 180.0
1213
+
1214
+ if solder_height <= 0:
1215
+ if flipped_stackup and not place_on_top or (place_on_top and not flipped_stackup):
1216
+ minimum_elevation = None
1217
+ layers_from_the_bottom = sorted(self.signal_layers.values(), key=lambda lay: lay.upper_elevation)
1218
+ for lay in layers_from_the_bottom:
1219
+ if minimum_elevation is None:
1220
+ minimum_elevation = lay.lower_elevation
1221
+ elif lay.lower_elevation > minimum_elevation:
1222
+ break
1223
+ lay_solder_height = self._get_solder_height(lay.name)
1224
+ solder_height = max(lay_solder_height, solder_height)
1225
+ self._remove_solder_pec(lay.name)
1226
+ else:
1227
+ maximum_elevation = None
1228
+ layers_from_the_top = sorted(self.signal_layers.values(), key=lambda lay: -lay.upper_elevation)
1229
+ for lay in layers_from_the_top:
1230
+ if maximum_elevation is None:
1231
+ maximum_elevation = lay.upper_elevation
1232
+ elif lay.upper_elevation < maximum_elevation:
1233
+ break
1234
+ lay_solder_height = self._get_solder_height(lay.name)
1235
+ solder_height = max(lay_solder_height, solder_height)
1236
+ self._remove_solder_pec(lay.name)
1237
+
1238
+ rotation = GrpcValue(0.0)
1239
+ if flipped_stackup:
1240
+ rotation = GrpcValue(math.pi)
1241
+
1242
+ edb_cell = edb.active_cell
1243
+ _offset_x = GrpcValue(offset_x)
1244
+ _offset_y = GrpcValue(offset_y)
1245
+
1246
+ if edb_cell.name not in self._pedb.cell_names:
1247
+ list_cells = self._pedb.copy_cells(edb_cell.api_object)
1248
+ edb_cell = list_cells[0]
1249
+ self._pedb.layout.cell.is_blackbox = True
1250
+ cell_inst2 = GrpcCellInstance.create(
1251
+ layout=edb_cell.layout, name=self._pedb.layout.cell.name, ref=self._pedb.active_layout
1252
+ )
1253
+
1254
+ stackup_target = edb_cell.layout.layer_collection
1255
+ stackup_source = self._pedb.layout.layer_collection
1256
+
1257
+ if place_on_top:
1258
+ cell_inst2.placement_layer = stackup_target.Layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)[0]
1259
+ else:
1260
+ cell_inst2.placement_layer = stackup_target.Layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)[-1]
1261
+ cell_inst2.placement_3d = True
1262
+ res = stackup_target.get_top_bottom_stackup_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)
1263
+ target_top_elevation = res[1]
1264
+ target_bottom_elevation = res[3]
1265
+ res_s = stackup_source.get_top_bottom_stackup_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)
1266
+ source_stack_top_elevation = res_s[1]
1267
+ source_stack_bot_elevation = res_s[3]
1268
+
1269
+ if place_on_top and flipped_stackup:
1270
+ elevation = target_top_elevation + source_stack_top_elevation
1271
+ elif place_on_top:
1272
+ elevation = target_top_elevation - source_stack_bot_elevation
1273
+ elif flipped_stackup:
1274
+ elevation = target_bottom_elevation + source_stack_bot_elevation
1275
+ solder_height = -solder_height
1276
+ else:
1277
+ elevation = target_bottom_elevation - source_stack_top_elevation
1278
+ solder_height = -solder_height
1279
+
1280
+ h_stackup = GrpcValue(elevation + solder_height)
1281
+
1282
+ zero_data = GrpcValue(0.0)
1283
+ one_data = GrpcValue(1.0)
1284
+ point3d_t = GrpcPoint3DData(_offset_x, _offset_y, h_stackup)
1285
+ point_loc = GrpcPoint3DData(zero_data, zero_data, zero_data)
1286
+ point_from = GrpcPoint3DData(one_data, zero_data, zero_data)
1287
+ point_to = GrpcPoint3DData(math.cos(_angle), -1 * math.sin(_angle), zero_data)
1288
+ cell_inst2.transform3d = GrpcTransform3D(point_loc, point_from, point_to, rotation, point3d_t) # TODO check
1289
+ return True
1290
+
1291
+ def place_instance(
1292
+ self,
1293
+ component_edb,
1294
+ angle=0.0,
1295
+ offset_x=0.0,
1296
+ offset_y=0.0,
1297
+ offset_z=0.0,
1298
+ flipped_stackup=True,
1299
+ place_on_top=True,
1300
+ solder_height=0,
1301
+ ):
1302
+ """Place current Cell into another cell using 3d placement method.
1303
+ Flip the current layer stackup of a layout if requested. Transform parameters currently not supported.
1304
+
1305
+ Parameters
1306
+ ----------
1307
+ component_edb : Edb
1308
+ Cell to place in the current layout.
1309
+ angle : double, optional
1310
+ The rotation angle applied on the design.
1311
+ offset_x : double, optional
1312
+ The x offset value.
1313
+ The default value is ``0.0``.
1314
+ offset_y : double, optional
1315
+ The y offset value.
1316
+ The default value is ``0.0``.
1317
+ offset_z : double, optional
1318
+ The z offset value. (i.e. elevation offset for placement relative to the top layer conductor).
1319
+ The default value is ``0.0``, which places the cell layout on top of the top conductor
1320
+ layer of the target EDB.
1321
+ flipped_stackup : bool, optional
1322
+ Either if the current layout is inverted.
1323
+ If `True` and place_on_top is `True` the stackup will be flipped before the merge.
1324
+ place_on_top : bool, optional
1325
+ Either if place the component_edb layout on Top or Bottom of destination Layout.
1326
+ solder_height : float, optional
1327
+ Solder Ball or Bumps eight.
1328
+ This value will be added to the elevation to align the two layouts.
1329
+
1330
+ Returns
1331
+ -------
1332
+ bool
1333
+ ``True`` when succeed ``False`` if not.
1334
+
1335
+ Examples
1336
+ --------
1337
+ >>> edb1 = Edb(edbpath=targetfile1, edbversion="2021.2")
1338
+ >>> edb2 = Edb(edbpath=targetfile2, edbversion="2021.2")
1339
+ >>> hosting_cmp = edb1.components.get_component_by_name("U100")
1340
+ >>> mounted_cmp = edb2.components.get_component_by_name("BGA")
1341
+ >>> edb1.stackup.place_instance(edb2, angle=0.0, offset_x="1mm",
1342
+ ... offset_y="2mm", flipped_stackup=False, place_on_top=True,
1343
+ ... )
1344
+ """
1345
+ _angle = angle * math.pi / 180.0
1346
+
1347
+ if solder_height <= 0:
1348
+ if flipped_stackup and not place_on_top or (place_on_top and not flipped_stackup):
1349
+ minimum_elevation = None
1350
+ layers_from_the_bottom = sorted(
1351
+ component_edb.stackup.signal_layers.values(), key=lambda lay: lay.upper_elevation
1352
+ )
1353
+ for lay in layers_from_the_bottom:
1354
+ if minimum_elevation is None:
1355
+ minimum_elevation = lay.lower_elevation
1356
+ elif lay.lower_elevation > minimum_elevation:
1357
+ break
1358
+ lay_solder_height = component_edb.stackup._get_solder_height(lay.name)
1359
+ solder_height = max(lay_solder_height, solder_height)
1360
+ component_edb.stackup._remove_solder_pec(lay.name)
1361
+ else:
1362
+ maximum_elevation = None
1363
+ layers_from_the_top = sorted(
1364
+ component_edb.stackup.signal_layers.values(), key=lambda lay: -lay.upper_elevation
1365
+ )
1366
+ for lay in layers_from_the_top:
1367
+ if maximum_elevation is None:
1368
+ maximum_elevation = lay.upper_elevation
1369
+ elif lay.upper_elevation < maximum_elevation:
1370
+ break
1371
+ lay_solder_height = component_edb.stackup._get_solder_height(lay.name)
1372
+ solder_height = max(lay_solder_height, solder_height)
1373
+ component_edb.stackup._remove_solder_pec(lay.name)
1374
+ edb_cell = component_edb.active_cell
1375
+ _offset_x = GrpcValue(offset_x)
1376
+ _offset_y = GrpcValue(offset_y)
1377
+
1378
+ if edb_cell.name not in self._pedb.cell_names:
1379
+ list_cells = self._pedb.copy_cells(edb_cell.api_object)
1380
+ edb_cell = list_cells[0]
1381
+ for cell in self._pedb.active_db.top_circuit_cells:
1382
+ if cell.name == edb_cell.name:
1383
+ edb_cell = cell
1384
+ # Keep Cell Independent
1385
+ edb_cell.is_black_box = True
1386
+ rotation = GrpcValue(0.0)
1387
+ if flipped_stackup:
1388
+ rotation = GrpcValue(math.pi)
1389
+
1390
+ _offset_x = GrpcValue(offset_x)
1391
+ _offset_y = GrpcValue(offset_y)
1392
+
1393
+ instance_name = generate_unique_name(edb_cell.name, n=2)
1394
+
1395
+ cell_inst2 = GrpcCellInstance.create(layout=self._pedb.active_layout, name=instance_name, ref=edb_cell.layout)
1396
+
1397
+ stackup_source = edb_cell.layout.layer_collection
1398
+ stackup_target = self._pedb.layout.layer_collection
1399
+
1400
+ if place_on_top:
1401
+ cell_inst2.placement_layer = stackup_target.get_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)[0]
1402
+ else:
1403
+ cell_inst2.placement_layer = stackup_target.get_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)[-1]
1404
+ cell_inst2.placement_3d = True
1405
+ res = stackup_target.get_top_bottom_stackup_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)
1406
+ target_top_elevation = res[1]
1407
+ target_bottom_elevation = res[3]
1408
+ res_s = stackup_source.get_top_bottom_stackup_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)
1409
+ source_stack_top_elevation = res_s[1]
1410
+ source_stack_bot_elevation = res_s[3]
1411
+
1412
+ if place_on_top and flipped_stackup:
1413
+ elevation = target_top_elevation + source_stack_top_elevation + offset_z
1414
+ elif place_on_top:
1415
+ elevation = target_top_elevation - source_stack_bot_elevation + offset_z
1416
+ elif flipped_stackup:
1417
+ elevation = target_bottom_elevation + source_stack_bot_elevation - offset_z
1418
+ solder_height = -solder_height
1419
+ else:
1420
+ elevation = target_bottom_elevation - source_stack_top_elevation - offset_z
1421
+ solder_height = -solder_height
1422
+
1423
+ h_stackup = elevation + solder_height
1424
+
1425
+ zero_data = GrpcValue(0.0)
1426
+ one_data = GrpcValue(1.0)
1427
+ point3d_t = GrpcPoint3DData(_offset_x, _offset_y, h_stackup)
1428
+ point_loc = GrpcPoint3DData(zero_data, zero_data, zero_data)
1429
+ point_from = GrpcPoint3DData(one_data, zero_data, zero_data)
1430
+ point_to = GrpcPoint3DData(math.cos(_angle), -1 * math.sin(_angle), zero_data)
1431
+ cell_inst2.transform3d = (point_loc, point_from, point_to, rotation, point3d_t) # TODO check
1432
+ return cell_inst2
1433
+
1434
+ def place_a3dcomp_3d_placement(
1435
+ self,
1436
+ a3dcomp_path,
1437
+ angle=0.0,
1438
+ offset_x=0.0,
1439
+ offset_y=0.0,
1440
+ offset_z=0.0,
1441
+ place_on_top=True,
1442
+ ):
1443
+ """Place a 3D Component into current layout.
1444
+ 3D Component ports are not visible via EDB. They will be visible after the EDB has been opened in Ansys
1445
+ Electronics Desktop as a project.
1446
+
1447
+ Parameters
1448
+ ----------
1449
+ a3dcomp_path : str
1450
+ Path to the 3D Component file (\\*.a3dcomp) to place.
1451
+ angle : double, optional
1452
+ Clockwise rotation angle applied to the a3dcomp.
1453
+ offset_x : double, optional
1454
+ The x offset value.
1455
+ The default value is ``0.0``.
1456
+ offset_y : double, optional
1457
+ The y offset value.
1458
+ The default value is ``0.0``.
1459
+ offset_z : double, optional
1460
+ The z offset value. (i.e. elevation)
1461
+ The default value is ``0.0``.
1462
+ place_on_top : bool, optional
1463
+ Whether to place the 3D Component on the top or the bottom of this layout.
1464
+ If ``False`` then the 3D Component will also be flipped over around its X axis.
1465
+
1466
+ Returns
1467
+ -------
1468
+ bool
1469
+ ``True`` if successful and ``False`` if not.
1470
+
1471
+ Examples
1472
+ --------
1473
+ >>> edb1 = Edb(edbpath=targetfile1, edbversion="2021.2")
1474
+ >>> a3dcomp_path = "connector.a3dcomp"
1475
+ >>> edb1.stackup.place_a3dcomp_3d_placement(a3dcomp_path, angle=0.0, offset_x="1mm",
1476
+ ... offset_y="2mm", flipped_stackup=False, place_on_top=True,
1477
+ ... )
1478
+ """
1479
+ zero_data = GrpcValue(0.0)
1480
+ one_data = GrpcValue(1.0)
1481
+ local_origin = GrpcPoint3DData(0.0, 0.0, 0.0)
1482
+ rotation_axis_from = GrpcPoint3DData(1.0, 0.0, 0.0)
1483
+ _angle = angle * math.pi / 180.0
1484
+ rotation_axis_to = GrpcPoint3DData(math.cos(_angle), -1 * math.sin(_angle), 0.0)
1485
+
1486
+ stackup_target = GrpcLayerCollection(self._pedb.layout.layer_collection)
1487
+ res = stackup_target.get_top_bottom_stackup_layers(GrpcLayerTypeSet.SIGNAL_LAYER_SET)
1488
+ target_top_elevation = res[1]
1489
+ target_bottom_elevation = res[3]
1490
+ flip_angle = GrpcValue("0deg")
1491
+ if place_on_top:
1492
+ elevation = target_top_elevation + offset_z
1493
+ else:
1494
+ flip_angle = GrpcValue("180deg")
1495
+ elevation = target_bottom_elevation - offset_z
1496
+ h_stackup = GrpcValue(elevation)
1497
+ location = GrpcPoint3DData(offset_x, offset_y, h_stackup)
1498
+ mcad_model = GrpcMcadModel.create_3d_comp(layout=self._pedb.active_layout, filename=a3dcomp_path)
1499
+ if mcad_model.is_null: # pragma: no cover
1500
+ logger.error("Failed to create MCAD model from a3dcomp")
1501
+ return False
1502
+
1503
+ if mcad_model.cell_instance.is_null: # pragma: no cover
1504
+ logger.error("Cell instance of a3dcomp is null")
1505
+ return False
1506
+
1507
+ mcad_model.cell_instance.placement_3d = True
1508
+ mcad_model.cell_instance.transform3d = GrpcTransform3D(
1509
+ local_origin, rotation_axis_from, rotation_axis_to, flip_angle, location
1510
+ )
1511
+ return True
1512
+
1513
+ def residual_copper_area_per_layer(self):
1514
+ """Report residual copper area per layer in percentage.
1515
+
1516
+ Returns
1517
+ -------
1518
+ dict
1519
+ Copper area per layer.
1520
+
1521
+ Examples
1522
+ --------
1523
+ >>> edb = Edb(edbpath=targetfile1, edbversion="2021.2")
1524
+ >>> edb.stackup.residual_copper_area_per_layer()
1525
+ """
1526
+ temp_data = {name: 0 for name, _ in self.signal_layers.items()}
1527
+ outline_area = 0
1528
+ for i in self._pedb.modeler.primitives:
1529
+ layer_name = i.layer.name
1530
+ if layer_name.lower() == "outline":
1531
+ if i.area() > outline_area:
1532
+ outline_area = i.area()
1533
+ elif layer_name not in temp_data:
1534
+ continue
1535
+ elif not i.is_void:
1536
+ temp_data[layer_name] = temp_data[layer_name] + i.area()
1537
+ else:
1538
+ pass
1539
+ temp_data = {name: area / outline_area * 100 for name, area in temp_data.items()}
1540
+ return temp_data
1541
+
1542
+ # TODO: This method might need some refactoring
1543
+
1544
+ def _import_dict(self, json_dict, rename=False):
1545
+ """Import stackup from a dictionary."""
1546
+ if not "materials" in json_dict:
1547
+ self._logger.info("Configuration file does not have material definition. Using aedb and syslib materials.")
1548
+ else:
1549
+ mats = json_dict["materials"]
1550
+ for name, material in mats.items():
1551
+ try:
1552
+ material_name = material["name"]
1553
+ del material["name"]
1554
+ except KeyError:
1555
+ material_name = name
1556
+ if material_name not in self._pedb.materials:
1557
+ self._pedb.materials.add_material(material_name, **material)
1558
+ else:
1559
+ self._pedb.materials.update_material(material_name, material)
1560
+ temp = json_dict
1561
+ if "layers" in json_dict:
1562
+ temp = {i: j for i, j in json_dict["layers"].items() if j["type"] in ["signal", "dielectric"]}
1563
+ config_file_layers = list(temp.keys())
1564
+ layout_layers = list(self.layers.keys())
1565
+ renamed_layers = {}
1566
+ if rename and len(config_file_layers) == len(layout_layers):
1567
+ for lay_ind in range(len(list(temp.keys()))):
1568
+ if not config_file_layers[lay_ind] == layout_layers[lay_ind]:
1569
+ renamed_layers[layout_layers[lay_ind]] = config_file_layers[lay_ind]
1570
+ layers_names = list(self.layers.keys())[::]
1571
+ for name in layers_names:
1572
+ layer = None
1573
+ if name in temp:
1574
+ layer = temp[name]
1575
+ elif name in renamed_layers:
1576
+ layer = temp[renamed_layers[name]]
1577
+ self.layers[name].name = renamed_layers[name]
1578
+ name = renamed_layers[name]
1579
+ else: # Remove layers not in config file.
1580
+ self.remove_layer(name)
1581
+ self._logger.warning(f"Layer {name} were not found in configuration file, removing layer")
1582
+ default_layer = {
1583
+ "name": "default",
1584
+ "type": "signal",
1585
+ "material": "copper",
1586
+ "dielectric_fill": "FR4_epoxy",
1587
+ "thickness": 3.5e-05,
1588
+ "etch_factor": 0.0,
1589
+ "roughness_enabled": False,
1590
+ "top_hallhuray_nodule_radius": 0.0,
1591
+ "top_hallhuray_surface_ratio": 0.0,
1592
+ "bottom_hallhuray_nodule_radius": 0.0,
1593
+ "bottom_hallhuray_surface_ratio": 0.0,
1594
+ "side_hallhuray_nodule_radius": 0.0,
1595
+ "side_hallhuray_surface_ratio": 0.0,
1596
+ "upper_elevation": 0.0,
1597
+ "lower_elevation": 0.0,
1598
+ "color": [242, 140, 102],
1599
+ }
1600
+ if layer:
1601
+ if "color" in layer:
1602
+ default_layer["color"] = layer["color"]
1603
+ elif not layer["type"] == "signal":
1604
+ default_layer["color"] = [27, 110, 76]
1605
+
1606
+ for k, v in layer.items():
1607
+ default_layer[k] = v
1608
+ self.layers[name]._load_layer(default_layer)
1609
+ for layer_name, layer in temp.items(): # looping over potential new layers to add
1610
+ if layer_name in self.layers:
1611
+ continue # if layer exist, skip
1612
+ # adding layer
1613
+ default_layer = {
1614
+ "name": "default",
1615
+ "type": "signal",
1616
+ "material": "copper",
1617
+ "dielectric_fill": "FR4_epoxy",
1618
+ "thickness": 3.5e-05,
1619
+ "etch_factor": 0.0,
1620
+ "roughness_enabled": False,
1621
+ "top_hallhuray_nodule_radius": 0.0,
1622
+ "top_hallhuray_surface_ratio": 0.0,
1623
+ "bottom_hallhuray_nodule_radius": 0.0,
1624
+ "bottom_hallhuray_surface_ratio": 0.0,
1625
+ "side_hallhuray_nodule_radius": 0.0,
1626
+ "side_hallhuray_surface_ratio": 0.0,
1627
+ "upper_elevation": 0.0,
1628
+ "lower_elevation": 0.0,
1629
+ "color": [242, 140, 102],
1630
+ }
1631
+
1632
+ if "color" in layer:
1633
+ default_layer["color"] = layer["color"]
1634
+ elif not layer["type"] == "signal":
1635
+ default_layer["color"] = [27, 110, 76]
1636
+
1637
+ for k, v in layer.items():
1638
+ default_layer[k] = v
1639
+
1640
+ temp_2 = list(temp.keys())
1641
+ if temp_2.index(layer_name) == 0:
1642
+ new_layer = self.add_layer(
1643
+ layer_name,
1644
+ method="add_on_top",
1645
+ layer_type=default_layer["type"],
1646
+ material=default_layer["material"],
1647
+ fillMaterial=default_layer["dielectric_fill"],
1648
+ thickness=default_layer["thickness"],
1649
+ )
1650
+
1651
+ elif temp_2.index(layer_name) == len(temp_2):
1652
+ new_layer = self.add_layer(
1653
+ layer_name,
1654
+ base_layer=layer_name,
1655
+ method="add_on_bottom",
1656
+ layer_type=default_layer["type"],
1657
+ material=default_layer["material"],
1658
+ fillMaterial=default_layer["dielectric_fill"],
1659
+ thickness=default_layer["thickness"],
1660
+ )
1661
+ else:
1662
+ new_layer = self.add_layer(
1663
+ layer_name,
1664
+ base_layer=temp_2[temp_2.index(layer_name) - 1],
1665
+ method="insert_below",
1666
+ layer_type=default_layer["type"],
1667
+ material=default_layer["material"],
1668
+ fillMaterial=default_layer["dielectric_fill"],
1669
+ thickness=default_layer["thickness"],
1670
+ )
1671
+
1672
+ new_layer.color = default_layer["color"]
1673
+ new_layer.etch_factor = default_layer["etch_factor"]
1674
+
1675
+ new_layer.roughness_enabled = default_layer["roughness_enabled"]
1676
+ new_layer.top_hallhuray_nodule_radius = default_layer["top_hallhuray_nodule_radius"]
1677
+ new_layer.top_hallhuray_surface_ratio = default_layer["top_hallhuray_surface_ratio"]
1678
+ new_layer.bottom_hallhuray_nodule_radius = default_layer["bottom_hallhuray_nodule_radius"]
1679
+ new_layer.bottom_hallhuray_surface_ratio = default_layer["bottom_hallhuray_surface_ratio"]
1680
+ new_layer.side_hallhuray_nodule_radius = default_layer["side_hallhuray_nodule_radius"]
1681
+ new_layer.side_hallhuray_surface_ratio = default_layer["side_hallhuray_surface_ratio"]
1682
+
1683
+ return True
1684
+
1685
+ def _import_json(self, file_path, rename=False):
1686
+ """Import stackup from a json file."""
1687
+ if file_path:
1688
+ f = open(file_path)
1689
+ json_dict = json.load(f) # pragma: no cover
1690
+ return self._import_dict(json_dict, rename)
1691
+
1692
+ def _import_csv(self, file_path):
1693
+ """Import stackup definition from a CSV file.
1694
+
1695
+ Parameters
1696
+ ----------
1697
+ file_path : str
1698
+ File path to the CSV file.
1699
+ """
1700
+ if not pd:
1701
+ self._pedb.logger.error("Pandas is needed. You must install it first.")
1702
+ return False
1703
+
1704
+ df = pd.read_csv(file_path, index_col=0)
1705
+
1706
+ for name in self.layers.keys(): # pragma: no cover
1707
+ if not name in df.index:
1708
+ logger.error(f"{name} doesn't exist in csv")
1709
+ return False
1710
+
1711
+ for name, layer_info in df.iterrows():
1712
+ layer_type = layer_info.Type
1713
+ if name in self.layers:
1714
+ layer = self.layers[name]
1715
+ layer.type = layer_type
1716
+ else:
1717
+ layer = self.add_layer(name, layer_type=layer_type, material="copper", fillMaterial="copper")
1718
+
1719
+ layer.material = layer_info.Material
1720
+ layer.thickness = layer_info.Thickness
1721
+ if not str(layer_info.Dielectric_Fill) == "nan":
1722
+ layer.dielectric_fill = layer_info.Dielectric_Fill
1723
+
1724
+ lc_new = GrpcLayerCollection.create()
1725
+ for name, _ in df.iterrows():
1726
+ layer = self.layers[name]
1727
+ lc_new.add_layer_bottom(layer)
1728
+
1729
+ for name, layer in self.non_stackup_layers.items():
1730
+ lc_new.add_layer_bottom(layer)
1731
+
1732
+ self._pedb.layout.layer_collection = lc_new
1733
+ return True
1734
+
1735
+ def _set(self, layers=None, materials=None, roughness=None, non_stackup_layers=None):
1736
+ """Update stackup information.
1737
+
1738
+ Parameters
1739
+ ----------
1740
+ layers: dict
1741
+ Dictionary containing layer information.
1742
+ materials: dict
1743
+ Dictionary containing material information.
1744
+ roughness: dict
1745
+ Dictionary containing roughness information.
1746
+
1747
+ Returns
1748
+ -------
1749
+
1750
+ """
1751
+ if materials:
1752
+ self._add_materials_from_dictionary(materials)
1753
+
1754
+ if layers:
1755
+ prev_layer = None
1756
+ for name, val in layers.items():
1757
+ etching_factor = float(val["EtchFactor"]) if "EtchFactor" in val else None
1758
+
1759
+ if not self.layers:
1760
+ self.add_layer(
1761
+ name,
1762
+ None,
1763
+ "add_on_top",
1764
+ val["Type"],
1765
+ val["Material"],
1766
+ val["FillMaterial"] if val["Type"] == "signal" else "",
1767
+ val["Thickness"],
1768
+ etching_factor,
1769
+ )
1770
+ else:
1771
+ if name in self.layers.keys():
1772
+ lyr = self.layers[name]
1773
+ lyr.type = val["Type"]
1774
+ lyr.material = val["Material"]
1775
+ lyr.dielectric_fill = val["FillMaterial"] if val["Type"] == "signal" else ""
1776
+ lyr.thickness = val["Thickness"]
1777
+ if prev_layer:
1778
+ self._set_layout_stackup(lyr._edb_layer, "change_position", prev_layer)
1779
+ else:
1780
+ if prev_layer and prev_layer in self.layers:
1781
+ layer_name = prev_layer
1782
+ else:
1783
+ layer_name = list(self.layers.keys())[-1] if self.layers else None
1784
+ self.add_layer(
1785
+ name,
1786
+ layer_name,
1787
+ "insert_above",
1788
+ val["Type"],
1789
+ val["Material"],
1790
+ val["FillMaterial"] if val["Type"] == "signal" else "",
1791
+ val["Thickness"],
1792
+ etching_factor,
1793
+ )
1794
+ prev_layer = name
1795
+ for name in self.layers:
1796
+ if name not in layers:
1797
+ self.remove_layer(name)
1798
+
1799
+ if roughness:
1800
+ for name, attr in roughness.items():
1801
+ layer = self.signal_layers[name]
1802
+ layer.roughness_enabled = True
1803
+
1804
+ attr_name = "HuraySurfaceRoughness"
1805
+ if attr_name in attr:
1806
+ on_surface = "top"
1807
+ layer.assign_roughness_model(
1808
+ "huray",
1809
+ attr[attr_name]["NoduleRadius"],
1810
+ attr[attr_name]["HallHuraySurfaceRatio"],
1811
+ apply_on_surface=on_surface,
1812
+ )
1813
+
1814
+ attr_name = "HurayBottomSurfaceRoughness"
1815
+ if attr_name in attr:
1816
+ on_surface = "bottom"
1817
+ layer.assign_roughness_model(
1818
+ "huray",
1819
+ attr[attr_name]["NoduleRadius"],
1820
+ attr[attr_name]["HallHuraySurfaceRatio"],
1821
+ apply_on_surface=on_surface,
1822
+ )
1823
+ attr_name = "HuraySideSurfaceRoughness"
1824
+ if attr_name in attr:
1825
+ on_surface = "side"
1826
+ layer.assign_roughness_model(
1827
+ "huray",
1828
+ attr[attr_name]["NoduleRadius"],
1829
+ attr[attr_name]["HallHuraySurfaceRatio"],
1830
+ apply_on_surface=on_surface,
1831
+ )
1832
+
1833
+ attr_name = "GroissSurfaceRoughness"
1834
+ if attr_name in attr:
1835
+ on_surface = "top"
1836
+ layer.assign_roughness_model(
1837
+ "groisse", groisse_roughness=attr[attr_name]["Roughness"], apply_on_surface=on_surface
1838
+ )
1839
+
1840
+ attr_name = "GroissBottomSurfaceRoughness"
1841
+ if attr_name in attr:
1842
+ on_surface = "bottom"
1843
+ layer.assign_roughness_model(
1844
+ "groisse", groisse_roughness=attr[attr_name]["Roughness"], apply_on_surface=on_surface
1845
+ )
1846
+
1847
+ attr_name = "GroissSideSurfaceRoughness"
1848
+ if attr_name in attr:
1849
+ on_surface = "side"
1850
+ layer.assign_roughness_model(
1851
+ "groisse", groisse_roughness=attr[attr_name]["Roughness"], apply_on_surface=on_surface
1852
+ )
1853
+
1854
+ if non_stackup_layers:
1855
+ for name, val in non_stackup_layers.items():
1856
+ if name in self.non_stackup_layers:
1857
+ continue
1858
+ else:
1859
+ self.add_layer(name, layer_type=val["Type"])
1860
+
1861
+ return True
1862
+
1863
+ def _get(self):
1864
+ """Get stackup information from layout.
1865
+
1866
+ Returns:
1867
+ tuple: (dict, dict, dict)
1868
+ layers, materials, roughness_models
1869
+ """
1870
+ layers = OrderedDict()
1871
+ roughness_models = OrderedDict()
1872
+ for name, val in self.layers.items():
1873
+ layer = dict()
1874
+ layer["Material"] = val.material
1875
+ layer["Name"] = val.name
1876
+ layer["Thickness"] = val.thickness
1877
+ layer["Type"] = val.type
1878
+ if not val.type == "dielectric":
1879
+ layer["FillMaterial"] = val.dielectric_fill
1880
+ layer["EtchFactor"] = val.etch_factor
1881
+ layers[name] = layer
1882
+
1883
+ if val.roughness_enabled:
1884
+ roughness_models[name] = {}
1885
+ model = val.get_roughness_model("top")
1886
+ if model.type.name.endswith("GroissRoughnessModel"):
1887
+ roughness_models[name]["GroissSurfaceRoughness"] = {"Roughness": model.get_Roughness.value}
1888
+ else:
1889
+ roughness_models[name]["HuraySurfaceRoughness"] = {
1890
+ "HallHuraySurfaceRatio": model.get_nodule_radius().value,
1891
+ "NoduleRadius": model.get_surface_ratio().value,
1892
+ }
1893
+ model = val.get_roughness_model("bottom")
1894
+ if model.type.name.endswith("GroissRoughnessModel"):
1895
+ roughness_models[name]["GroissBottomSurfaceRoughness"] = {"Roughness": model.get_roughness().value}
1896
+ else:
1897
+ roughness_models[name]["HurayBottomSurfaceRoughness"] = {
1898
+ "HallHuraySurfaceRatio": model.get_nodule_radius().value,
1899
+ "NoduleRadius": model.get_surface_ratio().value,
1900
+ }
1901
+ model = val.get_roughness_model("side")
1902
+ if model.ToString().endswith("GroissRoughnessModel"):
1903
+ roughness_models[name]["GroissSideSurfaceRoughness"] = {"Roughness": model.get_roughness().value}
1904
+ else:
1905
+ roughness_models[name]["HuraySideSurfaceRoughness"] = {
1906
+ "HallHuraySurfaceRatio": model.get_nodule_radius().value,
1907
+ "NoduleRadius": model.get_surface_ratio().value,
1908
+ }
1909
+
1910
+ non_stackup_layers = OrderedDict()
1911
+ for name, val in self.non_stackup_layers.items():
1912
+ layer = dict()
1913
+ layer["Name"] = val.name
1914
+ layer["Type"] = val.type
1915
+ non_stackup_layers[name] = layer
1916
+
1917
+ materials = {}
1918
+ for name, val in self._pedb.materials.materials.items():
1919
+ material = {}
1920
+ if val.conductivity:
1921
+ if val.conductivity > 4e7:
1922
+ material["Conductivity"] = val.conductivity
1923
+ else:
1924
+ material["Permittivity"] = val.permittivity
1925
+ material["DielectricLossTangent"] = val.dielectric_loss_tangent
1926
+ materials[name] = material
1927
+
1928
+ return layers, materials, roughness_models, non_stackup_layers
1929
+
1930
+ def _add_materials_from_dictionary(self, material_dict):
1931
+ materials = self.self._pedb.materials.materials
1932
+ for name, material_properties in material_dict.items():
1933
+ if not name in materials:
1934
+ if "Conductivity" in material_properties:
1935
+ materials.add_conductor_material(name, material_properties["Conductivity"])
1936
+ else:
1937
+ materials.add_dielectric_material(
1938
+ name,
1939
+ material_properties["Permittivity"],
1940
+ material_properties["DielectricLossTangent"],
1941
+ )
1942
+ else:
1943
+ material = materials[name]
1944
+ if "Conductivity" in material_properties:
1945
+ material.conductivity = material_properties["Conductivity"]
1946
+ else:
1947
+ material.permittivity = material_properties["Permittivity"]
1948
+ material.loss_tanget = material_properties["DielectricLossTangent"]
1949
+ return True
1950
+
1951
+ def _import_xml(self, file_path, rename=False):
1952
+ """Read external xml file and convert into json file.
1953
+ You can use xml file to import layer stackup but using json file is recommended.
1954
+ see :class:`pyedb.dotnet.database.edb_data.simulation_configuration.SimulationConfiguration´ class to
1955
+ generate files`.
1956
+
1957
+ Parameters
1958
+ ----------
1959
+ file_path: str
1960
+ Path to external XML file.
1961
+
1962
+ Returns
1963
+ -------
1964
+ bool
1965
+ ``True`` when successful, ``False`` when failed.
1966
+ """
1967
+ if not colors:
1968
+ self._pedb.logger.error("Matplotlib is needed. Please, install it first.")
1969
+ return False
1970
+ tree = ET.parse(file_path)
1971
+ root = tree.getroot()
1972
+ stackup = root.find("Stackup")
1973
+ stackup_dict = {}
1974
+ if stackup.find("Materials"):
1975
+ mats = []
1976
+ for m in stackup.find("Materials").findall("Material"):
1977
+ temp = dict()
1978
+ for i in list(m):
1979
+ value = list(i)[0].text
1980
+ temp[i.tag] = value
1981
+ mat = {"name": m.attrib["Name"]}
1982
+ temp_dict = {
1983
+ "Permittivity": "permittivity",
1984
+ "Conductivity": "conductivity",
1985
+ "DielectricLossTangent": "dielectric_loss_tangent",
1986
+ }
1987
+ for i in temp_dict.keys():
1988
+ value = temp.get(i, None)
1989
+ if value:
1990
+ mat[temp_dict[i]] = value
1991
+ mats.append(mat)
1992
+ stackup_dict["materials"] = mats
1993
+
1994
+ stackup_section = stackup.find("Layers")
1995
+ if stackup_section:
1996
+ length_unit = stackup_section.attrib["LengthUnit"]
1997
+ layers = []
1998
+ for l in stackup.find("Layers").findall("Layer"):
1999
+ temp = l.attrib
2000
+ layer = dict()
2001
+ temp_dict = {
2002
+ "Name": "name",
2003
+ "Color": "color",
2004
+ "Material": "material",
2005
+ "Thickness": "thickness",
2006
+ "Type": "type",
2007
+ "FillMaterial": "fill_material",
2008
+ }
2009
+ for i in temp_dict.keys():
2010
+ value = temp.get(i, None)
2011
+ if value:
2012
+ if i == "Thickness":
2013
+ value = str(round(float(value), 6)) + length_unit
2014
+ value = "signal" if value == "conductor" else value
2015
+ if i == "Color":
2016
+ value = [int(x * 255) for x in list(colors.to_rgb(value))]
2017
+ layer[temp_dict[i]] = value
2018
+ layers.append(layer)
2019
+ stackup_dict["layers"] = layers
2020
+ cfg = {"stackup": stackup_dict}
2021
+ return self._pedb.configuration.load(cfg, apply_file=True)
2022
+
2023
+ def _export_xml(self, file_path):
2024
+ """Export stackup information to an external XMLfile.
2025
+
2026
+ Parameters
2027
+ ----------
2028
+ file_path: str
2029
+ Path to external XML file.
2030
+
2031
+ Returns
2032
+ -------
2033
+ bool
2034
+ ``True`` when successful, ``False`` when failed.
2035
+ """
2036
+ layers, materials, roughness, non_stackup_layers = self._get()
2037
+
2038
+ root = ET.Element("{http://www.ansys.com/control}Control", attrib={"schemaVersion": "1.0"})
2039
+
2040
+ el_stackup = ET.SubElement(root, "Stackup", {"schemaVersion": "1.0"})
2041
+
2042
+ el_materials = ET.SubElement(el_stackup, "Materials")
2043
+ for mat, val in materials.items():
2044
+ material = ET.SubElement(el_materials, "Material")
2045
+ material.set("Name", mat)
2046
+ for pname, pval in val.items():
2047
+ mat_prop = ET.SubElement(material, pname)
2048
+ value = ET.SubElement(mat_prop, "Double")
2049
+ value.text = str(pval)
2050
+
2051
+ el_layers = ET.SubElement(el_stackup, "Layers", {"LengthUnit": "meter"})
2052
+ for lyr, val in layers.items():
2053
+ layer = ET.SubElement(el_layers, "Layer")
2054
+ val = {i: str(j) for i, j in val.items()}
2055
+ if val["Type"] == "signal":
2056
+ val["Type"] = "conductor"
2057
+ layer.attrib.update(val)
2058
+
2059
+ for lyr, val in non_stackup_layers.items():
2060
+ layer = ET.SubElement(el_layers, "Layer")
2061
+ val = {i: str(j) for i, j in val.items()}
2062
+ layer.attrib.update(val)
2063
+
2064
+ for lyr, val in roughness.items():
2065
+ el = el_layers.find("./Layer[@Name='{}']".format(lyr))
2066
+ for pname, pval in val.items():
2067
+ pval = {i: str(j) for i, j in pval.items()}
2068
+ ET.SubElement(el, pname, pval)
2069
+
2070
+ write_pretty_xml(root, file_path)
2071
+ return True
2072
+
2073
+ def load(self, file_path, rename=False):
2074
+ """Import stackup from a file. The file format can be XML, CSV, or JSON. Valid control file must
2075
+ have the same number of signal layers. Signals layers can be renamed. Dielectric layers can be
2076
+ added and deleted.
2077
+
2078
+
2079
+ Parameters
2080
+ ----------
2081
+ file_path : str, dict
2082
+ Path to stackup file or dict with stackup details.
2083
+ rename : bool
2084
+ If rename is ``False`` then layer in layout not found in the stackup file are deleted.
2085
+ Otherwise, if the number of layer in the stackup file equals the number of stackup layer
2086
+ in the layout, layers are renamed according the file.
2087
+ Note that layer order matters, and has to be writtent from top to bottom layer in the file.
2088
+
2089
+ Returns
2090
+ -------
2091
+ bool
2092
+ ``True`` when successful, ``False`` when failed.
2093
+
2094
+ Examples
2095
+ --------
2096
+ >>> from pyedb import Edb
2097
+ >>> edb = Edb()
2098
+ >>> edb.stackup.load("stackup.xml")
2099
+ """
2100
+
2101
+ if isinstance(file_path, dict):
2102
+ return self._import_dict(file_path)
2103
+ elif file_path.endswith(".csv"):
2104
+ return self._import_csv(file_path)
2105
+ elif file_path.endswith(".json"):
2106
+ return self._import_json(file_path, rename=rename)
2107
+ elif file_path.endswith(".xml"):
2108
+ return self._import_xml(file_path, rename=rename)
2109
+ else:
2110
+ return False
2111
+
2112
+ def plot(
2113
+ self,
2114
+ save_plot=None,
2115
+ size=(2000, 1500),
2116
+ plot_definitions=None,
2117
+ first_layer=None,
2118
+ last_layer=None,
2119
+ scale_elevation=True,
2120
+ show=True,
2121
+ ):
2122
+ """Plot current stackup and, optionally, overlap padstack definitions.
2123
+ Plot supports only 'Laminate' and 'Overlapping' stackup types.
2124
+
2125
+ Parameters
2126
+ ----------
2127
+ save_plot : str, optional
2128
+ If a path is specified the plot will be saved in this location.
2129
+ If ``save_plot`` is provided, the ``show`` parameter is ignored.
2130
+ size : tuple, optional
2131
+ Image size in pixel (width, height). Default value is ``(2000, 1500)``
2132
+ plot_definitions : str, list, optional
2133
+ List of padstack definitions to plot on the stackup.
2134
+ It is supported only for Laminate mode.
2135
+ first_layer : str or :class:`pyedb.dotnet.database.edb_data.layer_data.LayerEdbClass`
2136
+ First layer to plot from the bottom. Default is `None` to start plotting from bottom.
2137
+ last_layer : str or :class:`pyedb.dotnet.database.edb_data.layer_data.LayerEdbClass`
2138
+ Last layer to plot from the bottom. Default is `None` to plot up to top layer.
2139
+ scale_elevation : bool, optional
2140
+ The real layer thickness is scaled so that max_thickness = 3 * min_thickness.
2141
+ Default is `True`.
2142
+ show : bool, optional
2143
+ Whether to show the plot or not. Default is `True`.
2144
+
2145
+ Returns
2146
+ -------
2147
+ :class:`matplotlib.plt`
2148
+ """
2149
+
2150
+ from pyedb.generic.constants import CSS4_COLORS
2151
+ from pyedb.generic.plot import plot_matplotlib
2152
+
2153
+ layer_names = list(self.layers.keys())
2154
+ if first_layer is None or first_layer not in layer_names:
2155
+ bottom_layer = layer_names[-1]
2156
+ elif isinstance(first_layer, str):
2157
+ bottom_layer = first_layer
2158
+ elif isinstance(first_layer, Layer):
2159
+ bottom_layer = first_layer.name
2160
+ else:
2161
+ raise AttributeError("first_layer must be str or class `dotnet.database.edb_data.layer_data.LayerEdbClass`")
2162
+ if last_layer is None or last_layer not in layer_names:
2163
+ top_layer = layer_names[0]
2164
+ elif isinstance(last_layer, str):
2165
+ top_layer = last_layer
2166
+ elif isinstance(last_layer, Layer):
2167
+ top_layer = last_layer.name
2168
+ else:
2169
+ raise AttributeError("last_layer must be str or class `dotnet.database.edb_data.layer_data.LayerEdbClass`")
2170
+
2171
+ stackup_mode = self.mode
2172
+ if stackup_mode not in ["laminate", "overlapping"]:
2173
+ raise AttributeError("stackup plot supports only 'laminate' and 'overlapping' stackup types.")
2174
+
2175
+ # build the layers data
2176
+ layers_data = []
2177
+ skip_flag = True
2178
+ for layer in self.layers.values(): # start from top
2179
+ if layer.name != top_layer and skip_flag:
2180
+ continue
2181
+ else:
2182
+ skip_flag = False
2183
+ layers_data.append([layer, layer.lower_elevation, layer.upper_elevation, layer.thickness])
2184
+ if layer.name == bottom_layer:
2185
+ break
2186
+ layers_data.reverse() # let's start from the bottom
2187
+
2188
+ # separate dielectric and signal if overlapping stackup
2189
+ if stackup_mode == "overlapping":
2190
+ dielectric_layers = [l for l in layers_data if l[0].type == "dielectric"]
2191
+ signal_layers = [l for l in layers_data if l[0].type == "signal"]
2192
+
2193
+ # compress the thicknesses if required
2194
+ if scale_elevation:
2195
+ min_thickness = min([i[3] for i in layers_data if i[3] != 0])
2196
+ max_thickness = max([i[3] for i in layers_data])
2197
+ c = 3 # max_thickness = c * min_thickness
2198
+
2199
+ def _compress_t(y):
2200
+ m = min_thickness
2201
+ M = max_thickness
2202
+ k = (c - 1) * m / (M - m)
2203
+ if y > 0:
2204
+ return (y - m) * k + m
2205
+ else:
2206
+ return 0.0
2207
+
2208
+ if stackup_mode == "laminate":
2209
+ l0 = layers_data[0]
2210
+ compressed_layers_data = [[l0[0], l0[1], _compress_t(l0[3]), _compress_t(l0[3])]] # the first row
2211
+ lp = compressed_layers_data[0]
2212
+ for li in layers_data[1:]: # the other rows
2213
+ ct = _compress_t(li[3])
2214
+ compressed_layers_data.append([li[0], lp[2], lp[2] + ct, ct])
2215
+ lp = compressed_layers_data[-1]
2216
+ layers_data = compressed_layers_data
2217
+
2218
+ elif stackup_mode == "overlapping":
2219
+ compressed_diels = []
2220
+ first_diel = True
2221
+ for li in dielectric_layers:
2222
+ ct = _compress_t(li[3])
2223
+ if first_diel:
2224
+ if li[1] > 0:
2225
+ l0le = _compress_t(li[1])
2226
+ else:
2227
+ l0le = li[1]
2228
+ compressed_diels.append([li[0], l0le, l0le + ct, ct])
2229
+ first_diel = False
2230
+ else:
2231
+ lp = compressed_diels[-1]
2232
+ compressed_diels.append([li[0], lp[2], lp[2] + ct, ct])
2233
+
2234
+ def _convert_elevation(el):
2235
+ inside = False
2236
+ for i, li in enumerate(dielectric_layers):
2237
+ if li[1] <= el <= li[2]:
2238
+ inside = True
2239
+ break
2240
+ if inside:
2241
+ u = (el - li[1]) / (li[2] - li[1])
2242
+ cli = compressed_diels[i]
2243
+ cel = cli[1] + u * (cli[2] - cli[1])
2244
+ else:
2245
+ cel = el
2246
+ return cel
2247
+
2248
+ compressed_signals = []
2249
+ for li in signal_layers:
2250
+ cle = _convert_elevation(li[1])
2251
+ cue = _convert_elevation(li[2])
2252
+ ct = cue - cle
2253
+ compressed_signals.append([li[0], cle, cue, ct])
2254
+
2255
+ dielectric_layers = compressed_diels
2256
+ signal_layers = compressed_signals
2257
+
2258
+ # create the data for the plot
2259
+ diel_alpha = 0.4
2260
+ signal_alpha = 0.6
2261
+ zero_thickness_alpha = 1.0
2262
+ annotation_fontsize = 14
2263
+ annotation_x_margin = 0.01
2264
+ annotations = []
2265
+ plot_data = []
2266
+ if stackup_mode == "laminate":
2267
+ min_thickness = min([i[3] for i in layers_data if i[3] != 0])
2268
+ for ly in layers_data:
2269
+ layer = ly[0]
2270
+
2271
+ # set color and label
2272
+ color = [float(i) / 256 for i in layer.color]
2273
+ if color == [1.0, 1.0, 1.0]:
2274
+ color = [0.9, 0.9, 0.9]
2275
+ label = "{}, {}, thick: {:.3f}um, elev: {:.3f}um".format(
2276
+ layer.name, layer.material, layer.thickness * 1e6, layer.lower_elevation * 1e6
2277
+ )
2278
+
2279
+ # create patch
2280
+ x = [0, 0, 1, 1]
2281
+ if ly[3] > 0:
2282
+ lower_elevation = ly[1]
2283
+ upper_elevation = ly[2]
2284
+ y = [lower_elevation, upper_elevation, upper_elevation, lower_elevation]
2285
+ plot_data.insert(0, [x, y, color, label, signal_alpha, "fill"])
2286
+ else:
2287
+ lower_elevation = ly[1] - min_thickness * 0.1 # make the zero thickness layers more visible
2288
+ upper_elevation = ly[2] + min_thickness * 0.1
2289
+ y = [lower_elevation, upper_elevation, upper_elevation, lower_elevation]
2290
+ # put the zero thickness layers on top
2291
+ plot_data.append([x, y, color, label, zero_thickness_alpha, "fill"])
2292
+
2293
+ # create annotation
2294
+ y_pos = (lower_elevation + upper_elevation) / 2
2295
+ if layer.type == "dielectric":
2296
+ x_pos = -annotation_x_margin
2297
+ annotations.append(
2298
+ [x_pos, y_pos, layer.name, {"fontsize": annotation_fontsize, "horizontalalignment": "right"}]
2299
+ )
2300
+ elif layer.type == "signal":
2301
+ x_pos = 1.0 + annotation_x_margin
2302
+ annotations.append([x_pos, y_pos, layer.name, {"fontsize": annotation_fontsize}])
2303
+
2304
+ # evaluate the legend reorder
2305
+ legend_order = []
2306
+ for ly in layers_data:
2307
+ name = ly[0].name
2308
+ for i, a in enumerate(plot_data):
2309
+ iname = a[3].split(",")[0]
2310
+ if name == iname:
2311
+ legend_order.append(i)
2312
+ break
2313
+
2314
+ elif stackup_mode == "overlapping":
2315
+ min_thickness = min([i[3] for i in signal_layers if i[3] != 0])
2316
+ columns = [] # first column is x=[0,1], second column is x=[1,2] and so on...
2317
+ for ly in signal_layers:
2318
+ lower_elevation = ly[1] # lower elevation
2319
+ t = ly[3] # thickness
2320
+ put_in_column = 0
2321
+ cell_position = 0
2322
+ for c in columns:
2323
+ uep = c[-1][0][2] # upper elevation of the last entry of that column
2324
+ tp = c[-1][0][3] # thickness of the last entry of that column
2325
+ if lower_elevation < uep or (abs(lower_elevation - uep) < 1e-15 and tp == 0 and t == 0):
2326
+ put_in_column += 1
2327
+ cell_position = len(c)
2328
+ else:
2329
+ break
2330
+ if len(columns) < put_in_column + 1: # add a new column if required
2331
+ columns.append([])
2332
+ # put zeros at the beginning of the column until there is the first layer
2333
+ if cell_position != 0:
2334
+ fill_cells = cell_position - 1 - len(columns[put_in_column])
2335
+ for i in range(fill_cells):
2336
+ columns[put_in_column].append(0)
2337
+ # append the layer to the proper column and row
2338
+ x = [put_in_column + 1, put_in_column + 1, put_in_column + 2, put_in_column + 2]
2339
+ columns[put_in_column].append([ly, x])
2340
+
2341
+ # fill the columns matrix with zeros on top
2342
+ n_rows = max([len(i) for i in columns])
2343
+ for c in columns:
2344
+ while len(c) < n_rows:
2345
+ c.append(0)
2346
+ # expand to the right the fill for the signals that have no overlap on the right
2347
+ width = len(columns) + 1
2348
+ for i, c in enumerate(columns[:-1]):
2349
+ for j, r in enumerate(c):
2350
+ if r != 0: # and dname == r[0].name:
2351
+ if columns[i + 1][j] == 0:
2352
+ # nothing on the right, so expand the fill
2353
+ x = r[1]
2354
+ r[1] = [x[0], x[0], width, width]
2355
+
2356
+ for c in columns:
2357
+ for r in c:
2358
+ if r != 0:
2359
+ ly = r[0]
2360
+ layer = ly[0]
2361
+ x = r[1]
2362
+
2363
+ # set color and label
2364
+ color = [float(i) / 256 for i in layer.color]
2365
+ if color == [1.0, 1.0, 1.0]:
2366
+ color = [0.9, 0.9, 0.9]
2367
+ label = "{}, {}, thick: {:.3f}um, elev: {:.3f}um".format(
2368
+ layer.name, layer.material, layer.thickness * 1e6, layer.lower_elevation * 1e6
2369
+ )
2370
+
2371
+ if ly[3] > 0:
2372
+ lower_elevation = ly[1]
2373
+ upper_elevation = ly[2]
2374
+ y = [lower_elevation, upper_elevation, upper_elevation, lower_elevation]
2375
+ plot_data.insert(0, [x, y, color, label, signal_alpha, "fill"])
2376
+ else:
2377
+ lower_elevation = ly[1] - min_thickness * 0.1 # make the zero thickness layers more visible
2378
+ upper_elevation = ly[2] + min_thickness * 0.1
2379
+ y = [lower_elevation, upper_elevation, upper_elevation, lower_elevation]
2380
+ # put the zero thickness layers on top
2381
+ plot_data.append([x, y, color, label, zero_thickness_alpha, "fill"])
2382
+
2383
+ # create annotation
2384
+ x_pos = 1.0
2385
+ y_pos = (lower_elevation + upper_elevation) / 2
2386
+ annotations.append([x_pos, y_pos, layer.name, {"fontsize": annotation_fontsize}])
2387
+
2388
+ # order the annotations based on y_pos (it is necessary later to move them to avoid text overlapping)
2389
+ annotations.sort(key=lambda e: e[1])
2390
+ # move all the annotations to the final x (it could be larger than 1 due to additional columns)
2391
+ width = len(columns) + 1
2392
+ for i, a in enumerate(annotations):
2393
+ a[0] = width + annotation_x_margin * width
2394
+
2395
+ for ly in dielectric_layers:
2396
+ layer = ly[0]
2397
+ # set color and label
2398
+ color = [float(i) / 256 for i in layer.color]
2399
+ if color == [1.0, 1.0, 1.0]:
2400
+ color = [0.9, 0.9, 0.9]
2401
+ label = "{}, {}, thick: {:.3f}um, elev: {:.3f}um".format(
2402
+ layer.name, layer.material, layer.thickness * 1e6, layer.lower_elevation * 1e6
2403
+ )
2404
+ # create the patch
2405
+ lower_elevation = ly[1]
2406
+ upper_elevation = ly[2]
2407
+ y = [lower_elevation, upper_elevation, upper_elevation, lower_elevation]
2408
+ x = [0, 0, width, width]
2409
+ plot_data.insert(0, [x, y, color, label, diel_alpha, "fill"])
2410
+
2411
+ # create annotation
2412
+ x_pos = -annotation_x_margin * width
2413
+ y_pos = (lower_elevation + upper_elevation) / 2
2414
+ annotations.append(
2415
+ [x_pos, y_pos, layer.name, {"fontsize": annotation_fontsize, "horizontalalignment": "right"}]
2416
+ )
2417
+
2418
+ # evaluate the legend reorder
2419
+ legend_order = []
2420
+ for ly in dielectric_layers:
2421
+ name = ly[0].name
2422
+ for i, a in enumerate(plot_data):
2423
+ iname = a[3].split(",")[0]
2424
+ if name == iname:
2425
+ legend_order.append(i)
2426
+ break
2427
+ for ly in signal_layers:
2428
+ name = ly[0].name
2429
+ for i, a in enumerate(plot_data):
2430
+ iname = a[3].split(",")[0]
2431
+ if name == iname:
2432
+ legend_order.append(i)
2433
+ break
2434
+
2435
+ # calculate the extremities of the plot
2436
+ x_min = 0.0
2437
+ x_max = max([max(i[0]) for i in plot_data])
2438
+ if stackup_mode == "laminate":
2439
+ y_min = layers_data[0][1]
2440
+ y_max = layers_data[-1][2]
2441
+ elif stackup_mode == "overlapping":
2442
+ y_min = min(dielectric_layers[0][1], signal_layers[0][1])
2443
+ y_max = max(dielectric_layers[-1][2], signal_layers[-1][2])
2444
+
2445
+ # move the annotations to avoid text overlapping
2446
+ new_annotations = []
2447
+ for i, a in enumerate(annotations):
2448
+ if i > 0 and abs(a[1] - annotations[i - 1][1]) < (y_max - y_min) / 75:
2449
+ new_annotations[-1][2] = str(new_annotations[-1][2]) + ", " + str(a[2])
2450
+ else:
2451
+ new_annotations.append(a)
2452
+ annotations = new_annotations
2453
+
2454
+ if plot_definitions:
2455
+ if stackup_mode == "overlapping":
2456
+ self._logger.warning("Plot of padstacks are supported only for Laminate mode.")
2457
+
2458
+ max_plots = 10
2459
+
2460
+ if not isinstance(plot_definitions, list):
2461
+ plot_definitions = [plot_definitions]
2462
+ color_index = 0
2463
+ color_keys = list(CSS4_COLORS.keys())
2464
+ delta = 1 / (max_plots + 1) # padstack spacing in plot coordinates
2465
+ x_start = delta
2466
+
2467
+ # find the max padstack size to calculate the scaling factor
2468
+ max_padstak_size = 0.0
2469
+ for definition in plot_definitions:
2470
+ if isinstance(definition, str):
2471
+ definition = self._pedb.padstacks.definitions[definition]
2472
+ for layer, defs in definition.pad_by_layer.items():
2473
+ pad_shape = defs.shape
2474
+ params = defs.parameters_values
2475
+ pad_size = max([p for p in params])
2476
+ if pad_size > max_padstak_size:
2477
+ max_padstak_size = pad_size
2478
+ if not definition.is_null:
2479
+ hole_d = definition.hole_diameter
2480
+ max_padstak_size = max(hole_d, max_padstak_size)
2481
+ scaling_f_pad = (2 / ((max_plots + 1) * 3)) / max_padstak_size
2482
+
2483
+ for definition in plot_definitions:
2484
+ if isinstance(definition, str):
2485
+ definition = self._pedb.padstacks.definitions[definition]
2486
+ min_le = 1e12
2487
+ max_ue = -1e12
2488
+ max_x = 0
2489
+ padstack_name = definition.name
2490
+ annotations.append([x_start, y_max, padstack_name, {"rotation": 45}])
2491
+
2492
+ via_start_layer = definition.start_layer
2493
+ via_stop_layer = definition.stop_layer
2494
+
2495
+ if stackup_mode == "overlapping":
2496
+ # here search the column using the first and last layer. Pick the column with max index.
2497
+ pass
2498
+
2499
+ for layer, defs in definition.pad_by_layer.items():
2500
+ pad_shape = defs.shape
2501
+ params = defs.parameters_values
2502
+ pad_size = max([p for p in params])
2503
+ if stackup_mode == "laminate":
2504
+ x = [
2505
+ x_start - pad_size / 2 * scaling_f_pad,
2506
+ x_start - pad_size / 2 * scaling_f_pad,
2507
+ x_start + pad_size / 2 * scaling_f_pad,
2508
+ x_start + pad_size / 2 * scaling_f_pad,
2509
+ ]
2510
+ lower_elevation = [e[1] for e in layers_data if e[0].name == layer or layer == "Default"][0]
2511
+ upper_elevation = [e[2] for e in layers_data if e[0].name == layer or layer == "Default"][0]
2512
+ y = [lower_elevation, upper_elevation, upper_elevation, lower_elevation]
2513
+ # create the patch for that signal layer
2514
+ plot_data.append([x, y, color_keys[color_index], None, 1.0, "fill"])
2515
+ elif stackup_mode == "overlapping":
2516
+ # here evaluate the x based on the column evaluated before and the pad size
2517
+ pass
2518
+
2519
+ min_le = min(lower_elevation, min_le)
2520
+ max_ue = max(upper_elevation, max_ue)
2521
+ if not definition.is_null:
2522
+ # create patch for the hole
2523
+ hole_radius = definition.hole_diameter / 2 * scaling_f_pad
2524
+ x = [x_start - hole_radius, x_start - hole_radius, x_start + hole_radius, x_start + hole_radius]
2525
+ y = [min_le, max_ue, max_ue, min_le]
2526
+ plot_data.append([x, y, color_keys[color_index], None, 0.7, "fill"])
2527
+ # create patch for the dielectric
2528
+ max_x = max(max_x, hole_radius)
2529
+ rad = hole_radius * (100 - definition.hole_plating_ratio) / 100
2530
+ x = [x_start - rad, x_start - rad, x_start + rad, x_start + rad]
2531
+ plot_data.append([x, y, color_keys[color_index], None, 1.0, "fill"])
2532
+
2533
+ color_index += 1
2534
+ if color_index == max_plots:
2535
+ self._logger.warning("Maximum number of definitions plotted.")
2536
+ break
2537
+ x_start += delta
2538
+
2539
+ # plot the stackup
2540
+ plt = plot_matplotlib(
2541
+ plot_data,
2542
+ size=size,
2543
+ show_legend=False,
2544
+ xlabel="",
2545
+ ylabel="",
2546
+ title="",
2547
+ save_plot=None,
2548
+ x_limits=[x_min, x_max],
2549
+ y_limits=[y_min, y_max],
2550
+ axis_equal=False,
2551
+ annotations=annotations,
2552
+ show=False,
2553
+ )
2554
+ # we have to customize some defaults, so we plot or save the figure here
2555
+ plt.axis("off")
2556
+ plt.box(False)
2557
+ plt.title("Stackup\n ", fontsize=28)
2558
+ # evaluates the number of legend column based on the layer name max length
2559
+ ncol = 3 if max([len(n) for n in layer_names]) < 15 else 2
2560
+ handles, labels = plt.gca().get_legend_handles_labels()
2561
+ plt.legend(
2562
+ [handles[idx] for idx in legend_order],
2563
+ [labels[idx] for idx in legend_order],
2564
+ bbox_to_anchor=(0, -0.05),
2565
+ loc="upper left",
2566
+ borderaxespad=0,
2567
+ ncol=ncol,
2568
+ )
2569
+ plt.tight_layout()
2570
+ if save_plot:
2571
+ plt.savefig(save_plot)
2572
+ elif show:
2573
+ plt.show()
2574
+ return plt