.. DO NOT EDIT. .. THIS FILE WAS AUTOMATICALLY GENERATED BY SPHINX-GALLERY. .. TO MAKE CHANGES, EDIT THE SOURCE PYTHON FILE: .. "gallery/tutorials/tutorial_06_cdb_to_results.py" .. LINE NUMBERS ARE GIVEN BELOW. .. only:: html .. note:: :class: sphx-glr-download-link-note :ref:`Go to the end ` to download the full example code. .. rst-class:: sphx-glr-example-title .. _sphx_glr_gallery_tutorials_tutorial_06_cdb_to_results.py: Tutorial 6 — End-to-end deliverable workflow ============================================= The earlier tutorials all built their meshes natively in pyvista. Real engineering workflows rarely start that way: the geometry comes out of a CAD tool through a meshing pre-processor (HyperMesh, ANSA, Gmsh) and lands as a deck — almost always an MAPDL CDB or a NASTRAN BDF — that the analyst hands off to the solver, plus a snapshot of the solver-ready model that downstream consumers re-load. This tutorial walks the *complete* deliverable workflow from a pre-built model snapshot through a modal-plus-static analysis to a saved ``.pv`` result file the analyst can hand the next person on the team. Six steps: * **Step 1** — load a model snapshot and inspect what landed. Counts, element types, materials, units, and BCs. * **Step 2** — fill in the gaps. Pre-built snapshots sometimes ship without the BC / load setup the next analysis needs; we register them natively. * **Step 3** — run a modal solve and read the spectrum. * **Step 4** — set up a static load case from scratch on top of the snapshot's geometry, solve it, and recover the stress field. * **Step 5** — save the static result to a self-contained ``.pv`` file and re-load it through the disk-backed :class:`~femorph_solver.result.static.StaticResultDisk` handle. * **Step 6** — render a stress-distribution histogram for the design-review packet. This is the canonical end-to-end workflow the docs missed before — every other gallery example focuses on one slice (reader, solver, recovery, plotting). Tutorial 6 stitches them all together. Note on the source format ------------------------- The tutorial uses the bundled :func:`femorph_solver.examples.cyclic_bladed_rotor_sector_path` ``.pv`` fixture as the input. The same workflow applies when the source is an MAPDL CDB / NASTRAN BDF / Abaqus INP — call :func:`femorph_solver.interop.mapdl.from_cdb` / :func:`~femorph_solver.interop.nastran.from_bdf` / :func:`~femorph_solver.interop.abaqus.from_inp` instead of :meth:`Model.from_pv` in Step 1, and the rest is identical. Companion deeper-dive resources ------------------------------- * :doc:`/getting-started/mapdl-interop` — full MAPDL compatibility deep-dive (KEYOPT parity, binary-file coverage). * :doc:`/interop/nastran`, :doc:`/interop/abaqus` — equivalent migration deep-dives for the other two vendor decks. * :doc:`/user-guide/post-processing/result-objects` — how the disk-backed result handles work. * :doc:`/user-guide/solving/static`, :doc:`/user-guide/solving/modal` — the analysis-type pages each step exercises. .. GENERATED FROM PYTHON SOURCE LINES 65-82 .. code-block:: Python from __future__ import annotations import tempfile from pathlib import Path import matplotlib matplotlib.use("Agg") # headless: no GUI window pop in gallery / CI import matplotlib.pyplot as plt import numpy as np import femorph_solver from femorph_solver.recover import compute_nodal_stress, stress_invariants from femorph_solver.result.static import StaticResultDisk .. GENERATED FROM PYTHON SOURCE LINES 83-101 Step 1 — load the model snapshot -------------------------------- The bundled ``cyclic_bladed_rotor_sector_path`` returns a ``.pv`` file that is a complete :meth:`Model.save` snapshot of one sector of a bladed-rotor disk meshed with HEX8 cells. Real workflows would point :meth:`Model.from_pv` at the engineer's own ``.pv`` (or call the relevant vendor-deck reader from :mod:`femorph_solver.interop`); the API call is the same. :meth:`Model.from_pv ` returns a native :class:`~femorph_solver.Model` carrying everything stamped on the snapshot: geometry, connectivity, element-type registry, materials, real-constant table, the :class:`UnitSystem` stamp, and any BCs / loads the snapshot author registered. No re-parsing — the ``.pv`` is the canonical Model on-disk format. .. GENERATED FROM PYTHON SOURCE LINES 101-114 .. code-block:: Python source_path = femorph_solver.examples.cyclic_bladed_rotor_sector_path() print(f"Loading model snapshot:\n {source_path}\n") model = femorph_solver.Model.from_pv(source_path) print("After load:") print(f" Nodes: {model.n_nodes}") print(f" Elements: {model.n_elements}") print(f" Element-type registry: {model.etypes}") print(f" Materials: {list(model.materials.keys())}") print(f" Unit-system stamp: {model.unit_system}") .. rst-class:: sphx-glr-script-out .. code-block:: none Loading model snapshot: /home/runner/_work/solver/solver/src/femorph_solver/examples/_data/cyclic_bladed_rotor_sector.pv After load: Nodes: 230 Elements: 101 Element-type registry: {185: 'HEX8'} Materials: [1] Unit-system stamp: UnitSystem.UNSPECIFIED .. GENERATED FROM PYTHON SOURCE LINES 115-131 Step 2 — fill in the gaps ------------------------- Looking at the load-time inspection: the snapshot declared one element type and one material. That's typical — many pre-built models ship with the geometry + materials but without the BC setup the next analysis needs (because BCs are analysis-specific, not model-specific). We register them natively. This step uses the same :meth:`Model.fix ` and :meth:`Model.apply_force ` calls every other tutorial uses — the foreign-deck-loader contract is "after the loader returns, the resulting Model is indistinguishable from one built natively". .. GENERATED FROM PYTHON SOURCE LINES 131-142 .. code-block:: Python pts = np.asarray(model.grid.points) node_nums = np.asarray(model.grid.point_data["ansys_node_num"], dtype=np.int64) z_min = pts[:, 2].min() hub_mask = pts[:, 2] < z_min + 1e-6 print(f"\n Hub face at z={z_min:.4f} carries {int(hub_mask.sum())} nodes.") model.fix(nodes=node_nums[hub_mask].tolist(), dof="ALL") print(" Clamped the hub face (full-fix).") .. rst-class:: sphx-glr-script-out .. code-block:: none Hub face at z=-0.7875 carries 10 nodes. Clamped the hub face (full-fix). .. GENERATED FROM PYTHON SOURCE LINES 143-150 Step 3 — modal solve -------------------- A clamped-hub modal solve is the standard "is the rotor behaving" first check. Six modes is the default analyst size for a quick survey; commercial codes typically default to 10-20 modes for the same purpose. .. GENERATED FROM PYTHON SOURCE LINES 150-156 .. code-block:: Python modal = model.solve_modal(n_modes=6) print("\n Lowest six modes (Hz):") for i, f in enumerate(modal.frequency, start=1): print(f" mode {i}: {f:8.2f} Hz") .. rst-class:: sphx-glr-script-out .. code-block:: none Lowest six modes (Hz): mode 1: 1462.56 Hz mode 2: 2882.26 Hz mode 3: 5463.45 Hz mode 4: 9106.63 Hz mode 5: 17818.21 Hz mode 6: 23446.61 Hz .. GENERATED FROM PYTHON SOURCE LINES 157-171 Step 4 — static load case + stress recovery ------------------------------------------- Static analysis on the same model. We push a 1000 N axial load distributed across the tip face, solve, recover the stress field, and read out the peak von Mises. Note that the solve reuses the BCs already on the model from Step 2 — the cyclic faces of the sector are *not* fixed (that's appropriate for a fully-tip-loaded static demo). Real cyclic-symmetry analysis would use :class:`~femorph_solver.CyclicModel` and its :meth:`~femorph_solver.CyclicModel.solve_modal`; this tutorial keeps the static case simple to focus on the workflow. .. GENERATED FROM PYTHON SOURCE LINES 171-198 .. code-block:: Python z_max = pts[:, 2].max() tip_mask = pts[:, 2] > z_max - 1e-6 tip_nodes = node_nums[tip_mask] TOTAL_LOAD = 1000.0 # arbitrary, in the snapshot's unit system per_node = TOTAL_LOAD / tip_nodes.size for n in tip_nodes: model.apply_force(int(n), fz=per_node) print(f"\n Applied {TOTAL_LOAD} units total across {tip_nodes.size} tip nodes.") static = model.solve_static() print(f" Static solve done. displacement.shape = {static.displacement.shape}") # Stress recovery via the public free-function helper. Same # math as `StaticResultDisk.stress(model=model)` after the result # is loaded from disk (Step 5); we use the in-memory path here # because the model is already in scope. displacement = static.displacement.reshape(-1, 3) # 3 DOFs / node print(f" Peak displacement magnitude: {np.linalg.norm(displacement, axis=1).max():.4e}") stress_field = compute_nodal_stress(model, static.displacement) invariants = stress_invariants(stress_field) sigma_vm = invariants["von_mises"] peak_vm = float(sigma_vm.max()) print(f" Peak von Mises stress: {peak_vm:.3e}") .. rst-class:: sphx-glr-script-out .. code-block:: none Applied 1000.0 units total across 5 tip nodes. Static solve done. displacement.shape = (690,) Peak displacement magnitude: 1.5792e-01 Peak von Mises stress: 4.735e+06 .. GENERATED FROM PYTHON SOURCE LINES 199-212 Step 5 — save / reload through the disk-backed StaticResultDisk ---------------------------------------------------------------- The in-memory result lives in RAM only; the disk-backed :class:`~femorph_solver.result.static.StaticResultDisk` is the format you hand to the next person on the team. ``.save`` writes a single self-contained ``.pv`` (zstd-compressed pyvista) file; ``StaticResultDisk(path)`` re-loads it lazily. We write to ``tempfile.TemporaryDirectory`` so the gallery build doesn't litter the source tree, but real workflows would write to a project-results directory and check the ``.pv`` into the deliverables folder. .. GENERATED FROM PYTHON SOURCE LINES 212-235 .. code-block:: Python with tempfile.TemporaryDirectory() as tmp: out = static.save(Path(tmp) / "rotor_sector_static.pv", model) print(f"\n Wrote: {out.name} ({out.stat().st_size / 1024:.1f} KiB)") # Re-load through the disk-backed handle. Lazy IO — the # full grid only inflates on first .grid access. handle = StaticResultDisk(out) print(f" Re-loaded: {type(handle).__name__} from {out.name}") print(f" Disk-backed n_points: {handle.n_points}") # Stress recovery on the disk-backed handle uses the same # call shape as the in-memory path; the model has to be in # scope because stress isn't stored on disk (see # /reference/theory/stress_recovery). stress_handle = handle.stress(model=model) np.testing.assert_allclose(stress_handle, stress_field) print(" Round-tripped stress field matches the in-memory recovery.") # The lazy disk-backed handle is the canonical way to keep # several large analyses in scope without burning RAM. print(f" Available point arrays: {sorted(handle.available_point_arrays())}") .. rst-class:: sphx-glr-script-out .. code-block:: none Wrote: rotor_sector_static.pv (18.3 KiB) Re-loaded: StaticResultDisk from rotor_sector_static.pv Disk-backed n_points: 230 Round-tripped stress field matches the in-memory recovery. Available point arrays: ['REFINE', 'VTKorigID', 'angles', 'ansys_node_num', 'displacement', 'force', 'origid', 'vtkOriginalPointIds'] .. GENERATED FROM PYTHON SOURCE LINES 236-247 Step 6 — render the stress histogram ------------------------------------ A simple histogram of the von-Mises field plus a max-line annotation — enough to drop into a design-review slide and read off "where's the peak and how broad is the distribution?". Real packets would also include the rendered mesh from :ref:`sphx_glr_gallery_post-processing_example_principal_stress.py`, but this tutorial is about the workflow rather than the final plot polish. .. GENERATED FROM PYTHON SOURCE LINES 247-260 .. code-block:: Python fig, ax = plt.subplots(figsize=(7.5, 4.5)) ax.hist(sigma_vm, bins=40, color="C0", edgecolor="k", alpha=0.85) ax.axvline(peak_vm, color="C3", lw=2.0, label=f"peak = {peak_vm:.3e}") ax.set_xlabel(r"von Mises stress $\sigma_\mathrm{vm}$") ax.set_ylabel("Number of nodes") ax.set_title("Rotor sector — static stress distribution") ax.legend(loc="upper right") ax.grid(True, ls=":", alpha=0.5) fig.tight_layout() plt.show() .. image-sg:: /gallery/tutorials/images/sphx_glr_tutorial_06_cdb_to_results_001.png :alt: Rotor sector — static stress distribution :srcset: /gallery/tutorials/images/sphx_glr_tutorial_06_cdb_to_results_001.png :class: sphx-glr-single-img .. GENERATED FROM PYTHON SOURCE LINES 261-300 Engineering takeaway -------------------- Three things to read off the workflow before the deliverable: 1. **Snapshot-side gaps are routine.** The ``.pv`` snapshot came in without analysis-specific BCs / loads — the analyst's first move was to register them. Any pre-built-model workflow includes a "what did the snapshot declare vs what do I still need to assign" pass; the inspection in Step 1 surfaces it explicitly. 2. **The solver doesn't care where the mesh came from.** Steps 3 and 4 use the same APIs as the from-scratch tutorials. This is the foreign-deck-reader contract: after :func:`from_cdb` / :func:`from_bdf` / :func:`from_inp` / :meth:`Model.from_pv` returns, the resulting :class:`~femorph_solver.Model` is indistinguishable from one built natively. 3. **The .pv result file is the deliverable, not the script.** Step 5 round-trips through ``.pv`` because that's how the next person on the team picks the result up. The file carries the displacement + every metadata field, and stress is recovered on demand from the model the consumer has in scope. See :doc:`/reference/theory/stress_recovery` for why stress is *not* stored on disk. What's missing (and where to find it): * A composite cross-check against an originating MAPDL run — that's the verification-corpus pattern in :doc:`/verification/index`, not what a typical analyst needs in their day-to-day workflow. * A cyclic-symmetry expansion — :doc:`tutorial_05_cyclic_rotor` shows it; the modal solve in Step 3 is the static single-sector form. * A NASTRAN-deck variant — same pattern with :func:`femorph_solver.interop.nastran.from_bdf` instead. .. rst-class:: sphx-glr-timing **Total running time of the script:** (0 minutes 1.042 seconds) .. _sphx_glr_download_gallery_tutorials_tutorial_06_cdb_to_results.py: .. only:: html .. container:: sphx-glr-footer sphx-glr-footer-example .. container:: sphx-glr-download sphx-glr-download-jupyter :download:`Download Jupyter notebook: tutorial_06_cdb_to_results.ipynb ` .. container:: sphx-glr-download sphx-glr-download-python :download:`Download Python source code: tutorial_06_cdb_to_results.py ` .. container:: sphx-glr-download sphx-glr-download-zip :download:`Download zipped: tutorial_06_cdb_to_results.zip ` .. only:: html .. rst-class:: sphx-glr-signature `Gallery generated by Sphinx-Gallery `_