Build mesh recipes, not meshes¶
A key motivation behind the creation of adapy was to be able to find a way to be able to control the entire design and analyze pipeline related to structural engineering.
A recipe is code: it remeshes the next revision of the model the same way. adapy meshes with gmsh.
Mesh overrides¶
obj.mesh_override holds meshing rules for one object: a function (gs, obj, geom_repr) -> None.
- When: after all objects are added and partitioned against each other, before meshing.
gs.options.Mesh_MeshSizeMaxis the size the mesh is made at. - What: rules on the object's entities,
gs.model_map[obj].entities. Register sizes withgs.add_size_field. To set transfinite or recombine yourself, callgs.skip_default_meshing(obj). - Not: it never meshes. The session meshes all objects at once, so an overridden object conforms to its neighbours.
Two overrides, used in the examples below:
def refine_holes(gs: GmshSession, obj, geom_repr) -> None:
"""Elements 5x finer along every cutout of `obj`, graded back to the mesh size over 3 element lengths."""
curves = boolean_curves(gs, obj) # the curves each cutout left behind
if not curves:
return
size = gs.options.Mesh_MeshSizeMax # the size this mesh is made at
refine_curves(gs, curves, size=size / 5, max_size=size, dist_min=size / 5, dist_max=3 * size)
def half_size(gs: GmshSession, obj, geom_repr) -> None:
"""Half the mesh size on `obj` and its boundary; neighbours grade to it along shared edges."""
size = gs.options.Mesh_MeshSizeMax / 2
field = gs.model.mesh.field
constant = field.add("Constant")
field.setNumber(constant, "VIn", size)
field.setNumbers(constant, "SurfacesList", [tag for dim, tag in gs.model_map[obj].entities if dim == 2])
field.setNumber(constant, "IncludeBoundary", 1)
gs.add_size_field(constant, size)
ada.fem.meshing.overrides also has configurable refine_near_booleans(...) and element_size(...).
Each example below shows the default mesh (left) and the mesh with its overrides (right).
Plate with cutouts¶
pl = ada.Plate("pl1", [(0, 0), (2, 0), (2, 1), (0, 1)], 0.01)
pl.add_boolean(ada.PrimCyl("hole1", (0.5, 0.5, -0.1), (0.5, 0.5, 0.1), 0.15))
pl.add_boolean(ada.PrimCyl("hole2", (1.4, 0.5, -0.1), (1.4, 0.5, 0.1), 0.2))
pl.mesh_override = refine_holes
plate_before = pl.to_fem_obj(0.1, "shell", use_quads=True, use_mesh_override=False)
plate_after = pl.to_fem_obj(0.1, "shell", use_quads=True)
Beam with a web hole¶
bm = ada.Beam("bm", (0, 0, 0), (2, 0, 0), "IPE400")
bm.add_boolean(ada.PrimCyl("web_hole", (1.0, -0.5, 0.0), (1.0, 0.5, 0.0), 0.1))
bm.mesh_override = refine_holes
beam_before = bm.to_fem_obj(0.05, "shell", use_quads=True, use_mesh_override=False)
beam_after = bm.to_fem_obj(0.05, "shell", use_quads=True)
Girder through a deck¶
The deck is split along the girder's web, so the two share every node on that line.
deck = ada.Plate("deck", [(0, 0), (3, 0), (3, 2), (0, 2)], 0.01)
deck.add_boolean(ada.PrimCyl("hole", (2.2, 0.5, -0.1), (2.2, 0.5, 0.1), 0.2))
deck.mesh_override = refine_holes
girder = ada.Beam("girder", (0, 1, 0), (3, 1, 0), "IPE300") # its web crosses the deck along y=1
girder.mesh_override = half_size
p = ada.Part("p") / [deck, girder]
part_before = p.to_fem_obj(0.1, bm_repr="shell", pl_repr="shell", use_quads=True, use_mesh_override=False)
part_after = p.to_fem_obj(0.1, bm_repr="shell", pl_repr="shell", use_quads=True)