Tutorial: Gmsh+CalculiX#
This tutorial mirrors the Abaqus-based core tutorials with a fully open-source, conda-forge workflow. The geometry is created and meshed with Gmsh. The finite element simulation is performed with CalculiX. Finally, the data is extracted with ccx2paraview and meshio for scripted quantity of interest (QoI) calculations and post-processing.
References#
Environment#
Warning
The Gmsh+CalculiX tutorial requires a different compute environment than the other tutorials. The following commands
create a dedicated environment for the use of this tutorial. You can also use your existing tutorial environment with
the conda install
command while your environment is active.
All of the software required for this tutorial can be installed in a Conda environment with the Conda package manager. See the Conda installation and Conda environment management documentation for more details about using Conda.
Create the Gmsh+ tutorial environment if it doesn’t exist
$ conda create --name waves-gmsh-env --channel conda-forge waves 'scons>=4.6' python-gmsh calculix 'ccx2paraview>=3.2' meshio
Activate the environment
$ conda activate waves-gmsh-env
Directory Structure#
Create and change to a new project root directory to house the tutorial files if you have not already done so. For example
$ mkdir -p ~/waves-tutorials
$ cd ~/waves-tutorials
$ pwd
/home/roppenheimer/waves-tutorials
Create a new
tutorial_gmsh
directory with thewaves fetch
command below
$ pwd
/home/roppenheimer/waves-tutorials
$ waves fetch --destination tutorial_gmsh tutorials/tutorial_gmsh
$ ls tutorial_gmsh
SConscript SConstruct environment.yml post_processing.py rectangle.py rectangle_compression.inp strip_heading.py vtu2xarray.py
Make the new
tutorial_gmsh
directory the current working directory
$ pwd
/home/roppenheimer/waves-tutorials
$ cd tutorial_gmsh
$ pwd
/home/roppenheimer/waves-tutorials/tutorial_gmsh
$ ls
SConscript SConstruct environment.yml post_processing.py rectangle.py rectangle_compression.inp strip_heading.py vtu2xarray.py
SConscript#
Review the
SConscript
workflow configuration file.
The structure is sufficiently different from the core tutorials that a diff view is not as useful. Instead the contents of the new SConscript files are duplicated below.
waves-tutorials/tutorial_gmsh/SConscript
1#! /usr/bin/env python
2import waves
3
4Import("env")
5
6# Collect the target nodes to build concise target alias(es)
7artifacts = []
8workflow = []
9
10# Geometry, Partition, Mesh
11artifacts.extend(
12 env.PythonScript(
13 target=["rectangle_gmsh.inp"],
14 source=["rectangle.py"],
15 subcommand_options="--output-file=${TARGET.abspath}",
16 action_suffix=env["action_suffix"],
17 )
18)
19
20artifacts.extend(
21 env.PythonScript(
22 target=["rectangle_mesh.inp"],
23 source=["strip_heading.py", "rectangle_gmsh.inp"],
24 subcommand_options="--input-file=${SOURCES[1].abspath} --output-file=${TARGET.abspath}",
25 action_suffix=env["action_suffix"],
26 )
27)
28
29# CalculiX Solve
30artifacts.extend(
31 env.CalculiX(
32 target=[f"rectangle_compression.{suffix}" for suffix in ("frd", "dat", "sta", "cvg", "12d")],
33 source=["rectangle_compression.inp"],
34 action_suffix=env["action_suffix"],
35 )
36)
37
38# Extract
39artifacts.extend(
40 env.Command(
41 target=["rectangle_compression.vtu", "rectangle_compression.vtu.stdout"],
42 source=["rectangle_compression.frd"],
43 action=["cd ${TARGET.dir.abspath} && ccx2paraview ${SOURCES[0].abspath} vtu ${action_suffix}"],
44 action_suffix=env["action_suffix"],
45 )
46)
47workflow.extend(
48 env.PythonScript(
49 target=["rectangle_compression.h5"],
50 source=["vtu2xarray.py", "rectangle_compression.vtu"],
51 subcommand_options="--input-file ${SOURCES[1].abspath} --output-file ${TARGET.abspath}",
52 action_suffix=env["action_suffix"],
53 )
54)
55
56# Post-processing
57script_options = "--input-file ${SOURCES[1:].abspath}"
58script_options += " --output-file ${TARGET.file} --x-units mm/mm --y-units MPa"
59workflow.extend(
60 env.PythonScript(
61 target=["stress_strain_comparison.pdf", "stress_strain_comparison.csv"],
62 source=["post_processing.py", "rectangle_compression.h5"],
63 subcommand_options=script_options,
64 )
65)
66artifacts.extend(workflow)
67
68# Collector alias named after the model simulation
69env.Alias("rectangle", workflow)
70
71if not env["unconditional_build"] and (not env["CCX_PROGRAM"] or not env["ccx2paraview"]):
72 print(
73 "Program 'CalculiX (ccx)' or 'ccx2paraview' was not found in construction environment. "
74 "Ignoring 'rectangle' target(s)"
75 )
76 Ignore([".", "rectangle"], workflow + artifacts)
Gmsh Python script#
Review the Gmsh Python script
rectangle.py
.
The Gmsh script differs from the Abaqus journal files introduced in Tutorial 02: Partition and Mesh. Besides the differences in Abaqus and Gmsh commands, one major difference between the Abaqus and Gmsh scripts is the opportunity to use Python 3 with Gmsh, where Abaqus journal files must use the Abaqus controlled installation of Python 2. The Gmsh script creates the geometry, partition (sets), and mesh in a single script because the output formats used by Gmsh can not contain both the geometry definition and the mesh information.
waves-tutorials/tutorial_gmsh/rectangle.py
1import typing
2import pathlib
3import argparse
4
5import numpy
6import gmsh
7
8
9script_name = pathlib.Path(__file__)
10# Set default parameter values
11default_output_file = pathlib.Path(script_name.with_suffix(".msh").name)
12default_width = 1.0
13default_height = 1.0
14default_global_seed = 1.0
15
16
17def main(
18 output_file: pathlib.Path = default_output_file,
19 width: float = default_width,
20 height: float = default_height,
21 global_seed: float = default_global_seed,
22) -> None:
23 """Create a simple rectangle geometry.
24
25 This script creates a simple Gmsh model with a single rectangle part.
26
27 :param output_file: The output file for the Gmsh model. Extension must match a supported Gmsh file type.
28 :param width: The rectangle width
29 :param height: The rectangle height
30 :param global_seed: The global mesh seed size
31
32 :returns: writes ``output_file``
33 """
34 gmsh.initialize()
35 gmsh.logger.start()
36 gmsh.model.add("rectangle")
37
38 # Geometry
39 rectangle_tag = gmsh.model.occ.addRectangle(0, 0, 0, width, height)
40 gmsh.model.occ.synchronize()
41
42 # Partition
43 # TODO: Move to separate function/script
44 rectangle_group_tag = gmsh.model.addPhysicalGroup(2, [rectangle_tag], name="rectangle")
45
46 tolerance = 0.01 * min(width, height)
47 bottom_left = get_entities_at_coordinates((0.0, 0.0, 0.0), 0)
48 top_right = get_entities_at_coordinates((width, height, 0.0), 0)
49 # fmt: off
50 top = gmsh.model.getEntitiesInBoundingBox(
51 0.0 - tolerance, # noqa: E221 X-min
52 height - tolerance, # noqa: E221 Y-min
53 0.0 - tolerance, # noqa: E221 Z-min
54 width + tolerance, # noqa: E221 X-max
55 height + tolerance, # noqa: E221 Y-max
56 0.0 + tolerance, # noqa: E221 Z-max
57 0 # Entity dimension: points
58 )
59 bottom = gmsh.model.getEntitiesInBoundingBox(
60 0.0 - tolerance, # noqa: E221 X-min
61 0.0 - tolerance, # noqa: E221 Y-min
62 0.0 - tolerance, # noqa: E221 Z-min
63 width + tolerance, # noqa: E221 X-max
64 0.0 + tolerance, # noqa: E221 Y-max
65 0.0 + tolerance, # noqa: E221 Z-max
66 0 # Entity dimension: points
67 )
68 left = gmsh.model.getEntitiesInBoundingBox(
69 0.0 - tolerance, # noqa: E221 X-min
70 0.0 - tolerance, # noqa: E221 Y-min
71 0.0 - tolerance, # noqa: E221 Z-min
72 0.0 + tolerance, # noqa: E221 X-max
73 height + tolerance, # noqa: E221 Y-max
74 0.0 + tolerance, # noqa: E221 Z-max
75 0 # Entity dimension: points
76 )
77 # fmt: on
78 gmsh.model.addPhysicalGroup(0, tags_from_dimTags(bottom_left), name="bottom_left")
79 gmsh.model.addPhysicalGroup(0, tags_from_dimTags(top_right), name="top_right")
80 gmsh.model.addPhysicalGroup(0, tags_from_dimTags(top), name="top")
81 gmsh.model.addPhysicalGroup(0, tags_from_dimTags(bottom), name="bottom")
82 gmsh.model.addPhysicalGroup(0, tags_from_dimTags(left), name="left")
83
84 # Mesh
85 # TODO: Move to separate function/script
86 points = gmsh.model.getEntities(0)
87 gmsh.model.mesh.setSize(points, global_seed)
88 gmsh.model.mesh.generate(2)
89 gmsh.model.mesh.recombine()
90
91 gmsh.option.setNumber("Mesh.SaveGroupsOfElements", 1)
92 gmsh.option.setNumber("Mesh.SaveGroupsOfNodes", 1)
93
94 gmsh.write(str(output_file))
95 gmsh.logger.stop()
96 gmsh.finalize()
97
98
99def tags_from_dimTags(dimTags: typing.List[typing.Tuple[int, int]]) -> typing.List[int]:
100 """Return tags from Gmsh entity ``dimTags`` list of tuples
101
102 :returns: list of tags
103 """
104 return [dimTag[1] for dimTag in dimTags]
105
106
107def get_entities_at_coordinates(
108 coordinates: typing.Tuple[float, float, float],
109 dimension: int,
110 tolerance: float = 1.0e-6,
111) -> typing.List[typing.Tuple[int, int]]:
112 """Return Gmsh ``dimTags`` of entities of dimension within bounding box determined by coordinates and tolerance
113
114 :param coordinates: 3D coordinates (X, Y, Z) for center of bounding box
115 :param dimension: Return entities matching dimension
116 :param tolerance: Distance from center of bounding box to edges of bounding box
117
118 :returns: Gmsh entity ``dimTags``
119 """
120 if len(coordinates) != 3:
121 raise RuntimeError("coordinates must have length 3")
122 center = numpy.array(coordinates)
123 tolerance_array = numpy.array([tolerance, tolerance, tolerance])
124 min_coordinate = center - tolerance_array
125 max_coordinate = center + tolerance_array
126 return gmsh.model.getEntitiesInBoundingBox(*min_coordinate, *max_coordinate, dimension)
127
128
129def get_parser():
130 prog = f"python {script_name.name} "
131 cli_description = "Create a simple rectangle geometry and write an ``output_file``.msh Gmsh model file."
132 parser = argparse.ArgumentParser(description=cli_description, prog=prog)
133 parser.add_argument(
134 "--output-file",
135 type=pathlib.Path,
136 default=default_output_file,
137 # fmt: off
138 help="The output file for the Gmsh model. Extension must match a supported Gmsh file type, e.g. "
139 "``output_file``.msh (default: %(default)s",
140 # fmt: on
141 )
142 parser.add_argument(
143 "--width",
144 type=float,
145 default=default_width,
146 help="The rectangle width",
147 )
148 parser.add_argument(
149 "--height",
150 type=float,
151 default=default_height,
152 help="The rectangle height",
153 )
154 parser.add_argument(
155 "--global-seed",
156 type=float,
157 default=default_global_seed,
158 help="The global mesh seed size (default: %(default)s)",
159 )
160 return parser
161
162
163if __name__ == "__main__":
164 parser = get_parser()
165 args = parser.parse_args()
166 main(
167 output_file=args.output_file,
168 width=args.width,
169 height=args.height,
170 global_seed=args.global_seed,
171 )
The other Python scripts will not be discussed in detail. They are small content handling and data conversion utilities or discussed in greater depth in other tutorials.
CalculiX Input File#
Create or review the CalculiX input file from the contents below
waves-tutorials/tutorial_gmsh/rectangle_compression.inp
1**
2*HEADING
3Compressing a rectangle
4**
5*INCLUDE, INPUT=rectangle_mesh.inp
6*SOLID SECTION, ELSET=RECTANGLE, MATERIAL=mock
7**
8*MATERIAL, NAME=mock
9*ELASTIC
10100, .3
11*DENSITY
12.27000E-08,
13*SPECIFIC HEAT
148.96E+08
15*CONDUCTIVITY
16132, 200
17*EXPANSION,ZERO=293.15
1823.58E-06
19**
20*************************************************************************
21** STEP-1 LOAD STEP
22*************************************************************************
23*STEP, NLGEOM=NO, INC=100, AMPLITUDE=RAMP
24*STATIC
25.1, 1.0, 0.01, 0.5
26**
27*BOUNDARY
28bottom_left, 1,3
29bottom, 2,2
30top,2,2,-0.01
31**
32*EL FILE
33E, S
34*NODE FILE
35U
36**
37*END STEP
SConstruct#
Note that CalculiX differs from other solvers in the tutorials. CalculiX is deployed as a Conda package and is available in the launching Conda environment. It is still good practice to check if the executable is available and provide helpful feedback to developers about the excutable status and workflow configuration.
The structure has changed enough from the core tutorials that a diff view is not as useful. Instead the contents of the SConstruct file is duplicated below.
waves-tutorials/tutorial_gmsh/SConstruct
#! /usr/bin/env python
import os
import platform
import waves
# Add build directory CLI arg
AddOption(
"--build-dir",
dest="build_dir",
default="build",
nargs=1,
type="string",
action="store",
metavar="DIR",
help="SCons build (variant) root directory. Relative or absolute path. (default: '%default')",
)
AddOption(
"--unconditional-build",
dest="unconditional_build",
default=False,
action="store_true",
help="Boolean to force building of conditionally ignored targets. (default: '%default')",
)
AddOption(
"--verbose-tasks",
dest="verbose_tasks",
default=False,
action="store_true",
help="Boolean to 'tee' output for interactive and verbose task execution. (default: '%default')",
)
# Inherit user's full environment
env = waves.scons_extensions.WAVESEnvironment(
ENV=os.environ.copy(),
build_dir=GetOption("build_dir"),
unconditional_build=GetOption("unconditional_build"),
verbose_tasks=GetOption("verbose_tasks"),
)
# Action suffix
system = platform.system().lower()
if env["verbose_tasks"]:
if system == "windows": # Assume PowerShell
env["action_suffix"] = "$(| Tee-Object -FilePath ${TARGETS[-1].abspath}$)"
else: # *Nix style tee
env["action_suffix"] = "$(2>&1 | tee ${TARGETS[-1].abspath}$)"
else:
# Default WAVES action suffix
env["action_suffix"] = "$(> ${TARGETS[-1].abspath} 2>&1$)"
# Always copy (no sym-links) when duplicating
env.SetOption("duplicate", "copy")
# Always print failed task *.stdout files
env.PrintBuildFailures(print_stdout=True)
# Empty defaults list to avoid building all simulation targets by default
env.Default()
# Find required programs for conditional target ignoring and absolute path for use in target actions
env["CCX_PROGRAM"] = env.AddProgram(["ccx"])
env["ccx2paraview"] = env.AddProgram(["ccx2paraview"])
# Abaqus input file implicit dependency scanner
# Works for CalculiX because CalculiX uses the Abaqus include keyword semantics and input file extension
env.Append(SCANNERS=waves.scons_extensions.abaqus_input_scanner())
# Call SConscript file
SConscript("SConscript", variant_dir=env["build_dir"], exports={"env": env}, duplicate=True)
# List all aliases in help message.
# This must come *after* all expected Alias definitions and SConscript files.
env.ProjectHelp()
Build Targets#
Build the new targets
$ pwd
/path/to/waves-tutorials/tutorial_gmsh
$ scons fierro
scons: Reading SConscript files ...
Checking whether 'ccx' program exists.../home/roppenheimer/anaconda3/envs/waves-gmsh-env/bin/ccx
Checking whether 'ccx2paraview' program exists.../home/roppenheimer/anaconda3/envs/waves-gmsh-env/bin/ccx2paraview
scons: done reading SConscript files.
scons: Building targets ...
cd /home/roppenheimer/waves-tutorials/tutorial_gmsh/build && python /home/roppenheimer/waves-tutorials/tutorial_gmsh/build/rectangle.py --output-file=/home/roppenheimer/waves-tutorials/tutorial_gmsh/build/rectangle_gmsh.inp > /home/roppenheimer/waves-tutorials/tutorial_gmsh/build/rectangle_gmsh.inp.stdout 2>&1
cd /home/roppenheimer/waves-tutorials/tutorial_gmsh/build && python /home/roppenheimer/waves-tutorials/tutorial_gmsh/build/strip_heading.py --input-file=/home/roppenheimer/waves-tutorials/tutorial_gmsh/build/rectangle_gmsh.inp --output-file=/home/roppenheimer/waves-tutorials/tutorial_gmsh/build/rectangle_mesh.inp > /home/roppenheimer/waves-tutorials/tutorial_gmsh/build/rectangle_mesh.inp.stdout 2>&1
cd /home/roppenheimer/waves-tutorials/tutorial_gmsh/build && /home/roppenheimer/anaconda3/envs/waves-gmsh-env/bin/ccx -i rectangle_compression > /home/roppenheimer/waves-tutorials/tutorial_gmsh/build/rectangle_compression.frd.stdout 2>&1
cd /home/roppenheimer/waves-tutorials/tutorial_gmsh/build && ccx2paraview /home/roppenheimer/waves-tutorials/tutorial_gmsh/build/rectangle_compression.frd > /home/roppenheimer/waves-tutorials/tutorial_gmsh/build/rectangle_compression.vtu.stdout 2>&1
cd /home/roppenheimer/waves-tutorials/tutorial_gmsh/build && python /home/roppenheimer/waves-tutorials/tutorial_gmsh/build/vtu2xarray.py --input-file /home/roppenheimer/waves-tutorials/tutorial_gmsh/build/rectangle_compression.vtu --output-file /home/roppenheimer/waves-tutorials/tutorial_gmsh/build/rectangle_compression.h5 > /home/roppenheimer/waves-tutorials/tutorial_gmsh/build/rectangle_compression.h5.stdout 2>&1
cd /home/roppenheimer/waves-tutorials/tutorial_gmsh/build && python /home/roppenheimer/waves-tutorials/tutorial_gmsh/build/post_processing.py --input-file /home/roppenheimer/waves-tutorials/tutorial_gmsh/build/rectangle_compression.h5 --output-file stress_strain_comparison.pdf --x-units mm/mm --y-units MPa > /home/roppenheimer/waves-tutorials/tutorial_gmsh/build/stress_strain_comparison.pdf.stdout 2>&1
scons: done building targets.
Output Files#
Explore the contents of the build
directory using the find
command against the build
directory, as shown
below.
$ pwd
/home/roppenheimer/waves-tutorials/tutorial_cubit
$ find build -type f
build/rectangle_compression.h5
build/rectangle_compression.inp
build/rectangle_compression.h5.stdout
build/rectangle_compression.sta
build/rectangle_compression.dat
build/rectangle_mesh.inp
build/rectangle_compression.vtu.stdout
build/rectangle_compression.12d
build/spooles.out
build/rectangle_gmsh.inp
build/rectangle_compression.cvg
build/stress_strain_comparison.pdf.stdout
build/rectangle_compression.frd.stdout
build/rectangle_compression.vtu
build/rectangle.py
build/stress_strain_comparison.pdf
build/post_processing.py
build/rectangle_gmsh.inp.stdout
build/SConscript
build/rectangle_compression.frd
build/strip_heading.py
build/vtu2xarray.py
build/stress_strain_comparison.csv
build/rectangle_mesh.inp.stdout