PyHOPE 1.0.0__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. {pyhope-1.0.0 → pyhope-1.1.0}/PKG-INFO +13 -8
  2. {pyhope-1.0.0 → pyhope-1.1.0}/README.md +10 -6
  3. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/basis/basis_basis.py +77 -45
  4. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/basis/basis_connect.py +15 -9
  5. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/basis/basis_jacobian.py +28 -2
  6. pyhope-1.1.0/pyhope/basis/basis_orient.py +195 -0
  7. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/basis/basis_watertight.py +19 -13
  8. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/check/check_health.py +2 -2
  9. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/common/common_vars.py +37 -2
  10. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/gmsh/gmsh_install.py +90 -15
  11. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/io/io.py +1 -1
  12. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/io/io_debug.py +241 -142
  13. pyhope-1.1.0/pyhope/io/io_gmsh.py +69 -0
  14. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/connect/connect.py +13 -6
  15. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/extrude/mesh_extrude.py +21 -6
  16. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/mesh.py +1 -1
  17. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/mesh_sides.py +1 -1
  18. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/reader/reader_gmsh.py +3 -2
  19. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/transform/mesh_transform.py +1 -1
  20. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/transform/templates/cylinder.py +1 -1
  21. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/transform/templates/sphere.py +1 -1
  22. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/transform/templates/sphere_box.py +1 -1
  23. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/meshio/meshio_convert.py +2 -2
  24. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/script/pyhope_cli.py +6 -3
  25. {pyhope-1.0.0 → pyhope-1.1.0}/pyproject.toml +7 -4
  26. pyhope-1.0.0/pyhope/io/io_gmsh.py +0 -68
  27. pyhope-1.0.0/pyhope/mesh/mesh_orient.py +0 -196
  28. {pyhope-1.0.0 → pyhope-1.1.0}/AUTHORS.md +0 -0
  29. {pyhope-1.0.0 → pyhope-1.1.0}/LICENSE.md +0 -0
  30. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/__init__.py +0 -0
  31. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/__main__.py +0 -0
  32. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/check/check.py +0 -0
  33. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/check/check_install.py +0 -0
  34. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/check/check_unittest.py +0 -0
  35. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/common/common.py +0 -0
  36. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/common/common_git.py +0 -0
  37. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/common/common_numba.py +0 -0
  38. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/common/common_parallel.py +0 -0
  39. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/common/common_progress.py +0 -0
  40. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/common/common_template.py +0 -0
  41. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/common/common_tools.py +0 -0
  42. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/common/common_unique.py +0 -0
  43. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/config/config.py +0 -0
  44. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/gmsh/gmsh.py +0 -0
  45. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/gmsh/gmsh_override.py +0 -0
  46. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/io/formats/cgns.py +0 -0
  47. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/io/formats/gmsh.py +0 -0
  48. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/io/formats/meshio.py +0 -0
  49. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/io/formats/vtk.py +0 -0
  50. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/io/io_vars.py +0 -0
  51. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/io/io_xdmf.py +0 -0
  52. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/connect/connect_mortar.py +0 -0
  53. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/connect/connect_rbtree.py +0 -0
  54. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/elements/elements_ordering.py +0 -0
  55. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/elements/elements_shapefunctions.py +0 -0
  56. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/extrude/templates/linear.py +0 -0
  57. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/fem/fem.py +0 -0
  58. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/mesh_builtin.py +0 -0
  59. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/mesh_common.py +0 -0
  60. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/mesh_duplicates.py +0 -0
  61. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/mesh_external.py +0 -0
  62. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/mesh_mortar.py +0 -0
  63. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/mesh_sort.py +0 -0
  64. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/mesh_vars.py +0 -0
  65. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/reader/reader_gambit.py +0 -0
  66. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/reader/reader_hopr.py +0 -0
  67. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/sort/sort_hilbert.py +0 -0
  68. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/topology/mesh_serendipity.py +0 -0
  69. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/topology/mesh_splittohex.py +0 -0
  70. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/topology/mesh_topology.py +0 -0
  71. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/transform/templates/convtest.py +0 -0
  72. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/transform/templates/default.py +0 -0
  73. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/transform/templates/phill.py +0 -0
  74. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/mesh/transform/templates/sine.py +0 -0
  75. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/meshio/meshio_nodes.py +0 -0
  76. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/meshio/meshio_ordering.py +0 -0
  77. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/output/output.py +0 -0
  78. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/readintools/commandline.py +0 -0
  79. {pyhope-1.0.0 → pyhope-1.1.0}/pyhope/readintools/readintools.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: PyHOPE
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Python High-Order Preprocessing Environment
5
5
  License-Expression: GPL-3.0-only
6
6
  License-File: AUTHORS.md
@@ -28,7 +28,8 @@ Requires-Dist: meshio
28
28
  Requires-Dist: numba ; extra == "numba"
29
29
  Requires-Dist: numpy (>=2.0.0)
30
30
  Requires-Dist: packaging
31
- Requires-Dist: plotext
31
+ Requires-Dist: plotext (>=6.0.0)
32
+ Requires-Dist: requests
32
33
  Requires-Dist: scipy
33
34
  Requires-Dist: typing-extensions
34
35
  Project-URL: Changelog, https://github.com/hopr-framework/PyHOPE/blob/main/CHANGELOG.md
@@ -121,6 +122,7 @@ $ pyhope tutorials/1-01-cartbox/parameter.ini
121
122
  │ INIT OUTPUT...
122
123
  │ ProjectName │ 1-01-cartbox │ *CUSTOM │
123
124
  │ OutputFormat │ 0 [HDF5] │ *CUSTOM │
125
+ │ OutputBytes │ 0 [int32] │ DEFAULT │
124
126
  │ DebugMesh │ T │ *CUSTOM │
125
127
  │ DebugVisu │ F │ *CUSTOM │
126
128
  ├─────────────────────────────────────────────
@@ -162,23 +164,26 @@ $ pyhope tutorials/1-01-cartbox/parameter.ini
162
164
  ├── BUILD DATA STRUCTURE...
163
165
  ├────
164
166
  ├── Removing duplicate points
165
- ├── Ensuring normals point outward
166
- ├────
167
- │ CheckSurfaceNormals │ True │ DEFAULT │
168
- │ Processing Elements |█████████████████████████████████| 512/512 [100%] in 0.0s (24000.00/s)
169
167
  ├────
170
168
  ├── Generating sides
171
169
  ├─────────────────────────────────────────────
172
170
  │ SORT MESH...
173
171
  ├────
174
172
  │ MeshSorting │ 1 [SFC] │ DEFAULT │
173
+ │ MeshSortingSFC │ 0 [default] │ DEFAULT │
175
174
  ├────
176
175
  ├── Sorting elements along space-filling curve
177
176
  ├─────────────────────────────────────────────
177
+ │ CHECK NORMALS POINTING OUTWARDS...
178
+ ├────
179
+ │ CheckSurfaceNormals │ True │ DEFAULT │
180
+ │ Processing Elements |█████████████████████████████████| 512/512 [100%] in 0.0s (24000.00/s)
181
+ ├─────────────────────────────────────────────
178
182
  │ CONNECT MESH...
179
183
  ├────
180
184
  │ doPeriodicCorrect │ False │ DEFAULT │
181
185
  │ doMortars │ True │ DEFAULT │
186
+ │ doMortarRebuild │ 1 [auto] │ DEFAULT │
182
187
  │ Processing Sides |█████████████████████████████████| 3072/3072 [100%] in 0.0s (24000.00/s)
183
188
  ├────
184
189
  │ Number of sides : 3072
@@ -246,8 +251,8 @@ This is a scientific project. If you use PyHOPE for publications or presentation
246
251
  author = {Kopper, Patrick and Blind, Marcel P. and Schwarz, Anna and Kurz, Marius and Rodach, Felix and Copplestone, Stephen M. and Beck, Andrea D.},
247
252
  journal = {Journal of Open Source Software},
248
253
  year = {2025},
249
- volume = {10},
250
- number = {115},
254
+ volume = {10},
255
+ number = {115},
251
256
  pages = {8769},
252
257
  publisher = {The Open Journal},
253
258
  doi = {10.21105/joss.08769}
@@ -81,6 +81,7 @@ $ pyhope tutorials/1-01-cartbox/parameter.ini
81
81
  │ INIT OUTPUT...
82
82
  │ ProjectName │ 1-01-cartbox │ *CUSTOM │
83
83
  │ OutputFormat │ 0 [HDF5] │ *CUSTOM │
84
+ │ OutputBytes │ 0 [int32] │ DEFAULT │
84
85
  │ DebugMesh │ T │ *CUSTOM │
85
86
  │ DebugVisu │ F │ *CUSTOM │
86
87
  ├─────────────────────────────────────────────
@@ -122,23 +123,26 @@ $ pyhope tutorials/1-01-cartbox/parameter.ini
122
123
  ├── BUILD DATA STRUCTURE...
123
124
  ├────
124
125
  ├── Removing duplicate points
125
- ├── Ensuring normals point outward
126
- ├────
127
- │ CheckSurfaceNormals │ True │ DEFAULT │
128
- │ Processing Elements |█████████████████████████████████| 512/512 [100%] in 0.0s (24000.00/s)
129
126
  ├────
130
127
  ├── Generating sides
131
128
  ├─────────────────────────────────────────────
132
129
  │ SORT MESH...
133
130
  ├────
134
131
  │ MeshSorting │ 1 [SFC] │ DEFAULT │
132
+ │ MeshSortingSFC │ 0 [default] │ DEFAULT │
135
133
  ├────
136
134
  ├── Sorting elements along space-filling curve
137
135
  ├─────────────────────────────────────────────
136
+ │ CHECK NORMALS POINTING OUTWARDS...
137
+ ├────
138
+ │ CheckSurfaceNormals │ True │ DEFAULT │
139
+ │ Processing Elements |█████████████████████████████████| 512/512 [100%] in 0.0s (24000.00/s)
140
+ ├─────────────────────────────────────────────
138
141
  │ CONNECT MESH...
139
142
  ├────
140
143
  │ doPeriodicCorrect │ False │ DEFAULT │
141
144
  │ doMortars │ True │ DEFAULT │
145
+ │ doMortarRebuild │ 1 [auto] │ DEFAULT │
142
146
  │ Processing Sides |█████████████████████████████████| 3072/3072 [100%] in 0.0s (24000.00/s)
143
147
  ├────
144
148
  │ Number of sides : 3072
@@ -206,8 +210,8 @@ This is a scientific project. If you use PyHOPE for publications or presentation
206
210
  author = {Kopper, Patrick and Blind, Marcel P. and Schwarz, Anna and Kurz, Marius and Rodach, Felix and Copplestone, Stephen M. and Beck, Andrea D.},
207
211
  journal = {Journal of Open Source Software},
208
212
  year = {2025},
209
- volume = {10},
210
- number = {115},
213
+ volume = {10},
214
+ number = {115},
211
215
  pages = {8769},
212
216
  publisher = {The Open Journal},
213
217
  doi = {10.21105/joss.08769}
@@ -661,55 +661,87 @@ def change_basis_1D(Vdm: npt.NDArray[np.float64], x1D_In: npt.NDArray[np.float64
661
661
  return Vdm @ x1D_In
662
662
 
663
663
 
664
- def evaluate_jacobian(xGeo_In: npt.NDArray[np.float64],
665
- VdmGLtoAP: npt.NDArray[np.float64],
666
- D_EqToGL: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
667
- """ Calculate the Jacobian of the mapping for a given element
668
- """
669
- dim1 = xGeo_In.shape[0]
670
- n_In = xGeo_In.shape[1]
671
- n_Out = D_EqToGL.shape[0]
672
-
673
- # Perform tensor contraction for the first derivative (Xi direction)
674
- # dXdXiGL = np.tensordot(D_EqToGL, xGeo_In, axes=(1, 1))
675
- # dXdXiGL = np.moveaxis(dXdXiGL , 1, 0) # Correct the shape to (3, nGeoRef, nGeoRef, nGeoRef)
676
- _X1 = xGeo_In.reshape(dim1, n_In, n_In * n_In)
677
- dXdXiGL = (D_EqToGL @ _X1).reshape(dim1, n_Out, n_In, n_In)
678
-
679
- # Perform tensor contraction for the second derivative (Eta direction)
680
- dXdEtaGL = np.tensordot(D_EqToGL, xGeo_In, axes=(1, 2))
681
- dXdEtaGL = np.transpose(dXdEtaGL, (1, 2, 0, 3))
682
- # PERF: This is actually slower than the individual contractions
683
- # dXdEtaGL = np.einsum('qj,dijk->dqik', D_EqToGL, xGeo_In, optimize=True)
684
-
685
- # Perform tensor contraction for the third derivative (Zeta direction)
686
- # dXdZetaGL = np.tensordot(D_EqToGL, xGeo_In, axes=(1, 3))
687
- # dXdZetaGL = np.moveaxis(dXdZetaGL, 1, 0) # Correct the shape to (3, nGeoRef, nGeoRef, nGeoRef)
688
- _X3 = xGeo_In.reshape(dim1, n_In * n_In, n_In)
689
- dXdZetaGL = (_X3 @ D_EqToGL.T).reshape(dim1, n_In, n_In, n_Out)
690
- dXdZetaGL = np.transpose(dXdZetaGL, (0, 3, 1, 2))
691
- # PERF: This is actually slower than the individual contractions
692
- # dXdZetaGL = np.einsum('rk,dijk->drij', D_EqToGL, xGeo_In, optimize=True)
664
+ if not NUMBA_AVAILABLE:
665
+ def evaluate_jacobian(xGeo_In: npt.NDArray[np.float64],
666
+ VdmGLtoAP: npt.NDArray[np.float64],
667
+ D_EqToGL: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
668
+ """ Calculate the Jacobian of the mapping for a given element
669
+ """
670
+ dim1 = xGeo_In.shape[0]
671
+ n_In = xGeo_In.shape[1]
672
+ n_Out = D_EqToGL.shape[0]
673
+
674
+ # Perform tensor contraction for the first derivative (Xi direction)
675
+ # dXdXiGL = np.tensordot(D_EqToGL, xGeo_In, axes=(1, 1))
676
+ # dXdXiGL = np.moveaxis(dXdXiGL , 1, 0) # Correct the shape to (3, nGeoRef, nGeoRef, nGeoRef)
677
+ _X1 = xGeo_In.reshape(dim1, n_In, n_In * n_In)
678
+ dXdXiGL = (D_EqToGL @ _X1).reshape(dim1, n_Out, n_In, n_In)
679
+
680
+ # Perform tensor contraction for the second derivative (Eta direction)
681
+ dXdEtaGL = np.tensordot(D_EqToGL, xGeo_In, axes=(1, 2))
682
+ dXdEtaGL = np.transpose(dXdEtaGL, (1, 2, 0, 3))
683
+ # PERF: This is actually slower than the individual contractions
684
+ # dXdEtaGL = np.einsum('qj,dijk->dqik', D_EqToGL, xGeo_In, optimize=True)
685
+
686
+ # Perform tensor contraction for the third derivative (Zeta direction)
687
+ # dXdZetaGL = np.tensordot(D_EqToGL, xGeo_In, axes=(1, 3))
688
+ # dXdZetaGL = np.moveaxis(dXdZetaGL, 1, 0) # Correct the shape to (3, nGeoRef, nGeoRef, nGeoRef)
689
+ _X3 = xGeo_In.reshape(dim1, n_In * n_In, n_In)
690
+ dXdZetaGL = (_X3 @ D_EqToGL.T).reshape(dim1, n_In, n_In, n_Out)
691
+ dXdZetaGL = np.transpose(dXdZetaGL, (0, 3, 1, 2))
692
+ # PERF: This is actually slower than the individual contractions
693
+ # dXdZetaGL = np.einsum('rk,dijk->drij', D_EqToGL, xGeo_In, optimize=True)
694
+
695
+ # Change basis for each direction
696
+ dXdXiAP = change_basis_3D(VdmGLtoAP, dXdXiGL )
697
+ dXdEtaAP = change_basis_3D(VdmGLtoAP, dXdEtaGL )
698
+ dXdZetaAP = change_basis_3D(VdmGLtoAP, dXdZetaGL)
699
+
700
+ # Precompute cross products between dXdEtaAP and dXdZetaAP for all points
701
+ # cross_eta_zeta = np.cross(dXdEtaAP, dXdZetaAP, axis=0) # Shape: (3, nGeoRef, nGeoRef, nGeoRef)
702
+ # > Manually compute cross product
703
+ cross_eta_zeta = np.empty_like(dXdEtaAP)
704
+ cross_eta_zeta[0] = dXdEtaAP[1] * dXdZetaAP[2] - dXdEtaAP[2] * dXdZetaAP[1]
705
+ cross_eta_zeta[1] = dXdEtaAP[2] * dXdZetaAP[0] - dXdEtaAP[0] * dXdZetaAP[2]
706
+ cross_eta_zeta[2] = dXdEtaAP[0] * dXdZetaAP[1] - dXdEtaAP[1] * dXdZetaAP[0]
707
+
708
+ # Fill output Jacobian array
709
+ # jacOut = np.einsum('ijkl,ijkl->jkl', dXdXiAP, cross_eta_zeta)
710
+ # PERF: This is actually slower than the individual contractions
711
+ # jacOut = np.sum(dXdXiAP * cross_eta_zeta, axis=0)
693
712
 
694
- # Change basis for each direction
695
- dXdXiAP = change_basis_3D(VdmGLtoAP, dXdXiGL )
696
- dXdEtaAP = change_basis_3D(VdmGLtoAP, dXdEtaGL )
697
- dXdZetaAP = change_basis_3D(VdmGLtoAP, dXdZetaGL)
713
+ return np.einsum('ijkl,ijkl->jkl', dXdXiAP, cross_eta_zeta)
714
+ else:
715
+ @jit(nopython=True, cache=True, nogil=True)
716
+ def evaluate_jacobian(xGeo_In: npt.NDArray[np.float64],
717
+ VdmGLtoAP: npt.NDArray[np.float64],
718
+ D_EqToGL: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
719
+ """ Calculate the Jacobian of the mapping for a given element
720
+ """
721
+ dim1 = xGeo_In.shape[0]
722
+ n_In = xGeo_In.shape[1]
723
+ n_Out = D_EqToGL.shape[0]
698
724
 
699
- # Precompute cross products between dXdEtaAP and dXdZetaAP for all points
700
- # cross_eta_zeta = np.cross(dXdEtaAP, dXdZetaAP, axis=0) # Shape: (3, nGeoRef, nGeoRef, nGeoRef)
701
- # > Manually compute cross product
702
- cross_eta_zeta = np.empty_like(dXdEtaAP)
703
- cross_eta_zeta[0] = dXdEtaAP[1] * dXdZetaAP[2] - dXdEtaAP[2] * dXdZetaAP[1]
704
- cross_eta_zeta[1] = dXdEtaAP[2] * dXdZetaAP[0] - dXdEtaAP[0] * dXdZetaAP[2]
705
- cross_eta_zeta[2] = dXdEtaAP[0] * dXdZetaAP[1] - dXdEtaAP[1] * dXdZetaAP[0]
725
+ dXdXiGL = np.zeros((dim1, n_Out, n_In, n_In), dtype=np.float64)
726
+ for d in range(dim1):
727
+ _X1 = np.ascontiguousarray(xGeo_In[d]).reshape(n_In, n_In * n_In)
728
+ dXdXiGL[d] = (D_EqToGL @ _X1).reshape(n_Out, n_In, n_In)
706
729
 
707
- # Fill output Jacobian array
708
- # jacOut = np.einsum('ijkl,ijkl->jkl', dXdXiAP, cross_eta_zeta)
709
- # PERF: This is actually slower than the individual contractions
710
- # jacOut = np.sum(dXdXiAP * cross_eta_zeta, axis=0)
730
+ _X2 = np.ascontiguousarray(xGeo_In.transpose(0, 1, 3, 2)).reshape(-1, n_In)
731
+ dXdEtaGL = (_X2 @ D_EqToGL.T).reshape(dim1, n_In, n_In, n_Out).transpose(0, 1, 3, 2)
732
+
733
+ _X3 = np.ascontiguousarray(xGeo_In).reshape(-1, n_In)
734
+ dXdZetaGL = (_X3 @ D_EqToGL.T).reshape(dim1, n_In, n_In, n_Out).transpose(0, 3, 1, 2)
735
+
736
+ dXdXiAP = change_basis_3D(VdmGLtoAP, dXdXiGL )
737
+ dXdEtaAP = change_basis_3D(VdmGLtoAP, dXdEtaGL )
738
+ dXdZetaAP = change_basis_3D(VdmGLtoAP, dXdZetaGL)
739
+
740
+ cross_0 = dXdEtaAP[1] * dXdZetaAP[2] - dXdEtaAP[2] * dXdZetaAP[1]
741
+ cross_1 = dXdEtaAP[2] * dXdZetaAP[0] - dXdEtaAP[0] * dXdZetaAP[2]
742
+ cross_2 = dXdEtaAP[0] * dXdZetaAP[1] - dXdEtaAP[1] * dXdZetaAP[0]
711
743
 
712
- return np.einsum('ijkl,ijkl->jkl', dXdXiAP, cross_eta_zeta)
744
+ return dXdXiAP[0] * cross_0 + dXdXiAP[1] * cross_1 + dXdXiAP[2] * cross_2
713
745
 
714
746
 
715
747
  def evaluate_jacobian_simplex(xGeo_In: npt.NDArray[np.float64],
@@ -27,9 +27,9 @@
27
27
  # ----------------------------------------------------------------------------------------------------------------------------------
28
28
  from __future__ import annotations
29
29
  import re
30
- import sys
31
30
  from typing import Final, Optional, cast
32
31
  from collections.abc import Iterable
32
+ from operator import itemgetter
33
33
  # ----------------------------------------------------------------------------------------------------------------------------------
34
34
  # Third-party libraries
35
35
  # ----------------------------------------------------------------------------------------------------------------------------------
@@ -142,10 +142,12 @@ def CheckConnect() -> None:
142
142
  """ Check if the mesh is correctly connected
143
143
  """
144
144
  # Local imports ----------------------------------------
145
+ import pyhope.io.io_vars as io_vars
145
146
  import pyhope.output.output as hopout
146
147
  import pyhope.mesh.mesh_vars as mesh_vars
147
148
  from pyhope.common.common_parallel import run_in_parallel
148
149
  from pyhope.common.common_vars import np_mtp
150
+ from pyhope.io.io_debug import DebugIO
149
151
  from pyhope.readintools.readintools import GetLogical
150
152
  # ------------------------------------------------------
151
153
 
@@ -178,13 +180,13 @@ def CheckConnect() -> None:
178
180
  ordering = False, # noqa: E251
179
181
  )
180
182
  else:
181
- res = [elem for elem in elems if check_sides(elem, failed_only=True)]
183
+ res = [check_sides(elem, failed_only=True) for elem in elems]
182
184
 
183
- if len(res) > 0: # pragma: no cover
184
- # Flatten per-element results (skip None placeholders)
185
- results = tuple(result for elem_results in res if isinstance(elem_results, Iterable) and elem_results is not None
186
- for result in elem_results) # noqa: E272
185
+ # Flatten per-element results (skip None placeholders)
186
+ results = tuple(result for elem_results in res if isinstance(elem_results, Iterable) and elem_results is not None
187
+ for result in elem_results) # noqa: E272
187
188
 
189
+ if len(results): # pragma: no cover
188
190
  nGeo: Final[int] = mesh_vars.nGeo
189
191
  sides: Final[list] = mesh_vars.sides
190
192
  points: Final[np.ndarray] = mesh_vars.mesh.points
@@ -216,7 +218,7 @@ def CheckConnect() -> None:
216
218
  nodes = elem.nodes[face_to_nodes( side.face, elem.type, nGeo)]
217
219
  nbnodes = nbelem.nodes[face_to_nodes(nbside.face, nbelem.type, nGeo)]
218
220
 
219
- print()
221
+ hopout.info('')
220
222
  # Check if side is oriented inwards
221
223
  errStr = 'Side connectivity does not match the calculated neighbour side'
222
224
  print(hopout.warn(errStr, length=len(errStr)+16))
@@ -235,5 +237,9 @@ def CheckConnect() -> None:
235
237
  print(hopout.warn('- Coordinates : [' + ' '.join(f'{s:12.3f}' for s in points[nbnodes[-1, 0]]) + ']')) # noqa: E271
236
238
  print(hopout.warn('- Coordinates : [' + ' '.join(f'{s:12.3f}' for s in points[nbnodes[-1, -1]]) + ']')) # noqa: E271
237
239
 
238
- hopout.warning(f'Connectivity check failed for {len(results)} / {nconn} connections!')
239
- sys.exit(1)
240
+ # Write a low-order debug mesh if requested
241
+ if io_vars.debugmesh:
242
+ hopout.info('')
243
+ DebugIO(errSides=list(map(itemgetter(1), results)))
244
+
245
+ hopout.error(f'Connectivity check failed for {len(results)} / {nconn} connections!')
@@ -94,8 +94,34 @@ def plot_histogram(data: npt.NDArray[np.float64]) -> None:
94
94
  hopout.sep()
95
95
  hopout.info('Scaled Jacobians')
96
96
  hopout.separator(18)
97
- plt.simple_bar(ticks, count, width=STD_LENGTH)
98
- plt.show()
97
+
98
+ # Horizontal bars are stacked from the bottom up, thus reverse the data to keep the first bin on top
99
+ labels = ticks[::-1]
100
+ counts = count[::-1]
101
+
102
+ # Reserve space at the end of the longest bar for its count label
103
+ barMax = max(int(count.max()), 1)
104
+ barWidth = STD_LENGTH - len(ticks[0])
105
+ labelWidth = len(str(barMax)) + 1
106
+
107
+ # Assemble the frame-less bar plot from the regular [bar], see https://plotext.readthedocs.io/en/latest/bar.html#simple-bar-style
108
+ fig = plt.figure
109
+ fig.clear()
110
+ fig.theme('simple')
111
+ fig.plot_size(STD_LENGTH, len(labels))
112
+ fig.draw(fig.bar(labels, counts, orientation='horizontal', marker='brick', width=0, lines=True))
113
+
114
+ # Print the number of elements at the end of each bar
115
+ for row, c in enumerate(counts, start=1):
116
+ fig.draw(fig.text(c, row, plt.colorize(f' {c}', pixel=('gray+', None, 'bold')), alignment='left'))
117
+
118
+ # Remove the frame and the axis ticks, keeping only the bin labels
119
+ fig.axes(False)
120
+ fig.ruler('x').lim(0, barMax * barWidth / max(barWidth - labelWidth, 1))
121
+ fig.ruler('x').ticks([])
122
+ fig.ruler('y').alignment(tick='left')
123
+ fig.show(flush=True)
124
+
99
125
  hopout.separator(18)
100
126
 
101
127
 
@@ -0,0 +1,195 @@
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ #
4
+ # SPDX-License-Identifier: GPL-3.0-or-later
5
+ #
6
+ # This file is part of PyHOPE
7
+ #
8
+ # Copyright (c) 2024 Numerics Research Group, University of Stuttgart, Prof. Andrea Beck
9
+ #
10
+ # PyHOPE is free software: you can redistribute it and/or modify it under the
11
+ # terms of the GNU General Public License as published by the Free Software
12
+ # Foundation, either version 3 of the License, or (at your option) any later
13
+ # version.
14
+ #
15
+ # PyHOPE is distributed in the hope that it will be useful, but WITHOUT ANY
16
+ # WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
17
+ # A PARTICULAR PURPOSE. See the GNU General Public License for more details.
18
+ #
19
+ # You should have received a copy of the GNU General Public License along with
20
+ # PyHOPE. If not, see <http://www.gnu.org/licenses/>.
21
+
22
+ # ==================================================================================================================================
23
+ # Mesh generation library
24
+ # ==================================================================================================================================
25
+ # ----------------------------------------------------------------------------------------------------------------------------------
26
+ # Standard libraries
27
+ # ----------------------------------------------------------------------------------------------------------------------------------
28
+ from __future__ import annotations
29
+ import gc
30
+ import re
31
+ from typing import Final, Optional, cast
32
+ from collections.abc import Iterable
33
+ from operator import itemgetter
34
+ # ----------------------------------------------------------------------------------------------------------------------------------
35
+ # Third-party libraries
36
+ # ----------------------------------------------------------------------------------------------------------------------------------
37
+ import numpy as np
38
+ # ----------------------------------------------------------------------------------------------------------------------------------
39
+ # Typing libraries
40
+ # ----------------------------------------------------------------------------------------------------------------------------------
41
+ import typing
42
+ from pyhope.common.common_numba import NUMBA_AVAILABLE
43
+ if typing.TYPE_CHECKING or NUMBA_AVAILABLE:
44
+ import numpy.typing as npt
45
+ from pyhope.mesh.mesh_vars import ELEM
46
+ # ----------------------------------------------------------------------------------------------------------------------------------
47
+ # Local imports
48
+ # ----------------------------------------------------------------------------------------------------------------------------------
49
+ import pyhope.mesh.mesh_vars as mesh_vars
50
+ from pyhope.basis.basis_watertight import eval_nsurf
51
+ from pyhope.mesh.mesh_common import face_to_nodes, faces
52
+ # ==================================================================================================================================
53
+
54
+
55
+ def check_orientation(elem : ELEM,
56
+ VdmEqToGP : npt.NDArray[np.float64],
57
+ DGP : npt.NDArray[np.float64],
58
+ weights : npt.NDArray[np.float64],
59
+ failed_only: bool = False,
60
+ ) -> Optional[list[tuple]]:
61
+ """ Check the orientation of the surface normals
62
+ """
63
+ points = mesh_vars.mesh.points
64
+ nGeo = mesh_vars.nGeo
65
+ elemType = elem.type
66
+
67
+ if elemType % 10 != 8:
68
+ return None
69
+
70
+ # Center of element
71
+ cElem = points[elem.nodes].reshape(-1, 3).mean(axis=0)
72
+ results = None
73
+
74
+ # Fall back to geometry topology faces if sides array is not initialized
75
+ sides = getattr(mesh_vars, 'sides', None)
76
+ sideIter = ((sides[s].face, s) for s in elem.sides) if (elem.sides is not None and sides is not None) else \
77
+ ((f, None) for f in faces(elemType))
78
+
79
+ for face, SideID in sideIter:
80
+ idx = cast(np.ndarray, elem.nodes)[face_to_nodes(face, elemType, nGeo)]
81
+ fpoints = points[idx]
82
+
83
+ # Calculate high-order surface normal vector via Gauss integration
84
+ nSurf = eval_nsurf(fpoints.transpose(2, 0, 1), VdmEqToGP, DGP, weights)
85
+
86
+ # Vector pointing from face center to element center
87
+ fCenter = fpoints.reshape(-1, 3).mean(axis=0)
88
+ vCenter = cElem - fCenter
89
+
90
+ success = np.dot(nSurf, vCenter) <= 0.
91
+
92
+ # If requested, only return errors
93
+ if failed_only and success:
94
+ continue
95
+
96
+ # Lazily initialize results on first failure
97
+ if results is None:
98
+ results = []
99
+ results.append((success, elem.elemID, face, SideID))
100
+
101
+ # Avoid creating empty lists on elem_results
102
+ if results is None:
103
+ return None if failed_only else []
104
+
105
+ return results
106
+
107
+
108
+ def process_chunk(chunk: tuple) -> list:
109
+ """Process a chunk of elements by checking surface normal orientation
110
+ """
111
+ # Only keep failures to reduce memory and avoid building large arrays of successes
112
+ chunk_results = []
113
+ for elem in chunk:
114
+ elem_result = check_orientation(elem,
115
+ process_chunk.VdmEqToGP, # ty: ignore[unresolved-attribute]
116
+ process_chunk.DGP, # ty: ignore[unresolved-attribute]
117
+ process_chunk.weights, # ty: ignore[unresolved-attribute]
118
+ failed_only=True)
119
+ chunk_results.append(elem_result)
120
+ return chunk_results
121
+
122
+
123
+ def CheckOrient() -> None:
124
+ """ Check if element surface normals point outwards
125
+ """
126
+ # Local imports ----------------------------------------
127
+ import pyhope.io.io_vars as io_vars
128
+ import pyhope.output.output as hopout
129
+ import pyhope.mesh.mesh_vars as mesh_vars
130
+ from pyhope.basis.basis_basis import barycentric_weights, legendre_gauss_nodes
131
+ from pyhope.basis.basis_basis import calc_vandermonde, polynomial_derivative_matrix
132
+ from pyhope.basis.basis_watertight import init_worker
133
+ from pyhope.common.common_parallel import run_in_parallel
134
+ from pyhope.common.common_vars import np_mtp
135
+ from pyhope.io.io_debug import DebugIO
136
+ from pyhope.readintools.readintools import GetLogical
137
+ # ------------------------------------------------------
138
+
139
+ hopout.separator()
140
+ hopout.info('CHECK NORMALS POINTING OUTWARDS...')
141
+ hopout.sep()
142
+
143
+ checkSurfaceNormals = GetLogical('CheckSurfaceNormals')
144
+ if not checkSurfaceNormals:
145
+ return None
146
+
147
+ nGeo: Final[int] = mesh_vars.nGeo
148
+ elems: Final[list] = mesh_vars.elems
149
+
150
+ # Setup mathematical structures identical to CheckWatertight
151
+ xEq: Final[npt.NDArray[np.float64]] = np.linspace(-1., 1., nGeo+1)
152
+ wBaryEq: Final[npt.NDArray[np.float64]] = barycentric_weights(nGeo+1, xEq)
153
+
154
+ xGP, wGP = legendre_gauss_nodes(nGeo+1)
155
+ DGP: Final[npt.NDArray[np.float64]] = polynomial_derivative_matrix(nGeo+1, xGP)
156
+ VdmEqToGP: Final[npt.NDArray[np.float64]] = calc_vandermonde(nGeo+1, nGeo+1, wBaryEq, xEq, xGP)
157
+ weights: Final[npt.NDArray[np.float64]] = np.outer(wGP, wGP)
158
+
159
+ # Prepare elements for parallel processing
160
+ if np_mtp > 0:
161
+ # Run in parallel with a chunk size
162
+ # > Dispatch the tasks to the workers, minimum 10 tasks per worker, maximum 1000 tasks per worker
163
+ res = run_in_parallel(process_chunk,
164
+ elems,
165
+ chunk_size = max(1, min(1000, max(10, int(len(elems)/(40.*np_mtp))))), # noqa: E251
166
+ initializer = init_worker, # noqa: E251
167
+ init_args = (process_chunk, VdmEqToGP, DGP, weights), # noqa: E251
168
+ ordering = False, # noqa: E251
169
+ )
170
+ else:
171
+ res = [check_orientation(elem, VdmEqToGP, DGP, weights, failed_only=True) for elem in elems]
172
+
173
+ # Flatten results while filtering empty items
174
+ results = tuple(result for elem_results in res if isinstance(elem_results, Iterable) and elem_results is not None
175
+ for result in elem_results) # noqa: E272
176
+
177
+ if len(results):
178
+ for result in cast(tuple[tuple], results):
179
+ _, elemID, face, sideID = result
180
+ hopout.info('')
181
+ print(hopout.warn(f'Side is oriented inwards! Element {elemID + 1}, Face {face}, Side {sideID + 1}'))
182
+
183
+ if io_vars.debugmesh:
184
+ hopout.info('')
185
+ DebugIO(errSides=list(map(itemgetter(3), results)))
186
+
187
+ hopout.error(f'Surface normals check failed for {len(results)} / {len(elems)} elements!')
188
+
189
+ # Warn if we passed any element types
190
+ if any(e.type % 10 != 8 for e in elems):
191
+ elemTypes = list({e.type for e in elems if e.type % 10 != 8})
192
+ print(hopout.warn('Ignored element type: {}'.format([re.sub(r"\d+$", "", mesh_vars.ELEMTYPE.inam[e][0]) for e in elemTypes]))) # ruff: ignore[line-too-long]
193
+
194
+ # Run garbage collector to release memory
195
+ gc.collect()
@@ -30,6 +30,7 @@ import gc
30
30
  import re
31
31
  from typing import Final, Optional, cast
32
32
  from collections.abc import Callable, Iterable
33
+ from operator import itemgetter
33
34
  # ----------------------------------------------------------------------------------------------------------------------------------
34
35
  # Third-party libraries
35
36
  # ----------------------------------------------------------------------------------------------------------------------------------
@@ -96,7 +97,7 @@ def eval_nsurf(XGeo: npt.NDArray[np.float64],
96
97
  # # dXdetaGP = np.moveaxis(dXdetaGP, 0, 1).reshape(3, -1) # Flatten for cross computation (slower)
97
98
  # dXdetaGP = dXdetaGP.reshape(3, -1) # Flatten for cross computation
98
99
 
99
- # Compute derivatives at all Gauss points using matrix multiplications to avoid extra copies
100
+ # Compute derivatives at all Gauss points
100
101
  dXdxiGP = np.empty_like(xGP)
101
102
  dXdetaGP = np.empty_like(xGP)
102
103
  DT = DGP.T
@@ -120,7 +121,7 @@ def eval_nsurf(XGeo: npt.NDArray[np.float64],
120
121
  # Integrate over the Gauss points
121
122
  # return -np.sum(nVecW, axis=(1, 2)) # Sum over the last two axes
122
123
 
123
- # Compute the weighted cross product integral directly
124
+ # Compute the weighted cross product integral
124
125
  NSurf0, NSurf1, NSurf2 = eval_dotprod(dXdetaGP, dXdxiGP, weights)
125
126
 
126
127
  return np.array((NSurf0, NSurf1, NSurf2), dtype=xGP.dtype)
@@ -243,9 +244,9 @@ def process_chunk(chunk: tuple) -> list:
243
244
  chunk_results = []
244
245
  for elem in chunk:
245
246
  elem_result = check_sides(elem,
246
- process_chunk.VdmEqToGP, # pyright: ignore[reportFunctionMemberAccess] # ty: ignore[unresolved-attribute]
247
- process_chunk.DGP, # pyright: ignore[reportFunctionMemberAccess] # ty: ignore[unresolved-attribute]
248
- process_chunk.weights, # pyright: ignore[reportFunctionMemberAccess] # ty: ignore[unresolved-attribute]
247
+ process_chunk.VdmEqToGP, # ty: ignore[unresolved-attribute]
248
+ process_chunk.DGP, # ty: ignore[unresolved-attribute]
249
+ process_chunk.weights, # ty: ignore[unresolved-attribute]
249
250
  failed_only=True)
250
251
  # Append a lightweight sentinel (None) for successes, actual failure list otherwise
251
252
  chunk_results.append(elem_result)
@@ -256,12 +257,14 @@ def CheckWatertight() -> None:
256
257
  """ Check if the mesh is watertight
257
258
  """
258
259
  # Local imports ----------------------------------------
260
+ import pyhope.io.io_vars as io_vars
259
261
  import pyhope.output.output as hopout
260
262
  import pyhope.mesh.mesh_vars as mesh_vars
261
263
  from pyhope.basis.basis_basis import barycentric_weights, legendre_gauss_nodes
262
264
  from pyhope.basis.basis_basis import calc_vandermonde, polynomial_derivative_matrix
263
265
  from pyhope.common.common_parallel import run_in_parallel
264
266
  from pyhope.common.common_vars import np_mtp
267
+ from pyhope.io.io_debug import DebugIO
265
268
  from pyhope.readintools.readintools import GetLogical
266
269
  # ------------------------------------------------------
267
270
 
@@ -314,15 +317,13 @@ def CheckWatertight() -> None:
314
317
  ordering = False, # noqa: E251
315
318
  )
316
319
  else:
317
- res = [elem for elem in elems if check_sides(elem,
318
- VdmEqToGP, DGP, weights,
319
- failed_only=True)]
320
+ res = [check_sides(elem, VdmEqToGP, DGP, weights, failed_only=True) for elem in elems]
320
321
 
321
- if len(res) > 0: # pragma: no cover
322
- # Flatten per-element results (skip None placeholders)
323
- results = tuple(result for elem_results in res if isinstance(elem_results, Iterable) and elem_results is not None
324
- for result in elem_results) # noqa: E272
322
+ # Flatten per-element results (skip None placeholders)
323
+ results = tuple(result for elem_results in res if isinstance(elem_results, Iterable) and elem_results is not None
324
+ for result in elem_results) # noqa: E272
325
325
 
326
+ if len(results): # pragma: no cover
326
327
  # Compute total number of checked connections without materializing all results
327
328
  nconn = 0
328
329
  for SideID, side in enumerate(sides):
@@ -352,7 +353,7 @@ def CheckWatertight() -> None:
352
353
  nodes = elem.nodes[face_to_nodes( side.face, elem.type, nGeo)]
353
354
  nbnodes = nbelem.nodes[face_to_nodes(nbside.face, nbelem.type, nGeo)]
354
355
 
355
- print()
356
+ hopout.info('')
356
357
  # Check if side is oriented inwards
357
358
  errStr = 'Side is oriented inwards!' if nSurfErr < 0 \
358
359
  else f'Surface normals are not within tolerance {nSurfErr:9.6e} > {tol:9.6e}'
@@ -374,6 +375,11 @@ def CheckWatertight() -> None:
374
375
  print(hopout.warn('- Coordinates : [' + ' '.join(f'{s:12.3f}' for s in points[nbnodes[-1, 0]]) + ']')) # noqa: E271
375
376
  print(hopout.warn('- Coordinates : [' + ' '.join(f'{s:12.3f}' for s in points[nbnodes[-1, -1]]) + ']')) # noqa: E271
376
377
 
378
+ # Write a low-order debug mesh if requested
379
+ if io_vars.debugmesh:
380
+ hopout.info('')
381
+ DebugIO(errSides=list(map(itemgetter(1), results)))
382
+
377
383
  hopout.error(f'Watertightness check failed for {len(results)} / {nconn} connections!')
378
384
 
379
385
  # Run garbage collector to release memory