EasyFEA 3.2.1__tar.gz → 3.3.0__tar.gz

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 (95) hide show
  1. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Operators/Linear.py +17 -0
  2. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Operators/NonLinear.py +35 -34
  3. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/_field.py +3 -0
  4. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/_group_elem.py +58 -53
  5. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/_linalg.py +215 -105
  6. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/_mesh.py +15 -7
  7. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/_mesher.py +502 -234
  8. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Geoms/__init__.py +3 -2
  9. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Geoms/_circle.py +89 -19
  10. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Geoms/_contour.py +4 -0
  11. easyfea-3.3.0/EasyFEA/Geoms/_domain.py +124 -0
  12. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Geoms/_geom.py +91 -19
  13. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Geoms/_line.py +30 -5
  14. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Geoms/_points.py +15 -5
  15. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Geoms/_utils.py +42 -0
  16. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/Elastic/_laws.py +15 -45
  17. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/HyperElastic/_laws.py +18 -3
  18. easyfea-3.3.0/EasyFEA/Models/IsotropicHardening.py +98 -0
  19. easyfea-3.3.0/EasyFEA/Models/KinematicHardening.py +95 -0
  20. easyfea-3.3.0/EasyFEA/Models/ViscoElastic.py +40 -0
  21. easyfea-3.3.0/EasyFEA/Models/ViscoPlastic.py +90 -0
  22. easyfea-3.3.0/EasyFEA/Models/Yield.py +188 -0
  23. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/__init__.py +8 -0
  24. easyfea-3.3.0/EasyFEA/Models/_behaviour.py +827 -0
  25. easyfea-3.3.0/EasyFEA/Models/_kelvin.py +36 -0
  26. easyfea-3.3.0/EasyFEA/Models/_materialpoint.py +139 -0
  27. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/_phasefield.py +20 -19
  28. easyfea-3.3.0/EasyFEA/Models/_spectral.py +181 -0
  29. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/_utils.py +5 -38
  30. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/Solvers.py +0 -1
  31. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/__init__.py +1 -0
  32. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/_beam.py +5 -5
  33. easyfea-3.3.0/EasyFEA/Simulations/_behaviour.py +340 -0
  34. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/_hyperelastic.py +83 -43
  35. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/_phasefield.py +25 -21
  36. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/_simu.py +198 -17
  37. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/MeshIO.py +262 -69
  38. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/Paraview.py +2 -3
  39. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/_mpi.py +56 -0
  40. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/_params.py +17 -0
  41. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/_types.py +3 -0
  42. {easyfea-3.2.1/easyfea.egg-info → easyfea-3.3.0}/PKG-INFO +11 -10
  43. {easyfea-3.2.1 → easyfea-3.3.0}/README.md +10 -9
  44. {easyfea-3.2.1 → easyfea-3.3.0/easyfea.egg-info}/PKG-INFO +11 -10
  45. {easyfea-3.2.1 → easyfea-3.3.0}/easyfea.egg-info/SOURCES.txt +10 -0
  46. {easyfea-3.2.1 → easyfea-3.3.0}/pyproject.toml +4 -2
  47. easyfea-3.2.1/EasyFEA/Geoms/_domain.py +0 -73
  48. {easyfea-3.2.1 → easyfea-3.3.0}/AUTHORS.md +0 -0
  49. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Elems/__init__.py +0 -0
  50. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Elems/_beam.py +0 -0
  51. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Elems/_hexa.py +0 -0
  52. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Elems/_point.py +0 -0
  53. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Elems/_prism.py +0 -0
  54. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Elems/_quad.py +0 -0
  55. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Elems/_seg.py +0 -0
  56. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Elems/_tetra.py +0 -0
  57. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Elems/_tri.py +0 -0
  58. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Operators/Bilinear.py +0 -0
  59. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Operators/__init__.py +0 -0
  60. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/__init__.py +0 -0
  61. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/_boundary_conditions.py +0 -0
  62. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/_forms.py +0 -0
  63. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/_gauss.py +0 -0
  64. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/_utils.py +0 -0
  65. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/Beam/__init__.py +0 -0
  66. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/Beam/_beam.py +0 -0
  67. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/Elastic/__init__.py +0 -0
  68. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/HyperElastic/__init__.py +0 -0
  69. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/HyperElastic/_state.py +0 -0
  70. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/_thermal.py +0 -0
  71. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/_weakforms.py +0 -0
  72. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/_dic.py +0 -0
  73. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/_elastic.py +0 -0
  74. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/_thermal.py +0 -0
  75. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/_utils.py +0 -0
  76. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/_weakforms.py +0 -0
  77. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/Folder.py +0 -0
  78. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/GLTF.py +0 -0
  79. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/Matplotlib.py +0 -0
  80. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/PyVista.py +0 -0
  81. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/Terminal.py +0 -0
  82. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/USD.py +0 -0
  83. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/Vizir.py +0 -0
  84. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/__init__.py +0 -0
  85. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/_cache.py +0 -0
  86. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/_observers.py +0 -0
  87. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/_requires.py +0 -0
  88. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/_tic.py +0 -0
  89. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/__about__.py +0 -0
  90. {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/__init__.py +0 -0
  91. {easyfea-3.2.1 → easyfea-3.3.0}/LICENSE.txt +0 -0
  92. {easyfea-3.2.1 → easyfea-3.3.0}/easyfea.egg-info/dependency_links.txt +0 -0
  93. {easyfea-3.2.1 → easyfea-3.3.0}/easyfea.egg-info/requires.txt +0 -0
  94. {easyfea-3.2.1 → easyfea-3.3.0}/easyfea.egg-info/top_level.txt +0 -0
  95. {easyfea-3.2.1 → easyfea-3.3.0}/setup.cfg +0 -0
@@ -33,3 +33,20 @@ def V(
33
33
  Ne, nPg = vec_e_pg.shape[:2]
34
34
  f = FeArray.broadcast(f, Ne, nPg)
35
35
  return (f * vec_e_pg).integrate()
36
+
37
+
38
+ def InternalForce(
39
+ groupElem: "_GroupElem",
40
+ sigma_e_pg: FeArray.FeArrayALike,
41
+ matrixType: MatrixType = MatrixType.rigi,
42
+ ) -> np.ndarray:
43
+ """``∫_Ω σ : ε(v) dΩ`` — internal force of a known stress field.
44
+
45
+ Returns ``(Ne, nPe·dim)``.
46
+
47
+ ``sigma_e_pg`` is Kelvin-Mandel with shape ``(Ne, nPg, nstrain)``. This is the residual side
48
+ of a nonlinear problem: unlike :func:`Bilinear.LinearizedElasticity`, the stress is given
49
+ rather than derived from ``C : ε(u)``.
50
+ """
51
+ leftDispPart_e_pg = groupElem.Get_leftDispPart_e_pg(matrixType)
52
+ return (leftDispPart_e_pg @ FeArray.asfearray(sigma_e_pg)).integrate()
@@ -397,14 +397,13 @@ def __AdaptiveTimeQuadratureStressTensor(
397
397
  material: "_HyperElastic",
398
398
  state_n: "HyperElasticState",
399
399
  state_np1: "HyperElasticState",
400
+ coefK: float,
400
401
  tol: float,
401
402
  maxPoints: int,
402
403
  ) -> tuple["FeArray", "FeArray", int]:
403
404
  r"""Per-element adaptive strain-path quadrature — the ``tol``-driven path of :func:`TimeQuadratureStressTensor`.
404
405
 
405
- Each element refines along the nested chain ``1, 3, 5, 9, …`` until *its own* energy defect is within ``tol``, then freezes — so a low-strain element stops at one point while a high-strain one keeps refining. The test is the **integrated** relative error over the element, ``∫_Ωe |S:Δe − ΔW| dΩ ≤ tol · ∫_Ωe |ΔW| dΩ``, evaluated as the Gauss-point sums ``Σ_p V_(ep) |S:Δe − ΔW| ≤ tol · Σ_p V_(ep) |ΔW|`` with ``V_(ep)`` the Gauss point's volume (weight × Jacobian) — so the ``V_(ep)`` factor makes each side a genuine *integral over the element*, **not** a pointwise energy-density comparison. This L1 (absolute) form is the tightest simple bound on the element's actual per-step energy drift ``|∫_Ωe (S:Δe − ΔW) dΩ|`` (triangle inequality, no volume factor), and taking ``|·|`` before summing makes it safe against sign cancellation between Gauss points — a well-resolved region cannot mask a coarse one. Energy-safe because ``S:Δe = ΔW`` holds per Gauss point.
406
-
407
- Only the still-active elements are evaluated at each level — the rule is applied to that subset via :meth:`_StrainPathState._sliced` — so the constitutive cost tracks the hard elements, not the mesh. (Nodes shared with a coarser level are re-evaluated rather than cached; the active set shrinks fast, so that stays cheap and keeps the loop plain.) Returns ``(dWde_quad, d2Wde_quad)`` — each row carrying its element's accepted rule — and ``nPts_e``, the point count each element accepted.
406
+ Each element refines along the nested chain ``1, 3, 5, 9, …`` (capped at ``maxPoints``) until *its own* integrated energy defect is within ``tol`` — ``Σ_p V_(ep) |S:Δe − ΔW| ≤ tol · Σ_p V_(ep) |ΔW|``, ``V_(ep)`` the Gauss-point volume — then freezes, so points are spent only where the step is nonlinear. The defect is the quadrature error of the exact identity ``S:Δe = ΔW`` (``ΔW`` known from the endpoints), so the test is absolute; taking ``|·|`` before summing bounds the element's real energy drift and is safe against Gauss-point sign cancellation. Only still-active elements are evaluated at each level (via :meth:`_StrainPathState._sliced`), so cost tracks the hard elements. Returns ``(dWde_quad, d2Wde_quad, nPts_e)``, each row carrying its element's accepted rule and point count.
408
407
  """
409
408
  groupElem = state_n.groupElem # state_n and state_np1 share the group
410
409
  dim = groupElem.dim
@@ -439,7 +438,7 @@ def __AdaptiveTimeQuadratureStressTensor(
439
438
  ),
440
439
  0.0,
441
440
  )
442
- defect = einsum("epi,epi->ep", S, dE_vec[activeElements]) - dW[activeElements]
441
+ defect = S @ dE_vec[activeElements] - dW[activeElements]
443
442
  next_nPts = 3 if nPts == 1 else 2 * nPts - 1 # next level in the chain
444
443
  # accept an element once its own energy defect is within tol (all of them at the last level)
445
444
  isAccepted = (next_nPts > max(maxPoints, 1)) | (
@@ -449,10 +448,10 @@ def __AdaptiveTimeQuadratureStressTensor(
449
448
  if isAccepted.any():
450
449
  acceptedElems = activeElements[isAccepted] # elements accepting this rule
451
450
  dWde_quad[acceptedElems] = S[isAccepted]
452
- # their tangent only: Σ_k 2 w_k s_k d2Wde, s=0 drops out (∂e/∂u = s B)
451
+ # their tangent only: Σ_k (w_k s_k / coefK) d2Wde, s=0 drops out (∂e/∂u = s B)
453
452
  d2Wde_quad[acceptedElems] = sum(
454
453
  (
455
- (2.0 * w * s) * material.Compute_d2Wde(at(s, acceptedElems))
454
+ (w * s / coefK) * material.Compute_d2Wde(at(s, acceptedElems))
456
455
  for s, w in zip(nodes, weights)
457
456
  if s
458
457
  ),
@@ -468,46 +467,46 @@ def __AdaptiveTimeQuadratureStressTensor(
468
467
  def TimeQuadratureStressTensor(
469
468
  material: "_HyperElastic",
470
469
  state_n: "HyperElasticState",
471
- state_mid: "HyperElasticState",
470
+ state_t: "HyperElasticState",
472
471
  state_np1: "HyperElasticState",
472
+ coefK: float,
473
473
  nPoints: int,
474
474
  tol: Optional[float] = None,
475
475
  maxPoints: int = 33,
476
476
  ) -> tuple[np.ndarray, np.ndarray, int]:
477
477
  r"""Tangent and residual for the PK2 stress **averaged along the strain path** of the step.
478
478
 
479
- Returns ``(K_e, R_e)`` in ``(xi,yi,zi,...,xn,yn,zn)``. Where :func:`SecondPiolaKirchhoffStressTensor` samples the stress at one configuration, this integrates it along the straight segment joining the two end strains and contracts the result with the **midpoint** operator::
479
+ Returns ``(K_e, R_e)`` in ``(xi,yi,zi,...,xn,yn,zn)``. Where :func:`SecondPiolaKirchhoffStressTensor` samples the stress at one configuration, this averages it along the segment between the two end strains and tests it against ``B(u_t)``::
480
480
 
481
481
  e(s) = e_n + s Δe , Δe = e_{n+1} - e_n , s ∈ [0, 1]
482
-
483
482
  S_quad = ∫₀¹ ∂W/∂e(e(s)) ds ≈ Σ_k w_k ∂W/∂e(e(s_k))
484
- R_e = ∫ B(ū)ᵀ · S_quad dΩ
485
-
486
- ``(s_k, w_k)`` is the Clenshaw-Curtis rule on ``nPoints`` points (:func:`__clenshaw_curtis`); ``1, 2, 3`` are the midpoint, trapezoid and Simpson rules. Intermediate nodes are :class:`_StrainPathState`; ``s = 0, 1`` reuse the end states.
487
-
488
- **Adaptive (per-element) mode.** With ``tol`` set, the rule is chosen *element by element* by :func:`__AdaptiveTimeQuadratureStressTensor`: each element walks the nested chain ``1, 3, 5, 9, 17, 33`` (up to ``maxPoints``) and freezes once *its own* energy defect is within ``tol``, so points are spent only where the step is nonlinear — a low-strain element may stop at a single midpoint while a stiff one keeps refining. The defect ``S_quad:Δe − ΔW`` is the quadrature error of the discrete-gradient identity ``S_quad:Δe = ΔW`` (with ``ΔW`` *known* from the endpoints), so the test is absolute — no consecutive-difference guess. It is the **integrated** relative error over the element, ``∫_Ωe |S_quad:Δe − ΔW| dΩ ≤ tol · ∫_Ωe |ΔW| dΩ`` — the integrals are the volume-weighted Gauss-point sums ``Σ_p V_(ep)···``, *not* a pointwise energy-density comparison — so ``tol`` reads as "conserve energy to this relative tolerance". Taking the absolute value before summing bounds the element's actual energy drift directly and guards against sign cancellation between Gauss points. Since each level is scored on its own (no comparison to a coarser one) the test accepts the coarsest rule directly: the ``1``-point midpoint is exact for a linear energy integrand, so a quadratic ``W`` converges at a single point. The tangent uses each element's accepted rule, so residual and tangent stay consistent.
483
+ R_e = ∫ B(u_t)ᵀ · S_quad dΩ
489
484
 
490
- Since ``Δe = B(ū)·Δu`` exactly and ``de/ds = Δe`` is constant along the segment, the fundamental theorem of calculus gives ``S_quad:Δe = ΔW`` once the ``s``-integral is exact — a **discrete gradient**. The energy defect is therefore just the quadrature error, which Clenshaw-Curtis drives down spectrally; a quadratic ``W`` is exact at every rule. Note ``nPoints = 1`` is the average-strain stress ``S(½(e_n + e_{n+1}))``, *not* the midpoint-displacement stress of :func:`SecondPiolaKirchhoffStressTensor`.
485
+ ``(s_k, w_k)`` is the Clenshaw-Curtis rule on ``nPoints`` points; ``1, 2, 3`` are the midpoint, trapezoid and Simpson rules (``nPoints = 1`` is the *average-strain* stress, not the midpoint-displacement one). At midpoint ``Δe = B(ū)·Δu`` exactly, so ``S_quad:Δe = ΔW`` once the ``s``-integral is exact — a **discrete gradient** whose energy defect is just the quadrature error, driven down spectrally (a quadratic ``W`` is exact at every rule). With ``tol`` set the rule is refined *per element* by :func:`__AdaptiveTimeQuadratureStressTensor`.
491
486
 
492
- Tangent — only ``e_{n+1}`` depends on ``u_{n+1}``, and ``∂e(s_k)/∂u_{n+1} = s_k B_{n+1}``, so every node contracts with the same ``B_{n+1}`` and the constitutive tensors collapse into one weighted sum::
487
+ Tangent — only ``e_{n+1}`` depends on ``u_{n+1}`` (``∂e(s_k)/∂u_{n+1} = s_k B_{n+1}``), so::
493
488
 
494
- coefK · K_e = ½ A_geo(state_mid, S_quad) # raw
495
- + ∫ B(ū)ᵀ [ Σ_k w_k s_k ℂ(e(s_k)) ] B_{n+1} dΩ # pre-doubled
489
+ coefK · K_e = coefK · A_geo(state_t, S_quad) + ∫ B(u_t)ᵀ [ Σ_k w_k s_k ℂ(e(s_k)) ] B_{n+1} dΩ
496
490
 
497
- Built for :attr:`~EasyFEA.AlgoType.midpoint`'s ``coefK = 0.5`` as in :func:`GonzalezStressTensor`; the doubled weights sum to 1 whatever ``nPoints``. Pairing ``B(ū)`` with ``B_{n+1}`` makes ``K_e`` non-symmetric.
491
+ The geometric block is raw so it rides the caller's ``coefK`` chain factor ``∂u_t/∂u_{n+1}``; the material term is pre-scaled by ``1/coefK`` to survive it. Pairing ``B(u_t)`` with ``B_{n+1}`` makes ``K_e`` non-symmetric.
498
492
 
499
493
  Parameters
500
494
  ----------
501
495
  material
502
496
  Hyperelastic constitutive law — supplies ``Compute_dWde(state)`` and ``Compute_d2Wde(state)``.
503
- state_n, state_mid, state_np1
504
- Hyperelastic states at ``u_n``, ``ū`` and ``u_{n+1}`` (same group / matrix type).
497
+ state_n, state_t, state_np1
498
+ Hyperelastic states at ``u_n``, the time-scheme base displacement ``u_t``, and ``u_{n+1}`` (same group / matrix type).
499
+ coefK
500
+ Time-scheme K-coefficient ``= ∂u_t/∂u_{n+1}`` for the base displacement ``u_t`` passed as
501
+ ``state_t`` — ``0.5`` for :attr:`~EasyFEA.AlgoType.midpoint` (the only value that conserves energy),
502
+ ``1`` for :attr:`~EasyFEA.AlgoType.newmark`, ``1−α`` for :attr:`~EasyFEA.AlgoType.hht`. Other schemes
503
+ give a consistent but non-conserving stress.
505
504
  nPoints
506
505
  Number of Clenshaw-Curtis points for the fixed rule (``tol is None``). Ignored when
507
506
  adaptive, which always starts from 1 (the midpoint).
508
507
  tol
509
508
  If set, refine adaptively (see above) until the relative energy defect
510
- ``‖S_quad:Δe − ΔW‖ / ‖ΔW‖`` falls below ``tol``. ``None`` (default) keeps the fixed
509
+ ``|S_quad:Δe − ΔW| / |ΔW|`` falls below ``tol``. ``None`` (default) keeps the fixed
511
510
  ``nPoints`` rule.
512
511
  maxPoints
513
512
  Adaptive only: cap on the number of points, by default 33. Refinement stops here even
@@ -516,7 +515,7 @@ def TimeQuadratureStressTensor(
516
515
  Returns
517
516
  -------
518
517
  K_e : ndarray of shape ``(Ne, nPe·dim, nPe·dim)``
519
- Consistent tangent, built for ``coefK = 0.5``.
518
+ Consistent tangent, built so ``coefK · K_e = ∂R_e/∂u_{n+1}``.
520
519
  R_e : ndarray of shape ``(Ne, nPe·dim)``
521
520
  Internal residual force.
522
521
  nPts_e : ndarray of shape ``(Ne,)``
@@ -524,12 +523,12 @@ def TimeQuadratureStressTensor(
524
523
  when adaptive.
525
524
  """
526
525
 
527
- groupElem = state_mid.groupElem
528
- wJ_e_pg = groupElem.Get_weightedJacobian_e_pg(state_mid.matrixType)
526
+ groupElem = state_t.groupElem
527
+ wJ_e_pg = groupElem.Get_weightedJacobian_e_pg(state_t.matrixType)
529
528
  nPe = groupElem.nPe
530
529
  dim = groupElem.dim
531
530
 
532
- _, B_mid = __block_grad_B(state_mid)
531
+ _, B_t = __block_grad_B(state_t)
533
532
  _, B_np1 = __block_grad_B(state_np1)
534
533
 
535
534
  if tol is None:
@@ -547,7 +546,7 @@ def TimeQuadratureStressTensor(
547
546
  state = _StrainPathState(state_n, state_np1, s)
548
547
  dWde_quad += w * material.Compute_dWde(state)
549
548
  if s != 0.0:
550
- d2Wde_quad += (2.0 * w * s) * material.Compute_d2Wde(state)
549
+ d2Wde_quad += (w * s / coefK) * material.Compute_d2Wde(state)
551
550
  nPts_e = np.full(groupElem.Ne, int(nPoints)) # every element uses the same rule
552
551
  else:
553
552
  # Adaptive: refine per element on the *energy defect*. This stress exists so that
@@ -557,15 +556,15 @@ def TimeQuadratureStressTensor(
557
556
  # Refining element-by-element rather than the whole block spends points only where the
558
557
  # material is nonlinear over the step (see __AdaptiveTimeQuadratureStressTensor).
559
558
  dWde_quad, d2Wde_quad, nPts_e = __AdaptiveTimeQuadratureStressTensor(
560
- material, state_n, state_np1, tol, int(maxPoints)
559
+ material, state_n, state_np1, coefK, tol, int(maxPoints)
561
560
  )
562
561
 
563
562
  # not __second_piola_block: that pairs one B with itself, while here the tangent is
564
- # differentiated at u_{n+1} but tested against B(ū) — hence the non-symmetry.
565
- residual_e = einsum("ep,epi,epij->ej", wJ_e_pg, dWde_quad, B_mid)
563
+ # differentiated at u_{n+1} but tested against B(u_t) — hence the non-symmetry.
564
+ residual_e = einsum("ep,epi,epij->ej", wJ_e_pg, dWde_quad, B_t)
566
565
  tangent_e = einsum(
567
- "ep,epji,epjk,epkl->eil", wJ_e_pg, B_mid, d2Wde_quad, B_np1
568
- ) + __geometric_tangent(wJ_e_pg, state_mid, dWde_quad)
566
+ "ep,epji,epjk,epkl->eil", wJ_e_pg, B_t, d2Wde_quad, B_np1
567
+ ) + __geometric_tangent(wJ_e_pg, state_t, dWde_quad)
569
568
 
570
569
  if dim == 2:
571
570
  thickness = material.thickness
@@ -601,9 +600,11 @@ def ActiveStressTensor(
601
600
  Returns
602
601
  -------
603
602
  tuple
604
- ``(None, None)`` when ``material.active_stress == 0``. Otherwise ``Kgeo_e`` of shape ``(Ne, nPe·dim, nPe·dim)`` and ``R_e`` of shape ``(Ne, nPe·dim)``, reordered to ``(xi, yi, zi, ..., xn, yn, zn)``.
603
+ ``(None, None)`` when ``material.active_stress`` is nowhere non-zero. Otherwise ``Kgeo_e`` of shape ``(Ne, nPe·dim, nPe·dim)`` and ``R_e`` of shape ``(Ne, nPe·dim)``, reordered to ``(xi, yi, zi, ..., xn, yn, zn)``.
605
604
  """
606
- if material.active_stress == 0.0:
605
+ # `np.all` rather than `== 0.0`: the magnitude may be an (Ne, nPg) field, and a partly-active
606
+ # body still has to be assembled
607
+ if np.all(material.active_stress == 0.0):
607
608
  return None, None # type: ignore [return-value]
608
609
 
609
610
  groupElem = state.groupElem
@@ -17,6 +17,9 @@ from ..Utilities import _types
17
17
  class Field:
18
18
  """Field class."""
19
19
 
20
+ _isFeField = True
21
+ """marks the class for :mod:`._linalg`, which cannot import it without a cycle"""
22
+
20
23
  def __init__(
21
24
  self,
22
25
  groupElem: _GroupElem,
@@ -211,17 +211,22 @@ class _GroupElem(ABC):
211
211
  Remark
212
212
  ------
213
213
  Ghost nodes will be computed using the given (non-ghost) nodes array.
214
+
215
+ The four index arrays are stored **sorted**, whatever order they arrive in: `Mesher` builds them
216
+ from python sets, so they come in hash order, while `connect` is in global-index order. Every
217
+ consumer pairs the two through `np.searchsorted`, which requires sorted input and returns
218
+ out-of-range indices rather than complaining when it does not get it.
214
219
  """
215
220
 
216
221
  Ne = self.__connect.shape[0]
217
222
  # set elements (may be smaller than Ne when ghost elements are present)
218
- elements = np.asarray(elements, dtype=int)
223
+ elements = np.sort(np.asarray(elements, dtype=int))
219
224
  assert elements.size <= Ne
220
- ghostElements = np.asarray(ghostElements, dtype=int)
225
+ ghostElements = np.sort(np.asarray(ghostElements, dtype=int))
221
226
  assert ghostElements.size <= Ne
222
227
  # get nodes and ghost nodes
223
- nodes = np.asarray(nodes, dtype=int)
224
- ghostNodes = np.asarray(list(set(self.nodes) - set(nodes)), dtype=int)
228
+ nodes = np.sort(np.asarray(nodes, dtype=int))
229
+ ghostNodes = np.sort(np.asarray(list(set(self.nodes) - set(nodes)), dtype=int))
225
230
 
226
231
  self.__partitionned_data = (rank, elements, ghostElements, nodes, ghostNodes)
227
232
 
@@ -232,6 +237,30 @@ class _GroupElem(ABC):
232
237
  (rank, elements, ghostElements, nodes, ghostNodes)"""
233
238
  return self.__partitionned_data
234
239
 
240
+ @property
241
+ def _globalElements(self) -> _types.IntArray:
242
+ """Global element index of each row of `connect`.
243
+
244
+ `arange(Ne)` in serial. Under MPI the rows are this rank's owned and ghost elements, sorted by
245
+ global index. Use it to index an array stored for the whole mesh (a fibre field, a per-element
246
+ contractility) with this rank's elements — the element counterpart of `_global_to_local_nodes`.
247
+
248
+ Not to be confused with `elements`, which is `arange(Ne)` in both cases because tags, `vol_e`
249
+ and every other per-element array are indexed by local row.
250
+
251
+ Always `Ne` long, so it indexes a whole-mesh array into exactly this group's rows. That holds
252
+ at the edges too: an empty group gives an empty `int` array rather than a float one, and a rank
253
+ holding only the boundary layer of a type (no owned element, ghosts only) gives the ghosts —
254
+ `connect` is built from the same `unique(owned ∪ ghosts)`.
255
+ """
256
+ _, elements, ghostElements, _, _ = self.__partitionned_data
257
+
258
+ # both arrays are stored sorted, so the merge is the only work left
259
+ if ghostElements.size == 0:
260
+ return elements
261
+
262
+ return np.unique(np.concatenate([elements, ghostElements]))
263
+
235
264
  @property
236
265
  def _rankPartition(self) -> Optional[_types.IntArray]:
237
266
  """Array of shape (Ne,) where rankPartition[e] is the MPI rank that owned
@@ -606,7 +635,8 @@ class _GroupElem(ABC):
606
635
  coord_e_pg[:, :, 0], coord_e_pg[:, :, 1], coord_e_pg[:, :, 2]
607
636
  )
608
637
 
609
- eval_e_pg = FeArray.asfearray(eval_e_pg)
638
+ # func may return a constant, so go through the field constructor rather than a view
639
+ eval_e_pg = FeArray.broadcast(eval_e_pg, *wJ_e_pg.shape[:2])
610
640
 
611
641
  values_e = (wJ_e_pg * eval_e_pg).integrate()
612
642
 
@@ -1577,59 +1607,29 @@ class _GroupElem(ABC):
1577
1607
 
1578
1608
  assert isinstance(line, Line)
1579
1609
 
1580
- unitVector = line.unitVector
1581
-
1582
- vect = self.coord - line.coord[0]
1583
-
1584
- scalarProd = np.einsum("i,ni-> n", unitVector, vect, optimize="optimal")
1585
- crossProd = np.cross(vect, unitVector)
1586
- norm = np.linalg.norm(crossProd, axis=1)
1587
-
1588
- eps = 1e-12
1589
-
1590
- idx = np.where(
1591
- (norm < eps) & (scalarProd >= -eps) & (scalarProd <= line.length + eps)
1592
- )[0]
1593
-
1610
+ idx = np.where(line.Contains(self.coord, 1e-12))[0]
1594
1611
  return self.__nodes[idx].copy()
1595
1612
 
1596
1613
  def Get_Nodes_Domain(self, domain: "Domain") -> _types.IntArray:
1597
1614
  """Returns nodes in the domain."""
1598
1615
 
1599
- assert isinstance(domain, Line)
1600
-
1601
- xn, yn, zn = self.coord.T
1602
-
1603
- eps = 1e-12
1604
-
1605
- idx = np.where(
1606
- (xn >= domain.pt1.x - eps)
1607
- & (xn <= domain.pt2.x + eps)
1608
- & (yn >= domain.pt1.y - eps)
1609
- & (yn <= domain.pt2.y + eps)
1610
- & (zn >= domain.pt1.z - eps)
1611
- & (zn <= domain.pt2.z + eps)
1612
- )[0]
1616
+ assert isinstance(domain, Domain)
1613
1617
 
1618
+ idx = np.where(domain.Encloses(self.coord, 1e-12))[0]
1614
1619
  return self.__nodes[idx].copy()
1615
1620
 
1616
1621
  def Get_Nodes_Circle(self, circle: "Circle", onlyOnEdge=False) -> _types.IntArray:
1617
- """Returns nodes in the circle."""
1622
+ """Returns nodes in the circle.
1618
1623
 
1619
- assert isinstance(circle, Circle)
1620
-
1621
- eps = 1e-12
1624
+ Both selections are taken in the circle's own plane; use `Get_Nodes_Cylinder` to select through a thickness.
1625
+ """
1622
1626
 
1623
- vals = np.linalg.norm(self.coord - circle.center.coord, axis=1)
1627
+ assert isinstance(circle, Circle)
1624
1628
 
1625
- if onlyOnEdge:
1626
- idx = np.where(
1627
- (vals <= circle.diam / 2 + eps) & (vals >= circle.diam / 2 - eps)
1628
- )
1629
- else:
1630
- idx = np.where(vals <= circle.diam / 2 + eps)
1629
+ test = circle.Contains if onlyOnEdge else circle.Encloses
1631
1630
 
1632
- return self.__nodes[idx]
1631
+ idx = np.where(test(self.coord, 1e-12))[0]
1632
+ return self.__nodes[idx].copy()
1633
1633
 
1634
1634
  def Get_Nodes_Cylinder(
1635
1635
  self, circle: "Circle", direction=[0, 0, 1], onlyOnEdge=False
@@ -1756,7 +1756,7 @@ class _GroupElem(ABC):
1756
1756
  return self.__dict_elements_tags[tag]
1757
1757
  else:
1758
1758
  print(f"The {tag} tag is unknown")
1759
- return np.array([])
1759
+ return np.array([], dtype=int)
1760
1760
 
1761
1761
  def Get_Nodes_Tag(self, tag: str) -> _types.IntArray:
1762
1762
  """Returns node associated with the tag."""
@@ -1764,7 +1764,7 @@ class _GroupElem(ABC):
1764
1764
  return self.__dict_nodes_tags[tag]
1765
1765
  else:
1766
1766
  print(f"The {tag} tag is unknown")
1767
- return np.array([])
1767
+ return np.array([], dtype=int)
1768
1768
 
1769
1769
  def Locates_sol_e(
1770
1770
  self, sol: _types.FloatArray, dof_n: Optional[int] = None, asFeArray=False
@@ -2138,11 +2138,14 @@ class _GroupElem(ABC):
2138
2138
  # xiP are the n coordinates of the n points in (ξ, η, ζ).
2139
2139
  coordInElem_n[nodesInElement, :] = np.asarray(xiP) # type: ignore
2140
2140
 
2141
- detectedNodes = detectedNodes[detectedNodes != None].astype(int)
2141
+ # both arrays are object arrays pre-filled with None, so the comparison is
2142
+ # elementwise; np.not_equal says so without tripping E711
2143
+ mask_n = np.not_equal(detectedNodes, None)
2144
+ detectedNodes = detectedNodes[mask_n].astype(int)
2142
2145
 
2143
- mask = detectedElements_e != None
2144
- detectedElements_e = detectedElements_e[mask].astype(int)
2145
- connect_e_n = connect_e_n[mask]
2146
+ mask_e = np.not_equal(detectedElements_e, None)
2147
+ detectedElements_e = detectedElements_e[mask_e].astype(int)
2148
+ connect_e_n = connect_e_n[mask_e]
2146
2149
 
2147
2150
  return detectedNodes, detectedElements_e, connect_e_n, coordInElem_n
2148
2151
 
@@ -2216,7 +2219,7 @@ class _GroupElem(ABC):
2216
2219
  # --------------------------------------------------------------------------------------------
2217
2220
 
2218
2221
  # import must be done here to avoid circular imports
2219
- from . import Elems
2222
+ from . import Elems # noqa: E402
2220
2223
 
2221
2224
 
2222
2225
  class GroupElemFactory:
@@ -2264,7 +2267,9 @@ class GroupElemFactory:
2264
2267
  """
2265
2268
 
2266
2269
  if gmshId not in GroupElemFactory.DICT_GMSH_DATA:
2267
- raise KeyError("gmshId is unknown.")
2270
+ raise KeyError(
2271
+ f"gmshId {gmshId} is not supported yet, but it can be added to DICT_GMSH_DATA."
2272
+ )
2268
2273
 
2269
2274
  return GroupElemFactory.DICT_GMSH_DATA[gmshId]
2270
2275