Meshed plate with Gmsh
This example connects NuSA’s preprocessing and analysis layers in one complete workflow:
geometry -> Gmsh -> triangle mesh -> LinearTriangleModel
-> solve -> StaticResult -> recovered stress field
It is the recommended reference when a problem contains more triangles than is practical to create manually.
Problem
The model is a square plate with side length 0.3 and thickness 0.01.
The left edge is fixed in both translational degrees of freedom and a total
horizontal load of 6000 is distributed equally across the nodes on the
right edge.
The material parameters are:
Young’s modulus
E = 200e9Poisson ratio
nu = 0.3thickness
t = 0.01
The plate is discretized with constant-strain triangular elements.
1. Create the geometry
Modeler stores a small 2D geometry description and delegates mesh
generation to the external Gmsh executable:
from nusa.mesh import Modeler
modeler = Modeler()
modeler.add_rectangle((0.0, 0.0), (0.3, 0.3), esize=0.05)
coordinates, connectivity = modeler.generate_mesh()
coordinates contains the compacted mesh points and connectivity contains
zero-based three-node triangle indices.
For example, one connectivity row
[4, 7, 3]
means that one finite element is formed by mesh points 4, 7 and 3.
2. Convert the mesh to NuSA objects
Mesh points become nusa.node.Node objects:
nodes = [Node(tuple(point[:2])) for point in coordinates]
Each triangle is then converted to a nusa.element.LinearTriangle:
elements = [
LinearTriangle(
(nodes[int(i)], nodes[int(j)], nodes[int(k)]),
E=200e9,
nu=0.3,
t=0.01,
)
for i, j, k in connectivity
]
The mesher deliberately does not create a finite-element Model itself.
This keeps preprocessing independent from analysis and makes the conversion
step explicit.
3. Build the finite-element problem
model = LinearTriangleModel("Meshed square plate")
model.add_nodes(nodes)
model.add_elements(elements)
Boundary conditions and loads can be selected from geometry. Here, nodes on the minimum x-coordinate are fixed and nodes on the maximum x-coordinate share the total horizontal force:
xmin = coordinates[:, 0].min()
xmax = coordinates[:, 0].max()
loaded_nodes = [node for node in nodes if np.isclose(node.x, xmax)]
force_per_node = 6000.0 / len(loaded_nodes)
for node in nodes:
if np.isclose(node.x, xmin):
model.add_constraint(node, ux=0.0, uy=0.0)
if np.isclose(node.x, xmax):
model.add_force(node, (force_per_node, 0.0))
Using np.isclose is preferable to exact floating-point comparisons when
selecting mesh boundaries.
4. Solve
The solve stage is unchanged from manually created models:
result = model.solve()
The mesh is now frozen into the returned nusa.result.StaticResult
through its node coordinates and connectivity.
5. Post-process a stress field
A CST element produces element stress and strain components. NuSA can recover those values to nodes and compute the plane-stress von Mises field:
von_mises = result.nodal_field("von_mises_stress")
or plot it directly:
result.plot_nodal_field("von_mises_stress")
The current nodal recovery policy is the arithmetic average of adjacent element values. See Post-processing for details.
Visualizing preprocessing and results
The three visualization stages remain separate:
modeler.plot_mesh() # generated mesh
plot_model(model) # FE problem definition
result.plot_nodal_field(
"von_mises_stress"
) # solved field
This mirrors the ownership model used throughout NuSA:
Modeler -> mesh
Model -> finite-element problem
Result -> solved state
Complete executable example
The following script is the same meshed-plate example maintained under
examples/:
1# -*- coding: utf-8 -*-
2# ***********************************
3# Author: Pedro Jorge De Los Santos
4# E-mail: delossantosmfq@gmail.com
5# License: MIT License
6# ***********************************
7
8import matplotlib.pyplot as plt
9import numpy as np
10
11from nusa import LinearTriangle, LinearTriangleModel, Node, plot_model
12from nusa.mesh import Modeler
13
14
15E = 200e9
16NU = 0.3
17THICKNESS = 0.01
18TOTAL_FORCE = 6000.0
19
20
21def build_model(esize=0.05, gmsh_executable="gmsh"):
22 """Build a meshed square plate using Gmsh-generated CST elements."""
23 modeler = Modeler()
24 modeler.add_rectangle((0.0, 0.0), (0.3, 0.3), esize=esize)
25 coordinates, connectivity = modeler.generate_mesh(
26 gmsh_executable=gmsh_executable,
27 )
28
29 nodes = [Node(tuple(point[:2])) for point in coordinates]
30 elements = [
31 LinearTriangle(
32 (nodes[int(i)], nodes[int(j)], nodes[int(k)]),
33 E=E,
34 nu=NU,
35 t=THICKNESS,
36 )
37 for i, j, k in connectivity
38 ]
39
40 model = LinearTriangleModel("Meshed square plate")
41 model.add_nodes(nodes)
42 model.add_elements(elements)
43
44 xmin = coordinates[:, 0].min()
45 xmax = coordinates[:, 0].max()
46 loaded_nodes = [node for node in nodes if np.isclose(node.x, xmax)]
47 nodal_force = TOTAL_FORCE / len(loaded_nodes)
48
49 for node in nodes:
50 if np.isclose(node.x, xmin):
51 model.add_constraint(node, ux=0.0, uy=0.0)
52 if np.isclose(node.x, xmax):
53 model.add_force(node, (nodal_force, 0.0))
54
55 return model, modeler
56
57
58def main():
59 model, modeler = build_model()
60
61 modeler.plot_mesh()
62 plot_model(model)
63
64 result = model.solve()
65 result.plot_nodal_field("von_mises_stress")
66 plt.show()
67 return result
68
69
70if __name__ == "__main__":
71 main()
Gmsh requirement
Generating a new mesh requires the Gmsh command-line executable. Before running this example, the following should succeed in the same terminal or notebook environment:
gmsh --version
Loading an existing triangular mesh with generate_mesh_from_file() only
requires meshio and does not invoke Gmsh.
See Mesh utilities for installation notes, geometry helpers, custom executable selection, and troubleshooting.