pyvale 2025.5.3__cp311-cp311-win_amd64.whl

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.

Potentially problematic release.


This version of pyvale might be problematic. Click here for more details.

Files changed (174) hide show
  1. pyvale/__init__.py +89 -0
  2. pyvale/analyticmeshgen.py +102 -0
  3. pyvale/analyticsimdatafactory.py +91 -0
  4. pyvale/analyticsimdatagenerator.py +323 -0
  5. pyvale/blendercalibrationdata.py +15 -0
  6. pyvale/blenderlightdata.py +26 -0
  7. pyvale/blendermaterialdata.py +15 -0
  8. pyvale/blenderrenderdata.py +30 -0
  9. pyvale/blenderscene.py +488 -0
  10. pyvale/blendertools.py +420 -0
  11. pyvale/camera.py +146 -0
  12. pyvale/cameradata.py +69 -0
  13. pyvale/cameradata2d.py +84 -0
  14. pyvale/camerastereo.py +217 -0
  15. pyvale/cameratools.py +522 -0
  16. pyvale/cython/rastercyth.c +32211 -0
  17. pyvale/cython/rastercyth.cp311-win_amd64.pyd +0 -0
  18. pyvale/cython/rastercyth.py +640 -0
  19. pyvale/data/__init__.py +5 -0
  20. pyvale/data/cal_target.tiff +0 -0
  21. pyvale/data/case00_HEX20_out.e +0 -0
  22. pyvale/data/case00_HEX27_out.e +0 -0
  23. pyvale/data/case00_HEX8_out.e +0 -0
  24. pyvale/data/case00_TET10_out.e +0 -0
  25. pyvale/data/case00_TET14_out.e +0 -0
  26. pyvale/data/case00_TET4_out.e +0 -0
  27. pyvale/data/case13_out.e +0 -0
  28. pyvale/data/case16_out.e +0 -0
  29. pyvale/data/case17_out.e +0 -0
  30. pyvale/data/case18_1_out.e +0 -0
  31. pyvale/data/case18_2_out.e +0 -0
  32. pyvale/data/case18_3_out.e +0 -0
  33. pyvale/data/case25_out.e +0 -0
  34. pyvale/data/case26_out.e +0 -0
  35. pyvale/data/optspeckle_2464x2056px_spec5px_8bit_gblur1px.tiff +0 -0
  36. pyvale/dataset.py +325 -0
  37. pyvale/errorcalculator.py +109 -0
  38. pyvale/errordriftcalc.py +146 -0
  39. pyvale/errorintegrator.py +336 -0
  40. pyvale/errorrand.py +607 -0
  41. pyvale/errorsyscalib.py +134 -0
  42. pyvale/errorsysdep.py +327 -0
  43. pyvale/errorsysfield.py +414 -0
  44. pyvale/errorsysindep.py +808 -0
  45. pyvale/examples/__init__.py +5 -0
  46. pyvale/examples/basics/ex1_1_basicscalars_therm2d.py +131 -0
  47. pyvale/examples/basics/ex1_2_sensormodel_therm2d.py +158 -0
  48. pyvale/examples/basics/ex1_3_customsens_therm3d.py +216 -0
  49. pyvale/examples/basics/ex1_4_basicerrors_therm3d.py +153 -0
  50. pyvale/examples/basics/ex1_5_fielderrs_therm3d.py +168 -0
  51. pyvale/examples/basics/ex1_6_caliberrs_therm2d.py +133 -0
  52. pyvale/examples/basics/ex1_7_spatavg_therm2d.py +123 -0
  53. pyvale/examples/basics/ex2_1_basicvectors_disp2d.py +112 -0
  54. pyvale/examples/basics/ex2_2_vectorsens_disp2d.py +111 -0
  55. pyvale/examples/basics/ex2_3_sensangle_disp2d.py +139 -0
  56. pyvale/examples/basics/ex2_4_chainfielderrs_disp2d.py +196 -0
  57. pyvale/examples/basics/ex2_5_vectorfields3d_disp3d.py +109 -0
  58. pyvale/examples/basics/ex3_1_basictensors_strain2d.py +114 -0
  59. pyvale/examples/basics/ex3_2_tensorsens2d_strain2d.py +111 -0
  60. pyvale/examples/basics/ex3_3_tensorsens3d_strain3d.py +182 -0
  61. pyvale/examples/basics/ex4_1_expsim2d_thermmech2d.py +171 -0
  62. pyvale/examples/basics/ex4_2_expsim3d_thermmech3d.py +252 -0
  63. pyvale/examples/genanalyticdata/ex1_1_scalarvisualisation.py +35 -0
  64. pyvale/examples/genanalyticdata/ex1_2_scalarcasebuild.py +43 -0
  65. pyvale/examples/genanalyticdata/ex2_1_analyticsensors.py +80 -0
  66. pyvale/examples/imagedef2d/ex_imagedef2d_todisk.py +79 -0
  67. pyvale/examples/renderblender/ex1_1_blenderscene.py +121 -0
  68. pyvale/examples/renderblender/ex1_2_blenderdeformed.py +119 -0
  69. pyvale/examples/renderblender/ex2_1_stereoscene.py +128 -0
  70. pyvale/examples/renderblender/ex2_2_stereodeformed.py +131 -0
  71. pyvale/examples/renderblender/ex3_1_blendercalibration.py +120 -0
  72. pyvale/examples/renderrasterisation/ex_rastenp.py +153 -0
  73. pyvale/examples/renderrasterisation/ex_rastercyth_oneframe.py +218 -0
  74. pyvale/examples/renderrasterisation/ex_rastercyth_static_cypara.py +187 -0
  75. pyvale/examples/renderrasterisation/ex_rastercyth_static_pypara.py +190 -0
  76. pyvale/examples/visualisation/ex1_1_plot_traces.py +102 -0
  77. pyvale/examples/visualisation/ex2_1_animate_sim.py +89 -0
  78. pyvale/experimentsimulator.py +175 -0
  79. pyvale/field.py +128 -0
  80. pyvale/fieldconverter.py +351 -0
  81. pyvale/fieldsampler.py +111 -0
  82. pyvale/fieldscalar.py +166 -0
  83. pyvale/fieldtensor.py +218 -0
  84. pyvale/fieldtransform.py +388 -0
  85. pyvale/fieldvector.py +213 -0
  86. pyvale/generatorsrandom.py +505 -0
  87. pyvale/imagedef2d.py +569 -0
  88. pyvale/integratorfactory.py +240 -0
  89. pyvale/integratorquadrature.py +217 -0
  90. pyvale/integratorrectangle.py +165 -0
  91. pyvale/integratorspatial.py +89 -0
  92. pyvale/integratortype.py +43 -0
  93. pyvale/output.py +17 -0
  94. pyvale/pyvaleexceptions.py +11 -0
  95. pyvale/raster.py +31 -0
  96. pyvale/rastercy.py +77 -0
  97. pyvale/rasternp.py +603 -0
  98. pyvale/rendermesh.py +147 -0
  99. pyvale/sensorarray.py +178 -0
  100. pyvale/sensorarrayfactory.py +196 -0
  101. pyvale/sensorarraypoint.py +278 -0
  102. pyvale/sensordata.py +71 -0
  103. pyvale/sensordescriptor.py +213 -0
  104. pyvale/sensortools.py +142 -0
  105. pyvale/simcases/case00_HEX20.i +242 -0
  106. pyvale/simcases/case00_HEX27.i +242 -0
  107. pyvale/simcases/case00_HEX8.i +242 -0
  108. pyvale/simcases/case00_TET10.i +242 -0
  109. pyvale/simcases/case00_TET14.i +242 -0
  110. pyvale/simcases/case00_TET4.i +242 -0
  111. pyvale/simcases/case01.i +101 -0
  112. pyvale/simcases/case02.i +156 -0
  113. pyvale/simcases/case03.i +136 -0
  114. pyvale/simcases/case04.i +181 -0
  115. pyvale/simcases/case05.i +234 -0
  116. pyvale/simcases/case06.i +305 -0
  117. pyvale/simcases/case07.geo +135 -0
  118. pyvale/simcases/case07.i +87 -0
  119. pyvale/simcases/case08.geo +144 -0
  120. pyvale/simcases/case08.i +153 -0
  121. pyvale/simcases/case09.geo +204 -0
  122. pyvale/simcases/case09.i +87 -0
  123. pyvale/simcases/case10.geo +204 -0
  124. pyvale/simcases/case10.i +257 -0
  125. pyvale/simcases/case11.geo +337 -0
  126. pyvale/simcases/case11.i +147 -0
  127. pyvale/simcases/case12.geo +388 -0
  128. pyvale/simcases/case12.i +329 -0
  129. pyvale/simcases/case13.i +140 -0
  130. pyvale/simcases/case14.i +159 -0
  131. pyvale/simcases/case15.geo +337 -0
  132. pyvale/simcases/case15.i +150 -0
  133. pyvale/simcases/case16.geo +391 -0
  134. pyvale/simcases/case16.i +357 -0
  135. pyvale/simcases/case17.geo +135 -0
  136. pyvale/simcases/case17.i +144 -0
  137. pyvale/simcases/case18.i +254 -0
  138. pyvale/simcases/case18_1.i +254 -0
  139. pyvale/simcases/case18_2.i +254 -0
  140. pyvale/simcases/case18_3.i +254 -0
  141. pyvale/simcases/case19.geo +252 -0
  142. pyvale/simcases/case19.i +99 -0
  143. pyvale/simcases/case20.geo +252 -0
  144. pyvale/simcases/case20.i +250 -0
  145. pyvale/simcases/case21.geo +74 -0
  146. pyvale/simcases/case21.i +155 -0
  147. pyvale/simcases/case22.geo +82 -0
  148. pyvale/simcases/case22.i +140 -0
  149. pyvale/simcases/case23.geo +164 -0
  150. pyvale/simcases/case23.i +140 -0
  151. pyvale/simcases/case24.geo +79 -0
  152. pyvale/simcases/case24.i +123 -0
  153. pyvale/simcases/case25.geo +82 -0
  154. pyvale/simcases/case25.i +140 -0
  155. pyvale/simcases/case26.geo +166 -0
  156. pyvale/simcases/case26.i +140 -0
  157. pyvale/simcases/run_1case.py +61 -0
  158. pyvale/simcases/run_all_cases.py +69 -0
  159. pyvale/simcases/run_build_case.py +64 -0
  160. pyvale/simcases/run_example_cases.py +69 -0
  161. pyvale/simtools.py +67 -0
  162. pyvale/visualexpplotter.py +191 -0
  163. pyvale/visualimagedef.py +74 -0
  164. pyvale/visualimages.py +76 -0
  165. pyvale/visualopts.py +493 -0
  166. pyvale/visualsimanimator.py +111 -0
  167. pyvale/visualsimsensors.py +318 -0
  168. pyvale/visualtools.py +136 -0
  169. pyvale/visualtraceplotter.py +142 -0
  170. pyvale-2025.5.3.dist-info/METADATA +144 -0
  171. pyvale-2025.5.3.dist-info/RECORD +174 -0
  172. pyvale-2025.5.3.dist-info/WHEEL +5 -0
  173. pyvale-2025.5.3.dist-info/licenses/LICENSE +21 -0
  174. pyvale-2025.5.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,351 @@
1
+ # ==============================================================================
2
+ # pyvale: the python validation engine
3
+ # License: MIT
4
+ # Copyright (C) 2025 The Computer Aided Validation Team
5
+ # ==============================================================================
6
+
7
+ """
8
+ This module provides functions for manipulating simulation data objects to be
9
+ compatible with the underlying machinery of pyvale.
10
+ """
11
+
12
+ import numpy as np
13
+ import pyvista as pv
14
+ from pyvista import CellType
15
+ import mooseherder as mh
16
+
17
+ def simdata_to_pyvista(sim_data: mh.SimData,
18
+ components: tuple[str,...] | None,
19
+ elem_dims: int
20
+ ) -> tuple[pv.UnstructuredGrid,pv.UnstructuredGrid]:
21
+ """Converts the mesh and field data in a `SimData` object into a pyvista
22
+ UnstructuredGrid for sampling (interpolating) the data and visualisation.
23
+
24
+ Parameters
25
+ ----------
26
+ sim_data : mh.SimData
27
+ Object containing a mesh and associated field data from a simulation.
28
+ components : tuple[str,...] | None
29
+ String keys for the components of the field to extract from the
30
+ simulation data.
31
+ elem_dim : int
32
+ Number of spatial dimensions (2 or 3) used to determine the element
33
+ types in the mesh from the number of nodes per element.
34
+
35
+ Returns
36
+ -------
37
+ tuple[pv.UnstructuredGrid,pv.UnstructuredGrid]
38
+ The first UnstructuredGrid has the field components attached as dataset
39
+ arrays. The second has no field data attached for visualisation.
40
+ """
41
+ flat_connect = np.array([],dtype=np.int64)
42
+ cell_types = np.array([],dtype=np.int64)
43
+
44
+ for cc in sim_data.connect:
45
+ # NOTE: need the -1 here to make element numbers 0 indexed!
46
+ this_connect = np.copy(sim_data.connect[cc])-1
47
+ (nodes_per_elem,n_elems) = this_connect.shape
48
+
49
+ this_cell_type = _get_pyvista_cell_type(nodes_per_elem,elem_dims)
50
+ assert this_cell_type is not None, ("Cell type with dimension " +
51
+ f"{elem_dims} and {nodes_per_elem} nodes per element not recognised.")
52
+
53
+ # VTK and exodus have different winding for 3D higher order quads
54
+ this_connect = _exodus_to_pyvista_connect(this_cell_type,this_connect)
55
+
56
+ this_connect = this_connect.T.flatten()
57
+ idxs = np.arange(0,n_elems*nodes_per_elem,nodes_per_elem,dtype=np.int64)
58
+
59
+ this_connect = np.insert(this_connect,idxs,nodes_per_elem)
60
+
61
+ cell_types = np.hstack((cell_types,np.full(n_elems,this_cell_type)))
62
+ flat_connect = np.hstack((flat_connect,this_connect),dtype=np.int64)
63
+
64
+ cells = flat_connect
65
+
66
+ points = sim_data.coords
67
+ pv_grid = pv.UnstructuredGrid(cells, cell_types, points)
68
+ pv_grid_vis = pv.UnstructuredGrid(cells, cell_types, points)
69
+
70
+ if components is not None and sim_data.node_vars is not None:
71
+ for cc in components:
72
+ pv_grid[cc] = sim_data.node_vars[cc]
73
+
74
+ return (pv_grid,pv_grid_vis)
75
+
76
+
77
+ def scale_length_units(scale: float,
78
+ sim_data: mh.SimData,
79
+ disp_comps: tuple[str,...] | None = None,
80
+ ) -> mh.SimData:
81
+ """Used to scale the length units of a simulation. Commonly used to convert
82
+ SI units to mm for use with visualisation tools and rendering algorithms.
83
+
84
+ Parameters
85
+ ----------
86
+ scale : float
87
+ Scale multiplier used to scale the coordinates and displacement fields
88
+ if specified.
89
+ sim_data : mh.SimData
90
+ Simulation dataclass that will be scaled.
91
+ disp_comps : tuple[str,...] | None, optional
92
+ Tuple of string keys for the displacement keys to be scaled, by default
93
+ None. If None then the displacements are not scaled.
94
+
95
+ Returns
96
+ -------
97
+ mh.SimData
98
+ Simulation dataclass with scaled length units.
99
+ """
100
+ sim_data.coords = sim_data.coords*scale
101
+
102
+ if disp_comps is not None:
103
+ for cc in disp_comps:
104
+ sim_data.node_vars[cc] = sim_data.node_vars[cc]*scale
105
+
106
+ return sim_data
107
+
108
+
109
+ # TODO: make this work for sim_data with multiple connectivity
110
+ def extract_surf_mesh(sim_data: mh.SimData) -> mh.SimData:
111
+ """Extracts a surface mesh from a 3D simulation dataclass. Useful for
112
+ limiting the memory required for analysing sensors that only measure surface
113
+ fields. This function currently supports:
114
+ - A single connectivity table
115
+ - Higher order retrahedral and hexahedral elements (but not wedges or
116
+ pyramids)
117
+
118
+ NOTE: this function returns the surface mesh with element nodal winding
119
+ consistent with th exodus output format.
120
+
121
+ Parameters
122
+ ----------
123
+ sim_data : mh.SimData
124
+ Simulation dataclass containing the 3D mesh from which the surface mesh
125
+ is to be extracted.
126
+
127
+ Returns
128
+ -------
129
+ mh.SimData
130
+ Simulation data class containing the data for the surface mesh.
131
+ """
132
+
133
+ # NOTE: need to fix exodus 1 indexing for now and put it back at the end
134
+ # shape=(nodes_per_elem,num_elems)
135
+ connect = np.copy(sim_data.connect["connect1"])-1
136
+ num_elems = connect.shape[1]
137
+
138
+ assert "connect2" not in sim_data.connect, \
139
+ "Multiple connectivity tables not supported yet."
140
+
141
+ # Mapping of node numbers to faces for each element face
142
+ face_map = _get_surf_map(nodes_per_elem=connect.shape[0])
143
+ faces_per_elem = face_map.shape[0]
144
+ nodes_per_face = face_map.shape[1]
145
+
146
+ # shape=(faces_per_elem,nodes_per_face,num_elems)
147
+ faces_wound = connect[face_map,:]
148
+ # shape=(num_elems,faces_per_elem,nodes_per_face)
149
+ faces_wound = faces_wound.transpose((2,0,1))
150
+
151
+ # Create an array of all faces with shape=(total_faces,nodes_per_face)
152
+ faces_total = faces_per_elem*num_elems
153
+ faces_flat_wound = faces_wound.reshape((faces_total,nodes_per_face))
154
+ # Sort the rows so nodes are in the same order when comparing them
155
+ faces_flat_sorted = np.copy(np.sort(faces_flat_wound,axis=1))
156
+
157
+ # Count each unique face in the list of faces, faces that appear only once
158
+ # must be external faces
159
+ (_,
160
+ faces_unique_inds,
161
+ faces_unique_counts) = np.unique(faces_flat_sorted,
162
+ axis=0,
163
+ return_counts=True,
164
+ return_index=True)
165
+
166
+ # Indices of the external faces in faces_flat
167
+ faces_ext_inds_in_unique = np.where(faces_unique_counts==1)[0]
168
+
169
+ # shape=(num_ext_faces,nodes_per_face)
170
+ faces_ext_inds = faces_unique_inds[faces_ext_inds_in_unique]
171
+
172
+ faces_ext_wound = faces_flat_wound[faces_ext_inds]
173
+
174
+ faces_coord_inds = np.unique(faces_ext_wound.flatten())
175
+ faces_coords = np.copy(sim_data.coords[faces_coord_inds])
176
+
177
+ faces_shape = faces_ext_wound.shape
178
+ faces_ext_wound_flat = faces_ext_wound.flatten()
179
+ faces_ext_remap_flat = np.copy(faces_ext_wound_flat)
180
+
181
+ # Remap coordinates in the connectivity to match the trimmed list of coords
182
+ # that belong to the external faces
183
+ for mm,cc in enumerate(faces_coord_inds):
184
+ if mm == cc:
185
+ continue
186
+
187
+ ind_to_map = np.where(faces_ext_wound_flat == cc)[0]
188
+ faces_ext_remap_flat[ind_to_map] = mm
189
+
190
+ faces_ext_remap = faces_ext_remap_flat.reshape(faces_shape)
191
+ faces_ext_remap = faces_ext_remap + 1 # back to exodus 1 index
192
+
193
+ # Now we build the SimData object and slice out the node and element
194
+ # variables using the coordinate indexing.
195
+ face_data = mh.SimData(coords=faces_coords,
196
+ connect={"connect1":faces_ext_remap.T},
197
+ time=sim_data.time)
198
+
199
+ if sim_data.node_vars is not None:
200
+ face_data.node_vars = {}
201
+ for nn in sim_data.node_vars:
202
+ face_data.node_vars[nn] = sim_data.node_vars[nn][faces_coord_inds,:]
203
+
204
+ if sim_data.elem_vars is not None:
205
+ face_data.elem_vars = {}
206
+ for ee in sim_data.node_vars:
207
+ face_data.elem_vars[ee] = sim_data.elem_vars[ee][faces_coord_inds,:]
208
+
209
+ return face_data
210
+
211
+
212
+ def _get_pyvista_cell_type(nodes_per_elem: int, spat_dim: int) -> CellType | None:
213
+ """Helper function to identify the pyvista element type in the mesh.
214
+
215
+ Parameters
216
+ ----------
217
+ nodes_per_elem : int
218
+ Number of nodes per element.
219
+ spat_dim : int
220
+ Number of spatial dimensions in the mesh (2 or 3).
221
+
222
+ Returns
223
+ -------
224
+ CellType | None
225
+ Enumeration describing the element type in pyvista.
226
+ """
227
+ cell_type = None
228
+
229
+ if spat_dim == 2:
230
+ if nodes_per_elem == 4:
231
+ cell_type = CellType.QUAD
232
+ elif nodes_per_elem == 3:
233
+ cell_type = CellType.TRIANGLE
234
+ elif nodes_per_elem == 6:
235
+ cell_type = CellType.QUADRATIC_TRIANGLE
236
+ elif nodes_per_elem == 7:
237
+ cell_type = CellType.BIQUADRATIC_TRIANGLE
238
+ elif nodes_per_elem == 8:
239
+ cell_type = CellType.QUADRATIC_QUAD
240
+ elif nodes_per_elem == 9:
241
+ cell_type = CellType.BIQUADRATIC_QUAD
242
+ else:
243
+ if nodes_per_elem == 8:
244
+ cell_type = CellType.HEXAHEDRON
245
+ elif nodes_per_elem == 4:
246
+ cell_type = CellType.TETRA
247
+ elif nodes_per_elem == 10:
248
+ cell_type = CellType.QUADRATIC_TETRA
249
+ elif nodes_per_elem == 20:
250
+ cell_type = CellType.QUADRATIC_HEXAHEDRON
251
+ elif nodes_per_elem == 27:
252
+ cell_type = CellType.TRIQUADRATIC_HEXAHEDRON
253
+
254
+ return cell_type
255
+
256
+
257
+ def _exodus_to_pyvista_connect(cell_type: CellType,
258
+ connect: np.ndarray) -> np.ndarray:
259
+ """Helper function that specifies the nodal winding map for higher order
260
+ tet and hex elements between the exodus output format and pyvista (VTK).
261
+
262
+ Parameters
263
+ ----------
264
+ cell_type : CellType
265
+ pyvista (VTK) cell type enumeration.
266
+ connect : np.ndarray
267
+ Input connectivity table in exodus winding format.
268
+ shape=(nodes_per_elem,num_elems)
269
+
270
+ Returns
271
+ -------
272
+ np.ndarray
273
+ Output connectivity table in pyvista (VTK) format.
274
+ shape=(nodes_per_elem,num_elems)
275
+ """
276
+ copy_connect = np.copy(connect)
277
+
278
+ # NOTE: it looks like VTK does not support TET14
279
+ # VTK and exodus have different winding for 3D higher order quads
280
+ if cell_type == CellType.QUADRATIC_HEXAHEDRON:
281
+ connect[12:16,:] = copy_connect[16:20,:]
282
+ connect[16:20,:] = copy_connect[12:16,:]
283
+ elif cell_type == CellType.TRIQUADRATIC_HEXAHEDRON:
284
+ connect[12:16,:] = copy_connect[16:20,:]
285
+ connect[16:20,:] = copy_connect[12:16,:]
286
+ connect[20:24,:] = copy_connect[23:27,:]
287
+ connect[24,:] = copy_connect[21,:]
288
+ connect[25,:] = copy_connect[22,:]
289
+ connect[26,:] = copy_connect[20,:]
290
+
291
+ return connect
292
+
293
+
294
+ def _get_surf_map(nodes_per_elem: int) -> np.ndarray:
295
+ """Helper function specifying the mapping from 3D tet and hex elements to
296
+ the individual faces consistent with the exodus output format.
297
+
298
+ Parameters
299
+ ----------
300
+ nodes_per_elem : int
301
+ Number of nodes per element.
302
+
303
+ Returns
304
+ -------
305
+ np.ndarray
306
+ Array of indices mapping the nodes to faces with shape=(num_faces,n
307
+ odes_per_face)
308
+
309
+ Raises
310
+ ------
311
+ ValueError
312
+ Element type is not supported.
313
+ """
314
+ if nodes_per_elem == 4: # TET4
315
+ return np.array(((0,1,2),
316
+ (0,3,1),
317
+ (0,2,3),
318
+ (1,3,2)))
319
+
320
+ if nodes_per_elem == 8: # HEX8
321
+ return np.array(((0,1,2,3),
322
+ (0,3,7,4),
323
+ (4,7,6,5),
324
+ (1,5,6,2),
325
+ (0,4,5,1),
326
+ (2,6,7,3)))
327
+
328
+ if nodes_per_elem == 10: # TET10
329
+ return np.array(((0,1,2,4,5,6),
330
+ (0,3,1,4,8,7),
331
+ (0,2,3,6,9,7),
332
+ (1,3,2,8,9,5)))
333
+
334
+ if nodes_per_elem == 20: # HEX20
335
+ return np.array(((0,1,2,3,8,9,10,11),
336
+ (0,3,7,4,11,15,19,12),
337
+ (4,7,6,5,19,18,17,16),
338
+ (1,5,6,2,13,17,14,9),
339
+ (0,4,5,1,12,16,13,8),
340
+ (2,6,7,3,14,18,15,10)))
341
+
342
+ if nodes_per_elem == 27: # HEX27
343
+ return np.array(((0,1,2,3,8,9,10,11,21),
344
+ (0,3,7,4,11,15,19,12,23),
345
+ (4,7,6,5,19,18,17,16,22),
346
+ (1,5,6,2,13,17,14,9,24),
347
+ (0,4,5,1,12,16,13,8,25),
348
+ (2,6,7,3,14,18,15,10,26)))
349
+
350
+ raise ValueError("Number of nodes does not match a 3D element type for " \
351
+ "surface extraction.")
pyvale/fieldsampler.py ADDED
@@ -0,0 +1,111 @@
1
+ # ==============================================================================
2
+ # pyvale: the python validation engine
3
+ # License: MIT
4
+ # Copyright (C) 2025 The Computer Aided Validation Team
5
+ # ==============================================================================
6
+
7
+ import numpy as np
8
+ import pyvista as pv
9
+ from pyvale.field import IField
10
+ from pyvale.sensordata import SensorData
11
+ from pyvale.integratorfactory import build_spatial_averager
12
+
13
+
14
+ def sample_field_with_sensor_data(field: IField, sensor_data: SensorData
15
+ ) -> np.ndarray:
16
+ """Samples (interpolates) an `IField` object using the parameters specified
17
+ in the `SensorData` object.
18
+
19
+ Parameters
20
+ ----------
21
+ field : IField
22
+ The simulated physical field that the sensors will samples from. This is
23
+ normally a `FieldScalar`, `FieldVector` or `FieldTensor`.
24
+ sensor_data : SensorData
25
+ Contains sensor array parameters including: number of sensors, positions
26
+ and sample times. See the `SensorData` class for more information.
27
+
28
+ Returns
29
+ -------
30
+ np.ndarray
31
+ Array of sampled sensor measurements with shape=(num_sensors,
32
+ num_field_components,num_time_steps).
33
+ """
34
+ if sensor_data.spatial_averager is None:
35
+ return field.sample_field(sensor_data.positions,
36
+ sensor_data.sample_times,
37
+ sensor_data.angles)
38
+
39
+ spatial_integrator = build_spatial_averager(field,sensor_data)
40
+ return spatial_integrator.calc_averages()
41
+
42
+
43
+ # NOTE: sampling outside the bounds of the sample returns a value of 0
44
+ def sample_pyvista_grid(components: tuple[str,...],
45
+ pyvista_grid: pv.UnstructuredGrid,
46
+ sim_time_steps: np.ndarray,
47
+ points: np.ndarray,
48
+ sample_times: np.ndarray | None = None
49
+ ) -> np.ndarray:
50
+ """Function for sampling (interpolating) a pyvista grid object containing
51
+ simulated field data. The pyvista sample method uses VTK to perform the
52
+ spatial interpolation using the element shape functions. If the sampling
53
+ time steps are not the same as the simulation time then a linear
54
+ interpolation over time is performed using numpy.
55
+
56
+ NOTE: sampling outside the mesh bounds of the sample returns a value of 0.
57
+
58
+ Parameters
59
+ ----------
60
+ components : tuple[str,...]
61
+ String keys for the components to be sampled in the pyvista grid object.
62
+ Useful for only interpolating the field components of interest for speed
63
+ and memory reduction.
64
+ pyvista_grid : pv.UnstructuredGrid
65
+ Pyvista grid object containing the simulation mesh and the components of
66
+ the physical field that will be sampled.
67
+ sim_time_steps : np.ndarray
68
+ Simulation time steps corresponding to the fields in the pyvista grid
69
+ object.
70
+ points : np.ndarray
71
+ Coordinates of the points at which to sample the pyvista grid object.
72
+ shape=(num_points,3) where the columns are the X, Y and Z coordinates of
73
+ the sample points in simulation world coordintes.
74
+ sample_times : np.ndarray | None, optional
75
+ Array of time steps at which to sample the pyvista grid. If None then no
76
+ temporal interpolation is performed and the sample times are assumed to
77
+ be the simulation time steps.
78
+
79
+ Returns
80
+ -------
81
+ np.ndarray
82
+ Array of sampled sensor measurements with shape=(num_sensors,
83
+ num_field_components,num_time_steps).
84
+ """
85
+ pv_points = pv.PolyData(points)
86
+ sample_data = pv_points.sample(pyvista_grid)
87
+
88
+ n_comps = len(components)
89
+ (n_sensors,n_time_steps) = np.array(sample_data[components[0]]).shape
90
+ sample_at_sim_time = np.empty((n_sensors,n_comps,n_time_steps))
91
+
92
+ for ii,cc in enumerate(components):
93
+ sample_at_sim_time[:,ii,:] = np.array(sample_data[cc])
94
+
95
+ if sample_times is None:
96
+ return sample_at_sim_time
97
+
98
+ def sample_time_interp(x):
99
+ return np.interp(sample_times, sim_time_steps, x)
100
+
101
+ n_time_steps = sample_times.shape[0]
102
+ sample_at_spec_time = np.empty((n_sensors,n_comps,n_time_steps))
103
+
104
+ for ii,cc in enumerate(components):
105
+ sample_at_spec_time[:,ii,:] = np.apply_along_axis(sample_time_interp,-1,
106
+ sample_at_sim_time[:,ii,:])
107
+
108
+ return sample_at_spec_time
109
+
110
+
111
+
pyvale/fieldscalar.py ADDED
@@ -0,0 +1,166 @@
1
+ # ==============================================================================
2
+ # pyvale: the python validation engine
3
+ # License: MIT
4
+ # Copyright (C) 2025 The Computer Aided Validation Team
5
+ # ==============================================================================
6
+
7
+ import numpy as np
8
+ import pyvista as pv
9
+ from scipy.spatial.transform import Rotation
10
+ import mooseherder as mh
11
+
12
+ from pyvale.field import IField
13
+ from pyvale.fieldconverter import simdata_to_pyvista
14
+ from pyvale.fieldsampler import sample_pyvista_grid
15
+
16
+
17
+ class FieldScalar(IField):
18
+ """Class for sampling (interpolating) scalar fields from simulations to
19
+ provide sensor values at specified locations and times.
20
+
21
+ Implements the `IField` interface.
22
+ """
23
+ __slots__ = ("_field_key","_elem_dims","_sim_data","_pyvista_grid",
24
+ "_pyvista_vis")
25
+
26
+ def __init__(self,
27
+ sim_data: mh.SimData,
28
+ field_key: str,
29
+ elem_dims: int) -> None:
30
+ """
31
+ Parameters
32
+ ----------
33
+ sim_data : mh.SimData
34
+ Simulation data object containing the mesh and field to interpolate.
35
+ field_key : str
36
+ String key for the scalar field component in the `SimData` object.
37
+ elem_dims : int
38
+ Number of spatial dimensions (2 or 3) used for identifying element
39
+ types.
40
+ """
41
+
42
+ self._field_key = field_key
43
+ self._elem_dims = elem_dims
44
+
45
+ self._sim_data = sim_data
46
+ (self._pyvista_grid,self._pyvista_vis) = simdata_to_pyvista(
47
+ self._sim_data,
48
+ (self._field_key,),
49
+ self._elem_dims
50
+ )
51
+
52
+ def set_sim_data(self, sim_data: mh.SimData) -> None:
53
+ """Sets the `SimData` object that will be interpolated to obtain sensor
54
+ values. The purpose of this is to be able to apply the same sensor array
55
+ to an array of different simulations by setting a different `SimData`.
56
+
57
+ Parameters
58
+ ----------
59
+ sim_data : mh.SimData
60
+ Mooseherder SimData object. Contains a mesh and a simulated
61
+ physical field.
62
+ """
63
+ self._sim_data = sim_data
64
+ (self._pyvista_grid,self._pyvista_vis) = simdata_to_pyvista(
65
+ sim_data,
66
+ (self._field_key,),
67
+ self._elem_dims
68
+ )
69
+
70
+ def get_sim_data(self) -> mh.SimData:
71
+ """Gets the simulation data object associated with this field. Used by
72
+ pyvale visualisation tools to display simulation data with simulated
73
+ sensor values.
74
+
75
+ Returns
76
+ -------
77
+ mh.SimData
78
+ Mooseherder SimData object. Contains a mesh and a simulated
79
+ physical field.
80
+ """
81
+ return self._sim_data
82
+
83
+ def get_time_steps(self) -> np.ndarray:
84
+ """Gets a 1D array of time steps from the simulation data.
85
+
86
+ Returns
87
+ -------
88
+ np.ndarray
89
+ 1D array of simulation time steps. shape=(num_time_steps,)
90
+ """
91
+ return self._sim_data.time
92
+
93
+ def get_visualiser(self) -> pv.UnstructuredGrid:
94
+ """Gets a pyvista unstructured grid object for visualisation purposes.
95
+
96
+ Returns
97
+ -------
98
+ pv.UnstructuredGrid
99
+ Pyvista unstructured grid object containing only a mesh without any
100
+ physical field data attached.
101
+ """
102
+ return self._pyvista_vis
103
+
104
+ def get_all_components(self) -> tuple[str, ...]:
105
+ """Gets the string key for the component of the physical field. A scalar
106
+ field only has a single component so a tuple of length 1 is returned.
107
+
108
+ Returns
109
+ -------
110
+ tuple[str,...]
111
+ Tuple containing the string key for the physical field.
112
+ """
113
+ return (self._field_key,)
114
+
115
+ def get_component_index(self, comp: str) -> int:
116
+ """Gets the index for a component of the physical field. Used for
117
+ getting the index of a component in the sensor measurement array.
118
+
119
+ Parameters
120
+ ----------
121
+ component : str
122
+ String key for the field component (e.g. 'temperature' or 'disp_x').
123
+
124
+ Returns
125
+ -------
126
+ int
127
+ Index for the selected field component
128
+ """
129
+ return 0 # scalar fields only have one component!
130
+
131
+ def sample_field(self,
132
+ points: np.ndarray,
133
+ times: np.ndarray | None = None,
134
+ angles: tuple[Rotation,...] | None = None,
135
+ ) -> np.ndarray:
136
+ """Samples (interpolates) the simulation field at the specified
137
+ positions, times, and angles.
138
+
139
+ Parameters
140
+ ----------
141
+ points : np.ndarray
142
+ Spatial points to be sampled with the rows indicating the point
143
+ number of the columns indicating the X,Y and Z coordinates.
144
+ times : np.ndarray | None, optional
145
+ Times to sample the underlying simulation. If None then the
146
+ simulation time steps are used and no temporal interpolation is
147
+ performed, by default None.
148
+ angles : tuple[Rotation,...] | None, optional
149
+ Angles to rotate the sampled values into with rotations specified
150
+ with respect to the simulation world coordinates. If a single
151
+ rotation is specified then all points are assumed to have the same
152
+ angle and are batch processed for speed. If None then no rotation is
153
+ performed, by default None.
154
+
155
+ Returns
156
+ -------
157
+ np.ndarray
158
+ An array of sampled (interpolated) values with the following
159
+ dimensions: shape=(num_points,num_components,num_time_steps).
160
+ """
161
+ return sample_pyvista_grid((self._field_key,),
162
+ self._pyvista_grid,
163
+ self._sim_data.time,
164
+ points,
165
+ times)
166
+