fluxfem 0.1.3__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 +136 -161
- fluxfem/core/__init__.py +172 -41
- fluxfem/core/assembly.py +676 -91
- fluxfem/core/basis.py +73 -52
- fluxfem/core/context_types.py +36 -0
- fluxfem/core/dtypes.py +9 -1
- fluxfem/core/forms.py +15 -1
- 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 +1503 -312
- fluxfem/helpers_wf.py +53 -0
- fluxfem/mesh/__init__.py +54 -2
- fluxfem/mesh/base.py +322 -8
- fluxfem/mesh/contact.py +825 -0
- fluxfem/mesh/dtypes.py +12 -0
- fluxfem/mesh/hex.py +18 -16
- fluxfem/mesh/io.py +8 -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.3.dist-info/METADATA +0 -125
- fluxfem-0.1.3.dist-info/RECORD +0 -47
- {fluxfem-0.1.3.dist-info → fluxfem-0.2.0.dist-info}/LICENSE +0 -0
- {fluxfem-0.1.3.dist-info → fluxfem-0.2.0.dist-info}/WHEEL +0 -0
fluxfem/mesh/contact.py
ADDED
|
@@ -0,0 +1,825 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Mapping, Sequence
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
from .mortar import (
|
|
9
|
+
assemble_mixed_surface_jacobian,
|
|
10
|
+
assemble_mixed_surface_residual,
|
|
11
|
+
assemble_onesided_bilinear,
|
|
12
|
+
assemble_contact_onesided_floor,
|
|
13
|
+
assemble_mortar_matrices,
|
|
14
|
+
map_surface_facets_to_tet_elements,
|
|
15
|
+
map_surface_facets_to_hex_elements,
|
|
16
|
+
)
|
|
17
|
+
from .supermesh import build_surface_supermesh
|
|
18
|
+
from .surface import SurfaceMesh
|
|
19
|
+
from .base import BaseMesh
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class ContactSide:
|
|
24
|
+
surface: SurfaceMesh
|
|
25
|
+
elem_conn: np.ndarray | None
|
|
26
|
+
value_dim: int
|
|
27
|
+
space: object | None = None
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def from_facets(
|
|
31
|
+
cls,
|
|
32
|
+
mesh: BaseMesh,
|
|
33
|
+
facets: np.ndarray,
|
|
34
|
+
space=None,
|
|
35
|
+
*,
|
|
36
|
+
value_dim: int | None = None,
|
|
37
|
+
mode: str = "touching",
|
|
38
|
+
):
|
|
39
|
+
side = mesh.surface_with_elem_conn_from_facets(facets, mode=mode)
|
|
40
|
+
if value_dim is None:
|
|
41
|
+
if space is None:
|
|
42
|
+
raise ValueError("space or value_dim is required for ContactSide.from_facets")
|
|
43
|
+
value_dim = int(getattr(space, "value_dim", 1))
|
|
44
|
+
return cls(surface=side.surface, elem_conn=side.elem_conn, value_dim=int(value_dim), space=space)
|
|
45
|
+
|
|
46
|
+
@classmethod
|
|
47
|
+
def from_surfaces(
|
|
48
|
+
cls,
|
|
49
|
+
surface: SurfaceMesh,
|
|
50
|
+
*,
|
|
51
|
+
elem_conn: np.ndarray | None = None,
|
|
52
|
+
value_dim: int = 1,
|
|
53
|
+
space: object | None = None,
|
|
54
|
+
):
|
|
55
|
+
return cls(surface=surface, elem_conn=elem_conn, value_dim=int(value_dim), space=space)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _facet_map_for_elem_conn(surface: SurfaceMesh, elem_conn: np.ndarray | None) -> np.ndarray:
|
|
59
|
+
if elem_conn is None:
|
|
60
|
+
raise ValueError("elem_conn is required to build facet_to_elem mapping.")
|
|
61
|
+
if elem_conn.shape[1] in {4, 10}:
|
|
62
|
+
return map_surface_facets_to_tet_elements(surface, elem_conn)
|
|
63
|
+
if elem_conn.shape[1] in {8, 20, 27}:
|
|
64
|
+
return map_surface_facets_to_hex_elements(surface, elem_conn)
|
|
65
|
+
raise NotImplementedError("elem_conn must be tet4/tet10/hex8/hex20/hex27")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def facet_gap_values(
|
|
69
|
+
coords: np.ndarray,
|
|
70
|
+
facets: np.ndarray,
|
|
71
|
+
u: np.ndarray,
|
|
72
|
+
n: np.ndarray,
|
|
73
|
+
c: float,
|
|
74
|
+
*,
|
|
75
|
+
value_dim: int | None = None,
|
|
76
|
+
reduce: str = "min",
|
|
77
|
+
) -> tuple[np.ndarray, float]:
|
|
78
|
+
"""
|
|
79
|
+
Compute per-facet gap values for a one-sided contact plane.
|
|
80
|
+
|
|
81
|
+
Returns (g_f, min_g_all) where g_f is reduced per facet and min_g_all is
|
|
82
|
+
the global minimum node gap.
|
|
83
|
+
"""
|
|
84
|
+
coords_np = np.asarray(coords, dtype=float)
|
|
85
|
+
if value_dim is None:
|
|
86
|
+
value_dim = int(coords_np.shape[1])
|
|
87
|
+
u_nodes = np.asarray(u, dtype=float).reshape(-1, value_dim)
|
|
88
|
+
x_cur = coords_np + u_nodes
|
|
89
|
+
g_all = np.dot(x_cur, np.asarray(n, dtype=float)) - float(c)
|
|
90
|
+
min_g_all = float(np.min(g_all)) if g_all.size else 0.0
|
|
91
|
+
if facets is None or len(facets) == 0:
|
|
92
|
+
return np.zeros((0,), dtype=float), min_g_all
|
|
93
|
+
if reduce == "min":
|
|
94
|
+
g_f = np.array([np.min(g_all[np.asarray(facet, dtype=int)]) for facet in facets], dtype=float)
|
|
95
|
+
elif reduce == "mean":
|
|
96
|
+
g_f = np.array([np.mean(g_all[np.asarray(facet, dtype=int)]) for facet in facets], dtype=float)
|
|
97
|
+
else:
|
|
98
|
+
raise ValueError("reduce must be 'min' or 'mean'")
|
|
99
|
+
return g_f, min_g_all
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def active_contact_facets(
|
|
103
|
+
coords: np.ndarray,
|
|
104
|
+
facets: np.ndarray,
|
|
105
|
+
u: np.ndarray,
|
|
106
|
+
n: np.ndarray,
|
|
107
|
+
c: float,
|
|
108
|
+
*,
|
|
109
|
+
value_dim: int | None = None,
|
|
110
|
+
reduce: str = "min",
|
|
111
|
+
threshold: float = 0.0,
|
|
112
|
+
) -> tuple[np.ndarray, float]:
|
|
113
|
+
"""Return active facet indices and global minimum gap for one-sided contact."""
|
|
114
|
+
g_f, min_g_all = facet_gap_values(
|
|
115
|
+
coords,
|
|
116
|
+
facets,
|
|
117
|
+
u,
|
|
118
|
+
n,
|
|
119
|
+
c,
|
|
120
|
+
value_dim=value_dim,
|
|
121
|
+
reduce=reduce,
|
|
122
|
+
)
|
|
123
|
+
active_ids = np.nonzero(g_f < threshold)[0]
|
|
124
|
+
return active_ids, min_g_all
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@dataclass(frozen=True)
|
|
128
|
+
class OneSidedContact:
|
|
129
|
+
side: ContactSide
|
|
130
|
+
n: np.ndarray | None
|
|
131
|
+
c: float
|
|
132
|
+
k: float
|
|
133
|
+
beta: float
|
|
134
|
+
quad_order: int = 2
|
|
135
|
+
normal_sign: float = 1.0
|
|
136
|
+
tol: float = 1e-8
|
|
137
|
+
facet_map: np.ndarray | None = None
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def from_side(
|
|
141
|
+
cls,
|
|
142
|
+
side: ContactSide,
|
|
143
|
+
*,
|
|
144
|
+
n: np.ndarray | None,
|
|
145
|
+
c: float,
|
|
146
|
+
k: float,
|
|
147
|
+
beta: float,
|
|
148
|
+
quad_order: int = 2,
|
|
149
|
+
normal_sign: float = 1.0,
|
|
150
|
+
tol: float = 1e-8,
|
|
151
|
+
facet_map: np.ndarray | None = None,
|
|
152
|
+
) -> "OneSidedContact":
|
|
153
|
+
if facet_map is None:
|
|
154
|
+
facet_map = _facet_map_for_elem_conn(side.surface, side.elem_conn)
|
|
155
|
+
return cls(
|
|
156
|
+
side=side,
|
|
157
|
+
n=n,
|
|
158
|
+
c=float(c),
|
|
159
|
+
k=float(k),
|
|
160
|
+
beta=float(beta),
|
|
161
|
+
quad_order=int(quad_order),
|
|
162
|
+
normal_sign=float(normal_sign),
|
|
163
|
+
tol=float(tol),
|
|
164
|
+
facet_map=facet_map,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def assemble(self, u, *, return_metrics: bool = False):
|
|
168
|
+
return assemble_contact_onesided_floor(
|
|
169
|
+
self.side.surface,
|
|
170
|
+
np.asarray(u, dtype=float),
|
|
171
|
+
n=None if self.n is None else np.asarray(self.n, dtype=float),
|
|
172
|
+
c=self.c,
|
|
173
|
+
k=self.k,
|
|
174
|
+
beta=self.beta,
|
|
175
|
+
value_dim=self.side.value_dim,
|
|
176
|
+
elem_conn=np.asarray(self.side.elem_conn) if self.side.elem_conn is not None else None,
|
|
177
|
+
facet_to_elem=self.facet_map,
|
|
178
|
+
quad_order=self.quad_order,
|
|
179
|
+
normal_sign=self.normal_sign,
|
|
180
|
+
tol=self.tol,
|
|
181
|
+
return_metrics=return_metrics,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass(eq=False)
|
|
186
|
+
class OneSidedContactSurfaceSpace:
|
|
187
|
+
"""Surface wrapper for one-sided (Dirichlet) contact assembly."""
|
|
188
|
+
|
|
189
|
+
surface_slave: SurfaceMesh
|
|
190
|
+
elem_conn_slave: np.ndarray
|
|
191
|
+
facet_to_elem_slave: np.ndarray
|
|
192
|
+
value_dim: int = 1
|
|
193
|
+
quad_order: int = 2
|
|
194
|
+
normal_sign: float = 1.0
|
|
195
|
+
tol: float = 1e-8
|
|
196
|
+
surface_master: SurfaceMesh | None = None
|
|
197
|
+
elem_conn_master: np.ndarray | None = None
|
|
198
|
+
facet_to_elem_master: np.ndarray | None = None
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def from_side(
|
|
202
|
+
cls,
|
|
203
|
+
side: ContactSide,
|
|
204
|
+
*,
|
|
205
|
+
surface_master: SurfaceMesh | None = None,
|
|
206
|
+
elem_conn_master: np.ndarray | None = None,
|
|
207
|
+
facet_to_elem_master: np.ndarray | None = None,
|
|
208
|
+
quad_order: int = 2,
|
|
209
|
+
normal_sign: float = 1.0,
|
|
210
|
+
tol: float = 1e-8,
|
|
211
|
+
) -> "OneSidedContactSurfaceSpace":
|
|
212
|
+
if side.elem_conn is None:
|
|
213
|
+
raise ValueError("side.elem_conn is required for one-sided assembly")
|
|
214
|
+
facet_map_slave = _facet_map_for_elem_conn(side.surface, side.elem_conn)
|
|
215
|
+
facet_map_master = facet_to_elem_master
|
|
216
|
+
if surface_master is not None and elem_conn_master is not None and facet_map_master is None:
|
|
217
|
+
facet_map_master = _facet_map_for_elem_conn(surface_master, elem_conn_master)
|
|
218
|
+
return cls(
|
|
219
|
+
surface_slave=side.surface,
|
|
220
|
+
elem_conn_slave=np.asarray(side.elem_conn, dtype=int),
|
|
221
|
+
facet_to_elem_slave=np.asarray(facet_map_slave, dtype=int),
|
|
222
|
+
value_dim=int(side.value_dim),
|
|
223
|
+
quad_order=int(quad_order),
|
|
224
|
+
normal_sign=float(normal_sign),
|
|
225
|
+
tol=float(tol),
|
|
226
|
+
surface_master=surface_master,
|
|
227
|
+
elem_conn_master=None if elem_conn_master is None else np.asarray(elem_conn_master, dtype=int),
|
|
228
|
+
facet_to_elem_master=None if facet_map_master is None else np.asarray(facet_map_master, dtype=int),
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
@classmethod
|
|
232
|
+
def from_facets(
|
|
233
|
+
cls,
|
|
234
|
+
mesh: BaseMesh,
|
|
235
|
+
facets: np.ndarray,
|
|
236
|
+
space=None,
|
|
237
|
+
*,
|
|
238
|
+
surface_master: SurfaceMesh | None = None,
|
|
239
|
+
elem_conn_master: np.ndarray | None = None,
|
|
240
|
+
facet_to_elem_master: np.ndarray | None = None,
|
|
241
|
+
value_dim: int | None = None,
|
|
242
|
+
quad_order: int = 2,
|
|
243
|
+
normal_sign: float = 1.0,
|
|
244
|
+
tol: float = 1e-8,
|
|
245
|
+
mode: str = "touching",
|
|
246
|
+
) -> "OneSidedContactSurfaceSpace":
|
|
247
|
+
side = ContactSide.from_facets(mesh, facets, space, value_dim=value_dim, mode=mode)
|
|
248
|
+
return cls.from_side(
|
|
249
|
+
side,
|
|
250
|
+
surface_master=surface_master,
|
|
251
|
+
elem_conn_master=elem_conn_master,
|
|
252
|
+
facet_to_elem_master=facet_to_elem_master,
|
|
253
|
+
quad_order=quad_order,
|
|
254
|
+
normal_sign=normal_sign,
|
|
255
|
+
tol=tol,
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
def assemble_bilinear(
|
|
259
|
+
self,
|
|
260
|
+
u_hat_fn,
|
|
261
|
+
params: "WeakParams",
|
|
262
|
+
*,
|
|
263
|
+
u_master: np.ndarray | None = None,
|
|
264
|
+
grad_source: str = "volume",
|
|
265
|
+
dof_source: str = "volume",
|
|
266
|
+
quad_order: int | None = None,
|
|
267
|
+
normal_sign: float | None = None,
|
|
268
|
+
tol: float | None = None,
|
|
269
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
270
|
+
return assemble_onesided_bilinear(
|
|
271
|
+
self.surface_slave,
|
|
272
|
+
u_hat_fn,
|
|
273
|
+
params,
|
|
274
|
+
surface_master=self.surface_master,
|
|
275
|
+
u_master=u_master,
|
|
276
|
+
value_dim=self.value_dim,
|
|
277
|
+
elem_conn=self.elem_conn_slave,
|
|
278
|
+
facet_to_elem=self.facet_to_elem_slave,
|
|
279
|
+
elem_conn_master=self.elem_conn_master,
|
|
280
|
+
facet_to_elem_master=self.facet_to_elem_master,
|
|
281
|
+
grad_source=grad_source,
|
|
282
|
+
dof_source=dof_source,
|
|
283
|
+
quad_order=self.quad_order if quad_order is None else int(quad_order),
|
|
284
|
+
normal_sign=self.normal_sign if normal_sign is None else float(normal_sign),
|
|
285
|
+
tol=self.tol if tol is None else float(tol),
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
@dataclass(eq=False)
|
|
290
|
+
class ContactSurfaceSpace:
|
|
291
|
+
"""Surface interface wrapper for contact assembly on a supermesh."""
|
|
292
|
+
|
|
293
|
+
surface_master: SurfaceMesh
|
|
294
|
+
surface_slave: SurfaceMesh
|
|
295
|
+
supermesh_coords: np.ndarray
|
|
296
|
+
supermesh_conn: np.ndarray
|
|
297
|
+
source_facets_master: np.ndarray
|
|
298
|
+
source_facets_slave: np.ndarray
|
|
299
|
+
elem_conn_master: np.ndarray | None
|
|
300
|
+
elem_conn_slave: np.ndarray | None
|
|
301
|
+
facet_to_elem_master: np.ndarray | None
|
|
302
|
+
facet_to_elem_slave: np.ndarray | None
|
|
303
|
+
field_master: str = "a"
|
|
304
|
+
field_slave: str = "b"
|
|
305
|
+
value_dim_master: int = 1
|
|
306
|
+
value_dim_slave: int = 1
|
|
307
|
+
quad_order: int = 1
|
|
308
|
+
normal_sign: float | None = None
|
|
309
|
+
tol: float = 1e-8
|
|
310
|
+
backend: str = "jax"
|
|
311
|
+
fd_eps: float = 1e-6
|
|
312
|
+
fd_mode: str = "central"
|
|
313
|
+
fd_block_size: int = 1
|
|
314
|
+
batch_jac: bool | None = None
|
|
315
|
+
|
|
316
|
+
@classmethod
|
|
317
|
+
def from_surfaces(
|
|
318
|
+
cls,
|
|
319
|
+
surface_master: SurfaceMesh,
|
|
320
|
+
surface_slave: SurfaceMesh,
|
|
321
|
+
*,
|
|
322
|
+
elem_conn_master: np.ndarray | None = None,
|
|
323
|
+
elem_conn_slave: np.ndarray | None = None,
|
|
324
|
+
field_master: str = "a",
|
|
325
|
+
field_slave: str = "b",
|
|
326
|
+
value_dim_master: int = 1,
|
|
327
|
+
value_dim_slave: int = 1,
|
|
328
|
+
quad_order: int = 1,
|
|
329
|
+
normal_sign: float | None = None,
|
|
330
|
+
tol: float = 1e-8,
|
|
331
|
+
backend: str = "jax",
|
|
332
|
+
fd_eps: float = 1e-6,
|
|
333
|
+
fd_mode: str = "central",
|
|
334
|
+
fd_block_size: int = 1,
|
|
335
|
+
batch_jac: bool | None = None,
|
|
336
|
+
setup_cache_enabled: bool | None = None,
|
|
337
|
+
setup_cache_trace: bool | None = None,
|
|
338
|
+
) -> "ContactSurfaceSpace":
|
|
339
|
+
import hashlib
|
|
340
|
+
import os
|
|
341
|
+
|
|
342
|
+
if setup_cache_enabled is None:
|
|
343
|
+
setup_cache_enabled = os.getenv("FLUXFEM_CONTACT_SETUP_CACHE", "0") not in ("0", "", "false", "False")
|
|
344
|
+
if setup_cache_trace is None:
|
|
345
|
+
setup_cache_trace = os.getenv("FLUXFEM_CONTACT_SETUP_CACHE_TRACE", "0") not in ("0", "", "false", "False")
|
|
346
|
+
|
|
347
|
+
def _array_sig(arr: np.ndarray) -> tuple:
|
|
348
|
+
arr_c = np.ascontiguousarray(arr)
|
|
349
|
+
h = hashlib.blake2b(arr_c.view(np.uint8), digest_size=8).hexdigest()
|
|
350
|
+
return (arr_c.shape, str(arr_c.dtype), h)
|
|
351
|
+
|
|
352
|
+
if setup_cache_enabled:
|
|
353
|
+
global _CONTACT_SETUP_CACHE
|
|
354
|
+
try:
|
|
355
|
+
_CONTACT_SETUP_CACHE
|
|
356
|
+
except NameError:
|
|
357
|
+
_CONTACT_SETUP_CACHE = {}
|
|
358
|
+
key = (
|
|
359
|
+
_array_sig(np.asarray(surface_master.coords)),
|
|
360
|
+
_array_sig(np.asarray(surface_master.conn)),
|
|
361
|
+
_array_sig(np.asarray(surface_slave.coords)),
|
|
362
|
+
_array_sig(np.asarray(surface_slave.conn)),
|
|
363
|
+
None if elem_conn_master is None else _array_sig(np.asarray(elem_conn_master)),
|
|
364
|
+
None if elem_conn_slave is None else _array_sig(np.asarray(elem_conn_slave)),
|
|
365
|
+
field_master,
|
|
366
|
+
field_slave,
|
|
367
|
+
int(value_dim_master),
|
|
368
|
+
int(value_dim_slave),
|
|
369
|
+
int(quad_order),
|
|
370
|
+
float(normal_sign) if normal_sign is not None else None,
|
|
371
|
+
float(tol),
|
|
372
|
+
backend,
|
|
373
|
+
float(fd_eps),
|
|
374
|
+
fd_mode,
|
|
375
|
+
int(fd_block_size),
|
|
376
|
+
bool(batch_jac) if batch_jac is not None else None,
|
|
377
|
+
)
|
|
378
|
+
cached = _CONTACT_SETUP_CACHE.get(key)
|
|
379
|
+
if cached is not None:
|
|
380
|
+
if setup_cache_trace:
|
|
381
|
+
print(
|
|
382
|
+
f"[contact] setup cache hit n_tris={int(cached.supermesh_conn.shape[0])}",
|
|
383
|
+
flush=True,
|
|
384
|
+
)
|
|
385
|
+
return cached
|
|
386
|
+
|
|
387
|
+
sm = build_surface_supermesh(surface_master, surface_slave, tol=tol)
|
|
388
|
+
facet_map_master = None
|
|
389
|
+
facet_map_slave = None
|
|
390
|
+
if elem_conn_master is not None:
|
|
391
|
+
if elem_conn_master.shape[1] in {4, 10}:
|
|
392
|
+
facet_map_master = map_surface_facets_to_tet_elements(surface_master, elem_conn_master)
|
|
393
|
+
elif elem_conn_master.shape[1] in {8, 20, 27}:
|
|
394
|
+
facet_map_master = map_surface_facets_to_hex_elements(surface_master, elem_conn_master)
|
|
395
|
+
else:
|
|
396
|
+
raise NotImplementedError("elem_conn_master must be tet4/tet10/hex8/hex20/hex27")
|
|
397
|
+
if elem_conn_slave is not None:
|
|
398
|
+
if elem_conn_slave.shape[1] in {4, 10}:
|
|
399
|
+
facet_map_slave = map_surface_facets_to_tet_elements(surface_slave, elem_conn_slave)
|
|
400
|
+
elif elem_conn_slave.shape[1] in {8, 20, 27}:
|
|
401
|
+
facet_map_slave = map_surface_facets_to_hex_elements(surface_slave, elem_conn_slave)
|
|
402
|
+
else:
|
|
403
|
+
raise NotImplementedError("elem_conn_slave must be tet4/tet10/hex8/hex20/hex27")
|
|
404
|
+
obj = cls(
|
|
405
|
+
surface_master=surface_master,
|
|
406
|
+
surface_slave=surface_slave,
|
|
407
|
+
supermesh_coords=sm.coords,
|
|
408
|
+
supermesh_conn=sm.conn,
|
|
409
|
+
source_facets_master=sm.source_facets_a,
|
|
410
|
+
source_facets_slave=sm.source_facets_b,
|
|
411
|
+
elem_conn_master=elem_conn_master,
|
|
412
|
+
elem_conn_slave=elem_conn_slave,
|
|
413
|
+
facet_to_elem_master=facet_map_master,
|
|
414
|
+
facet_to_elem_slave=facet_map_slave,
|
|
415
|
+
field_master=field_master,
|
|
416
|
+
field_slave=field_slave,
|
|
417
|
+
value_dim_master=value_dim_master,
|
|
418
|
+
value_dim_slave=value_dim_slave,
|
|
419
|
+
quad_order=quad_order,
|
|
420
|
+
normal_sign=normal_sign,
|
|
421
|
+
tol=tol,
|
|
422
|
+
backend=backend,
|
|
423
|
+
fd_eps=fd_eps,
|
|
424
|
+
fd_mode=fd_mode,
|
|
425
|
+
fd_block_size=fd_block_size,
|
|
426
|
+
batch_jac=batch_jac,
|
|
427
|
+
)
|
|
428
|
+
if setup_cache_enabled:
|
|
429
|
+
_CONTACT_SETUP_CACHE[key] = obj
|
|
430
|
+
if setup_cache_trace:
|
|
431
|
+
print(
|
|
432
|
+
f"[contact] setup cache store n_tris={int(obj.supermesh_conn.shape[0])}",
|
|
433
|
+
flush=True,
|
|
434
|
+
)
|
|
435
|
+
return obj
|
|
436
|
+
|
|
437
|
+
@classmethod
|
|
438
|
+
def from_facets(
|
|
439
|
+
cls,
|
|
440
|
+
coords: np.ndarray,
|
|
441
|
+
facets: np.ndarray,
|
|
442
|
+
*,
|
|
443
|
+
elem_conn: np.ndarray | None = None,
|
|
444
|
+
value_dim: int = 1,
|
|
445
|
+
quad_order: int = 1,
|
|
446
|
+
normal_sign: float | None = None,
|
|
447
|
+
tol: float = 1e-8,
|
|
448
|
+
backend: str = "jax",
|
|
449
|
+
fd_eps: float = 1e-6,
|
|
450
|
+
fd_mode: str = "central",
|
|
451
|
+
fd_block_size: int = 1,
|
|
452
|
+
batch_jac: bool | None = None,
|
|
453
|
+
setup_cache_enabled: bool | None = None,
|
|
454
|
+
setup_cache_trace: bool | None = None,
|
|
455
|
+
) -> "ContactSurfaceSpace":
|
|
456
|
+
surface = SurfaceMesh.from_facets(coords, facets)
|
|
457
|
+
return cls.from_surfaces(
|
|
458
|
+
surface,
|
|
459
|
+
surface,
|
|
460
|
+
elem_conn_master=elem_conn,
|
|
461
|
+
elem_conn_slave=elem_conn,
|
|
462
|
+
value_dim_master=value_dim,
|
|
463
|
+
value_dim_slave=value_dim,
|
|
464
|
+
quad_order=quad_order,
|
|
465
|
+
normal_sign=normal_sign,
|
|
466
|
+
tol=tol,
|
|
467
|
+
backend=backend,
|
|
468
|
+
fd_eps=fd_eps,
|
|
469
|
+
fd_mode=fd_mode,
|
|
470
|
+
fd_block_size=fd_block_size,
|
|
471
|
+
batch_jac=batch_jac,
|
|
472
|
+
setup_cache_enabled=setup_cache_enabled,
|
|
473
|
+
setup_cache_trace=setup_cache_trace,
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
@classmethod
|
|
477
|
+
def from_surfaces_and_spaces(
|
|
478
|
+
cls,
|
|
479
|
+
surface_master: SurfaceMesh,
|
|
480
|
+
surface_slave: SurfaceMesh,
|
|
481
|
+
space_master,
|
|
482
|
+
space_slave,
|
|
483
|
+
*,
|
|
484
|
+
elem_conn_master: np.ndarray | None = None,
|
|
485
|
+
elem_conn_slave: np.ndarray | None = None,
|
|
486
|
+
field_master: str = "a",
|
|
487
|
+
field_slave: str = "b",
|
|
488
|
+
value_dim_master: int | None = None,
|
|
489
|
+
value_dim_slave: int | None = None,
|
|
490
|
+
quad_order: int = 1,
|
|
491
|
+
normal_sign: float | None = None,
|
|
492
|
+
tol: float = 1e-8,
|
|
493
|
+
backend: str = "jax",
|
|
494
|
+
fd_eps: float = 1e-6,
|
|
495
|
+
fd_mode: str = "central",
|
|
496
|
+
fd_block_size: int = 1,
|
|
497
|
+
batch_jac: bool | None = None,
|
|
498
|
+
) -> "ContactSurfaceSpace":
|
|
499
|
+
if value_dim_master is None:
|
|
500
|
+
value_dim_master = int(getattr(space_master, "value_dim", 1))
|
|
501
|
+
if value_dim_slave is None:
|
|
502
|
+
value_dim_slave = int(getattr(space_slave, "value_dim", 1))
|
|
503
|
+
return cls.from_surfaces(
|
|
504
|
+
surface_master,
|
|
505
|
+
surface_slave,
|
|
506
|
+
elem_conn_master=elem_conn_master,
|
|
507
|
+
elem_conn_slave=elem_conn_slave,
|
|
508
|
+
field_master=field_master,
|
|
509
|
+
field_slave=field_slave,
|
|
510
|
+
value_dim_master=value_dim_master,
|
|
511
|
+
value_dim_slave=value_dim_slave,
|
|
512
|
+
quad_order=quad_order,
|
|
513
|
+
normal_sign=normal_sign,
|
|
514
|
+
tol=tol,
|
|
515
|
+
backend=backend,
|
|
516
|
+
fd_eps=fd_eps,
|
|
517
|
+
fd_mode=fd_mode,
|
|
518
|
+
fd_block_size=fd_block_size,
|
|
519
|
+
batch_jac=batch_jac,
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
@classmethod
|
|
523
|
+
def from_sides(
|
|
524
|
+
cls,
|
|
525
|
+
master: ContactSide,
|
|
526
|
+
slave: ContactSide,
|
|
527
|
+
*,
|
|
528
|
+
field_master: str = "a",
|
|
529
|
+
field_slave: str = "b",
|
|
530
|
+
quad_order: int = 1,
|
|
531
|
+
normal_sign: float | None = None,
|
|
532
|
+
tol: float = 1e-8,
|
|
533
|
+
backend: str = "jax",
|
|
534
|
+
fd_eps: float = 1e-6,
|
|
535
|
+
fd_mode: str = "central",
|
|
536
|
+
fd_block_size: int = 1,
|
|
537
|
+
batch_jac: bool | None = None,
|
|
538
|
+
setup_cache_enabled: bool | None = None,
|
|
539
|
+
setup_cache_trace: bool | None = None,
|
|
540
|
+
) -> "ContactSurfaceSpace":
|
|
541
|
+
return cls.from_surfaces(
|
|
542
|
+
master.surface,
|
|
543
|
+
slave.surface,
|
|
544
|
+
elem_conn_master=master.elem_conn,
|
|
545
|
+
elem_conn_slave=slave.elem_conn,
|
|
546
|
+
field_master=field_master,
|
|
547
|
+
field_slave=field_slave,
|
|
548
|
+
value_dim_master=master.value_dim,
|
|
549
|
+
value_dim_slave=slave.value_dim,
|
|
550
|
+
quad_order=quad_order,
|
|
551
|
+
normal_sign=normal_sign,
|
|
552
|
+
tol=tol,
|
|
553
|
+
backend=backend,
|
|
554
|
+
fd_eps=fd_eps,
|
|
555
|
+
fd_mode=fd_mode,
|
|
556
|
+
fd_block_size=fd_block_size,
|
|
557
|
+
batch_jac=batch_jac,
|
|
558
|
+
setup_cache_enabled=setup_cache_enabled,
|
|
559
|
+
setup_cache_trace=setup_cache_trace,
|
|
560
|
+
)
|
|
561
|
+
|
|
562
|
+
@classmethod
|
|
563
|
+
def from_facets(
|
|
564
|
+
cls,
|
|
565
|
+
coords_master: np.ndarray,
|
|
566
|
+
facets_master: np.ndarray,
|
|
567
|
+
coords_slave: np.ndarray,
|
|
568
|
+
facets_slave: np.ndarray,
|
|
569
|
+
*,
|
|
570
|
+
elem_conn_master: np.ndarray | None = None,
|
|
571
|
+
elem_conn_slave: np.ndarray | None = None,
|
|
572
|
+
field_master: str = "a",
|
|
573
|
+
field_slave: str = "b",
|
|
574
|
+
value_dim_master: int = 1,
|
|
575
|
+
value_dim_slave: int = 1,
|
|
576
|
+
quad_order: int = 1,
|
|
577
|
+
normal_sign: float | None = None,
|
|
578
|
+
tol: float = 1e-8,
|
|
579
|
+
backend: str = "jax",
|
|
580
|
+
fd_eps: float = 1e-6,
|
|
581
|
+
fd_mode: str = "central",
|
|
582
|
+
batch_jac: bool | None = None,
|
|
583
|
+
setup_cache_enabled: bool | None = None,
|
|
584
|
+
setup_cache_trace: bool | None = None,
|
|
585
|
+
) -> "ContactSurfaceSpace":
|
|
586
|
+
surface_master = SurfaceMesh.from_facets(coords_master, facets_master)
|
|
587
|
+
surface_slave = SurfaceMesh.from_facets(coords_slave, facets_slave)
|
|
588
|
+
return cls.from_surfaces(
|
|
589
|
+
surface_master,
|
|
590
|
+
surface_slave,
|
|
591
|
+
elem_conn_master=elem_conn_master,
|
|
592
|
+
elem_conn_slave=elem_conn_slave,
|
|
593
|
+
field_master=field_master,
|
|
594
|
+
field_slave=field_slave,
|
|
595
|
+
value_dim_master=value_dim_master,
|
|
596
|
+
value_dim_slave=value_dim_slave,
|
|
597
|
+
quad_order=quad_order,
|
|
598
|
+
normal_sign=normal_sign,
|
|
599
|
+
tol=tol,
|
|
600
|
+
backend=backend,
|
|
601
|
+
fd_eps=fd_eps,
|
|
602
|
+
fd_mode=fd_mode,
|
|
603
|
+
batch_jac=batch_jac,
|
|
604
|
+
setup_cache_enabled=setup_cache_enabled,
|
|
605
|
+
setup_cache_trace=setup_cache_trace,
|
|
606
|
+
)
|
|
607
|
+
|
|
608
|
+
def _split_fields(self, u: Mapping[str, np.ndarray] | Sequence[np.ndarray]):
|
|
609
|
+
if isinstance(u, Mapping):
|
|
610
|
+
return u[self.field_master], u[self.field_slave]
|
|
611
|
+
if len(u) != 2:
|
|
612
|
+
raise ValueError("u must be a mapping or a length-2 sequence")
|
|
613
|
+
return u[0], u[1]
|
|
614
|
+
|
|
615
|
+
def _auto_normal_sign(self) -> float:
|
|
616
|
+
if not hasattr(self.surface_master, "facet_normals"):
|
|
617
|
+
return 1.0
|
|
618
|
+
normals = self.surface_master.facet_normals()
|
|
619
|
+
coords = np.asarray(self.surface_master.coords)
|
|
620
|
+
coords_slave = np.asarray(self.surface_slave.coords)
|
|
621
|
+
facets_m = np.asarray(self.surface_master.conn, dtype=int)
|
|
622
|
+
facets_s = np.asarray(self.surface_slave.conn, dtype=int)
|
|
623
|
+
dots = []
|
|
624
|
+
for fa, fb in zip(self.source_facets_master, self.source_facets_slave):
|
|
625
|
+
n = normals[int(fa)]
|
|
626
|
+
cm = np.mean(coords[facets_m[int(fa)]], axis=0)
|
|
627
|
+
cs = np.mean(coords_slave[facets_s[int(fb)]], axis=0)
|
|
628
|
+
dots.append(float(np.dot(n, cs - cm)))
|
|
629
|
+
if not dots:
|
|
630
|
+
return 1.0
|
|
631
|
+
return 1.0 if np.sum(dots) >= 0.0 else -1.0
|
|
632
|
+
|
|
633
|
+
def _resolve_backend(self, backend: str | None) -> str:
|
|
634
|
+
use_backend = self.backend if backend is None else backend
|
|
635
|
+
if use_backend not in {"jax", "numpy"}:
|
|
636
|
+
raise ValueError("backend must be 'jax' or 'numpy'")
|
|
637
|
+
return use_backend
|
|
638
|
+
|
|
639
|
+
def assemble_mortar_matrices(self):
|
|
640
|
+
return assemble_mortar_matrices(
|
|
641
|
+
self.supermesh_coords,
|
|
642
|
+
self.supermesh_conn,
|
|
643
|
+
self.source_facets_master,
|
|
644
|
+
self.source_facets_slave,
|
|
645
|
+
self.surface_master,
|
|
646
|
+
self.surface_slave,
|
|
647
|
+
)
|
|
648
|
+
|
|
649
|
+
def assemble_residual(
|
|
650
|
+
self,
|
|
651
|
+
res_form,
|
|
652
|
+
u,
|
|
653
|
+
params,
|
|
654
|
+
*,
|
|
655
|
+
normal_sign: float | None = None,
|
|
656
|
+
normal_source: str = "master",
|
|
657
|
+
):
|
|
658
|
+
u_master, u_slave = self._split_fields(u)
|
|
659
|
+
if normal_sign is None:
|
|
660
|
+
normal_sign = self.normal_sign
|
|
661
|
+
if normal_sign is None:
|
|
662
|
+
normal_sign = self._auto_normal_sign()
|
|
663
|
+
return assemble_mixed_surface_residual(
|
|
664
|
+
self.supermesh_coords,
|
|
665
|
+
self.supermesh_conn,
|
|
666
|
+
self.source_facets_master,
|
|
667
|
+
self.source_facets_slave,
|
|
668
|
+
self.surface_master,
|
|
669
|
+
self.surface_slave,
|
|
670
|
+
res_form,
|
|
671
|
+
u_master,
|
|
672
|
+
u_slave,
|
|
673
|
+
params,
|
|
674
|
+
value_dim_a=self.value_dim_master,
|
|
675
|
+
value_dim_b=self.value_dim_slave,
|
|
676
|
+
field_a=self.field_master,
|
|
677
|
+
field_b=self.field_slave,
|
|
678
|
+
elem_conn_a=self.elem_conn_master,
|
|
679
|
+
elem_conn_b=self.elem_conn_slave,
|
|
680
|
+
facet_to_elem_a=self.facet_to_elem_master,
|
|
681
|
+
facet_to_elem_b=self.facet_to_elem_slave,
|
|
682
|
+
normal_source=normal_source,
|
|
683
|
+
normal_from="master",
|
|
684
|
+
master_field=self.field_master,
|
|
685
|
+
normal_sign=normal_sign,
|
|
686
|
+
grad_source="volume",
|
|
687
|
+
dof_source="volume",
|
|
688
|
+
quad_order=self.quad_order,
|
|
689
|
+
tol=self.tol,
|
|
690
|
+
)
|
|
691
|
+
|
|
692
|
+
def assemble_jacobian(
|
|
693
|
+
self,
|
|
694
|
+
res_form,
|
|
695
|
+
u,
|
|
696
|
+
params,
|
|
697
|
+
*,
|
|
698
|
+
normal_sign: float | None = None,
|
|
699
|
+
normal_source: str = "master",
|
|
700
|
+
sparse: bool = False,
|
|
701
|
+
backend: str | None = None,
|
|
702
|
+
batch_jac: bool | None = None,
|
|
703
|
+
):
|
|
704
|
+
u_master, u_slave = self._split_fields(u)
|
|
705
|
+
if normal_sign is None:
|
|
706
|
+
normal_sign = self.normal_sign
|
|
707
|
+
if normal_sign is None:
|
|
708
|
+
normal_sign = self._auto_normal_sign()
|
|
709
|
+
use_backend = self._resolve_backend(backend)
|
|
710
|
+
use_batch_jac = self.batch_jac if batch_jac is None else batch_jac
|
|
711
|
+
return assemble_mixed_surface_jacobian(
|
|
712
|
+
self.supermesh_coords,
|
|
713
|
+
self.supermesh_conn,
|
|
714
|
+
self.source_facets_master,
|
|
715
|
+
self.source_facets_slave,
|
|
716
|
+
self.surface_master,
|
|
717
|
+
self.surface_slave,
|
|
718
|
+
res_form,
|
|
719
|
+
u_master,
|
|
720
|
+
u_slave,
|
|
721
|
+
params,
|
|
722
|
+
value_dim_a=self.value_dim_master,
|
|
723
|
+
value_dim_b=self.value_dim_slave,
|
|
724
|
+
field_a=self.field_master,
|
|
725
|
+
field_b=self.field_slave,
|
|
726
|
+
elem_conn_a=self.elem_conn_master,
|
|
727
|
+
elem_conn_b=self.elem_conn_slave,
|
|
728
|
+
facet_to_elem_a=self.facet_to_elem_master,
|
|
729
|
+
facet_to_elem_b=self.facet_to_elem_slave,
|
|
730
|
+
normal_source=normal_source,
|
|
731
|
+
normal_from="master",
|
|
732
|
+
master_field=self.field_master,
|
|
733
|
+
normal_sign=normal_sign,
|
|
734
|
+
grad_source="volume",
|
|
735
|
+
dof_source="volume",
|
|
736
|
+
quad_order=self.quad_order,
|
|
737
|
+
tol=self.tol,
|
|
738
|
+
sparse=sparse,
|
|
739
|
+
backend=use_backend,
|
|
740
|
+
batch_jac=use_batch_jac,
|
|
741
|
+
fd_eps=self.fd_eps,
|
|
742
|
+
fd_mode=self.fd_mode,
|
|
743
|
+
fd_block_size=self.fd_block_size,
|
|
744
|
+
)
|
|
745
|
+
|
|
746
|
+
def assemble_bilinear(
|
|
747
|
+
self,
|
|
748
|
+
bilin,
|
|
749
|
+
u_master,
|
|
750
|
+
u_slave=None,
|
|
751
|
+
params=None,
|
|
752
|
+
*,
|
|
753
|
+
sparse: bool = False,
|
|
754
|
+
normal_source: str = "master",
|
|
755
|
+
):
|
|
756
|
+
"""
|
|
757
|
+
Assemble a mixed surface bilinear form with signature (v1, v2, u1, u2, params).
|
|
758
|
+
|
|
759
|
+
Notes:
|
|
760
|
+
- v1/v2/u1/u2 are symbolic field refs; use .val/.grad/.sym_grad in the expression.
|
|
761
|
+
- The bilinear must be linear in v1 and v2 and include ds() in its expression.
|
|
762
|
+
- When building dot products, prefer dot(v1, ...) and dot(v2, ...) to keep shapes consistent.
|
|
763
|
+
- Normal orientation, grad_source, and dof_source are fixed internally for simplicity.
|
|
764
|
+
- u_master/u_slave can be passed as a single mapping/length-2 sequence; in that case,
|
|
765
|
+
pass params as the next positional arg or a keyword.
|
|
766
|
+
"""
|
|
767
|
+
from ..core.weakform import (
|
|
768
|
+
compile_mixed_surface_residual,
|
|
769
|
+
compile_mixed_surface_residual_numpy,
|
|
770
|
+
unknown_ref,
|
|
771
|
+
test_ref,
|
|
772
|
+
param_ref,
|
|
773
|
+
zero_ref,
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
def _is_field_pair(obj) -> bool:
|
|
777
|
+
if isinstance(obj, Mapping):
|
|
778
|
+
return True
|
|
779
|
+
return isinstance(obj, Sequence) and not hasattr(obj, "shape")
|
|
780
|
+
|
|
781
|
+
if params is None:
|
|
782
|
+
if u_slave is None:
|
|
783
|
+
raise TypeError("params is required")
|
|
784
|
+
if _is_field_pair(u_master):
|
|
785
|
+
params = u_slave
|
|
786
|
+
u_master, u_slave = self._split_fields(u_master)
|
|
787
|
+
else:
|
|
788
|
+
raise TypeError("params is required")
|
|
789
|
+
elif u_slave is None:
|
|
790
|
+
u_master, u_slave = self._split_fields(u_master)
|
|
791
|
+
|
|
792
|
+
v1 = test_ref(self.field_master)
|
|
793
|
+
v2 = test_ref(self.field_slave)
|
|
794
|
+
u1 = unknown_ref(self.field_master)
|
|
795
|
+
u2 = unknown_ref(self.field_slave)
|
|
796
|
+
z1 = zero_ref(self.field_master)
|
|
797
|
+
z2 = zero_ref(self.field_slave)
|
|
798
|
+
p = param_ref()
|
|
799
|
+
|
|
800
|
+
expr_a = bilin(v1, z2, u1, u2, p)
|
|
801
|
+
expr_b = bilin(z1, v2, u1, u2, p)
|
|
802
|
+
use_backend = self._resolve_backend(None)
|
|
803
|
+
if use_backend == "numpy":
|
|
804
|
+
res_form = compile_mixed_surface_residual_numpy({self.field_master: expr_a, self.field_slave: expr_b})
|
|
805
|
+
else:
|
|
806
|
+
res_form = compile_mixed_surface_residual({self.field_master: expr_a, self.field_slave: expr_b})
|
|
807
|
+
return self.assemble_jacobian(
|
|
808
|
+
res_form,
|
|
809
|
+
{self.field_master: u_master, self.field_slave: u_slave},
|
|
810
|
+
params,
|
|
811
|
+
normal_sign=None,
|
|
812
|
+
normal_source=normal_source,
|
|
813
|
+
sparse=sparse,
|
|
814
|
+
backend=use_backend,
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
|
|
818
|
+
__all__ = [
|
|
819
|
+
"ContactSide",
|
|
820
|
+
"OneSidedContact",
|
|
821
|
+
"OneSidedContactSurfaceSpace",
|
|
822
|
+
"ContactSurfaceSpace",
|
|
823
|
+
"facet_gap_values",
|
|
824
|
+
"active_contact_facets",
|
|
825
|
+
]
|