easyfea 3.2.2__tar.gz → 3.4.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.2 → easyfea-3.4.0}/EasyFEA/FEM/Operators/Linear.py +17 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/Operators/NonLinear.py +5 -3
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/_field.py +3 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/_group_elem.py +58 -53
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/_linalg.py +215 -105
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/_mesh.py +15 -7
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/_mesher.py +502 -234
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Geoms/__init__.py +3 -2
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Geoms/_circle.py +89 -19
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Geoms/_contour.py +4 -0
- easyfea-3.4.0/EasyFEA/Geoms/_domain.py +124 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Geoms/_geom.py +91 -19
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Geoms/_line.py +30 -5
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Geoms/_points.py +15 -5
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Geoms/_utils.py +42 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Models/Beam/_beam.py +13 -9
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Models/Elastic/_laws.py +15 -45
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Models/HyperElastic/_laws.py +18 -3
- easyfea-3.4.0/EasyFEA/Models/InElastic/IsotropicHardening.py +98 -0
- easyfea-3.4.0/EasyFEA/Models/InElastic/KinematicHardening.py +95 -0
- easyfea-3.4.0/EasyFEA/Models/InElastic/ViscoElastic.py +40 -0
- easyfea-3.4.0/EasyFEA/Models/InElastic/ViscoPlastic.py +90 -0
- easyfea-3.4.0/EasyFEA/Models/InElastic/Yield.py +188 -0
- easyfea-3.4.0/EasyFEA/Models/InElastic/__init__.py +15 -0
- easyfea-3.4.0/EasyFEA/Models/InElastic/_behavior.py +832 -0
- easyfea-3.4.0/EasyFEA/Models/InElastic/_materialpoint.py +139 -0
- easyfea-3.4.0/EasyFEA/Models/InElastic/_spectral.py +184 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Models/__init__.py +1 -0
- easyfea-3.4.0/EasyFEA/Models/_kelvin.py +36 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Models/_phasefield.py +20 -19
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Models/_utils.py +5 -38
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Simulations/Solvers.py +0 -1
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Simulations/__init__.py +1 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Simulations/_beam.py +5 -5
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Simulations/_elastic.py +1 -1
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Simulations/_hyperelastic.py +45 -25
- easyfea-3.4.0/EasyFEA/Simulations/_inelastic.py +340 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Simulations/_phasefield.py +14 -16
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Simulations/_simu.py +190 -14
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Simulations/_thermal.py +1 -1
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Utilities/MeshIO.py +262 -69
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Utilities/Paraview.py +2 -3
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Utilities/_mpi.py +56 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Utilities/_params.py +17 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Utilities/_types.py +3 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/PKG-INFO +11 -10
- {easyfea-3.2.2 → easyfea-3.4.0}/README.md +10 -9
- {easyfea-3.2.2 → easyfea-3.4.0}/easyfea.egg-info/PKG-INFO +11 -10
- {easyfea-3.2.2 → easyfea-3.4.0}/easyfea.egg-info/SOURCES.txt +11 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/pyproject.toml +4 -2
- easyfea-3.2.2/EasyFEA/Geoms/_domain.py +0 -73
- {easyfea-3.2.2 → easyfea-3.4.0}/AUTHORS.md +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/Elems/__init__.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/Elems/_beam.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/Elems/_hexa.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/Elems/_point.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/Elems/_prism.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/Elems/_quad.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/Elems/_seg.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/Elems/_tetra.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/Elems/_tri.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/Operators/Bilinear.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/Operators/__init__.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/__init__.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/_boundary_conditions.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/_forms.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/_gauss.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/FEM/_utils.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Models/Beam/__init__.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Models/Elastic/__init__.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Models/HyperElastic/__init__.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Models/HyperElastic/_state.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Models/_thermal.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Models/_weakforms.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Simulations/_dic.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Simulations/_utils.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Simulations/_weakforms.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Utilities/Folder.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Utilities/GLTF.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Utilities/Matplotlib.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Utilities/PyVista.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Utilities/Terminal.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Utilities/USD.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Utilities/Vizir.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Utilities/__init__.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Utilities/_cache.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Utilities/_observers.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Utilities/_requires.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/Utilities/_tic.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/__about__.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/EasyFEA/__init__.py +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/LICENSE.txt +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/easyfea.egg-info/dependency_links.txt +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/easyfea.egg-info/requires.txt +0 -0
- {easyfea-3.2.2 → easyfea-3.4.0}/easyfea.egg-info/top_level.txt +0 -0
- {easyfea-3.2.2 → easyfea-3.4.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()
|
|
@@ -438,7 +438,7 @@ def __AdaptiveTimeQuadratureStressTensor(
|
|
|
438
438
|
),
|
|
439
439
|
0.0,
|
|
440
440
|
)
|
|
441
|
-
defect =
|
|
441
|
+
defect = 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
444
|
isAccepted = (next_nPts > max(maxPoints, 1)) | (
|
|
@@ -600,9 +600,11 @@ def ActiveStressTensor(
|
|
|
600
600
|
Returns
|
|
601
601
|
-------
|
|
602
602
|
tuple
|
|
603
|
-
``(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)``.
|
|
604
604
|
"""
|
|
605
|
-
|
|
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):
|
|
606
608
|
return None, None # type: ignore [return-value]
|
|
607
609
|
|
|
608
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
|
|
|
@@ -11,10 +11,81 @@ from typing import Union, Optional, Iterable
|
|
|
11
11
|
from ..Utilities import _types
|
|
12
12
|
|
|
13
13
|
|
|
14
|
+
def _Evaluate(operand):
|
|
15
|
+
"""A Field evaluates to its finite element array; anything else is passed through."""
|
|
16
|
+
return operand() if getattr(operand, "_isFeField", False) else operand
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _Base(operand):
|
|
20
|
+
"""Drops the FeArray view, so numpy's own machinery handles the operand."""
|
|
21
|
+
if isinstance(operand, FeArray):
|
|
22
|
+
return operand.view(np.ndarray)
|
|
23
|
+
elif isinstance(operand, (list, tuple)):
|
|
24
|
+
return type(operand)(_Base(item) for item in operand)
|
|
25
|
+
else:
|
|
26
|
+
return operand
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _KeepsFeAxes(axis, ndim: int) -> bool:
|
|
30
|
+
"""True when a reduction consumes tensor axes only, so the (Ne, nPg) axes survive."""
|
|
31
|
+
if axis is None:
|
|
32
|
+
return False
|
|
33
|
+
axes = axis if isinstance(axis, tuple) else (axis,)
|
|
34
|
+
return all(a >= 2 if a >= 0 else a >= 2 - ndim for a in axes)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
_REDUCERS = frozenset(
|
|
38
|
+
{
|
|
39
|
+
np.sum,
|
|
40
|
+
np.prod,
|
|
41
|
+
np.mean,
|
|
42
|
+
np.std,
|
|
43
|
+
np.var,
|
|
44
|
+
np.median,
|
|
45
|
+
np.average,
|
|
46
|
+
np.max,
|
|
47
|
+
np.min,
|
|
48
|
+
np.amax,
|
|
49
|
+
np.amin,
|
|
50
|
+
np.all,
|
|
51
|
+
np.any,
|
|
52
|
+
np.argmax,
|
|
53
|
+
np.argmin,
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _FeShape(operands) -> tuple:
|
|
59
|
+
"""The (Ne, nPg) an operation runs at: the broadcast of its FeArray operands'."""
|
|
60
|
+
shapes = set()
|
|
61
|
+
stack = list(operands)
|
|
62
|
+
while stack:
|
|
63
|
+
operand = stack.pop()
|
|
64
|
+
if isinstance(operand, FeArray):
|
|
65
|
+
shapes.add(operand.shape[:2])
|
|
66
|
+
elif isinstance(operand, (list, tuple)):
|
|
67
|
+
stack.extend(operand)
|
|
68
|
+
if len(shapes) == 1:
|
|
69
|
+
return shapes.pop()
|
|
70
|
+
return np.broadcast_shapes(*shapes) if shapes else ()
|
|
71
|
+
|
|
72
|
+
|
|
14
73
|
class FeArray(np.ndarray):
|
|
15
74
|
"""Finite Element array.\n
|
|
16
75
|
|
|
17
76
|
FeArray is a Python class designed to optimize finite element simulations by leveraging NumPy arrays with a shape of `(Ne, nPg, ...)`. This structure enables vectorized operations, eliminating the need for slow loops over elements and integration points. By using np.einsum, it efficiently handles tensor computations, significantly improving performance and code clarity for finite element analyses.
|
|
77
|
+
|
|
78
|
+
Two rules govern how it mixes with other arrays:
|
|
79
|
+
|
|
80
|
+
- **Rank.** A FeArray's tensor rank is ``ndim - 2`` and a plain array's is its own ``ndim``
|
|
81
|
+
-- always, with no exception and nothing inferred from a shape coincidence. So a
|
|
82
|
+
``(Ne, nPg)`` FeArray is a scalar field even where ``Ne`` and ``nPg`` match a tensor's
|
|
83
|
+
dimensions, and a plain array is a constant tensor held at every Gauss point. Fields are
|
|
84
|
+
padded to the widest rank, then broadcast once. A plain array that is really a field must
|
|
85
|
+
say so with :meth:`asfearray`, or it multiplies out as a constant tensor.
|
|
86
|
+
- **Type.** An operation stays a FeArray exactly when the ``(Ne, nPg)`` axes come out
|
|
87
|
+
unchanged. ``np.einsum``, ``np.where`` and ``np.linalg.solve`` keep them; ``reshape``,
|
|
88
|
+
``ravel`` and a sum over elements do not.
|
|
18
89
|
"""
|
|
19
90
|
|
|
20
91
|
FeArrayALike = Union["FeArray", _types.AnyArray]
|
|
@@ -33,110 +104,134 @@ class FeArray(np.ndarray):
|
|
|
33
104
|
if obj is None:
|
|
34
105
|
return
|
|
35
106
|
|
|
107
|
+
def __check_fe_dims(self) -> None:
|
|
108
|
+
"""A FeArray must keep its leading (Ne, nPg) axes.
|
|
109
|
+
|
|
110
|
+
``__new__`` enforces that, but indexing and reshaping do not go through it: ``fe[0]``
|
|
111
|
+
returns a FeArray of one dimension, whose finite element rank would be -1. Nothing
|
|
112
|
+
downstream expects that, so it is caught here rather than surfacing later as a
|
|
113
|
+
confusing shape error.
|
|
114
|
+
"""
|
|
115
|
+
if self.ndim < 2:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"this FeArray has shape {self.shape}, which has lost the leading "
|
|
118
|
+
"(Ne, nPg) axes -- indexing or reshaping dropped them. Use np.asarray(...) "
|
|
119
|
+
"if plain array semantics are what is wanted here."
|
|
120
|
+
)
|
|
121
|
+
|
|
36
122
|
@property
|
|
37
123
|
def _shape(self) -> tuple:
|
|
38
124
|
"""finite element shape"""
|
|
125
|
+
self.__check_fe_dims()
|
|
39
126
|
return self.shape[2:]
|
|
40
127
|
|
|
41
128
|
@property
|
|
42
129
|
def _ndim(self) -> int:
|
|
43
130
|
"""finite element ndim"""
|
|
131
|
+
self.__check_fe_dims()
|
|
44
132
|
return self.ndim - 2
|
|
45
133
|
|
|
46
|
-
@
|
|
47
|
-
def
|
|
48
|
-
"""
|
|
49
|
-
see https://numpy.org/doc/stable/reference/generated/numpy.einsum.html
|
|
50
|
-
"""
|
|
51
|
-
if self._ndim == 0:
|
|
52
|
-
return ""
|
|
53
|
-
elif self._ndim == 1:
|
|
54
|
-
return "i"
|
|
55
|
-
elif self._ndim == 2:
|
|
56
|
-
return "ij"
|
|
57
|
-
elif self._ndim == 4:
|
|
58
|
-
return "ijkl"
|
|
59
|
-
else:
|
|
60
|
-
raise ValueError("wrong dimension")
|
|
61
|
-
|
|
62
|
-
@property
|
|
63
|
-
def _type(self) -> str:
|
|
64
|
-
if self._ndim == 0:
|
|
65
|
-
return "scalar"
|
|
66
|
-
elif self._ndim == 1:
|
|
67
|
-
return "vector"
|
|
68
|
-
elif self._ndim == 2:
|
|
69
|
-
return "matrix"
|
|
70
|
-
elif self._ndim == 4:
|
|
71
|
-
return "tensor"
|
|
72
|
-
else:
|
|
73
|
-
raise ValueError("wrong dimension")
|
|
74
|
-
|
|
75
|
-
def __get_array1_array2(self, other) -> tuple[_types.AnyArray, _types.AnyArray]:
|
|
76
|
-
array1 = np.asarray(self)
|
|
77
|
-
ndim1 = self._ndim
|
|
78
|
-
shape1 = self._shape
|
|
134
|
+
@staticmethod
|
|
135
|
+
def _align(operands: tuple) -> tuple:
|
|
136
|
+
"""Pads each field's tensor rank up to the widest, so one numpy broadcast is correct.
|
|
79
137
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
other: FeArray = other() # type: ignore [no-redef]
|
|
89
|
-
array2 = np.asarray(other)
|
|
90
|
-
ndim2 = other._ndim
|
|
91
|
-
shape2 = other._shape
|
|
138
|
+
The finite element axes then line up on the left and the tensor axes on the right,
|
|
139
|
+
which is numpy's own rule.
|
|
140
|
+
"""
|
|
141
|
+
# by far the commonest case: all fields of the same shape, nothing to line up
|
|
142
|
+
shape = operands[0].shape if isinstance(operands[0], FeArray) else None
|
|
143
|
+
for op in operands:
|
|
144
|
+
if not isinstance(op, FeArray) or op.shape != shape:
|
|
145
|
+
break
|
|
92
146
|
else:
|
|
93
|
-
|
|
147
|
+
return operands
|
|
94
148
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
# array1(Ne, nPg, ...) array2(Ne, nPg) => (Ne, nPg, ...)
|
|
106
|
-
array2 = array2[(Ellipsis,) + (np.newaxis,) * ndim1]
|
|
107
|
-
elif shape1 == shape2:
|
|
108
|
-
pass
|
|
109
|
-
else:
|
|
110
|
-
type_str = "FeArray" if isinstance(other, FeArray) else "np.array"
|
|
111
|
-
raise ValueError(
|
|
112
|
-
f"The {type_str} `other` with shape {shape2} must be either a {self.shape} np.array or a {shape1} FeArray."
|
|
149
|
+
operands = tuple(_Evaluate(op) for op in operands)
|
|
150
|
+
ranks = [
|
|
151
|
+
op.ndim - 2 if isinstance(op, FeArray) else np.ndim(op) for op in operands
|
|
152
|
+
]
|
|
153
|
+
nt = max(ranks)
|
|
154
|
+
return tuple(
|
|
155
|
+
(
|
|
156
|
+
op[(slice(None), slice(None)) + (None,) * (nt - rank)]
|
|
157
|
+
if isinstance(op, FeArray) and rank < nt
|
|
158
|
+
else op
|
|
113
159
|
)
|
|
160
|
+
for op, rank in zip(operands, ranks)
|
|
161
|
+
)
|
|
114
162
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if isinstance(
|
|
119
|
-
return
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
163
|
+
@staticmethod
|
|
164
|
+
def __wrap(res, feShape: tuple):
|
|
165
|
+
"""A result is a field exactly when it came out on the operation's (Ne, nPg) axes."""
|
|
166
|
+
if not isinstance(res, np.ndarray):
|
|
167
|
+
return res
|
|
168
|
+
elif res.ndim >= 2 and res.shape[:2] == feShape:
|
|
169
|
+
return res.view(FeArray)
|
|
170
|
+
else:
|
|
171
|
+
return np.asarray(res)
|
|
172
|
+
|
|
173
|
+
def __array_ufunc__(self, ufunc, method, *inputs, **kwargs):
|
|
174
|
+
# numpy asks that a subclass put all its override logic here rather than also defining
|
|
175
|
+
# __add__ and friends, so that the type hierarchy is decided in one place
|
|
176
|
+
elementwise = method == "__call__" and ufunc.signature is None
|
|
177
|
+
|
|
178
|
+
# two fields of the same shape need no alignment and no rewrapping decision; this is
|
|
179
|
+
# the overwhelming majority of calls, and it is what keeps small arrays cheap
|
|
180
|
+
if elementwise and not kwargs and len(inputs) == 2:
|
|
181
|
+
left, right = inputs
|
|
182
|
+
if (
|
|
183
|
+
type(left) is FeArray
|
|
184
|
+
and type(right) is FeArray
|
|
185
|
+
and left.shape == right.shape
|
|
186
|
+
):
|
|
187
|
+
return ufunc(left.view(np.ndarray), right.view(np.ndarray)).view(
|
|
188
|
+
FeArray
|
|
189
|
+
)
|
|
128
190
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
191
|
+
if elementwise:
|
|
192
|
+
inputs = FeArray._align(inputs)
|
|
193
|
+
|
|
194
|
+
# ndarray refuses to run a ufunc on a subclass that overrides __array_ufunc__, so hand
|
|
195
|
+
# it plain views -- of the `out` and `where` operands too, or the call comes straight
|
|
196
|
+
# back here -- and put the type back afterwards
|
|
197
|
+
out = kwargs.pop("out", None) if kwargs else None
|
|
198
|
+
if kwargs:
|
|
199
|
+
kwargs = {key: _Base(value) for key, value in kwargs.items()}
|
|
200
|
+
if out is not None:
|
|
201
|
+
kwargs["out"] = _Base(out)
|
|
202
|
+
|
|
203
|
+
args = [_Base(array) for array in inputs]
|
|
204
|
+
res = (
|
|
205
|
+
ufunc(*args, **kwargs)
|
|
206
|
+
if elementwise
|
|
207
|
+
else getattr(ufunc, method)(*args, **kwargs)
|
|
208
|
+
)
|
|
134
209
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
210
|
+
if out is not None:
|
|
211
|
+
return out[0] if len(out) == 1 else out
|
|
212
|
+
elif elementwise and type(res) is np.ndarray:
|
|
213
|
+
# broadcasting against a FeArray always keeps the (Ne, nPg) axes
|
|
214
|
+
return res.view(FeArray)
|
|
215
|
+
feShape = _FeShape(inputs)
|
|
216
|
+
if isinstance(res, tuple):
|
|
217
|
+
return tuple(FeArray.__wrap(array, feShape) for array in res)
|
|
218
|
+
return FeArray.__wrap(res, feShape)
|
|
219
|
+
|
|
220
|
+
def __array_function__(self, func, types, args, kwargs):
|
|
221
|
+
# numpy's own implementations broadcast the plain way and must keep doing so: einsum
|
|
222
|
+
# with optimize= reaches for np.multiply internally, which would otherwise come back
|
|
223
|
+
# through __array_ufunc__ and be aligned a second time
|
|
224
|
+
feShape = _FeShape(args) or _FeShape(kwargs.values())
|
|
225
|
+
# numpy calls a dispatched reduction on the stripped array, so the method wrapper never
|
|
226
|
+
# sees it and the axis has to be read here instead
|
|
227
|
+
if func in _REDUCERS:
|
|
228
|
+
axis = kwargs.get("axis", args[1] if len(args) > 1 else None)
|
|
229
|
+
if not _KeepsFeAxes(axis, np.ndim(args[0])):
|
|
230
|
+
feShape = ()
|
|
231
|
+
args = tuple(_Base(arg) for arg in args)
|
|
232
|
+
kwargs = {key: _Base(value) for key, value in kwargs.items()}
|
|
233
|
+
res = super().__array_function__(func, types, args, kwargs)
|
|
234
|
+
return FeArray.__wrap(res, feShape)
|
|
140
235
|
|
|
141
236
|
@property
|
|
142
237
|
def T(self) -> FeArrayALike: # type: ignore [override]
|
|
@@ -158,7 +253,7 @@ class FeArray(np.ndarray):
|
|
|
158
253
|
ndim2 = other._ndim
|
|
159
254
|
elif isinstance(other, np.ndarray):
|
|
160
255
|
ndim2 = other.ndim
|
|
161
|
-
elif
|
|
256
|
+
elif getattr(other, "_isFeField", False):
|
|
162
257
|
other: FeArray = other() # type: ignore [no-redef]
|
|
163
258
|
ndim2 = other._ndim
|
|
164
259
|
else:
|
|
@@ -204,7 +299,7 @@ class FeArray(np.ndarray):
|
|
|
204
299
|
ndim2 = other._ndim
|
|
205
300
|
elif isinstance(other, np.ndarray):
|
|
206
301
|
ndim2 = other.ndim
|
|
207
|
-
elif
|
|
302
|
+
elif getattr(other, "_isFeField", False):
|
|
208
303
|
other: FeArray = other() # type: ignore [no-redef]
|
|
209
304
|
ndim2 = other._ndim
|
|
210
305
|
else:
|
|
@@ -230,7 +325,7 @@ class FeArray(np.ndarray):
|
|
|
230
325
|
ndim2 = other._ndim
|
|
231
326
|
elif isinstance(other, np.ndarray):
|
|
232
327
|
ndim2 = other.ndim
|
|
233
|
-
elif
|
|
328
|
+
elif getattr(other, "_isFeField", False):
|
|
234
329
|
other: FeArray = other() # type: ignore [no-redef]
|
|
235
330
|
ndim2 = other._ndim
|
|
236
331
|
else:
|
|
@@ -245,17 +340,23 @@ class FeArray(np.ndarray):
|
|
|
245
340
|
|
|
246
341
|
return result.view(FeArray)
|
|
247
342
|
|
|
248
|
-
#
|
|
249
|
-
#
|
|
343
|
+
# A reduction over a tensor axis is still a field; one over elements or Gauss points is
|
|
344
|
+
# not. Which axes were consumed is read from `axis`, never guessed from the result shape.
|
|
250
345
|
def _make_reducer(_name: str):
|
|
251
346
|
_parent = getattr(np.ndarray, _name)
|
|
252
347
|
|
|
253
348
|
def _reducer(self, *args, **kwargs):
|
|
254
|
-
|
|
349
|
+
res = _parent(self, *args, **kwargs)
|
|
350
|
+
axis = kwargs.get("axis", args[0] if args else None)
|
|
351
|
+
if _KeepsFeAxes(axis, self.ndim) and getattr(res, "ndim", 0) >= 2:
|
|
352
|
+
return res.view(FeArray)
|
|
353
|
+
return np.asarray(res)
|
|
255
354
|
|
|
256
355
|
_reducer.__name__ = _name
|
|
257
356
|
_reducer.__qualname__ = f"FeArray.{_name}"
|
|
258
|
-
_reducer.__doc__ =
|
|
357
|
+
_reducer.__doc__ = (
|
|
358
|
+
f"``np.{_name}()`` wrapper — ``ndarray`` unless (Ne, nPg) survives."
|
|
359
|
+
)
|
|
259
360
|
return _reducer
|
|
260
361
|
|
|
261
362
|
for _name in (
|
|
@@ -276,12 +377,10 @@ class FeArray(np.ndarray):
|
|
|
276
377
|
del _name, _make_reducer
|
|
277
378
|
|
|
278
379
|
def reshape(self, *args, **kwargs):
|
|
279
|
-
Ne, nPg = self.shape[:2]
|
|
280
380
|
new = super().reshape(*args, **kwargs)
|
|
281
|
-
if new.shape[:2] ==
|
|
381
|
+
if self.ndim >= 2 and new.shape[:2] == self.shape[:2]:
|
|
282
382
|
return new
|
|
283
|
-
|
|
284
|
-
return np.asarray(new)
|
|
383
|
+
return np.asarray(new)
|
|
285
384
|
|
|
286
385
|
def integrate(self) -> np.ndarray:
|
|
287
386
|
"""Integrate over the Gauss-point axis (axis 1). Returns ``(Ne, ...)`` ndarray."""
|
|
@@ -310,15 +409,19 @@ class FeArray(np.ndarray):
|
|
|
310
409
|
self[tuple(idx)] = value
|
|
311
410
|
|
|
312
411
|
@staticmethod
|
|
313
|
-
def asfearray(array, broadcastFeArrays=False) ->
|
|
412
|
+
def asfearray(array, broadcastFeArrays=False) -> "FeArray":
|
|
413
|
+
"""Views ``array`` as a FeArray. Refuses anything without the (Ne, nPg) axes."""
|
|
314
414
|
if not isinstance(array, np.ndarray):
|
|
315
415
|
array = np.asarray(array)
|
|
316
416
|
if broadcastFeArrays:
|
|
317
417
|
return FeArray(array, broadcastFeArrays=broadcastFeArrays)
|
|
318
|
-
elif array.ndim
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
418
|
+
elif array.ndim < 2:
|
|
419
|
+
raise ValueError(
|
|
420
|
+
f"cannot view a {array.shape} array as a FeArray: it has no (Ne, nPg) axes. "
|
|
421
|
+
"Pass broadcastFeArrays=True to hold it at every Gauss point, or keep it a "
|
|
422
|
+
"plain array."
|
|
423
|
+
)
|
|
424
|
+
return array.view(FeArray)
|
|
322
425
|
|
|
323
426
|
@staticmethod
|
|
324
427
|
def broadcast(
|
|
@@ -380,13 +483,20 @@ class FeArray(np.ndarray):
|
|
|
380
483
|
for array in arrays
|
|
381
484
|
]
|
|
382
485
|
|
|
486
|
+
@staticmethod
|
|
487
|
+
def __shape(shape: tuple) -> tuple:
|
|
488
|
+
"""Accepts both ``zeros(Ne, nPg, 6)`` and ``zeros((Ne, nPg, 6))``."""
|
|
489
|
+
if len(shape) == 1 and isinstance(shape[0], (tuple, list)):
|
|
490
|
+
return tuple(shape[0])
|
|
491
|
+
return shape
|
|
492
|
+
|
|
383
493
|
@staticmethod
|
|
384
494
|
def zeros(*shape, dtype=None) -> FeArrayALike:
|
|
385
|
-
return FeArray.asfearray(np.zeros(shape
|
|
495
|
+
return FeArray.asfearray(np.zeros(FeArray.__shape(shape), dtype=dtype))
|
|
386
496
|
|
|
387
497
|
@staticmethod
|
|
388
498
|
def ones(*shape, dtype=None) -> FeArrayALike:
|
|
389
|
-
return FeArray.asfearray(np.ones(shape
|
|
499
|
+
return FeArray.asfearray(np.ones(FeArray.__shape(shape), dtype=dtype))
|
|
390
500
|
|
|
391
501
|
|
|
392
502
|
def __CheckMat(mat: FeArray.FeArrayALike) -> None:
|