fluxfem 0.1.4__py3-none-any.whl → 0.2.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.
- fluxfem/__init__.py +68 -0
- fluxfem/core/__init__.py +115 -10
- fluxfem/core/assembly.py +676 -91
- fluxfem/core/basis.py +73 -52
- fluxfem/core/dtypes.py +9 -1
- fluxfem/core/forms.py +10 -0
- fluxfem/core/mixed_assembly.py +263 -0
- fluxfem/core/mixed_space.py +348 -0
- fluxfem/core/mixed_weakform.py +97 -0
- fluxfem/core/solver.py +2 -0
- fluxfem/core/space.py +262 -17
- fluxfem/core/weakform.py +768 -7
- fluxfem/helpers_wf.py +49 -0
- fluxfem/mesh/__init__.py +54 -2
- fluxfem/mesh/base.py +316 -7
- fluxfem/mesh/contact.py +825 -0
- fluxfem/mesh/dtypes.py +12 -0
- fluxfem/mesh/hex.py +17 -16
- fluxfem/mesh/io.py +6 -4
- fluxfem/mesh/mortar.py +3907 -0
- fluxfem/mesh/supermesh.py +316 -0
- fluxfem/mesh/surface.py +22 -4
- fluxfem/mesh/tet.py +10 -4
- fluxfem/physics/diffusion.py +3 -0
- fluxfem/physics/elasticity/hyperelastic.py +3 -0
- fluxfem/physics/elasticity/linear.py +9 -2
- fluxfem/solver/__init__.py +42 -2
- fluxfem/solver/bc.py +38 -2
- fluxfem/solver/block_matrix.py +132 -0
- fluxfem/solver/block_system.py +454 -0
- fluxfem/solver/cg.py +115 -33
- fluxfem/solver/dirichlet.py +334 -4
- fluxfem/solver/newton.py +237 -60
- fluxfem/solver/petsc.py +439 -0
- fluxfem/solver/preconditioner.py +106 -0
- fluxfem/solver/result.py +18 -0
- fluxfem/solver/solve_runner.py +168 -1
- fluxfem/solver/solver.py +12 -1
- fluxfem/solver/sparse.py +124 -9
- fluxfem-0.2.0.dist-info/METADATA +303 -0
- fluxfem-0.2.0.dist-info/RECORD +59 -0
- fluxfem-0.1.4.dist-info/METADATA +0 -127
- fluxfem-0.1.4.dist-info/RECORD +0 -48
- {fluxfem-0.1.4.dist-info → fluxfem-0.2.0.dist-info}/LICENSE +0 -0
- {fluxfem-0.1.4.dist-info → fluxfem-0.2.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import os
|
|
5
|
+
from typing import Iterable
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from .surface import SurfaceMesh
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(eq=False)
|
|
13
|
+
class SurfaceSupermesh:
|
|
14
|
+
"""Intersection supermesh for two surface meshes."""
|
|
15
|
+
coords: np.ndarray
|
|
16
|
+
conn: np.ndarray
|
|
17
|
+
source_facets_a: np.ndarray
|
|
18
|
+
source_facets_b: np.ndarray
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _polygon_area_2d(pts: np.ndarray) -> float:
|
|
22
|
+
x = pts[:, 0]
|
|
23
|
+
y = pts[:, 1]
|
|
24
|
+
return 0.5 * float(np.sum(x * np.roll(y, -1) - y * np.roll(x, -1)))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _cross2(a: np.ndarray, b: np.ndarray) -> float:
|
|
28
|
+
return float(a[0] * b[1] - a[1] * b[0])
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _line_intersection(p1, p2, p3, p4, *, tol: float):
|
|
32
|
+
d1 = p2 - p1
|
|
33
|
+
d2 = p4 - p3
|
|
34
|
+
denom = _cross2(d1, d2)
|
|
35
|
+
if abs(denom) < tol:
|
|
36
|
+
return p2
|
|
37
|
+
t = _cross2(p3 - p1, d2) / denom
|
|
38
|
+
return p1 + t * d1
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _sutherland_hodgman(subject: list[np.ndarray], clip: list[np.ndarray], *, tol: float):
|
|
42
|
+
if not subject:
|
|
43
|
+
return []
|
|
44
|
+
orient = np.sign(_polygon_area_2d(np.array(clip)))
|
|
45
|
+
if orient == 0:
|
|
46
|
+
return []
|
|
47
|
+
|
|
48
|
+
def inside(pt, a, b):
|
|
49
|
+
return orient * _cross2(b - a, pt - a) >= -tol
|
|
50
|
+
|
|
51
|
+
output = subject
|
|
52
|
+
for i in range(len(clip)):
|
|
53
|
+
input_list = output
|
|
54
|
+
if not input_list:
|
|
55
|
+
break
|
|
56
|
+
output = []
|
|
57
|
+
cp1 = clip[i]
|
|
58
|
+
cp2 = clip[(i + 1) % len(clip)]
|
|
59
|
+
s = input_list[-1]
|
|
60
|
+
for e in input_list:
|
|
61
|
+
if inside(e, cp1, cp2):
|
|
62
|
+
if not inside(s, cp1, cp2):
|
|
63
|
+
output.append(_line_intersection(s, e, cp1, cp2, tol=tol))
|
|
64
|
+
output.append(e)
|
|
65
|
+
elif inside(s, cp1, cp2):
|
|
66
|
+
output.append(_line_intersection(s, e, cp1, cp2, tol=tol))
|
|
67
|
+
s = e
|
|
68
|
+
return output
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _plane_basis(normal: np.ndarray):
|
|
72
|
+
n = normal / np.linalg.norm(normal)
|
|
73
|
+
ref = np.array([1.0, 0.0, 0.0], dtype=float)
|
|
74
|
+
if abs(np.dot(n, ref)) > 0.9:
|
|
75
|
+
ref = np.array([0.0, 1.0, 0.0], dtype=float)
|
|
76
|
+
t1 = np.cross(n, ref)
|
|
77
|
+
t1 = t1 / np.linalg.norm(t1)
|
|
78
|
+
t2 = np.cross(n, t1)
|
|
79
|
+
return t1, t2, n
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _facet_plane(pts: np.ndarray, *, tol: float):
|
|
83
|
+
n = None
|
|
84
|
+
for i in range(len(pts) - 2):
|
|
85
|
+
v1 = pts[i + 1] - pts[i]
|
|
86
|
+
v2 = pts[i + 2] - pts[i]
|
|
87
|
+
n_candidate = np.cross(v1, v2)
|
|
88
|
+
n_norm = np.linalg.norm(n_candidate)
|
|
89
|
+
if n_norm > tol:
|
|
90
|
+
n = n_candidate / n_norm
|
|
91
|
+
d = -float(np.dot(n, pts[i]))
|
|
92
|
+
return n, d
|
|
93
|
+
return None, None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _coplanar(pts_a: np.ndarray, pts_b: np.ndarray, *, tol: float) -> bool:
|
|
97
|
+
n, d = _facet_plane(pts_a, tol=tol)
|
|
98
|
+
if n is None:
|
|
99
|
+
return False
|
|
100
|
+
n2, d2 = _facet_plane(pts_b, tol=tol)
|
|
101
|
+
if n2 is None:
|
|
102
|
+
return False
|
|
103
|
+
if abs(abs(np.dot(n, n2)) - 1.0) > 1e-4:
|
|
104
|
+
return False
|
|
105
|
+
dist_a = np.abs(pts_a @ n + d)
|
|
106
|
+
dist_b = np.abs(pts_b @ n + d)
|
|
107
|
+
return np.max(dist_a) <= tol and np.max(dist_b) <= tol
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _project(points: np.ndarray, origin: np.ndarray, t1: np.ndarray, t2: np.ndarray):
|
|
111
|
+
rel = points - origin[None, :]
|
|
112
|
+
x = rel @ t1
|
|
113
|
+
y = rel @ t2
|
|
114
|
+
return np.stack([x, y], axis=1)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _unique_points(points: Iterable[np.ndarray], *, tol: float):
|
|
118
|
+
scale = 1.0 / tol
|
|
119
|
+
mapping: dict[tuple[int, int, int], int] = {}
|
|
120
|
+
coords: list[np.ndarray] = []
|
|
121
|
+
indices: list[int] = []
|
|
122
|
+
for p in points:
|
|
123
|
+
key = tuple(np.round(p * scale).astype(int))
|
|
124
|
+
idx = mapping.get(key)
|
|
125
|
+
if idx is None:
|
|
126
|
+
idx = len(coords)
|
|
127
|
+
mapping[key] = idx
|
|
128
|
+
coords.append(p)
|
|
129
|
+
indices.append(idx)
|
|
130
|
+
return np.asarray(coords, dtype=float), indices
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _facet_polygon_coords(coords: np.ndarray, facet: np.ndarray) -> np.ndarray:
|
|
134
|
+
n = int(len(facet))
|
|
135
|
+
if n == 9:
|
|
136
|
+
corner = [0, 2, 8, 6]
|
|
137
|
+
return coords[facet][corner]
|
|
138
|
+
return coords[facet]
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _triangle_min_angle(p0: np.ndarray, p1: np.ndarray, p2: np.ndarray) -> float:
|
|
142
|
+
def angle(a, b, c):
|
|
143
|
+
v1 = a - b
|
|
144
|
+
v2 = c - b
|
|
145
|
+
n1 = np.linalg.norm(v1)
|
|
146
|
+
n2 = np.linalg.norm(v2)
|
|
147
|
+
if n1 == 0.0 or n2 == 0.0:
|
|
148
|
+
return 0.0
|
|
149
|
+
cosang = np.clip(np.dot(v1, v2) / (n1 * n2), -1.0, 1.0)
|
|
150
|
+
return float(np.arccos(cosang))
|
|
151
|
+
|
|
152
|
+
return min(angle(p1, p0, p2), angle(p0, p1, p2), angle(p0, p2, p1))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _triangulate_polygon(indices: list[int], poly2d: np.ndarray) -> list[tuple[int, int, int]]:
|
|
156
|
+
n = len(indices)
|
|
157
|
+
if n < 3:
|
|
158
|
+
return []
|
|
159
|
+
if n == 3:
|
|
160
|
+
return [(indices[0], indices[1], indices[2])]
|
|
161
|
+
if n == 4:
|
|
162
|
+
p = poly2d
|
|
163
|
+
diag_pref = os.getenv("FLUXFEM_SUPERMESH_QUAD_DIAG", "alt").lower()
|
|
164
|
+
if diag_pref == "alt":
|
|
165
|
+
return [(indices[0], indices[1], indices[3]), (indices[1], indices[2], indices[3])]
|
|
166
|
+
if diag_pref == "fan":
|
|
167
|
+
return [(indices[0], indices[1], indices[2]), (indices[0], indices[2], indices[3])]
|
|
168
|
+
min_a = min(
|
|
169
|
+
_triangle_min_angle(p[0], p[1], p[2]),
|
|
170
|
+
_triangle_min_angle(p[0], p[2], p[3]),
|
|
171
|
+
)
|
|
172
|
+
min_b = min(
|
|
173
|
+
_triangle_min_angle(p[0], p[1], p[3]),
|
|
174
|
+
_triangle_min_angle(p[1], p[2], p[3]),
|
|
175
|
+
)
|
|
176
|
+
if min_b > min_a:
|
|
177
|
+
return [(indices[0], indices[1], indices[3]), (indices[1], indices[2], indices[3])]
|
|
178
|
+
return [(indices[0], indices[1], indices[2]), (indices[0], indices[2], indices[3])]
|
|
179
|
+
tris = []
|
|
180
|
+
for i in range(1, n - 1):
|
|
181
|
+
tris.append((indices[0], indices[i], indices[i + 1]))
|
|
182
|
+
return tris
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def build_surface_supermesh(
|
|
186
|
+
surface_a: SurfaceMesh,
|
|
187
|
+
surface_b: SurfaceMesh,
|
|
188
|
+
*,
|
|
189
|
+
tol: float = 1e-8,
|
|
190
|
+
cache_enabled: bool | None = None,
|
|
191
|
+
cache_trace: bool | None = None,
|
|
192
|
+
) -> SurfaceSupermesh:
|
|
193
|
+
from ..solver.bc import facet_normals
|
|
194
|
+
import hashlib
|
|
195
|
+
|
|
196
|
+
if cache_enabled is None:
|
|
197
|
+
cache_enabled = os.getenv("FLUXFEM_SUPERMESH_CACHE", "0") not in ("0", "", "false", "False")
|
|
198
|
+
if cache_trace is None:
|
|
199
|
+
cache_trace = os.getenv("FLUXFEM_SUPERMESH_CACHE_TRACE", "0") not in ("0", "", "false", "False")
|
|
200
|
+
|
|
201
|
+
def _array_sig(arr: np.ndarray) -> tuple:
|
|
202
|
+
arr_c = np.ascontiguousarray(arr)
|
|
203
|
+
h = hashlib.blake2b(arr_c.view(np.uint8), digest_size=8).hexdigest()
|
|
204
|
+
return (arr_c.shape, str(arr_c.dtype), h)
|
|
205
|
+
if cache_enabled:
|
|
206
|
+
global _SUPERMESH_CACHE
|
|
207
|
+
try:
|
|
208
|
+
_SUPERMESH_CACHE
|
|
209
|
+
except NameError:
|
|
210
|
+
_SUPERMESH_CACHE = {}
|
|
211
|
+
key = (
|
|
212
|
+
_array_sig(np.asarray(surface_a.coords)),
|
|
213
|
+
_array_sig(np.asarray(surface_a.conn)),
|
|
214
|
+
_array_sig(np.asarray(surface_b.coords)),
|
|
215
|
+
_array_sig(np.asarray(surface_b.conn)),
|
|
216
|
+
float(tol),
|
|
217
|
+
)
|
|
218
|
+
cached = _SUPERMESH_CACHE.get(key)
|
|
219
|
+
if cached is not None:
|
|
220
|
+
if cache_trace:
|
|
221
|
+
print(f"[supermesh] cache hit n_tris={int(cached.conn.shape[0])}", flush=True)
|
|
222
|
+
return cached
|
|
223
|
+
|
|
224
|
+
coords_a = np.asarray(surface_a.coords, dtype=float)
|
|
225
|
+
coords_b = np.asarray(surface_b.coords, dtype=float)
|
|
226
|
+
facets_a = np.asarray(surface_a.conn, dtype=int)
|
|
227
|
+
facets_b = np.asarray(surface_b.conn, dtype=int)
|
|
228
|
+
normals_a = facet_normals(surface_a, outward_from=np.mean(coords_a, axis=0), normalize=True)
|
|
229
|
+
|
|
230
|
+
all_coords: list[np.ndarray] = []
|
|
231
|
+
all_conn: list[tuple[int, int, int]] = []
|
|
232
|
+
src_a: list[int] = []
|
|
233
|
+
src_b: list[int] = []
|
|
234
|
+
|
|
235
|
+
for ia, fa in enumerate(facets_a):
|
|
236
|
+
pts_a = _facet_polygon_coords(coords_a, fa)
|
|
237
|
+
min_a = pts_a.min(axis=0)
|
|
238
|
+
max_a = pts_a.max(axis=0)
|
|
239
|
+
for ib, fb in enumerate(facets_b):
|
|
240
|
+
pts_b = _facet_polygon_coords(coords_b, fb)
|
|
241
|
+
if np.any(pts_b.max(axis=0) < min_a - tol) or np.any(pts_b.min(axis=0) > max_a + tol):
|
|
242
|
+
continue
|
|
243
|
+
if not _coplanar(pts_a, pts_b, tol=tol):
|
|
244
|
+
continue
|
|
245
|
+
|
|
246
|
+
n, _d = _facet_plane(pts_a, tol=tol)
|
|
247
|
+
if n is not None:
|
|
248
|
+
n_ref = normals_a[int(ia)]
|
|
249
|
+
if np.dot(n, n_ref) < 0.0:
|
|
250
|
+
n = -n
|
|
251
|
+
t1, t2, _ = _plane_basis(n)
|
|
252
|
+
origin = pts_a[0]
|
|
253
|
+
|
|
254
|
+
poly_a = _project(pts_a, origin, t1, t2)
|
|
255
|
+
poly_b = _project(pts_b, origin, t1, t2)
|
|
256
|
+
|
|
257
|
+
inter = _sutherland_hodgman(
|
|
258
|
+
[p.copy() for p in poly_a],
|
|
259
|
+
[p.copy() for p in poly_b],
|
|
260
|
+
tol=tol,
|
|
261
|
+
)
|
|
262
|
+
if len(inter) < 3:
|
|
263
|
+
continue
|
|
264
|
+
inter_np = np.asarray(inter)
|
|
265
|
+
if abs(_polygon_area_2d(inter_np)) <= tol:
|
|
266
|
+
continue
|
|
267
|
+
center = np.mean(inter_np, axis=0)
|
|
268
|
+
angles = np.arctan2(inter_np[:, 1] - center[1], inter_np[:, 0] - center[0])
|
|
269
|
+
order = np.argsort(angles)
|
|
270
|
+
inter_np = inter_np[order]
|
|
271
|
+
|
|
272
|
+
inter_3d = origin[None, :] + inter_np[:, 0:1] * t1 + inter_np[:, 1:2] * t2
|
|
273
|
+
coords_local, idx = _unique_points(inter_3d, tol=tol)
|
|
274
|
+
base = len(all_coords)
|
|
275
|
+
for p in coords_local:
|
|
276
|
+
all_coords.append(p)
|
|
277
|
+
tris = _triangulate_polygon(idx, inter_np)
|
|
278
|
+
for a_idx, b_idx, c_idx in tris:
|
|
279
|
+
a_id = base + a_idx
|
|
280
|
+
b_id = base + b_idx
|
|
281
|
+
c_id = base + c_idx
|
|
282
|
+
if n is not None:
|
|
283
|
+
pa = all_coords[a_id]
|
|
284
|
+
pb = all_coords[b_id]
|
|
285
|
+
pc = all_coords[c_id]
|
|
286
|
+
n_tri = np.cross(pb - pa, pc - pa)
|
|
287
|
+
if np.dot(n_tri, n) < 0.0:
|
|
288
|
+
b_id, c_id = c_id, b_id
|
|
289
|
+
all_conn.append((a_id, b_id, c_id))
|
|
290
|
+
src_a.append(ia)
|
|
291
|
+
src_b.append(ib)
|
|
292
|
+
|
|
293
|
+
if not all_conn:
|
|
294
|
+
sm = SurfaceSupermesh(
|
|
295
|
+
coords=np.zeros((0, 3), dtype=float),
|
|
296
|
+
conn=np.zeros((0, 3), dtype=int),
|
|
297
|
+
source_facets_a=np.zeros((0,), dtype=int),
|
|
298
|
+
source_facets_b=np.zeros((0,), dtype=int),
|
|
299
|
+
)
|
|
300
|
+
if cache_enabled:
|
|
301
|
+
_SUPERMESH_CACHE[key] = sm
|
|
302
|
+
return sm
|
|
303
|
+
|
|
304
|
+
coords = np.asarray(all_coords, dtype=float)
|
|
305
|
+
conn = np.asarray(all_conn, dtype=int)
|
|
306
|
+
sm = SurfaceSupermesh(
|
|
307
|
+
coords=coords,
|
|
308
|
+
conn=conn,
|
|
309
|
+
source_facets_a=np.asarray(src_a, dtype=int),
|
|
310
|
+
source_facets_b=np.asarray(src_b, dtype=int),
|
|
311
|
+
)
|
|
312
|
+
if cache_enabled:
|
|
313
|
+
_SUPERMESH_CACHE[key] = sm
|
|
314
|
+
if cache_trace:
|
|
315
|
+
print(f"[supermesh] cache store n_tris={int(sm.conn.shape[0])}", flush=True)
|
|
316
|
+
return sm
|
fluxfem/mesh/surface.py
CHANGED
|
@@ -4,6 +4,8 @@ from dataclasses import dataclass
|
|
|
4
4
|
from typing import Optional
|
|
5
5
|
import jax
|
|
6
6
|
import jax.numpy as jnp
|
|
7
|
+
|
|
8
|
+
from .dtypes import INDEX_DTYPE
|
|
7
9
|
import numpy as np
|
|
8
10
|
|
|
9
11
|
DTYPE = jnp.float64 if jax.config.read("jax_enable_x64") else jnp.float32
|
|
@@ -53,8 +55,8 @@ class SurfaceMesh(BaseMesh):
|
|
|
53
55
|
node_tags: Optional[jnp.ndarray] = None,
|
|
54
56
|
) -> "SurfaceMesh":
|
|
55
57
|
coords_j = jnp.asarray(coords, dtype=DTYPE)
|
|
56
|
-
facets_j = jnp.asarray(facets, dtype=
|
|
57
|
-
tags_j = None if facet_tags is None else jnp.asarray(facet_tags, dtype=
|
|
58
|
+
facets_j = jnp.asarray(facets, dtype=INDEX_DTYPE)
|
|
59
|
+
tags_j = None if facet_tags is None else jnp.asarray(facet_tags, dtype=INDEX_DTYPE)
|
|
58
60
|
node_tags_j = None if node_tags is None else jnp.asarray(node_tags)
|
|
59
61
|
return cls(coords=coords_j, conn=facets_j, cell_tags=tags_j, node_tags=node_tags_j, facet_tags=tags_j)
|
|
60
62
|
|
|
@@ -117,6 +119,22 @@ class SurfaceMesh(BaseMesh):
|
|
|
117
119
|
n_total_nodes = int(getattr(space, "mesh", self).n_nodes)
|
|
118
120
|
return self.assemble_linear_form(form, params, dim=dim, n_total_nodes=n_total_nodes, F0=F0)
|
|
119
121
|
|
|
122
|
+
|
|
123
|
+
@dataclass(frozen=True)
|
|
124
|
+
class SurfaceWithElemConn:
|
|
125
|
+
surface: SurfaceMesh
|
|
126
|
+
elem_conn: np.ndarray
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def surface_with_elem_conn(mesh: BaseMesh, facets, *, mode: str = "touching") -> SurfaceWithElemConn:
|
|
130
|
+
"""
|
|
131
|
+
Build a SurfaceMesh from facets and return it with a matching elem_conn array.
|
|
132
|
+
"""
|
|
133
|
+
surface = SurfaceMesh.from_facets(mesh.coords, facets, node_tags=mesh.node_tags)
|
|
134
|
+
elems = mesh.elements_from_facets(facets, mode=mode)
|
|
135
|
+
elem_conn = np.asarray(mesh.conn, dtype=int)[elems]
|
|
136
|
+
return SurfaceWithElemConn(surface=surface, elem_conn=elem_conn)
|
|
137
|
+
|
|
120
138
|
def assemble_traction(
|
|
121
139
|
self,
|
|
122
140
|
traction,
|
|
@@ -162,8 +180,8 @@ class SurfaceMeshPytree(BaseMeshPytree):
|
|
|
162
180
|
node_tags: Optional[jnp.ndarray] = None,
|
|
163
181
|
) -> "SurfaceMeshPytree":
|
|
164
182
|
coords_j = jnp.asarray(coords, dtype=DTYPE)
|
|
165
|
-
facets_j = jnp.asarray(facets, dtype=
|
|
166
|
-
tags_j = None if facet_tags is None else jnp.asarray(facet_tags, dtype=
|
|
183
|
+
facets_j = jnp.asarray(facets, dtype=INDEX_DTYPE)
|
|
184
|
+
tags_j = None if facet_tags is None else jnp.asarray(facet_tags, dtype=INDEX_DTYPE)
|
|
167
185
|
node_tags_j = None if node_tags is None else jnp.asarray(node_tags)
|
|
168
186
|
return cls(coords=coords_j, conn=facets_j, cell_tags=tags_j, node_tags=node_tags_j, facet_tags=tags_j)
|
|
169
187
|
|
fluxfem/mesh/tet.py
CHANGED
|
@@ -5,6 +5,8 @@ import jax
|
|
|
5
5
|
import jax.numpy as jnp
|
|
6
6
|
import numpy as np
|
|
7
7
|
|
|
8
|
+
from .dtypes import NP_INDEX_DTYPE
|
|
9
|
+
|
|
8
10
|
DTYPE = jnp.float64 if jax.config.read("jax_enable_x64") else jnp.float32
|
|
9
11
|
|
|
10
12
|
|
|
@@ -58,6 +60,10 @@ class StructuredTetBox:
|
|
|
58
60
|
J = np.stack([p1 - p0, p2 - p0, p3 - p0], axis=1)
|
|
59
61
|
if np.linalg.det(J) < 0:
|
|
60
62
|
tet[[1, 2]] = tet[[2, 1]] # swap corner1/corner2
|
|
63
|
+
if tet.shape[0] == 10:
|
|
64
|
+
# keep edge-node ordering consistent with corner swap
|
|
65
|
+
tet[[4, 6]] = tet[[6, 4]] # edges (0-1) <-> (0-2)
|
|
66
|
+
tet[[8, 9]] = tet[[9, 8]] # edges (1-3) <-> (2-3)
|
|
61
67
|
conn[idx] = tet
|
|
62
68
|
return conn
|
|
63
69
|
|
|
@@ -115,10 +121,10 @@ class StructuredTetBox:
|
|
|
115
121
|
n12 = add_node(mid(p1, p2))
|
|
116
122
|
n13 = add_node(mid(p1, p3))
|
|
117
123
|
n23 = add_node(mid(p2, p3))
|
|
118
|
-
conn_list.append([n0, n1, n2, n3, n01, n02, n03,
|
|
124
|
+
conn_list.append([n0, n1, n2, n3, n01, n12, n02, n03, n13, n23])
|
|
119
125
|
|
|
120
126
|
coords = np.asarray(coords_list, dtype=DTYPE)
|
|
121
|
-
conn = np.asarray(conn_list, dtype=
|
|
127
|
+
conn = np.asarray(conn_list, dtype=NP_INDEX_DTYPE)
|
|
122
128
|
conn = self._fix_orientation(coords, conn)
|
|
123
129
|
return TetMesh(coords=jnp.array(coords), conn=jnp.array(conn))
|
|
124
130
|
|
|
@@ -169,7 +175,7 @@ class StructuredTetBox:
|
|
|
169
175
|
[v010, v001, v011, v111],
|
|
170
176
|
]
|
|
171
177
|
)
|
|
172
|
-
conn = np.asarray(conn_list, dtype=
|
|
178
|
+
conn = np.asarray(conn_list, dtype=NP_INDEX_DTYPE)
|
|
173
179
|
conn = self._fix_orientation(coords, conn)
|
|
174
180
|
return TetMesh(coords=jnp.array(coords), conn=jnp.array(conn))
|
|
175
181
|
|
|
@@ -241,6 +247,6 @@ class StructuredTetTensorBox:
|
|
|
241
247
|
T[:, (5 * ne):] = t[[0, 3, 6, 7]]
|
|
242
248
|
|
|
243
249
|
coords = p.T.astype(DTYPE, copy=False)
|
|
244
|
-
conn = T.T.astype(
|
|
250
|
+
conn = T.T.astype(NP_INDEX_DTYPE, copy=False)
|
|
245
251
|
conn = self._fix_orientation(coords, conn)
|
|
246
252
|
return TetMesh(coords=jnp.array(coords), conn=jnp.array(conn))
|
fluxfem/physics/diffusion.py
CHANGED
|
@@ -78,6 +78,9 @@ def neo_hookean_residual_form(ctx: FormContext, u_elem: jnp.ndarray, params) ->
|
|
|
78
78
|
return jnp.einsum("qik,qk->qi", BT, S_voigt) # (n_q, n_ldofs)
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
neo_hookean_residual_form._ff_kind = "residual"
|
|
82
|
+
neo_hookean_residual_form._ff_domain = "volume"
|
|
83
|
+
|
|
81
84
|
__all__ = [
|
|
82
85
|
"right_cauchy_green",
|
|
83
86
|
"green_lagrange_strain",
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import jax.numpy as jnp
|
|
2
2
|
|
|
3
|
-
from ...core.assembly import assemble_linear_form
|
|
4
3
|
from ...core.forms import FormContext, vector_load_form
|
|
5
4
|
from ...core.basis import build_B_matrices
|
|
6
5
|
from ...physics.operators import sym_grad
|
|
@@ -26,21 +25,29 @@ def linear_elasticity_form(ctx: FormContext, D: jnp.ndarray) -> jnp.ndarray:
|
|
|
26
25
|
symmetric-gradient operator for the test/trial fields.
|
|
27
26
|
"""
|
|
28
27
|
Bu = sym_grad(ctx.trial) # (n_q, 6, ndofs_e)
|
|
29
|
-
Bv =
|
|
28
|
+
Bv = Bu if ctx.test is ctx.trial else sym_grad(ctx.test)
|
|
30
29
|
return jnp.einsum("qik,kl,qlm->qim", jnp.swapaxes(Bv, 1, 2), D, Bu)
|
|
31
30
|
|
|
32
31
|
|
|
32
|
+
linear_elasticity_form._ff_kind = "bilinear"
|
|
33
|
+
linear_elasticity_form._ff_domain = "volume"
|
|
34
|
+
|
|
35
|
+
|
|
33
36
|
def vector_body_force_form(ctx: FormContext, load_vec: jnp.ndarray) -> jnp.ndarray:
|
|
34
37
|
"""Linear form for 3D vector body force f (constant in space)."""
|
|
35
38
|
return vector_load_form(ctx.test, load_vec)
|
|
36
39
|
|
|
37
40
|
|
|
41
|
+
vector_body_force_form._ff_kind = "linear"
|
|
42
|
+
vector_body_force_form._ff_domain = "volume"
|
|
43
|
+
|
|
38
44
|
def assemble_constant_body_force(space, gravity_vec, density: float, *, sparse: bool = False):
|
|
39
45
|
"""
|
|
40
46
|
Convenience: assemble body force from density * gravity vector.
|
|
41
47
|
gravity_vec: length-3 array-like (direction and magnitude of g)
|
|
42
48
|
density: scalar density (consistent with unit system)
|
|
43
49
|
"""
|
|
50
|
+
from ...core.assembly import assemble_linear_form
|
|
44
51
|
g = jnp.asarray(gravity_vec)
|
|
45
52
|
f_vec = density * g
|
|
46
53
|
return assemble_linear_form(space, vector_body_force_form, params=f_vec, sparse=sparse)
|
fluxfem/solver/__init__.py
CHANGED
|
@@ -1,13 +1,30 @@
|
|
|
1
|
-
from .sparse import
|
|
1
|
+
from .sparse import (
|
|
2
|
+
SparsityPattern,
|
|
3
|
+
FluxSparseMatrix,
|
|
4
|
+
coalesce_coo,
|
|
5
|
+
concat_flux,
|
|
6
|
+
block_diag_flux,
|
|
7
|
+
)
|
|
2
8
|
from .dirichlet import (
|
|
9
|
+
DirichletBC,
|
|
3
10
|
enforce_dirichlet_dense,
|
|
11
|
+
enforce_dirichlet_dense_jax,
|
|
12
|
+
enforce_dirichlet_fluxsparse,
|
|
4
13
|
enforce_dirichlet_sparse,
|
|
5
14
|
free_dofs,
|
|
15
|
+
split_dirichlet_matrix,
|
|
16
|
+
restrict_flux_to_free,
|
|
17
|
+
condense_dirichlet_system,
|
|
18
|
+
enforce_dirichlet_system,
|
|
6
19
|
condense_dirichlet_fluxsparse,
|
|
20
|
+
condense_dirichlet_fluxsparse_coo,
|
|
7
21
|
condense_dirichlet_dense,
|
|
8
22
|
expand_dirichlet_solution,
|
|
9
23
|
)
|
|
10
|
-
from .cg import cg_solve, cg_solve_jax
|
|
24
|
+
from .cg import cg_solve, cg_solve_jax, build_cg_operator, CGOperator
|
|
25
|
+
from .preconditioner import make_block_jacobi_preconditioner
|
|
26
|
+
from .block_system import build_block_system, split_block_matrix, BlockSystem
|
|
27
|
+
from .block_matrix import diag as block_diag, make as make_block_matrix
|
|
11
28
|
from .newton import newton_solve
|
|
12
29
|
from .solve_runner import (
|
|
13
30
|
NonlinearAnalysis,
|
|
@@ -21,18 +38,38 @@ from .solve_runner import (
|
|
|
21
38
|
LinearSolveRunner,
|
|
22
39
|
)
|
|
23
40
|
from .solver import LinearSolver, NonlinearSolver
|
|
41
|
+
from .petsc import petsc_solve, petsc_shell_solve, petsc_is_available
|
|
24
42
|
|
|
25
43
|
__all__ = [
|
|
26
44
|
"SparsityPattern",
|
|
27
45
|
"FluxSparseMatrix",
|
|
46
|
+
"coalesce_coo",
|
|
47
|
+
"concat_flux",
|
|
48
|
+
"block_diag_flux",
|
|
49
|
+
"DirichletBC",
|
|
28
50
|
"enforce_dirichlet_dense",
|
|
51
|
+
"enforce_dirichlet_dense_jax",
|
|
52
|
+
"enforce_dirichlet_fluxsparse",
|
|
29
53
|
"enforce_dirichlet_sparse",
|
|
54
|
+
"split_dirichlet_matrix",
|
|
55
|
+
"enforce_dirichlet_system",
|
|
30
56
|
"free_dofs",
|
|
57
|
+
"restrict_flux_to_free",
|
|
58
|
+
"condense_dirichlet_system",
|
|
31
59
|
"condense_dirichlet_fluxsparse",
|
|
60
|
+
"condense_dirichlet_fluxsparse_coo",
|
|
32
61
|
"condense_dirichlet_dense",
|
|
33
62
|
"expand_dirichlet_solution",
|
|
34
63
|
"cg_solve",
|
|
35
64
|
"cg_solve_jax",
|
|
65
|
+
"build_cg_operator",
|
|
66
|
+
"CGOperator",
|
|
67
|
+
"make_block_jacobi_preconditioner",
|
|
68
|
+
"build_block_system",
|
|
69
|
+
"split_block_matrix",
|
|
70
|
+
"BlockSystem",
|
|
71
|
+
"block_diag",
|
|
72
|
+
"make_block_matrix",
|
|
36
73
|
"newton_solve",
|
|
37
74
|
"LinearAnalysis",
|
|
38
75
|
"LinearSolveConfig",
|
|
@@ -44,4 +81,7 @@ __all__ = [
|
|
|
44
81
|
"solve_nonlinear",
|
|
45
82
|
"LinearSolver",
|
|
46
83
|
"NonlinearSolver",
|
|
84
|
+
"petsc_solve",
|
|
85
|
+
"petsc_shell_solve",
|
|
86
|
+
"petsc_is_available",
|
|
47
87
|
]
|
fluxfem/solver/bc.py
CHANGED
|
@@ -107,6 +107,10 @@ def vector_surface_load_form(ctx: SurfaceFormContext, load: npt.ArrayLike) -> np
|
|
|
107
107
|
return np.asarray(vector_load_form(ctx.v, load_arr))
|
|
108
108
|
|
|
109
109
|
|
|
110
|
+
vector_surface_load_form._ff_kind = "linear"
|
|
111
|
+
vector_surface_load_form._ff_domain = "surface"
|
|
112
|
+
|
|
113
|
+
|
|
110
114
|
def make_vector_surface_load_form(load_fn):
|
|
111
115
|
"""
|
|
112
116
|
Build a vector surface load form from a callable f(x_q) -> (n_q, dim).
|
|
@@ -115,9 +119,32 @@ def make_vector_surface_load_form(load_fn):
|
|
|
115
119
|
load_q = load_fn(ctx.x_q)
|
|
116
120
|
return vector_surface_load_form(ctx, load_q)
|
|
117
121
|
|
|
122
|
+
_form._ff_kind = "linear"
|
|
123
|
+
_form._ff_domain = "surface"
|
|
118
124
|
return _form
|
|
119
125
|
|
|
120
126
|
|
|
127
|
+
def traction_vector(traction, traction_dir: str) -> np.ndarray:
|
|
128
|
+
"""
|
|
129
|
+
Resolve traction magnitude and direction string into a vector.
|
|
130
|
+
"""
|
|
131
|
+
dir_map = {
|
|
132
|
+
"x": (1.0, 0.0, 0.0),
|
|
133
|
+
"xpos": (1.0, 0.0, 0.0),
|
|
134
|
+
"xneg": (-1.0, 0.0, 0.0),
|
|
135
|
+
"y": (0.0, 1.0, 0.0),
|
|
136
|
+
"ypos": (0.0, 1.0, 0.0),
|
|
137
|
+
"yneg": (0.0, -1.0, 0.0),
|
|
138
|
+
"z": (0.0, 0.0, 1.0),
|
|
139
|
+
"zpos": (0.0, 0.0, 1.0),
|
|
140
|
+
"zneg": (0.0, 0.0, -1.0),
|
|
141
|
+
}
|
|
142
|
+
key = traction_dir.strip().lower()
|
|
143
|
+
if key not in dir_map:
|
|
144
|
+
raise ValueError("TRACTION_DIR must be one of x/xpos/xneg/y/ypos/yneg/z/zpos/zneg")
|
|
145
|
+
return float(traction) * np.asarray(dir_map[key], dtype=float)
|
|
146
|
+
|
|
147
|
+
|
|
121
148
|
def _surface_quadrature(node_coords: np.ndarray):
|
|
122
149
|
m = node_coords.shape[0]
|
|
123
150
|
if m == 4:
|
|
@@ -404,8 +431,17 @@ def facet_normals(surface: SurfaceMesh, *, outward_from: npt.ArrayLike | None =
|
|
|
404
431
|
for i, facet in enumerate(facets):
|
|
405
432
|
if len(facet) < 3:
|
|
406
433
|
continue
|
|
407
|
-
|
|
408
|
-
|
|
434
|
+
n = None
|
|
435
|
+
p0 = coords[facet[0]]
|
|
436
|
+
for j in range(1, len(facet) - 1):
|
|
437
|
+
p1 = coords[facet[j]]
|
|
438
|
+
p2 = coords[facet[j + 1]]
|
|
439
|
+
n_candidate = np.cross(p1 - p0, p2 - p0)
|
|
440
|
+
if np.linalg.norm(n_candidate) > 0.0:
|
|
441
|
+
n = n_candidate
|
|
442
|
+
break
|
|
443
|
+
if n is None:
|
|
444
|
+
continue
|
|
409
445
|
if normalize:
|
|
410
446
|
norm = np.linalg.norm(n)
|
|
411
447
|
n = n / norm if norm != 0.0 else n
|