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.
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Operators/Linear.py +17 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Operators/NonLinear.py +35 -34
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/_field.py +3 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/_group_elem.py +58 -53
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/_linalg.py +215 -105
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/_mesh.py +15 -7
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/_mesher.py +502 -234
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Geoms/__init__.py +3 -2
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Geoms/_circle.py +89 -19
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Geoms/_contour.py +4 -0
- easyfea-3.3.0/EasyFEA/Geoms/_domain.py +124 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Geoms/_geom.py +91 -19
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Geoms/_line.py +30 -5
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Geoms/_points.py +15 -5
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Geoms/_utils.py +42 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/Elastic/_laws.py +15 -45
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/HyperElastic/_laws.py +18 -3
- easyfea-3.3.0/EasyFEA/Models/IsotropicHardening.py +98 -0
- easyfea-3.3.0/EasyFEA/Models/KinematicHardening.py +95 -0
- easyfea-3.3.0/EasyFEA/Models/ViscoElastic.py +40 -0
- easyfea-3.3.0/EasyFEA/Models/ViscoPlastic.py +90 -0
- easyfea-3.3.0/EasyFEA/Models/Yield.py +188 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/__init__.py +8 -0
- easyfea-3.3.0/EasyFEA/Models/_behaviour.py +827 -0
- easyfea-3.3.0/EasyFEA/Models/_kelvin.py +36 -0
- easyfea-3.3.0/EasyFEA/Models/_materialpoint.py +139 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/_phasefield.py +20 -19
- easyfea-3.3.0/EasyFEA/Models/_spectral.py +181 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/_utils.py +5 -38
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/Solvers.py +0 -1
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/__init__.py +1 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/_beam.py +5 -5
- easyfea-3.3.0/EasyFEA/Simulations/_behaviour.py +340 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/_hyperelastic.py +83 -43
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/_phasefield.py +25 -21
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/_simu.py +198 -17
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/MeshIO.py +262 -69
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/Paraview.py +2 -3
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/_mpi.py +56 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/_params.py +17 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/_types.py +3 -0
- {easyfea-3.2.1/easyfea.egg-info → easyfea-3.3.0}/PKG-INFO +11 -10
- {easyfea-3.2.1 → easyfea-3.3.0}/README.md +10 -9
- {easyfea-3.2.1 → easyfea-3.3.0/easyfea.egg-info}/PKG-INFO +11 -10
- {easyfea-3.2.1 → easyfea-3.3.0}/easyfea.egg-info/SOURCES.txt +10 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/pyproject.toml +4 -2
- easyfea-3.2.1/EasyFEA/Geoms/_domain.py +0 -73
- {easyfea-3.2.1 → easyfea-3.3.0}/AUTHORS.md +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Elems/__init__.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Elems/_beam.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Elems/_hexa.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Elems/_point.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Elems/_prism.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Elems/_quad.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Elems/_seg.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Elems/_tetra.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Elems/_tri.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Operators/Bilinear.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/Operators/__init__.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/__init__.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/_boundary_conditions.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/_forms.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/_gauss.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/FEM/_utils.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/Beam/__init__.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/Beam/_beam.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/Elastic/__init__.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/HyperElastic/__init__.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/HyperElastic/_state.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/_thermal.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Models/_weakforms.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/_dic.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/_elastic.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/_thermal.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/_utils.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Simulations/_weakforms.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/Folder.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/GLTF.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/Matplotlib.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/PyVista.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/Terminal.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/USD.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/Vizir.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/__init__.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/_cache.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/_observers.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/_requires.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/Utilities/_tic.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/__about__.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/EasyFEA/__init__.py +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/LICENSE.txt +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/easyfea.egg-info/dependency_links.txt +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/easyfea.egg-info/requires.txt +0 -0
- {easyfea-3.2.1 → easyfea-3.3.0}/easyfea.egg-info/top_level.txt +0 -0
- {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
|
|
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 =
|
|
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
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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}
|
|
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 =
|
|
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
|
-
|
|
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,
|
|
504
|
-
Hyperelastic states at ``u_n``,
|
|
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
|
-
|
|
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
|
|
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 =
|
|
528
|
-
wJ_e_pg = groupElem.Get_weightedJacobian_e_pg(
|
|
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
|
-
_,
|
|
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 += (
|
|
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(
|
|
565
|
-
residual_e = einsum("ep,epi,epij->ej", wJ_e_pg, dWde_quad,
|
|
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,
|
|
568
|
-
) + __geometric_tangent(wJ_e_pg,
|
|
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
|
|
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
|
-
|
|
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
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2144
|
-
detectedElements_e = detectedElements_e[
|
|
2145
|
-
connect_e_n = connect_e_n[
|
|
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(
|
|
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
|
|