@@ -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+
234336def 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
415516def 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
464571def set_meshing_options (mesh_field_ids : list [int ], max_size : float , n_threads : int ) -> None :
0 commit comments