EasyFEA 3.2.0__tar.gz → 3.2.1__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.
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/Operators/NonLinear.py +41 -36
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Simulations/_hyperelastic.py +51 -29
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Simulations/_simu.py +158 -92
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Utilities/PyVista.py +20 -5
- {easyfea-3.2.0 → easyfea-3.2.1}/PKG-INFO +1 -1
- {easyfea-3.2.0 → easyfea-3.2.1}/easyfea.egg-info/PKG-INFO +1 -1
- {easyfea-3.2.0 → easyfea-3.2.1}/pyproject.toml +1 -1
- {easyfea-3.2.0 → easyfea-3.2.1}/AUTHORS.md +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/Elems/__init__.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/Elems/_beam.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/Elems/_hexa.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/Elems/_point.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/Elems/_prism.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/Elems/_quad.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/Elems/_seg.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/Elems/_tetra.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/Elems/_tri.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/Operators/Bilinear.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/Operators/Linear.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/Operators/__init__.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/__init__.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/_boundary_conditions.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/_field.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/_forms.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/_gauss.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/_group_elem.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/_linalg.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/_mesh.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/_mesher.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/FEM/_utils.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Geoms/__init__.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Geoms/_circle.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Geoms/_contour.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Geoms/_domain.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Geoms/_geom.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Geoms/_line.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Geoms/_points.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Geoms/_utils.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Models/Beam/__init__.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Models/Beam/_beam.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Models/Elastic/__init__.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Models/Elastic/_laws.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Models/HyperElastic/__init__.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Models/HyperElastic/_laws.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Models/HyperElastic/_state.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Models/__init__.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Models/_phasefield.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Models/_thermal.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Models/_utils.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Models/_weakforms.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Simulations/Solvers.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Simulations/__init__.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Simulations/_beam.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Simulations/_dic.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Simulations/_elastic.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Simulations/_phasefield.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Simulations/_thermal.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Simulations/_utils.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Simulations/_weakforms.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Utilities/Folder.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Utilities/GLTF.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Utilities/Matplotlib.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Utilities/MeshIO.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Utilities/Paraview.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Utilities/Terminal.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Utilities/USD.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Utilities/Vizir.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Utilities/__init__.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Utilities/_cache.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Utilities/_mpi.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Utilities/_observers.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Utilities/_params.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Utilities/_requires.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Utilities/_tic.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/Utilities/_types.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/__about__.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/EasyFEA/__init__.py +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/LICENSE.txt +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/README.md +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/easyfea.egg-info/SOURCES.txt +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/easyfea.egg-info/dependency_links.txt +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/easyfea.egg-info/requires.txt +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/easyfea.egg-info/top_level.txt +0 -0
- {easyfea-3.2.0 → easyfea-3.2.1}/setup.cfg +0 -0
|
@@ -402,9 +402,9 @@ def __AdaptiveTimeQuadratureStressTensor(
|
|
|
402
402
|
) -> tuple["FeArray", "FeArray", int]:
|
|
403
403
|
r"""Per-element adaptive strain-path quadrature — the ``tol``-driven path of :func:`TimeQuadratureStressTensor`.
|
|
404
404
|
|
|
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
|
|
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
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
|
|
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.
|
|
408
408
|
"""
|
|
409
409
|
groupElem = state_n.groupElem # state_n and state_np1 share the group
|
|
410
410
|
dim = groupElem.dim
|
|
@@ -417,7 +417,7 @@ def __AdaptiveTimeQuadratureStressTensor(
|
|
|
417
417
|
dW = material.Compute_W(state_np1) - material.Compute_W(state_n) # ΔW per point
|
|
418
418
|
dE = state_np1.Compute_GreenLagrange() - state_n.Compute_GreenLagrange()
|
|
419
419
|
dE_vec = Project_matrix_to_vector(dE[..., :dim, :dim]) # Δe (Kelvin-Mandel)
|
|
420
|
-
refW = einsum("ep,ep
|
|
420
|
+
refW = einsum("ep,ep->e", wJ_e_pg, np.abs(dW)) # Σ_p V |ΔW| (Ne,)
|
|
421
421
|
Ne, nPg, ncomp = *wJ_e_pg.shape[:2], dE_vec.shape[-1]
|
|
422
422
|
|
|
423
423
|
def at(
|
|
@@ -428,7 +428,8 @@ def __AdaptiveTimeQuadratureStressTensor(
|
|
|
428
428
|
dWde_quad = FeArray.zeros(Ne, nPg, ncomp)
|
|
429
429
|
d2Wde_quad = FeArray.zeros(Ne, nPg, ncomp, ncomp)
|
|
430
430
|
activeElements = np.arange(Ne) # elements still refining
|
|
431
|
-
|
|
431
|
+
nPts_e = np.zeros(Ne, dtype=int) # accepted point count per element (diagnostic)
|
|
432
|
+
nPts = 1
|
|
432
433
|
while activeElements.size:
|
|
433
434
|
nodes, weights = __clenshaw_curtis(nPts)
|
|
434
435
|
S = sum(
|
|
@@ -441,27 +442,27 @@ def __AdaptiveTimeQuadratureStressTensor(
|
|
|
441
442
|
defect = einsum("epi,epi->ep", S, dE_vec[activeElements]) - dW[activeElements]
|
|
442
443
|
next_nPts = 3 if nPts == 1 else 2 * nPts - 1 # next level in the chain
|
|
443
444
|
# accept an element once its own energy defect is within tol (all of them at the last level)
|
|
444
|
-
|
|
445
|
-
einsum("ep,ep
|
|
446
|
-
<= tol *
|
|
445
|
+
isAccepted = (next_nPts > max(maxPoints, 1)) | (
|
|
446
|
+
einsum("ep,ep->e", wJ_e_pg[activeElements], np.abs(defect))
|
|
447
|
+
<= tol * refW[activeElements]
|
|
447
448
|
)
|
|
448
|
-
if
|
|
449
|
-
|
|
450
|
-
dWde_quad[
|
|
449
|
+
if isAccepted.any():
|
|
450
|
+
acceptedElems = activeElements[isAccepted] # elements accepting this rule
|
|
451
|
+
dWde_quad[acceptedElems] = S[isAccepted]
|
|
451
452
|
# their tangent only: Σ_k 2 w_k s_k d2Wde, s=0 drops out (∂e/∂u = s B)
|
|
452
|
-
d2Wde_quad[
|
|
453
|
+
d2Wde_quad[acceptedElems] = sum(
|
|
453
454
|
(
|
|
454
|
-
(2.0 * w * s) * material.Compute_d2Wde(at(s,
|
|
455
|
+
(2.0 * w * s) * material.Compute_d2Wde(at(s, acceptedElems))
|
|
455
456
|
for s, w in zip(nodes, weights)
|
|
456
457
|
if s
|
|
457
458
|
),
|
|
458
459
|
0.0,
|
|
459
460
|
)
|
|
460
|
-
|
|
461
|
-
activeElements = activeElements[~
|
|
461
|
+
nPts_e[acceptedElems] = nPts # record each frozen element's accepted rule
|
|
462
|
+
activeElements = activeElements[~isAccepted]
|
|
462
463
|
nPts = next_nPts
|
|
463
464
|
|
|
464
|
-
return dWde_quad, d2Wde_quad,
|
|
465
|
+
return dWde_quad, d2Wde_quad, nPts_e
|
|
465
466
|
|
|
466
467
|
|
|
467
468
|
def TimeQuadratureStressTensor(
|
|
@@ -484,7 +485,7 @@ def TimeQuadratureStressTensor(
|
|
|
484
485
|
|
|
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.
|
|
486
487
|
|
|
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
|
|
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.
|
|
488
489
|
|
|
489
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`.
|
|
490
491
|
|
|
@@ -518,9 +519,9 @@ def TimeQuadratureStressTensor(
|
|
|
518
519
|
Consistent tangent, built for ``coefK = 0.5``.
|
|
519
520
|
R_e : ndarray of shape ``(Ne, nPe·dim)``
|
|
520
521
|
Internal residual force.
|
|
521
|
-
|
|
522
|
-
Clenshaw-Curtis points used — ``nPoints`` when fixed
|
|
523
|
-
|
|
522
|
+
nPts_e : ndarray of shape ``(Ne,)``
|
|
523
|
+
Clenshaw-Curtis points each element used — constant ``nPoints`` when fixed, per-element
|
|
524
|
+
when adaptive.
|
|
524
525
|
"""
|
|
525
526
|
|
|
526
527
|
groupElem = state_mid.groupElem
|
|
@@ -547,7 +548,7 @@ def TimeQuadratureStressTensor(
|
|
|
547
548
|
dWde_quad += w * material.Compute_dWde(state)
|
|
548
549
|
if s != 0.0:
|
|
549
550
|
d2Wde_quad += (2.0 * w * s) * material.Compute_d2Wde(state)
|
|
550
|
-
|
|
551
|
+
nPts_e = np.full(groupElem.Ne, int(nPoints)) # every element uses the same rule
|
|
551
552
|
else:
|
|
552
553
|
# Adaptive: refine per element on the *energy defect*. This stress exists so that
|
|
553
554
|
# S_quad : Δe = ΔW exactly (a discrete gradient, for energy conservation); the only error
|
|
@@ -555,7 +556,7 @@ def TimeQuadratureStressTensor(
|
|
|
555
556
|
# is *known* from the endpoints — so the test is absolute (each level scored on its own).
|
|
556
557
|
# Refining element-by-element rather than the whole block spends points only where the
|
|
557
558
|
# material is nonlinear over the step (see __AdaptiveTimeQuadratureStressTensor).
|
|
558
|
-
dWde_quad, d2Wde_quad,
|
|
559
|
+
dWde_quad, d2Wde_quad, nPts_e = __AdaptiveTimeQuadratureStressTensor(
|
|
559
560
|
material, state_n, state_np1, tol, int(maxPoints)
|
|
560
561
|
)
|
|
561
562
|
|
|
@@ -572,7 +573,7 @@ def TimeQuadratureStressTensor(
|
|
|
572
573
|
residual_e *= thickness
|
|
573
574
|
|
|
574
575
|
K_e, R_e = __reorder_dofs(dim, nPe, tangent_e, residual_e)
|
|
575
|
-
return K_e, R_e,
|
|
576
|
+
return K_e, R_e, nPts_e
|
|
576
577
|
|
|
577
578
|
|
|
578
579
|
def ActiveStressTensor(
|
|
@@ -628,17 +629,20 @@ def KelvinVoigtDamping(
|
|
|
628
629
|
material: "_HyperElastic",
|
|
629
630
|
state: "HyperElasticState",
|
|
630
631
|
velocity: np.ndarray,
|
|
631
|
-
) -> tuple[np.ndarray, np.ndarray]:
|
|
632
|
-
r"""Kelvin–Voigt viscous element contributions (
|
|
632
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray]:
|
|
633
|
+
r"""Kelvin–Voigt viscous element contributions (Kgeo_e, R_e, C_e) for the
|
|
633
634
|
large-strain viscous force F_visco(u) = C(u)·v, with Σ_visco = η·Ė
|
|
634
635
|
(Green-Lagrange strain rate of velocity) and B = De(u)·grad.
|
|
635
636
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
and the coefC·C tangent).
|
|
637
|
+
Ordered ``(tangent, residual, extra)`` like the rest of this module.
|
|
638
|
+
|
|
639
639
|
- Kgeo_e — the configuration tangent ∂(C·v)/∂u at fixed velocity (geometric
|
|
640
640
|
stiffening from Σ_visco plus the ∂Ė/∂u term); the simulation adds it to
|
|
641
641
|
K_e so it rides coefK.
|
|
642
|
+
- R_e = thickness · η · ∫ Bᵀ Ė dΩ — the viscous residual, which the simulation
|
|
643
|
+
subtracts from F_e.
|
|
644
|
+
- C_e = thickness · η · ∫ Bᵀ B dΩ — the damping matrix; the simulation puts it
|
|
645
|
+
in slot 2 of (K, C, M, F), where it rides the coefC·C tangent.
|
|
642
646
|
|
|
643
647
|
Parameters
|
|
644
648
|
----------
|
|
@@ -653,11 +657,12 @@ def KelvinVoigtDamping(
|
|
|
653
657
|
Returns
|
|
654
658
|
-------
|
|
655
659
|
tuple
|
|
656
|
-
(None, None) when material.eta == 0 or velocity is None.
|
|
657
|
-
are (Ne, nPe·dim, nPe·dim)
|
|
660
|
+
(None, None, None) when material.eta == 0 or velocity is None. Kgeo_e and C_e
|
|
661
|
+
are (Ne, nPe·dim, nPe·dim) and R_e is (Ne, nPe·dim), all reordered to
|
|
662
|
+
(xi, yi, zi, ..., xn, yn, zn).
|
|
658
663
|
"""
|
|
659
664
|
if material.eta == 0.0 or velocity is None:
|
|
660
|
-
return None, None # type: ignore [return-value]
|
|
665
|
+
return None, None, None # type: ignore [return-value]
|
|
661
666
|
|
|
662
667
|
groupElem = state.groupElem
|
|
663
668
|
matrixType = state.matrixType
|
|
@@ -668,22 +673,22 @@ def KelvinVoigtDamping(
|
|
|
668
673
|
|
|
669
674
|
grad_e_pg, B_e_pg = __block_grad_B(state)
|
|
670
675
|
Beta_e_pg = state.Compute_Deta(velocity) @ grad_e_pg
|
|
676
|
+
sig_e_pg = material.eta * state.Compute_Edot_vec(velocity) # Σ_visco = η·Ė
|
|
671
677
|
|
|
672
678
|
# damping matrix C = thickness · η · ∫ Bᵀ B (fused einsum, see SPK above)
|
|
673
679
|
subscripts = "ep,epji,epjl->eil"
|
|
674
680
|
C_e = thickness * material.eta * einsum(subscripts, wJ_e_pg, B_e_pg, B_e_pg)
|
|
675
681
|
|
|
682
|
+
# viscous residual ∫ Bᵀ Σ_visco — same contraction as the active stress
|
|
683
|
+
residual_e = thickness * einsum("ep,epji,epj->ei", wJ_e_pg, B_e_pg, sig_e_pg)
|
|
684
|
+
|
|
676
685
|
# configuration tangent ∂(C·v)/∂u = geometric (∫ gradᵀ Sig grad) + material-like
|
|
677
686
|
# (η ∫ Bᵀ (∂Ė/∂u)) pieces
|
|
678
687
|
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
|
-
)
|
|
688
|
+
A_geo = __geometric_tangent(wJ_e_pg, state, sig_e_pg)
|
|
684
689
|
Kgeo_e = thickness * (A_mat + A_geo)
|
|
685
690
|
|
|
686
|
-
return __reorder_dofs(dim, nPe,
|
|
691
|
+
return __reorder_dofs(dim, nPe, Kgeo_e, residual_e, C_e)
|
|
687
692
|
|
|
688
693
|
|
|
689
694
|
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
|
-
|
|
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
|
|
|
@@ -192,18 +193,18 @@ class HyperElastic(_Simu):
|
|
|
192
193
|
nPoints : int, optional
|
|
193
194
|
``quadrature`` only: number of Clenshaw-Curtis points, by default 3 (Simpson).
|
|
194
195
|
``1`` and ``2`` are the midpoint and trapezoid rules; more converges spectrally.
|
|
195
|
-
When ``
|
|
196
|
+
When ``energyTol`` is set this is the starting (minimum) level instead of a fixed count.
|
|
196
197
|
useConsistentTangent : bool, optional
|
|
197
198
|
``gonzalez`` only: if False, drop the discrete-gradient corrections from the
|
|
198
199
|
tangent. Same residual and same exact conservation, but Newton converges linearly
|
|
199
200
|
— a diagnostic, to measure what the consistent tangent is worth.
|
|
200
|
-
|
|
201
|
+
energyTol : float, optional
|
|
201
202
|
``quadrature`` only: if set, the rule is refined adaptively *element by element*
|
|
202
203
|
along the nested Clenshaw-Curtis chain ``1, 3, 5, 9, ...`` (capped at 33) — each
|
|
203
204
|
element stops once its own **integrated** relative energy defect
|
|
204
|
-
|
|
205
|
+
``∫|S_quad:Δe − ΔW| dΩ ≤ energyTol · ∫|ΔW| dΩ`` over that element is met (a volume
|
|
205
206
|
integral over its Gauss points, not a pointwise density). So energy is conserved to
|
|
206
|
-
``
|
|
207
|
+
``energyTol`` while points are spent only where the step is nonlinear. ``None``
|
|
207
208
|
(default) keeps the fixed ``nPoints`` rule.
|
|
208
209
|
"""
|
|
209
210
|
stressType = HyperElastic.StressType(stressType)
|
|
@@ -216,13 +217,14 @@ class HyperElastic(_Simu):
|
|
|
216
217
|
)
|
|
217
218
|
assert nPoints >= 1, f"nPoints must be >= 1 (got {nPoints})."
|
|
218
219
|
|
|
219
|
-
self.__stressParams = (stressType, nPoints, useConsistentTangent,
|
|
220
|
-
|
|
220
|
+
self.__stressParams = (stressType, nPoints, useConsistentTangent, energyTol)
|
|
221
|
+
# diagnostic: per-element quadrature-point counts, last assembly
|
|
222
|
+
self.__nPts_e = None
|
|
221
223
|
|
|
222
224
|
def __Solver_Get_Stress_Params(
|
|
223
225
|
self,
|
|
224
226
|
) -> tuple["HyperElastic.StressType", int, bool, Optional[float]]:
|
|
225
|
-
"""Returns (stressType, nPoints, useConsistentTangent,
|
|
227
|
+
"""Returns (stressType, nPoints, useConsistentTangent, energyTol) internal-force props."""
|
|
226
228
|
return self.__stressParams
|
|
227
229
|
|
|
228
230
|
@property
|
|
@@ -230,6 +232,11 @@ class HyperElastic(_Simu):
|
|
|
230
232
|
"""Stress used by the internal force — see :py:meth:`Solver_Set_Stress`."""
|
|
231
233
|
return self.__Solver_Get_Stress_Params()[0]
|
|
232
234
|
|
|
235
|
+
@cache_computed_values
|
|
236
|
+
def __Mass_e(self, groupElem, rho, thickness, dim):
|
|
237
|
+
"""Constant element mass matrix ``thickness · ∫ρ N·N``."""
|
|
238
|
+
return thickness * Operators.Bilinear.UV(groupElem, rho, dof_n=dim)
|
|
239
|
+
|
|
233
240
|
def Construct_local_matrix_system(
|
|
234
241
|
self,
|
|
235
242
|
problemType,
|
|
@@ -237,22 +244,22 @@ class HyperElastic(_Simu):
|
|
|
237
244
|
):
|
|
238
245
|
"""Returns ``{groupElem: (K_e, C_e, M_e, F_e)}`` for the current Newton iterate.
|
|
239
246
|
|
|
240
|
-
Newton solves ``A(u)·Δu = -R(u)
|
|
247
|
+
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
248
|
"""
|
|
242
249
|
dim = self.dim
|
|
243
250
|
thickness = self.material.thickness if dim == 2 else 1
|
|
244
251
|
isDynamic = self.algo in AlgoType.Get_Hyperbolic_Types()
|
|
245
252
|
|
|
246
|
-
# current Newton-Raphson iterate; for dynamic schemes also capture the
|
|
247
|
-
#
|
|
253
|
+
# current Newton-Raphson iterate; for dynamic schemes also capture the velocity
|
|
254
|
+
# (Kelvin–Voigt viscosity) and the acceleration (inertia), both needed by F_e.
|
|
248
255
|
displacement = self._Solver_Get_Newton_Raphson_current_solution()
|
|
249
|
-
velocity = None
|
|
256
|
+
velocity = accel = None
|
|
250
257
|
# Both non-default stresses are built from the step endpoints (u_n, u_{n+1}) on top
|
|
251
258
|
# of the midpoint base point, so both need u_{n+1} kept before the midpoint
|
|
252
259
|
# evaluation overwrites `displacement` with ū. Re-checked here (not only in the
|
|
253
260
|
# setter) so that re-calling Solver_Set_Hyperbolic_Algorithm with another algo
|
|
254
261
|
# can't leave a stale selection.
|
|
255
|
-
stressType, nPoints, useConsistentTangent,
|
|
262
|
+
stressType, nPoints, useConsistentTangent, energyTol = (
|
|
256
263
|
self.__Solver_Get_Stress_Params()
|
|
257
264
|
)
|
|
258
265
|
isPointwise = stressType == HyperElastic.StressType.pointwise
|
|
@@ -261,7 +268,7 @@ class HyperElastic(_Simu):
|
|
|
261
268
|
), f"the '{stressType}' stress requires AlgoType.midpoint (got {self.algo})."
|
|
262
269
|
u_np1 = displacement
|
|
263
270
|
if isDynamic:
|
|
264
|
-
displacement, velocity,
|
|
271
|
+
displacement, velocity, accel = self._Solver_Evaluate_u_v_a_for_time_scheme(
|
|
265
272
|
problemType, displacement
|
|
266
273
|
)
|
|
267
274
|
if not isPointwise:
|
|
@@ -270,7 +277,7 @@ class HyperElastic(_Simu):
|
|
|
270
277
|
errDetF = "det(F) < 0 - reduce load steps"
|
|
271
278
|
|
|
272
279
|
out = {}
|
|
273
|
-
|
|
280
|
+
list_nPts_e = [] # per-element point counts across groups (quadrature only)
|
|
274
281
|
for groupElem in self.mesh.Get_list_groupElem():
|
|
275
282
|
state = HyperElasticState(groupElem, displacement, matrixType)
|
|
276
283
|
|
|
@@ -298,15 +305,15 @@ class HyperElastic(_Simu):
|
|
|
298
305
|
useConsistentTangent,
|
|
299
306
|
)
|
|
300
307
|
elif stressType == HyperElastic.StressType.quadrature:
|
|
301
|
-
K_e, residual_e,
|
|
308
|
+
K_e, residual_e, nPts_e = (
|
|
302
309
|
Operators.NonLinear.TimeQuadratureStressTensor(
|
|
303
310
|
self.material,
|
|
304
311
|
*hyperElasticStates,
|
|
305
312
|
nPoints,
|
|
306
|
-
|
|
313
|
+
energyTol,
|
|
307
314
|
)
|
|
308
315
|
)
|
|
309
|
-
|
|
316
|
+
list_nPts_e.append(nPts_e)
|
|
310
317
|
else:
|
|
311
318
|
raise NotImplementedError
|
|
312
319
|
|
|
@@ -324,24 +331,41 @@ class HyperElastic(_Simu):
|
|
|
324
331
|
|
|
325
332
|
F_e = -residual_e
|
|
326
333
|
|
|
327
|
-
# Kelvin–Voigt viscosity: C_e is the damping matrix (slot 2, rides
|
|
328
|
-
#
|
|
329
|
-
#
|
|
334
|
+
# Kelvin–Voigt viscosity: C_e is the damping matrix (slot 2, so it rides coefC
|
|
335
|
+
# in A), R_visco_e its residual, and Kgeo_e the configuration tangent
|
|
336
|
+
# ∂(C·v)/∂u, added to K_e so it rides coefK.
|
|
330
337
|
C_e = None
|
|
331
338
|
if self.material.eta != 0 and velocity is not None:
|
|
332
|
-
|
|
339
|
+
Kgeo_e, R_visco_e, C_e = Operators.NonLinear.KelvinVoigtDamping(
|
|
333
340
|
self.material, state, velocity
|
|
334
341
|
)
|
|
335
342
|
K_e += Kgeo_e
|
|
343
|
+
F_e -= R_visco_e
|
|
336
344
|
|
|
337
|
-
# mass matrix
|
|
345
|
+
# mass matrix and the inertia residual — dynamic schemes only
|
|
338
346
|
M_e = None
|
|
339
347
|
if isDynamic:
|
|
340
|
-
M_e = thickness
|
|
348
|
+
# M_e = thickness · ∫ρ N·N is constant across the solve, so it is cached (see __Mass_e)
|
|
349
|
+
# instead of rebuilt every Newton iteration. An array ρ is unhashable, so it cannot key the
|
|
350
|
+
# cache and is recomputed directly.
|
|
351
|
+
if isinstance(self.rho, np.ndarray):
|
|
352
|
+
M_e = thickness * Operators.Bilinear.UV(
|
|
353
|
+
groupElem, self.rho, dof_n=dim
|
|
354
|
+
)
|
|
355
|
+
else:
|
|
356
|
+
M_e = self.__Mass_e(groupElem, self.rho, thickness, dim)
|
|
357
|
+
F_e -= np.einsum(
|
|
358
|
+
"eij,ej->ei",
|
|
359
|
+
M_e,
|
|
360
|
+
groupElem.Locates_sol_e(accel, dim),
|
|
361
|
+
optimize=True,
|
|
362
|
+
)
|
|
341
363
|
|
|
342
364
|
out[groupElem] = (K_e, C_e, M_e, F_e)
|
|
343
365
|
|
|
344
|
-
|
|
366
|
+
# per-element Clenshaw-Curtis point counts this assembly, or None if the quadrature
|
|
367
|
+
# stress did not run (pointwise / gonzalez)
|
|
368
|
+
self.__nPts_e = np.concatenate(list_nPts_e) if list_nPts_e else None
|
|
345
369
|
|
|
346
370
|
return out
|
|
347
371
|
|
|
@@ -358,10 +382,8 @@ class HyperElastic(_Simu):
|
|
|
358
382
|
if self.algo in AlgoType.Get_Hyperbolic_Types():
|
|
359
383
|
iter["speed"] = self._Get_v_n(self.problemType)
|
|
360
384
|
iter["accel"] = self._Get_a_n(self.problemType)
|
|
361
|
-
if
|
|
362
|
-
self.
|
|
363
|
-
): # points the converged step used (quadrature stress only)
|
|
364
|
-
iter["quadNPoints"] = self.__quadNPoints
|
|
385
|
+
if self.__nPts_e is not None: # per-element point counts, quadrature only
|
|
386
|
+
iter["nPts_e"] = self.__nPts_e
|
|
365
387
|
|
|
366
388
|
return super().Save_Iter(iter)
|
|
367
389
|
|
|
@@ -16,6 +16,7 @@ from ..__about__ import __version__
|
|
|
16
16
|
|
|
17
17
|
# utilities
|
|
18
18
|
from ..Utilities import Folder, Terminal, Tic, _params, _types
|
|
19
|
+
from ..Utilities._cache import cache_computed_values, clear_cached_computed_values
|
|
19
20
|
from ..Utilities._observers import Observable, _IObserver
|
|
20
21
|
from ..Utilities._mpi import CAN_USE_MPI, MPI_SIZE, MPI_RANK, MPI_COMM
|
|
21
22
|
|
|
@@ -220,7 +221,12 @@ class _Simu(_IObserver, _params.Updatable, ABC):
|
|
|
220
221
|
Optional[np.ndarray],
|
|
221
222
|
],
|
|
222
223
|
]:
|
|
223
|
-
r"""Construct the local matrix system :math:`\Krm \, \mathrm{u} + \Crm \, \vrm + \Mrm \, \arm = \Frm` for the given problem, returned per contributing group of elements `{groupElem: (K_e, C_e, M_e, F_e)}`.
|
|
224
|
+
r"""Construct the local matrix system :math:`\Krm \, \mathrm{u} + \Crm \, \vrm + \Mrm \, \arm = \Frm` for the given problem, returned per contributing group of elements `{groupElem: (K_e, C_e, M_e, F_e)}`.
|
|
225
|
+
|
|
226
|
+
For a **linear** problem :math:`\Frm` is the load alone: :py:meth:`_Solver_Apply_Neumann` moves :math:`\mathrm{u}^n, \vrm^n, \arm^n` to the right-hand side with the history terms of the active time scheme.
|
|
227
|
+
|
|
228
|
+
For a **nonlinear** problem the unknown is :math:`\Delta \mathrm{u}`, so :math:`\Frm_e` is the complete residual :math:`-\Rrm_e`, inertia and damping included (:math:`-\Crm_e \, \vrm_t - \Mrm_e \, \arm_t`, with :math:`\vrm_t, \arm_t` from :py:meth:`_Solver_Evaluate_u_v_a_for_time_scheme`).
|
|
229
|
+
"""
|
|
224
230
|
raise NotImplementedError
|
|
225
231
|
|
|
226
232
|
# Iterations
|
|
@@ -711,6 +717,11 @@ class _Simu(_IObserver, _params.Updatable, ABC):
|
|
|
711
717
|
self.__listMesh.append(mesh)
|
|
712
718
|
self.__mesh = mesh
|
|
713
719
|
|
|
720
|
+
# New connectivity invalidates the geometry-derived caches.
|
|
721
|
+
# This cannot live in `Need_Update`, which fires on every Newton
|
|
722
|
+
# iteration — exactly the case those caches exist to serve.
|
|
723
|
+
clear_cached_computed_values(self)
|
|
724
|
+
|
|
714
725
|
# The mesh changes, so the matrices must be reconstructed
|
|
715
726
|
self.Need_Update()
|
|
716
727
|
# Initialize boundary conditions
|
|
@@ -738,7 +749,7 @@ class _Simu(_IObserver, _params.Updatable, ABC):
|
|
|
738
749
|
if MPI_SIZE == 1:
|
|
739
750
|
return
|
|
740
751
|
|
|
741
|
-
list_mesh = []
|
|
752
|
+
list_mesh: list[Mesh] = []
|
|
742
753
|
for mesh in self.__listMesh:
|
|
743
754
|
if isinstance(mesh, str):
|
|
744
755
|
mesh = Load_Mesh(Folder.Join(self.folder, mesh))
|
|
@@ -750,6 +761,9 @@ class _Simu(_IObserver, _params.Updatable, ABC):
|
|
|
750
761
|
self.__listMesh[i] = mesh
|
|
751
762
|
self.__mesh = self.__listMesh[self.__indexMesh]
|
|
752
763
|
self.Bc_Init()
|
|
764
|
+
# rank 0 swaps its partition for the full global mesh — the geometry-derived caches describe the
|
|
765
|
+
# partition and no longer apply.
|
|
766
|
+
clear_cached_computed_values(self)
|
|
753
767
|
self.Need_Update()
|
|
754
768
|
self.__isGathered = True
|
|
755
769
|
|
|
@@ -774,6 +788,9 @@ class _Simu(_IObserver, _params.Updatable, ABC):
|
|
|
774
788
|
|
|
775
789
|
self.__mesh = mesh
|
|
776
790
|
|
|
791
|
+
# switching to another mesh in the history changes the connectivity
|
|
792
|
+
clear_cached_computed_values(self)
|
|
793
|
+
|
|
777
794
|
self.Need_Update() # need to reconstruct matrices
|
|
778
795
|
|
|
779
796
|
def _Update(self, observable: Observable, event: str) -> None:
|
|
@@ -793,8 +810,8 @@ class _Simu(_IObserver, _params.Updatable, ABC):
|
|
|
793
810
|
# Solver
|
|
794
811
|
# ----------------------------------------------
|
|
795
812
|
|
|
796
|
-
@staticmethod
|
|
797
813
|
def __Assemble_csr(
|
|
814
|
+
self,
|
|
798
815
|
dict_group_data: dict["_GroupElem", np.ndarray],
|
|
799
816
|
dof_n: int,
|
|
800
817
|
Ndof: int,
|
|
@@ -802,6 +819,23 @@ class _Simu(_IObserver, _params.Updatable, ABC):
|
|
|
802
819
|
) -> sparse.csr_matrix:
|
|
803
820
|
"""Scatter per-group local element matrices/vectors into a global CSR.
|
|
804
821
|
|
|
822
|
+
A finite-element matrix has far more element-level entries than stored coefficients — each shared
|
|
823
|
+
node makes several element contributions land on the same ``(row, col)`` — so assembly is
|
|
824
|
+
fundamentally "sum every contribution into its slot". The element→DOF coordinates depend only on
|
|
825
|
+
connectivity, so across a Newton loop or time stepping they never change; only the values do. This
|
|
826
|
+
method exploits that: scipy builds and owns the sparsity pattern on the first assembly, and a small
|
|
827
|
+
precomputed map lets every later assembly do the summation with a single ``np.bincount``, skipping
|
|
828
|
+
scipy's repeated sort-and-sum-duplicates.
|
|
829
|
+
|
|
830
|
+
The map is cached per ``(dof_n, isMatrix, Ndof, contributing groups)``. The contributing groups are
|
|
831
|
+
part of the key because a simulation may feed different group sets to different slots — in the
|
|
832
|
+
cardiac benchmark the surface group contributes to K, C and F but leaves M ``None``, so K and M have
|
|
833
|
+
genuinely different patterns.
|
|
834
|
+
|
|
835
|
+
Reuse is exact to floating-point round-off (~1e-16 relative), not bit-identical to a fresh scipy
|
|
836
|
+
build, because ``bincount`` sums each slot's contributions in a different order than scipy's
|
|
837
|
+
coo->csr does.
|
|
838
|
+
|
|
805
839
|
Parameters
|
|
806
840
|
----------
|
|
807
841
|
dict_group_data : dict[_GroupElem, FeArray]
|
|
@@ -819,28 +853,77 @@ class _Simu(_IObserver, _params.Updatable, ABC):
|
|
|
819
853
|
if not dict_group_data:
|
|
820
854
|
return sparse.csr_matrix(shape)
|
|
821
855
|
|
|
822
|
-
|
|
823
|
-
for
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
if isMatrix:
|
|
827
|
-
rows = groupElem.Get_rows_e(dof_n).ravel()
|
|
828
|
-
cols = groupElem.Get_columns_e(dof_n).ravel()
|
|
829
|
-
else:
|
|
830
|
-
rows = groupElem.Get_assembly_e(dof_n).ravel()
|
|
831
|
-
cols = np.zeros_like(rows)
|
|
832
|
-
list_rows.append(rows)
|
|
833
|
-
list_cols.append(cols)
|
|
834
|
-
list_data.append(X_e.ravel())
|
|
856
|
+
# Fixing the group order here keeps `data` aligned with the map built from these same groups.
|
|
857
|
+
groups = tuple(g for g, X_e in dict_group_data.items() if X_e is not None)
|
|
858
|
+
if not groups:
|
|
859
|
+
return sparse.csr_matrix(shape)
|
|
835
860
|
|
|
836
|
-
|
|
861
|
+
data = np.concatenate([dict_group_data[g].ravel() for g in groups])
|
|
862
|
+
|
|
863
|
+
# An MPI rank may own no element of any contributing group.
|
|
864
|
+
if data.size == 0:
|
|
837
865
|
return sparse.csr_matrix(shape)
|
|
838
866
|
|
|
839
|
-
|
|
867
|
+
# The reduction map (which CSR slot each element entry feeds) is cached per key; it depends only on
|
|
868
|
+
# connectivity, so a Newton loop / time stepping reuses it and only the values below change.
|
|
869
|
+
inv, indices, indptr, nnz = self.__Get_csr_map(dof_n, isMatrix, Ndof, groups)
|
|
870
|
+
assert data.size == inv.size, f"Not enough data to fill a {shape} CSR."
|
|
871
|
+
|
|
872
|
+
# Sum every element entry into its slot with one bincount. `bincount` is real-only, so a complex
|
|
873
|
+
# system is summed as its real and imaginary parts — still one pass each, still the rare path.
|
|
874
|
+
if np.iscomplexobj(data):
|
|
875
|
+
csr_data = np.bincount(
|
|
876
|
+
inv, weights=data.real, minlength=nnz
|
|
877
|
+
) + 1j * np.bincount(inv, weights=data.imag, minlength=nnz)
|
|
878
|
+
else:
|
|
879
|
+
csr_data = np.bincount(inv, weights=data, minlength=nnz)
|
|
880
|
+
|
|
881
|
+
matrix = sparse.csr_matrix((csr_data, indices, indptr), shape=shape)
|
|
882
|
+
# Canonical by construction (scipy sorted the pattern): lets Solvers skip its canonical fixup.
|
|
883
|
+
matrix.has_canonical_format = True
|
|
884
|
+
return matrix
|
|
885
|
+
|
|
886
|
+
@cache_computed_values
|
|
887
|
+
def __Get_csr_map(self, dof_n, isMatrix, Ndof, groups):
|
|
888
|
+
"""Reduction map from element entries to global CSR slots for one assembly key.
|
|
889
|
+
|
|
890
|
+
Returns ``(inv, indices, indptr, nnz)``: ``inv[k]`` is the CSR-data slot that element entry ``k``
|
|
891
|
+
feeds, and ``(indices, indptr, nnz)`` is scipy's canonical pattern. Depends only on connectivity, so
|
|
892
|
+
``@cache_computed_values`` (keyed on ``dof_n, isMatrix, Ndof, groups``) reuses it across every
|
|
893
|
+
assembly of a key and rebuilds it when the groups / Ndof / dof_n change or the mesh is reset. ``inv``
|
|
894
|
+
is the single largest cached array; int32 halves it and is safe since slots are bounded by nnz < 2^31.
|
|
895
|
+
"""
|
|
896
|
+
shape = (Ndof, Ndof) if isMatrix else (Ndof, 1)
|
|
897
|
+
|
|
898
|
+
list_rows, list_cols = [], []
|
|
899
|
+
for groupElem in groups:
|
|
900
|
+
if isMatrix:
|
|
901
|
+
list_rows.append(groupElem.Get_rows_e(dof_n).ravel())
|
|
902
|
+
list_cols.append(groupElem.Get_columns_e(dof_n).ravel())
|
|
903
|
+
else:
|
|
904
|
+
r = groupElem.Get_assembly_e(dof_n).ravel()
|
|
905
|
+
list_rows.append(r)
|
|
906
|
+
list_cols.append(np.zeros_like(r))
|
|
840
907
|
rows = np.concatenate(list_rows)
|
|
841
908
|
cols = np.concatenate(list_cols)
|
|
842
|
-
|
|
843
|
-
|
|
909
|
+
|
|
910
|
+
# scipy owns the canonical sparsity pattern; build it once from a structure-only matrix (the values
|
|
911
|
+
# are irrelevant — only the nonzero positions matter).
|
|
912
|
+
matrix = sparse.csr_matrix((np.ones(rows.size), (rows, cols)), shape=shape)
|
|
913
|
+
matrix.sort_indices()
|
|
914
|
+
|
|
915
|
+
# `canon` is the sorted linear (row-major) index of each stored coefficient; `searchsorted` maps each
|
|
916
|
+
# element entry to the slot whose `(row, col)` it matches. Building the map from scipy's own pattern
|
|
917
|
+
# (rather than a parallel hand-built one) is correct by construction, independent of any sort order.
|
|
918
|
+
ncol = Ndof if isMatrix else 1
|
|
919
|
+
canon = (
|
|
920
|
+
np.repeat(np.arange(shape[0]), np.diff(matrix.indptr)) * ncol
|
|
921
|
+
+ matrix.indices
|
|
922
|
+
)
|
|
923
|
+
inv = np.searchsorted(canon, rows.astype(np.int64) * ncol + cols).astype(
|
|
924
|
+
np.int32
|
|
925
|
+
)
|
|
926
|
+
return inv, matrix.indices, matrix.indptr, matrix.nnz
|
|
844
927
|
|
|
845
928
|
def Assembly(
|
|
846
929
|
self, problemType: ModelType
|
|
@@ -1464,96 +1547,79 @@ class _Simu(_IObserver, _params.Updatable, ABC):
|
|
|
1464
1547
|
|
|
1465
1548
|
b += F
|
|
1466
1549
|
|
|
1467
|
-
if
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
# get the current newton raphson solution (updated via u += delta_u)
|
|
1473
|
-
u = self._Solver_Get_Newton_Raphson_current_solution()
|
|
1474
|
-
|
|
1475
|
-
# here evaluate the vectors according to the time scheme
|
|
1476
|
-
_, v_t, a_t = self._Solver_Evaluate_u_v_a_for_time_scheme(problemType, u)
|
|
1477
|
-
|
|
1478
|
-
# add residual contributions in b
|
|
1479
|
-
if v_t is not None:
|
|
1480
|
-
b -= C @ sparse.csr_matrix(v_t.reshape(-1, 1))
|
|
1481
|
-
if a_t is not None:
|
|
1482
|
-
b -= M @ sparse.csr_matrix(a_t.reshape(-1, 1))
|
|
1483
|
-
# K(u) Δu = - R(u)
|
|
1484
|
-
# = - (F(u) - b)
|
|
1485
|
-
# = - F(u) + b
|
|
1486
|
-
|
|
1487
|
-
else:
|
|
1550
|
+
if self.isNonLinear:
|
|
1551
|
+
# The unknown is Δu and `F` is the complete residual -R(u), so b is already
|
|
1552
|
+
# built. The history terms below belong to a solve for u^{n+1}.
|
|
1553
|
+
pass
|
|
1488
1554
|
|
|
1489
|
-
|
|
1490
|
-
|
|
1555
|
+
elif algo == AlgoType.elliptic:
|
|
1556
|
+
pass
|
|
1491
1557
|
|
|
1492
|
-
|
|
1493
|
-
|
|
1558
|
+
elif algo == AlgoType.parabolic:
|
|
1559
|
+
dt, alpha = self.__Solver_Get_Parabolic_Params()
|
|
1494
1560
|
|
|
1495
|
-
|
|
1561
|
+
ut_np1 = u_n + (1 - alpha) * dt * v_n
|
|
1496
1562
|
|
|
1497
|
-
|
|
1563
|
+
b += 1 / (alpha * dt) * C @ ut_np1
|
|
1498
1564
|
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1565
|
+
elif algo == AlgoType.newmark:
|
|
1566
|
+
# same as hht in accel with alpha = 0
|
|
1567
|
+
dt, beta, gamma, _ = self.__Solver_Get_Hyperbolic_Params()
|
|
1502
1568
|
|
|
1503
|
-
|
|
1504
|
-
|
|
1569
|
+
ut_np1 = u_n + dt * v_n + dt**2 / 2 * (1 - 2 * beta) * a_n
|
|
1570
|
+
vt_np1 = v_n + dt * (1 - gamma) * a_n
|
|
1505
1571
|
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1572
|
+
# ut_np1
|
|
1573
|
+
coefC = gamma / (beta * dt)
|
|
1574
|
+
coefM = 1 / (beta * dt**2)
|
|
1575
|
+
b += (coefC * C + coefM * M) @ ut_np1
|
|
1510
1576
|
|
|
1511
|
-
|
|
1512
|
-
|
|
1577
|
+
# vt_np1
|
|
1578
|
+
b -= C @ vt_np1
|
|
1513
1579
|
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1580
|
+
elif algo == AlgoType.midpoint:
|
|
1581
|
+
# hht with alpha = 1/2, gamma = 1/2 and beta = 1/4
|
|
1582
|
+
dt = self.__Solver_Get_Hyperbolic_Params()[0]
|
|
1517
1583
|
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1584
|
+
# u_n
|
|
1585
|
+
coefM = 2 / dt**2
|
|
1586
|
+
coefC = 1 / dt
|
|
1587
|
+
b += (coefM * M + coefC * C - 1 / 2 * K) @ u_n
|
|
1522
1588
|
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1589
|
+
# v_n
|
|
1590
|
+
coefM = 2 / dt
|
|
1591
|
+
b += coefM * M @ v_n
|
|
1526
1592
|
|
|
1527
|
-
|
|
1528
|
-
|
|
1593
|
+
elif algo == AlgoType.hht:
|
|
1594
|
+
dt, beta, gamma, alpha = self.__Solver_Get_Hyperbolic_Params()
|
|
1529
1595
|
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1596
|
+
# u_n
|
|
1597
|
+
coefM = 1 / (beta * dt**2)
|
|
1598
|
+
coefC = gamma / (beta * dt)
|
|
1599
|
+
b -= ((alpha - 1) * (coefM * M + coefC * C) + alpha * K) @ u_n
|
|
1534
1600
|
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1601
|
+
# v_n
|
|
1602
|
+
coefM = (alpha - 1) / (beta * dt)
|
|
1603
|
+
coefC = (alpha - 1) * (gamma / beta) + 1
|
|
1604
|
+
b -= (coefM * M + coefC * C) @ v_n
|
|
1539
1605
|
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1606
|
+
# a_n
|
|
1607
|
+
coefM = (alpha - 1) / (2 * beta) + 1
|
|
1608
|
+
coefC = dt * (alpha - 1) * (gamma / (2 * beta) - 1)
|
|
1609
|
+
b -= (coefM * M + coefC * C) @ a_n
|
|
1544
1610
|
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1611
|
+
elif algo == AlgoType.euler_implicit:
|
|
1612
|
+
dt = self.__Solver_Get_Hyperbolic_Params()[0]
|
|
1613
|
+
b += (1 / dt**2 * M + 1 / dt * C) @ u_n
|
|
1614
|
+
b += (1 / dt * M) @ v_n
|
|
1549
1615
|
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1616
|
+
elif algo == AlgoType.euler_explicit:
|
|
1617
|
+
# b = F^n - K u^n - C v^n (A = M, solving for a^n)
|
|
1618
|
+
b -= K @ u_n
|
|
1619
|
+
b -= C @ v_n
|
|
1554
1620
|
|
|
1555
|
-
|
|
1556
|
-
|
|
1621
|
+
else:
|
|
1622
|
+
raise NotImplementedError(f"Algo {algo} is not implemented here.")
|
|
1557
1623
|
|
|
1558
1624
|
tic.Tac("Solver", f"Neumann ({problemType}, {algo})", self._verbosity)
|
|
1559
1625
|
|
|
@@ -63,6 +63,7 @@ def Plot(
|
|
|
63
63
|
show_grid=False,
|
|
64
64
|
colorbarTitle=None,
|
|
65
65
|
verticalColobar=True,
|
|
66
|
+
scalar_bar_kwargs: Optional[dict] = None,
|
|
66
67
|
**kwargs,
|
|
67
68
|
):
|
|
68
69
|
"""Plots the object obj that can be either a simu, mesh, MultiBlock, PolyData.\n
|
|
@@ -111,6 +112,15 @@ def Plot(
|
|
|
111
112
|
colorbar title, by default None
|
|
112
113
|
verticalColobar : bool, optionnal
|
|
113
114
|
color bar is vertical, by default True
|
|
115
|
+
scalar_bar_kwargs : dict, optionnal
|
|
116
|
+
Extra scalar-bar options, merged over the ones built from ``colorbarTitle`` and ``verticalColobar`` and taking precedence over them, by default None.
|
|
117
|
+
Useful when the defaults are unreadable — a horizontal bar in a narrow subplot needs ``n_labels`` and ``fmt`` to stop the ticks overlapping::
|
|
118
|
+
|
|
119
|
+
PyVista.Plot(simu, "uz", verticalColobar=False,
|
|
120
|
+
scalar_bar_kwargs={"n_labels": 3, "fmt": "%.1e"})
|
|
121
|
+
|
|
122
|
+
Note that pyvista keys scalar bars by title, so several bars in one figure need distinct ``title`` entries.
|
|
123
|
+
Everything accepted by https://docs.pyvista.org/api/plotting/_autosummary/pyvista.plotter.add_scalar_bar
|
|
114
124
|
**kwargs:
|
|
115
125
|
Everything that can goes in add_mesh function https://docs.pyvista.org/version/stable/api/plotting/_autosummary/pyvista.Plotter.add_mesh.html#pyvista.Plotter.add_mesh
|
|
116
126
|
|
|
@@ -172,6 +182,15 @@ def Plot(
|
|
|
172
182
|
pos = "position_y"
|
|
173
183
|
val = 0.025
|
|
174
184
|
|
|
185
|
+
# caller options win, so `scalar_bar_kwargs` can override the title, the orientation or the position as well as add to them
|
|
186
|
+
scalar_bar_args = {
|
|
187
|
+
"title": colorbarTitle,
|
|
188
|
+
"vertical": verticalColobar,
|
|
189
|
+
pos: val,
|
|
190
|
+
}
|
|
191
|
+
if scalar_bar_kwargs is not None:
|
|
192
|
+
scalar_bar_args.update(scalar_bar_kwargs)
|
|
193
|
+
|
|
175
194
|
# plot the mesh
|
|
176
195
|
if not isinstance(pvMesh, list):
|
|
177
196
|
pvMeshs = [pvMesh]
|
|
@@ -191,11 +210,7 @@ def Plot(
|
|
|
191
210
|
cmap=cmap,
|
|
192
211
|
n_colors=nColors,
|
|
193
212
|
clim=clim,
|
|
194
|
-
scalar_bar_args=
|
|
195
|
-
"title": colorbarTitle,
|
|
196
|
-
"vertical": verticalColobar,
|
|
197
|
-
pos: val,
|
|
198
|
-
},
|
|
213
|
+
scalar_bar_args=scalar_bar_args,
|
|
199
214
|
**kwargs,
|
|
200
215
|
)
|
|
201
216
|
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "easyfea"
|
|
7
|
-
version = "3.2.
|
|
7
|
+
version = "3.2.1"
|
|
8
8
|
description = "User-friendly Python library that simplifies finite element analysis."
|
|
9
9
|
authors = [{ name = "Matthieu Noel", email = "matthieu.noel7@gmail.com" }]
|
|
10
10
|
readme = "README.md"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|