(guide-compilers-simulation-compiler)=
# Simulation Compiler
```{include} ../_guide-sidebar-compiler-membership.md
```
The **SimulationCompiler** turns an OpenVCAD design into a finite-element mesh plus cell-wise simulation data. The supported backends in this guide are:
- **`FENICSX_XDMF`** for **`.xdmf` + `.h5`** mesh/data bundles
- **`ABAQUS_INP`** for **`.inp`** mesh export
This page is a hands-on guide to the exporter workflow. It is organized as three
lessons: a volume-fractions export, an export-only direct-mechanical beam, and a
self-contained OpenVCAD-to-FEniCSx beam solve. It focuses on the tetrahedral
**XDMF/H5** path, shows the matching hexahedral **Abaqus** calls, and explains
the two export modes that matter most:
- **`volume_fractions`** exported as both a stochastic **`material_id`** field and raw **`volume_fraction_*`** fields
- direct scalar fields such as **`modulus`**, **`poissons_ratio`**, and **`density`** exported directly per element
## Prerequisites
- OpenVCAD installed with **`pip install OpenVCAD`**
- Familiarity with the [Getting Started guide](../getting-started.md) and the [Functional Grading Guide](../gradients.md)
The guide example files live under **`examples/compilers/simulation/`**:
- **`01_volume_fractions_export.py`**
- **`02_direct_mechanical_properties_export.py`**
- **`03_fenicsx_load_exported_design.py`**
## What this compiler exports
The simulation compiler always starts from the same ingredients:
1. a prepared OpenVCAD design
2. a target mesh type
3. one export backend
4. the attributes you want carried into the simulation mesh
The mesh itself can be:
- **hexahedral** using a regular voxel-style grid
- **tetrahedral** using a tet mesh with variable sized elements
The attribute path depends on what you attach to the model:
- **`volume_fractions`** is treated as a multi-material mixture. The compiler samples the continuous mixture, performs a deterministic weighted stochastic draw per element, and writes the resulting **`material_id`** field. It also writes the raw **`volume_fraction_*`** cell fields for convenience.
- direct scalar attributes such as **`modulus`**, **`poissons_ratio`**, and **`density`** are sampled directly as cell-wise values. There is no stochastic material draw in this path.
### Why XDMF comes with an H5 file
The **`.xdmf`** file is the lightweight descriptor that tells downstream tools where the mesh and cell datasets live. The bulk numeric arrays live in the paired **`.h5`** file. In practice:
- **`.xdmf`** describes the topology, geometry, and named attributes
- **`.h5`** stores the actual node coordinates, element connectivity, and cell-data arrays
That pairing is the normal way to move simulation-sized meshes into tools such as **DOLFINx/FEniCSx** and viewers such as **ParaView**.
## Lesson 1: Volume fractions
The script **`examples/compilers/simulation/01_volume_fractions_export.py`** builds a centered rectangular bar with a left-to-right material blend. It exports the same design three ways:
- **hex + Abaqus**
- **tet + XDMF/H5 (high resolution)**
- **tet + XDMF/H5 (low resolution)**
```python
import os
import shutil
import pyvcad as pv
import pyvcad_compilers as pvc
import pyvcad_rendering as viz
materials = pv.default_materials
bar_length = 30.0
bar_width = 10.0
bar_height = 10.0
half_length = 0.5 * bar_length
bar = pv.RectPrism(pv.Vec3(0, 0, 0), pv.Vec3(bar_length, bar_width, bar_height))
fractions = pv.VolumeFractionsAttribute(
[
(f"1 - clamp((x + {half_length}) / {bar_length}, 0, 1)", materials.id("red")),
(f"clamp((x + {half_length}) / {bar_length}, 0, 1)", materials.id("blue")),
]
)
bar.set_attribute(pv.DefaultAttributes.VOLUME_FRACTIONS, fractions)
root = bar
output_dir = os.path.join(os.path.dirname(__file__), "output", "volume_fractions")
hex_settings = pvc.SimulationHexMeshSettings()
hex_settings.voxel_size = pv.Vec3(0.25, 0.25, 0.25)
abaqus_settings = pvc.SimulationCompilerSettings()
abaqus_settings.output_directory = output_dir
abaqus_settings.file_prefix = "volume_fractions_hex"
abaqus_settings.backend = pvc.SimulationBackend.ABAQUS_INP
abaqus_settings.mesh_kind = pvc.SimulationMeshKind.HEX
abaqus_settings.hex_settings = hex_settings
abaqus_settings.random_seed = 42
abaqus_settings.material_defs = materials
high_fenics_settings = pvc.SimulationCompilerSettings()
high_fenics_settings.output_directory = output_dir
high_fenics_settings.file_prefix = "volume_fractions_tet_high"
high_fenics_settings.backend = pvc.SimulationBackend.FENICSX_XDMF
high_fenics_settings.mesh_kind = pvc.SimulationMeshKind.TET
high_fenics_settings.random_seed = 42
high_fenics_settings.material_defs = materials
high_tet_settings = pvc.SimulationTetFixedMeshSettings()
high_tet_settings.facet_size = 0.75
high_tet_settings.facet_distance = 0.5
high_tet_settings.cell_size = 0.75
high_fenics_settings.tet_fixed_settings = high_tet_settings
low_fenics_settings = pvc.SimulationCompilerSettings()
low_fenics_settings.output_directory = output_dir
low_fenics_settings.file_prefix = "volume_fractions_tet_low"
low_fenics_settings.backend = pvc.SimulationBackend.FENICSX_XDMF
low_fenics_settings.mesh_kind = pvc.SimulationMeshKind.TET
low_fenics_settings.random_seed = 42
low_fenics_settings.material_defs = materials
low_tet_settings = pvc.SimulationTetFixedMeshSettings()
low_tet_settings.facet_size = 2.25
low_tet_settings.facet_distance = 1.5
low_tet_settings.cell_size = 2.25
low_fenics_settings.tet_fixed_settings = low_tet_settings
pvc.SimulationCompiler(root, abaqus_settings).compile()
pvc.SimulationCompiler(root, high_fenics_settings).compile()
pvc.SimulationCompiler(root, low_fenics_settings).compile()
viz.Render(root, materials)
```
From the repository root, with the project virtual environment activated:
```bash
python examples/compilers/simulation/01_volume_fractions_export.py
```
This example highlights the settings you will change most often:
- **`backend`** selects the output family
- **`mesh_kind`** selects **hex** or **tet**
- **`material_defs`** provides the base material ids used by the sampled **`material_id`** field
- **`random_seed`** makes the weighted stochastic element selection repeatable
- tet mesh sizing controls the amount of element-level mixing you can resolve
### OpenVCAD preview
This is the original continuous **`volume_fractions`** field before any stochastic element assignment:
### High-resolution tet export in ParaView
The high-resolution tetrahedral export makes the stochastic **`material_id`** field look like a well-mixed gradient because there are enough elements to interdigitate the two materials. The raw **`volume_fraction_*`** fields stay smooth because they store the sampled continuous field directly. In this example the suffixes **`1`** and **`3`** come from the material ids assigned by **`pv.default_materials`**.
### Low-resolution tet export in ParaView
The low-resolution export uses the same underlying gradient, but the stochastic **`material_id`** field is much harder to read as a smooth transition because there are fewer elements available to represent the mixing pattern. The raw **`volume_fraction_1`** field still shows the same continuous ramp.
The practical takeaway is simple:
- if your downstream solver only has a discrete material assignment path, you may need a finer mesh so the stochastic **`material_id`** pattern captures the intended mixture behavior
- if your downstream code can use the raw **`volume_fraction_*`** arrays directly, the exporter already saves them for you
ParaView exposes the available cell datasets directly in the attribute selector:
## Lesson 2: Direct mechanical property export
The script **`examples/compilers/simulation/02_direct_mechanical_properties_export.py`** shows the direct scalar path. Here the bar carries:
- **`modulus`** graded along **x**
- **`poissons_ratio`** graded along **z**
This lesson is intentionally export-only. It uses a different beam than Lesson 3:
here **`poissons_ratio`** varies through the beam height, while Lesson 3 uses a
separate cantilever beam where both elastic fields vary along the beam length.
Density is omitted so the walkthrough stays focused on the two scalar fields
that are carried directly into the simulation mesh.
```python
import os
import shutil
import pyvcad as pv
import pyvcad_compilers as pvc
import pyvcad_rendering as viz
bar = pv.RectPrism(pv.Vec3(0, 0, 0), pv.Vec3(40.0, 12.0, 12.0))
bar.set_attribute(
pv.DefaultAttributes.MODULUS,
pv.FloatAttribute("1200 + 800 * clamp((x + 20.0) / 40.0, 0, 1)")
)
bar.set_attribute(
pv.DefaultAttributes.POISSONS_RATIO,
pv.FloatAttribute("0.20 + 0.15 * clamp((z + 6.0) / 12.0, 0, 1)")
)
root = bar
output_dir = "output"
direct_attributes = [
pv.DefaultAttributes.MODULUS,
pv.DefaultAttributes.POISSONS_RATIO,
]
fenics_settings = pvc.SimulationCompilerSettings()
fenics_settings.output_directory = output_dir
fenics_settings.file_prefix = "mechanical_tet"
fenics_settings.backend = pvc.SimulationBackend.FENICSX_XDMF
fenics_settings.mesh_kind = pvc.SimulationMeshKind.TET
fenics_settings.random_seed = 7
fenics_settings.direct_attributes = direct_attributes
tet_settings = pvc.SimulationTetFixedMeshSettings()
tet_settings.facet_size = 0.75
tet_settings.facet_distance = 0.75
tet_settings.cell_size = 0.75
fenics_settings.tet_fixed_settings = tet_settings
pvc.SimulationCompiler(root, fenics_settings).compile()
viz.Render(root)
```
Run the export with the **`fenicsx`** conda environment active:
```bash
conda activate fenicsx
python examples/compilers/simulation/02_direct_mechanical_properties_export.py
```
The key setting here is **`direct_attributes`**. It tells the compiler which
non-volume-fraction fields to carry into the output mesh as direct cell data.
Unlike the **`volume_fractions`** workflow, these attributes are not converted
into a stochastic material assignment. Each element simply stores its sampled
scalar value.
That means:
- the exported fields remain smooth as long as the underlying field varies smoothly
- mesh resolution still matters for how accurately the gradient is sampled
- but resolution is no longer about representing a stochastic mixing pattern
### OpenVCAD previews
### ParaView views of the exported cell fields
### Abaqus export from the same Lesson 2 beam
The same Lesson 2 design can be exported to Abaqus by switching the backend and
choosing a hexahedral mesh:
```python
hex_settings = pvc.SimulationHexMeshSettings()
hex_settings.voxel_size = pv.Vec3(1.5, 1.5, 1.5)
abaqus_settings = pvc.SimulationCompilerSettings()
abaqus_settings.output_directory = "output"
abaqus_settings.file_prefix = "mechanical_hex"
abaqus_settings.backend = pvc.SimulationBackend.ABAQUS_INP
abaqus_settings.mesh_kind = pvc.SimulationMeshKind.HEX
abaqus_settings.random_seed = 7
abaqus_settings.direct_attributes = [
pv.DefaultAttributes.MODULUS,
pv.DefaultAttributes.POISSONS_RATIO,
]
abaqus_settings.hex_settings = hex_settings
pvc.SimulationCompiler(root, abaqus_settings).compile()
```
Think of the Abaqus path as the same mesh-and-attributes export idea written
into **`.inp`** form instead of an **XDMF/H5** bundle.
## Lesson 3: Solving the exported beam in FEniCSx
The script **`examples/compilers/simulation/03_fenicsx_load_exported_design.py`**
is the full one-shot workflow. It builds a second graded beam inside the same
script, exports the FEniCSx XDMF/H5 bundle, reloads the exported cell-wise
material fields inside DOLFINx, and solves a small-strain static cantilever
problem.
Lesson 3 intentionally uses a different beam than Lesson 2. In this solved
cantilever example, both **`modulus`** and **`poissons_ratio`** ramp along the
beam length so the constitutive variation follows the loading direction.
Install notes:
```bash
conda create -n fenicsx -c conda-forge fenics-dolfinx mpi4py h5py numpy
conda activate fenicsx
```
If you already have a working DOLFINx environment, **`h5py`** and **`numpy`**
can also be installed with **`pip`** inside that environment.
### OpenVCAD previews for the Lesson 3 beam
The simulation script builds its own beam before exporting it to FEniCSx.
These previews show the exact OpenVCAD fields used by the solve:
The lesson-3 beam definition is intentionally small and direct:
```python
def build_design():
beam = pv.RectPrism(pv.Vec3(0, 0, 0), pv.Vec3(40.0, 12.0, 12.0))
beam.set_attribute(
pv.DefaultAttributes.MODULUS,
pv.FloatAttribute("1200 + 800 * clamp((x + 20.0) / 40.0, 0, 1)")
)
beam.set_attribute(
pv.DefaultAttributes.POISSONS_RATIO,
pv.FloatAttribute("0.20 + 0.15 * clamp((x + 20.0) / 40.0, 0, 1)")
)
return beam
```
### Governing equations
The solve is the standard 3D isotropic linear-elastic form with a spatially
varying modulus **`E(x)`** and Poisson ratio **`nu(x)`**:
$$
\varepsilon(u) = \mathrm{sym}(\nabla u)
$$
$$
\sigma(u) = 2\mu(x)\varepsilon(u) + \lambda(x)\,\mathrm{tr}(\varepsilon(u)) I
$$
with Lame parameters reconstructed from the exported OpenVCAD fields:
$$
\mu(x) = \frac{E(x)}{2(1 + \nu(x))}
\qquad
\lambda(x) = \frac{E(x)\nu(x)}{(1 + \nu(x))(1 - 2\nu(x))}
$$
The weak form is:
$$
\int_{\Omega} \sigma(u) : \varepsilon(v)\,dx = \int_{\Gamma_{\mathrm{load}}} t \cdot v\,ds
$$
with the left beam face fixed (**`u = 0`**) and a uniform end traction
**`t = (0, 0, -5)`** applied on the right beam face.
Because the exported tetrahedral surface is slightly perturbed from the exact
analytic box, the script locates the left and right beam faces using a small
**`0.05 mm`** threshold around the mesh bounding box instead of relying on
exact **`x == min/max`** comparisons.
### Key implementation snippets
The full script lives at **`examples/compilers/simulation/03_fenicsx_load_exported_design.py`**.
The snippets below show the important stages without reproducing the whole file.
#### 1. Export once on rank 0
```python
export_error = None
if comm.rank == 0:
try:
root = build_design()
fenics_settings = pvc.SimulationCompilerSettings()
fenics_settings.output_directory = output_dir
fenics_settings.file_prefix = "simulation_results"
fenics_settings.backend = pvc.SimulationBackend.FENICSX_XDMF
fenics_settings.mesh_kind = pvc.SimulationMeshKind.TET
fenics_settings.direct_attributes = [
pv.DefaultAttributes.MODULUS,
pv.DefaultAttributes.POISSONS_RATIO,
]
pvc.SimulationCompiler(root, fenics_settings).compile()
except Exception as exc:
export_error = str(exc)
export_error = comm.bcast(export_error, root=0)
if export_error is not None:
raise RuntimeError(f"OpenVCAD export failed on MPI rank 0: {export_error}")
```
#### 2. Reload the exported cell fields with MPI-safe cell indexing
The example uses **`beam_mesh.topology.original_cell_index`** so each MPI rank
pulls the correct subset of the exported cell arrays back into its local DG0
storage.
```python
with XDMFFile(comm, xdmf_path, "r") as xdmf:
beam_mesh = xdmf.read_mesh(name="OpenVCADMesh")
cell_space = fem.functionspace(beam_mesh, ("DG", 0))
modulus = fem.Function(cell_space, name="modulus")
poissons_ratio = fem.Function(cell_space, name="poissons_ratio")
with h5py.File(h5_path, "r") as h5:
modulus_data = np.asarray(h5["/CellData/modulus"])
poissons_ratio_data = np.asarray(h5["/CellData/poissons_ratio"])
original_cell_index = np.asarray(beam_mesh.topology.original_cell_index, dtype=np.int64)
modulus.x.array[:] = modulus_data[original_cell_index]
poissons_ratio.x.array[:] = poissons_ratio_data[original_cell_index]
modulus.x.scatter_forward()
poissons_ratio.x.scatter_forward()
```
#### 3. Solve and patch the exported file in place
After reloading the DG0 fields, the script reconstructs the Lam\'e parameters,
solves the cantilever problem, evaluates DG0 von Mises stress, gathers the
result arrays to rank 0, and updates the original OpenVCAD export in place.
That means **`simulation_results.xdmf`** keeps the same mesh but gains two new
fields:
- **`displacement`** as a node-centered vector field
- **`von_mises`** as a cell-centered scalar field
Run the full one-shot workflow with the **`fenicsx`** environment active:
```bash
conda activate fenicsx
python examples/compilers/simulation/03_fenicsx_load_exported_design.py
```
## ParaView inspection workflow
1. Open **`simulation_results.xdmf`**
2. Click **Apply**
3. Switch the coloring dropdown between **`modulus`**, **`poissons_ratio`**,
**`displacement`**, and **`von_mises`**
4. Use **Warp By Vector** with **`displacement`** if you want a deformed beam view
## Practical settings reference
| Setting | Meaning |
| ------- | ------- |
| **`output_directory`** | Folder where the compiler writes the exported files. |
| **`file_prefix`** | Prefix used for the written files. |
| **`backend`** | Output family such as **`FENICSX_XDMF`** or **`ABAQUS_INP`**. |
| **`mesh_kind`** | Element family: **`HEX`** or **`TET`**. |
| **`hex_settings.voxel_size`** | Regular hexahedral cell size in millimeters. |
| **`tet_fixed_settings.facet_size`** | Surface triangle size target for fixed tet meshing. |
| **`tet_fixed_settings.facet_distance`** | Surface approximation tolerance for fixed tet meshing. |
| **`tet_fixed_settings.cell_size`** | Interior tetrahedral size target. |
| **`random_seed`** | Seed used for deterministic stochastic material assignment. |
| **`material_defs`** | Base material definitions used when exporting **`volume_fractions`**. |
| **`direct_attributes`** | List of scalar attributes to export directly as cell data. |
## Further examples
- **`examples/compilers/simulation/01_volume_fractions_export.py`** — stochastic material assignment plus raw volume-fraction datasets
- **`examples/compilers/simulation/02_direct_mechanical_properties_export.py`** — export-only direct scalar mechanical fields with **`poissons_ratio`** varying through **z**
- **`examples/compilers/simulation/03_fenicsx_load_exported_design.py`** — self-contained DOLFINx/FEniCSx cantilever solve with both elastic fields varying along **x**