(guide-simulation-driven-design)= # Simulation-driven design This tutorial shows how to bring a finite-element result back into OpenVCAD and use it like any other spatial attribute. We will walk through a cantilever that places more Vero material where its baseline strain energy is high, then use the same pattern to grade a BCC lattice. You should already be comfortable with [Getting Started](getting-started.md) and [Functional grading](gradients.md). You do not need prior FEniCSx experience to follow the OpenVCAD parts of the tutorial. The workflow is: ```text make geometry -> create a simulation mesh -> solve -> import the result -> check coverage -> turn the result into an attribute -> update the design ``` One rule keeps the workflow easy to reason about: **simulation results are data, not geometry**. In these studies, we retain the original OpenVCAD geometry and attach result attributes to it. If your solver mesh came from another CAD system, import that source geometry separately and align it with the result dataset. ## 1. Run the cantilever study The complete example is {download}`01_cantilever_material_feedback.py <../../../examples/applications/simulation_driven_design/01_cantilever_material_feedback.py>`. It starts with a 30 x 6 x 6 mm cantilever, solves a soft Agilus30 baseline, imports the strain-energy result, and converts that result into an Agilus30/Vero composition. The study uses a pinned conda environment so FEniCSx, PETSc, and MPI do not become normal OpenVCAD dependencies. Create it once: ```bash conda env create --file examples/applications/simulation_driven_design/environment.yml ``` Install OpenVCAD normally from PyPI into that environment: ```bash conda run -n openvcad-fenicsx python -m pip install openvcad ``` Now run the faster tutorial-sized version: ```bash conda run -n openvcad-fenicsx python \ examples/applications/simulation_driven_design/01_cantilever_material_feedback.py --fast ``` Use the same command without `--fast` for the default mesh and higher-resolution outputs. Study outputs go to `.tmp/simulation_driven_design/study1_cantilever/`, including the XDMF/HDF5 result bundle, renders, solver log, and `metrics.json`. :::{note} The study runs in serial and rejects MPI sizes above one. Native Windows is not supported by this conda-forge FEniCSx environment; use Linux under WSL2. ::: ## 2. Bring a result field into OpenVCAD The reusable OpenVCAD feature begins after the solve. A result dataset needs: - point coordinates; - four point indices for every TET4 cell; - one or more named point or cell fields. The study constructs the dataset from the solver arrays: ```python results = pv.UnstructuredFieldDataset.from_tetrahedra(points, cells) results.add_point_scalar( "projected_strain_energy_density", projected_energy, units="mJ/mm^3", ) results.add_point_vector( "baseline_displacement", displacement, units="mm", ) ``` You can inspect what was imported before using it: ```python for name in results.field_names: field = results.field_metadata(name) print(name, field.association, field.component_count, field.units) ``` Then create ordinary OpenVCAD attributes: ```python energy = results.float_attribute("projected_strain_energy_density") displacement = results.vec3_attribute("baseline_displacement") ``` Point fields interpolate between the four corners of each tetrahedron. Cell fields return one constant value for the entire cell, so they can have visible jumps at cell boundaries. OpenVCAD never silently converts cell results to point results; ask the solver to project or recover a point field when you need a smooth design control. For a small solver-neutral example you can run without FEniCSx, see {download}`03_array_result_import.py <../../../examples/applications/simulation_driven_design/03_array_result_import.py>`: ```bash ./.venv/bin/python examples/applications/simulation_driven_design/03_array_result_import.py ``` ## 3. Check that the field covers the design The simulation mesh approximates the cantilever boundary, while the retained OpenVCAD box is exact. A few OpenVCAD sample points can therefore fall just outside the tetrahedral surface. Check that mismatch before rendering or compiling: ```python coverage = results.coverage( sample_positions, outside="boundary_clamp", max_distance=clamp_distance, ) print("inside:", coverage.inside_count) print("clamped:", coverage.boundary_clamped_count) print("outside:", coverage.outside_count) print("largest clamp:", coverage.maximum_clamp_distance) ``` There are three outcomes: 1. A point is inside and samples normally. 2. A point is just beyond the meshed boundary and is clamped to the nearest boundary point, but only within `max_distance`. 3. A point is truly outside and remains an error. Choose `max_distance` from the expected mesh-boundary approximation—not from the size of the whole part. If many points clamp, points collect on one side, or distances approach the limit, check coordinate alignment and units rather than increasing the distance. Avoid silent zero or constant fill because it can turn an alignment error into a plausible-looking design. Once coverage passes, create the attribute with the same bounded policy: ```python energy = results.float_attribute( "projected_strain_energy_density", outside="boundary_clamp", max_distance=clamp_distance, ) source_root.set_attribute("projected_strain_energy_density", energy) ``` ## 4. Turn strain energy into a design signal Raw solver values are rarely ready to drive material or geometry directly. The study uses the 5th and 95th percentiles as explicit bounds, normalizes that interval to 0–1, and clamps values beyond it: ```python control = energy.normalize(robust_minimum, robust_maximum).clamp(0.0, 1.0) ``` This makes the design rule easy to read: - `0` means the low end of the chosen result range; - `1` means the high end; - values outside the range stop at the nearest bound. The first render below shows that normalized 0–1 signal with a high-contrast palette. It is a display of the imported strain-energy result after the same named normalization used by the feedback rule; the physical values remain available in `projected_strain_energy_density`.
Normalized baseline strain-energy signal
Agilus30/Vero composition from that signal
Constant radius: 0.90 mm
Result-driven radius: 0.55–1.30 mm