fluxfem 0.2.0__py3-none-any.whl → 0.2.1__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.
Files changed (41) hide show
  1. fluxfem/__init__.py +1 -13
  2. fluxfem/core/__init__.py +53 -71
  3. fluxfem/core/assembly.py +41 -32
  4. fluxfem/core/basis.py +2 -2
  5. fluxfem/core/context_types.py +36 -12
  6. fluxfem/core/mixed_space.py +42 -8
  7. fluxfem/core/mixed_weakform.py +1 -1
  8. fluxfem/core/space.py +68 -28
  9. fluxfem/core/weakform.py +95 -77
  10. fluxfem/mesh/base.py +3 -3
  11. fluxfem/mesh/contact.py +33 -17
  12. fluxfem/mesh/io.py +3 -2
  13. fluxfem/mesh/mortar.py +106 -43
  14. fluxfem/mesh/supermesh.py +2 -0
  15. fluxfem/mesh/surface.py +82 -22
  16. fluxfem/mesh/tet.py +7 -4
  17. fluxfem/physics/elasticity/hyperelastic.py +32 -3
  18. fluxfem/physics/elasticity/linear.py +13 -2
  19. fluxfem/physics/elasticity/stress.py +9 -5
  20. fluxfem/physics/operators.py +12 -5
  21. fluxfem/physics/postprocess.py +29 -3
  22. fluxfem/solver/__init__.py +6 -1
  23. fluxfem/solver/block_matrix.py +165 -13
  24. fluxfem/solver/block_system.py +52 -29
  25. fluxfem/solver/cg.py +43 -30
  26. fluxfem/solver/dirichlet.py +35 -12
  27. fluxfem/solver/history.py +15 -3
  28. fluxfem/solver/newton.py +25 -12
  29. fluxfem/solver/petsc.py +13 -7
  30. fluxfem/solver/preconditioner.py +7 -4
  31. fluxfem/solver/solve_runner.py +42 -24
  32. fluxfem/solver/solver.py +23 -11
  33. fluxfem/solver/sparse.py +32 -13
  34. fluxfem/tools/jit.py +19 -7
  35. fluxfem/tools/timer.py +14 -12
  36. fluxfem/tools/visualizer.py +16 -4
  37. {fluxfem-0.2.0.dist-info → fluxfem-0.2.1.dist-info}/METADATA +18 -7
  38. fluxfem-0.2.1.dist-info/RECORD +59 -0
  39. fluxfem-0.2.0.dist-info/RECORD +0 -59
  40. {fluxfem-0.2.0.dist-info → fluxfem-0.2.1.dist-info}/LICENSE +0 -0
  41. {fluxfem-0.2.0.dist-info → fluxfem-0.2.1.dist-info}/WHEEL +0 -0
fluxfem/mesh/mortar.py CHANGED
@@ -3,13 +3,14 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  import os
5
5
  import time
6
- from typing import Iterable, TYPE_CHECKING
6
+ from typing import Any, Callable, Iterable, Sequence, TYPE_CHECKING, cast
7
7
 
8
8
  import jax
9
9
  import jax.numpy as jnp
10
10
  import numpy as np
11
11
 
12
12
  from .surface import SurfaceMesh
13
+ from ..core.forms import FormFieldLike
13
14
  if TYPE_CHECKING:
14
15
  from ..core.forms import FieldPair
15
16
  from ..core.weakform import Params as WeakParams
@@ -54,7 +55,7 @@ _DEBUG_CONTACT_PROJ_ONCE = False
54
55
  _DEBUG_PROJ_QP_CACHE = None
55
56
  _DEBUG_PROJ_QP_SOURCE = None
56
57
  _DEBUG_PROJ_QP_DUMPED = False
57
- _PROJ_DIAG_STATS = None
58
+ _PROJ_DIAG_STATS: dict[str, Any] | None = None
58
59
  _PROJ_DIAG_COUNT = 0
59
60
  _PROJ_DIAG_CONTEXT: dict[str, int | str] = {}
60
61
 
@@ -139,6 +140,8 @@ def _quad_quadrature(order: int) -> tuple[np.ndarray, np.ndarray]:
139
140
  order = 2
140
141
  n = int(np.ceil((order + 1.0) / 2.0))
141
142
  x1d, w1d = np.polynomial.legendre.leggauss(n)
143
+ X: np.ndarray
144
+ Y: np.ndarray
142
145
  X, Y = np.meshgrid(x1d, x1d, indexing="xy")
143
146
  W = np.outer(w1d, w1d)
144
147
  pts = np.stack([X.ravel(), Y.ravel()], axis=1)
@@ -360,7 +363,7 @@ def _proj_diag_log(
360
363
  if _PROJ_DIAG_STATS is None:
361
364
  return
362
365
  _PROJ_DIAG_STATS["fail"] += 1
363
- by_code = _PROJ_DIAG_STATS["by_code"]
366
+ by_code = cast(dict[str, int], _PROJ_DIAG_STATS["by_code"])
364
367
  by_code[code] = by_code.get(code, 0) + 1
365
368
  if _PROJ_DIAG_COUNT >= _proj_diag_max():
366
369
  return
@@ -534,7 +537,7 @@ def _barycentric(p: np.ndarray, a: np.ndarray, b: np.ndarray, c: np.ndarray):
534
537
 
535
538
 
536
539
  def _point_in_tri(lam: np.ndarray, *, tol: float) -> bool:
537
- return np.all(lam >= -tol) and np.all(lam <= 1.0 + tol)
540
+ return bool(np.all(lam >= -tol) and np.all(lam <= 1.0 + tol))
538
541
 
539
542
 
540
543
  def _plane_basis(pts: np.ndarray, *, tol: float):
@@ -978,13 +981,13 @@ def map_surface_facets_to_tet_elements(surface: SurfaceMesh, tet_conn: np.ndarra
978
981
  """
979
982
  Map surface triangle facets to parent tet elements by node matching (tet4/tet10).
980
983
  """
981
- face_patterns_corner = [
984
+ face_patterns_corner: list[tuple[int, ...]] = [
982
985
  (0, 1, 2),
983
986
  (0, 1, 3),
984
987
  (0, 2, 3),
985
988
  (1, 2, 3),
986
989
  ]
987
- face_patterns_quad = [
990
+ face_patterns_quad: list[tuple[int, ...]] = [
988
991
  (0, 1, 2, 4, 5, 6),
989
992
  (0, 1, 3, 4, 8, 7),
990
993
  (0, 2, 3, 6, 9, 7),
@@ -997,7 +1000,7 @@ def map_surface_facets_to_tet_elements(surface: SurfaceMesh, tet_conn: np.ndarra
997
1000
  mapping_quad: dict[tuple[int, ...], int] = {}
998
1001
  for e_id, elem in enumerate(tet_conn):
999
1002
  for pattern in face_patterns_corner:
1000
- face_nodes = tuple(sorted(int(elem[i]) for i in pattern))
1003
+ face_nodes: tuple[int, ...] = tuple(sorted(int(elem[i]) for i in pattern))
1001
1004
  mapping_corner.setdefault(face_nodes, e_id)
1002
1005
  if elem.shape[0] == 10:
1003
1006
  for pattern in face_patterns_quad:
@@ -1022,7 +1025,7 @@ def map_surface_facets_to_hex_elements(surface: SurfaceMesh, hex_conn: np.ndarra
1022
1025
  hex_conn = np.asarray(hex_conn, dtype=int)
1023
1026
  if hex_conn.shape[1] not in {8, 20, 27}:
1024
1027
  raise NotImplementedError("Only hex8/hex20/hex27 are supported.")
1025
- face_patterns_corner = [
1028
+ face_patterns_corner: list[tuple[int, ...]] = [
1026
1029
  (0, 1, 2, 3),
1027
1030
  (4, 5, 6, 7),
1028
1031
  (0, 1, 5, 4),
@@ -1030,7 +1033,7 @@ def map_surface_facets_to_hex_elements(surface: SurfaceMesh, hex_conn: np.ndarra
1030
1033
  (2, 3, 7, 6),
1031
1034
  (3, 0, 4, 7),
1032
1035
  ]
1033
- face_patterns_corner27 = [
1036
+ face_patterns_corner27: list[tuple[int, ...]] = [
1034
1037
  (0, 2, 8, 6),
1035
1038
  (18, 20, 26, 24),
1036
1039
  (0, 2, 20, 18),
@@ -1038,7 +1041,7 @@ def map_surface_facets_to_hex_elements(surface: SurfaceMesh, hex_conn: np.ndarra
1038
1041
  (0, 6, 24, 18),
1039
1042
  (2, 8, 26, 20),
1040
1043
  ]
1041
- face_patterns_quad = [
1044
+ face_patterns_quad: list[tuple[int, ...]] = [
1042
1045
  (0, 1, 2, 3, 8, 9, 10, 11),
1043
1046
  (4, 5, 6, 7, 12, 13, 14, 15),
1044
1047
  (0, 1, 5, 4, 8, 17, 12, 16),
@@ -1046,7 +1049,7 @@ def map_surface_facets_to_hex_elements(surface: SurfaceMesh, hex_conn: np.ndarra
1046
1049
  (2, 3, 7, 6, 10, 19, 14, 18),
1047
1050
  (3, 0, 4, 7, 11, 16, 15, 19),
1048
1051
  ]
1049
- face_patterns_quad9 = [
1052
+ face_patterns_quad9: list[tuple[int, ...]] = [
1050
1053
  (0, 1, 2, 3, 4, 5, 6, 7, 8),
1051
1054
  (18, 19, 20, 21, 22, 23, 24, 25, 26),
1052
1055
  (0, 1, 2, 9, 10, 11, 18, 19, 20),
@@ -1062,7 +1065,7 @@ def map_surface_facets_to_hex_elements(surface: SurfaceMesh, hex_conn: np.ndarra
1062
1065
  else:
1063
1066
  corner_patterns = face_patterns_corner
1064
1067
  for pattern in corner_patterns:
1065
- face_nodes = tuple(sorted(int(elem[i]) for i in pattern))
1068
+ face_nodes: tuple[int, ...] = tuple(sorted(int(elem[i]) for i in pattern))
1066
1069
  mapping_corner.setdefault(face_nodes, e_id)
1067
1070
  if elem.shape[0] == 20:
1068
1071
  for pattern in face_patterns_quad:
@@ -2186,7 +2189,13 @@ def assemble_mixed_surface_residual(
2186
2189
  if offset_b is None:
2187
2190
  offset_b = offset_a + n_a
2188
2191
  n_total = int(offset_b + n_b)
2189
- R = np.zeros((n_total,), dtype=float)
2192
+ R: np.ndarray = np.zeros((n_total,), dtype=float)
2193
+
2194
+ trace = os.getenv("FLUXFEM_MORTAR_TRACE", "0") not in ("0", "", "false", "False")
2195
+
2196
+ def _trace_time(msg: str, t0: float) -> None:
2197
+ if trace:
2198
+ print(f"{msg} dt={time.perf_counter() - t0:.3e}s", flush=True)
2190
2199
 
2191
2200
  t_norm = time.perf_counter()
2192
2201
  normals_a = None
@@ -2213,6 +2222,12 @@ def assemble_mixed_surface_residual(
2213
2222
 
2214
2223
  use_elem_a = elem_conn_a is not None and facet_to_elem_a is not None
2215
2224
  use_elem_b = elem_conn_b is not None and facet_to_elem_b is not None
2225
+ if use_elem_a:
2226
+ assert elem_conn_a is not None
2227
+ assert facet_to_elem_a is not None
2228
+ if use_elem_b:
2229
+ assert elem_conn_b is not None
2230
+ assert facet_to_elem_b is not None
2216
2231
 
2217
2232
  if grad_source not in {"volume", "surface"}:
2218
2233
  raise ValueError("grad_source must be 'volume' or 'surface'")
@@ -2295,8 +2310,8 @@ def assemble_mixed_surface_residual(
2295
2310
  basis=_SurfaceBasis(dofs_per_node=value_dim_b),
2296
2311
  )
2297
2312
  fields = {
2298
- field_a: FieldPair(test=field_a_obj, trial=field_a_obj),
2299
- field_b: FieldPair(test=field_b_obj, trial=field_b_obj),
2313
+ field_a: FieldPair(test=cast("FormFieldLike", field_a_obj), trial=cast("FormFieldLike", field_a_obj)),
2314
+ field_b: FieldPair(test=cast("FormFieldLike", field_b_obj), trial=cast("FormFieldLike", field_b_obj)),
2300
2315
  }
2301
2316
  ctx = SurfaceMixedFormContext(
2302
2317
  fields=fields,
@@ -2372,6 +2387,8 @@ def assemble_mixed_surface_residual(
2372
2387
  elem_nodes_a = None
2373
2388
  elem_coords_a = None
2374
2389
  if use_elem_a:
2390
+ assert elem_conn_a is not None
2391
+ assert facet_to_elem_a is not None
2375
2392
  elem_id_a = int(facet_to_elem_a[int(fa)])
2376
2393
  if elem_id_a < 0:
2377
2394
  raise ValueError("facet_to_elem_a has invalid mapping")
@@ -2384,6 +2401,8 @@ def assemble_mixed_surface_residual(
2384
2401
  elem_nodes_b = None
2385
2402
  elem_coords_b = None
2386
2403
  if use_elem_b:
2404
+ assert elem_conn_b is not None
2405
+ assert facet_to_elem_b is not None
2387
2406
  elem_id_b = int(facet_to_elem_b[int(fb)])
2388
2407
  if elem_id_b < 0:
2389
2408
  raise ValueError("facet_to_elem_b has invalid mapping")
@@ -2411,10 +2430,14 @@ def assemble_mixed_surface_residual(
2411
2430
  dtype=float,
2412
2431
  )
2413
2432
  if use_elem_a and grad_source == "volume":
2433
+ assert elem_nodes_a is not None
2434
+ assert elem_coords_a is not None
2414
2435
  local = _local_indices(elem_nodes_a, facet_a)
2415
2436
  gradNa = _tet_gradN_at_points(x_q, elem_coords_a, local=local, tol=tol)
2416
2437
 
2417
2438
  if use_elem_b and grad_source == "volume":
2439
+ assert elem_nodes_b is not None
2440
+ assert elem_coords_b is not None
2418
2441
  local = _local_indices(elem_nodes_b, facet_b)
2419
2442
  gradNb = _tet_gradN_at_points(x_q, elem_coords_b, local=local, tol=tol)
2420
2443
 
@@ -2492,8 +2515,8 @@ def assemble_mixed_surface_residual(
2492
2515
  basis=_SurfaceBasis(dofs_per_node=value_dim_b),
2493
2516
  )
2494
2517
  fields = {
2495
- field_a: FieldPair(test=field_a_obj, trial=field_a_obj),
2496
- field_b: FieldPair(test=field_b_obj, trial=field_b_obj),
2518
+ field_a: FieldPair(test=cast("FormFieldLike", field_a_obj), trial=cast("FormFieldLike", field_a_obj)),
2519
+ field_b: FieldPair(test=cast("FormFieldLike", field_b_obj), trial=cast("FormFieldLike", field_b_obj)),
2497
2520
  }
2498
2521
  normal_q = None if normal is None else np.repeat(normal[None, :], quad_pts.shape[0], axis=0)
2499
2522
  ctx = SurfaceMixedFormContext(
@@ -2575,6 +2598,8 @@ def assemble_mixed_surface_jacobian(
2575
2598
  to pick which field acts as the master when normal_source is "master"/"slave".
2576
2599
  dof_source="volume" assembles into element nodes (requires elem_conn_* mappings).
2577
2600
  """
2601
+ source_facets_a = list(source_facets_a)
2602
+ source_facets_b = list(source_facets_b)
2578
2603
  from ..core.forms import FieldPair
2579
2604
  _mortar_dbg(
2580
2605
  f"[mortar] enter assemble_mixed_surface_jacobian quad_order={quad_order} backend={backend}"
@@ -2648,7 +2673,7 @@ def assemble_mixed_surface_jacobian(
2648
2673
  rows: list[int] = []
2649
2674
  cols: list[int] = []
2650
2675
  data: list[float] = []
2651
- K_dense = np.zeros((n_total, n_total), dtype=float) if not sparse else None
2676
+ K_dense: np.ndarray | None = np.zeros((n_total, n_total), dtype=float) if not sparse else None
2652
2677
 
2653
2678
  use_elem_a = elem_conn_a is not None and facet_to_elem_a is not None
2654
2679
  use_elem_b = elem_conn_b is not None and facet_to_elem_b is not None
@@ -2746,8 +2771,8 @@ def assemble_mixed_surface_jacobian(
2746
2771
  basis=_SurfaceBasis(dofs_per_node=value_dim_b),
2747
2772
  )
2748
2773
  fields = {
2749
- field_a: FieldPair(test=field_a_obj, trial=field_a_obj),
2750
- field_b: FieldPair(test=field_b_obj, trial=field_b_obj),
2774
+ field_a: FieldPair(test=cast("FormFieldLike", field_a_obj), trial=cast("FormFieldLike", field_a_obj)),
2775
+ field_b: FieldPair(test=cast("FormFieldLike", field_b_obj), trial=cast("FormFieldLike", field_b_obj)),
2751
2776
  }
2752
2777
  ctx = SurfaceMixedFormContext(
2753
2778
  fields=fields,
@@ -2831,6 +2856,7 @@ def assemble_mixed_surface_jacobian(
2831
2856
  cols.append(int(gj))
2832
2857
  data.append(val)
2833
2858
  else:
2859
+ assert K_dense is not None
2834
2860
  K_dense[int(gi), int(gj)] += val
2835
2861
  if sparse:
2836
2862
  return np.asarray(rows, dtype=int), np.asarray(cols, dtype=int), np.asarray(data, dtype=float), n_total
@@ -2881,8 +2907,8 @@ def assemble_mixed_surface_jacobian(
2881
2907
  basis=_SurfaceBasis(dofs_per_node=value_dim_b),
2882
2908
  )
2883
2909
  fields = {
2884
- field_a: FieldPair(test=field_a_obj, trial=field_a_obj),
2885
- field_b: FieldPair(test=field_b_obj, trial=field_b_obj),
2910
+ field_a: FieldPair(test=cast("FormFieldLike", field_a_obj), trial=cast("FormFieldLike", field_a_obj)),
2911
+ field_b: FieldPair(test=cast("FormFieldLike", field_b_obj), trial=cast("FormFieldLike", field_b_obj)),
2886
2912
  }
2887
2913
  normal_q = jnp.repeat(normal[None, :], x_q.shape[0], axis=0)
2888
2914
  ctx = SurfaceMixedFormContext(
@@ -2916,7 +2942,7 @@ def assemble_mixed_surface_jacobian(
2916
2942
  jac_fun = jax.vmap(jax.jacrev(_res_local_batch))
2917
2943
  return jax.jit(jac_fun) if jit_batch else jac_fun
2918
2944
 
2919
- jac_fun_cache: dict[tuple[int, int], object] = {}
2945
+ jac_fun_cache: dict[tuple[int, int], Callable[..., jnp.ndarray]] = {}
2920
2946
 
2921
2947
  def _emit_batch(
2922
2948
  Na_b,
@@ -2999,9 +3025,13 @@ def assemble_mixed_surface_jacobian(
2999
3025
  facet_b = facets_b[int(fb)]
3000
3026
  x_q = np.array([a + r * (b - a) + s * (c - a) for r, s in quad_pts], dtype=float)
3001
3027
 
3028
+ assert facet_to_elem_a is not None
3029
+ assert elem_conn_a is not None
3002
3030
  elem_id_a = int(facet_to_elem_a[int(fa)])
3003
3031
  elem_nodes_a = np.asarray(elem_conn_a[elem_id_a], dtype=int)
3004
3032
  elem_coords_a = coords_a[elem_nodes_a]
3033
+ assert facet_to_elem_b is not None
3034
+ assert elem_conn_b is not None
3005
3035
  elem_id_b = int(facet_to_elem_b[int(fb)])
3006
3036
  elem_nodes_b = np.asarray(elem_conn_b[elem_id_b], dtype=int)
3007
3037
  elem_coords_b = coords_b[elem_nodes_b]
@@ -3072,6 +3102,8 @@ def assemble_mixed_surface_jacobian(
3072
3102
  normal_b = jnp.asarray(np.stack(normal_b, axis=0))
3073
3103
  u_local_b = jnp.asarray(np.stack(u_local_batch, axis=0))
3074
3104
  dofs_batch_np = np.asarray(dofs_batch, dtype=int)
3105
+ assert n_a_local_const is not None
3106
+ assert n_b_local_const is not None
3075
3107
  _emit_batch(
3076
3108
  Na_b,
3077
3109
  Nb_b,
@@ -3108,6 +3140,8 @@ def assemble_mixed_surface_jacobian(
3108
3140
  normal_b = jnp.asarray(np.stack(normal_b, axis=0))
3109
3141
  u_local_b = jnp.asarray(np.stack(u_local_batch, axis=0))
3110
3142
  dofs_batch_np = np.asarray(dofs_batch, dtype=int)
3143
+ assert n_a_local_const is not None
3144
+ assert n_b_local_const is not None
3111
3145
  _emit_batch(
3112
3146
  Na_b,
3113
3147
  Nb_b,
@@ -3139,6 +3173,8 @@ def assemble_mixed_surface_jacobian(
3139
3173
  normal_b = jnp.asarray(np.stack(normal_b, axis=0))
3140
3174
  u_local_b = jnp.asarray(np.stack(u_local_batch, axis=0))
3141
3175
  dofs_batch_np = np.asarray(dofs_batch, dtype=int)
3176
+ assert n_a_local_const is not None
3177
+ assert n_b_local_const is not None
3142
3178
  _emit_batch(
3143
3179
  Na_b,
3144
3180
  Nb_b,
@@ -3158,14 +3194,14 @@ def assemble_mixed_surface_jacobian(
3158
3194
  if not batch_failed and (batch_rows or (not sparse and K_dense is not None)):
3159
3195
  if sparse:
3160
3196
  if batch_rows:
3161
- rows = np.concatenate(batch_rows)
3162
- cols = np.concatenate(batch_cols)
3163
- data = np.concatenate(batch_data)
3197
+ rows_np = np.concatenate(batch_rows)
3198
+ cols_np = np.concatenate(batch_cols)
3199
+ data_np = np.concatenate(batch_data)
3164
3200
  else:
3165
- rows = np.zeros((0,), dtype=int)
3166
- cols = np.zeros((0,), dtype=int)
3167
- data = np.zeros((0,), dtype=float)
3168
- return rows, cols, data, n_total
3201
+ rows_np = np.zeros((0,), dtype=int)
3202
+ cols_np = np.zeros((0,), dtype=int)
3203
+ data_np = np.zeros((0,), dtype=float)
3204
+ return rows_np, cols_np, data_np, n_total
3169
3205
  assert K_dense is not None
3170
3206
  return K_dense
3171
3207
 
@@ -3250,6 +3286,8 @@ def assemble_mixed_surface_jacobian(
3250
3286
  elem_coords_a = None
3251
3287
  local_a = None
3252
3288
  if use_elem_a:
3289
+ assert elem_conn_a is not None
3290
+ assert facet_to_elem_a is not None
3253
3291
  elem_id_a = int(facet_to_elem_a[int(fa)])
3254
3292
  if elem_id_a < 0:
3255
3293
  raise ValueError("facet_to_elem_a has invalid mapping")
@@ -3263,6 +3301,8 @@ def assemble_mixed_surface_jacobian(
3263
3301
  elem_coords_b = None
3264
3302
  local_b = None
3265
3303
  if use_elem_b:
3304
+ assert elem_conn_b is not None
3305
+ assert facet_to_elem_b is not None
3266
3306
  elem_id_b = int(facet_to_elem_b[int(fb)])
3267
3307
  if elem_id_b < 0:
3268
3308
  raise ValueError("facet_to_elem_b has invalid mapping")
@@ -3291,10 +3331,14 @@ def assemble_mixed_surface_jacobian(
3291
3331
  dtype=float,
3292
3332
  )
3293
3333
  if use_elem_a and grad_source == "volume":
3334
+ assert elem_nodes_a is not None
3335
+ assert elem_coords_a is not None
3294
3336
  local_a = _local_indices(elem_nodes_a, facet_a)
3295
3337
  gradNa = _tet_gradN_at_points(x_q, elem_coords_a, local=local_a, tol=tol)
3296
3338
 
3297
3339
  if use_elem_b and grad_source == "volume":
3340
+ assert elem_nodes_b is not None
3341
+ assert elem_coords_b is not None
3298
3342
  local_b = _local_indices(elem_nodes_b, facet_b)
3299
3343
  gradNb = _tet_gradN_at_points(x_q, elem_coords_b, local=local_b, tol=tol)
3300
3344
 
@@ -3325,8 +3369,16 @@ def assemble_mixed_surface_jacobian(
3325
3369
 
3326
3370
  global _DEBUG_CONTACT_MAP_ONCE
3327
3371
  if diag_map and not _DEBUG_CONTACT_MAP_ONCE:
3328
- elem_id_a = int(facet_to_elem_a[int(fa)]) if use_elem_a else -1
3329
- elem_id_b = int(facet_to_elem_b[int(fb)]) if use_elem_b else -1
3372
+ if use_elem_a:
3373
+ assert facet_to_elem_a is not None
3374
+ elem_id_a = int(facet_to_elem_a[int(fa)])
3375
+ else:
3376
+ elem_id_a = -1
3377
+ if use_elem_b:
3378
+ assert facet_to_elem_b is not None
3379
+ elem_id_b = int(facet_to_elem_b[int(fb)])
3380
+ else:
3381
+ elem_id_b = -1
3330
3382
  print("[fluxfem][diag][contact-map] first facet")
3331
3383
  print(f" fa={int(fa)} fb={int(fb)} elem_a={elem_id_a} elem_b={elem_id_b}")
3332
3384
  print(f" facet_nodes_a={facet_a.tolist()}")
@@ -3389,8 +3441,8 @@ def assemble_mixed_surface_jacobian(
3389
3441
  basis=_SurfaceBasis(dofs_per_node=value_dim_b),
3390
3442
  )
3391
3443
  fields = {
3392
- field_a: FieldPair(test=field_a_obj, trial=field_a_obj),
3393
- field_b: FieldPair(test=field_b_obj, trial=field_b_obj),
3444
+ field_a: FieldPair(test=cast("FormFieldLike", field_a_obj), trial=cast("FormFieldLike", field_a_obj)),
3445
+ field_b: FieldPair(test=cast("FormFieldLike", field_b_obj), trial=cast("FormFieldLike", field_b_obj)),
3394
3446
  }
3395
3447
  normal_q = None if normal is None else np.repeat(normal[None, :], quad_pts.shape[0], axis=0)
3396
3448
  ctx = SurfaceMixedFormContext(
@@ -3501,6 +3553,7 @@ def assemble_mixed_surface_jacobian(
3501
3553
  _trace_time(f"[CONTACT] tri {it} fd_block r_m", t_rm)
3502
3554
  cols = (r_p - r_m) / (2.0 * fd_eps)
3503
3555
  else:
3556
+ assert r0 is not None
3504
3557
  cols = (r_p - r0[:, None]) / fd_eps
3505
3558
  J_local_np[:, idxs] = np.asarray(cols, dtype=float)
3506
3559
  if log_tri:
@@ -3543,6 +3596,7 @@ def assemble_mixed_surface_jacobian(
3543
3596
  cols.extend(np.tile(dofs, n_ldofs).tolist())
3544
3597
  data.extend(J_local_np.reshape(-1).tolist())
3545
3598
  else:
3599
+ assert K_dense is not None
3546
3600
  K_dense[np.ix_(dofs, dofs)] += J_local_np
3547
3601
  if log_tri:
3548
3602
  _trace_time(f"[CONTACT] tri {it} scatter_done", t_scatter)
@@ -3591,8 +3645,8 @@ def assemble_onesided_bilinear(
3591
3645
  coords_m = np.asarray(surface_master.coords, dtype=float) if surface_master is not None else coords_s
3592
3646
  facets_m = np.asarray(surface_master.conn, dtype=int) if surface_master is not None else facets_s
3593
3647
  n_s = int(coords_s.shape[0] * value_dim)
3594
- K = np.zeros((n_s, n_s), dtype=float)
3595
- f = np.zeros((n_s,), dtype=float)
3648
+ K: np.ndarray = np.zeros((n_s, n_s), dtype=float)
3649
+ f: np.ndarray = np.zeros((n_s,), dtype=float)
3596
3650
 
3597
3651
  normals_s = surface_slave.facet_normals() if hasattr(surface_slave, "facet_normals") else None
3598
3652
  use_elem = elem_conn is not None and facet_to_elem is not None
@@ -3654,6 +3708,8 @@ def assemble_onesided_bilinear(
3654
3708
  elem_coords = None
3655
3709
  local = None
3656
3710
  if use_elem:
3711
+ assert facet_to_elem is not None
3712
+ assert elem_conn is not None
3657
3713
  elem_id = int(facet_to_elem[int(f_id)])
3658
3714
  if elem_id < 0:
3659
3715
  raise ValueError("facet_to_elem has invalid mapping")
@@ -3668,6 +3724,7 @@ def assemble_onesided_bilinear(
3668
3724
  x_q = np.array([a + r * (b - a) + s * (c - a) for r, s in quad_pts], dtype=float)
3669
3725
  if use_master:
3670
3726
  if dof_source == "surface":
3727
+ assert u_master is not None
3671
3728
  facet_m = facets_m[int(f_id)]
3672
3729
  u_master_local = _gather_u_local(u_master, facet_m, value_dim).reshape(-1, value_dim)
3673
3730
  N_master = np.array(
@@ -3676,6 +3733,9 @@ def assemble_onesided_bilinear(
3676
3733
  )
3677
3734
  u_hat = N_master @ u_master_local
3678
3735
  else:
3736
+ assert u_master is not None
3737
+ assert facet_to_elem_master is not None
3738
+ assert elem_conn_master is not None
3679
3739
  elem_id_m = int(facet_to_elem_master[int(f_id)])
3680
3740
  if elem_id_m < 0:
3681
3741
  raise ValueError("facet_to_elem_master has invalid mapping")
@@ -3699,6 +3759,8 @@ def assemble_onesided_bilinear(
3699
3759
  dtype=float,
3700
3760
  )
3701
3761
  if use_elem and grad_source == "volume":
3762
+ assert elem_nodes is not None
3763
+ assert elem_coords is not None
3702
3764
  local = _local_indices(elem_nodes, facet)
3703
3765
  gradN = _tet_gradN_at_points(x_q, elem_coords, local=local, tol=tol)
3704
3766
 
@@ -3718,7 +3780,7 @@ def assemble_onesided_bilinear(
3718
3780
  value_dim=value_dim,
3719
3781
  basis=_SurfaceBasis(dofs_per_node=value_dim),
3720
3782
  )
3721
- fields = {"u": FieldPair(test=field, trial=field)}
3783
+ fields = {"u": FieldPair(test=cast("FormFieldLike", field), trial=cast("FormFieldLike", field))}
3722
3784
  normal = normals_s[int(f_id)] if normals_s is not None else None
3723
3785
  if normal is not None:
3724
3786
  normal = normal_sign * normal
@@ -3740,7 +3802,7 @@ def assemble_onesided_bilinear(
3740
3802
  inv_h=inv_h,
3741
3803
  u_hat=u_hat,
3742
3804
  )
3743
- u_zero = np.zeros((len(nodes) * value_dim,), dtype=float)
3805
+ u_zero: np.ndarray = np.zeros((len(nodes) * value_dim,), dtype=float)
3744
3806
  u_dict = {"u": u_zero}
3745
3807
  sizes = (u_zero.shape[0],)
3746
3808
  slices = {"u": slice(0, sizes[0])}
@@ -3763,11 +3825,11 @@ def assemble_onesided_bilinear(
3763
3825
 
3764
3826
  f_local = _res_local_np(u_zero)
3765
3827
  n_ldofs = int(u_zero.shape[0])
3766
- k_local = np.zeros((n_ldofs, n_ldofs), dtype=float)
3828
+ k_local: np.ndarray = np.zeros((n_ldofs, n_ldofs), dtype=float)
3767
3829
  block = max(1, int(os.getenv("FLUXFEM_ONESIDE_BLOCK_SIZE", "16")))
3768
3830
  for start in range(0, n_ldofs, block):
3769
- idxs = np.arange(start, min(n_ldofs, start + block), dtype=int)
3770
- u_block = np.zeros((n_ldofs, idxs.size), dtype=float)
3831
+ idxs: np.ndarray = np.arange(start, min(n_ldofs, start + block), dtype=int)
3832
+ u_block: np.ndarray = np.zeros((n_ldofs, idxs.size), dtype=float)
3771
3833
  u_block[idxs, np.arange(idxs.size, dtype=int)] = 1.0
3772
3834
  r_block = _res_local_np(u_block)
3773
3835
  k_local[:, idxs] = r_block - f_local[:, None]
@@ -3815,8 +3877,8 @@ def assemble_contact_onesided_floor(
3815
3877
  coords_s = np.asarray(surface_slave.coords, dtype=float)
3816
3878
  facets_s = np.asarray(surface_slave.conn, dtype=int)
3817
3879
  n_s = int(coords_s.shape[0] * value_dim)
3818
- K = np.zeros((n_s, n_s), dtype=float)
3819
- f = np.zeros((n_s,), dtype=float)
3880
+ K: np.ndarray = np.zeros((n_s, n_s), dtype=float)
3881
+ f: np.ndarray = np.zeros((n_s,), dtype=float)
3820
3882
 
3821
3883
  normals_s = surface_slave.facet_normals() if hasattr(surface_slave, "facet_normals") else None
3822
3884
  if n is not None:
@@ -3852,6 +3914,7 @@ def assemble_contact_onesided_floor(
3852
3914
  if n is not None:
3853
3915
  normal = n
3854
3916
  else:
3917
+ assert normals_s is not None
3855
3918
  normal = normal_sign * normals_s[int(f_id)]
3856
3919
 
3857
3920
  for a, b, c_tri in triangles:
fluxfem/mesh/supermesh.py CHANGED
@@ -8,6 +8,8 @@ import numpy as np
8
8
 
9
9
  from .surface import SurfaceMesh
10
10
 
11
+ _SUPERMESH_CACHE: dict[tuple, "SurfaceSupermesh"] = {}
12
+
11
13
 
12
14
  @dataclass(eq=False)
13
15
  class SurfaceSupermesh: