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, native API

The first-class entry point is Model.assign(), which declares the element kernel + material in one call:

import femorph_solver as fs

model = fs.Model.from_grid(grid)
model.assign(
    fs.ELEMENTS.HEX8,
    {"EX": 2.0e11, "PRXY": 0.30, "DENS": 7850.0},
)
By hand, APDL-dialect

For users porting an APDL deck command-by-command, femorph_solver.interop.mapdl.APDL exposes the N / E / ET / MAT / MP verbs over the same Model. MAPDL element-catalogue spellings are translated to neutral kernel names at this boundary ("SOLID185""HEX8" etc.):

from femorph_solver.interop.mapdl import APDL

model = fs.Model()
with APDL(model) as apdl:
    apdl.et(1, "SOLID185")           # ET,1,SOLID185
    apdl.type(1)                      # TYPE,1
    apdl.mat(1)                       # MAT,1
    apdl.n(1, 0, 0, 0)                # N,1, 0, 0, 0
    apdl.n(2, 1, 0, 0)
    # ... nodes 3..8 ...
    apdl.e(1, 2, 3, 4, 5, 6, 7, 8)    # E,1,2,3,4,5,6,7,8

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#