Skip to main content
`Ctrl`+`K`

# 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

- Mesh Convergence Studies [42]

- SCons file objects: SCons Node Package

## 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

   Linux/MacOS

       $ 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

   Windows

       PS > conda create --name waves-tutorial-env --channel conda-forge waves scons matplotlib pandas pyyaml xarray seaborn numpy salib pytest

2. Activate the environment

   Linux/MacOS

       $ conda activate waves-tutorial-env

   Windows

       PS > 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

3. Create and change to a new project root directory to house the tutorial files if you have not already done so. For example

Linux/MacOS

    $ mkdir -p ~/waves-tutorials
    $ cd ~/waves-tutorials
    $ pwd
    /home/roppenheimer/waves-tutorials

Windows

    PS > New-Item $HOME\waves-tutorials -ItemType "Directory"
    PS > Set-Location $HOME\waves-tutorials
    PS > Get-Location

    Path
    ----
    C:\Users\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'

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

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

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

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

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

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

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