Skip to content

General information

Trelis is an easy to use meshing-tool which interfaces with openCFS via the Ansys-CDB format. It comes with a GUI which can be used for interactive meshing but also offers a journal which can be used to script and document the meshing process. This documentation focuses on the scripting side and demonstrates this via an exemplary mesh. In this example we will build and mesh a channel which is split by a perforated plate, as it can be seen in Fig. 1.

Channel demo mesh.

This example will be later used to perform a computation where we couple the AcousticPDE with the LinFlowPDE.

Defining variables

In the beginning a set of variables defining the geometry and meshing parameters as well as general stuff should be set.

We start by resetting the current status in Trelis (thus deleting everything which has been modelled, otherwise we won't have a clean start and variables and IDs might get messed up). Afterwards, we enable the 'undo' function as well as disabling the (additional) journal and the echo function.

reset
undo on
Journal off
Echo off

Afterwards, we define our geometry with the help of variables, which can be set by the following scheme:

#{VAR = VAL}; # COMMENT

In our case, we define the geomtry like this:

#{d_v = 1e-3}; # diameter of the channel
#{d_h = 0.2e-3}; # diameter of the holes
#{h_gap = 0.1e-3}; # height of the holes
#{midpoint_dist = 1.25*d_h}; # distance between the holes as seen from the center of each hole

#{l_Ch_trans = 0.5e-3}; # length of the transition zone for the channel
#{l_Ch_lower = 5e-3}; # length of the lower part of the channel
#{l_Ch_upper = 5e-3}; # length of the upper part of the channel
#{l_PML = 0.3e-3}; # length of the PML

Afterwards, we define the meshing parameters like the size of the mesh in different volumes.

#{h_el_f = 20e-6*sf} # Element size fine
#{h_el_m = 0.1e-3*sf} # Element size standard

Finally, we set the merge tolerance to the value of 1e-6 m, which is the smallest one possible.

#{tol = 1e-6}
merge tolerance {tol}

Geometry

In this section, we build the actual geometry with the help of our predefined variables. We start by creating surfaces which will then be extruded to get our final volumes. In our example, we have an inner section which needs to be meshed relatively fine in order to approximate our flow field (LinFlow-region) adequately.

# channel itself
create surface circle radius {d_v/2}

We continue by creating more surfaces which will represent our holes. First of all we add the hole in the center.

# inner most hole
create surface circle radius {d_h/2}
#{Ring_start_ID = 3} # ID of the first hole of the ring
#{ID_counter = 3} # ID-counter to keep track of the current ID

Here we also start keeping track of our surface IDs. This will help us in the future when we have to perform certain operations (e.g. slicing) on the surface for which we need an ID. We continue by creating another 6 holes, but this time we use a loop. In this scenario it is not necessary, but when dealing with large geometrical features this can increase flexibility and readability of the code quite drastially.

# group of six holes
#{uu=1}
#{Loop(10)}
    create surface circle radius {d_h/2}
    #{angle = 360/6*uu}
    move surface {ID_counter} x {midpoint_dist*sind(angle)} y {midpoint_dist*cosd(angle)} z 0
    #{uu++}
    #{ID_counter++}
    #{If(uu>=7)}
        #{Break}
    #{EndIf}
#{EndLoop}

#{Ring_end_ID = ID_counter-1} # ID of the last hole of the ring

In this loop, we create a surface representing the hole in the center and move it to its final position all in one cycle. We also keep track of our loop variable as well as the countr for the IDs by increasing them in each iteration with the '++' operation. For demonstration purposes we have also included an 'If'-statement which will cause the loop to exit when the loop counter reaches the value 7. After creating the surfaces we want to use symmetries in order to reduce the model. Therefore, we cout out one half of the whole domain (we could cut out even more but otherwise we would not have enough volumes to demonstrate the features). It has to be noted that one can already exlude the generation of surfaces which will be cut out later one, but since it does not affect performance this part has been ommited for this example.

# cut out one half of the whole channel
split surface 1 across location position 0 0 0 location position {d_v/2} 0 0
split surface 1 across location position 0 0 0 location position {d_v/2*cosd(180)} {d_v/2*sind(180)} 0
split surface 2 across location position 0 0 0 location position {d_v/2} 0 0
split surface 2 across location position 0 0 0 location position {d_v/2*cosd(180)} {d_v/2*sind(180)} 0

After the cutting process we can delete the surfaces which we don't need anymore.

# delete the rest
delete surface all with y_coord<0

Now we can start creating volumes from our surfaces.

# create the volumes

# model the middle region

group "ID_dummy" add surface all in volume 2 
#{Inner_hole_ID = GroupMemberId("ID_dummy","Surface",0)}

In order to get the ID of the surface in the center we use a little trick and create a group which contains all surfaces in volume 2. Then we simply extract the ID of the first member of this group (since we created only a surface which belongs to the volume we only have one entry and automatically get the correct ID). Afterwards, we use the 'sweep' command to extrude our surfaces. Here we have to pay attention that we keep the original surfaces if we want to reuse them. Furthermore, it is always a good idea to keep track of all the volume IDs. In this case, we only store the first and the last ID of all the volumes belonging to the ring. The reason for this is, that we perform the same operations on all of them, hence we can simply use the 'to' command in order to specify a range between the first and the last ID.

sweep surface {Inner_hole_ID} direction 0 0 1 distance {h_gap} keep
#{Mid_vol_ID = Id("Volume")};

sweep surface {Ring_start_ID} to {Ring_end_ID} direction 0 0 1 distance {h_gap} keep
#{Ring_vol_start_ID = Mid_vol_ID+1)};
#{Ring_vol_end_ID = Id("Volume")};

It has to be noted that there are many different keywords which can be used for specifying ranges. The most important ones are 'to', 'and' and 'except'. Afterwards, we start to sweep the lower transition region.

# lower transition region

group "ID_dummy_2" add surface all in volume 1 
#{Channel_surf_ID = GroupMemberId("ID_dummy_2","Surface",0)}

sweep surface {Channel_surf_ID} direction 0 0 -1 distance {l_CH_trans} keep
#{Lower_trans_ID = Id("Volume")};

Afterwards we sweep the upper transition region which is the last part considering the LinFlow region.

# upper transition region

sweep surface {Channel_surf_ID} direction 0 0 1 distance {l_CH_trans} keep
#{Upper_trans_ID = Id("Volume")};
move volume {Upper_trans_ID} x 0 y 0 z {h_gap}

Here we also have to use the 'move' command in order to move the volume in z direction since the volumes would intersect otherwise. Finally, we create the lower and upper channel region as well as the PML.

# Channel lower
sweep surface {Channel_surf_ID} direction 0 0 -1 distance {l_Ch_lower} keep
#{Lower_Ch_ID = Id("Volume")};
move volume {Lower_Ch_ID} x 0 y 0 z {-l_Ch_trans}


# Channel upper
sweep surface {Channel_surf_ID} direction 0 0 1 distance {l_Ch_upper} keep
#{Upper_Ch_ID = Id("Volume")};
move volume {Upper_Ch_ID} x 0 y 0 z {h_gap+l_Ch_trans}


# PML
sweep surface {Channel_surf_ID} direction 0 0 1 distance {l_PML} keep
#{PML_ID = Id("Volume")};
move volume {PML_ID} x 0 y 0 z {h_gap+l_Ch_trans+l_Ch_upper}
#{PML_ID = Id("Volume")};

Now, we delete the remaining surfaces.

# delete the surfaces
delete surface 1 to {Inner_hole_ID}

Finally, we merge the regions accordingly. We couple the acoustic region and the LinFlow region with a Non-Conforming-Interface, hence we have to merge everything except for the interfaces between these regions.

# merge the LinFlow region
imprint tolerant volume 1 to {Upper_trans_ID}
merge volume 1 to {Upper_trans_ID}

# merge the acoustic region
imprint tolerant volume {Upper_Ch_ID} {PML_ID}
merge volume {Upper_Ch_ID} {PML_ID}

Meshing

In the next step we mesh the newly created volumes. We start with the holes where we utilize the IDs we stored earlier in order to specify the element size and the meshing scheme for the respective volumes. Furthermore, we use a geometry dependent call for the surfaces of those volumes.

# Mesh

# Holes

surface in volume {Inner_hole_vol_ID} {Ring_vol_start_ID} to {Ring_vol_end_ID} with z_coord=0 size {h_el_f} scheme pave
mesh surface in volume {Inner_hole_vol_ID} {Ring_vol_start_ID} to {Ring_vol_end_ID} with z_coord=0

In the beginning we only mesh the surfaces which we than extrude with the sweeping option (which is automatically set in this case - Trelis tries to find the optimal scheme itself which results in the sweeping-scheme in this szenario).

# sweep (automatically set) to get the holes meshed
volume {Inner_hole_vol_ID} {Ring_vol_start_ID} to {Ring_vol_end_ID} size {h_el_f}
mesh volume {Inner_hole_vol_ID} {Ring_vol_start_ID} to {Ring_vol_end_ID}

We continue doing so with the rest of the LinFlow-region.

# Lower transition region

surface in volume {Lower_trans_ID} with z_coord=0 size {h_el_f} scheme pave
mesh surface in volume {Lower_trans_ID} with z_coord=0

volume {Lower_trans_ID} size {h_el_f}
mesh volume {Lower_trans_ID}


# Upper transition region

surface in volume {Upper_trans_ID} with z_coord={h_gap} size {h_el_f} scheme pave
mesh surface in volume {Upper_trans_ID} with z_coord={h_gap}

volume {Upper_trans_ID} size {h_el_f}
mesh volume {Upper_trans_ID}

For the acoustic region the procedure is pretty much equivalent - the only real difference is the different element size. Since we do not need to resolve any boundary layers in the acoustic-region, we can use coarser elements and save computational ressources.

# Lower channel

surface in volume {Lower_Ch_ID} with z_coord={-l_Ch_trans} size {h_el_m} scheme pave
mesh surface in volume {Lower_Ch_ID} with z_coord={-l_Ch_trans}

volume {Lower_Ch_ID} size {h_el_m}
mesh volume {Lower_Ch_ID}


# Upper channel

surface in volume {Upper_Ch_ID} with z_coord={h_gap+l_Ch_trans} size {h_el_m} scheme pave
mesh surface in volume {Upper_Ch_ID} with z_coord={h_gap+l_Ch_trans}

volume {Upper_Ch_ID} size {h_el_m}
mesh volume {Upper_Ch_ID}


# PML

volume {PML_ID} size {h_el_m}
mesh volume {PML_ID}

It has to be noted that since the top surface of the upper channel is merged with the bottom surface of the PML, we already have a surface mesh for the volume of the PML. Hence, we can directly use the volume mesh command and ommit the surface mesh.

Blocks and Nodesets

In order to use the meshed volumes in openCFS we have to define blocks and nodesets. Blocks can be seen as a group of for example in our case volumes. The important part is though, that this group should only contain entries with similar element types. If you want to mix element types (e.g. hexes and wedges) you either have to assign different blocks or write the wedge elements as degenerated versions of a hex element. This has to be enabled seperately and needs to be checked if its working properly (this can be done by letting openCFS put out only the grid and checking the grid in e.g. Paraview) - more to this in the 'Advanced tips and tricks' section. The blocks which we define here will also be used for the assignment in openCFS.

In order to assign these blocks, we simply use the volume IDs from earlier.

# Blocks

block 1 add volume all except {Lower_Ch_ID} {Upper_Ch_ID} {PML_ID}
block 1 name "LinFlow"

block 2 add volume {Lower_Ch_ID}
block 2 name "Lower_Ch"

block 3 add volume {Upper_Ch_ID}
block 3 name "Upper_Ch"

block 4 add volume {PML_ID}
block 4 name "PML"

Regarding the nodesets we have to define an excitation surface, the symmetry, the wall boundary condition as well as the Non-Conforming-Interfaces. We start with the surface used for the excitation at the bottom of the channel.

# Nodesets

nodeset 1 add surface with z_coord={-l_Ch_trans-l_Ch_lower}
nodeset 1 name "Excite"

For the symmetry surfaces we only use the LinFlow-region. Furthermore, we have to be careful to actually include all the surfaces. It has to be noted that someties the "=" statement might lead to unwanted outcomes since the actual position might be off just a little bit. Therefore, it is better to include a very small tolerance.

nodeset 2 add surface in volume 1 to {Lower_Ch_ID-1} with y_coord<{1e-8}
nodeset 2 name "Sym1"

In the next step, we define the wall boundary condition. Here we take all outer surfaces of the LinFlow-region which do not already belong to the symmetry nodeset.

nodeset 3 add surface all with z_coord={h_gap/2} and y_coord>{1e-8}
nodeset 3 add surface all with z_coord=0 and y_max>={d_ch/2-1e-8}
nodeset 3 add surface all with z_coord={h_gap} and y_max>={d_ch/2-1e-8}
nodeset 3 name "Wall_BC"

In the last step we define our Non-Conforming interfaces. The easiest way again is to just use the volume IDs in combination with the geometry. Since the position of the master and the slave side is equivalent, only the volume ID has to be redefined.

nodeset 4 add surface in volume {Lower_trans_ID} with z_coord={-l_Ch_trans}
nodeset 4 name "NCIM_Lower"

nodeset 5 add surface in volume {Lower_Ch_ID} with z_coord={-l_Ch_trans}
nodeset 5 name "NCIS_Lower"

nodeset 6 add surface in volume {Upper_trans_ID} with z_coord>={h_gap+l_Ch_trans-1e-8}
nodeset 6 name "NCIM_Upper"

nodeset 7 add surface in volume {Upper_Ch_ID} with z_coord={h_gap+l_Ch_trans}
nodeset 7 name "NCIS_Upper"

It has to be noted that nodeset 6 was defined with a small tolerance since it caused problems otherwise.

Advanced tips and tricks

A lot of advanced tips and tricks like keeping track of IDs and using loops have already been covered in the example. This section serves as a quick reference guide where the most important features are listed again - for their application please refer to the example above.

List of useful commands

  • Get the ID of the last volume:
Id("Volume")
  • Loop for a certain number of times:
#{Loop(10)}
    ...
#{EndLoop}
  • If-statemet:
#{If(uu>=7)}
    ...
#{EndIf}
  • Get surfaces of a certain volume (range):
surface in volume {ID}
  • Selection based on geometry:
with z_coord={z_val}
with x_max>={x_val}
with y_min<{y_val}
  • General selection of volumes (and surfaces etc.):
all
except
and

Export degenerate elements

Since blocks can only contain one type of element, the only option to export multiple element types in one block is to write them as a degenarte version of the "main" element type - if possible (e.g. write a wedge as a hex element). Attention: This feature is not activated by default, hence exporting a block with multiple element types will result in holes! If you have strange problems with your simulation, check if this might be the cause! To activate this feature just use

Set Block Mixed Element Output Degenerate

somewhere before you export the mesh.

Using COMSOL Multiphysics as mesh-generator

In order to use a mesh generated with COMSOL, the mesh has to be exported from COMSOl and converted to .hdf5 using "comsol_meshconvert.py". The following steps need to be taken:

Geometry

If non-conforming grids are going to be used, it is important that the geometric domains are not merged via “union” to avoid collapsing the shared boundaries of the adjacent domains into one. Only the domains, whose shared interfaces are going to be conforming are merged by creating a “union”. At the end of the geometric sequence a “Form Assembly”-Node must be used, the “create pairs” box must be checked and the pairing type “identity pair” must be chosen. That way, the previously formed unions will stay unions and the rest of the domains will stay separate while the boundaries on both sides will be kept.

form_assembly.

Selections

All regions (domains, boundaries,..) need to be defined as “selections” and can later be used in openCFS by the name given in COMSOL. When the selections for the boundaries at non-conforming interfaces are created (master and slave), the boundary belonging to the correct region must be chosen. (boundaries of adjacent regions are indistinguishable, an easy way of selecting the boundary of one domain is by hiding the other domain).

selections

Mesh and export

After the usual meshing-process the mesh is exported as .mphtxt via mesh -> export. It is important that the selections are included in the export.

export

Conversion

Change the filename in “comsol_meshconvert.py” accordingly and run the code (.mphtxt – file must be in the same folder).