The Model#

femorph_solver.Model is the pre-processing wrapper. It holds a pyvista.UnstructuredGrid (nodes + cells), the global material and real-constant tables, the element-type registry, and the current Dirichlet / force records. From it the solver family assembles the global sparse \(K\) and \(M\).

Two ways to construct one#

From an existing mesh

If you already have a pyvista.UnstructuredGrid or a mapdl_archive.Archive:

import mapdl_archive
import femorph_solver

archive = mapdl_archive.Archive("blade.cdb")
m = femorph_solver.Model.from_grid(archive.grid)

from_grid expects the MAPDL-style cell/point arrays (ansys_node_num, ansys_elem_num, ansys_elem_type_num, ansys_material_type, ansys_real_constant). Missing arrays are auto-filled with sequential 1-based ids, so a plain pyvista mesh also works.

By hand, MAPDL-style

If you’re building a model programmatically:

m = femorph_solver.Model()
m.et(1, "SOLID185")     # ET,1,SOLID185
m.type(1)                # TYPE,1
m.mat(1)                 # MAT,1
m.n(1, 0, 0, 0)          # N,1, 0, 0, 0
m.n(2, 1, 0, 0)
# ... nodes 3..8 ...
m.e(1, 2, 3, 4, 5, 6, 7, 8)   # E,1,2,3,4,5,6,7,8

The verbs mirror the MAPDL commands one-to-one: n() adds a node, e() adds an element, et() declares an element type, type() / mat() / real() stamp the current type / material / real-constant id that e() will pick up.

What’s on a Model#

The grid is the canonical mesh representation:

m.grid                       # pyvista.UnstructuredGrid
m.grid.point_data["ansys_node_num"]
m.grid.cell_data["ansys_elem_num"]
m.grid.cell_data["ansys_elem_type_num"]
m.grid.cell_data["ansys_material_type"]
m.grid.cell_data["ansys_real_constant"]

Shortcut accessors return typed views of the same data:

Attribute

Returns

n_nodes

int — number of points on the grid.

n_elements

int — number of cells.

node_numbers

(N,) int — 1-based MAPDL node numbers.

element_numbers

(M,) int — 1-based MAPDL element numbers.

etypes

{et_id: name} — declared element types.

materials

{mat_id: {prop: value}} — material property tables.

real_constants

{real_id: np.ndarray} — real-constant vectors.

Assembly#

Once pre-processing is done, materialise the global matrices:

K = m.stiffness_matrix()     # scipy.sparse.csr_array
M = m.mass_matrix()          # scipy.sparse.csr_array (consistent)
M_lumped = m.mass_matrix(lumped=True)

The same cached grid feeds every call, so successive assemblies reuse the CSR sparsity pattern. The global DOF layout is exposed via Model.dof_map() — an (N, 2) array of (mapdl_node_num, local_dof_idx) that lets you translate between solver vectors and physical node / component.

Solve shortcuts#

For the three most common analyses you can bypass the solver module and ask the Model directly:

static = m.solve()                          # linear static
modal = m.modal_solve(n_modes=10)           # free-vibration modal
transient = m.transient_solve(dt=1e-4, n_steps=1000, F=load_vec)

These delegate to solve_static(), solve_modal(), and solve_transient() respectively, passing the current Dirichlet / force records. For finer control (non-auto backends, explicit thread limits, custom prescribed dicts) call those solver functions directly with K and M — see Solving.

See also#