Skip to content

Commit

Permalink
Implement outer BCs for 2D simulation
Browse files Browse the repository at this point in the history
In this commit boundary condition are implemented for 2D electrostatic simulation.
One can give "potential" value for `xmin`, `xmax`, `ymin` and `ymax`
boundaries.

This feature is demonstrated in
`klayout_package/python/scripts/simulations/xsection_cull_with_boundaries.py`
  • Loading branch information
ettaka committed Oct 9, 2023
1 parent 9dd1329 commit 5eed17a
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ def export_elmer_json(simulation,
p_element_order=1,
frequency=5,
mesh_size=None,
boundary_conditions=None,
workflow=None,
percent_error=0.005,
max_error_scale=2,
Expand All @@ -72,6 +73,7 @@ def export_elmer_json(simulation,
p_element_order(int): polynomial order of p-elements (Default: 1)
frequency: Units are in GHz. To set up multifrequency analysis, use list of numbers.
mesh_size(dict): Parameters to determine mesh element sizes
boundary_conditions(dict): Parameters to determine boundary conditions
workflow(dict): Parameters for simulation workflow
percent_error(float): Stopping criterion in adaptive meshing.
max_error_scale(float): Maximum element error, relative to percent_error, allowed in individual elements.
Expand Down Expand Up @@ -113,6 +115,7 @@ def export_elmer_json(simulation,
**simulation.get_simulation_data(),
**({'layers': {k: (v.layer, v.datatype) for k, v in layers.items()}} if is_cross_section else {}),
'mesh_size': {} if mesh_size is None else mesh_size,
'boundary conditions': boundary_conditions,
'workflow': {} if workflow is None else workflow,
'percent_error': percent_error,
'max_error_scale': max_error_scale,
Expand Down Expand Up @@ -416,6 +419,7 @@ def export_elmer(simulations: Sequence[Simulation],
file_prefix='simulation',
script_file='scripts/run.py',
mesh_size=None,
boundary_conditions=None,
workflow=None,
percent_error=0.005,
max_error_scale=2,
Expand All @@ -439,6 +443,7 @@ def export_elmer(simulations: Sequence[Simulation],
file_prefix: File prefix of the script file to be created.
script_file: Name of the script file to run.
mesh_size(dict): Parameters to determine mesh element sizes
boundary_conditions(dict): Parameters to determine boundary conditions
workflow(dict): Parameters for simulation workflow
percent_error(float): Stopping criterion in adaptive meshing.
max_error_scale(float): Maximum element error, relative to percent_error, allowed in individual elements.
Expand Down Expand Up @@ -500,6 +505,7 @@ def export_elmer(simulations: Sequence[Simulation],
p_element_order=p_element_order,
frequency=frequency,
mesh_size=mesh_size,
boundary_conditions=boundary_conditions,
workflow=workflow,
percent_error=percent_error,
max_error_scale=max_error_scale,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ def produce_cross_section_mesh(json_data, msh_file):

# Create mesh using geometries in gds file
gmsh.model.add("cross_section")

dim_tags = {}
for name, num in layers.items():
reg = pya.Region(cell.shapes(layout.layer(*num))) & bbox
Expand Down Expand Up @@ -152,6 +153,8 @@ def produce_cross_section_mesh(json_data, msh_file):
metal_boundary = gmsh.model.getBoundary(new_tags[n], combined=False, oriented=False, recursive=False)
gmsh.model.addPhysicalGroup(1, [t[1] for t in metal_boundary], name=f'{n}_boundary')

set_outer_bcs(bbox, layout)

# Generate and save mesh
gmsh.model.mesh.generate(2)
gmsh.write(str(msh_file))
Expand All @@ -162,6 +165,55 @@ def produce_cross_section_mesh(json_data, msh_file):

gmsh.finalize()

def set_outer_bcs(bbox, layout, beps=1e-6):
"""
Sets the outer boundaries so that `xmin`, `xmax`, `ymin` and `ymax` can be accessed as physical groups.
This is a desperate attempt because occ module seems buggy: tried to new draw lines and add them to the
fragment then fetch the correct `dim_tags`, but the fragment breaks down. So, now we search each boundary
using a bounding box search.
Args:
bbox(pya.DBox): bounding box in klayout format
layout(pya.Layout): klayout layout
beps(float): tolerance for the search bounding box
"""
outer_bc_dim_tags = {}
outer_bc_dim_tags['xmin'] = gmsh.model.occ.getEntitiesInBoundingBox(
bbox.p1.x * layout.dbu-beps,
bbox.p1.y * layout.dbu-beps,
-beps,
bbox.p1.x * layout.dbu+beps,
bbox.p2.y * layout.dbu+beps,
beps,
dim=1)
outer_bc_dim_tags['xmax'] = gmsh.model.occ.getEntitiesInBoundingBox(
bbox.p2.x * layout.dbu-beps,
bbox.p1.y * layout.dbu-beps,
-beps,
bbox.p2.x * layout.dbu+beps,
bbox.p2.y * layout.dbu+beps,
beps,
dim=1)
outer_bc_dim_tags['ymin'] = gmsh.model.occ.getEntitiesInBoundingBox(
bbox.p1.x * layout.dbu-beps,
bbox.p1.y * layout.dbu-beps,
-beps,
bbox.p2.x * layout.dbu+beps,
bbox.p1.y * layout.dbu+beps,
beps,
dim=1)
outer_bc_dim_tags['ymax'] = gmsh.model.occ.getEntitiesInBoundingBox(
bbox.p1.x * layout.dbu-beps,
bbox.p2.y * layout.dbu-beps,
-beps,
bbox.p2.x * layout.dbu+beps,
bbox.p2.y * layout.dbu+beps,
beps,
dim=1)

for n, v in outer_bc_dim_tags.items():
gmsh.model.addPhysicalGroup(1, [t[1] for t in v], name=f'{n}_boundary')

def produce_cross_section_sif_files(json_data, folder_path):
"""
Produces sif files required for capacitance and inductance simulations.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -876,6 +876,20 @@ def sif_capacitance(json_data: dict, folder_path: Path, vtu_name: str,
conditions=[f'Capacitance Body = {i}'])
n_boundaries += len(signals)

bc_dict = json_data.get('boundary conditions', None)
if bc_dict is not None:
for bc in ['xmin', 'xmax', 'ymin', 'ymax']:
bc_name = f'{bc}_boundary'
b = bc_dict.get(bc, None)
if b is not None:
if 'potential' in b:
conditions = [f"Potential = {b['potential']}"]
boundary_conditions += sif_boundary_condition(
ordinate= 1 + n_boundaries,
target_boundaries=[bc_name],
conditions=conditions)
n_boundaries += 1

# Add place-holder boundaries (if additional physical groups are given)
other_groups = [n for n in mesh_names if n not in body_list + grounds + signals and not n.startswith('port_')]
for i, s in enumerate(other_groups, 1):
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
# This code is part of KQCircuits
# Copyright (C) 2023 IQM Finland Oy
#
# This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public
# License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied
# warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with this program. If not, see
# https://www.gnu.org/licenses/gpl-3.0.html.
#
# The software distribution should follow IQM trademark policy for open-source software
# (meetiqm.com/developers/osstmpolicy). IQM welcomes contributions to the code. Please see our contribution agreements
# for individuals (meetiqm.com/developers/clas/individual) and organizations (meetiqm.com/developers/clas/organization).

import sys
import logging
import argparse
from pathlib import Path

from kqcircuits.pya_resolver import pya
from kqcircuits.simulations.export.simulation_export import export_simulation_oas
from kqcircuits.simulations.export.elmer.elmer_export import export_elmer
from kqcircuits.simulations.export.xsection.xsection_export import create_xsections_from_simulations, \
separate_signal_layer_shapes
from kqcircuits.simulations.waveguides_sim import WaveGuidesSim
from kqcircuits.util.export_helper import create_or_empty_tmp_directory, get_active_or_new_layout, \
open_with_klayout_or_default_application

parser = argparse.ArgumentParser()

parser.add_argument('--flip-chip', action="store_true", help='Make a flip chip')
parser.add_argument('--n-guides', nargs="+", default=[1, 2, 3],
type=int, help='Number of waveguides in each simulation')
parser.add_argument('--p-element-order', default=3, type=int, help='Order of p-elements in the FEM computation')
parser.add_argument('--london-penetration-depth', default=0.0, type=float,
help='London penetration depth of superconductor in [m]')
parser.add_argument('--etch-opposite-face', action="store_true", help='If true, the top face metal will be etched away')

args, unknown = parser.parse_known_args()


# This testcase is derived from waveguides_sim_compare.py and
# provides an example of how to use the XSection tool to produce cross section simulations.
#
# Simulation parameters
sim_class = WaveGuidesSim # pylint: disable=invalid-name

multiface = args.flip_chip

path = create_or_empty_tmp_directory(Path(__file__).stem + "_output")

cpw_length = 100
sim_box_height = 1000
sim_parameters = {
'name': 'xsection_cull_with_boundaries',
'box': pya.DBox(pya.DPoint(-cpw_length/2., -sim_box_height/2.), pya.DPoint(cpw_length/2., sim_box_height/2.)),
'cpw_length': cpw_length,
'face_stack': ['1t1', '2b1'] if multiface else ['1t1'],
'etch_opposite_face': args.etch_opposite_face,
'n_guides': 1,
}

boundary_conditions = {
'xmin':{
'potential':0
},
'ymax':{
'potential':0
}
}

workflow = {
'run_gmsh': True,
'run_gmsh_gui': True,
'run_elmergrid': True,
'run_elmer': True,
'run_paraview': True, # this is visual view of the results which can be removed to speed up the process
'python_executable': 'python', # use 'kqclib' when using singularity image (you can also put a full path)
'elmer_n_processes': -1, # -1 means all the physical cores
'elmer_n_threads': 1, # number of omp threads
}

mesh_size = {
'vacuum': 100,
'b_substrate': 100,
# 'b_signal_1': 1,
# 'b_signal_2': 1,
# 'b_signal_3': 1,
'b_simulation_ground': 4,
'ma_layer': 0.02,
'ms_layer': 0.02,
'sa_layer': 0.02,
't_substrate': 100,
't_simulation_ground': 4,
}

# Get layout
logging.basicConfig(level=logging.WARN, stream=sys.stdout)
layout = get_active_or_new_layout()

simulations = [sim_class(layout, **sim_parameters)]

for simulation in simulations:
separate_signal_layer_shapes(simulation)

# Create cross sections using xsection tool
# Oxide layer permittivity and thickness values same as in double_pads_sim.py simulation
xsection_simulations = create_xsections_from_simulations(simulations, path,
(pya.DPoint(0, -8), pya.DPoint(0, -2)), # Cut coordinates
ma_permittivity=8.0,
ms_permittivity=11.4,
sa_permittivity=4.0,
ma_thickness=0.0048,
ms_thickness=0.0003,
sa_thickness=0.0024,
magnification_order=3, # Zoom to nanometers due to thin oxide layers
london_penetration_depth=args.london_penetration_depth,
vertical_cull=(-3,3),
)
open_with_klayout_or_default_application(export_simulation_oas(xsection_simulations, path))
export_elmer(xsection_simulations,
path,
tool='cross-section',
mesh_size=mesh_size,
boundary_conditions=boundary_conditions,
workflow=workflow,
p_element_order=args.p_element_order,
linear_system_method='mg')

0 comments on commit 5eed17a

Please sign in to comment.