|
| 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