Source code for qiskit_metal.renderers.renderer_gmsh.gmsh_renderer

from typing import Union, List, Optional
from collections import defaultdict
import pandas as pd
import gmsh
import numpy as np

from qiskit_metal.renderers.renderer_base import QRenderer

from .gmsh_utils import Vec3D, Vec3DArray, line_width_offset_pts, render_path_curves
from qiskit_metal.toolbox_metal.bounds_for_path_and_poly_tables import BoundsForPathAndPolyTables
from qiskit_metal.toolbox_metal.parsing import parse_value

from qiskit_metal import Dict

from qiskit_metal import config
if not config.is_building_docs():
    from qiskit_metal.toolbox_python.utility_functions import clean_name
    from qiskit_metal.toolbox_python.utility_functions import bad_fillet_idxs


[docs] class QGmshRenderer(QRenderer): """Extends QRendererAnalysis class to export designs to Gmsh using the Gmsh python API. Default Options: * x_buffer_width_mm -- Buffer between max/min x and edge of ground plane, in mm * y_buffer_width_mm -- Buffer between max/min y and edge of ground plane, in mm * mesh -- to define meshing parameters * max_size -- upper bound for the size of mesh node * min_size -- lower bound for the size of mesh node * max_size_jj -- maximum size of mesh nodes at jj * smoothing -- mesh smoothing value * nodes_per_2pi_curve -- number of nodes for every 2π radians of curvature * algorithm_3d -- value to indicate meshing algorithm used by Gmsh * num_threads -- number of threads for parallel meshing * mesh_size_fields -- specify mesh size field parameters * min_distance_from_edges -- min distance for mesh gradient generation * max_distance_from_edges -- max distance for mesh gradient generation * distance_delta -- delta change in distance with each consecutive step * gradient_delta -- delta change in gradient with each consecutive step * colors -- specify colors for the mesh elements, chips or layers * metal -- color for metallized entities * jj -- color for JJs * dielectric -- color for dielectric entity """ default_options = Dict( x_buffer_width_mm=0.2, y_buffer_width_mm=0.2, mesh=Dict( max_size="70um", min_size="5um", max_size_jj="5um", smoothing=10, nodes_per_2pi_curve=90, algorithm_3d=10, num_threads=8, mesh_size_fields=Dict(min_distance_from_edges="10um", max_distance_from_edges="130um", distance_delta="30um", gradient_delta="3um"), ), colors=Dict( metal=(84, 140, 168, 255), jj=(84, 140, 168, 150), dielectric=(180, 180, 180, 255), ), ) name = "gmsh" """Name""" def __init__(self, design: 'MultiPlanar', layer_types: Union[dict, None] = None, initiate=True, options: Dict = None): """ Args: design ('MultiPlanar'): The design. layer_types (Union[dict, None]): the type of layer in the format: dict(metal=[...], dielectric=[...]). Defaults to None. initiate (bool): True to initiate the renderer (Default: False). options (Dict, optional): Used to override default options. Defaults to None. """ super().__init__(design=design, initiate=initiate, render_options=options) self._model_name = "gmsh_model" default_layer_types = dict(metal=[1], dielectric=[3]) self.layer_types = default_layer_types if layer_types is None else layer_types self.bounds_handler = BoundsForPathAndPolyTables(self.design) @property def initialized(self): """Returns boolean True if initialized successfully. False otherwise.""" if gmsh.isInitialized(): return True return False @property def modeler(self): """Returns an instance to the Gmsh modeler""" return gmsh.model @property def model(self): """Returns the name of the current model""" return gmsh.model.getCurrent() @model.setter def model(self, name: str): """Sets the name of the current model. If not already present, adds a new model with the given name. """ print_info = False try: gmsh.model.setCurrent(name) except Exception: gmsh.model.add(name) self._model_name = name print_info = True if print_info: self.logger.info(f"Added new model '{name}' and set as current.")
[docs] def remove_current_model(self): """Removes the current Gmsh model""" try: gmsh.model.remove() except Exception: self.logger.error("No model found in Gmsh to be removed.")
[docs] def clear_design(self): """Clears the design in the current Gmsh model""" gmsh.clear()
def _initiate_renderer(self): """Initializes the Gmsh renderer""" gmsh.initialize() return True def _close_renderer(self): """Finalizes the Gmsh renderer""" gmsh.clear() gmsh.finalize() return True
[docs] def close(self): """Public method to close the Gmsh renderer""" return self._close_renderer()
[docs] def get_thickness_for_layer_datatype(self, layer_num: int, datatype: int = 0) -> float: """Function to get thickness of a particular layer and datatype from the layer stack. Args: layer_num (int): layer number in the layer stack datatype (int): datatype in the layer stack Returns: float: returns the thickness value """ props = ["thickness"] result = self.parse_units_gmsh( self.design.ls.get_properties_for_layer_datatype( properties=props, layer_number=layer_num, datatype=datatype)) if result: return result[0] # thickness is result[0] else: raise ValueError( f"Could not find {props} for the layer_number={layer_num}. " "Check your design and try again.")
[docs] def get_thickness_zcoord_for_layer_datatype(self, layer_num: int, datatype: int = 0 ) -> tuple[float, float]: """Function to get the thickness and z_coord of a particular layer and datatype from the layer stack. Args: layer_num (int): layer number in the layer stack datatype (int): datatype in the layer stack Returns: tuple[float, float]: returns the tuple (thickness, z_coord) """ props = ["thickness", "z_coord"] result = self.parse_units_gmsh( self.design.ls.get_properties_for_layer_datatype( properties=props, layer_number=layer_num, datatype=datatype)) if result: return result else: raise ValueError( f"Could not find {props} for the layer_number={layer_num}. " "Check your design and try again.")
[docs] def render_design( self, selection: Union[list, None] = None, open_pins: Union[list, None] = None, box_plus_buffer: bool = True, draw_sample_holder: bool = True, skip_junctions: bool = False, mesh_geoms: bool = True, ignore_metal_volume: bool = False, omit_ground_for_layers: Optional[list[int]] = None, ): """Render the design in Gmsh and apply changes to modify the geometries according to the type of simulation. Simulation parameters provided by the user. Args: selection (Union[list, None], optional): List of selected components to render. Defaults to None. open_pins (Union[list, None], optional): List of open pins to add endcaps. Defaults to None. box_plus_buffer (bool, optional): Set to True for adding buffer to chip dimensions. Defaults to True. draw_sample_holder (bool, optional): To draw the sample holder box. Defaults to True. skip_junctions (bool, optional): Set to True to sip rendering the junctions. Defaults to False. mesh_geoms (bool, optional): Set to True for meshing the geometries. Defaults to True. ignore_metal_volume (bool, optional): ignore the volume of metals and replace it with a list of surfaces instead. Defaults to False. omit_ground_for_layers (Optional[list[int]]): omit rendering the ground plane for specified layers. Defaults to None. """ # For handling the case when the user wants to use # QGmshRenderer from design.renderers.gmsh instance. if not self.initialized: self._initiate_renderer() # defaultdict: chip -- geom_tag self.layers_dict = defaultdict(list) # defaultdict: chip -- set(geom_tag) self.layer_subtract_dict = defaultdict(set) # defaultdict: chip -- dict(geom_name: geom_tag) self.polys_dict = defaultdict(dict) self.paths_dict = defaultdict(dict) self.juncs_dict = defaultdict(dict) self.physical_groups = defaultdict(dict) self.clear_design() self.draw_geometries(selection=selection, open_pins=open_pins, box_plus_buffer=box_plus_buffer, draw_sample_holder=draw_sample_holder, skip_junctions=skip_junctions, omit_ground_for_layers=omit_ground_for_layers) self.apply_changes_for_simulation( ignore_metal_volume=ignore_metal_volume, draw_sample_holder=draw_sample_holder) if mesh_geoms: try: self.add_mesh() # generate mesh except Exception as e: self.logger.info(f"ERROR: Generate Mesh: {e}")
[docs] def draw_geometries(self, draw_sample_holder: bool, selection: Union[list, None] = None, open_pins: Union[list, None] = None, box_plus_buffer: bool = True, skip_junctions: bool = False, omit_ground_for_layers: Optional[list[int]] = None): """This function draws the raw geometries in Gmsh as taken from the QGeometry tables and applies thickness depending on the layer-stack. Args: selection (Union[list, None], optional): List of selected components to render. Defaults to None. open_pins (Union[list, None], optional): List of open pins to add endcaps. Defaults to None. box_plus_buffer (bool, optional): Set to True for adding buffer to chip dimensions. Defaults to True. draw_sample_holder (bool): To draw the sample holder box. skip_junctions (bool, optional): Set to True to sip rendering the junctions. Defaults to False. omit_ground_for_layers (Optional[list[int]]): omit rendering the ground plane for specified layers. Defaults to None. """ self.qcomp_ids, self.case = self.get_unique_component_ids(selection) if self.case == 2: self.logger.warning( "Unable to proceed with rendering. Please check selection.") return self.render_tables(skip_junction=skip_junctions) self.add_endcaps(open_pins=open_pins) self.render_layers(box_plus_buffer=box_plus_buffer, omit_layers=omit_ground_for_layers, draw_sample_holder=draw_sample_holder) self.subtract_from_layers(omit_layers=omit_ground_for_layers) self.gmsh_occ_synchronize()
[docs] def apply_changes_for_simulation(self, ignore_metal_volume: bool, draw_sample_holder: bool): """This function fragments interfaces to fuse the boundaries and assigns physical groups to be used by an FEM solvers for defining bodies and boundary conditions. Args: ignore_metal_volume (bool, optional): ignore the volume of metals and replace it with a list of surfaces instead. draw_sample_holder (bool): To draw the sample holder box. Raises: ValueError: raised when self.layer_types isn't set to a valid dictionary """ if ignore_metal_volume and self.layer_types is None: raise ValueError( f"Expected dict for `layer_types`, but found {type(self.layer_types)}." ) self.fragment_interfaces(draw_sample_holder=draw_sample_holder) self.gmsh_occ_synchronize() # Add physical groups self.assign_physical_groups(ignore_metal_volume=ignore_metal_volume, draw_sample_holder=draw_sample_holder)
[docs] def gmsh_occ_synchronize(self): """Synchronize Gmsh with the internal OpenCascade graphics engine """ gmsh.model.occ.synchronize()
[docs] def render_tables(self, skip_junction: bool = True): """Render components in design grouped by table type (path, poly, or junction). """ for table_type in self.design.qgeometry.get_element_types(): if not (skip_junction and table_type == "junction"): self.render_components(table_type)
[docs] def render_components(self, table_type: str): """ Render components by breaking them down into individual elements. Args: table_type (str): Table type (poly, path, or junction). """ table = self.design.qgeometry.tables[table_type] if self.case == 0: mask = table["component"].isin(self.qcomp_ids) table = table[mask] for _, qgeom in table.iterrows(): self.render_element(qgeom, table_type) if table_type == "path": # TODO: how to do auto wirebonds? Active issue: #841 # Make a function to render wire bonds manually? pass
[docs] def render_element(self, qgeom: pd.Series, table_type: str): """Render the specified element Args: qgeom (pd.Series): QGeometry element to be rendered table_type (str): Table type (poly, path, or junction). """ if table_type == "junction": self.render_element_junction(qgeom) elif table_type == "path": self.render_element_path(qgeom) elif table_type == "poly": self.render_element_poly(qgeom) else: self.logger.error( f'RENDERER ERROR: Unkown element type: {table_type}')
[docs] def make_general_surface(self, curves: List[int]) -> int: """Create a general Gmsh surface. Args: curves (List[int]): List of Gmsh curves to make surface Returns: int: tag of created Gmsh surface """ curve_loop = gmsh.model.occ.addCurveLoop(curves) surface = gmsh.model.occ.addPlaneSurface([curve_loop]) return surface
[docs] def parse_units_gmsh(self, _input: Union[int, float, np.ndarray, list, tuple, str]): """Helper function to parse numbers and units Args: _input (Union[int, float, np.ndarray, list, tuple, str]): input to parse Returns: Union[int, float, np.ndarray, list, tuple, str]: parsed input value """ if isinstance(_input, (int, float)): return _input elif isinstance(_input, (np.ndarray)): output = [] for i in _input: output += [self.parse_units_gmsh(i)] return np.array(output) elif isinstance(_input, (list, tuple)): output = [] for i in _input: output += [self.parse_units_gmsh(i)] return type(_input)(output) elif isinstance(_input, str): return parse_value(_input, self.design.variables) else: self.logger.error( f"RENDERER ERROR: Expected int, str, list, np.ndarray, or tuple. Got: {type(_input)}." )
[docs] def render_element_junction(self, junc: pd.Series): """Render an element of type: 'junction' Args: junc (pd.Series): Junction to render. """ qc_shapely = junc.geometry qc_width = self.parse_units_gmsh(junc.width) qc_thickness, qc_z = self.get_thickness_zcoord_for_layer_datatype( layer_num=junc.layer) vecs = Vec3DArray.make_vec3DArray( self.parse_units_gmsh(list(qc_shapely.coords)), qc_z) qc_name = self.design._components[ junc["component"]].name + '_' + clean_name(junc["name"]) # Considering JJ will always be a rectangle v1, v2 = line_width_offset_pts(vecs.points[0], vecs.path_vecs[0], qc_width, qc_z, ret_pts=False) v3, v4 = line_width_offset_pts(vecs.points[1], vecs.path_vecs[0], qc_width, qc_z, ret_pts=False) v1_v3 = Vec3D.get_distance(v1, v3) v1_v4 = Vec3D.get_distance(v1, v4) vecs = [v1, v2, v4, v3, v1] if v1_v3 <= v1_v4 else [v1, v2, v3, v4, v1] pts = [gmsh.model.occ.addPoint(v[0], v[1], qc_z) for v in vecs[:-1]] pts += [pts[0]] lines = [] for i, p in enumerate(pts[:-1]): lines += [gmsh.model.occ.addLine(p, pts[i + 1])] curve_loop = gmsh.model.occ.addCurveLoop(lines) surface = gmsh.model.occ.addPlaneSurface([curve_loop]) # Translate the junction to the middle of the layer gmsh.model.occ.translate([(2, surface)], dx=0, dy=0, dz=qc_thickness / 2) if junc.layer not in self.juncs_dict: self.juncs_dict[junc.layer] = dict() self.juncs_dict[junc.layer][qc_name] = [surface]
[docs] def render_element_path(self, path: pd.Series): """Render an element of type: 'path' Args: path (pd.Series): Path to render. """ qc_shapely = path.geometry qc_width = path.width qc_fillet = self.parse_units_gmsh(path.fillet) if float( path.fillet) is not np.nan else 0.0 qc_thickness, qc_z = self.get_thickness_zcoord_for_layer_datatype( layer_num=path.layer) vecs = Vec3DArray.make_vec3DArray( self.parse_units_gmsh(list(qc_shapely.coords)), qc_z) qc_name = self.design._components[ path["component"]].name + '_' + clean_name(path["name"]) bad_fillets = bad_fillet_idxs(qc_shapely.coords, qc_fillet) curves = render_path_curves(vecs, qc_z, qc_fillet, qc_width, bad_fillets) surface = self.make_general_surface(curves) if path.layer not in self.layer_subtract_dict: self.layer_subtract_dict[path.layer] = set() if path.layer not in self.paths_dict: self.paths_dict[path.layer] = dict() if np.abs(qc_thickness) > 0: extruded_entity = gmsh.model.occ.extrude([(2, surface)], dx=0, dy=0, dz=qc_thickness) volume = [tag for dim, tag in extruded_entity if dim == 3] if path["subtract"]: self.layer_subtract_dict[path.layer].add(volume[0]) else: self.paths_dict[path.layer][qc_name] = [volume[0]] else: if path["subtract"]: self.layer_subtract_dict[path.layer].add(surface) else: self.paths_dict[path.layer][qc_name] = [surface]
[docs] def make_poly_surface(self, points: List[np.ndarray], chip_z: float) -> int: """Make a Gmsh surface for creating poly type QGeometries Args: points (List[np.ndarray]): A list of 3D vectors (np.ndarray) defining polygon chip_z (float): z-coordinate of the chip Returns: int: tag of the created Gmsh surface """ lines = [] first_tag = -1 prev_tag = -1 for i, pt in enumerate(points[:-1]): p1 = gmsh.model.occ.addPoint(pt[0], pt[1], chip_z) if i == 0 else prev_tag p2 = first_tag if i == (len(points) - 2) else gmsh.model.occ.addPoint( points[i + 1][0], points[i + 1][1], chip_z) lines += [gmsh.model.occ.addLine(p1, p2)] prev_tag = p2 if i == 0: first_tag = p1 return self.make_general_surface(lines)
[docs] def render_element_poly(self, poly: pd.Series): """Render an element of type: 'poly' Args: poly (pd.Series): Poly to render. """ qc_shapely = poly.geometry qc_thickness, qc_z = self.get_thickness_zcoord_for_layer_datatype( layer_num=poly.layer) vecs = Vec3DArray.make_vec3DArray( self.parse_units_gmsh(list(qc_shapely.exterior.coords)), qc_z) qc_name = self.design._components[ poly["component"]].name + '_' + clean_name(poly["name"]) surface = self.make_poly_surface(vecs.points, qc_z) if len(qc_shapely.interiors) > 0: pts = np.array(list(qc_shapely.interiors[0].coords)) int_vecs = Vec3DArray.make_vec3DArray(pts, qc_z) int_surface = self.make_poly_surface(int_vecs.points, qc_z) surface = gmsh.model.occ.cut([(2, surface)], [(2, int_surface)])[0][0][1] if poly.layer not in self.layer_subtract_dict: self.layer_subtract_dict[poly.layer] = set() if poly.layer not in self.polys_dict: self.polys_dict[poly.layer] = dict() if np.abs(qc_thickness) > 0: extruded_entity = gmsh.model.occ.extrude([(2, surface)], dx=0, dy=0, dz=qc_thickness) volume = [tag for dim, tag in extruded_entity if dim == 3] if poly["subtract"]: self.layer_subtract_dict[poly.layer].add(volume[0]) else: self.polys_dict[poly.layer][qc_name] = [volume[0]] else: if poly["subtract"]: self.layer_subtract_dict[poly.layer].add(surface) else: self.polys_dict[poly.layer][qc_name] = [surface]
[docs] def add_endcaps(self, open_pins: Union[list, None] = None): """Create endcaps (rectangular cutouts) for all pins in the list open_pins and add them to layer_subtract_dict. Each element in open_pins takes on the form (component_name, pin_name) and corresponds to a single pin. Args: open_pins (Union[list, None], optional): List of tuples of pins that are open. Defaults to None. """ open_pins = open_pins if open_pins is not None else [] for comp, pin in open_pins: if comp not in self.design.components: raise ValueError( f"Component '{comp}' not present in current design.") qcomp = self.design.components[comp] qc_layer = int(qcomp.options.layer) if pin not in qcomp.pins: raise ValueError( f"Pin '{pin}' not present in component '{comp}'.") pin_dict = qcomp.pins[pin] width, gap = self.parse_units_gmsh( [pin_dict["width"], pin_dict["gap"]]) mid, normal = self.parse_units_gmsh( pin_dict["middle"]), pin_dict["normal"] qc_thickness, qc_z = self.get_thickness_zcoord_for_layer_datatype( layer_num=qc_layer) rect_mid = mid + normal * gap / 2 rect_vec = np.array([rect_mid[0], rect_mid[1], qc_z]) # Assumption: pins only point in x or y directions # If this assumption is not satisfied, addBox() no longer works # Solution: must draw points, lines, and shapes manually and then extrude if abs(normal[0]) > abs(normal[1]): dx = gap dy = width + 2 * gap rect_x = rect_vec[0] - dx / 2 rect_y = rect_vec[1] - dy / 2 rect_z = rect_vec[2] else: dy = gap dx = width + 2 * gap rect_x = rect_vec[0] - dx / 2 rect_y = rect_vec[1] - dy / 2 rect_z = rect_vec[2] if np.abs(qc_thickness) > 0: endcap = gmsh.model.occ.addBox(x=rect_x, y=rect_y, z=rect_z, dx=dx, dy=dy, dz=qc_thickness) else: endcap = gmsh.model.occ.addRectangle(x=rect_x, y=rect_y, z=rect_z, dx=dx, dy=dy) self.layer_subtract_dict[qc_layer].add(endcap)
[docs] def render_layers(self, draw_sample_holder: bool, omit_layers: Optional[List[int]] = None, box_plus_buffer: bool = True): """Render all chips of the design. calls `render_chip` to render the actual geometries Args: omit_layers (Optional[List[int]]): List of layers to omit render. Renders all if [] or None is given. Defaults to None. draw_sample_holder (bool): To draw the sample holder box. box_plus_buffer (bool, optional): For adding buffer to chip dimensions. Defaults to True. """ layer_list = list(set(l for l in self.design.ls.ls_df["layer"])) if omit_layers is not None: layer_list = list(l for l in layer_list if l not in omit_layers) for layer in layer_list: # Add the buffer, using options for renderer. x_buff = self.parse_units_gmsh(self._options["x_buffer_width_mm"]) y_buff = self.parse_units_gmsh(self._options["y_buffer_width_mm"]) result = self.bounds_handler.get_bounds_of_path_and_poly_tables( box_plus_buffer, self.qcomp_ids, self.case, x_buff, y_buff) (self.box_xy_bounds, self.path_and_poly_with_valid_comps, self.path_poly_and_junction_valid_comps, self.chip_names_matched, self.valid_chip_names) = result if not self.chip_names_matched: raise ValueError( "The chip names in Qgeometry tables do not match with " "the ones in the layer-stack. Please re-check your design.") self.render_layer(layer) if draw_sample_holder: if "sample_holder_top" in self.design.variables.keys(): p = self.design.variables else: p = self.design._uwave_package vac_height = self.parse_units_gmsh( [p["sample_holder_top"], p["sample_holder_bottom"]]) # This tolerance is needed for Gmsh to not cut # the vacuum_box into two separate volumes when the # substrate volume is subtracted from it tol = self.parse_units_gmsh("1um") x = self.box_xy_bounds[0] - tol y = self.box_xy_bounds[1] - tol z = -vac_height[1] dx = (self.box_xy_bounds[2] - self.box_xy_bounds[0]) + 2 * tol dy = (self.box_xy_bounds[3] - self.box_xy_bounds[1]) + 2 * tol dz = sum(vac_height) self.vacuum_box = gmsh.model.occ.addBox(x, y, z, dx, dy, dz)
[docs] def render_layer(self, layer_number: int, datatype: int = 0): """Render the given layer number and datatype. Args: layer_number (int): number of the layer to render datatype (int): number of the datatype. Defaults to 0. Raises: ValueError: if the required properties are not found in the layer-stack """ thickness, z_coord = self.get_thickness_zcoord_for_layer_datatype( layer_num=layer_number, datatype=datatype) layer_x, layer_y = self.box_xy_bounds[0:2] layer_wx = (self.box_xy_bounds[2] - self.box_xy_bounds[0]) layer_wy = (self.box_xy_bounds[3] - self.box_xy_bounds[1]) # Check if thickness == 0, then draw a rectangle instead if np.abs(thickness) > 0: layer_tag = gmsh.model.occ.addBox(layer_x, layer_y, z_coord, layer_wx, layer_wy, thickness) else: layer_tag = gmsh.model.occ.addRectangle(layer_x, layer_y, z_coord, layer_wx, layer_wy) if layer_number not in self.layers_dict: self.layers_dict[layer_number] = [-1] self.layers_dict[layer_number] = [layer_tag]
[docs] def subtract_from_layers(self, omit_layers: Optional[list[int]] = None): """Subtract the QGeometries in tables from the chip ground plane Args: omit_layers (Optional[List[int]]): List of layers to omit render. Renders all if [] or None is given. Defaults to None. """ for layer_num, shapes in self.layer_subtract_dict.items(): if omit_layers is not None and layer_num in omit_layers: continue thickness = self.get_thickness_for_layer_datatype( layer_num=layer_num) # Check if thickness == 0, then subtract with dim=2 dim = 3 if np.abs(thickness) > 0 else 2 shape_dim_tags = [(dim, s) for s in shapes] layer_dim_tag = (dim, self.layers_dict[layer_num][0]) tool_dimtags = [layer_dim_tag] if len(shape_dim_tags) > 0: subtract_layer = gmsh.model.occ.cut(tool_dimtags, shape_dim_tags) updated_layer_geoms = [] for i in range(len(tool_dimtags)): if len(subtract_layer[1][i]) > 0: updated_layer_geoms += [ tag for _, tag in subtract_layer[1][i] ] self.layers_dict[layer_num] = updated_layer_geoms
[docs] def fragment_interfaces(self, draw_sample_holder: bool): """Fragment Gmsh surfaces to ensure consistent tetrahedral meshing across interfaces between different materials. Args: draw_sample_holder (bool): To draw the sample holder box. """ all_geom_dimtags = list() all_layer_geoms = defaultdict(dict) all_dicts = (self.paths_dict, self.polys_dict) for d in all_dicts: for layer, geoms in d.items(): if layer not in all_layer_geoms: all_layer_geoms[layer] = dict() all_layer_geoms[layer].update(geoms) for layer, geom_id in self.layers_dict.items(): if layer not in all_layer_geoms: all_layer_geoms[layer] = dict() layer_type = "ground" if layer in self.layer_types[ "metal"] else "dielectric" all_layer_geoms[layer].update( {f"{layer_type}_layer_{layer}": geom_id}) for layer, geoms in all_layer_geoms.items(): # Check if thickness == 0, then fragment differently thickness = self.get_thickness_for_layer_datatype(layer_num=layer) geom_dim = 3 if np.abs(thickness) > 0 else 2 for _, geom_ids in geoms.items(): all_geom_dimtags += [(geom_dim, id) for id in geom_ids] for _, geoms in self.juncs_dict.items(): for _, jj_sfs in geoms.items(): all_geom_dimtags += [(2, jj) for jj in jj_sfs] if draw_sample_holder: object_dimtag = (3, self.vacuum_box) all_layer_geoms[-1] = dict(vacuum_box=[self.vacuum_box]) fragmented_geoms = gmsh.model.occ.fragment([object_dimtag], all_geom_dimtags) # Extract the new vacuum_box volume self.vacuum_box = fragmented_geoms[1][0][0][1] object_dimtag = (3, self.vacuum_box) else: # Get one of the dim=3 objects dim3_dimtag = [ (dim, tag) for dim, tag in all_geom_dimtags if dim == 3 ] object_dimtag = dim3_dimtag[0] if len( dim3_dimtag) > 0 else all_geom_dimtags[0] all_geom_dimtags.remove(object_dimtag) fragmented_geoms = gmsh.model.occ.fragment([object_dimtag], all_geom_dimtags) updated_geoms = fragmented_geoms[0] insert_idx = updated_geoms.index(object_dimtag) all_geom_dimtags.insert(insert_idx, object_dimtag) all_dicts = { 0: self.paths_dict, 1: self.polys_dict, 2: self.juncs_dict, 3: self.layers_dict } for old, new in zip(all_geom_dimtags, updated_geoms): if old != new: for i, d in all_dicts.items(): for l, geoms in d.items(): if isinstance(geoms, dict): for name, geom_id in geoms.items(): if len(geom_id) > 0 and geom_id[0] == old[1]: all_dicts[i][l][name].append(new[1]) all_dicts[i][l][name].remove(old[1]) elif isinstance(geoms, list): for geom_id in geoms: if geom_id == old[1]: all_dicts[i][l].append(new[1]) all_dicts[i][l].remove(old[1])
# TODO: Do we require 3D junctions? Active issue: #842 # all_juncs = [] # for layer_juncs in self.juncs_dict.values(): # for _, surf in layer_juncs.items(): # all_juncs += surf # junc_dimtags = [(2, junc) for junc in all_juncs] # gmsh.model.occ.fragment([(3, self.vacuum_box)], junc_dimtags)
[docs] def get_all_metal_surfaces(self): metal_geoms = list() surf_tags = list() for layer in self.layer_types["metal"]: thickness = self.get_thickness_for_layer_datatype(layer_num=layer) for _, tag in self.juncs_dict[layer].items(): surf_tags += tag if thickness > 0.0: for _, tag in self.polys_dict[layer].items(): metal_geoms += tag for _, tag in self.paths_dict[layer].items(): metal_geoms += tag metal_geoms += self.layers_dict[layer] else: for _, tag in self.polys_dict[layer].items(): surf_tags += tag for _, tag in self.paths_dict[layer].items(): surf_tags += tag surf_tags += self.layers_dict[layer] for geom in metal_geoms: surf_tags += list(gmsh.model.occ.getSurfaceLoops(geom)[1][0]) return surf_tags
[docs] def assign_physical_groups(self, ignore_metal_volume: bool, draw_sample_holder: bool): """Assign physical groups to classify different geometries physically. Args: ignore_metal_volume (bool, optional): ignore the volume of metals and replace it with a list of surfaces instead. draw_sample_holder (bool): To draw the sample holder box. Raises: ValueError: if self.layer_types is not a dict ValueError: if layer number is not in self.layer_types """ layer_numbers = list(set(l for l in self.design.ls.ls_df["layer"])) for layer in layer_numbers: # TODO: check if thickness == 0, then fragment differently layer_thickness = self.get_thickness_for_layer_datatype( layer_num=layer) layer_dim = 3 if np.abs(layer_thickness) > 0 else 2 if layer not in self.physical_groups: self.physical_groups[layer] = dict() # Check if a component is drawn on that layer valid_layers = set( list(self.paths_dict.keys()) + list(self.polys_dict.keys())) if layer in valid_layers: # Make physical groups for components layer_geoms = dict(self.paths_dict[layer], **self.polys_dict[layer]) for name, tag in layer_geoms.items(): if layer_dim == 3: tags = gmsh.model.occ.getSurfaceLoops(tag[0])[1][0] metal_layer = True if layer in self.layer_types[ "metal"] else False if not metal_layer or (metal_layer and not ignore_metal_volume): ph_vol_tag = gmsh.model.addPhysicalGroup( dim=layer_dim, tags=tag, name=name) self.physical_groups[layer][name] = ph_vol_tag ph_sfs_tag = gmsh.model.addPhysicalGroup( dim=2, tags=tags, name=f"{name}_sfs") self.physical_groups[layer][f"{name}_sfs"] = ph_sfs_tag else: ph_tag = gmsh.model.addPhysicalGroup(dim=layer_dim, tags=tag, name=name) self.physical_groups[layer][name] = ph_tag # TODO: Do we require 3D junctions? Active issue: #842 for name, tag in self.juncs_dict[layer].items(): ph_junc_tag = gmsh.model.addPhysicalGroup(dim=2, tags=tag, name=name) self.physical_groups[layer][name] = ph_junc_tag # Make physical groups for each layer if self.layer_types is None: raise ValueError( f"Expected `self.layer_types` to be a dict, found {type(self.layer_types)}" ) else: if layer in self.layer_types["metal"]: layer_type = "ground_plane" elif layer in self.layer_types["dielectric"]: layer_type = "dielectric" else: raise ValueError( "Layer number not in the specified `self.layer_types` dict." ) layer_name = layer_type + f'_(layer {layer})' layer_tag = self.layers_dict[layer] all_metal_surfs = self.get_all_metal_surfaces() if len(layer_tag) > 0: if layer_dim == 3: layer_sfs_tags = [] for vol in layer_tag: layer_sfs = list( gmsh.model.occ.getSurfaceLoops(vol)[1][0]) if layer_type == "ground_plane": layer_sfs_tags += layer_sfs else: layer_sfs_tags += [ sf for sf in layer_sfs if sf not in all_metal_surfs ] if layer_type != "ground_plane" or ( layer_type == "ground_plane" and not ignore_metal_volume): ph_vol_tag = gmsh.model.addPhysicalGroup( dim=layer_dim, tags=layer_tag, name=layer_name) self.physical_groups[layer][layer_name] = ph_vol_tag ph_sfs_tag = gmsh.model.addPhysicalGroup( dim=2, tags=layer_sfs_tags, name=f"{layer_name}_sfs") self.physical_groups[layer][ f"{layer_name}_sfs"] = ph_sfs_tag else: ph_tag = gmsh.model.addPhysicalGroup(dim=layer_dim, tags=layer_tag, name=layer_name) self.physical_groups[layer][layer_name] = ph_tag if draw_sample_holder: # Make physical groups for vacuum box (volume) vb_name = "vacuum_box" ph_vb_tag = gmsh.model.addPhysicalGroup(dim=3, tags=[self.vacuum_box], name=vb_name) self.physical_groups["global"][vb_name] = ph_vb_tag # Make physical groups for vacuum box (surfaces) vb_sfs = list(gmsh.model.occ.getSurfaceLoops(self.vacuum_box)[1][0]) ph_vb_sfs_tag = gmsh.model.addPhysicalGroup(dim=2, tags=vb_sfs, name=(vb_name + "_sfs")) self.physical_groups["global"][vb_name + "_sfs"] = ph_vb_sfs_tag
[docs] def isometric_projection(self): """Set the view in Gmsh to isometric view manually. """ gmsh.option.setNumber("General.Trackball", 0) gmsh.option.setNumber("General.RotationX", -np.degrees(np.arcsin(np.tan(np.pi / 6)))) gmsh.option.setNumber("General.RotationY", 0) gmsh.option.setNumber("General.RotationZ", -45)
[docs] def define_mesh_size_fields(self): """Define size fields for mesh varying the mesh density across the design. """ min_mesh_size = self.parse_units_gmsh(self._options["mesh"]["min_size"]) max_mesh_size = self.parse_units_gmsh(self._options["mesh"]["max_size"]) min_mesh_size_jj = self.parse_units_gmsh( self._options["mesh"]["max_size_jj"]) grad_delta = self.parse_units_gmsh( self._options["mesh"]["mesh_size_fields"]["gradient_delta"]) dist_min = self.parse_units_gmsh( self._options["mesh"]["mesh_size_fields"] ["min_distance_from_edges"]) dist_max = self.parse_units_gmsh( self._options["mesh"]["mesh_size_fields"] ["max_distance_from_edges"]) dist_delta = self.parse_units_gmsh( self._options["mesh"]["mesh_size_fields"]["distance_delta"]) grad_steps = int((dist_max - dist_min) / dist_delta) all_vols = [] all_surfs = [] all_dicts = (self.polys_dict, self.paths_dict) for d in all_dicts: for layer, geoms in d.items(): thickness = self.get_thickness_for_layer_datatype(layer) if np.abs(thickness) > 0: all_vols += [tag[0] for tag in geoms.values()] else: all_surfs += [tag[0] for tag in geoms.values()] # Metal layers for layer in self.layer_types["metal"]: thickness = self.get_thickness_for_layer_datatype(layer) if np.abs(thickness) > 0: all_vols += self.layers_dict[layer] else: all_surfs += self.layers_dict[layer] for vol in all_vols: all_surfs += [ surf for surf in gmsh.model.occ.getSurfaceLoops(vol)[1][0] ] curve_loops = [gmsh.model.occ.getCurveLoops(surf) for surf in all_surfs] curves = [] for cl in curve_loops: for curve_tag_list in cl[1]: # extract curves for curve in curve_tag_list: curves += [curve] thresh_fields = [] df = gmsh.model.mesh.field.add("Distance") gmsh.model.mesh.field.setNumbers(df, "CurvesList", curves) gmsh.model.mesh.field.setNumber(df, "NumPointsPerCurve", 100) for i in range(grad_steps): tf = gmsh.model.mesh.field.add("Threshold") gmsh.model.mesh.field.setNumber(tf, "DistMin", dist_min) gmsh.model.mesh.field.setNumber( tf, "DistMax", dist_max - ((grad_steps - i - 1) * dist_delta)) gmsh.model.mesh.field.setNumber(tf, "Sigmoid", 1) gmsh.model.mesh.field.setNumber(tf, "InField", df) gmsh.model.mesh.field.setNumber(tf, "SizeMin", (i * grad_delta) + min_mesh_size) gmsh.model.mesh.field.setNumber(tf, "SizeMax", max_mesh_size) thresh_fields += [tf] jj_surfs = [] for _, geoms in self.juncs_dict.items(): jj_surfs += [tag[0] for tag in geoms.values()] jj_curve_loops = [ gmsh.model.occ.getCurveLoops(surf) for surf in all_surfs ] jj_curves = [] for cl in jj_curve_loops: for curve_tag_list in cl[1]: # extract curves for curve in curve_tag_list: jj_curves += [curve] jj_df = gmsh.model.mesh.field.add("Distance") gmsh.model.mesh.field.setNumbers(df, "CurvesList", jj_curves) gmsh.model.mesh.field.setNumber(df, "NumPointsPerCurve", 100) jj_tf = gmsh.model.mesh.field.add("Threshold") gmsh.model.mesh.field.setNumber(jj_tf, "DistMin", dist_min) gmsh.model.mesh.field.setNumber(jj_tf, "DistMax", dist_max) gmsh.model.mesh.field.setNumber(jj_tf, "Sigmoid", 1) gmsh.model.mesh.field.setNumber(jj_tf, "InField", jj_df) gmsh.model.mesh.field.setNumber(jj_tf, "SizeMin", min_mesh_size_jj) gmsh.model.mesh.field.setNumber(jj_tf, "SizeMax", max_mesh_size) thresh_fields += [jj_tf] min_field = gmsh.model.mesh.field.add("Min") gmsh.model.mesh.field.setNumbers(min_field, "FieldsList", thresh_fields) gmsh.model.mesh.field.setAsBackgroundMesh(min_field) gmsh.option.setNumber("Mesh.MeshSizeExtendFromBoundary", 0)
[docs] def define_mesh_properties(self): """Define properties for mesh depending on renderer options. """ min_mesh_size = self.parse_units_gmsh(self._options["mesh"]["min_size"]) max_mesh_size = self.parse_units_gmsh(self._options["mesh"]["max_size"]) gmsh.option.setNumber("Mesh.MeshSizeFromCurvature", self._options["mesh"]["nodes_per_2pi_curve"]) gmsh.option.setNumber("Mesh.Smoothing", self._options["mesh"]["smoothing"]) gmsh.option.setNumber("Mesh.Algorithm3D", self._options["mesh"]["algorithm_3d"]) gmsh.option.setNumber("General.NumThreads", self._options["mesh"]["num_threads"]) gmsh.option.setNumber("Mesh.MeshSizeMin", min_mesh_size) gmsh.option.setNumber("Mesh.MeshSizeMax", max_mesh_size)
[docs] def add_mesh(self, dim: int = 3, intelli_mesh: bool = True, custom_mesh_fn: callable = None): """Generate mesh for all geometries. Args: dim (int, optional): Specify the dimension of mesh. Defaults to 3. intelli_mesh (bool): Set to mesh the geometries intelligently. True by default. custom_mesh_fn (callable): Custom meshing function specifying mesh size fields using Gmsh python script (for advanced users only) """ if intelli_mesh: if custom_mesh_fn is None: self.define_mesh_size_fields() else: self.logger.info("Applying custom meshing function...") custom_mesh_fn() self.define_mesh_properties() gmsh.model.mesh.generate(dim=dim) self.assign_mesh_color()
[docs] def assign_mesh_color(self): """Assign mesh color according to the type of layer specified by self.layer_types and colors taken from self._options as provided by the user. """ color_dict = lambda color: dict( r=color[0], g=color[1], b=color[2], a=color[3]) metal_color = color_dict(self._options["colors"]["metal"]) jj_color = color_dict(self._options["colors"]["jj"]) dielectric_color = color_dict(self._options["colors"]["dielectric"]) valid_layers = set( list(self.paths_dict.keys()) + list(self.polys_dict.keys())) # Assign mesh color to dielectric layer for layer in list(self.layers_dict.keys()): if layer not in self.layer_types["dielectric"]: continue thickness = self.get_thickness_for_layer_datatype(layer) layer_tags = self.layers_dict[layer] layer_dim = 3 if np.abs(thickness) > 0 else 2 if layer_dim == 3: layer_sfs = [] for vol in layer_tags: layer_sfs += list(gmsh.model.occ.getSurfaceLoops(vol)[1][0]) gmsh.model.setColor([(3, tag) for tag in layer_tags], **dielectric_color) else: layer_sfs = layer_tags gmsh.model.setColor([(2, sf) for sf in layer_sfs], **dielectric_color) # Assign colors to geometries and metal (ground plane) layers for layer in list(self.layers_dict.keys()): if layer in self.layer_types["dielectric"]: continue thickness = self.get_thickness_for_layer_datatype(layer) if layer in valid_layers: metal_vols = [] metal_surfs = [] metal_dicts = (self.polys_dict[layer], self.paths_dict[layer]) # Component geomtries for d in metal_dicts: for _, geoms in d.items(): if np.abs(thickness) > 0: metal_vols += geoms else: metal_surfs += geoms # Metal layers if np.abs(thickness) > 0: metal_vols += self.layers_dict[layer] else: metal_surfs += self.layers_dict[layer] for vol in metal_vols: metal_surfs += [ surf for surf in gmsh.model.occ.getSurfaceLoops(vol)[1][0] ] if len(metal_vols) > 0: gmsh.model.setColor([(3, metal) for metal in metal_vols], **metal_color) gmsh.model.setColor([(2, metal) for metal in metal_surfs], **metal_color) # Junctions jj_surfs = [] for _, surf in self.juncs_dict[layer].items(): jj_surfs += surf gmsh.model.setColor([(2, jj) for jj in jj_surfs], **jj_color)
[docs] def launch_gui(self): """Launch Gmsh GUI for viewing the model. """ self.isometric_projection() # set isometric projection try: gmsh.fltk.run() except Exception: self.logger.info( "Encountered an error while launching the Gmsh GUI. Retrying to launch the GUI..." ) gmsh.fltk.run()
[docs] def export_mesh(self, filepath: str, scaling_factor: float = 1e-3): """Export mesh from Gmsh into a file. Supported formats: (.msh, .msh2, .mesh). Args: filepath (str): path of the file to export mesh to. scaling_factor (float): specify a scaling factor for the mesh. Defaults to 1e-3. """ valid_file_exts = ["msh", "msh2", "mesh"] file_ext = filepath.split(".")[-1] if file_ext not in valid_file_exts: self.logger.error( "RENDERER ERROR: filename needs to have a .msh extension. Exporting failed." ) return import os from pathlib import Path par_dir = Path(filepath).parent.absolute() if not os.path.exists(par_dir): raise ValueError(f"Directory not found: {par_dir}") gmsh.option.setNumber("Mesh.ScalingFactor", scaling_factor) gmsh.write(filepath)
[docs] def export_geo_unrolled(self, filepath: str): """Export the Gmsh geometry as geo_unrolled file. Supported formats: .geo_unrolled Args: filepath (str): path of the file to export geometry to """ valid_file_exts = ["geo_unrolled"] file_ext = filepath.split(".")[-1] if file_ext not in valid_file_exts: self.logger.error( "RENDERER ERROR: filename needs to have a .geo_unrolled extension. Exporting failed." ) return import os from pathlib import Path par_dir = Path(filepath).parent.absolute() if not os.path.exists(par_dir): raise ValueError(f"Directory not found: {par_dir}") has_mesh = False if len(gmsh.model.mesh.field.list()) == 0 else True if has_mesh: self.logger.warning( "WARNING: The existing model contains mesh size field definitions, " "which will show up in your exported .geo_unrolled file. If " "you aren't explicitly handling the mesh size fields, we recommend " "to export the geometry before generating the mesh in your design as " "it might interfere with your .geo_unrolled file imports.") gmsh.write(filepath) # Prepend "SetFactory("OpenCASCADE");" in the exported file line = 'SetFactory("OpenCASCADE");' with open(filepath, 'r+') as f: content = f.read() f.seek(0, 0) f.write(line.rstrip('\r\n') + '\n' + content)
[docs] def import_post_processing_data(self, filename: str, launch_gui: bool = True, close_gmsh_on_closing_gui: bool = False): """Import the post processing data for visualization in Gmsh. Args: filename (str): a target file ending with '.msh' extension launch_gui (bool): launch the Gmsh GUI. Defaults to True. close_gmsh_on_closing_gui (bool): finalize gmsh when the GUI is closed. Defaults to True. Raises: ValueError: raises when the input file isn't a .msh file """ if ".msh" not in filename: raise ValueError( "Only .msh files supported for post processing views.") self.model = "post_processing" gmsh.open(filename) if launch_gui: self.launch_gui() if close_gmsh_on_closing_gui: self.close()
[docs] def save_screenshot(self, path: str = None, show: bool = True): """Save the screenshot. Args: path (str, optional): Path to save location. Defaults to None. show (bool, optional): Whether or not to display the screenshot. Defaults to True. """ valid_file_exts = ["jpg", "png", "gif", "bmp"] file_ext = path.split(".")[-1] if file_ext not in valid_file_exts: self.logger.error( f"Expected png, jpg, bmp, or gif format, got .{path.split('.')[-1]}." ) # FIXME: This doesn't work right now!!! Active issue: #843 # There is no method in Gmsh python wrapper to give # the 'Print' command which can provide screenshot feature. raise NotImplementedError("""This feature is pending and depends on Gmsh general command 'Print' being available through the API.""" )
[docs] def render_component(self, component): pass