Skip to content

Commit d80e232

Browse files
Test case for the Inp parser.
1 parent 62a780e commit d80e232

File tree

1 file changed

+216
-0
lines changed

1 file changed

+216
-0
lines changed

examples/fem/test_connec.py

Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
import numpy as np
2+
import re
3+
import matplotlib.pyplot as plt
4+
import matplotlib.tri as tri
5+
6+
7+
def plot_mesh(X, elem_conn, edge_dict, jpg_name=None):
8+
fig, ax = plt.subplots()
9+
10+
# --- Plot element triangulation (background mesh) ---
11+
triang = tri.Triangulation(X[:, 0], X[:, 1], elem_conn)
12+
ax.triplot(triang, color="grey", linestyle="--", linewidth=1.0)
13+
14+
# --- Plot node tags ---
15+
for n in range(len(X)):
16+
ax.text(X[n, 0], X[n, 1], f"{n}", fontsize=8)
17+
18+
# --- Plot unique global edges ---
19+
for edge_tag, nodes in edge_dict.items():
20+
21+
if len(nodes) == 2:
22+
# Linear edge
23+
n1, n2 = nodes
24+
x = [X[n1, 0], X[n2, 0]]
25+
y = [X[n1, 1], X[n2, 1]]
26+
ax.plot(x, y, linewidth=2.0)
27+
28+
xm = 0.5 * (x[0] + x[1])
29+
ym = 0.5 * (y[0] + y[1])
30+
else:
31+
raise ValueError("nnodes along edge > 2")
32+
33+
# Edge label
34+
ax.text(xm, ym, f"E{edge_tag}", fontsize=9)
35+
36+
ax.set_aspect("equal")
37+
ax.set_xlabel("X")
38+
ax.set_ylabel("Y")
39+
40+
if jpg_name is not None:
41+
plt.savefig(jpg_name + ".jpg", dpi=1000)
42+
43+
plt.show()
44+
45+
46+
class InpParser:
47+
def __init__(self):
48+
self.elements = {}
49+
50+
def _read_file(self, filename):
51+
with open(filename, "r", errors="ignore") as fp:
52+
return [line.rstrip("\n") for line in fp]
53+
54+
def _split_csv_line(self, s):
55+
# ABAQUS lines are simple CSV-like, no quotes typically
56+
return [p.strip() for p in s.split(",") if p.strip()]
57+
58+
def _find_kw(self, header, key):
59+
m = re.search(rf"\b{re.escape(key)}\s*=\s*([^,\s]+)", header, flags=re.I)
60+
return m.group(1) if m else None
61+
62+
def parse_inp(self, filename):
63+
# Read the entire file in
64+
lines = self._read_file(filename)
65+
66+
self.X = {}
67+
self.elem_conn = {}
68+
69+
elem_type = None
70+
71+
index = 0
72+
section = None
73+
while index < len(lines):
74+
raw = lines[index].strip()
75+
index += 1
76+
77+
if not raw or raw.startswith("**"):
78+
continue
79+
80+
if raw.startswith("*"):
81+
header = raw.upper()
82+
83+
if header.startswith("*NODE"):
84+
section = "NODE"
85+
elif header.startswith("*ELEMENT"):
86+
section = "ELEMENT"
87+
elem_type = self._find_kw(header, "TYPE")
88+
elset = self._find_kw(header, "ELSET")
89+
if elem_type not in self.elem_conn:
90+
self.elem_conn[elset] = {}
91+
if elset not in self.elem_conn[elset]:
92+
self.elem_conn[elset][elem_type] = {}
93+
continue
94+
95+
if section == "NODE":
96+
parts = self._split_csv_line(raw)
97+
nid = int(parts[0])
98+
x = float(parts[1])
99+
y = float(parts[2])
100+
z = float(parts[3])
101+
self.X[nid - 1] = (x, y, z)
102+
103+
elif section == "ELEMENT":
104+
parts = self._split_csv_line(raw)
105+
eid = int(parts[0])
106+
conn = [(int(p) - 1) for p in parts[1:]]
107+
self.elem_conn[elset][elem_type][eid - 1] = conn
108+
109+
def get_nodes(self):
110+
return np.array([self.X[k] for k in sorted(self.X.keys())])
111+
112+
def get_domains(self):
113+
names = {}
114+
for elset in self.elem_conn:
115+
names[elset] = []
116+
for elem_type in self.elem_conn[elset]:
117+
names[elset].append(elem_type)
118+
119+
return names
120+
121+
def get_conn(self, elset, elem_type):
122+
conn = self.elem_conn[elset][elem_type]
123+
return np.array([conn[k] for k in sorted(conn.keys())], dtype=int)
124+
125+
# Local edge definitions per element type.
126+
# Each entry is a list of (local_node_i, local_node_j) pairs that form
127+
# the edges of that element. Corner nodes only — mid-side nodes are
128+
# included as the third entry where present so the full edge is
129+
# (corner_a, corner_b, optional_midside).
130+
_EDGE_LOCAL = {
131+
# Linear triangle: 3 edges
132+
"CPS3": [(0, 1), (1, 2), (2, 0)],
133+
# Linear quad: 4 edges
134+
"CPS4": [(0, 1), (1, 2), (2, 3), (3, 0)],
135+
# Quadratic triangle: 3 edges, each with a mid-side node
136+
# 0-3-1 / 1-4-2 / 2-5-0
137+
"CPS6": [(0, 1, 3), (1, 2, 4), (2, 0, 5)],
138+
# 9-node quad (serendipity/Lagrange):
139+
# 0-4-1 / 1-5-2 / 2-6-3 / 3-7-0
140+
"M3D9": [(0, 1, 4), (1, 2, 5), (2, 3, 6), (3, 0, 7)],
141+
}
142+
143+
def get_conn_edges(self, elset, elem_type):
144+
"""
145+
Build a unique-edge connectivity dictionary for the given elset/element type.
146+
147+
Returns
148+
-------
149+
edge_dict : dict
150+
Keys are integer edge tags (0, 1, 2, …), one per unique global edge.
151+
Values are tuples of global node indices that make up that edge.
152+
The first two entries are always the corner nodes in ascending order
153+
(lowest index first). For higher-order elements a third entry
154+
carries the mid-side node, preserving its original orientation so
155+
that the mid-side node is correctly associated with its edge even
156+
after the corners are sorted.
157+
158+
Example (linear triangle mesh with 4 nodes, 2 elements)
159+
-------------------------------------------------------
160+
edge_dict = {
161+
0: (0, 1),
162+
1: (1, 2),
163+
2: (0, 2),
164+
3: (2, 3),
165+
4: (1, 3),
166+
}
167+
"""
168+
if elem_type not in self._EDGE_LOCAL:
169+
raise NotImplementedError(
170+
f"Edge connectivity not defined for element type '{elem_type}'. "
171+
f"Supported types: {list(self._EDGE_LOCAL.keys())}"
172+
)
173+
174+
conn = self.get_conn(elset, elem_type) # shape (nelems, nodes_per_elem)
175+
local_edges = self._EDGE_LOCAL[elem_type]
176+
177+
seen = {} # canonical_corner_pair -> edge_tag
178+
edge_dict = {} # edge_tag -> full node tuple (corner0, corner1[, midside])
179+
tag = 0
180+
181+
for elem_nodes in conn:
182+
for local_edge in local_edges:
183+
# Corner nodes that define uniqueness
184+
n0 = int(elem_nodes[local_edge[0]])
185+
n1 = int(elem_nodes[local_edge[1]])
186+
187+
# Canonical key: lowest node index first
188+
key = (min(n0, n1), max(n0, n1))
189+
190+
if key not in seen:
191+
seen[key] = tag
192+
193+
# Build the full tuple: sorted corners + optional mid-side
194+
if len(local_edge) == 3:
195+
mid = int(elem_nodes[local_edge[2]])
196+
# Mid-side node follows the corner ordering
197+
full = (
198+
(key[0], key[1], mid) if n0 < n1 else (key[0], key[1], mid)
199+
)
200+
edge_dict[tag] = full
201+
else:
202+
edge_dict[tag] = key
203+
204+
tag += 1
205+
206+
return edge_dict
207+
208+
209+
fname = "plate.inp"
210+
parser = InpParser()
211+
parser.parse_inp(fname)
212+
X = parser.get_nodes()
213+
elem_conn = parser.get_conn("SURFACE1", "CPS3")
214+
edge_conn = parser.get_conn_edges("SURFACE1", "CPS3")
215+
plot_mesh(X, elem_conn, edge_conn, "mesh_with_edges")
216+
plt.show()

0 commit comments

Comments
 (0)