|
| 1 | +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 2 | +# SPDX-License-Identifier: Apache-2.0 |
| 3 | + |
| 4 | +#= |
| 5 | +# README |
| 6 | +
|
| 7 | +This Julia script uses Gmsh to create a mesh for a dipole antenna enclosed within a |
| 8 | +spherical boundary. A dipole antenna consists of two equal cylinders (the arms) separated by |
| 9 | +a thin gap. In the thin gap, there is a flat rectangle that connects the cylinder and that |
| 10 | +can is used as a lumped port. |
| 11 | +
|
| 12 | +The generated mesh contains two regions: |
| 13 | +1. A 3D volume region (the space inside the sphere) |
| 14 | +2. A large outer spherical boundary (typically set to "absorbing" boundary conditions) |
| 15 | +
|
| 16 | +## Prerequisites |
| 17 | +
|
| 18 | +This script requires the Gmsh Julia package. If you don't already have it installed, you can |
| 19 | +install it with |
| 20 | +
|
| 21 | +```bash |
| 22 | +julia -e 'using Pkg; Pkg.add("Gmsh")' |
| 23 | +``` |
| 24 | +
|
| 25 | +## How to run |
| 26 | +
|
| 27 | +From this directory, run: |
| 28 | +```bash |
| 29 | +julia -e 'include("mesh.jl"); generate_antenna_mesh(; filename="antenna.msh")' |
| 30 | +``` |
| 31 | +To visualize the mesh in Gmsh's graphical interface, add the `gui=true` parameter: |
| 32 | +```bash |
| 33 | +julia -e 'include("mesh.jl"); generate_antenna_mesh(; filename="antenna.msh", gui=true)' |
| 34 | +``` |
| 35 | +
|
| 36 | +The script will generate a mesh file and print the "attribute" numbers for each region. |
| 37 | +These attributes are needed when configuring Palace simulations. |
| 38 | +=# |
| 39 | + |
| 40 | +using Gmsh: gmsh |
| 41 | + |
| 42 | +""" |
| 43 | + extract_tag(object) |
| 44 | +
|
| 45 | +Extract the Gmsh tag in `object`. |
| 46 | +
|
| 47 | +If `object` contains only one tag, return it as an integer, otherwise, preserve its |
| 48 | +container. |
| 49 | +
|
| 50 | +Most gmsh functions return list of tuples like `[(2, 5), (2, 8), (2, 10), ...]`, where the |
| 51 | +first number is dimensionality and the second is the integer tag associated to that object. |
| 52 | +
|
| 53 | +#### Example |
| 54 | +
|
| 55 | +```jldoctest |
| 56 | +julia> extract_tag((3, 6)) |
| 57 | +6 |
| 58 | +
|
| 59 | +julia> entities = [(2, 5), (2, 8), (2, 10)]; |
| 60 | +
|
| 61 | +julia> extract_tag.(entities) |
| 62 | +[5, 8, 10] |
| 63 | +``` |
| 64 | +""" |
| 65 | +extract_tag(object) = extract_tag(only(object)) |
| 66 | +extract_tag(object::Tuple) = last(object) |
| 67 | +extract_tag(object::Integer) = error("You passed an integer tag directly to `extract_tag`") |
| 68 | + |
| 69 | +# Convenience functions to extract extrema of the bounding box of an entity |
| 70 | +xmin(x::Tuple) = gmsh.model.occ.get_bounding_box(x...)[1] |
| 71 | +ymin(x::Tuple) = gmsh.model.occ.get_bounding_box(x...)[2] |
| 72 | +zmin(x::Tuple) = gmsh.model.occ.get_bounding_box(x...)[3] |
| 73 | +xmax(x::Tuple) = gmsh.model.occ.get_bounding_box(x...)[4] |
| 74 | +ymax(x::Tuple) = gmsh.model.occ.get_bounding_box(x...)[5] |
| 75 | +zmax(x::Tuple) = gmsh.model.occ.get_bounding_box(x...)[6] |
| 76 | + |
| 77 | +""" |
| 78 | + generate_antenna_mesh(; |
| 79 | + filename::AbstractString, |
| 80 | + wavelength::Real=4.0, |
| 81 | + arm_length::Real=wavelength/4, |
| 82 | + arm_radius::Real=arm_length/20, |
| 83 | + gap_size::Real=arm_length/100, |
| 84 | + outer_boundary_radius::Real=1.5wavelength, |
| 85 | + verbose::Integer=5, |
| 86 | + gui::Bool=false |
| 87 | + ) |
| 88 | +
|
| 89 | +Generate a mesh for a dipole antenna using Gmsh. |
| 90 | +
|
| 91 | +# Arguments |
| 92 | +
|
| 93 | + - filename - output mesh filename |
| 94 | + - wavelength - wavelength of the resulting electromagnetic wave |
| 95 | + - arm_length - length of each antenna arm |
| 96 | + - arm_radius - radius of the cylindrical antenna arms |
| 97 | + - gap_size - size of the gap between the two arms (port region) |
| 98 | + - outer_boundary_radius - radius of the outer spherical boundary |
| 99 | + - verbose - gmsh verbosity level (0-5, higher = more verbose) |
| 100 | + - gui - whether to launch the Gmsh GUI after mesh generation |
| 101 | +""" |
| 102 | +function generate_antenna_mesh(; |
| 103 | + filename::AbstractString, |
| 104 | + wavelength::Real=4.0, |
| 105 | + arm_length::Real=wavelength/4, |
| 106 | + arm_radius::Real=arm_length/20, |
| 107 | + gap_size::Real=arm_length/100, |
| 108 | + outer_boundary_radius::Real=1.5wavelength, |
| 109 | + verbose::Integer=5, |
| 110 | + gui::Bool=false |
| 111 | +) |
| 112 | + # We will create this mesh with a simple approach. We create the 3D |
| 113 | + # sphere only, which produces: |
| 114 | + # - 1 3D entity (the domain) |
| 115 | + # - 1 2D entity (the outer boundary) |
| 116 | + # |
| 117 | + # After creating, we add them to the correct gmsh physical groups. Finally, we |
| 118 | + # control mesh size with a mesh size field and generate the mesh. |
| 119 | + |
| 120 | + # Boilerplate |
| 121 | + gmsh.initialize() |
| 122 | + kernel = gmsh.model.occ |
| 123 | + gmsh.option.setNumber("General.Verbosity", verbose) |
| 124 | + |
| 125 | + # Create a new model. The name dipole is not important. If a model was already added, |
| 126 | + # remove it first (this is useful when interactively evaluating the body of this |
| 127 | + # function in the REPL). |
| 128 | + if "dipole" in gmsh.model.list() |
| 129 | + gmsh.model.setCurrent("dipole") |
| 130 | + gmsh.model.remove() |
| 131 | + end |
| 132 | + gmsh.model.add("dipole") |
| 133 | + |
| 134 | + # Mesh refinement parameter: controls elements around cylinder circumference. |
| 135 | + # Higher number = higher resolution. |
| 136 | + n_circle = 12 |
| 137 | + # How many elements per wavelength on the outer sphere. |
| 138 | + # Higher number = higher resolution. |
| 139 | + n_farfield = 3 |
| 140 | + |
| 141 | + # Create geometry |
| 142 | + outer_boundary = kernel.addSphere(0, 0, 0, outer_boundary_radius) |
| 143 | + |
| 144 | + # Synchronize CAD operations with Gmsh model. |
| 145 | + kernel.synchronize() |
| 146 | + |
| 147 | + # Helper functions to identify the various components. |
| 148 | + all_2d_entities = kernel.getEntities(2) |
| 149 | + all_3d_entities = kernel.getEntities(3) |
| 150 | + |
| 151 | + # For a simple sphere, there should be exactly one 2D and one 3D entity. |
| 152 | + outer_sphere_dimtags = all_2d_entities |
| 153 | + domain_dimtags = all_3d_entities |
| 154 | + |
| 155 | + # Verify we found the expected number of entities. |
| 156 | + @assert length(outer_sphere_dimtags) == 1 # Single outer boundary |
| 157 | + @assert length(domain_dimtags) == 1 # Single 3D domain |
| 158 | + |
| 159 | + # Create physical groups (these become attributes in Palace). |
| 160 | + outer_boundary_group = gmsh.model.addPhysicalGroup( |
| 161 | + 2, |
| 162 | + extract_tag.(outer_sphere_dimtags), |
| 163 | + -1, |
| 164 | + "outer_boundary" |
| 165 | + ) |
| 166 | + domain_group = |
| 167 | + gmsh.model.addPhysicalGroup(3, extract_tag.(domain_dimtags), -1, "domain") |
| 168 | + |
| 169 | + # Set mesh size parameters. |
| 170 | + gmsh.option.setNumber("Mesh.MeshSizeMin", 2.0 * pi * arm_radius / n_circle / 2.0) |
| 171 | + gmsh.option.setNumber("Mesh.MeshSizeMax", wavelength / n_farfield) |
| 172 | + # Set minimum number of elements per 2π radians of curvature. |
| 173 | + gmsh.option.setNumber("Mesh.MeshSizeFromCurvature", n_circle) |
| 174 | + # Don't extend mesh size constraints from boundaries into the volume. |
| 175 | + # This option is typically activated when working with mesh size fields. |
| 176 | + gmsh.option.setNumber("Mesh.MeshSizeExtendFromBoundary", 0) |
| 177 | + |
| 178 | + # Finally, we control mesh size using a mesh size field. |
| 179 | + |
| 180 | + # Create a simple distance field from the center for mesh sizing. |
| 181 | + gmsh.model.mesh.field.add("Distance", 1) |
| 182 | + gmsh.model.mesh.field.setNumbers(1, "PointsList", []) # No specific points |
| 183 | + |
| 184 | + # Use a Box field to control mesh size - finer at center, coarser toward boundary |
| 185 | + gmsh.model.mesh.field.add("Box", 2) |
| 186 | + gmsh.model.mesh.field.setNumber(2, "VIn", wavelength / n_farfield / 2) # Fine mesh at center |
| 187 | + gmsh.model.mesh.field.setNumber(2, "VOut", wavelength / n_farfield) # Coarser toward boundary |
| 188 | + gmsh.model.mesh.field.setNumber(2, "XMin", -outer_boundary_radius/4) |
| 189 | + gmsh.model.mesh.field.setNumber(2, "XMax", outer_boundary_radius/4) |
| 190 | + gmsh.model.mesh.field.setNumber(2, "YMin", -outer_boundary_radius/4) |
| 191 | + gmsh.model.mesh.field.setNumber(2, "YMax", outer_boundary_radius/4) |
| 192 | + gmsh.model.mesh.field.setNumber(2, "ZMin", -outer_boundary_radius/4) |
| 193 | + gmsh.model.mesh.field.setNumber(2, "ZMax", outer_boundary_radius/4) |
| 194 | + |
| 195 | + # Use this Box field to determine element sizes. |
| 196 | + gmsh.model.mesh.field.setAsBackgroundMesh(2) |
| 197 | + |
| 198 | + # Set 2D/3D meshing algorithm. Chosen to be deterministic, not necessarily the |
| 199 | + # best. |
| 200 | + gmsh.option.setNumber("Mesh.Algorithm3D", 1) |
| 201 | + gmsh.option.setNumber("Mesh.Algorithm", 6) |
| 202 | + |
| 203 | + # Generate 3D volume mesh and set to 3rd order elements. |
| 204 | + gmsh.model.mesh.generate(3) |
| 205 | + gmsh.model.mesh.setOrder(3) |
| 206 | + |
| 207 | + # Set output format for Palace compatibility. |
| 208 | + gmsh.option.setNumber("Mesh.MshFileVersion", 2.2) |
| 209 | + gmsh.option.setNumber("Mesh.Binary", 1) |
| 210 | + gmsh.write(joinpath(@__DIR__, filename)) |
| 211 | + |
| 212 | + println("\nFinished generating mesh. Physical group tags:") |
| 213 | + println("Farfield boundary (2D): ", outer_boundary_group) |
| 214 | + println("Domain (3D): ", domain_group) |
| 215 | + println() |
| 216 | + |
| 217 | + # Optionally launch the Gmsh GUI. |
| 218 | + if gui |
| 219 | + gmsh.fltk.run() |
| 220 | + end |
| 221 | + |
| 222 | + # Clean up Gmsh resources. |
| 223 | + return gmsh.finalize() |
| 224 | +end |
0 commit comments