chilmesh 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chilmesh/CHILmesh.py +933 -0
- chilmesh/__init__.py +10 -0
- chilmesh/utils/plot_utils.py +365 -0
- chilmesh-0.1.0.dist-info/METADATA +215 -0
- chilmesh-0.1.0.dist-info/RECORD +8 -0
- chilmesh-0.1.0.dist-info/WHEEL +5 -0
- chilmesh-0.1.0.dist-info/licenses/LICENSE +21 -0
- chilmesh-0.1.0.dist-info/top_level.txt +1 -0
chilmesh/CHILmesh.py
ADDED
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from .utils.plot_utils import CHILmeshPlotMixin
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
import matplotlib.cm as cm
|
|
8
|
+
from scipy.spatial import Delaunay
|
|
9
|
+
from typing import List, Tuple, Optional as Opt, Dict, Set, Union, Any
|
|
10
|
+
from scipy.sparse import lil_matrix
|
|
11
|
+
from scipy.sparse.linalg import spsolve
|
|
12
|
+
from copy import deepcopy
|
|
13
|
+
|
|
14
|
+
__all__ = ['CHILmesh', 'write_fort14']
|
|
15
|
+
|
|
16
|
+
class CHILmesh(CHILmeshPlotMixin):
|
|
17
|
+
"""
|
|
18
|
+
A class for triangular, quadrilateral, or mixed-element meshes in 2D.
|
|
19
|
+
|
|
20
|
+
This Python implementation is based on the MATLAB CHILmesh class from the
|
|
21
|
+
Computational Hydrodynamics & Informatics Laboratory (CHIL) at The Ohio State University,
|
|
22
|
+
focusing on the mesh layers approach described in Mattioli's thesis.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def grid_name(self):
|
|
27
|
+
"""Grid name property."""
|
|
28
|
+
return self._grid_name
|
|
29
|
+
|
|
30
|
+
@grid_name.setter
|
|
31
|
+
def grid_name(self, value):
|
|
32
|
+
"""Set grid name."""
|
|
33
|
+
self._grid_name = value
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def Layers(self):
|
|
37
|
+
"""
|
|
38
|
+
Backwards compatibility property to access layers with uppercase name.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
The layers dictionary
|
|
42
|
+
"""
|
|
43
|
+
return self.layers
|
|
44
|
+
|
|
45
|
+
def change_points(self, new_points, acknowledge_change=False):
|
|
46
|
+
"""
|
|
47
|
+
Change the mesh's (x,y,z) locations of its points.
|
|
48
|
+
|
|
49
|
+
Parameters:
|
|
50
|
+
new_points: New coordinates for the points
|
|
51
|
+
acknowledge_change: If True, acknowledges the change in the mesh
|
|
52
|
+
"""
|
|
53
|
+
assert acknowledge_change, "acknowledge_change must be True to change points -- this will change the mesh, make sure you understand this before using this method within a broader algorithm."
|
|
54
|
+
if new_points.shape[1] == 2: self.points[:, :2] = new_points
|
|
55
|
+
elif new_points.shape[1] == 3: self.points = new_points
|
|
56
|
+
else: raise ValueError("new_points must have 2 or 3 columns")
|
|
57
|
+
|
|
58
|
+
def __init__( self, connectivity: Opt[np.ndarray] = None, points: Opt[np.ndarray] = None, grid_name: Opt[str] = None ) -> None:
|
|
59
|
+
"""
|
|
60
|
+
Initialize a CHILmesh object.
|
|
61
|
+
|
|
62
|
+
Parameters:
|
|
63
|
+
connectivity: Element connectivity list
|
|
64
|
+
points: Vertex coordinates
|
|
65
|
+
grid_name: Name of the mesh
|
|
66
|
+
"""
|
|
67
|
+
# Public properties
|
|
68
|
+
self.grid_name = grid_name
|
|
69
|
+
self.points = points
|
|
70
|
+
self.connectivity_list = connectivity
|
|
71
|
+
self.boundary_condition = None
|
|
72
|
+
|
|
73
|
+
# Hidden properties
|
|
74
|
+
self.adjacencies: Dict[str, Any] = {}
|
|
75
|
+
self.n_verts: int = 0
|
|
76
|
+
self.n_elems: int = 0
|
|
77
|
+
self.n_edges: int = 0
|
|
78
|
+
self.n_layers: int = 0
|
|
79
|
+
self.layers: Dict[str, List] = {"OE": [], "IE": [], "OV": [], "IV": [], "bEdgeIDs": []}
|
|
80
|
+
self.type: Opt[str] = None
|
|
81
|
+
|
|
82
|
+
# If no inputs are provided, create a random Delaunay triangulation
|
|
83
|
+
if connectivity is None and points is None:
|
|
84
|
+
self._create_random_triangulation()
|
|
85
|
+
|
|
86
|
+
# Initialize the mesh
|
|
87
|
+
self._initialize_mesh()
|
|
88
|
+
|
|
89
|
+
def _create_random_triangulation( self ) -> None:
|
|
90
|
+
"""Create a random Delaunay triangulation for testing"""
|
|
91
|
+
# Generate random points in a 10x10 domain
|
|
92
|
+
points = np.random.rand( 20, 2 ) * 10
|
|
93
|
+
# Create Delaunay triangulation
|
|
94
|
+
tri = Delaunay( points )
|
|
95
|
+
# Store points and connectivity
|
|
96
|
+
self.points = np.column_stack( ( tri.points, np.zeros( tri.points.shape[0] ) ) )
|
|
97
|
+
self.connectivity_list = tri.simplices
|
|
98
|
+
self.grid_name = "Random Delaunay"
|
|
99
|
+
|
|
100
|
+
def _initialize_mesh( self ) -> None:
|
|
101
|
+
"""Initialize the mesh properties"""
|
|
102
|
+
if self.points is not None and self.connectivity_list is not None:
|
|
103
|
+
self.n_verts = self.points.shape[0]
|
|
104
|
+
self.n_elems = self.connectivity_list.shape[0]
|
|
105
|
+
|
|
106
|
+
# Ensure points have z-coordinate
|
|
107
|
+
if self.points.shape[1] == 2:
|
|
108
|
+
self.points = np.column_stack( ( self.points, np.zeros( self.n_verts ) ) )
|
|
109
|
+
|
|
110
|
+
# Check connectivity orientation and correct if needed
|
|
111
|
+
self._ensure_ccw_orientation()
|
|
112
|
+
|
|
113
|
+
# Build adjacency lists
|
|
114
|
+
self._build_adjacencies()
|
|
115
|
+
|
|
116
|
+
# Identify mesh layers
|
|
117
|
+
self._mesh_layers()
|
|
118
|
+
|
|
119
|
+
def _ensure_ccw_orientation( self ) -> None:
|
|
120
|
+
"""Ensure counter-clockwise orientation of elements"""
|
|
121
|
+
# Calculate signed area of each element
|
|
122
|
+
areas = self.signed_area()
|
|
123
|
+
|
|
124
|
+
# Find elements with clockwise orientation (negative area)
|
|
125
|
+
cw_elements = np.where( areas < 0 )[0]
|
|
126
|
+
|
|
127
|
+
# Flip orientation of clockwise elements
|
|
128
|
+
for elem_id in cw_elements:
|
|
129
|
+
# For triangular elements (3 vertices)
|
|
130
|
+
if self.connectivity_list.shape[1] == 3:
|
|
131
|
+
self.connectivity_list[elem_id] = self.connectivity_list[elem_id, [0, 2, 1]]
|
|
132
|
+
# For quadrilateral elements (4 vertices)
|
|
133
|
+
elif self.connectivity_list.shape[1] == 4:
|
|
134
|
+
self.connectivity_list[elem_id] = self.connectivity_list[elem_id, [0, 3, 2, 1]]
|
|
135
|
+
|
|
136
|
+
def signed_area( self, elem_ids: Opt[Union[int, List[int], np.ndarray]] = None ) -> np.ndarray:
|
|
137
|
+
"""
|
|
138
|
+
Calculate the signed area of elements.
|
|
139
|
+
|
|
140
|
+
Parameters:
|
|
141
|
+
elem_ids: Indices of elements to evaluate.
|
|
142
|
+
If None, all elements are evaluated.
|
|
143
|
+
|
|
144
|
+
Returns:
|
|
145
|
+
Signed areas of elements
|
|
146
|
+
"""
|
|
147
|
+
if elem_ids is None:
|
|
148
|
+
elem_ids = np.arange( self.n_elems )
|
|
149
|
+
|
|
150
|
+
if np.isscalar( elem_ids ):
|
|
151
|
+
elem_ids = [elem_ids]
|
|
152
|
+
|
|
153
|
+
areas = np.zeros( len( elem_ids ) )
|
|
154
|
+
|
|
155
|
+
# Determine element types
|
|
156
|
+
tri_elems, quad_elems = self._elem_type( elem_ids )
|
|
157
|
+
|
|
158
|
+
# Calculate areas for triangular elements
|
|
159
|
+
for i, elem_id in enumerate( elem_ids ):
|
|
160
|
+
if elem_id in tri_elems:
|
|
161
|
+
vertices = self.connectivity_list[elem_id][:3] # First 3 vertices for triangles
|
|
162
|
+
x = self.points[vertices, 0]
|
|
163
|
+
y = self.points[vertices, 1]
|
|
164
|
+
# Shoelace formula for triangle
|
|
165
|
+
areas[i] = 0.5 * ((x[0]*(y[1]-y[2]) + x[1]*(y[2]-y[0]) + x[2]*(y[0]-y[1])))
|
|
166
|
+
elif elem_id in quad_elems:
|
|
167
|
+
vertices = self.connectivity_list[elem_id]
|
|
168
|
+
x = self.points[vertices, 0]
|
|
169
|
+
y = self.points[vertices, 1]
|
|
170
|
+
# Shoelace formula for quadrilateral
|
|
171
|
+
areas[i] = 0.5 * ((x[0]*(y[1]-y[3]) + x[1]*(y[2]-y[0]) +
|
|
172
|
+
x[2]*(y[3]-y[1]) + x[3]*(y[0]-y[2])))
|
|
173
|
+
|
|
174
|
+
return areas
|
|
175
|
+
|
|
176
|
+
def _elem_type( self, elem_ids: Opt[Union[int, List[int], np.ndarray]] = None
|
|
177
|
+
) -> Tuple[np.ndarray, np.ndarray]:
|
|
178
|
+
"""
|
|
179
|
+
Identify triangular and quadrilateral elements.
|
|
180
|
+
|
|
181
|
+
Parameters:
|
|
182
|
+
elem_ids: Indices of elements to check.
|
|
183
|
+
If None, all elements are checked.
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Tuple of (tri_elems, quad_elems) arrays of element indices
|
|
187
|
+
"""
|
|
188
|
+
if elem_ids is None:
|
|
189
|
+
elem_ids = np.arange( self.n_elems )
|
|
190
|
+
|
|
191
|
+
# Convert to numpy array if not already
|
|
192
|
+
elem_ids = np.array( elem_ids )
|
|
193
|
+
|
|
194
|
+
if self.connectivity_list.shape[1] == 3:
|
|
195
|
+
# All triangular elements
|
|
196
|
+
return elem_ids, np.array( [] )
|
|
197
|
+
|
|
198
|
+
# Check for triangular elements in a quad/mixed-element mesh
|
|
199
|
+
tri_mask = np.zeros( len( elem_ids ), dtype=bool )
|
|
200
|
+
|
|
201
|
+
for i, elem_id in enumerate( elem_ids ):
|
|
202
|
+
# Check for redundant vertices or zero-valued vertices
|
|
203
|
+
vertices = self.connectivity_list[elem_id]
|
|
204
|
+
if (vertices[0] == vertices[1] or
|
|
205
|
+
vertices[1] == vertices[2] or
|
|
206
|
+
vertices[2] == vertices[3] or
|
|
207
|
+
vertices[3] == vertices[0] or
|
|
208
|
+
vertices[3] == 0):
|
|
209
|
+
tri_mask[i] = True
|
|
210
|
+
|
|
211
|
+
tri_elems = elem_ids[tri_mask]
|
|
212
|
+
quad_elems = elem_ids[~tri_mask]
|
|
213
|
+
|
|
214
|
+
return tri_elems, quad_elems
|
|
215
|
+
|
|
216
|
+
def _build_adjacencies( self ) -> None:
|
|
217
|
+
"""Build adjacency lists for the mesh"""
|
|
218
|
+
# Identify triangular and quadrilateral elements
|
|
219
|
+
tri_elems, quad_elems = self._elem_type()
|
|
220
|
+
|
|
221
|
+
# Set mesh type based on element types
|
|
222
|
+
if len( quad_elems ) == 0:
|
|
223
|
+
self.type = "Triangular"
|
|
224
|
+
elif len( tri_elems ) == 0:
|
|
225
|
+
self.type = "Quadrilateral"
|
|
226
|
+
else:
|
|
227
|
+
self.type = "Mixed-Element"
|
|
228
|
+
# For mixed-element mesh, triangles may need adjustment for consistency
|
|
229
|
+
if self.connectivity_list.shape[1] == 4: # Already have space for 4 vertices
|
|
230
|
+
for elem_id in tri_elems:
|
|
231
|
+
self.connectivity_list[elem_id, 3] = self.connectivity_list[elem_id, 0]
|
|
232
|
+
|
|
233
|
+
# Identify edges of the mesh
|
|
234
|
+
edges = self._identify_edges()
|
|
235
|
+
edge2vert = np.array( edges )
|
|
236
|
+
self.n_edges = len( edge2vert )
|
|
237
|
+
|
|
238
|
+
# Build Elem2Edge
|
|
239
|
+
elem2edge = self._build_elem2edge( edge2vert )
|
|
240
|
+
|
|
241
|
+
# Build Vert2Edge
|
|
242
|
+
vert2edge = self._build_vert2edge( edge2vert )
|
|
243
|
+
|
|
244
|
+
# Build Vert2Elem
|
|
245
|
+
vert2elem = self._build_vert2elem()
|
|
246
|
+
|
|
247
|
+
# Build Edge2Elem
|
|
248
|
+
edge2elem = self._build_edge2elem( edge2vert )
|
|
249
|
+
|
|
250
|
+
# Store adjacencies
|
|
251
|
+
self.adjacencies = {
|
|
252
|
+
"Elem2Vert": self.connectivity_list,
|
|
253
|
+
"Edge2Vert": edge2vert,
|
|
254
|
+
"Elem2Edge": elem2edge,
|
|
255
|
+
"Vert2Edge": vert2edge,
|
|
256
|
+
"Vert2Elem": vert2elem,
|
|
257
|
+
"Edge2Elem": edge2elem
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
def _identify_edges( self ) -> List[Tuple[int, int]]:
|
|
261
|
+
"""
|
|
262
|
+
Identify edges of the mesh.
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
List of edges, where each edge is a tuple of two vertex indices
|
|
266
|
+
"""
|
|
267
|
+
edges = set()
|
|
268
|
+
|
|
269
|
+
for elem_id in range( self.n_elems ):
|
|
270
|
+
vertices = self.connectivity_list[elem_id]
|
|
271
|
+
n_vertices = 3 if self.type == "Triangular" else 4
|
|
272
|
+
|
|
273
|
+
for i in range( n_vertices ):
|
|
274
|
+
v1 = vertices[i]
|
|
275
|
+
v2 = vertices[(i+1) % n_vertices]
|
|
276
|
+
|
|
277
|
+
# Skip invalid edges (negative vertex ids)
|
|
278
|
+
# In MATLAB the value 0 signified a placeholder for a missing
|
|
279
|
+
# vertex in mixed element meshes. In this Python port we use
|
|
280
|
+
# 0-based indexing, therefore vertex id ``0`` is valid and
|
|
281
|
+
# should not be discarded. Only negative ids are considered
|
|
282
|
+
# invalid.
|
|
283
|
+
if v1 < 0 or v2 < 0:
|
|
284
|
+
continue
|
|
285
|
+
|
|
286
|
+
# Store edge as a sorted tuple to avoid duplicates
|
|
287
|
+
edge = tuple( sorted( [int(v1), int(v2)] ) )
|
|
288
|
+
edges.add( edge )
|
|
289
|
+
|
|
290
|
+
return list( edges )
|
|
291
|
+
|
|
292
|
+
def _build_elem2edge( self, edge2vert: np.ndarray ) -> np.ndarray:
|
|
293
|
+
"""
|
|
294
|
+
Build Elem2Edge adjacency.
|
|
295
|
+
|
|
296
|
+
Parameters:
|
|
297
|
+
edge2vert: Edge-to-vertex adjacency
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
Element-to-edge adjacency
|
|
301
|
+
"""
|
|
302
|
+
max_edges_per_elem = 4 if self.type != "Triangular" else 3
|
|
303
|
+
elem2edge = np.zeros( ( self.n_elems, max_edges_per_elem ), dtype=int )
|
|
304
|
+
|
|
305
|
+
# For each element
|
|
306
|
+
for elem_id in range( self.n_elems ):
|
|
307
|
+
vertices = self.connectivity_list[elem_id]
|
|
308
|
+
n_vertices = 3 if self.type == "Triangular" else 4
|
|
309
|
+
|
|
310
|
+
# For each edge of the element
|
|
311
|
+
for i in range( n_vertices ):
|
|
312
|
+
v1 = vertices[i]
|
|
313
|
+
v2 = vertices[(i+1) % n_vertices]
|
|
314
|
+
|
|
315
|
+
# Skip invalid edges (negative vertex ids)
|
|
316
|
+
if v1 < 0 or v2 < 0:
|
|
317
|
+
continue
|
|
318
|
+
|
|
319
|
+
# Find the edge index
|
|
320
|
+
edge = tuple( sorted( [int(v1), int(v2)] ) )
|
|
321
|
+
for j, e in enumerate( edge2vert ):
|
|
322
|
+
if set( e ) == set( edge ):
|
|
323
|
+
elem2edge[elem_id, i] = j
|
|
324
|
+
break
|
|
325
|
+
|
|
326
|
+
return elem2edge
|
|
327
|
+
|
|
328
|
+
def _build_vert2edge( self, edge2vert: np.ndarray ) -> List[List[int]]:
|
|
329
|
+
"""
|
|
330
|
+
Build Vert2Edge adjacency.
|
|
331
|
+
|
|
332
|
+
Parameters:
|
|
333
|
+
edge2vert: Edge-to-vertex adjacency
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
Vertex-to-edge adjacency as list of lists
|
|
337
|
+
"""
|
|
338
|
+
# Initialize with empty lists for each vertex
|
|
339
|
+
vert2edge = [[] for _ in range( self.n_verts )]
|
|
340
|
+
|
|
341
|
+
# Populate the lists
|
|
342
|
+
for edge_id, (v1, v2) in enumerate( edge2vert ):
|
|
343
|
+
vert2edge[v1].append( edge_id )
|
|
344
|
+
vert2edge[v2].append( edge_id )
|
|
345
|
+
|
|
346
|
+
return vert2edge
|
|
347
|
+
|
|
348
|
+
def _build_vert2elem( self ) -> List[List[int]]:
|
|
349
|
+
"""
|
|
350
|
+
Build Vert2Elem adjacency.
|
|
351
|
+
|
|
352
|
+
Returns:
|
|
353
|
+
Vertex-to-element adjacency as list of lists
|
|
354
|
+
"""
|
|
355
|
+
# Initialize with empty lists for each vertex
|
|
356
|
+
vert2elem = [[] for _ in range( self.n_verts )]
|
|
357
|
+
|
|
358
|
+
# Populate the lists
|
|
359
|
+
for elem_id in range( self.n_elems ):
|
|
360
|
+
vertices = self.connectivity_list[elem_id]
|
|
361
|
+
for v in vertices:
|
|
362
|
+
# Skip invalid vertex ids (negative values represent
|
|
363
|
+
# placeholders in mixed-element meshes). Zero is a valid
|
|
364
|
+
# vertex index in this Python implementation.
|
|
365
|
+
if v >= 0:
|
|
366
|
+
vert2elem[v].append( elem_id )
|
|
367
|
+
|
|
368
|
+
return vert2elem
|
|
369
|
+
|
|
370
|
+
def _build_edge2elem( self, edge2vert: np.ndarray ) -> np.ndarray:
|
|
371
|
+
"""
|
|
372
|
+
Build Edge2Elem adjacency.
|
|
373
|
+
|
|
374
|
+
Parameters:
|
|
375
|
+
edge2vert: Edge-to-vertex adjacency
|
|
376
|
+
|
|
377
|
+
Returns:
|
|
378
|
+
Edge-to-element adjacency
|
|
379
|
+
"""
|
|
380
|
+
# Initialize with zeros - at most 2 elements per edge
|
|
381
|
+
edge2elem = np.zeros( ( self.n_edges, 2 ), dtype=int )
|
|
382
|
+
|
|
383
|
+
# Iterate through elements and their edges
|
|
384
|
+
for elem_id in range( self.n_elems ):
|
|
385
|
+
vertices = self.connectivity_list[elem_id]
|
|
386
|
+
n_vertices = 3 if self.type == "Triangular" else 4
|
|
387
|
+
|
|
388
|
+
# For each edge of the element
|
|
389
|
+
for i in range( n_vertices ):
|
|
390
|
+
v1 = vertices[i]
|
|
391
|
+
v2 = vertices[(i+1) % n_vertices]
|
|
392
|
+
|
|
393
|
+
# Skip invalid edges (negative vertex ids)
|
|
394
|
+
if v1 < 0 or v2 < 0:
|
|
395
|
+
continue
|
|
396
|
+
|
|
397
|
+
# Find the edge index
|
|
398
|
+
edge = tuple( sorted( [int(v1), int(v2)] ) )
|
|
399
|
+
for edge_id, e in enumerate( edge2vert ):
|
|
400
|
+
if set( e ) == set( edge ):
|
|
401
|
+
# Check if edge already has an element assigned
|
|
402
|
+
if edge2elem[edge_id, 0] == 0:
|
|
403
|
+
edge2elem[edge_id, 0] = elem_id + 1 # +1 to avoid 0
|
|
404
|
+
else:
|
|
405
|
+
edge2elem[edge_id, 1] = elem_id + 1
|
|
406
|
+
break
|
|
407
|
+
|
|
408
|
+
# Adjust indices (remove the +1 offset)
|
|
409
|
+
edge2elem[edge2elem > 0] -= 1
|
|
410
|
+
|
|
411
|
+
return edge2elem
|
|
412
|
+
|
|
413
|
+
def boundary_edges( self ) -> np.ndarray:
|
|
414
|
+
"""
|
|
415
|
+
Identify boundary edges of the mesh.
|
|
416
|
+
|
|
417
|
+
Returns:
|
|
418
|
+
Indices of boundary edges
|
|
419
|
+
"""
|
|
420
|
+
# Boundary edges have only one adjacent element
|
|
421
|
+
edge2elem = self.adjacencies["Edge2Elem"]
|
|
422
|
+
boundary_mask = (edge2elem[:, 1] == 0) # Second element is zero
|
|
423
|
+
return np.where( boundary_mask )[0]
|
|
424
|
+
|
|
425
|
+
def edge2vert( self, edge_ids: Opt[Union[int, List[int], np.ndarray]] = None ) -> np.ndarray:
|
|
426
|
+
"""
|
|
427
|
+
Get vertices that define specified edges.
|
|
428
|
+
|
|
429
|
+
Parameters:
|
|
430
|
+
edge_ids: Indices of edges to query.
|
|
431
|
+
If None, all edges are queried.
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
Array of vertex indices for each edge
|
|
435
|
+
"""
|
|
436
|
+
if edge_ids is None:
|
|
437
|
+
edge_ids = np.arange( self.n_edges )
|
|
438
|
+
|
|
439
|
+
if np.isscalar( edge_ids ):
|
|
440
|
+
edge_ids = [edge_ids]
|
|
441
|
+
|
|
442
|
+
return self.adjacencies["Edge2Vert"][edge_ids]
|
|
443
|
+
|
|
444
|
+
def elem2edge( self, elem_ids: Opt[Union[int, List[int], np.ndarray]] = None ) -> np.ndarray:
|
|
445
|
+
"""
|
|
446
|
+
Get edges that define specified elements.
|
|
447
|
+
|
|
448
|
+
Parameters:
|
|
449
|
+
elem_ids: Indices of elements to query.
|
|
450
|
+
If None, all elements are queried.
|
|
451
|
+
|
|
452
|
+
Returns:
|
|
453
|
+
Array of edge indices for each element
|
|
454
|
+
"""
|
|
455
|
+
if elem_ids is None:
|
|
456
|
+
elem_ids = np.arange( self.n_elems )
|
|
457
|
+
|
|
458
|
+
if np.isscalar( elem_ids ):
|
|
459
|
+
elem_ids = [elem_ids]
|
|
460
|
+
|
|
461
|
+
return self.adjacencies["Elem2Edge"][elem_ids]
|
|
462
|
+
|
|
463
|
+
def edge2elem( self, edge_ids: Opt[Union[int, List[int], np.ndarray]] = None ) -> np.ndarray:
|
|
464
|
+
"""
|
|
465
|
+
Get elements adjacent to specified edges.
|
|
466
|
+
|
|
467
|
+
Parameters:
|
|
468
|
+
edge_ids: Indices of edges to query.
|
|
469
|
+
If None, all edges are queried.
|
|
470
|
+
|
|
471
|
+
Returns:
|
|
472
|
+
Array of element indices for each edge
|
|
473
|
+
"""
|
|
474
|
+
if edge_ids is None:
|
|
475
|
+
edge_ids = np.arange( self.n_edges )
|
|
476
|
+
|
|
477
|
+
if np.isscalar( edge_ids ):
|
|
478
|
+
edge_ids = [edge_ids]
|
|
479
|
+
|
|
480
|
+
return self.adjacencies["Edge2Elem"][edge_ids]
|
|
481
|
+
|
|
482
|
+
def _mesh_layers(self) -> None:
|
|
483
|
+
"""
|
|
484
|
+
Discretize the mesh into layers starting from the boundary.
|
|
485
|
+
This implements the mesh layers approach described in Mattioli's thesis.
|
|
486
|
+
"""
|
|
487
|
+
# Reset layers
|
|
488
|
+
self.layers = {"OE": [], "IE": [], "OV": [], "IV": [], "bEdgeIDs": []}
|
|
489
|
+
|
|
490
|
+
# Get boundary edges (edges with only one adjacent element)
|
|
491
|
+
edge2elem = self.adjacencies["Edge2Elem"]
|
|
492
|
+
boundary_mask = (edge2elem[:, 1] == 0)
|
|
493
|
+
boundary_edges = np.where(boundary_mask)[0]
|
|
494
|
+
|
|
495
|
+
# Keep track of which elements have been assigned to a layer
|
|
496
|
+
remaining_elements = set(range(self.n_elems))
|
|
497
|
+
|
|
498
|
+
# Get element-to-element connectivity using edge2elem
|
|
499
|
+
elem2elem = [[] for _ in range(self.n_elems)]
|
|
500
|
+
for edge_idx, (e1, e2) in enumerate(edge2elem):
|
|
501
|
+
if e1 >= 0 and e2 >= 0: # Both elements exist
|
|
502
|
+
elem2elem[e1].append(e2)
|
|
503
|
+
elem2elem[e2].append(e1)
|
|
504
|
+
|
|
505
|
+
# Process layers from the boundary inward
|
|
506
|
+
layer_idx = 0
|
|
507
|
+
while remaining_elements and len(boundary_edges) > 0:
|
|
508
|
+
# Get boundary vertices
|
|
509
|
+
edge2vert = self.adjacencies["Edge2Vert"]
|
|
510
|
+
outer_vertices = np.array(list(set(edge2vert[boundary_edges].flatten())))
|
|
511
|
+
self.layers["OV"].append(outer_vertices)
|
|
512
|
+
self.layers["bEdgeIDs"].append(boundary_edges)
|
|
513
|
+
|
|
514
|
+
# Get outer elements (elements adjacent to boundary edges)
|
|
515
|
+
outer_elems = []
|
|
516
|
+
for edge_idx in boundary_edges:
|
|
517
|
+
elems = edge2elem[edge_idx]
|
|
518
|
+
for elem in elems:
|
|
519
|
+
if elem >= 0 and elem in remaining_elements:
|
|
520
|
+
outer_elems.append(elem)
|
|
521
|
+
|
|
522
|
+
# Convert to numpy array of integers
|
|
523
|
+
outer_elems = np.array(list(set(outer_elems)), dtype=int)
|
|
524
|
+
|
|
525
|
+
# Skip if no outer elements found
|
|
526
|
+
if len(outer_elems) == 0:
|
|
527
|
+
break
|
|
528
|
+
|
|
529
|
+
self.layers["OE"].append(outer_elems)
|
|
530
|
+
for elem in outer_elems:
|
|
531
|
+
remaining_elements.remove(elem)
|
|
532
|
+
|
|
533
|
+
# Get inner elements (neighbors of outer elements that haven't been assigned yet)
|
|
534
|
+
inner_elems = []
|
|
535
|
+
for elem in outer_elems:
|
|
536
|
+
for neighbor in elem2elem[elem]:
|
|
537
|
+
if neighbor in remaining_elements:
|
|
538
|
+
inner_elems.append(neighbor)
|
|
539
|
+
|
|
540
|
+
# Convert to numpy array of integers and remove duplicates
|
|
541
|
+
inner_elems = np.array(list(set(inner_elems)), dtype=int)
|
|
542
|
+
|
|
543
|
+
# Store inner elements
|
|
544
|
+
self.layers["IE"].append(inner_elems)
|
|
545
|
+
for elem in inner_elems:
|
|
546
|
+
if elem in remaining_elements:
|
|
547
|
+
remaining_elements.remove(elem)
|
|
548
|
+
|
|
549
|
+
# Get inner vertices
|
|
550
|
+
all_vertices = set()
|
|
551
|
+
for elem in np.concatenate((outer_elems, inner_elems)):
|
|
552
|
+
vertices = self.connectivity_list[elem]
|
|
553
|
+
for v in vertices:
|
|
554
|
+
# Ignore negative placeholders (if any). Vertex index
|
|
555
|
+
# 0 is valid in this implementation.
|
|
556
|
+
if v >= 0:
|
|
557
|
+
all_vertices.add(v)
|
|
558
|
+
|
|
559
|
+
inner_vertices = np.array(list(all_vertices - set(outer_vertices)), dtype=int)
|
|
560
|
+
self.layers["IV"].append(inner_vertices)
|
|
561
|
+
|
|
562
|
+
# Get new boundary by finding edges that have one element in the remaining set
|
|
563
|
+
# and one element in the processed set
|
|
564
|
+
boundary_edges = []
|
|
565
|
+
for edge_idx, (e1, e2) in enumerate(edge2elem):
|
|
566
|
+
# Skip boundary edges of the original mesh
|
|
567
|
+
if e2 < 0:
|
|
568
|
+
continue
|
|
569
|
+
|
|
570
|
+
# An edge is a boundary if exactly one of its adjacent elements
|
|
571
|
+
# is in the remaining set
|
|
572
|
+
if ((e1 in remaining_elements) != (e2 in remaining_elements)):
|
|
573
|
+
boundary_edges.append(edge_idx)
|
|
574
|
+
|
|
575
|
+
boundary_edges = np.array(boundary_edges, dtype=int)
|
|
576
|
+
|
|
577
|
+
# Move to next layer
|
|
578
|
+
layer_idx += 1
|
|
579
|
+
|
|
580
|
+
# Set number of layers
|
|
581
|
+
self.n_layers = layer_idx
|
|
582
|
+
|
|
583
|
+
# # Print summary
|
|
584
|
+
# print(f"Created {self.n_layers} mesh layers")
|
|
585
|
+
# for i in range(self.n_layers):
|
|
586
|
+
# print(f" Layer {i}: {len(self.layers['OE'][i])} outer elements, {len(self.layers['IE'][i])} inner elements")
|
|
587
|
+
|
|
588
|
+
def get_layer( self, layer_idx: int ) -> Dict[str, np.ndarray]:
|
|
589
|
+
"""
|
|
590
|
+
Get the components of a specific mesh layer.
|
|
591
|
+
|
|
592
|
+
Parameters:
|
|
593
|
+
layer_idx: Index of the layer to retrieve
|
|
594
|
+
|
|
595
|
+
Returns:
|
|
596
|
+
Dictionary with outer elements (OE), inner elements (IE),
|
|
597
|
+
outer vertices (OV), and inner vertices (IV) of the layer
|
|
598
|
+
"""
|
|
599
|
+
if layer_idx < 0 or layer_idx >= self.n_layers:
|
|
600
|
+
raise ValueError( f"Layer index {layer_idx} out of range [0, {self.n_layers-1}]" )
|
|
601
|
+
|
|
602
|
+
return {
|
|
603
|
+
"OE": self.layers["OE"][layer_idx],
|
|
604
|
+
"IE": self.layers["IE"][layer_idx],
|
|
605
|
+
"OV": self.layers["OV"][layer_idx],
|
|
606
|
+
"IV": self.layers["IV"][layer_idx],
|
|
607
|
+
"bEdgeIDs": self.layers["bEdgeIDs"][layer_idx]
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
|
|
611
|
+
@staticmethod
|
|
612
|
+
def read_from_fort14(full_file_name: Path) -> "CHILmesh":
|
|
613
|
+
"""
|
|
614
|
+
Load a mesh from a FORT.14 file.
|
|
615
|
+
|
|
616
|
+
Parameters:
|
|
617
|
+
full_file_name: Path object pointing to the FORT.14 file
|
|
618
|
+
|
|
619
|
+
Returns:
|
|
620
|
+
A CHILmesh object
|
|
621
|
+
"""
|
|
622
|
+
with open(full_file_name, 'r') as f:
|
|
623
|
+
# Read header
|
|
624
|
+
header = f.readline().strip()
|
|
625
|
+
|
|
626
|
+
# Read element and node counts
|
|
627
|
+
counts = f.readline().strip().split()
|
|
628
|
+
n_elements = int(counts[0])
|
|
629
|
+
n_nodes = int(counts[1])
|
|
630
|
+
|
|
631
|
+
# Read nodes
|
|
632
|
+
points = np.zeros((n_nodes, 3)) # x, y, z
|
|
633
|
+
for i in range(n_nodes):
|
|
634
|
+
line = f.readline().strip().split()
|
|
635
|
+
points[i] = [float(line[1]), float(line[2]), float(line[3])]
|
|
636
|
+
|
|
637
|
+
# Read elements
|
|
638
|
+
elements = np.zeros((n_elements, 3), dtype=int)
|
|
639
|
+
for i in range(n_elements):
|
|
640
|
+
line = f.readline().strip().split()
|
|
641
|
+
num_nodes = int(line[1])
|
|
642
|
+
if num_nodes != 3:
|
|
643
|
+
raise ValueError(f"Only triangular elements supported, found element with {num_nodes} nodes.")
|
|
644
|
+
node_indices = [int(line[j+2]) - 1 for j in range(num_nodes)]
|
|
645
|
+
elements[i] = node_indices
|
|
646
|
+
return CHILmesh( connectivity=elements, points=points, grid_name=header )
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
def write_to_fort14( self, filename: str, grid_name: Opt[str] = "CHILmesh Grid") -> bool:
|
|
650
|
+
"""
|
|
651
|
+
Export the current mesh to ADCIRC .fort.14 format.
|
|
652
|
+
|
|
653
|
+
Parameters:
|
|
654
|
+
filename: Path to save the file
|
|
655
|
+
grid_name: Optional title for the mesh
|
|
656
|
+
"""
|
|
657
|
+
return CHILmesh.write_to_fort14(filename, self.points, self.connectivity_list, grid_name)
|
|
658
|
+
|
|
659
|
+
def interior_angles(self, elem_ids=None) -> np.ndarray:
|
|
660
|
+
"""
|
|
661
|
+
Calculate interior angles of mesh elements.
|
|
662
|
+
|
|
663
|
+
Parameters:
|
|
664
|
+
elem_ids: Indices of elements to evaluate.
|
|
665
|
+
If None, all elements are evaluated.
|
|
666
|
+
|
|
667
|
+
Returns:
|
|
668
|
+
Array of interior angles for each element
|
|
669
|
+
"""
|
|
670
|
+
if elem_ids is None:
|
|
671
|
+
elem_ids = np.arange(self.n_elems)
|
|
672
|
+
|
|
673
|
+
if np.isscalar(elem_ids):
|
|
674
|
+
elem_ids = [elem_ids]
|
|
675
|
+
|
|
676
|
+
# Determine element types
|
|
677
|
+
tri_elems, quad_elems = self._elem_type(elem_ids)
|
|
678
|
+
|
|
679
|
+
# Maximum number of angles per element
|
|
680
|
+
max_angles = 4 if len(quad_elems) > 0 else 3
|
|
681
|
+
|
|
682
|
+
# Initialize angles array
|
|
683
|
+
angles = np.zeros((len(elem_ids), max_angles))
|
|
684
|
+
|
|
685
|
+
# Calculate angles for each element
|
|
686
|
+
for i, elem_id in enumerate(elem_ids):
|
|
687
|
+
if elem_id in tri_elems:
|
|
688
|
+
# Triangle angles
|
|
689
|
+
vertices = self.connectivity_list[elem_id][:3] # First 3 vertices for triangles
|
|
690
|
+
coords = self.points[vertices, :2] # Get x,y coordinates
|
|
691
|
+
|
|
692
|
+
# Calculate angles at each vertex
|
|
693
|
+
for j in range(3):
|
|
694
|
+
v1 = coords[(j+1)%3] - coords[j]
|
|
695
|
+
v2 = coords[(j-1)%3] - coords[j]
|
|
696
|
+
|
|
697
|
+
# Normalize vectors safely to avoid runtime warnings of NaN
|
|
698
|
+
# v1_norm = v1 / np.linalg.norm(v1)
|
|
699
|
+
# v2_norm = v2 / np.linalg.norm(v2)
|
|
700
|
+
v1_norm = v1 / (np.linalg.norm(v1) + 1e-12)
|
|
701
|
+
v2_norm = v2 / (np.linalg.norm(v2) + 1e-12)
|
|
702
|
+
|
|
703
|
+
# Calculate angle in degrees
|
|
704
|
+
dot_product = np.clip(np.dot(v1_norm, v2_norm), -1.0, 1.0)
|
|
705
|
+
angle = np.arccos(dot_product) * 180 / np.pi
|
|
706
|
+
angles[i, j] = angle
|
|
707
|
+
|
|
708
|
+
elif elem_id in quad_elems:
|
|
709
|
+
# Quadrilateral angles
|
|
710
|
+
vertices = self.connectivity_list[elem_id] # All 4 vertices
|
|
711
|
+
coords = self.points[vertices, :2] # Get x,y coordinates
|
|
712
|
+
|
|
713
|
+
# Calculate angles at each vertex
|
|
714
|
+
for j in range(4):
|
|
715
|
+
v1 = coords[(j+1)%4] - coords[j]
|
|
716
|
+
v2 = coords[(j-1)%4] - coords[j]
|
|
717
|
+
|
|
718
|
+
# Normalize vectors
|
|
719
|
+
v1_norm = v1 / np.linalg.norm(v1)
|
|
720
|
+
v2_norm = v2 / np.linalg.norm(v2)
|
|
721
|
+
|
|
722
|
+
# Calculate angle in degrees
|
|
723
|
+
dot_product = np.clip(np.dot(v1_norm, v2_norm), -1.0, 1.0)
|
|
724
|
+
angle = np.arccos(dot_product) * 180 / np.pi
|
|
725
|
+
angles[i, j] = angle
|
|
726
|
+
return angles
|
|
727
|
+
|
|
728
|
+
def elem_quality(self, elem_ids=None, quality_type='skew') -> Tuple[np.ndarray, np.ndarray, dict]:
|
|
729
|
+
"""
|
|
730
|
+
Calculate the quality of mesh elements.
|
|
731
|
+
|
|
732
|
+
Parameters:
|
|
733
|
+
elem_ids: Indices of elements to evaluate.
|
|
734
|
+
If None, all elements are evaluated.
|
|
735
|
+
quality_type: Type of quality metric to use.
|
|
736
|
+
'skew', 'skewness', 'angular skewness': Measures deviation from ideal angles
|
|
737
|
+
|
|
738
|
+
Returns:
|
|
739
|
+
Tuple of (Quality, Angles) where:
|
|
740
|
+
- Quality: Array of quality measurements for each element
|
|
741
|
+
- Angles: Array of interior angles for each element
|
|
742
|
+
"""
|
|
743
|
+
if elem_ids is None:
|
|
744
|
+
elem_ids = np.arange(self.n_elems)
|
|
745
|
+
|
|
746
|
+
if np.isscalar(elem_ids):
|
|
747
|
+
elem_ids = [elem_ids]
|
|
748
|
+
|
|
749
|
+
# Determine element types
|
|
750
|
+
tri_elems, quad_elems = self._elem_type(elem_ids)
|
|
751
|
+
|
|
752
|
+
# Calculate interior angles
|
|
753
|
+
angles = self.interior_angles(elem_ids)
|
|
754
|
+
|
|
755
|
+
# Initialize quality array
|
|
756
|
+
quality = np.zeros(len(elem_ids))
|
|
757
|
+
|
|
758
|
+
# Compute quality based on the selected metric
|
|
759
|
+
if quality_type in ['skew', 'skewness', 'angular skewness']:
|
|
760
|
+
# Process triangular elements
|
|
761
|
+
tri_mask = np.array([elem_id in tri_elems for elem_id in elem_ids])
|
|
762
|
+
if np.any(tri_mask):
|
|
763
|
+
# Get angles for triangular elements
|
|
764
|
+
tri_angles = angles[tri_mask, :3]
|
|
765
|
+
|
|
766
|
+
# Calculate max and min angles
|
|
767
|
+
tri_max = np.max(tri_angles, axis=1)
|
|
768
|
+
tri_min = np.min(tri_angles, axis=1)
|
|
769
|
+
|
|
770
|
+
# Equiangular skew for triangles (ideal angle = 60°)
|
|
771
|
+
quality[tri_mask] = 1 - np.maximum(
|
|
772
|
+
(tri_max - 60) / (180 - 60),
|
|
773
|
+
(60 - tri_min) / 60
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
# Process quadrilateral elements
|
|
777
|
+
quad_mask = np.array([elem_id in quad_elems for elem_id in elem_ids])
|
|
778
|
+
if np.any(quad_mask):
|
|
779
|
+
# Get angles for quadrilateral elements
|
|
780
|
+
quad_angles = angles[quad_mask, :]
|
|
781
|
+
|
|
782
|
+
# Calculate max and min angles
|
|
783
|
+
quad_max = np.max(quad_angles, axis=1)
|
|
784
|
+
quad_min = np.min(quad_angles, axis=1)
|
|
785
|
+
|
|
786
|
+
# Equiangular skew for quads (ideal angle = 90°)
|
|
787
|
+
quality[quad_mask] = 1 - np.maximum(
|
|
788
|
+
(quad_max - 90) / (180 - 90),
|
|
789
|
+
(90 - quad_min) / 90
|
|
790
|
+
)
|
|
791
|
+
|
|
792
|
+
# Handle poor angle calculations (concave elements, etc.)
|
|
793
|
+
# For triangles, sum of angles should be close to 180°
|
|
794
|
+
tri_sum_mask = tri_mask & (np.sum(angles[:, :3], axis=1) <= 179.99)
|
|
795
|
+
quality[tri_sum_mask] = 0
|
|
796
|
+
|
|
797
|
+
# For quads, sum of angles should be close to 360°
|
|
798
|
+
quad_sum_mask = quad_mask & (np.sum(angles, axis=1) <= 359.99)
|
|
799
|
+
quality[quad_sum_mask] = 0
|
|
800
|
+
|
|
801
|
+
else:
|
|
802
|
+
raise ValueError(f"Unknown quality type: {quality_type}")
|
|
803
|
+
|
|
804
|
+
# Calculate statistics for the computed quality
|
|
805
|
+
stats = {
|
|
806
|
+
'mean': float(np.mean(quality)),
|
|
807
|
+
'median': float(np.median(quality)),
|
|
808
|
+
'min': float(np.min(quality)),
|
|
809
|
+
'max': float(np.max(quality)),
|
|
810
|
+
'std': float(np.std(quality))
|
|
811
|
+
}
|
|
812
|
+
return quality, angles, stats
|
|
813
|
+
|
|
814
|
+
def smooth_mesh(self, method: str, acknowledge_change: bool=False, *kwargs) -> np.ndarray:
|
|
815
|
+
"""
|
|
816
|
+
Perform mesh smoothing using a modified FEM-based approach.
|
|
817
|
+
|
|
818
|
+
Parameters:
|
|
819
|
+
method: Smoothing method ('FEM','angle-based')
|
|
820
|
+
acknowledge_change: If True, acknowledges the change in the mesh
|
|
821
|
+
"""
|
|
822
|
+
assert acknowledge_change, "acknowledge_change must be True to change mesh -- this will change the mesh, make sure you understand this before using this method within a broader algorithm."
|
|
823
|
+
if method.lower() == 'fem':
|
|
824
|
+
new_points = self.direct_smoother( *kwargs )
|
|
825
|
+
elif method.lower() == 'angle-based':
|
|
826
|
+
new_points = self.angle_based_smoother( *kwargs )
|
|
827
|
+
else:
|
|
828
|
+
raise ValueError(f"Unknown smoothing method: {method}")
|
|
829
|
+
self.change_points( new_points, acknowledge_change=True )
|
|
830
|
+
return new_points
|
|
831
|
+
|
|
832
|
+
def angle_based_smoother( self, angle_limit: float = 30.0 ) -> np.ndarray:
|
|
833
|
+
"""
|
|
834
|
+
Perform angle-based smoothing of the mesh.
|
|
835
|
+
Based on this: https://www.andrew.cmu.edu/user/shimada/papers/00-imr-zhou.pdf
|
|
836
|
+
Parameters:
|
|
837
|
+
angle_limit: Maximum allowable angle deviation in degrees
|
|
838
|
+
"""
|
|
839
|
+
# Placeholder for angle-based smoothing logic
|
|
840
|
+
# This would involve checking angles and adjusting points accordingly
|
|
841
|
+
# For now, just return the original points
|
|
842
|
+
raise NotImplementedError("Angle-based smoothing not implemented yet.")
|
|
843
|
+
return self.points
|
|
844
|
+
|
|
845
|
+
def direct_smoother( self, kinf=1e12 ) -> np.ndarray:
|
|
846
|
+
"""
|
|
847
|
+
Perform direct (non-iterative) FEM smoothing with fixed boundary nodes.
|
|
848
|
+
Based on the triangle stiffness formulation in Balendran (2006).
|
|
849
|
+
|
|
850
|
+
Parameters:
|
|
851
|
+
kinf: Large stiffness value for fixed boundary vertices
|
|
852
|
+
|
|
853
|
+
Reference:
|
|
854
|
+
Zhou, M., & Shimada, K. (2000).
|
|
855
|
+
"An angle-based approach to two-dimensional mesh smoothing".
|
|
856
|
+
In *Proceedings of the 9th International Meshing Roundtable*, 373–384.
|
|
857
|
+
Sandia National Laboratories.
|
|
858
|
+
https://api.semanticscholar.org/CorpusID:34335417
|
|
859
|
+
"""
|
|
860
|
+
import numpy as np
|
|
861
|
+
from scipy.sparse import csr_matrix
|
|
862
|
+
from scipy.sparse.linalg import spsolve
|
|
863
|
+
|
|
864
|
+
p = self.points[:, :2] # Only use x, y
|
|
865
|
+
t = self.connectivity_list[:, :3] # Assume triangles only
|
|
866
|
+
|
|
867
|
+
# Identify boundary nodes
|
|
868
|
+
edge_verts = self.edge2vert(self.boundary_edges())
|
|
869
|
+
boundary_nodes = np.unique(edge_verts.flatten())
|
|
870
|
+
|
|
871
|
+
n = self.n_verts
|
|
872
|
+
D = 2.0 * np.eye(2)
|
|
873
|
+
T = np.array([[-1, -np.sqrt(3)], [np.sqrt(3), -1]])
|
|
874
|
+
|
|
875
|
+
rows, cols, data = [], [], []
|
|
876
|
+
for tri in t:
|
|
877
|
+
for i in range(3):
|
|
878
|
+
for j in range(3):
|
|
879
|
+
block = D if i == j else T if j == (i+1)%3 else T.T
|
|
880
|
+
for di in range(2):
|
|
881
|
+
for dj in range(2):
|
|
882
|
+
rows.append(2*tri[i]+di)
|
|
883
|
+
cols.append(2*tri[j]+dj)
|
|
884
|
+
data.append(block[di, dj])
|
|
885
|
+
|
|
886
|
+
K = csr_matrix((data, (rows, cols)), shape=(2*n, 2*n))
|
|
887
|
+
F = np.zeros(2*n)
|
|
888
|
+
|
|
889
|
+
# Apply boundary constraints
|
|
890
|
+
for v in boundary_nodes:
|
|
891
|
+
F[2*v:2*v+2] = kinf * p[v]
|
|
892
|
+
K[2*v, 2*v] = kinf
|
|
893
|
+
K[2*v+1, 2*v+1] = kinf
|
|
894
|
+
|
|
895
|
+
c = spsolve(K, F)
|
|
896
|
+
new_points = np.zeros_like(self.points)
|
|
897
|
+
new_points[:, :2] = c.reshape(-1, 2)
|
|
898
|
+
new_points[:, 2] = self.points[:, 2] # preserve z if needed
|
|
899
|
+
return new_points
|
|
900
|
+
|
|
901
|
+
|
|
902
|
+
def copy( self ) -> "CHILmesh":
|
|
903
|
+
""" Returns: a deep copy of the new CHILmesh object with the same properties."""
|
|
904
|
+
return deepcopy(self)
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def write_fort14( filename: Path, points: np.ndarray, elements: np.ndarray, grid_name: str ) -> bool:
|
|
908
|
+
"""
|
|
909
|
+
Write mesh data to a .fort.14 ADCIRC file.
|
|
910
|
+
|
|
911
|
+
Parameters:
|
|
912
|
+
filename: Output path
|
|
913
|
+
points: (n_nodes, 2 or 3) numpy array of node coordinates
|
|
914
|
+
elements: (n_elems, 3) array of triangle vertex indices (0-based)
|
|
915
|
+
grid_name: Header string
|
|
916
|
+
"""
|
|
917
|
+
try:
|
|
918
|
+
with open(filename, 'w') as f:
|
|
919
|
+
f.write(f"{grid_name}\n")
|
|
920
|
+
f.write(f"{len(elements)} {len(points)}\n")
|
|
921
|
+
|
|
922
|
+
for i, pt in enumerate(points, start=1):
|
|
923
|
+
x, y = pt[:2]
|
|
924
|
+
z = pt[2] if len(pt) == 3 else 0.0
|
|
925
|
+
f.write(f"{i} {x:.8f} {y:.8f} {z:.8f}\n")
|
|
926
|
+
|
|
927
|
+
for i, tri in enumerate(elements, start=1):
|
|
928
|
+
n1, n2, n3 = tri + 1 # switch to 1-based indexing
|
|
929
|
+
f.write(f"{i} 3 {n1} {n2} {n3}\n")
|
|
930
|
+
return True
|
|
931
|
+
except Exception as e:
|
|
932
|
+
print(f"Error writing fort14 file {filename}: {e}")
|
|
933
|
+
return False
|