Skip to main content
`Ctrl`+`K`

# Tutorial: Cubit+Fierro

Note

Depending on your Cubit installation and operating system, it may not be possible to import Cubit into arbitrary Python 3 environments. Instead, it may be necessary to execute Cubit Python scripts using the Python interpreter bundled with Cubit. An alternate Cubit tutorial using `waves.scons_extensions.add_cubit_python()` is provided as `tutorial_cubit_alternate`

    $ pwd
    /home/roppenheimer/waves-tutorials
    $ waves fetch --destination tutorial_cubit_alternate tutorials/tutorial_cubit_alternate

## References

- Cubit: Python Interface [62]

- Cubit: Importing Cubit into Python [62]

- Fierro documentation [65, 66]

- meshio [40]

- vtk [67]

- MPI [68]

## Environment

Warning

The Fierro 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 environment if you add the FierroMechanics channel and install the `fierro-fe-cpu` and `meshio` packages.

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

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

       $ conda create --name waves-fierro-env --channel fierromechanics --channel conda-forge waves 'scons>=4.6' fierro-fe-cpu meshio

2. Activate the environment

       $ conda activate waves-fierro-env

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

4. Create a new `tutorial_cubit` directory with the `waves fetch` command below

    $ pwd
    /home/roppenheimer/waves-tutorials
    $ waves fetch --destination tutorial_cubit tutorials/tutorial_cubit
    $ ls tutorial_cubit
    modsim_package/  abaqus  cubit  SConstruct  sierra fierro

5. Make the new `tutorial_cubit` directory the current working directory

    $ pwd
    /home/roppenheimer/waves-tutorials
    $ cd tutorial_cubit
    $ pwd
    /home/roppenheimer/waves-tutorials/tutorial_cubit
    $ ls
    modsim_package/  abaqus  cubit  SConstruct  sierra fierro

## SConscript

Note that the `tutorial_cubit` directory has four SConscript files: `cubit`, `abaqus`, `sierra`, and `fierro`. The `cubit` and `fierro` files are relevant to the current tutorial. The `abaqus` and `sierra` workflows are described in the complementary Tutorial: Cubit+Abaqus and Tutorial: Cubit+Sierra.

6. Review the `cubit` and `fierro` tutorials and compare them against the Tutorial 04: Simulation files.

The structure has changed enough that a diff view is not as useful. Instead the contents of the new SConscript files are duplicated below.

waves-tutorials/tutorial_cubit/cubit

      1#! /usr/bin/env python
      2"""Rectangle compression workflow: geometry, partition, mesh
      3
      4Requires the following ``SConscript(..., exports={})``
      5
      6* ``env`` - The SCons construction environment with the following required keys
      7
      8  * ``unconditional_build`` - Boolean flag to force building of conditionally ignored targets
      9  * ``cubit`` - String path for the Cubit executable
     10
     11* ``element_type`` - The Cubit 4 node quadrilateral element type
     12* ``solver`` - The target solver to use when writing a mesh file
     13"""
     14import pathlib
     15
     16# Inherit the parent construction environment
     17Import("env", "element_type", "solver")
     18
     19# Simulation variables
     20build_directory = pathlib.Path(Dir(".").abspath)
     21workflow_name = build_directory.name
     22
     23# Collect the target nodes to build a concise alias for all targets
     24workflow = []
     25
     26# Rectangle 2D
     27# Geometry
     28workflow.extend(
     29    env.PythonScript(
     30        target=["rectangle_geometry.cub"],
     31        source=["#/modsim_package/cubit/rectangle_geometry.py"],
     32        subcommand_options="",
     33    )
     34)
     35
     36# Partition
     37workflow.extend(
     38    env.PythonScript(
     39        target=["rectangle_partition.cub"],
     40        source=["#/modsim_package/cubit/rectangle_partition.py", "rectangle_geometry.cub"],
     41        subcommand_options="",
     42    )
     43)
     44
     45# Mesh
     46if solver.lower() == "abaqus":
     47    mesh_extension = "inp"
     48elif solver.lower() in ["sierra", "adagio"]:
     49    mesh_extension = "g"
     50else:
     51    raise RuntimeError(f"Unknown solver '{solver}'")
     52workflow.extend(
     53    env.PythonScript(
     54        target=[f"rectangle_mesh.{mesh_extension}", "rectangle_mesh.cub"],
     55        source=["#/modsim_package/cubit/rectangle_mesh.py", "rectangle_partition.cub"],
     56        subcommand_options="--element-type ${element_type} --solver ${solver}",
     57        element_type=element_type,
     58        solver=solver,
     59    )
     60)
     61
     62
     63# Cube 3D
     64# Geometry
     65workflow.extend(
     66    env.PythonScript(
     67        target=["cube_geometry.cub"],
     68        source=["#/modsim_package/cubit/cube_geometry.py"],
     69        subcommand_options="",
     70    )
     71)
     72
     73# Partition
     74workflow.extend(
     75    env.PythonScript(
     76        target=["cube_partition.cub"],
     77        source=["#/modsim_package/cubit/cube_partition.py", "cube_geometry.cub"],
     78        subcommand_options="",
     79    )
     80)
     81
     82# Mesh
     83if solver.lower() == "abaqus":
     84    mesh_extension = "inp"
     85elif solver.lower() in ["sierra", "adagio"]:
     86    mesh_extension = "g"
     87else:
     88    raise RuntimeError(f"Unknown solver '{solver}'")
     89workflow.extend(
     90    env.PythonScript(
     91        target=[f"cube_mesh.{mesh_extension}", "cube_mesh.cub"],
     92        source=["#/modsim_package/cubit/cube_mesh.py", "cube_partition.cub"],
     93        subcommand_options="--element-type ${element_type} --solver ${solver}",
     94        element_type=element_type,
     95        solver=solver,
     96    )
     97)
     98
     99# Collector alias based on parent directory name
    100env.Alias(f"{workflow_name}_cubit", workflow)
    101
    102if not env["unconditional_build"] and not env["CUBIT_PROGRAM"]:
    103    print(f"Program 'cubit' was not found in construction environment. Ignoring '{workflow_name}' target(s)")
    104    Ignore([".", workflow_name], workflow)

waves-tutorials/tutorial_cubit/fierro

     1#! /usr/bin/env python
     2"""Rectangle compression workflow: Fierro solve
     3
     4Requires the following ``SConscript(..., exports={})``
     5
     6* ``env`` - The SCons construction environment with the following required keys
     7
     8  * ``unconditional_build`` - Boolean flag to force building of conditionally ignored targets
     9  * ``cubit`` - String path for the Cubit executable
    10"""
    11
    12import pathlib
    13
    14# Inherit the parent construction environment
    15Import("env")
    16
    17# Simulation variables
    18build_directory = pathlib.Path(Dir(".").abspath)
    19workflow_name = build_directory.name
    20
    21# Collect the target nodes to build a concise alias for all targets
    22workflow = []
    23
    24element_type = "HEX"
    25solver = "sierra"
    26SConscript("cubit", exports={"env": env, "element_type": element_type, "solver": solver}, duplicate=False)
    27
    28# Convert mesh file type for Fierro
    29env.PythonScript(
    30    target=["cube_mesh.vtk"],
    31    source=["#/modsim_package/fierro/convert_to_vtk2ascii.py", "cube_mesh.g"],
    32    subcommand_options="--input-format=exodus ${SOURCES[1].abspath} ${TARGET.abspath}",
    33)
    34
    35# SolverPrep
    36fierro_source_list = ["#/modsim_package/fierro/cube_compression.yaml"]
    37fierro_source_list = [pathlib.Path(source_file) for source_file in fierro_source_list]
    38workflow.extend(env.CopySubstfile(fierro_source_list))
    39
    40# Fierro Solve
    41solve_source_list = [source_file.name for source_file in fierro_source_list]
    42solve_source_list.append("cube_mesh.vtk")
    43workflow.extend(
    44    env.FierroImplicit(
    45        target=["cube_compression.stdout", "TecplotTO0.dat", "TecplotTO_undeformed0.dat"],
    46        source=solve_source_list,
    47    )
    48)
    49
    50# Collector alias based on parent directory name
    51env.Alias(workflow_name, workflow)
    52
    53if not env["unconditional_build"] and not env["FIERRO_IMPLICIT_PROGRAM"]:
    54    print(f"Program 'fierro' was not found in construction environment. Ignoring '{workflow_name}' target(s)")
    55    Ignore([".", workflow_name], workflow)

## Cubit Journal Files

Unlike Tutorial: Cubit+Abaqus and Tutorial: Cubit+Sierra, this tutorial creates a 3D cube geometry. The Fierro implicit solver works best with 3D geometries and meshes. The 3D geometry, mesh, and simulation match the uniaxial compression boundary conditions of the 2D simulation as closely as possible with the Fierro boundary conditions.

7. Review the following journal files in the `waves-tutorials/modsim_package/cubit` directory.

The Cubit journal files include a similar CLI to the Abaqus journal files introduced in Tutorial 02: Partition and Mesh files. Besides the differences in Abaqus and Cubit commands, the major difference between the Abaqus and Cubit journal files is the opportunity to use Python 3 with Cubit, where Abaqus journal files must use the Abaqus controlled installation of Python 2. The API and CLI built from the Cubit journal files’ docstrings may be found in the WAVES-TUTORIAL API for cubit and the WAVES-TUTORIAL CLI for cubit, respectively.

waves-tutorials/tutorial_cubit/modsim_package/cubit/cube_geometry.py

     1import sys
     2import pathlib
     3import argparse
     4
     5import cubit
     6
     7
     8def main(output_file, width, height, depth):
     9    """Create a simple cube geometry.
    10
    11    This script creates a simple Cubit model with a single cube part.
    12
    13    :param str output_file: The output file for the Cubit model. Will be stripped of the extension and ``.cub`` will be
    14        used.
    15    :param float width: The cube width (X-axis)
    16    :param float height: The cube height (Y-axis)
    17    :param float depth: The cube depth (Z-axis)
    18
    19    :returns: writes ``output_file``.cub
    20    """
    21    output_file = pathlib.Path(output_file).with_suffix(".cub")
    22
    23    cubit.init(["cubit", "-noecho", "-nojournal", "-nographics", "-batch"])
    24    cubit.cmd("new")
    25    cubit.cmd("reset")
    26
    27    cubit.cmd(f"brick x {width} y {height} z {depth}")
    28    cubit.cmd(f"move volume 1 x {width / 2} y {height / 2} z {depth / 2} include_merged")
    29
    30    cubit.cmd(f"save as '{output_file}' overwrite")
    31
    32
    33def get_parser():
    34    script_name = pathlib.Path(__file__)
    35    # Set default parameter values
    36    default_output_file = script_name.with_suffix(".cub").name
    37    default_width = 1.0
    38    default_height = 1.0
    39    default_depth = 1.0
    40
    41    prog = f"python {script_name.name} "
    42    cli_description = "Create a simple cube geometry and write an ``output_file``.cub Cubit model file."
    43    parser = argparse.ArgumentParser(description=cli_description, prog=prog)
    44    parser.add_argument(
    45        "--output-file",
    46        type=str,
    47        default=default_output_file,
    48        # fmt: off
    49        help="The output file for the Cubit model. "
    50             "Will be stripped of the extension and ``.cub`` will be used, e.g. ``output_file``.cub "
    51             "(default: %(default)s",
    52        # fmt: on
    53    )
    54    parser.add_argument(
    55        "--width",
    56        type=float,
    57        default=default_width,
    58        help="The cube width (X-axis)",
    59    )
    60    parser.add_argument(
    61        "--height",
    62        type=float,
    63        default=default_height,
    64        help="The cube height (Y-axis)",
    65    )
    66    parser.add_argument(
    67        "--depth",
    68        type=float,
    69        default=default_depth,
    70        help="The cube depth (Z-axis)",
    71    )
    72    return parser
    73
    74
    75if __name__ == "__main__":
    76    parser = get_parser()
    77    args = parser.parse_args()
    78    sys.exit(
    79        main(
    80            output_file=args.output_file,
    81            width=args.width,
    82            height=args.height,
    83            depth=args.depth,
    84        )
    85    )

waves-tutorials/tutorial_cubit/modsim_package/cubit/cube_partition.py

      1import sys
      2import shutil
      3import pathlib
      4import argparse
      5
      6import cubit
      7
      8
      9def main(input_file, output_file):
     10    """Partition the simple cube geometry created by ``cube_geometry.py``
     11
     12    This script partitions a simple Cubit model with a single cube part.
     13
     14    **Feature labels:**
     15
     16    * ``top``: +Y surface
     17    * ``bottom``: -Y surface
     18    * ``left``: -X surface
     19    * ``right``: +X surface
     20    * ``front``: +Z surface
     21    * ``back``: -Z surface
     22
     23    :param str input_file: The Cubit model file created by ``cube_geometry.py``. Will be stripped of the extension
     24        and ``.cub`` will be used.
     25    :param str output_file: The output file for the Cubit model. Will be stripped of the extension and ``.cub`` will be
     26        used.
     27
     28    :returns: writes ``output_file``.cub
     29    """
     30    input_file = pathlib.Path(input_file).with_suffix(".cub")
     31    output_file = pathlib.Path(output_file).with_suffix(".cub")
     32
     33    # Avoid modifying the contents or timestamp on the input file.
     34    # Required to get conditional re-builds with a build system such as GNU Make, CMake, or SCons
     35    if input_file != output_file:
     36        shutil.copyfile(input_file, output_file)
     37
     38    cubit.init(["cubit", "-noecho", "-nojournal", "-nographics", "-batch"])
     39    cubit.cmd("new")
     40    cubit.cmd("reset")
     41
     42    cubit.cmd(f"open '{output_file}'")
     43
     44    cubit.cmd("nodeset 1 add surface 5")
     45    cubit.cmd("nodeset 1 name 'top'")
     46    cubit.cmd("sideset 1 add surface 5")
     47    cubit.cmd("sideset 1 name 'top'")
     48
     49    cubit.cmd("nodeset 2 add surface 3")
     50    cubit.cmd("nodeset 2 name 'bottom'")
     51    cubit.cmd("sideset 2 add surface 3")
     52    cubit.cmd("sideset 2 name 'bottom'")
     53
     54    cubit.cmd("nodeset 3 add surface 4")
     55    cubit.cmd("nodeset 3 name 'left'")
     56    cubit.cmd("sideset 3 add surface 4")
     57    cubit.cmd("sideset 3 name 'left'")
     58
     59    cubit.cmd("nodeset 4 add surface 6")
     60    cubit.cmd("nodeset 4 name 'right'")
     61    cubit.cmd("sideset 4 add surface 6")
     62    cubit.cmd("sideset 4 name 'right'")
     63
     64    cubit.cmd("nodeset 5 add surface 1")
     65    cubit.cmd("nodeset 5 name 'front'")
     66    cubit.cmd("sideset 5 add surface 1")
     67    cubit.cmd("sideset 5 name 'front'")
     68
     69    cubit.cmd("nodeset 6 add surface 2")
     70    cubit.cmd("nodeset 6 name 'back'")
     71    cubit.cmd("sideset 6 add surface 2")
     72    cubit.cmd("sideset 6 name 'back'")
     73
     74    cubit.cmd(f"save as '{output_file}' overwrite")
     75
     76
     77def get_parser():
     78    script_name = pathlib.Path(__file__)
     79    # Set default parameter values
     80    default_input_file = script_name.with_suffix(".cub").name.replace("_partition", "_geometry")
     81    default_output_file = script_name.with_suffix(".cub").name
     82
     83    prog = f"python {script_name.name} "
     84    cli_description = (
     85        "Partition the simple cube geometry created by ``cube_geometry.py`` "
     86        "and write an ``output_file``.cub Cubit model file."
     87    )
     88    parser = argparse.ArgumentParser(description=cli_description, prog=prog)
     89    parser.add_argument(
     90        "--input-file",
     91        type=str,
     92        default=default_input_file,
     93        # fmt: off
     94        help="The Cubit model file created by ``cube_geometry.py``. "
     95             "Will be stripped of the extension and ``.cub`` will be used, e.g. ``input_file``.cub "
     96             "(default: %(default)s",
     97        # fmt: on
     98    )
     99    parser.add_argument(
    100        "--output-file",
    101        type=str,
    102        default=default_output_file,
    103        # fmt: off
    104        help="The output file for the Cubit model. "
    105             "Will be stripped of the extension and ``.cub`` will be used, e.g. ``output_file``.cub "
    106             "(default: %(default)s",
    107        # fmt: on
    108    )
    109    return parser
    110
    111
    112if __name__ == "__main__":
    113    parser = get_parser()
    114    args = parser.parse_args()
    115    sys.exit(
    116        main(
    117            input_file=args.input_file,
    118            output_file=args.output_file,
    119        )
    120    )

waves-tutorials/tutorial_cubit/modsim_package/cubit/cube_mesh.py

      1import sys
      2import shutil
      3import pathlib
      4import argparse
      5
      6import cubit
      7
      8
      9def main(input_file, output_file, global_seed, element_type="QUAD", solver="abaqus"):
     10    """Mesh the simple cube geometry partitioned by ``cube_partition.py``
     11
     12    This script meshes a simple Cubit model with a single cube part.
     13
     14    **Feature labels:**
     15
     16    * ``NODES`` - all part nodes
     17    * ``ELEMENTS`` - all part elements
     18
     19    :param str input_file: The Cubit model file created by ``cube_partition.py``. Will be stripped of the extension
     20        and ``.cub`` will be used.
     21    :param str output_file: The output file for the Cubit model. Will be stripped of the extension and ``.cub`` and
     22        ``.inp`` will be used for the model and orphan mesh output files, respectively.
     23    :param float global_seed: The global mesh seed size
     24    :param str element_type: The model element type. Must be a supported Cubit 4 node element type.
     25    :param str solver: The solver type to use when exporting the mesh
     26
     27    :returns: writes ``output_file``.cub and ``output_file``.inp
     28
     29    :raises RuntimeError: If the solver is not supported
     30    """
     31    input_file = pathlib.Path(input_file).with_suffix(".cub")
     32    output_file = pathlib.Path(output_file).with_suffix(".cub")
     33    abaqus_mesh_file = output_file.with_suffix(".inp")
     34    sierra_mesh_file = output_file.with_suffix(".g")
     35
     36    # Avoid modifying the contents or timestamp on the input file.
     37    # Required to get conditional re-builds with a build system such as GNU Make, CMake, or SCons
     38    if input_file != output_file:
     39        shutil.copyfile(input_file, output_file)
     40
     41    cubit.init(["cubit", "-noecho", "-nojournal", "-nographics", "-batch"])
     42    cubit.cmd("new")
     43    cubit.cmd("reset")
     44
     45    cubit.cmd(f"open '{output_file}'")
     46
     47    cubit.cmd(f"volume 1 size {global_seed}")
     48    cubit.cmd("mesh volume 1")
     49    cubit.cmd("set duplicate block elements off")
     50
     51    cubit.cmd("nodeset 9 add volume 1")
     52    cubit.cmd("nodeset 9 name 'NODES'")
     53
     54    cubit.cmd("block 1 add volume 1")
     55    cubit.cmd(f"block 1 name 'ELEMENTS' Element type {element_type}")
     56
     57    cubit.cmd(f"save as '{output_file}' overwrite")
     58
     59    if solver.lower() == "abaqus":
     60        # Export Abaqus orphan mesh for Abaqus workflow
     61        cubit.cmd(f"export abaqus '{abaqus_mesh_file}' partial dimension 2 block 1 overwrite everything")
     62    elif solver.lower() in ["sierra", "adagio"]:
     63        # Export Genesis file for Sierra workflow
     64        cubit.cmd(f"export mesh '{sierra_mesh_file}' overwrite")
     65    else:
     66        raise RuntimeError(f"Uknown solver '{solver}'")
     67
     68
     69def get_parser():
     70    script_name = pathlib.Path(__file__)
     71    # Set default parameter values
     72    default_input_file = script_name.with_suffix(".cub").name.replace("_mesh", "_partition")
     73    default_output_file = script_name.with_suffix(".cub").name
     74    default_global_seed = 1.0
     75    default_element_type = "HEX"
     76    default_solver = "abaqus"
     77
     78    prog = f"python {script_name.name} "
     79    cli_description = (
     80        "Mesh the simple cube geometry partitioned by ``cube_partition.py`` "
     81        "and write an ``output_file``.cub Cubit model file and ``output_file``.inp orphan mesh file."
     82    )
     83    parser = argparse.ArgumentParser(description=cli_description, prog=prog)
     84    parser.add_argument(
     85        "--input-file",
     86        type=str,
     87        default=default_input_file,
     88        # fmt: off
     89        help="The Cubit model file created by ``cube_partition.py``. "
     90             "Will be stripped of the extension and ``.cub`` will be used, e.g. ``input_file``.cub "
     91             "(default: %(default)s",
     92        # fmt: on
     93    )
     94    parser.add_argument(
     95        "--output-file",
     96        type=str,
     97        default=default_output_file,
     98        # fmt: off
     99        help="The output file for the Cubit model. "
    100             "Will be stripped of the extension and ``.cub`` will be used, e.g. ``output_file``.cub",
    101        # fmt: on
    102    )
    103    parser.add_argument(
    104        "--global-seed",
    105        type=float,
    106        default=default_global_seed,
    107        help="The global mesh seed size (default: %(default)s)",
    108    )
    109    parser.add_argument(
    110        "--element-type",
    111        type=str,
    112        default=default_element_type,
    113        help="The model element type. Must be a supported Cubit 4 node element type. " "(default: %(default)s)",
    114    )
    115    parser.add_argument(
    116        "--solver",
    117        type=str,
    118        default=default_solver,
    119        choices=["abaqus", "sierra", "adagio"],
    120        help="The target solver for the mesh file. (default: %(default)s)",
    121    )
    122
    123    return parser
    124
    125
    126if __name__ == "__main__":
    127    parser = get_parser()
    128    args = parser.parse_args()
    129    sys.exit(
    130        main(
    131            input_file=args.input_file,
    132            output_file=args.output_file,
    133            global_seed=args.global_seed,
    134            element_type=args.element_type,
    135            solver=args.solver,
    136        )
    137    )

## Python Script

Fierro doesn’t read Sierra formatted (Genesis/Exodus) meshes natively. We need to convert from the Cubit Genesis mesh to an older vtk ASCII text mesh file with meshio. The Fierro development team provided a draft conversion script that has been updated here to use a similar CLI to meshio.

waves-tutorials/tutorial_cubit/modsim_package/fierro/convert_to_vtk2ascii.py

     1import pathlib
     2import argparse
     3
     4import meshio
     5
     6
     7def get_parser():
     8    cli_description = (
     9        "Convert mesh files to older VTK ASCII version 2 mesh files required by Fierro with MeshIO API. "
    10        "This output is not supported by the MeshIO CLI and must be constructed manually."
    11    )
    12    parser = argparse.ArgumentParser(description=cli_description)
    13    parser.add_argument(
    14        "infile",
    15        type=pathlib.Path,
    16        help="Input file to convert",
    17    )
    18    parser.add_argument(
    19        "outfile",
    20        type=pathlib.Path,
    21        help="Output file to write",
    22    )
    23    parser.add_argument(
    24        "-i",
    25        "--input-format",
    26        type=str,
    27        default=None,
    28        help="Explicit input file format (default: %(default)s)",
    29    )
    30    return parser
    31
    32
    33def main():
    34    parser = get_parser()
    35    args = parser.parse_args()
    36
    37    infile = pathlib.Path(args.infile).resolve()
    38    mesh = meshio.read(infile, file_format=args.input_format)
    39
    40    # Ensure input mesh structure meets Fierro criteria
    41    if len(mesh.cells) != 1:
    42        raise ValueError("More than one kind of cell type present")
    43    if mesh.cells[0].dim == 1:
    44        raise ValueError("Input file is 1D, only 2D/3D work")
    45    if mesh.cells[0].type != "hexahedron" and mesh.cells[0].type != "quad":
    46        raise ValueError("Cell type is not quad/hexahedron")
    47
    48    points = mesh.points
    49    npoints = points.shape[0]
    50
    51    ndim = mesh.cells[0].dim
    52    cells = mesh.cells[0].data
    53    ncells = cells.shape[0]
    54    pts_per_cell = cells.shape[1]
    55
    56    outfile = pathlib.Path(args.outfile).resolve()
    57    with open(outfile, "w") as mesh_out:
    58
    59        mesh_out.write("# vtk DataFile Version 2.0\n")
    60        mesh_out.write("meshio converted to Fierro VTK\n")
    61        mesh_out.write("ASCII\n")
    62        mesh_out.write("DATASET UNSTRUCTURED_GRID\n")
    63        # Write points
    64        mesh_out.write(f"POINTS {npoints} double\n")
    65        for n in range(npoints):
    66            for i in range(ndim):
    67                mesh_out.write(f"{points[n, i]} ")
    68            mesh_out.write("\n")
    69        mesh_out.write("\n")
    70
    71        # Write cells
    72        mesh_out.write(f"CELLS {ncells} {ncells * pts_per_cell + ncells}\n")
    73        for n in range(ncells):
    74            mesh_out.write(f"{pts_per_cell} ")
    75            for i in range(pts_per_cell):
    76                mesh_out.write(f"{cells[n, i]} ")
    77            mesh_out.write("\n")
    78        mesh_out.write("\n")
    79        mesh_out.write(f"CELL_TYPES {ncells}\n")
    80        if ndim == 2:
    81            for n in range(ncells):
    82                mesh_out.write("9\n")
    83        else:
    84            for n in range(ncells):
    85                mesh_out.write("12\n")
    86
    87
    88if __name__ == "__main__":
    89    main()

## Fierro Input File(s)

8. Create or review the Fierro input file from the contents below

waves-tutorials/tutorial_cubit_fierro/modsim_package/fierro/cube_compression.yaml

     1num_dims: 3
     2input_options:
     3    mesh_file_format: vtk
     4    mesh_file_name: cube_mesh.vtk
     5    element_type: hex8
     6    zero_index_base: true
     7
     8output_options:
     9  output_fields:
    10    - displacement
    11    - strain
    12
    13materials:
    14  - id: 0
    15    elastic_modulus: 100
    16    poisson_ratio: 0.3
    17    density: 0.27000e-08
    18    initial_temperature: 293
    19
    20fea_module_parameters:
    21  - type: Elasticity
    22    material_id: 0
    23    boundary_conditions:
    24      - surface:
    25          type: y_plane
    26          # TODO: replace with element set "bottom"
    27          plane_position: 0.0
    28        type: displacement
    29        value: 0.0
    30
    31      - surface:
    32          type: y_plane
    33          # TODO: replace with element set "top", or at least the "height" parameter
    34          plane_position: 1.0
    35        type: displacement
    36        # TODO: replace with "displacement" parameter
    37        value: -0.01

## SConstruct

Note that Fierro differs from other solvers in the tutorials. Fierro 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_cubit/SConstruct

    #! /usr/bin/env python

    import os
    import sys
    import typing
    import pathlib
    import subprocess

    import waves

    # Comments used in tutorial code snippets: marker-1

    # Accept command line options with fall back default values
    AddOption(
        "--build-dir",
        dest="variant_dir_base",
        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 flag to force building of conditionally ignored targets. (default: '%default')",
    )
    AddOption(
        "--print-build-failures",
        dest="print_build_failures",
        default=False,
        action="store_true",
        help="Print task *.stdout target file(s) on build failures. (default: '%default')",
    )
    # Python optparse appends to the default list instead of overriding. Must implement default/override ourselves.
    default_abaqus_commands = [
        "/apps/abaqus/Commands/abq2024",
        "/usr/projects/ea/abaqus/Commands/abq2024",
        "abq2024",
        "abaqus",
    ]
    AddOption(
        "--abaqus-command",
        dest="abaqus_command",
        nargs=1,
        type="string",
        action="append",
        metavar="COMMAND",
        help=f"Override for the Abaqus command. Repeat to specify more than one (default: {default_abaqus_commands})",
    )
    default_cubit_commands = [
        "/apps/Cubit-16.12/cubit",
        "/usr/projects/ea/Cubit/Cubit-16.12/cubit",
        "cubit",
    ]
    AddOption(
        "--cubit-command",
        dest="cubit_command",
        nargs=1,
        type="string",
        action="append",
        metavar="COMMAND",
        help=f"Override for the Cubit command. Repeat to specify more than one (default: {default_cubit_commands})",
    )

    # Comments used in tutorial code snippets: marker-2

    # Inherit user's full environment and set project variables
    env = waves.scons_extensions.WAVESEnvironment(
        ENV=os.environ.copy(),
        variant_dir_base=pathlib.Path(GetOption("variant_dir_base")),
        unconditional_build=GetOption("unconditional_build"),
        print_build_failures=GetOption("print_build_failures"),
        abaqus_commands=GetOption("abaqus_command"),
        cubit_commands=GetOption("cubit_command"),
    )

    # Conditionally print failed task *.stdout files
    env.PrintBuildFailures(print_stdout=env["print_build_failures"])

    # Comments used in tutorial code snippets: marker-3

    # Find required programs for conditional target ignoring and absolute path for use in target actions
    env["ABAQUS_PROGRAM"] = env.AddProgram(
        env["abaqus_commands"] if env["abaqus_commands"] is not None else default_abaqus_commands
    )
    env["CUBIT_PROGRAM"] = env.AddCubit(
        env["cubit_commands"] if env["cubit_commands"] is not None else default_cubit_commands
    )
    env["FIERRO_IMPLICIT_PROGRAM"] = env.AddProgram(["fierro-parallel-implicit"])

    # Sierra requires a separate construction environment
    def return_modulepath(modules: typing.Iterable[str]) -> pathlib.Path:
        """Return parent path of first found module or the current working directory"""
        return next((pathlib.Path(path).parent for path in modules if pathlib.Path(path).exists()), pathlib.Path("."))

    sierra_modules = [
        "/projects/aea_compute/modulefiles/sierra/5.21.6",
    ]
    sierra_modulefile_parent = return_modulepath(sierra_modules)
    try:
        envSierra = waves.scons_extensions.shell_environment(f"module use {sierra_modulefile_parent} && module load sierra")
    except subprocess.CalledProcessError:
        print("Failed to initialize the Sierra environment", file=sys.stderr)
        envSierra = Environment()
    envSierra["sierra"] = waves.scons_extensions.add_program(envSierra, ["sierra"])

    # Comments used in tutorial code snippets: marker-4

    # Set project internal variables and variable substitution dictionaries
    project_name = "WAVES-TUTORIAL"
    version = "0.1.0"
    project_dir = pathlib.Path(Dir(".").abspath)
    project_variables = {
        "project_name": project_name,
        "project_dir": project_dir,
        "version": version,
    }
    for key, value in project_variables.items():
        env[key] = value

    # Comments used in tutorial code snippets: marker-5

    # Add builders and pseudo-builders
    env.Append(BUILDERS={})

    envSierra.Append(
        BUILDERS={
            "Sierra": waves.scons_extensions.sierra_builder_factory(program=envSierra["sierra"]),
        }
    )

    # Comments used in tutorial code snippets: marker-6

    # Add simulation targets
    workflow_configurations = [
        "abaqus",
        "sierra",
        "fierro",
    ]
    for workflow in workflow_configurations:
        build_dir = env["variant_dir_base"] / workflow
        SConscript(
            workflow,
            variant_dir=build_dir,
            exports={"env": env, "envSierra": envSierra},
            duplicate=False,
        )

    # Comments used in tutorial code snippets: marker-7

    # Add default target list to help message
    env.Default()  # Empty defaults list to avoid building all simulation targets by default
    # Add aliases to help message so users know what build target options are available
    # This must come *after* all expected Alias definitions and SConscript files.
    env.ProjectHelp()

    # Comments used in tutorial code snippets: marker-8

## Build Targets

9. Build the new targets

    $ pwd
    /path/to/waves-tutorials/tutorial_cubit
    $ scons fierro
    scons: Reading SConscript files ...
    Checking whether /apps/abaqus/Commands/abq2024 program exists.../apps/abaqus/Commands/abq2024
    Checking whether abq2024 program exists...no
    Checking whether /apps/Cubit-16.12/cubit program exists.../apps/Cubit-16.12/cubit
    Checking whether cubit program exists...no
    Checking whether fierro-parallel-implicit program exists.../projects/aea_compute/waves-env/bin/fierro-parallel-implicit
    Sourcing the shell environment with command 'module use /projects/aea_compute/modulefiles && module load sierra' ...
    Checking whether sierra program exists.../projects/sierra/sierra5193/install/tools/sntools/engine/sierra
    scons: done reading SConscript files.
    scons: Building targets ...
    Copy("build/fierro/cube_compression.yaml", "modsim_package/fierro/cube_compression.yaml")
    Copy("build/fierro/elasticity3D.xml", "modsim_package/fierro/elasticity3D.xml")
    cd /home/roppenheimer/waves-tutorials/tutorial_cubit/build/fierro && python
    /home/roppenheimer/waves-tutorials/tutorial_cubit/modsim_package/cubit/cube_geometry.py >
    /home/roppenheimer/waves-tutorials/tutorial_cubit/build/fierro/cube_geometry.cub.stdout 2>&1
    cd /home/roppenheimer/waves-tutorials/tutorial_cubit/build/fierro && python
    /home/roppenheimer/waves-tutorials/tutorial_cubit/modsim_package/cubit/cube_partition.py >
    /home/roppenheimer/waves-tutorials/tutorial_cubit/build/fierro/cube_partition.cub.stdout 2>&1
    cd /home/roppenheimer/waves-tutorials/tutorial_cubit/build/fierro && python
    /home/roppenheimer/waves-tutorials/tutorial_cubit/modsim_package/cubit/cube_mesh.py --element-type HEX
    --solver sierra > /home/roppenheimer/waves-tutorials/tutorial_cubit/build/fierro/cube_mesh.g.stdout 2>&1
    cd /home/roppenheimer/waves-tutorials/tutorial_cubit/build/fierro && python
    /home/roppenheimer/waves-tutorials/tutorial_cubit/modsim_package/fierro/convert_to_vtk2ascii.py
    --input-format=exodus /home/roppenheimer/waves-tutorials/tutorial_cubit/build/fierro/cube_mesh.g
    /home/roppenheimer/waves-tutorials/tutorial_cubit/build/fierro/cube_mesh.vtk >
    /home/roppenheimer/waves-tutorials/tutorial_cubit/build/fierro/cube_mesh.vtk.stdout 2>&1
    cd /home/roppenheimer/waves-tutorials/tutorial_cubit/build/fierro && mpirun -np 1
    fierro-parallel-implicit
    /home/roppenheimer/waves-tutorials/tutorial_cubit/build/fierro/cube_compression.yaml >
    /home/roppenheimer/waves-tutorials/tutorial_cubit/build/fierro/cube_compression.stdout 2>&1
    scons: done building targets.

## Output Files

Explore the contents of the `build` directory using the `tree` command against the `build` directory, as shown below.

    $ pwd
    /home/roppenheimer/waves-tutorials/tutorial_cubit
    $ tree build/fierro/
    build/fierro/
    |-- TecplotTO0.dat
    |-- TecplotTO_undeformed0.dat
    |-- cube_compression.stdout
    |-- cube_compression.yaml
    |-- cube_geometry.cub
    |-- cube_geometry.cub.stdout
    |-- cube_mesh.cub
    |-- cube_mesh.g
    |-- cube_mesh.g.stdout
    |-- cube_mesh.vtk
    |-- cube_mesh.vtk.stdout
    |-- cube_partition.cub
    |-- cube_partition.cub.stdout
    `-- elasticity3D.xml

    0 directories, 14 files