Skip to content

Commit e893f79

Browse files
author
Tuomas Myllari
committed
Implement local mesh refinement in GMSH, trying to remove degenerate mesh elements.
Add Solution parameter `min_mesh_quality` defaulting to 5e-7 which determined a mesh element quality threshold under which elements are refined by redoing the whole mesh with locally modified mesh size fields. Set `mesh_optimizer` default to Netgen in ElmerSolution
1 parent f30cd4c commit e893f79

7 files changed

Lines changed: 126 additions & 19 deletions

File tree

klayout_package/python/kqcircuits/simulations/export/elmer/elmer_solution.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,18 @@ class ElmerSolution(Solution):
4848
For example, if the dictionary is {'substrate*': 10, 'substrate*&vacuum': [2, 5], 'global_max': 100}, then
4949
the maximal mesh element length is 10 inside the substrates and 2 on region which is less than 5 units away
5050
from any substrate-vacuum interface. Outside these regions, the mesh element size can increase up to 100.
51-
mesh_optimizer: Dictionary to determine mesh optimization, or None (default) to ignore optimization. The
52-
dictionary can contain keywords 'method', 'force', 'niter' and 'dimTags'. See Gmsh manual
53-
(gmsh.model.mesh.optimize) for details. The default value for 'method' is 'Netgen'.
51+
mesh_optimizer: Dictionary to determine mesh optimization, or None to ignore optimization. The dictionary can
52+
contain keywords 'method', 'force', 'niter' and 'dimTags'. See Gmsh manual (gmsh.model.mesh.optimize) for
53+
details. The default value is {'method': 'Netgen'}.
5454
vtu_output: Output vtu files to view fields in Paraview.
5555
Turning this off will make the simulations slightly faster
5656
save_elmer_data: Save the full Elmer model after simulation. This can be used to restart the simulation
5757
or extract result field values as a post-processing step.
58+
min_mesh_quality: If the initial Gmsh mesh contains elements with quality below this limit, a local mesh
59+
refinement around those elements is applied. This causes the whole mesh to be recomputed and can therefore
60+
cause performance issues if set too high. The aim of the remeshing is to prevent fatal errors due
61+
to degenerate elements in Elmer. The default value of 5e-7 is determined experimentally and might need
62+
adjustment. Setting this to 0 disables the feature.
5863
5964
linear_system_method: Method for solving the FEM linear system of equations in Elmer. For iterative methods use
6065
"GCR", "bicgstab" or any other iterative solver mentioned in ElmerSolver manual section 4.3.1.
@@ -90,9 +95,10 @@ class ElmerSolution(Solution):
9095
is_axisymmetric: bool = False
9196
mesh_levels: int = 1
9297
mesh_size: dict = field(default_factory=dict)
93-
mesh_optimizer: dict | None = None
98+
mesh_optimizer: dict | None = field(default_factory=lambda: {"method": "Netgen"})
9499
vtu_output: bool = True
95100
save_elmer_data: bool = False
101+
min_mesh_quality: float = 5e-7
96102

97103
linear_system_method: str = "GCR"
98104
convergence_tolerance: float = 1.0e-9
@@ -118,6 +124,8 @@ def __post_init__(self):
118124
if self.mg_smoothing_iterations is None:
119125
defaults = {"sgs": 1, "wjacobi": 4, "cjacobi": 4, "cg": 8}
120126
object.__setattr__(self, "mg_smoothing_iterations", defaults.get(self.mg_smoother.lower(), 1))
127+
if self.mesh_optimizer == {}:
128+
object.__setattr__(self, "mesh_optimizer", {"method": "Netgen"})
121129

122130

123131
@dataclass(kw_only=True, frozen=True)

klayout_package/python/scripts/simulations/circular_capacitor_c_deembed_sim.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,7 @@
7474
"n": 64,
7575
}
7676

77-
solution = ElmerCapacitanceSolution(
78-
mesh_size=refine_metal_edges(2.0, 0.5),
79-
mesh_optimizer={},
80-
)
77+
solution = ElmerCapacitanceSolution(mesh_size=refine_metal_edges(2.0, 0.5))
8178

8279
# Prepare output directory
8380
dir_path = create_or_empty_tmp_directory(Path(__file__).stem + var_str + "_output")

klayout_package/python/scripts/simulations/circular_capacitor_epr_sim.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@
9797

9898
solution = ElmerEPR3DSolution(
9999
mesh_size=refine_metal_edges(2.0, 0.5),
100-
mesh_optimizer={},
100+
mesh_optimizer={"method": "Netgen", "force": True},
101101
voltage_excitations=voltage_excitations,
102102
)
103103

klayout_package/python/scripts/simulations/circular_transmon_single_island_epr_sim.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,7 @@
5555
}
5656

5757

58-
solution = ElmerEPR3DSolution(
59-
mesh_size=refine_metal_edges(3.0, 0.5),
60-
mesh_optimizer={},
61-
)
58+
solution = ElmerEPR3DSolution(mesh_size=refine_metal_edges(3.0, 0.5))
6259

6360
# Prepare output directory
6461
dir_path = create_or_empty_tmp_directory(Path(__file__).stem + "_output")

klayout_package/python/scripts/simulations/double_pads_epr_sim.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,6 @@
8282
# Set opposite voltages [1, -1] on the islands and smaller voltage 0.1 on coupler
8383
voltage_excitations=[1, 0.1, -1],
8484
mesh_size=refine_metal_edges(5.0, 0.5),
85-
mesh_optimizer={},
8685
p_element_order=2,
8786
)
8887
# Get layout

klayout_package/python/scripts/simulations/elmer/gmsh_helpers.py

Lines changed: 111 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def produce_mesh(json_data: dict[str, Any], msh_file: Path) -> None:
159159
# Set meshing
160160
mesh_size = json_data.get("mesh_size", {})
161161
workflow = json_data.get("workflow", {})
162-
set_meshing(mesh_size, new_tags, workflow)
162+
mesh_field_ids, global_max_mesh_size = set_meshing(mesh_size, new_tags, workflow)
163163

164164
# Remove layers without material
165165
for name, data in layers.items():
@@ -222,6 +222,34 @@ def produce_mesh(json_data: dict[str, Any], msh_file: Path) -> None:
222222
gmsh.model.mesh.generate(3)
223223

224224
optimize_mesh(json_data.get("mesh_optimizer"))
225+
226+
min_quality = json_data["min_mesh_quality"]
227+
if min_quality > 0:
228+
deg_elements = get_elements_by_quality(min_quality)
229+
if len(deg_elements) > 0:
230+
logging.warning(f"Found {len(deg_elements)} poor elements. Starting iterative refinement")
231+
232+
refined_mesh_field_ids = mesh_field_ids.copy()
233+
max_iter = 5
234+
for i in range(max_iter):
235+
print_mesh_quality_metrics()
236+
logging.warning(f"Iterative refinement {i + 1}: Remeshing...")
237+
238+
refined_mesh_field_ids = remesh_with_local_box_refinement(
239+
deg_elements, refined_mesh_field_ids, global_max_mesh_size
240+
)
241+
optimize_mesh(json_data.get("mesh_optimizer"))
242+
243+
deg_elements = get_elements_by_quality(min_quality)
244+
245+
if len(deg_elements) == 0:
246+
logging.warning(f"Iterative refinement {i + 1}: Completed succesfully.")
247+
break
248+
logging.warning(f"Iterative refinement {i + 1}: {len(deg_elements)} poor elements still left")
249+
else:
250+
logging.warning(f"Iterative refinement: Failed to improve the mesh in {max_iter} iterations")
251+
print_mesh_quality_metrics()
252+
225253
gmsh.write(str(msh_file))
226254

227255
# Open mesh viewer
@@ -231,13 +259,86 @@ def produce_mesh(json_data: dict[str, Any], msh_file: Path) -> None:
231259
gmsh.finalize()
232260

233261

262+
def get_element_qualities(element_type: int | None = None) -> dict[int, float]:
263+
"""Get element quality metrics in dictionary format ElementTag -> Quality.
264+
`element_type` can be used to filter by element type, for example tetras=4"""
265+
qualities = {}
266+
elemTypes, elemTags, _ = gmsh.model.mesh.getElements(3)
267+
for etype, tags in zip(elemTypes, elemTags):
268+
if element_type and element_type != etype:
269+
continue
270+
271+
tag_qualities = gmsh.model.mesh.getElementQualities(tags)
272+
qualities.update(dict(zip(tags, tag_qualities)))
273+
274+
return qualities
275+
276+
277+
def get_elements_by_quality(threshold: float, element_type: int | None = None) -> list[int]:
278+
"""Get Tags of Elements with quality below given threshold."""
279+
return sorted([e for e, q in get_element_qualities(element_type).items() if q < threshold])
280+
281+
282+
def remesh_with_local_box_refinement(
283+
refined_elements: list[int],
284+
existing_mesh_fields: list,
285+
global_max: float,
286+
mesh_size_factor: float = 1.0,
287+
) -> list:
288+
"""Applies mesh refinement trying to improve quality of target elements. Mesh is refined in a box around each
289+
target element with a mesh size equal to average edge length of the element.
290+
291+
Args:
292+
refined_elements: List of elements to refine.
293+
existing_mesh_fields: GMSH mesh field list of the current mesh
294+
global_max: Global maximum mesh size
295+
mesh_size_factor: Mesh size in the refinement box relative to the average edge length of the element.
296+
297+
Returns:
298+
Gmsh mesh size fields for the refined mesh (including the original fields)
299+
"""
300+
box_fields = []
301+
for e in refined_elements:
302+
nodeTags = gmsh.model.mesh.getElement(e)[1]
303+
node_coords = [gmsh.model.mesh.getNode(n)[0] for n in nodeTags]
304+
305+
average_dist = sum(coord_dist(p1, p2) for p1, p2 in itertools.combinations(node_coords, 2)) / 6
306+
# Set mesh size inside refinement box as average edge length times a factor
307+
f_box = gmsh.model.mesh.field.add("Box")
308+
mesh_size_inside = mesh_size_factor * average_dist
309+
gmsh.model.mesh.field.setNumber(f_box, "VIn", mesh_size_inside)
310+
gmsh.model.mesh.field.setNumber(f_box, "VOut", global_max)
311+
312+
gmsh.model.mesh.field.setNumber(f_box, "Thickness", (global_max - mesh_size_inside) / 2)
313+
for coord, values in zip(["X", "Y", "Z"], zip(*node_coords)):
314+
gmsh.model.mesh.field.setNumber(f_box, coord + "Min", min(values))
315+
gmsh.model.mesh.field.setNumber(f_box, coord + "Max", max(values))
316+
317+
box_fields.append(f_box)
318+
319+
all_mesh_fields = existing_mesh_fields + box_fields
320+
background_field_id = gmsh.model.mesh.field.add("Min")
321+
gmsh.model.mesh.field.setNumbers(background_field_id, "FieldsList", all_mesh_fields)
322+
gmsh.model.mesh.field.setAsBackgroundMesh(background_field_id)
323+
324+
gmsh.model.mesh.clear()
325+
gmsh.model.mesh.generate(3)
326+
327+
return all_mesh_fields
328+
329+
330+
def print_mesh_quality_metrics(element_type: int | None = None):
331+
"""Prints minimum and average element quality metrics"""
332+
qualities = np.array(list(get_element_qualities(element_type).values()))
333+
logging.warning(f"Min quality: {float(np.min(qualities)):.4g}. Mean quality: {float(np.average(qualities)):.4g}")
334+
335+
234336
def optimize_mesh(mesh_optimizer: dict | None) -> None:
235337
"""Optimize the mesh if the mesh_optimizer is a dictionary. Ignore mesh optimization if mesh_optimizer is None."""
236338
if mesh_optimizer is None:
237339
return
238340
try:
239-
# Try optimizing with Netgen as the default method
240-
gmsh.model.mesh.optimize(**{"method": "Netgen", **mesh_optimizer})
341+
gmsh.model.mesh.optimize(**mesh_optimizer)
241342
except Exception as error: # pylint: disable=broad-except
242343
logging.warning(f"Mesh optimization failed: {error}")
243344

@@ -414,13 +515,18 @@ def get_recursive_children(dim_tags: Iterable[DimTag], include_parent: bool = Fa
414515

415516
def set_meshing(
416517
mesh_size: dict[str, float | list[float]], layer_dts: dict[str, list[DimTag]], workflow: dict[str, Any]
417-
):
518+
) -> tuple[list, float]:
418519
"""Applies mesh refinement and sets meshing options
419520
420521
Args:
421522
mesh_size: Dictionary to determine mesh refinement
422523
layer_dts: dictionary with layer names as keys and lists of dim-tags as values
423524
workflow: Parameters for simulation workflow
525+
526+
Returns:
527+
Tuple of
528+
1. List of mesh field ids used in the refinement
529+
2. global max mesh size
424530
"""
425531
# Find global maximum mesh element length
426532
mesh_global_max_size = mesh_size.pop("global_max", 0)
@@ -459,6 +565,7 @@ def set_meshing(
459565
n_threads_dict = workflow["sbatch_parameters"] if "sbatch_parameters" in workflow else workflow
460566
gmsh_n_threads = int(n_threads_dict.get("gmsh_n_threads", 1))
461567
set_meshing_options(mesh_field_ids, mesh_global_max_size, gmsh_n_threads)
568+
return mesh_field_ids, mesh_global_max_size
462569

463570

464571
def set_meshing_options(mesh_field_ids: list[int], max_size: float, n_threads: int) -> None:

klayout_package/python/scripts/simulations/swissmon_epr_sim.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,6 @@
9292
voltage_excitations=[1.0, 0.01],
9393
# poor mesh for faster computation
9494
mesh_size=refine_metal_edges(5.0, 1.0),
95-
mesh_optimizer={},
9695
),
9796
)
9897

0 commit comments

Comments
 (0)