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/surface.py CHANGED
@@ -1,18 +1,32 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
- from typing import Optional
4
+ from typing import Callable, Optional, Protocol, Sequence, TYPE_CHECKING, TypeVar, cast
5
5
  import jax
6
6
  import jax.numpy as jnp
7
7
 
8
8
  from .dtypes import INDEX_DTYPE
9
9
  import numpy as np
10
+ import numpy.typing as npt
10
11
 
11
12
  DTYPE = jnp.float64 if jax.config.read("jax_enable_x64") else jnp.float32
12
13
 
13
14
  from .base import BaseMesh, BaseMeshPytree
14
15
  from .hex import HexMesh, HexMeshPytree
15
16
 
17
+ P = TypeVar("P")
18
+
19
+ if TYPE_CHECKING:
20
+ from ..solver.bc import SurfaceFormContext
21
+
22
+
23
+ class SurfaceSpaceLike(Protocol):
24
+ value_dim: int
25
+ mesh: BaseMesh
26
+
27
+
28
+ SurfaceLinearForm = Callable[["SurfaceFormContext", P], npt.ArrayLike]
29
+
16
30
 
17
31
  def _polygon_area(pts: np.ndarray) -> float:
18
32
  """
@@ -26,7 +40,7 @@ def _polygon_area(pts: np.ndarray) -> float:
26
40
  for i in range(1, pts.shape[0] - 1):
27
41
  v1 = pts[i] - p0
28
42
  v2 = pts[i + 1] - p0
29
- area += 0.5 * np.linalg.norm(np.cross(v1, v2))
43
+ area += float(0.5 * np.linalg.norm(np.cross(v1, v2)))
30
44
  return float(area)
31
45
 
32
46
 
@@ -103,20 +117,43 @@ class SurfaceMesh(BaseMesh):
103
117
  from ..solver.bc import facet_normals
104
118
  return facet_normals(self, outward_from=outward_from, normalize=normalize)
105
119
 
106
- def assemble_load(self, load, *, dim: int, n_total_nodes: int | None = None, F0=None):
120
+ def assemble_load(
121
+ self,
122
+ load: npt.ArrayLike,
123
+ *,
124
+ dim: int,
125
+ n_total_nodes: int | None = None,
126
+ F0: npt.ArrayLike | None = None,
127
+ ) -> np.ndarray:
107
128
  from ..solver.bc import assemble_surface_load
108
129
  return assemble_surface_load(self, load, dim=dim, n_total_nodes=n_total_nodes, F0=F0)
109
130
 
110
- def assemble_linear_form(self, form, params, *, dim: int, n_total_nodes: int | None = None, F0=None):
131
+ def assemble_linear_form(
132
+ self,
133
+ form: SurfaceLinearForm[P],
134
+ params: P,
135
+ *,
136
+ dim: int,
137
+ n_total_nodes: int | None = None,
138
+ F0: npt.ArrayLike | None = None,
139
+ ) -> np.ndarray:
111
140
  from ..solver.bc import assemble_surface_linear_form
112
141
  return assemble_surface_linear_form(self, form, params, dim=dim, n_total_nodes=n_total_nodes, F0=F0)
113
142
 
114
- def assemble_linear_form_on_space(self, space, form, params, *, F0=None):
143
+ def assemble_linear_form_on_space(
144
+ self,
145
+ space: SurfaceSpaceLike,
146
+ form: SurfaceLinearForm[P],
147
+ params: P,
148
+ *,
149
+ F0: npt.ArrayLike | None = None,
150
+ ) -> np.ndarray:
115
151
  """
116
152
  Assemble surface linear form using global size inferred from a volume space.
117
153
  """
118
154
  dim = int(getattr(space, "value_dim", 1))
119
- n_total_nodes = int(getattr(space, "mesh", self).n_nodes)
155
+ mesh = cast(BaseMesh, getattr(space, "mesh", self))
156
+ n_total_nodes = int(mesh.n_nodes)
120
157
  return self.assemble_linear_form(form, params, dim=dim, n_total_nodes=n_total_nodes, F0=F0)
121
158
 
122
159
 
@@ -137,13 +174,13 @@ def surface_with_elem_conn(mesh: BaseMesh, facets, *, mode: str = "touching") ->
137
174
 
138
175
  def assemble_traction(
139
176
  self,
140
- traction,
177
+ traction: float | Sequence[float],
141
178
  *,
142
179
  dim: int = 3,
143
180
  n_total_nodes: int | None = None,
144
- F0=None,
145
- outward_from=None,
146
- ):
181
+ F0: npt.ArrayLike | None = None,
182
+ outward_from: npt.ArrayLike | None = None,
183
+ ) -> np.ndarray:
147
184
  from ..solver.bc import assemble_surface_traction
148
185
  return assemble_surface_traction(
149
186
  self,
@@ -222,31 +259,54 @@ class SurfaceMeshPytree(BaseMeshPytree):
222
259
  from ..solver.bc import facet_normals
223
260
  return facet_normals(self, outward_from=outward_from, normalize=normalize)
224
261
 
225
- def assemble_load(self, load, *, dim: int, n_total_nodes: int | None = None, F0=None):
262
+ def assemble_load(
263
+ self,
264
+ load: npt.ArrayLike,
265
+ *,
266
+ dim: int,
267
+ n_total_nodes: int | None = None,
268
+ F0: npt.ArrayLike | None = None,
269
+ ) -> np.ndarray:
226
270
  from ..solver.bc import assemble_surface_load
227
271
  return assemble_surface_load(self, load, dim=dim, n_total_nodes=n_total_nodes, F0=F0)
228
272
 
229
- def assemble_linear_form(self, form, params, *, dim: int, n_total_nodes: int | None = None, F0=None):
273
+ def assemble_linear_form(
274
+ self,
275
+ form: SurfaceLinearForm[P],
276
+ params: P,
277
+ *,
278
+ dim: int,
279
+ n_total_nodes: int | None = None,
280
+ F0: npt.ArrayLike | None = None,
281
+ ) -> np.ndarray:
230
282
  from ..solver.bc import assemble_surface_linear_form
231
283
  return assemble_surface_linear_form(self, form, params, dim=dim, n_total_nodes=n_total_nodes, F0=F0)
232
284
 
233
- def assemble_linear_form_on_space(self, space, form, params, *, F0=None):
285
+ def assemble_linear_form_on_space(
286
+ self,
287
+ space: SurfaceSpaceLike,
288
+ form: SurfaceLinearForm[P],
289
+ params: P,
290
+ *,
291
+ F0: npt.ArrayLike | None = None,
292
+ ) -> np.ndarray:
234
293
  """
235
294
  Assemble surface linear form using global size inferred from a volume space.
236
295
  """
237
296
  dim = int(getattr(space, "value_dim", 1))
238
- n_total_nodes = int(getattr(space, "mesh", self).n_nodes)
297
+ mesh = cast(BaseMesh, getattr(space, "mesh", self))
298
+ n_total_nodes = int(mesh.n_nodes)
239
299
  return self.assemble_linear_form(form, params, dim=dim, n_total_nodes=n_total_nodes, F0=F0)
240
300
 
241
301
  def assemble_traction(
242
302
  self,
243
- traction,
303
+ traction: float | Sequence[float],
244
304
  *,
245
305
  dim: int = 3,
246
306
  n_total_nodes: int | None = None,
247
- F0=None,
248
- outward_from=None,
249
- ):
307
+ F0: npt.ArrayLike | None = None,
308
+ outward_from: npt.ArrayLike | None = None,
309
+ ) -> np.ndarray:
250
310
  from ..solver.bc import assemble_surface_traction
251
311
  return assemble_surface_traction(
252
312
  self,
@@ -259,12 +319,12 @@ class SurfaceMeshPytree(BaseMeshPytree):
259
319
 
260
320
  def assemble_flux(
261
321
  self,
262
- flux,
322
+ flux: npt.ArrayLike,
263
323
  *,
264
324
  n_total_nodes: int | None = None,
265
- F0=None,
266
- outward_from=None,
267
- ):
325
+ F0: npt.ArrayLike | None = None,
326
+ outward_from: npt.ArrayLike | None = None,
327
+ ) -> np.ndarray:
268
328
  from ..solver.bc import assemble_surface_flux
269
329
  return assemble_surface_flux(
270
330
  self,
fluxfem/mesh/tet.py CHANGED
@@ -223,11 +223,14 @@ class StructuredTetTensorBox:
223
223
  npx = len(xs)
224
224
  npy = len(ys)
225
225
  npz = len(zs)
226
+ X: np.ndarray
227
+ Y: np.ndarray
228
+ Z: np.ndarray
226
229
  X, Y, Z = np.meshgrid(np.sort(xs), np.sort(ys), np.sort(zs))
227
230
  p = np.vstack((X.flatten("F"), Y.flatten("F"), Z.flatten("F")))
228
- ix = np.arange(npx * npy * npz)
231
+ ix: np.ndarray = np.arange(npx * npy * npz)
229
232
  ne = (npx - 1) * (npy - 1) * (npz - 1)
230
- t = np.zeros((8, ne), dtype=np.int64)
233
+ t: np.ndarray = np.zeros((8, ne), dtype=np.int64)
231
234
  ix = ix.reshape(npy, npx, npz, order="F").copy()
232
235
  t[0] = ix[0:(npy - 1), 0:(npx - 1), 0:(npz - 1)].reshape(ne, 1, order="F").copy().flatten()
233
236
  t[1] = ix[1:npy, 0:(npx - 1), 0:(npz - 1)].reshape(ne, 1, order="F").copy().flatten()
@@ -238,7 +241,7 @@ class StructuredTetTensorBox:
238
241
  t[6] = ix[0:(npy - 1), 1:npx, 1:npz].reshape(ne, 1, order="F").copy().flatten()
239
242
  t[7] = ix[1:npy, 1:npx, 1:npz].reshape(ne, 1, order="F").copy().flatten()
240
243
 
241
- T = np.zeros((4, 6 * ne), dtype=np.int64)
244
+ T: np.ndarray = np.zeros((4, 6 * ne), dtype=np.int64)
242
245
  T[:, :ne] = t[[0, 1, 5, 7]]
243
246
  T[:, ne:(2 * ne)] = t[[0, 1, 4, 7]]
244
247
  T[:, (2 * ne):(3 * ne)] = t[[0, 2, 4, 7]]
@@ -247,6 +250,6 @@ class StructuredTetTensorBox:
247
250
  T[:, (5 * ne):] = t[[0, 3, 6, 7]]
248
251
 
249
252
  coords = p.T.astype(DTYPE, copy=False)
250
- conn = T.T.astype(NP_INDEX_DTYPE, copy=False)
253
+ conn: np.ndarray = T.T.astype(NP_INDEX_DTYPE, copy=False)
251
254
  conn = self._fix_orientation(coords, conn)
252
255
  return TetMesh(coords=jnp.array(coords), conn=jnp.array(conn))
@@ -1,11 +1,23 @@
1
+ from typing import Mapping, TYPE_CHECKING, TypeAlias
2
+
1
3
  import jax
2
4
  import jax.numpy as jnp
3
5
  import numpy as np
4
6
 
5
7
  from ...core.forms import FormContext
8
+ from ...core.space import FESpace
9
+ from ...mesh import BaseMesh
6
10
  from ...core.basis import build_B_matrices_finite
7
11
  from ..postprocess import make_point_data_displacement, write_point_data_vtu
8
12
 
13
+ if TYPE_CHECKING:
14
+ from jax import Array as JaxArray
15
+
16
+ ArrayLike: TypeAlias = np.ndarray | JaxArray
17
+ else:
18
+ ArrayLike: TypeAlias = np.ndarray
19
+ ParamsLike: TypeAlias = Mapping[str, float] | tuple[float, float]
20
+
9
21
 
10
22
  def right_cauchy_green(F: jnp.ndarray) -> jnp.ndarray:
11
23
  """C = F^T F (right Cauchy-Green)."""
@@ -46,7 +58,9 @@ def pk2_neo_hookean(F: jnp.ndarray, mu: float, lam: float) -> jnp.ndarray:
46
58
  return mu * (I - C_inv) + lam * jnp.log(J)[..., None, None] * C_inv
47
59
 
48
60
 
49
- def neo_hookean_residual_form(ctx: FormContext, u_elem: jnp.ndarray, params) -> jnp.ndarray:
61
+ def neo_hookean_residual_form(
62
+ ctx: FormContext, u_elem: jnp.ndarray, params: ParamsLike
63
+ ) -> jnp.ndarray:
50
64
  """
51
65
  Compressible Neo-Hookean residual (Total Lagrangian, PK2).
52
66
  params: dict-like with keys \"mu\", \"lam\" or tuple (mu, lam)
@@ -92,11 +106,26 @@ __all__ = [
92
106
  ]
93
107
 
94
108
 
95
- def make_elastic_point_data(mesh, space, u, *, compute_j: bool = True, deformed_scale: float = 1.0):
109
+ def make_elastic_point_data(
110
+ mesh: BaseMesh,
111
+ space: FESpace,
112
+ u: ArrayLike,
113
+ *,
114
+ compute_j: bool = True,
115
+ deformed_scale: float = 1.0,
116
+ ) -> dict[str, np.ndarray]:
96
117
  """Alias to postprocess.make_point_data_displacement for backward compatibility."""
97
118
  return make_point_data_displacement(mesh, space, u, compute_j=compute_j, deformed_scale=deformed_scale)
98
119
 
99
120
 
100
- def write_elastic_vtu(mesh, space, u, filepath: str, *, compute_j: bool = True, deformed_scale: float = 1.0):
121
+ def write_elastic_vtu(
122
+ mesh: BaseMesh,
123
+ space: FESpace,
124
+ u: ArrayLike,
125
+ filepath: str,
126
+ *,
127
+ compute_j: bool = True,
128
+ deformed_scale: float = 1.0,
129
+ ) -> None:
101
130
  """Alias to postprocess.write_point_data_vtu for backward compatibility."""
102
131
  return write_point_data_vtu(mesh, space, u, filepath, compute_j=compute_j, deformed_scale=deformed_scale)
@@ -1,9 +1,14 @@
1
+ from typing import TypeAlias
2
+
1
3
  import jax.numpy as jnp
2
4
 
5
+ from ...core.assembly import LinearReturn
3
6
  from ...core.forms import FormContext, vector_load_form
4
- from ...core.basis import build_B_matrices
7
+ from ...core.space import FESpace
5
8
  from ...physics.operators import sym_grad
6
9
 
10
+ ArrayLike: TypeAlias = jnp.ndarray
11
+
7
12
  # from ...mechanics.kinematics import build_B_matrices
8
13
 
9
14
 
@@ -41,7 +46,13 @@ def vector_body_force_form(ctx: FormContext, load_vec: jnp.ndarray) -> jnp.ndarr
41
46
  vector_body_force_form._ff_kind = "linear"
42
47
  vector_body_force_form._ff_domain = "volume"
43
48
 
44
- def assemble_constant_body_force(space, gravity_vec, density: float, *, sparse: bool = False):
49
+ def assemble_constant_body_force(
50
+ space: FESpace,
51
+ gravity_vec: ArrayLike,
52
+ density: float,
53
+ *,
54
+ sparse: bool = False,
55
+ ) -> LinearReturn:
45
56
  """
46
57
  Convenience: assemble body force from density * gravity vector.
47
58
  gravity_vec: length-3 array-like (direction and magnitude of g)
@@ -1,13 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TypeAlias
4
+
3
5
  import jax.numpy as jnp
4
6
 
7
+ ArrayLike: TypeAlias = jnp.ndarray
8
+
5
9
 
6
- def _sym(A: jnp.ndarray) -> jnp.ndarray:
10
+ def _sym(A: ArrayLike) -> ArrayLike:
7
11
  return 0.5 * (A + jnp.swapaxes(A, -1, -2))
8
12
 
9
13
 
10
- def principal_stresses(S: jnp.ndarray) -> jnp.ndarray:
14
+ def principal_stresses(S: ArrayLike) -> ArrayLike:
11
15
  """
12
16
  Return principal stresses (eigvals) for symmetric 3x3 stress tensor.
13
17
  Supports batching over leading dimensions.
@@ -16,12 +20,12 @@ def principal_stresses(S: jnp.ndarray) -> jnp.ndarray:
16
20
  return jnp.linalg.eigvalsh(S_sym)
17
21
 
18
22
 
19
- def principal_sum(S: jnp.ndarray) -> jnp.ndarray:
23
+ def principal_sum(S: ArrayLike) -> ArrayLike:
20
24
  """Sum of principal stresses (trace)."""
21
25
  return jnp.trace(S, axis1=-2, axis2=-1)
22
26
 
23
27
 
24
- def max_shear_stress(S: jnp.ndarray) -> jnp.ndarray:
28
+ def max_shear_stress(S: ArrayLike) -> ArrayLike:
25
29
  """
26
30
  Maximum shear stress = (sigma_max - sigma_min) / 2.
27
31
  """
@@ -29,7 +33,7 @@ def max_shear_stress(S: jnp.ndarray) -> jnp.ndarray:
29
33
  return 0.5 * (vals[..., -1] - vals[..., 0])
30
34
 
31
35
 
32
- def von_mises_stress(S: jnp.ndarray) -> jnp.ndarray:
36
+ def von_mises_stress(S: ArrayLike) -> ArrayLike:
33
37
  """
34
38
  von Mises equivalent stress: sqrt(3/2 * dev(S):dev(S)).
35
39
  """
@@ -2,11 +2,18 @@
2
2
 
3
3
  # fluxfem/mechanics/operators.py
4
4
  from __future__ import annotations
5
+
6
+ from typing import Any, TypeAlias
7
+
5
8
  import jax
6
9
  import jax.numpy as jnp
7
10
 
11
+ from ..core.context_types import FormFieldLike
12
+
13
+ ArrayLike: TypeAlias = jnp.ndarray
14
+
8
15
 
9
- def dot(a, b):
16
+ def dot(a: FormFieldLike | ArrayLike, b: ArrayLike) -> ArrayLike:
10
17
  """
11
18
  Batched matrix product on the last two axes.
12
19
 
@@ -19,7 +26,7 @@ def dot(a, b):
19
26
  return jnp.matmul(a, b)
20
27
 
21
28
 
22
- def ddot(a, b, c=None):
29
+ def ddot(a: ArrayLike, b: ArrayLike, c: ArrayLike | None = None) -> ArrayLike:
23
30
  """
24
31
  Double contraction on the last two axes.
25
32
 
@@ -32,12 +39,12 @@ def ddot(a, b, c=None):
32
39
  return jnp.einsum("...ik,kl,...lm->...im", a_t, b, c)
33
40
 
34
41
 
35
- def transpose_last2(a):
42
+ def transpose_last2(a: ArrayLike) -> ArrayLike:
36
43
  """Swap the last two axes (batched transpose)."""
37
44
  return jnp.swapaxes(a, -1, -2)
38
45
 
39
46
 
40
- def sym_grad(field) -> jnp.ndarray:
47
+ def sym_grad(field: FormFieldLike) -> jnp.ndarray:
41
48
  """
42
49
  Symmetric gradient operator for vector mechanics (small strain).
43
50
 
@@ -89,7 +96,7 @@ def sym_grad(field) -> jnp.ndarray:
89
96
  return jax.vmap(B_single)(gradN)
90
97
 
91
98
 
92
- def sym_grad_u(field, u_elem: jnp.ndarray) -> jnp.ndarray:
99
+ def sym_grad_u(field: FormFieldLike, u_elem: jnp.ndarray) -> jnp.ndarray:
93
100
  """
94
101
  Apply sym_grad(field) to a local displacement vector.
95
102
 
@@ -1,15 +1,33 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING, TypeAlias
4
+
3
5
  import numpy as np
4
6
  import jax
5
7
  import jax.numpy as jnp
6
8
 
7
9
  # from ..core.assembly import build_form_contexts
8
10
  from ..tools.visualizer import write_vtu
11
+ from ..mesh import BaseMesh
12
+ from ..core.space import FESpace
9
13
  from ..core.interp import interpolate_field_at_element_points
10
14
 
15
+ if TYPE_CHECKING:
16
+ from jax import Array as JaxArray
17
+
18
+ ArrayLike: TypeAlias = np.ndarray | JaxArray
19
+ else:
20
+ ArrayLike: TypeAlias = np.ndarray
21
+
11
22
 
12
- def make_point_data_displacement(mesh, space, u, *, compute_j: bool = True, deformed_scale: float = 1.0):
23
+ def make_point_data_displacement(
24
+ mesh: BaseMesh,
25
+ space: FESpace,
26
+ u: ArrayLike,
27
+ *,
28
+ compute_j: bool = True,
29
+ deformed_scale: float = 1.0,
30
+ ) -> dict[str, np.ndarray]:
13
31
  """
14
32
  Common postprocess helper to build point data dictionaries:
15
33
  - displacement
@@ -58,14 +76,22 @@ def make_point_data_displacement(mesh, space, u, *, compute_j: bool = True, defo
58
76
  return point_data
59
77
 
60
78
 
61
- def write_point_data_vtu(mesh, space, u, filepath: str, *, compute_j: bool = True, deformed_scale: float = 1.0):
79
+ def write_point_data_vtu(
80
+ mesh: BaseMesh,
81
+ space: FESpace,
82
+ u: ArrayLike,
83
+ filepath: str,
84
+ *,
85
+ compute_j: bool = True,
86
+ deformed_scale: float = 1.0,
87
+ ) -> None:
62
88
  """Write VTU with displacement/deformed_coords and optional J."""
63
89
  pdata = make_point_data_displacement(mesh, space, u, compute_j=compute_j, deformed_scale=deformed_scale)
64
90
  write_vtu(mesh, filepath, point_data=pdata)
65
91
 
66
92
 
67
93
  __all__ = ["make_point_data_displacement", "write_point_data_vtu", "interpolate_at_points"]
68
- def interpolate_at_points(space, u, points: np.ndarray):
94
+ def interpolate_at_points(space: FESpace, u: ArrayLike, points: np.ndarray) -> np.ndarray:
69
95
  """
70
96
  Interpolate displacement field at given physical points (Hex8 only, structured search).
71
97
  - points: (m,3) array of physical coordinates.
@@ -24,8 +24,10 @@ from .dirichlet import (
24
24
  from .cg import cg_solve, cg_solve_jax, build_cg_operator, CGOperator
25
25
  from .preconditioner import make_block_jacobi_preconditioner
26
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
27
+ from .block_matrix import FluxBlockMatrix, diag as block_diag, make as make_block_matrix
28
28
  from .newton import newton_solve
29
+ from .result import SolverResult
30
+ from .history import NewtonIterRecord
29
31
  from .solve_runner import (
30
32
  NonlinearAnalysis,
31
33
  NewtonLoopConfig,
@@ -68,9 +70,12 @@ __all__ = [
68
70
  "build_block_system",
69
71
  "split_block_matrix",
70
72
  "BlockSystem",
73
+ "FluxBlockMatrix",
71
74
  "block_diag",
72
75
  "make_block_matrix",
73
76
  "newton_solve",
77
+ "SolverResult",
78
+ "NewtonIterRecord",
74
79
  "LinearAnalysis",
75
80
  "LinearSolveConfig",
76
81
  "LinearStepResult",