Tutorial: Mesh Convergence#

This tutorial is intended to demonstrate several advanced usage features that were glossed over in previous tutorials. Specifically, this tutorial will discuss

  • Building a parameter study from somewhere in the middle of the SCons workflow using a common source

  • Utilizing --input-file and --output-file command-line arguments

  • Using the post_processing.py script to create multiple plots

References#

Environment#

SCons and WAVES 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.

Note

The SALib and numpy versions may not need to be this strict for most tutorials. However, Tutorial: Sensitivity Study uncovered some undocumented SALib version sensitivity to numpy surrounding the numpy v2 rollout.

  1. Create the tutorials environment if it doesn’t exist

    $ conda create --name waves-tutorial-env --channel conda-forge waves 'scons>=4.6' matplotlib pandas pyyaml xarray seaborn 'numpy>=2' 'salib>=1.5.1' pytest
    
  2. Activate the environment

    $ conda activate waves-tutorial-env
    

Some tutorials require additional third-party software that is not available for the Conda package manager. This software must be installed separately and either made available to SConstruct by modifying your system’s PATH or by modifying the SConstruct search paths provided to the waves.scons_extensions.add_program() method.

Directory Structure#

  1. 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

Note

If you skipped any of the previous tutorials, run the following commands to create a copy of the necessary tutorial files.

$ pwd
/home/roppenheimer/waves-tutorials
$ waves fetch --overwrite --tutorial 11 && mv tutorial_11_regression_testing_SConstruct SConstruct
WAVES fetch
Destination directory: '/home/roppenheimer/waves-tutorials'
  1. Download and copy the tutorial_11_regression_testing file to a new file named tutorial_mesh_convergence with the WAVES Command-Line Utility fetch subcommand.

$ pwd
/home/roppenheimer/waves-tutorials
$ waves fetch --overwrite tutorials/tutorial_11_regression_testing && cp tutorial_11_regression_testing tutorial_mesh_convergence
WAVES fetch
Destination directory: '/home/roppenheimer/waves-tutorials'

Parameter Study File#

  1. Create a new file modsim_package/python/rectangle_compression_mesh_convergence.py from the content below.

waves-tutorials/modsim_package/python/rectangle_compression_mesh_convergence.py

"""Parameter sets and schemas for the rectangle compression simulation"""


def parameter_schema(
    global_seed=[1.0, 0.5, 0.25, 0.125],
):
    """Return mesh convergence WAVES CartesianProduct parameter schema

    :param float global_seed: The global mesh seed size

    :returns: WAVES CartesianProduct parameter schema
    :rtype: dict
    """
    schema = {
        "global_seed": global_seed,
    }
    return schema

This parameter study will define a mesh convergence study where the global size of the finite elements in the model is decreased several times by a factor of two.

Mesh Convergence Selection Dictionary#

  1. Create a new file modsim_package/python/mesh_convergence_stress.yaml from the content below.

waves-tutorials/modsim_package/python/mesh_convergence_stress.yaml

'E values': 'E22'
'S values': 'S22'
'elements': 1
'step': 'Step-1'
'time': 1.0
'integration point': 0

This file defines a YAML formatted dictionary that will be used to change the default data selection of the post_processing.py script.

SConscript#

  1. A diff against the tutorial_11_regression_testing file from Tutorial 11: Regression Testing is included below to help identify the changes made in this tutorial. Use the diff to update your tutorial_mesh_convergence file, and then review the paragraphs that follow to understand the meaning of these changes.

waves-tutorials/tutorial_mesh_convergence

--- /home/runner/work/waves/waves/build/docs/tutorials_tutorial_11_regression_testing
+++ /home/runner/work/waves/waves/build/docs/tutorials_tutorial_mesh_convergence
@@ -1,5 +1,5 @@
 #! /usr/bin/env python
-"""Rectangle compression workflow
+"""Rectangle mesh convergence workflow
 
 Requires the following ``SConscript(..., exports={})``
 
@@ -15,7 +15,7 @@
 
 import waves
 
-from modsim_package.python.rectangle_compression_cartesian_product import parameter_schema
+from modsim_package.python.rectangle_compression_mesh_convergence import parameter_schema
 
 # Inherit the parent construction environment
 Import("env")
@@ -26,6 +26,11 @@
 build_directory = pathlib.Path(Dir(".").abspath)
 workflow_name = build_directory.name
 parameter_study_file = build_directory / "parameter_study.h5"
+simulation_constants = {
+    "width": 1.0,
+    "height": 1.0,
+    "displacement": -0.01,
+}
 
 # Collect the target nodes to build a concise alias for all targets
 workflow = []
@@ -43,34 +48,37 @@
 
 # Comment used in tutorial code snippets: marker-3
 
+# Geometry
+workflow.extend(
+    env.AbaqusJournal(
+        target=["rectangle_geometry.cae", "rectangle_geometry.jnl"],
+        source=["#/modsim_package/abaqus/rectangle_geometry.py"],
+        subcommand_options="--width ${width} --height ${height}",
+        **simulation_constants,
+    )
+)
+
+# Partition
+partition_targets = env.AbaqusJournal(
+    target=["rectangle_partition.cae", "rectangle_partition.jnl"],
+    source=["#/modsim_package/abaqus/rectangle_partition.py", "rectangle_geometry.cae"],
+    subcommand_options="--width ${width} --height ${height}",
+    **simulation_constants,
+)
+workflow.extend(partition_targets)
+partition_cae_node = partition_targets[0]
+
 # Parameterized targets must live inside current simulation_variables for loop
 for set_name, parameters in parameter_generator.parameter_study_to_dict().items():
     set_name = pathlib.Path(set_name)
-    simulation_variables = parameters
+    simulation_variables = {**parameters, **simulation_constants}
 
     # Comment used in tutorial code snippets: marker-4
 
-    # Geometry
-    workflow.extend(
-        env.AbaqusJournal(
-            target=[set_name / "rectangle_geometry.cae", set_name / "rectangle_geometry.jnl"],
-            source=["#/modsim_package/abaqus/rectangle_geometry.py"],
-            subcommand_options="--width ${width} --height ${height}",
-            **simulation_variables,
-        )
-    )
-
-    # Partition
-    workflow.extend(
-        env.AbaqusJournal(
-            target=[set_name / "rectangle_partition.cae", set_name / "rectangle_partition.jnl"],
-            source=["#/modsim_package/abaqus/rectangle_partition.py", set_name / "rectangle_geometry.cae"],
-            subcommand_options="--width ${width} --height ${height}",
-            **simulation_variables,
-        )
-    )
-
     # Mesh
+    journal_options = (
+        "--global-seed ${global_seed} --input-file ${SOURCES[1].abspath} --output-file ${TARGETS[0].abspath}"
+    )
     workflow.extend(
         env.AbaqusJournal(
             target=[
@@ -78,8 +86,11 @@
                 set_name / "rectangle_mesh.cae",
                 set_name / "rectangle_mesh.jnl",
             ],
-            source=["#/modsim_package/abaqus/rectangle_mesh.py", set_name / "rectangle_partition.cae"],
-            subcommand_options="--global-seed ${global_seed}",
+            source=[
+                "#/modsim_package/abaqus/rectangle_mesh.py",
+                partition_cae_node,
+            ],
+            subcommand_options=journal_options,
             **simulation_variables,
         )
     )
@@ -178,16 +189,23 @@
     )
 )
 
-# Regression test
+script_options = (
+    "--input-file ${post_processing_source}"
+    " --output-file ${TARGET.file} --x-units mm --y-units MPa --x-var global_seed --y-var S"
+    " --parameter-study-file ${SOURCES[1].file}"
+    " --selection-dict ${SOURCES[2].abspath}"
+)
 workflow.extend(
     env.PythonScript(
-        target=["regression.yaml"],
+        target=["mesh_convergence_stress.pdf", "mesh_convergence_stress.csv"],
         source=[
-            "#/modsim_package/python/regression.py",
-            "stress_strain_comparison.csv",
-            "#/modsim_package/python/rectangle_compression_cartesian_product.csv",
-        ],
-        subcommand_options="${SOURCES[1:].abspath} --output-file ${TARGET.abspath}",
+            "#/modsim_package/python/post_processing.py",
+            parameter_study_file.name,
+            "#/modsim_package/python/mesh_convergence_stress.yaml",
+        ]
+        + post_processing_source,  # noqa: W503
+        subcommand_options=script_options,
+        post_processing_source=post_processing_source,
     )
 )
 

waves-tutorials/tutorial_mesh_convergence

 1#! /usr/bin/env python
 2"""Rectangle mesh convergence workflow
 3
 4Requires the following ``SConscript(..., exports={})``
 5
 6* ``env`` - The SCons construction environment with the following required keys
 7
 8  * ``datacheck_alias`` - String for the alias collecting the datacheck workflow targets
 9  * ``regression_alias`` - String for the alias collecting the regression test suite targets
10  * ``unconditional_build`` - Boolean flag to force building of conditionally ignored targets
11  * ``abaqus`` - String path for the Abaqus executable
12"""
13
14import pathlib
15
16import waves
17
18from modsim_package.python.rectangle_compression_mesh_convergence import parameter_schema
19
20# Inherit the parent construction environment
21Import("env")
22
23# Comment used in tutorial code snippets: marker-1
24
25# Simulation variables
26build_directory = pathlib.Path(Dir(".").abspath)
27workflow_name = build_directory.name
28parameter_study_file = build_directory / "parameter_study.h5"
29simulation_constants = {
30    "width": 1.0,
31    "height": 1.0,
32    "displacement": -0.01,
33}
34
35# Collect the target nodes to build a concise alias for all targets
36workflow = []
37datacheck = []

The highlighted code above points out two key changes from diff at the beginning of the file. First, we import the parameter_schema from the rectangle_compression_mesh_convergence.py file you created in the beginning of this tutorial. The second change is the addition of a simulation_constants dictionary. This parameter study only changes the meshing global_seed parameter, and all other model parameters stay constant. One way to achieve this would be to set the remaining parameters as single-value parameter sets in the parameter_schema. This was done with the global_seed and displacement parameters in Tutorial 07: Cartesian Product. Rather, we will set the width, height, and displacement variables as constants in the SConscript file, and they will not appear in the parameter study definition. The individual parameters from the parameter_schema and simulation_constants dictionaries will be combined later in the SConscript file.

waves-tutorials/tutorial_mesh_convergence

50# Geometry
51workflow.extend(
52    env.AbaqusJournal(
53        target=["rectangle_geometry.cae", "rectangle_geometry.jnl"],
54        source=["#/modsim_package/abaqus/rectangle_geometry.py"],
55        subcommand_options="--width ${width} --height ${height}",
56        **simulation_constants,
57    )
58)
59
60# Partition
61partition_targets = env.AbaqusJournal(
62    target=["rectangle_partition.cae", "rectangle_partition.jnl"],
63    source=["#/modsim_package/abaqus/rectangle_partition.py", "rectangle_geometry.cae"],
64    subcommand_options="--width ${width} --height ${height}",
65    **simulation_constants,
66)
67workflow.extend(partition_targets)
68partition_cae_node = partition_targets[0]
69
70# Parameterized targets must live inside current simulation_variables for loop
71for set_name, parameters in parameter_generator.parameter_study_to_dict().items():
72    set_name = pathlib.Path(set_name)
73    simulation_variables = {**parameters, **simulation_constants}

The code above is largely copy and paste from Tutorial 11: Regression Testing, with a few significant differences:

  • The code pertainting to # Geometry and # Partition has been moved out of the parameter study’s for loop. As this parameter study only involves a meshing parameter, the Geometry and Partition workflow steps need only happen once. Then, the mesh convergence parameter study can reuse rectangle_partition.cae as a common source.

  • Note the highlighted lines for the target definitions in the # Geometry and # Partition code. Since this code is no longer inside of the for loop, the set_name directory has been dropped from the target definitions. As the first bullet alluded to, the targets for # Geometry and # Partition will be built in the overall build directory, build/tutorial_mesh_convergence.

  • The following highlighted line is necessary for the parameterized # Mesh workflow steps to reuse a common target. The SCons file object for the rectangle_partition.cae file is extracted from the target list, partition_target, as a source.

  • The final highlighted line shows how the simulation_variables dictionary is constructed by combining the simulation_constants and the global_seed parameters for every simulation.

waves-tutorials/tutorial_mesh_convergence

 77    # Mesh
 78    journal_options = (
 79        "--global-seed ${global_seed} --input-file ${SOURCES[1].abspath} --output-file ${TARGETS[0].abspath}"
 80    )
 81    workflow.extend(
 82        env.AbaqusJournal(
 83            target=[
 84                set_name / "rectangle_mesh.inp",
 85                set_name / "rectangle_mesh.cae",
 86                set_name / "rectangle_mesh.jnl",
 87            ],
 88            source=[
 89                "#/modsim_package/abaqus/rectangle_mesh.py",
 90                partition_cae_node,
 91            ],
 92            subcommand_options=journal_options,
 93            **simulation_variables,
 94        )
 95    )
 96
 97    # SolverPrep
 98    copy_source_list = [
 99        "#/modsim_package/abaqus/rectangle_compression.inp.in",
100        "#/modsim_package/abaqus/assembly.inp",
101        "#/modsim_package/abaqus/boundary.inp",
102        "#/modsim_package/abaqus/field_output.inp",
103        "#/modsim_package/abaqus/materials.inp",
104        "#/modsim_package/abaqus/parts.inp",
105        "#/modsim_package/abaqus/history_output.inp",
106    ]
107    workflow.extend(
108        env.CopySubstfile(
109            copy_source_list,
110            substitution_dictionary=env.SubstitutionSyntax(simulation_variables),
111            build_subdirectory=set_name,
112        )
113    )

The highlighted lines above demonstrate the usage of --input-file and --output--file command-line arguments for the rectangle_mesh.py file. In previous tutorials, we have accepted the default values for input and output files. In this case, however, we must specify that a common input file is used, as we want to reuse the target from the Partition task as a source. If we would have accepted the default input file name, the rectangle_mesh.py script would try to open a rectangle_partition.cae file in every parameter set build directory. The script would fail to do so, because rectangle_partition.cae resides a directory above in the main build directory. We avoid this issue by providing the absolute path to rectangle_partition.cae as the --input-file.

The --output-file command-line argument is specified in this case only for demonstration (the default value would actually work just fine).

The highlighted line containing partition_cae_object demonstrates the usage of an SCons file object as a source. Rather than pointing to the rectangle_partition.cae file via absolute path, we can let SCons find the file for us in the build directory. This is achieved by simply pointing to the SCons file object that was created when we specified rectangle_partition.cae as a target in the # Partition workflow.

waves-tutorials/tutorial_mesh_convergence

175# Post-processing
176post_processing_source = [
177    pathlib.Path(set_name) / "rectangle_compression_datasets.h5"
178    for set_name in parameter_generator.parameter_study_to_dict().keys()
179]
180script_options = "--input-file ${SOURCES[2:].abspath}"
181script_options += " --output-file ${TARGET.file} --x-units mm/mm --y-units MPa"
182script_options += " --parameter-study-file ${SOURCES[1].abspath}"
183workflow.extend(
184    env.PythonScript(
185        target=["stress_strain_comparison.pdf", "stress_strain_comparison.csv"],
186        source=["#/modsim_package/python/post_processing.py", parameter_study_file.name] + post_processing_source,
187        subcommand_options=script_options,
188    )
189)
190
191script_options = (
192    "--input-file ${post_processing_source}"
193    " --output-file ${TARGET.file} --x-units mm --y-units MPa --x-var global_seed --y-var S"
194    " --parameter-study-file ${SOURCES[1].file}"
195    " --selection-dict ${SOURCES[2].abspath}"
196)
197workflow.extend(
198    env.PythonScript(
199        target=["mesh_convergence_stress.pdf", "mesh_convergence_stress.csv"],
200        source=[
201            "#/modsim_package/python/post_processing.py",
202            parameter_study_file.name,
203            "#/modsim_package/python/mesh_convergence_stress.yaml",
204        ]
205        + post_processing_source,  # noqa: W503
206        subcommand_options=script_options,
207        post_processing_source=post_processing_source,
208    )
209)
210
211# Collector alias based on parent directory name
212env.Alias(workflow_name, workflow)
213env.Alias(f"{workflow_name}_datacheck", datacheck)
214env.Alias(env["datacheck_alias"], datacheck)
215env.Alias(env["regression_alias"], datacheck)
216
217if not env["unconditional_build"] and not env["ABAQUS_PROGRAM"]:
218    print(f"Program 'abaqus' was not found in construction environment. Ignoring '{workflow_name}' target(s)")
219    Ignore([".", workflow_name], workflow)
220    Ignore([".", f"{workflow_name}_datacheck"], datacheck)
221    Ignore([".", env["datacheck_alias"], env["regression_alias"]], datacheck)

The highlighted code above demonstrated the usage of the post_processing.py script to generate a second plot. The first plot, as demonstrated in Tutorial 09: Post-Processing, is a simple stress-strain comparison for each parameter set. The highlighted code is used to generate a plot of global mesh size versus the stress in the model at the end of the simulation. As the global mesh size decreases, the final stress should start to converge to a common value.

The specification of a selection_dict demonstrates another feature of the post_processing.py command-line interface. In this case, the only key: value pair added to the selection_dict that does not already exist in the post_processing.py CLI defaults is the specification of the time point 'time': 1.0. This down selects our data to the largest compressive stress produced by the simulation, which will be our quantity of interest (QoI) for this simulation workflow.

The remaining changes are rather simple. The --x-units and --x-var command-line arguments are updated to reflect the usage of the global_seed parameter as the independent variable.

SConstruct#

  1. A diff against the SConstruct file from Tutorial 11: Regression Testing is included below to help identify the changes made in this tutorial. Make these changes to your SConstruct file.

waves-tutorials/SConstruct

--- /home/runner/work/waves/waves/build/docs/tutorials_tutorial_11_regression_testing_SConstruct
+++ /home/runner/work/waves/waves/build/docs/tutorials_tutorial_mesh_convergence_SConstruct
@@ -114,6 +114,7 @@
     "tutorial_08_data_extraction",
     "tutorial_09_post_processing",
     "tutorial_11_regression_testing",
+    "tutorial_mesh_convergence",
 ]
 for workflow in workflow_configurations:
     build_dir = env["variant_dir_base"] / workflow

Build Targets#

  1. Build the new targets

$ pwd
/home/roppenheimer/waves-tutorials
$ scons tutorial_mesh_convergence --jobs=4

The output from building the targets is not shown explicitly here, but look for one particular thing in your terminal output. You should notice the execution of the rectangle_geometry.py and rectangle_partition.py scripts first, and then the parameter study is kicked off with multiple executions of the rectangle_mesh.py script.

Output Files#

  1. Observe the catenated parameter results and parameter study dataset in the post-processing task’s STDOUT file.

$ cat build/tutorial_mesh_convergence/mesh_convergence_stress.stdout

<xarray.Dataset>
Dimensions:              (E values: 4, S values: 4, elements: 64, step: 1,
                         time: 5, set_name: 4)
Coordinates:
  * E values            (E values) object 'E11' 'E22' 'E33' 'E12'
  * S values            (S values) object 'S11' 'S22' 'S33' 'S12'
  * elements            (elements) int64 1 2 3 4 5 6 7 ... 58 59 60 61 62 63 64
  * step                (step) object 'Step-1'
  * time                (time) float64 0.0175 0.07094 0.2513 0.86 1.0
    integrationPoint    (set_name, elements) float64 1.0 nan ... 1.0 1.0
  * set_name            (set_name) <U14 'parameter_set0' ... 'parameter...
    set_hash            (set_name) object ...
Data variables:
    E                   (set_name, step, time, elements, E values) float32 ...
    S                   (set_name, step, time, elements, S values) float32 ...
    global_seed         (set_name) float64 ...