pyedb 0.37.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.
- pyedb/__init__.py +1 -1
- pyedb/common/nets.py +53 -139
- pyedb/configuration/cfg_common.py +1 -1
- pyedb/configuration/cfg_components.py +229 -201
- pyedb/configuration/cfg_data.py +3 -1
- pyedb/configuration/cfg_general.py +4 -2
- pyedb/configuration/cfg_modeler.py +7 -7
- pyedb/configuration/cfg_package_definition.py +1 -1
- pyedb/configuration/cfg_padstacks.py +346 -290
- pyedb/configuration/cfg_ports_sources.py +243 -65
- pyedb/configuration/configuration.py +23 -3
- pyedb/dotnet/{application → database}/Variables.py +21 -21
- pyedb/dotnet/{edb_core → database}/cell/connectable.py +5 -5
- pyedb/dotnet/{edb_core → database}/cell/hierarchy/component.py +11 -11
- pyedb/dotnet/{edb_core → database}/cell/hierarchy/hierarchy_obj.py +1 -1
- pyedb/dotnet/{edb_core → database}/cell/hierarchy/model.py +1 -1
- pyedb/dotnet/{edb_core → database}/cell/layout.py +19 -19
- pyedb/dotnet/{edb_core → database}/cell/layout_obj.py +3 -3
- pyedb/dotnet/{edb_core → database}/cell/primitive/bondwire.py +1 -1
- pyedb/dotnet/{edb_core → database}/cell/primitive/path.py +4 -4
- pyedb/dotnet/{edb_core → database}/cell/primitive/primitive.py +14 -14
- pyedb/dotnet/{edb_core → database}/cell/terminal/bundle_terminal.py +2 -2
- pyedb/dotnet/{edb_core → database}/cell/terminal/edge_terminal.py +4 -4
- pyedb/dotnet/{edb_core → database}/cell/terminal/padstack_instance_terminal.py +2 -2
- pyedb/dotnet/{edb_core → database}/cell/terminal/pingroup_terminal.py +2 -2
- pyedb/dotnet/{edb_core → database}/cell/terminal/point_terminal.py +2 -2
- pyedb/dotnet/{edb_core → database}/cell/terminal/terminal.py +11 -11
- pyedb/dotnet/{edb_core → database}/cell/voltage_regulator.py +2 -2
- pyedb/dotnet/{edb_core → database}/components.py +101 -124
- pyedb/dotnet/{edb_core → database}/definition/component_def.py +5 -5
- pyedb/dotnet/{edb_core → database}/definition/component_model.py +1 -1
- pyedb/dotnet/{edb_core → database}/definition/definition_obj.py +1 -1
- pyedb/dotnet/{edb_core → database}/definition/definitions.py +2 -2
- pyedb/dotnet/{edb_core → database}/definition/package_def.py +4 -4
- pyedb/dotnet/{edb_core → database}/dotnet/database.py +8 -8
- pyedb/dotnet/{edb_core → database}/dotnet/primitive.py +9 -9
- pyedb/dotnet/{edb_core → database}/edb_data/control_file.py +12 -12
- pyedb/dotnet/{edb_core → database}/edb_data/hfss_extent_info.py +7 -7
- pyedb/dotnet/{edb_core → database}/edb_data/nets_data.py +10 -13
- pyedb/dotnet/{edb_core → database}/edb_data/padstacks_data.py +60 -73
- pyedb/dotnet/{edb_core → database}/edb_data/ports.py +4 -4
- pyedb/dotnet/{edb_core → database}/edb_data/primitives_data.py +5 -5
- pyedb/dotnet/{edb_core → database}/edb_data/raptor_x_simulation_setup_data.py +4 -4
- pyedb/dotnet/{edb_core → database}/edb_data/simulation_configuration.py +10 -10
- pyedb/dotnet/{edb_core → database}/edb_data/sources.py +4 -4
- pyedb/dotnet/{edb_core → database}/edb_data/variables.py +1 -1
- pyedb/dotnet/{edb_core → database}/geometry/polygon_data.py +4 -4
- pyedb/dotnet/{edb_core → database}/hfss.py +8 -8
- pyedb/dotnet/{edb_core → database}/layout_obj_instance.py +1 -1
- pyedb/dotnet/{edb_core → database}/layout_validation.py +2 -2
- pyedb/dotnet/{edb_core → database}/materials.py +23 -8
- pyedb/dotnet/{edb_core → database}/modeler.py +27 -27
- pyedb/dotnet/{edb_core → database}/net_class.py +8 -8
- pyedb/dotnet/{edb_core → database}/nets.py +12 -12
- pyedb/dotnet/{edb_core → database}/padstack.py +17 -16
- pyedb/dotnet/{edb_core → database}/sim_setup_data/data/mesh_operation.py +1 -1
- pyedb/dotnet/{edb_core → database}/sim_setup_data/data/settings.py +18 -3
- pyedb/dotnet/{edb_core → database}/sim_setup_data/data/sim_setup_info.py +2 -2
- pyedb/dotnet/{edb_core → database}/sim_setup_data/data/simulation_settings.py +1 -1
- pyedb/dotnet/{edb_core → database}/sim_setup_data/data/siw_dc_ir_settings.py +1 -1
- pyedb/dotnet/{edb_core → database}/sim_setup_data/data/sweep_data.py +4 -4
- pyedb/dotnet/{edb_core → database}/siwave.py +10 -10
- pyedb/dotnet/{edb_core → database}/stackup.py +12 -12
- pyedb/dotnet/{edb_core → database}/utilities/hfss_simulation_setup.py +15 -15
- pyedb/dotnet/{edb_core → database}/utilities/obj_base.py +1 -1
- pyedb/dotnet/{edb_core → database}/utilities/simulation_setup.py +4 -3
- pyedb/dotnet/{edb_core → database}/utilities/siwave_simulation_setup.py +6 -6
- pyedb/dotnet/edb.py +118 -113
- pyedb/extensions/pre_layout_design_toolkit/via_design.py +1151 -0
- pyedb/generic/design_types.py +26 -19
- pyedb/generic/general_methods.py +1 -1
- pyedb/generic/plot.py +0 -2
- pyedb/grpc/database/__init__.py +1 -0
- pyedb/grpc/database/components.py +2354 -0
- pyedb/grpc/database/control_file.py +1277 -0
- pyedb/grpc/database/definition/component_def.py +218 -0
- pyedb/grpc/database/definition/component_model.py +39 -0
- pyedb/grpc/database/definition/component_pin.py +32 -0
- pyedb/grpc/database/definition/materials.py +1207 -0
- pyedb/grpc/database/definition/n_port_component_model.py +34 -0
- pyedb/grpc/database/definition/package_def.py +227 -0
- pyedb/grpc/database/definition/padstack_def.py +842 -0
- pyedb/grpc/database/definitions.py +70 -0
- pyedb/grpc/database/general.py +43 -0
- pyedb/grpc/database/geometry/__init__.py +0 -0
- pyedb/grpc/database/geometry/arc_data.py +93 -0
- pyedb/grpc/database/geometry/point_3d_data.py +79 -0
- pyedb/grpc/database/geometry/point_data.py +30 -0
- pyedb/grpc/database/geometry/polygon_data.py +133 -0
- pyedb/grpc/database/hfss.py +1279 -0
- pyedb/grpc/database/hierarchy/__init__.py +0 -0
- pyedb/grpc/database/hierarchy/component.py +1301 -0
- pyedb/grpc/database/hierarchy/model.py +31 -0
- pyedb/grpc/database/hierarchy/netlist_model.py +30 -0
- pyedb/grpc/database/hierarchy/pin_pair_model.py +128 -0
- pyedb/grpc/database/hierarchy/pingroup.py +245 -0
- pyedb/grpc/database/hierarchy/s_parameter_model.py +33 -0
- pyedb/grpc/database/hierarchy/spice_model.py +48 -0
- pyedb/grpc/database/layers/__init__.py +0 -0
- pyedb/grpc/database/layers/layer.py +57 -0
- pyedb/grpc/database/layers/stackup_layer.py +410 -0
- pyedb/grpc/database/layout/__init__.py +0 -0
- pyedb/grpc/database/layout/cell.py +30 -0
- pyedb/grpc/database/layout/layout.py +196 -0
- pyedb/grpc/database/layout/voltage_regulator.py +149 -0
- pyedb/grpc/database/layout_validation.py +319 -0
- pyedb/grpc/database/modeler.py +1468 -0
- pyedb/grpc/database/net/__init__.py +0 -0
- pyedb/grpc/database/net/differential_pair.py +138 -0
- pyedb/grpc/database/net/extended_net.py +340 -0
- pyedb/grpc/database/net/net.py +198 -0
- pyedb/grpc/database/net/net_class.py +93 -0
- pyedb/grpc/database/nets.py +633 -0
- pyedb/grpc/database/padstacks.py +1500 -0
- pyedb/grpc/database/ports/__init__.py +0 -0
- pyedb/grpc/database/ports/ports.py +396 -0
- pyedb/grpc/database/primitive/__init__.py +3 -0
- pyedb/grpc/database/primitive/bondwire.py +181 -0
- pyedb/grpc/database/primitive/circle.py +75 -0
- pyedb/grpc/database/primitive/padstack_instance.py +1116 -0
- pyedb/grpc/database/primitive/path.py +346 -0
- pyedb/grpc/database/primitive/polygon.py +276 -0
- pyedb/grpc/database/primitive/primitive.py +739 -0
- pyedb/grpc/database/primitive/rectangle.py +146 -0
- pyedb/grpc/database/simulation_setup/__init__.py +0 -0
- pyedb/grpc/database/simulation_setup/adaptive_frequency.py +33 -0
- pyedb/grpc/database/simulation_setup/hfss_advanced_meshing_settings.py +32 -0
- pyedb/grpc/database/simulation_setup/hfss_advanced_settings.py +59 -0
- pyedb/grpc/database/simulation_setup/hfss_dcr_settings.py +35 -0
- pyedb/grpc/database/simulation_setup/hfss_general_settings.py +61 -0
- pyedb/grpc/database/simulation_setup/hfss_settings_options.py +78 -0
- pyedb/grpc/database/simulation_setup/hfss_simulation_settings.py +118 -0
- pyedb/grpc/database/simulation_setup/hfss_simulation_setup.py +355 -0
- pyedb/grpc/database/simulation_setup/hfss_solver_settings.py +34 -0
- pyedb/grpc/database/simulation_setup/mesh_operation.py +34 -0
- pyedb/grpc/database/simulation_setup/raptor_x_advanced_settings.py +34 -0
- pyedb/grpc/database/simulation_setup/raptor_x_general_settings.py +33 -0
- pyedb/grpc/database/simulation_setup/raptor_x_simulation_settings.py +64 -0
- pyedb/grpc/database/simulation_setup/raptor_x_simulation_setup.py +125 -0
- pyedb/grpc/database/simulation_setup/siwave_dcir_simulation_setup.py +34 -0
- pyedb/grpc/database/simulation_setup/siwave_simulation_setup.py +119 -0
- pyedb/grpc/database/simulation_setup/sweep_data.py +32 -0
- pyedb/grpc/database/siwave.py +1023 -0
- pyedb/grpc/database/source_excitations.py +2572 -0
- pyedb/grpc/database/stackup.py +2574 -0
- pyedb/grpc/database/terminal/__init__.py +0 -0
- pyedb/grpc/database/terminal/bundle_terminal.py +218 -0
- pyedb/grpc/database/terminal/edge_terminal.py +51 -0
- pyedb/grpc/database/terminal/padstack_instance_terminal.py +171 -0
- pyedb/grpc/database/terminal/pingroup_terminal.py +162 -0
- pyedb/grpc/database/terminal/point_terminal.py +99 -0
- pyedb/grpc/database/terminal/terminal.py +470 -0
- pyedb/grpc/database/utility/__init__.py +3 -0
- pyedb/grpc/database/utility/constants.py +25 -0
- pyedb/grpc/database/utility/heat_sink.py +124 -0
- pyedb/grpc/database/utility/hfss_extent_info.py +448 -0
- pyedb/grpc/database/utility/layout_statistics.py +277 -0
- pyedb/grpc/database/utility/rlc.py +80 -0
- pyedb/grpc/database/utility/simulation_configuration.py +3305 -0
- pyedb/grpc/database/utility/sources.py +388 -0
- pyedb/grpc/database/utility/sweep_data_distribution.py +83 -0
- pyedb/grpc/database/utility/xml_control_file.py +1277 -0
- pyedb/grpc/edb.py +4151 -0
- pyedb/grpc/edb_init.py +481 -0
- pyedb/grpc/rpc_session.py +177 -0
- pyedb/ipc2581/ecad/cad_data/assembly_drawing.py +3 -2
- pyedb/ipc2581/ecad/cad_data/feature.py +4 -3
- pyedb/ipc2581/ecad/cad_data/layer_feature.py +32 -20
- pyedb/ipc2581/ecad/cad_data/outline.py +3 -2
- pyedb/ipc2581/ecad/cad_data/package.py +4 -3
- pyedb/ipc2581/ecad/cad_data/path.py +82 -31
- pyedb/ipc2581/ecad/cad_data/polygon.py +122 -60
- pyedb/ipc2581/ecad/cad_data/profile.py +13 -12
- pyedb/ipc2581/ecad/cad_data/step.py +52 -20
- pyedb/ipc2581/ipc2581.py +47 -49
- pyedb/modeler/geometry_operators.py +1 -1
- {pyedb-0.37.0.dist-info → pyedb-0.39.0.dist-info}/METADATA +9 -6
- pyedb-0.39.0.dist-info/RECORD +288 -0
- pyedb-0.37.0.dist-info/RECORD +0 -194
- /pyedb/dotnet/{edb_core → database}/__init__.py +0 -0
- /pyedb/dotnet/{application → database/cell}/__init__.py +0 -0
- /pyedb/dotnet/{edb_core/cell → database/cell/hierarchy}/__init__.py +0 -0
- /pyedb/dotnet/{edb_core → database}/cell/hierarchy/netlist_model.py +0 -0
- /pyedb/dotnet/{edb_core → database}/cell/hierarchy/pin_pair_model.py +0 -0
- /pyedb/dotnet/{edb_core → database}/cell/hierarchy/s_parameter_model.py +0 -0
- /pyedb/dotnet/{edb_core → database}/cell/hierarchy/spice_model.py +0 -0
- /pyedb/dotnet/{edb_core → database}/cell/primitive/__init__.py +0 -0
- /pyedb/dotnet/{edb_core/cell/hierarchy → database/cell/terminal}/__init__.py +0 -0
- /pyedb/dotnet/{edb_core/cell/terminal → database/definition}/__init__.py +0 -0
- /pyedb/dotnet/{edb_core/definition → database/dotnet}/__init__.py +0 -0
- /pyedb/dotnet/{edb_core/dotnet → database/edb_data}/__init__.py +0 -0
- /pyedb/dotnet/{edb_core → database}/edb_data/design_options.py +0 -0
- /pyedb/dotnet/{edb_core → database}/edb_data/edbvalue.py +0 -0
- /pyedb/dotnet/{edb_core → database}/edb_data/layer_data.py +0 -0
- /pyedb/dotnet/{edb_core → database}/edb_data/utilities.py +0 -0
- /pyedb/dotnet/{edb_core → database}/general.py +0 -0
- /pyedb/dotnet/{edb_core/edb_data → database/geometry}/__init__.py +0 -0
- /pyedb/dotnet/{edb_core → database}/geometry/point_data.py +0 -0
- /pyedb/dotnet/{edb_core → database}/sim_setup_data/__init__.py +0 -0
- /pyedb/dotnet/{edb_core → database}/sim_setup_data/data/__init__.py +0 -0
- /pyedb/dotnet/{edb_core → database}/sim_setup_data/data/adaptive_frequency_data.py +0 -0
- /pyedb/dotnet/{edb_core/geometry → database/sim_setup_data/io}/__init__.py +0 -0
- /pyedb/dotnet/{edb_core → database}/sim_setup_data/io/siwave.py +0 -0
- /pyedb/dotnet/{edb_core → database}/utilities/__init__.py +0 -0
- /pyedb/dotnet/{edb_core → database}/utilities/heatsink.py +0 -0
- /pyedb/{dotnet/edb_core/sim_setup_data/io → grpc/database/definition}/__init__.py +0 -0
- {pyedb-0.37.0.dist-info → pyedb-0.39.0.dist-info}/LICENSE +0 -0
- {pyedb-0.37.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
|