easyfea 3.2.0__tar.gz → 3.2.2__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 (84) hide show
  1. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/Operators/NonLinear.py +67 -63
  2. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Simulations/_hyperelastic.py +90 -48
  3. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Simulations/_phasefield.py +11 -5
  4. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Simulations/_simu.py +166 -95
  5. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Utilities/PyVista.py +20 -5
  6. {easyfea-3.2.0 → easyfea-3.2.2}/PKG-INFO +1 -1
  7. {easyfea-3.2.0 → easyfea-3.2.2}/easyfea.egg-info/PKG-INFO +1 -1
  8. {easyfea-3.2.0 → easyfea-3.2.2}/pyproject.toml +1 -1
  9. {easyfea-3.2.0 → easyfea-3.2.2}/AUTHORS.md +0 -0
  10. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/Elems/__init__.py +0 -0
  11. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/Elems/_beam.py +0 -0
  12. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/Elems/_hexa.py +0 -0
  13. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/Elems/_point.py +0 -0
  14. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/Elems/_prism.py +0 -0
  15. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/Elems/_quad.py +0 -0
  16. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/Elems/_seg.py +0 -0
  17. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/Elems/_tetra.py +0 -0
  18. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/Elems/_tri.py +0 -0
  19. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/Operators/Bilinear.py +0 -0
  20. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/Operators/Linear.py +0 -0
  21. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/Operators/__init__.py +0 -0
  22. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/__init__.py +0 -0
  23. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/_boundary_conditions.py +0 -0
  24. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/_field.py +0 -0
  25. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/_forms.py +0 -0
  26. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/_gauss.py +0 -0
  27. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/_group_elem.py +0 -0
  28. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/_linalg.py +0 -0
  29. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/_mesh.py +0 -0
  30. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/_mesher.py +0 -0
  31. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/FEM/_utils.py +0 -0
  32. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Geoms/__init__.py +0 -0
  33. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Geoms/_circle.py +0 -0
  34. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Geoms/_contour.py +0 -0
  35. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Geoms/_domain.py +0 -0
  36. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Geoms/_geom.py +0 -0
  37. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Geoms/_line.py +0 -0
  38. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Geoms/_points.py +0 -0
  39. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Geoms/_utils.py +0 -0
  40. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Models/Beam/__init__.py +0 -0
  41. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Models/Beam/_beam.py +0 -0
  42. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Models/Elastic/__init__.py +0 -0
  43. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Models/Elastic/_laws.py +0 -0
  44. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Models/HyperElastic/__init__.py +0 -0
  45. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Models/HyperElastic/_laws.py +0 -0
  46. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Models/HyperElastic/_state.py +0 -0
  47. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Models/__init__.py +0 -0
  48. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Models/_phasefield.py +0 -0
  49. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Models/_thermal.py +0 -0
  50. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Models/_utils.py +0 -0
  51. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Models/_weakforms.py +0 -0
  52. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Simulations/Solvers.py +0 -0
  53. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Simulations/__init__.py +0 -0
  54. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Simulations/_beam.py +0 -0
  55. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Simulations/_dic.py +0 -0
  56. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Simulations/_elastic.py +0 -0
  57. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Simulations/_thermal.py +0 -0
  58. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Simulations/_utils.py +0 -0
  59. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Simulations/_weakforms.py +0 -0
  60. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Utilities/Folder.py +0 -0
  61. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Utilities/GLTF.py +0 -0
  62. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Utilities/Matplotlib.py +0 -0
  63. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Utilities/MeshIO.py +0 -0
  64. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Utilities/Paraview.py +0 -0
  65. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Utilities/Terminal.py +0 -0
  66. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Utilities/USD.py +0 -0
  67. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Utilities/Vizir.py +0 -0
  68. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Utilities/__init__.py +0 -0
  69. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Utilities/_cache.py +0 -0
  70. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Utilities/_mpi.py +0 -0
  71. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Utilities/_observers.py +0 -0
  72. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Utilities/_params.py +0 -0
  73. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Utilities/_requires.py +0 -0
  74. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Utilities/_tic.py +0 -0
  75. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/Utilities/_types.py +0 -0
  76. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/__about__.py +0 -0
  77. {easyfea-3.2.0 → easyfea-3.2.2}/EasyFEA/__init__.py +0 -0
  78. {easyfea-3.2.0 → easyfea-3.2.2}/LICENSE.txt +0 -0
  79. {easyfea-3.2.0 → easyfea-3.2.2}/README.md +0 -0
  80. {easyfea-3.2.0 → easyfea-3.2.2}/easyfea.egg-info/SOURCES.txt +0 -0
  81. {easyfea-3.2.0 → easyfea-3.2.2}/easyfea.egg-info/dependency_links.txt +0 -0
  82. {easyfea-3.2.0 → easyfea-3.2.2}/easyfea.egg-info/requires.txt +0 -0
  83. {easyfea-3.2.0 → easyfea-3.2.2}/easyfea.egg-info/top_level.txt +0 -0
  84. {easyfea-3.2.0 → easyfea-3.2.2}/setup.cfg +0 -0
@@ -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Ω``. It is 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 is what makes each side a genuine *integral over the element*, **not** a pointwise energy-density comparison. Energy-safe because ``S:Δe = ΔW`` holds per Gauss point; the squared, volume-weighted form is safe against sign cancellation between Gauss points, and per element the well-resolved regions cannot mask a coarse one.
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 the largest point count any element reached.
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
@@ -417,7 +416,7 @@ def __AdaptiveTimeQuadratureStressTensor(
417
416
  dW = material.Compute_W(state_np1) - material.Compute_W(state_n) # ΔW per point
418
417
  dE = state_np1.Compute_GreenLagrange() - state_n.Compute_GreenLagrange()
419
418
  dE_vec = Project_matrix_to_vector(dE[..., :dim, :dim]) # Δe (Kelvin-Mandel)
420
- refW = einsum("ep,ep,ep->e", wJ_e_pg, dW, dW) # Σ_p V ΔW² (Ne,)
419
+ refW = einsum("ep,ep->e", wJ_e_pg, np.abs(dW)) # Σ_p V |ΔW| (Ne,)
421
420
  Ne, nPg, ncomp = *wJ_e_pg.shape[:2], dE_vec.shape[-1]
422
421
 
423
422
  def at(
@@ -428,7 +427,8 @@ def __AdaptiveTimeQuadratureStressTensor(
428
427
  dWde_quad = FeArray.zeros(Ne, nPg, ncomp)
429
428
  d2Wde_quad = FeArray.zeros(Ne, nPg, ncomp, ncomp)
430
429
  activeElements = np.arange(Ne) # elements still refining
431
- nPts, nPts_used = 1, 1
430
+ nPts_e = np.zeros(Ne, dtype=int) # accepted point count per element (diagnostic)
431
+ nPts = 1
432
432
  while activeElements.size:
433
433
  nodes, weights = __clenshaw_curtis(nPts)
434
434
  S = sum(
@@ -441,72 +441,72 @@ def __AdaptiveTimeQuadratureStressTensor(
441
441
  defect = einsum("epi,epi->ep", S, dE_vec[activeElements]) - dW[activeElements]
442
442
  next_nPts = 3 if nPts == 1 else 2 * nPts - 1 # next level in the chain
443
443
  # accept an element once its own energy defect is within tol (all of them at the last level)
444
- done = (next_nPts > max(maxPoints, 1)) | (
445
- einsum("ep,ep,ep->e", wJ_e_pg[activeElements], defect, defect)
446
- <= tol * tol * (refW[activeElements] + 1e-300)
444
+ isAccepted = (next_nPts > max(maxPoints, 1)) | (
445
+ einsum("ep,ep->e", wJ_e_pg[activeElements], np.abs(defect))
446
+ <= tol * refW[activeElements]
447
447
  )
448
- if done.any():
449
- e = activeElements[done] # elements accepting this rule
450
- dWde_quad[e] = S[done]
451
- # their tangent only: Σ_k 2 w_k s_k d2Wde, s=0 drops out (∂e/∂u = s B)
452
- d2Wde_quad[e] = sum(
448
+ if isAccepted.any():
449
+ acceptedElems = activeElements[isAccepted] # elements accepting this rule
450
+ dWde_quad[acceptedElems] = S[isAccepted]
451
+ # their tangent only: Σ_k (w_k s_k / coefK) d2Wde, s=0 drops out (∂e/∂u = s B)
452
+ d2Wde_quad[acceptedElems] = sum(
453
453
  (
454
- (2.0 * w * s) * material.Compute_d2Wde(at(s, e))
454
+ (w * s / coefK) * material.Compute_d2Wde(at(s, acceptedElems))
455
455
  for s, w in zip(nodes, weights)
456
456
  if s
457
457
  ),
458
458
  0.0,
459
459
  )
460
- nPts_used = max(nPts_used, nPts)
461
- activeElements = activeElements[~done]
460
+ nPts_e[acceptedElems] = nPts # record each frozen element's accepted rule
461
+ activeElements = activeElements[~isAccepted]
462
462
  nPts = next_nPts
463
463
 
464
- return dWde_quad, d2Wde_quad, nPts_used
464
+ return dWde_quad, d2Wde_quad, nPts_e
465
465
 
466
466
 
467
467
  def TimeQuadratureStressTensor(
468
468
  material: "_HyperElastic",
469
469
  state_n: "HyperElasticState",
470
- state_mid: "HyperElasticState",
470
+ state_t: "HyperElasticState",
471
471
  state_np1: "HyperElasticState",
472
+ coefK: float,
472
473
  nPoints: int,
473
474
  tol: Optional[float] = None,
474
475
  maxPoints: int = 33,
475
476
  ) -> tuple[np.ndarray, np.ndarray, int]:
476
477
  r"""Tangent and residual for the PK2 stress **averaged along the strain path** of the step.
477
478
 
478
- 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)``::
479
480
 
480
481
  e(s) = e_n + s Δe , Δe = e_{n+1} - e_n , s ∈ [0, 1]
481
-
482
482
  S_quad = ∫₀¹ ∂W/∂e(e(s)) ds ≈ Σ_k w_k ∂W/∂e(e(s_k))
483
- R_e = ∫ B(ū)ᵀ · S_quad dΩ
484
-
485
- ``(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.
483
+ R_e = ∫ B(u_t)ᵀ · S_quad dΩ
486
484
 
487
- **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". Squaring guards against sign cancellation between Gauss points, and being absolute 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.
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`.
488
486
 
489
- 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`.
487
+ Tangent — only ``e_{n+1}`` depends on ``u_{n+1}`` (``∂e(s_k)/∂u_{n+1} = s_k B_{n+1}``), so::
490
488
 
491
- 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::
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Ω
492
490
 
493
- coefK · K_e = ½ A_geo(state_mid, S_quad) # raw
494
- + ∫ B(ū)ᵀ [ Σ_k w_k s_k ℂ(e(s_k)) ] B_{n+1} dΩ # pre-doubled
495
-
496
- 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.
497
492
 
498
493
  Parameters
499
494
  ----------
500
495
  material
501
496
  Hyperelastic constitutive law — supplies ``Compute_dWde(state)`` and ``Compute_d2Wde(state)``.
502
- state_n, state_mid, state_np1
503
- 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.
504
504
  nPoints
505
505
  Number of Clenshaw-Curtis points for the fixed rule (``tol is None``). Ignored when
506
506
  adaptive, which always starts from 1 (the midpoint).
507
507
  tol
508
508
  If set, refine adaptively (see above) until the relative energy defect
509
- ``‖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
510
510
  ``nPoints`` rule.
511
511
  maxPoints
512
512
  Adaptive only: cap on the number of points, by default 33. Refinement stops here even
@@ -515,20 +515,20 @@ def TimeQuadratureStressTensor(
515
515
  Returns
516
516
  -------
517
517
  K_e : ndarray of shape ``(Ne, nPe·dim, nPe·dim)``
518
- Consistent tangent, built for ``coefK = 0.5``.
518
+ Consistent tangent, built so ``coefK · K_e = ∂R_e/∂u_{n+1}``.
519
519
  R_e : ndarray of shape ``(Ne, nPe·dim)``
520
520
  Internal residual force.
521
- nPts : int
522
- Clenshaw-Curtis points used — ``nPoints`` when fixed; when adaptive, the *largest* level
523
- any element reached (individual elements may use fewer). Lets the caller log the cost.
521
+ nPts_e : ndarray of shape ``(Ne,)``
522
+ Clenshaw-Curtis points each element used — constant ``nPoints`` when fixed, per-element
523
+ when adaptive.
524
524
  """
525
525
 
526
- groupElem = state_mid.groupElem
527
- 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)
528
528
  nPe = groupElem.nPe
529
529
  dim = groupElem.dim
530
530
 
531
- _, B_mid = __block_grad_B(state_mid)
531
+ _, B_t = __block_grad_B(state_t)
532
532
  _, B_np1 = __block_grad_B(state_np1)
533
533
 
534
534
  if tol is None:
@@ -546,8 +546,8 @@ def TimeQuadratureStressTensor(
546
546
  state = _StrainPathState(state_n, state_np1, s)
547
547
  dWde_quad += w * material.Compute_dWde(state)
548
548
  if s != 0.0:
549
- d2Wde_quad += (2.0 * w * s) * material.Compute_d2Wde(state)
550
- nPts_used = int(nPoints)
549
+ d2Wde_quad += (w * s / coefK) * material.Compute_d2Wde(state)
550
+ nPts_e = np.full(groupElem.Ne, int(nPoints)) # every element uses the same rule
551
551
  else:
552
552
  # Adaptive: refine per element on the *energy defect*. This stress exists so that
553
553
  # S_quad : Δe = ΔW exactly (a discrete gradient, for energy conservation); the only error
@@ -555,16 +555,16 @@ def TimeQuadratureStressTensor(
555
555
  # is *known* from the endpoints — so the test is absolute (each level scored on its own).
556
556
  # Refining element-by-element rather than the whole block spends points only where the
557
557
  # material is nonlinear over the step (see __AdaptiveTimeQuadratureStressTensor).
558
- dWde_quad, d2Wde_quad, nPts_used = __AdaptiveTimeQuadratureStressTensor(
559
- material, state_n, state_np1, tol, int(maxPoints)
558
+ dWde_quad, d2Wde_quad, nPts_e = __AdaptiveTimeQuadratureStressTensor(
559
+ material, state_n, state_np1, coefK, tol, int(maxPoints)
560
560
  )
561
561
 
562
562
  # not __second_piola_block: that pairs one B with itself, while here the tangent is
563
- # differentiated at u_{n+1} but tested against B(ū) — hence the non-symmetry.
564
- 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)
565
565
  tangent_e = einsum(
566
- "ep,epji,epjk,epkl->eil", wJ_e_pg, B_mid, d2Wde_quad, B_np1
567
- ) + __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)
568
568
 
569
569
  if dim == 2:
570
570
  thickness = material.thickness
@@ -572,7 +572,7 @@ def TimeQuadratureStressTensor(
572
572
  residual_e *= thickness
573
573
 
574
574
  K_e, R_e = __reorder_dofs(dim, nPe, tangent_e, residual_e)
575
- return K_e, R_e, nPts_used
575
+ return K_e, R_e, nPts_e
576
576
 
577
577
 
578
578
  def ActiveStressTensor(
@@ -628,17 +628,20 @@ def KelvinVoigtDamping(
628
628
  material: "_HyperElastic",
629
629
  state: "HyperElasticState",
630
630
  velocity: np.ndarray,
631
- ) -> tuple[np.ndarray, np.ndarray]:
632
- r"""Kelvin–Voigt viscous element contributions (C_e, Kgeo_e) for the
631
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
632
+ r"""Kelvin–Voigt viscous element contributions (Kgeo_e, R_e, C_e) for the
633
633
  large-strain viscous force F_visco(u) = C(u)·v, with Σ_visco = η·Ė
634
634
  (Green-Lagrange strain rate of velocity) and B = De(u)·grad.
635
635
 
636
- - C_e = thickness · η · ∫ Bᵀ B dΩ — the damping matrix; the simulation puts
637
- it in slot 2 of (K, C, M, F) (residual b -= C @ v_t, time-scheme history,
638
- and the coefC·C tangent).
636
+ Ordered ``(tangent, residual, extra)`` like the rest of this module.
637
+
639
638
  - Kgeo_e — the configuration tangent ∂(C·v)/∂u at fixed velocity (geometric
640
639
  stiffening from Σ_visco plus the ∂Ė/∂u term); the simulation adds it to
641
640
  K_e so it rides coefK.
641
+ - R_e = thickness · η · ∫ Bᵀ Ė dΩ — the viscous residual, which the simulation
642
+ subtracts from F_e.
643
+ - C_e = thickness · η · ∫ Bᵀ B dΩ — the damping matrix; the simulation puts it
644
+ in slot 2 of (K, C, M, F), where it rides the coefC·C tangent.
642
645
 
643
646
  Parameters
644
647
  ----------
@@ -653,11 +656,12 @@ def KelvinVoigtDamping(
653
656
  Returns
654
657
  -------
655
658
  tuple
656
- (None, None) when material.eta == 0 or velocity is None. Both matrices
657
- are (Ne, nPe·dim, nPe·dim) reordered to (xi, yi, zi, ..., xn, yn, zn).
659
+ (None, None, None) when material.eta == 0 or velocity is None. Kgeo_e and C_e
660
+ are (Ne, nPe·dim, nPe·dim) and R_e is (Ne, nPe·dim), all reordered to
661
+ (xi, yi, zi, ..., xn, yn, zn).
658
662
  """
659
663
  if material.eta == 0.0 or velocity is None:
660
- return None, None # type: ignore [return-value]
664
+ return None, None, None # type: ignore [return-value]
661
665
 
662
666
  groupElem = state.groupElem
663
667
  matrixType = state.matrixType
@@ -668,22 +672,22 @@ def KelvinVoigtDamping(
668
672
 
669
673
  grad_e_pg, B_e_pg = __block_grad_B(state)
670
674
  Beta_e_pg = state.Compute_Deta(velocity) @ grad_e_pg
675
+ sig_e_pg = material.eta * state.Compute_Edot_vec(velocity) # Σ_visco = η·Ė
671
676
 
672
677
  # damping matrix C = thickness · η · ∫ Bᵀ B (fused einsum, see SPK above)
673
678
  subscripts = "ep,epji,epjl->eil"
674
679
  C_e = thickness * material.eta * einsum(subscripts, wJ_e_pg, B_e_pg, B_e_pg)
675
680
 
681
+ # viscous residual ∫ Bᵀ Σ_visco — same contraction as the active stress
682
+ residual_e = thickness * einsum("ep,epji,epj->ei", wJ_e_pg, B_e_pg, sig_e_pg)
683
+
676
684
  # configuration tangent ∂(C·v)/∂u = geometric (∫ gradᵀ Sig grad) + material-like
677
685
  # (η ∫ Bᵀ (∂Ė/∂u)) pieces
678
686
  A_mat = material.eta * einsum(subscripts, wJ_e_pg, B_e_pg, Beta_e_pg)
679
- A_geo = __geometric_tangent(
680
- wJ_e_pg,
681
- state,
682
- material.eta * state.Compute_Edot_vec(velocity),
683
- )
687
+ A_geo = __geometric_tangent(wJ_e_pg, state, sig_e_pg)
684
688
  Kgeo_e = thickness * (A_mat + A_geo)
685
689
 
686
- return __reorder_dofs(dim, nPe, C_e, Kgeo_e)
690
+ return __reorder_dofs(dim, nPe, Kgeo_e, residual_e, C_e)
687
691
 
688
692
 
689
693
  def __skew(v: np.ndarray) -> np.ndarray:
@@ -15,6 +15,7 @@ from ..Utilities import Terminal, _types
15
15
  if TYPE_CHECKING:
16
16
  from ..FEM import Mesh
17
17
  from ..FEM import MatrixType, Operators
18
+ from ..Utilities._cache import cache_computed_values
18
19
 
19
20
  # models
20
21
  from ..Models import ModelType, Project_Kelvin, Result_strain_or_stress_field_e
@@ -169,7 +170,7 @@ class HyperElastic(_Simu):
169
170
  stressType: "HyperElastic.StressType" = StressType.pointwise,
170
171
  nPoints: int = 3,
171
172
  useConsistentTangent: bool = True,
172
- quadTol: Optional[float] = None,
173
+ energyTol: Optional[float] = None,
173
174
  ) -> None:
174
175
  r"""Selects the stress used by the internal force. Call **after** :py:meth:`~EasyFEA.Simulations._Simu.Solver_Set_Hyperbolic_Algorithm`.
175
176
 
@@ -180,9 +181,11 @@ class HyperElastic(_Simu):
180
181
  total energy :math:`\mathrm{KE} + W` is conserved. See :class:`StressType` for the three
181
182
  options and the operators for their construction.
182
183
 
183
- Both non-default stresses are built on the midpoint base point :math:`\bar{\ub}` — the
184
- identity :math:`\Delta \eb = \Brm(\bar{\ub}) \cdot \Delta \ub` is what makes their energy
185
- proofs work — so they require :attr:`~EasyFEA.AlgoType.midpoint`.
184
+ Energy conservation for both non-default stresses rests on the midpoint identity
185
+ :math:`\Delta \eb = \Brm(\bar{\ub}) \cdot \Delta \ub`, so it holds only under
186
+ :attr:`~EasyFEA.AlgoType.midpoint`. ``gonzalez`` is intrinsically a midpoint discrete gradient and is
187
+ rejected off midpoint. ``quadrature`` also runs under any dynamic scheme — it scales its tangent by
188
+ that scheme's ``coefK`` and stays Newton-consistent, but conserves energy only at midpoint.
186
189
 
187
190
  Parameters
188
191
  ----------
@@ -192,37 +195,41 @@ class HyperElastic(_Simu):
192
195
  nPoints : int, optional
193
196
  ``quadrature`` only: number of Clenshaw-Curtis points, by default 3 (Simpson).
194
197
  ``1`` and ``2`` are the midpoint and trapezoid rules; more converges spectrally.
195
- When ``quadTol`` is set this is the starting (minimum) level instead of a fixed count.
198
+ When ``energyTol`` is set this is the starting (minimum) level instead of a fixed count.
196
199
  useConsistentTangent : bool, optional
197
200
  ``gonzalez`` only: if False, drop the discrete-gradient corrections from the
198
201
  tangent. Same residual and same exact conservation, but Newton converges linearly
199
202
  — a diagnostic, to measure what the consistent tangent is worth.
200
- quadTol : float, optional
203
+ energyTol : float, optional
201
204
  ``quadrature`` only: if set, the rule is refined adaptively *element by element*
202
205
  along the nested Clenshaw-Curtis chain ``1, 3, 5, 9, ...`` (capped at 33) — each
203
206
  element stops once its own **integrated** relative energy defect
204
- ``∫(S_quad:Δe − ΔW)² dΩ ≤ quadTol² ∫ΔW² dΩ`` over that element is met (a volume
207
+ ``∫|S_quad:Δe − ΔW| dΩ ≤ energyTol · ∫|ΔW| dΩ`` over that element is met (a volume
205
208
  integral over its Gauss points, not a pointwise density). So energy is conserved to
206
- ``quadTol`` while points are spent only where the step is nonlinear. ``None``
209
+ ``energyTol`` while points are spent only where the step is nonlinear. ``None``
207
210
  (default) keeps the fixed ``nPoints`` rule.
208
211
  """
209
212
  stressType = HyperElastic.StressType(stressType)
210
213
 
211
- if stressType != HyperElastic.StressType.pointwise:
212
- algo = self.algo
213
- assert algo == AlgoType.midpoint, (
214
- f"the '{stressType}' stress requires AlgoType.midpoint (got {algo}); it is built "
215
- "on the midpoint base point ū. Call Solver_Set_Hyperbolic_Algorithm(dt, algo=AlgoType.midpoint) first."
214
+ if stressType == HyperElastic.StressType.gonzalez:
215
+ # gonzalez is the midpoint energy-momentum stress: its discrete gradient is built on ū and
216
+ # conserves energy only there — intrinsically midpoint-only.
217
+ assert self.algo == AlgoType.midpoint, (
218
+ f"the 'gonzalez' stress requires AlgoType.midpoint (got {self.algo}). "
219
+ "Call Solver_Set_Hyperbolic_Algorithm(dt, algo=AlgoType.midpoint) first."
216
220
  )
221
+ # quadrature works with any dynamic scheme (its tangent is scaled by coefK); the dynamic-scheme
222
+ # requirement is checked at assembly, so the algo need not be set before this call.
217
223
  assert nPoints >= 1, f"nPoints must be >= 1 (got {nPoints})."
218
224
 
219
- self.__stressParams = (stressType, nPoints, useConsistentTangent, quadTol)
220
- self.__quadNPoints = 0 # diagnostic: points the last assembly used
225
+ self.__stressParams = (stressType, nPoints, useConsistentTangent, energyTol)
226
+ # diagnostic: per-element quadrature-point counts, last assembly
227
+ self.__nPts_e = None
221
228
 
222
229
  def __Solver_Get_Stress_Params(
223
230
  self,
224
231
  ) -> tuple["HyperElastic.StressType", int, bool, Optional[float]]:
225
- """Returns (stressType, nPoints, useConsistentTangent, quadTol) internal-force props."""
232
+ """Returns (stressType, nPoints, useConsistentTangent, energyTol) internal-force props."""
226
233
  return self.__stressParams
227
234
 
228
235
  @property
@@ -230,6 +237,11 @@ class HyperElastic(_Simu):
230
237
  """Stress used by the internal force — see :py:meth:`Solver_Set_Stress`."""
231
238
  return self.__Solver_Get_Stress_Params()[0]
232
239
 
240
+ @cache_computed_values
241
+ def __Mass_e(self, groupElem, rho, thickness, dim):
242
+ """Constant element mass matrix ``thickness · ∫ρ N·N``."""
243
+ return thickness * Operators.Bilinear.UV(groupElem, rho, dof_n=dim)
244
+
233
245
  def Construct_local_matrix_system(
234
246
  self,
235
247
  problemType,
@@ -237,31 +249,39 @@ class HyperElastic(_Simu):
237
249
  ):
238
250
  """Returns ``{groupElem: (K_e, C_e, M_e, F_e)}`` for the current Newton iterate.
239
251
 
240
- Newton solves ``A(u)·Δu = -R(u)``; the simulation assembles ``A = coefK·K + coefC·C + coefM·M`` and ``b -= C @ v_t``. Per group of elements: ``K_e`` is the elastic tangent (plus the viscous configuration tangent ``Kgeo_e``), ``F_e = -R_e`` the residual, ``C_e`` the Kelvin–Voigt damping matrix and ``M_e`` the mass matrix (dynamic schemes only).
252
+ Newton solves ``A(u)·Δu = -R(u)`` with ``A = coefK·K + coefC·C + coefM·M``. Per group of elements: ``K_e`` is the elastic tangent (plus the viscous configuration tangent ``Kgeo_e``), ``C_e`` the Kelvin–Voigt damping matrix, ``M_e`` the mass matrix (dynamic schemes only) and ``F_e = -R_e`` the complete residual — internal force together with the ``C_e·v_t + M_e·a_t`` inertia and damping terms.
241
253
  """
242
254
  dim = self.dim
243
255
  thickness = self.material.thickness if dim == 2 else 1
244
256
  isDynamic = self.algo in AlgoType.Get_Hyperbolic_Types()
245
257
 
246
- # current Newton-Raphson iterate; for dynamic schemes also capture the
247
- # velocity, for Kelvin–Voigt viscosity.
258
+ # current Newton-Raphson iterate; for dynamic schemes also capture the velocity
259
+ # (Kelvin–Voigt viscosity) and the acceleration (inertia), both needed by F_e.
248
260
  displacement = self._Solver_Get_Newton_Raphson_current_solution()
249
- velocity = None
250
- # Both non-default stresses are built from the step endpoints (u_n, u_{n+1}) on top
251
- # of the midpoint base point, so both need u_{n+1} kept before the midpoint
252
- # evaluation overwrites `displacement` with ū. Re-checked here (not only in the
253
- # setter) so that re-calling Solver_Set_Hyperbolic_Algorithm with another algo
254
- # can't leave a stale selection.
255
- stressType, nPoints, useConsistentTangent, quadTol = (
261
+ velocity = accel = None
262
+ # Both non-default stresses are built from the step endpoints (u_n, u_{n+1}) on top of the scheme's
263
+ # base point u_t (ū at midpoint), so both need u_{n+1} kept before the scheme evaluation overwrites
264
+ # `displacement` with u_t. Re-checked here (not only in the setter) so that re-calling
265
+ # Solver_Set_Hyperbolic_Algorithm with another algo can't leave a stale selection.
266
+ stressType, nPoints, useConsistentTangent, energyTol = (
256
267
  self.__Solver_Get_Stress_Params()
257
268
  )
258
269
  isPointwise = stressType == HyperElastic.StressType.pointwise
259
- assert (
260
- isPointwise or self.algo == AlgoType.midpoint
261
- ), f"the '{stressType}' stress requires AlgoType.midpoint (got {self.algo})."
270
+ if stressType == HyperElastic.StressType.gonzalez:
271
+ # gonzalez is the midpoint energy-momentum stress: its discrete gradient Ŝ = S̄ + α Δe is built
272
+ # on ū and conserves energy only there — it is intrinsically midpoint-only.
273
+ assert (
274
+ self.algo == AlgoType.midpoint
275
+ ), f"the 'gonzalez' stress requires AlgoType.midpoint (got {self.algo})."
276
+ elif stressType == HyperElastic.StressType.quadrature:
277
+ # quadrature builds a consistent tangent for any dynamic scheme via its `coefK = ∂u_t/∂u_{n+1}`;
278
+ # energy is conserved only at midpoint (coefK = 0.5) — see TimeQuadratureStressTensor.
279
+ assert (
280
+ isDynamic
281
+ ), f"the 'quadrature' stress requires a dynamic (hyperbolic) time scheme (got {self.algo})."
262
282
  u_np1 = displacement
263
283
  if isDynamic:
264
- displacement, velocity, _ = self._Solver_Evaluate_u_v_a_for_time_scheme(
284
+ displacement, velocity, accel = self._Solver_Evaluate_u_v_a_for_time_scheme(
265
285
  problemType, displacement
266
286
  )
267
287
  if not isPointwise:
@@ -270,7 +290,7 @@ class HyperElastic(_Simu):
270
290
  errDetF = "det(F) < 0 - reduce load steps"
271
291
 
272
292
  out = {}
273
- quadNPoints = 0 # max Clenshaw-Curtis points over groups this Newton iteration
293
+ list_nPts_e = [] # per-element point counts across groups (quadrature only)
274
294
  for groupElem in self.mesh.Get_list_groupElem():
275
295
  state = HyperElasticState(groupElem, displacement, matrixType)
276
296
 
@@ -298,15 +318,18 @@ class HyperElastic(_Simu):
298
318
  useConsistentTangent,
299
319
  )
300
320
  elif stressType == HyperElastic.StressType.quadrature:
301
- K_e, residual_e, nPts = (
321
+ # coefK = ∂u_t/∂u_{n+1} scales the tangent for the active scheme (0.5 at midpoint).
322
+ coefK = self._Solver_Get_K_C_M_coefs_for_time_scheme()[0]
323
+ K_e, residual_e, nPts_e = (
302
324
  Operators.NonLinear.TimeQuadratureStressTensor(
303
325
  self.material,
304
326
  *hyperElasticStates,
327
+ coefK,
305
328
  nPoints,
306
- quadTol,
329
+ energyTol,
307
330
  )
308
331
  )
309
- quadNPoints = max(quadNPoints, nPts)
332
+ list_nPts_e.append(nPts_e)
310
333
  else:
311
334
  raise NotImplementedError
312
335
 
@@ -324,24 +347,41 @@ class HyperElastic(_Simu):
324
347
 
325
348
  F_e = -residual_e
326
349
 
327
- # Kelvin–Voigt viscosity: C_e is the damping matrix (slot 2, rides
328
- # coefC, carries the viscous residual b -= C @ v_t); Kgeo_e is the
329
- # configuration tangent ∂(C·v)/∂u, added to K_e so it rides coefK.
350
+ # Kelvin–Voigt viscosity: C_e is the damping matrix (slot 2, so it rides coefC
351
+ # in A), R_visco_e its residual, and Kgeo_e the configuration tangent
352
+ # ∂(C·v)/∂u, added to K_e so it rides coefK.
330
353
  C_e = None
331
354
  if self.material.eta != 0 and velocity is not None:
332
- C_e, Kgeo_e = Operators.NonLinear.KelvinVoigtDamping(
355
+ Kgeo_e, R_visco_e, C_e = Operators.NonLinear.KelvinVoigtDamping(
333
356
  self.material, state, velocity
334
357
  )
335
358
  K_e += Kgeo_e
359
+ F_e -= R_visco_e
336
360
 
337
- # mass matrix — only assembled for dynamic schemes
361
+ # mass matrix and the inertia residual — dynamic schemes only
338
362
  M_e = None
339
363
  if isDynamic:
340
- M_e = thickness * Operators.Bilinear.UV(groupElem, self.rho, dof_n=dim)
364
+ # M_e = thickness · ∫ρ N·N is constant across the solve, so it is cached (see __Mass_e)
365
+ # instead of rebuilt every Newton iteration. An array ρ is unhashable, so it cannot key the
366
+ # cache and is recomputed directly.
367
+ if isinstance(self.rho, np.ndarray):
368
+ M_e = thickness * Operators.Bilinear.UV(
369
+ groupElem, self.rho, dof_n=dim
370
+ )
371
+ else:
372
+ M_e = self.__Mass_e(groupElem, self.rho, thickness, dim)
373
+ F_e -= np.einsum(
374
+ "eij,ej->ei",
375
+ M_e,
376
+ groupElem.Locates_sol_e(accel, dim),
377
+ optimize=True,
378
+ )
341
379
 
342
380
  out[groupElem] = (K_e, C_e, M_e, F_e)
343
381
 
344
- self.__quadNPoints = quadNPoints # 0 unless quadrature stress ran this assembly
382
+ # per-element Clenshaw-Curtis point counts this assembly, or None if the quadrature
383
+ # stress did not run (pointwise / gonzalez)
384
+ self.__nPts_e = np.concatenate(list_nPts_e) if list_nPts_e else None
345
385
 
346
386
  return out
347
387
 
@@ -358,10 +398,8 @@ class HyperElastic(_Simu):
358
398
  if self.algo in AlgoType.Get_Hyperbolic_Types():
359
399
  iter["speed"] = self._Get_v_n(self.problemType)
360
400
  iter["accel"] = self._Get_a_n(self.problemType)
361
- if (
362
- self.__quadNPoints
363
- ): # points the converged step used (quadrature stress only)
364
- iter["quadNPoints"] = self.__quadNPoints
401
+ if self.__nPts_e is not None: # per-element point counts, quadrature only
402
+ iter["nPts_e"] = self.__nPts_e
365
403
 
366
404
  return super().Save_Iter(iter)
367
405
 
@@ -491,10 +529,14 @@ class HyperElastic(_Simu):
491
529
  elif result == "W_e":
492
530
  values = self._Calc_W(False)
493
531
 
494
- elif ("S" in result or "E" in result) and ("_norm" not in result):
495
- # Green-Lagrange (E) and second Piola-Kirchhoff (S), group by group
532
+ elif result in ["Green-Lagrange", "Piola-Kirchhoff"] or (
533
+ ("S" in result or "E" in result) and ("_norm" not in result)
534
+ ):
535
+ # Green-Lagrange (E) and second Piola-Kirchhoff (S), group by group.
536
+ # "Green-Lagrange" and "Piola-Kirchhoff" carry no upper-case E or S, so they have to
537
+ # be matched by name: they are what Results_nodeFields_elementFields hands to Paraview.
496
538
 
497
- isStress = "S" in result
539
+ isStress = result == "Piola-Kirchhoff" or "S" in result
498
540
  res = (
499
541
  result
500
542
  if result in ["Green-Lagrange", "Piola-Kirchhoff"]
@@ -687,16 +687,22 @@ class PhaseField(_Simu):
687
687
  return self._Calc_Psi_Elas()
688
688
 
689
689
  elif result == "Wdef_e":
690
- values = self._Calc_Psi_Elas()
690
+ values = self._Calc_Psi_Elas(returnScalar=False)
691
691
 
692
692
  elif result == "Psi_Crack":
693
693
  return self._Calc_Psi_Crack()
694
694
 
695
- if result == "psiP":
696
- values_e_pg = self.__Calc_psiPlus_e_pg()
697
- values = np.mean(values_e_pg, axis=1)
695
+ elif result == "psiP":
696
+ # group by group, in Get_list_groupElem order, so the (Ne,) result lines up with
697
+ # Get_Node_Values on meshes carrying several element groups
698
+ values = np.concatenate(
699
+ [
700
+ np.asarray(self.__Calc_psiPlus_e_pg(groupElem)).mean(1)
701
+ for groupElem in self.mesh.Get_list_groupElem()
702
+ ]
703
+ )
698
704
 
699
- if result == "damage":
705
+ elif result == "damage":
700
706
  values = self.damage # type: ignore [assignment]
701
707
 
702
708
  elif result in ["ux", "uy", "uz"]: