pyvale 2025.4.0__py3-none-any.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 (157) hide show
  1. pyvale/__init__.py +75 -0
  2. pyvale/core/__init__.py +7 -0
  3. pyvale/core/analyticmeshgen.py +59 -0
  4. pyvale/core/analyticsimdatafactory.py +63 -0
  5. pyvale/core/analyticsimdatagenerator.py +160 -0
  6. pyvale/core/camera.py +146 -0
  7. pyvale/core/cameradata.py +64 -0
  8. pyvale/core/cameradata2d.py +82 -0
  9. pyvale/core/cameratools.py +328 -0
  10. pyvale/core/cython/rastercyth.c +32267 -0
  11. pyvale/core/cython/rastercyth.py +636 -0
  12. pyvale/core/dataset.py +250 -0
  13. pyvale/core/errorcalculator.py +112 -0
  14. pyvale/core/errordriftcalc.py +146 -0
  15. pyvale/core/errorintegrator.py +339 -0
  16. pyvale/core/errorrand.py +614 -0
  17. pyvale/core/errorsysdep.py +331 -0
  18. pyvale/core/errorsysfield.py +407 -0
  19. pyvale/core/errorsysindep.py +905 -0
  20. pyvale/core/experimentsimulator.py +99 -0
  21. pyvale/core/field.py +136 -0
  22. pyvale/core/fieldconverter.py +154 -0
  23. pyvale/core/fieldsampler.py +112 -0
  24. pyvale/core/fieldscalar.py +167 -0
  25. pyvale/core/fieldtensor.py +221 -0
  26. pyvale/core/fieldtransform.py +384 -0
  27. pyvale/core/fieldvector.py +215 -0
  28. pyvale/core/generatorsrandom.py +528 -0
  29. pyvale/core/imagedef2d.py +566 -0
  30. pyvale/core/integratorfactory.py +241 -0
  31. pyvale/core/integratorquadrature.py +192 -0
  32. pyvale/core/integratorrectangle.py +88 -0
  33. pyvale/core/integratorspatial.py +90 -0
  34. pyvale/core/integratortype.py +44 -0
  35. pyvale/core/optimcheckfuncs.py +153 -0
  36. pyvale/core/raster.py +31 -0
  37. pyvale/core/rastercy.py +76 -0
  38. pyvale/core/rasternp.py +604 -0
  39. pyvale/core/rendermesh.py +156 -0
  40. pyvale/core/sensorarray.py +179 -0
  41. pyvale/core/sensorarrayfactory.py +210 -0
  42. pyvale/core/sensorarraypoint.py +280 -0
  43. pyvale/core/sensordata.py +72 -0
  44. pyvale/core/sensordescriptor.py +101 -0
  45. pyvale/core/sensortools.py +143 -0
  46. pyvale/core/visualexpplotter.py +151 -0
  47. pyvale/core/visualimagedef.py +71 -0
  48. pyvale/core/visualimages.py +75 -0
  49. pyvale/core/visualopts.py +180 -0
  50. pyvale/core/visualsimanimator.py +83 -0
  51. pyvale/core/visualsimplotter.py +182 -0
  52. pyvale/core/visualtools.py +81 -0
  53. pyvale/core/visualtraceplotter.py +256 -0
  54. pyvale/data/__init__.py +7 -0
  55. pyvale/data/case13_out.e +0 -0
  56. pyvale/data/case16_out.e +0 -0
  57. pyvale/data/case17_out.e +0 -0
  58. pyvale/data/case18_1_out.e +0 -0
  59. pyvale/data/case18_2_out.e +0 -0
  60. pyvale/data/case18_3_out.e +0 -0
  61. pyvale/data/case25_out.e +0 -0
  62. pyvale/data/case26_out.e +0 -0
  63. pyvale/data/optspeckle_2464x2056px_spec5px_8bit_gblur1px.tiff +0 -0
  64. pyvale/examples/__init__.py +7 -0
  65. pyvale/examples/analyticdatagen/__init__.py +7 -0
  66. pyvale/examples/analyticdatagen/ex1_1_scalarvisualisation.py +38 -0
  67. pyvale/examples/analyticdatagen/ex1_2_scalarcasebuild.py +46 -0
  68. pyvale/examples/analyticdatagen/ex2_1_analyticsensors.py +83 -0
  69. pyvale/examples/ex1_1_thermal2d.py +89 -0
  70. pyvale/examples/ex1_2_thermal2d.py +111 -0
  71. pyvale/examples/ex1_3_thermal2d.py +113 -0
  72. pyvale/examples/ex1_4_thermal2d.py +89 -0
  73. pyvale/examples/ex1_5_thermal2d.py +105 -0
  74. pyvale/examples/ex2_1_thermal3d .py +87 -0
  75. pyvale/examples/ex2_2_thermal3d.py +51 -0
  76. pyvale/examples/ex2_3_thermal3d.py +109 -0
  77. pyvale/examples/ex3_1_displacement2d.py +47 -0
  78. pyvale/examples/ex3_2_displacement2d.py +79 -0
  79. pyvale/examples/ex3_3_displacement2d.py +104 -0
  80. pyvale/examples/ex3_4_displacement2d.py +105 -0
  81. pyvale/examples/ex4_1_strain2d.py +57 -0
  82. pyvale/examples/ex4_2_strain2d.py +79 -0
  83. pyvale/examples/ex4_3_strain2d.py +100 -0
  84. pyvale/examples/ex5_1_multiphysics2d.py +78 -0
  85. pyvale/examples/ex6_1_multiphysics2d_expsim.py +118 -0
  86. pyvale/examples/ex6_2_multiphysics3d_expsim.py +158 -0
  87. pyvale/examples/features/__init__.py +7 -0
  88. pyvale/examples/features/ex_animation_tools_3dmonoblock.py +83 -0
  89. pyvale/examples/features/ex_area_avg.py +89 -0
  90. pyvale/examples/features/ex_calibration_error.py +108 -0
  91. pyvale/examples/features/ex_chain_field_errs.py +141 -0
  92. pyvale/examples/features/ex_field_errs.py +78 -0
  93. pyvale/examples/features/ex_sensor_single_angle_batch.py +110 -0
  94. pyvale/examples/imagedef2d/ex_imagedef2d_todisk.py +86 -0
  95. pyvale/examples/rasterisation/ex_rastenp.py +154 -0
  96. pyvale/examples/rasterisation/ex_rastercyth_oneframe.py +220 -0
  97. pyvale/examples/rasterisation/ex_rastercyth_static_cypara.py +194 -0
  98. pyvale/examples/rasterisation/ex_rastercyth_static_pypara.py +193 -0
  99. pyvale/simcases/case00_HEX20.i +242 -0
  100. pyvale/simcases/case00_HEX27.i +242 -0
  101. pyvale/simcases/case00_TET10.i +242 -0
  102. pyvale/simcases/case00_TET14.i +242 -0
  103. pyvale/simcases/case01.i +101 -0
  104. pyvale/simcases/case02.i +156 -0
  105. pyvale/simcases/case03.i +136 -0
  106. pyvale/simcases/case04.i +181 -0
  107. pyvale/simcases/case05.i +234 -0
  108. pyvale/simcases/case06.i +305 -0
  109. pyvale/simcases/case07.geo +135 -0
  110. pyvale/simcases/case07.i +87 -0
  111. pyvale/simcases/case08.geo +144 -0
  112. pyvale/simcases/case08.i +153 -0
  113. pyvale/simcases/case09.geo +204 -0
  114. pyvale/simcases/case09.i +87 -0
  115. pyvale/simcases/case10.geo +204 -0
  116. pyvale/simcases/case10.i +257 -0
  117. pyvale/simcases/case11.geo +337 -0
  118. pyvale/simcases/case11.i +147 -0
  119. pyvale/simcases/case12.geo +388 -0
  120. pyvale/simcases/case12.i +329 -0
  121. pyvale/simcases/case13.i +140 -0
  122. pyvale/simcases/case14.i +159 -0
  123. pyvale/simcases/case15.geo +337 -0
  124. pyvale/simcases/case15.i +150 -0
  125. pyvale/simcases/case16.geo +391 -0
  126. pyvale/simcases/case16.i +357 -0
  127. pyvale/simcases/case17.geo +135 -0
  128. pyvale/simcases/case17.i +144 -0
  129. pyvale/simcases/case18.i +254 -0
  130. pyvale/simcases/case18_1.i +254 -0
  131. pyvale/simcases/case18_2.i +254 -0
  132. pyvale/simcases/case18_3.i +254 -0
  133. pyvale/simcases/case19.geo +252 -0
  134. pyvale/simcases/case19.i +99 -0
  135. pyvale/simcases/case20.geo +252 -0
  136. pyvale/simcases/case20.i +250 -0
  137. pyvale/simcases/case21.geo +74 -0
  138. pyvale/simcases/case21.i +155 -0
  139. pyvale/simcases/case22.geo +82 -0
  140. pyvale/simcases/case22.i +140 -0
  141. pyvale/simcases/case23.geo +164 -0
  142. pyvale/simcases/case23.i +140 -0
  143. pyvale/simcases/case24.geo +79 -0
  144. pyvale/simcases/case24.i +123 -0
  145. pyvale/simcases/case25.geo +82 -0
  146. pyvale/simcases/case25.i +140 -0
  147. pyvale/simcases/case26.geo +166 -0
  148. pyvale/simcases/case26.i +140 -0
  149. pyvale/simcases/run_1case.py +61 -0
  150. pyvale/simcases/run_all_cases.py +69 -0
  151. pyvale/simcases/run_build_case.py +64 -0
  152. pyvale/simcases/run_example_cases.py +69 -0
  153. pyvale-2025.4.0.dist-info/METADATA +140 -0
  154. pyvale-2025.4.0.dist-info/RECORD +157 -0
  155. pyvale-2025.4.0.dist-info/WHEEL +5 -0
  156. pyvale-2025.4.0.dist-info/licenses/LICENSE +21 -0
  157. pyvale-2025.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,99 @@
1
+ """
2
+ ================================================================================
3
+ pyvale: the python validation engine
4
+ License: MIT
5
+ Copyright (C) 2025 The Computer Aided Validation Team
6
+ ================================================================================
7
+ """
8
+ from dataclasses import dataclass
9
+ import numpy as np
10
+ from pyvale.core.sensorarray import ISensorArray
11
+ import mooseherder as mh
12
+
13
+ # NOTE: This module is a feature under developement.
14
+
15
+ @dataclass(slots=True)
16
+ class ExperimentStats:
17
+ """Dataclass holding summary statistics for a series of simulated
18
+ experiments.
19
+ """
20
+ mean: np.ndarray | None = None
21
+ std: np.ndarray | None = None
22
+ cov: np.ndarray | None = None
23
+ max: np.ndarray | None = None
24
+ min: np.ndarray | None = None
25
+ med: np.ndarray | None = None
26
+ q25: np.ndarray | None = None
27
+ q75: np.ndarray | None = None
28
+ mad: np.ndarray | None = None
29
+
30
+
31
+ class ExperimentSimulator:
32
+ """An experiment simulator for running monte-carlo analysis by applying a
33
+ list of sensor arrays to a list of simulations over a given number of user
34
+ defined experiments. Calculates summary statistics for each sensor array
35
+ applied to each simulation.
36
+ """
37
+ __slots__ = ("sim_list","sensor_arrays","num_exp_per_sim","_exp_data",
38
+ "_exp_stats")
39
+
40
+ def __init__(self,
41
+ sim_list: list[mh.SimData],
42
+ sensor_arrays: list[ISensorArray],
43
+ num_exp_per_sim: int
44
+ ) -> None:
45
+
46
+ self.sim_list = sim_list
47
+ self.sensor_arrays = sensor_arrays
48
+ self.num_exp_per_sim = num_exp_per_sim
49
+ self._exp_data = [None]*len(self.sensor_arrays)
50
+ self._exp_stats = [None]*len(self.sensor_arrays)
51
+
52
+ def get_data(self) -> list[np.ndarray | None]:
53
+ return self._exp_data
54
+
55
+ def get_stats(self) -> list[np.ndarray | None]:
56
+ return self._exp_stats
57
+
58
+ def run_experiments(self) -> list[np.ndarray]:
59
+ n_sims = len(self.sim_list)
60
+ # shape=list[n_arrays](n_sims,n_exps,n_sens,n_comps,n_time_steps)
61
+ self._exp_data = [None]*len(self.sensor_arrays)
62
+
63
+ for ii,aa in enumerate(self.sensor_arrays):
64
+ meas_array = np.zeros((n_sims,self.num_exp_per_sim)+
65
+ aa.get_measurement_shape())
66
+
67
+ for jj,ss in enumerate(self.sim_list):
68
+ aa.get_field().set_sim_data(ss)
69
+
70
+ for ee in range(self.num_exp_per_sim):
71
+ meas_array[jj,ee,:,:,:] = aa.calc_measurements()
72
+
73
+ self._exp_data[ii] = meas_array
74
+
75
+ return self._exp_data
76
+
77
+
78
+ def calc_stats(self) -> list[ExperimentStats]:
79
+ # shape=list[n_arrays](n_sims,n_exps,n_sens,n_comps,n_time_steps)
80
+ self._exp_stats = [None]*len(self.sensor_arrays)
81
+ for ii,_ in enumerate(self.sensor_arrays):
82
+ array_stats = ExperimentStats()
83
+ array_stats.max = np.max(self._exp_data[ii],axis=1)
84
+ array_stats.min = np.min(self._exp_data[ii],axis=1)
85
+ array_stats.mean = np.mean(self._exp_data[ii],axis=1)
86
+ array_stats.std = np.std(self._exp_data[ii],axis=1)
87
+ array_stats.med = np.median(self._exp_data[ii],axis=1)
88
+ array_stats.q25 = np.quantile(self._exp_data[ii],0.25,axis=1)
89
+ array_stats.q75 = np.quantile(self._exp_data[ii],0.75,axis=1)
90
+ array_stats.mad = np.median(np.abs(self._exp_data[ii] -
91
+ np.median(self._exp_data[ii],axis=1,keepdims=True)),axis=1)
92
+ self._exp_stats[ii] = array_stats
93
+
94
+ return self._exp_stats
95
+
96
+
97
+
98
+
99
+
pyvale/core/field.py ADDED
@@ -0,0 +1,136 @@
1
+ """
2
+ ================================================================================
3
+ pyvale: the python validation engine
4
+ License: MIT
5
+ Copyright (C) 2025 The Computer Aided Validation Team
6
+ ================================================================================
7
+ """
8
+ from abc import ABC, abstractmethod
9
+ import numpy as np
10
+ from scipy.spatial.transform import Rotation
11
+ import pyvista as pv
12
+ import mooseherder as mh
13
+
14
+
15
+ class IField(ABC):
16
+ """Interface (abstract base class) for sampling (interpolating) physical
17
+ fields from simulations to provide sensor values at specified locations and
18
+ times.
19
+ """
20
+
21
+ @abstractmethod
22
+ def set_sim_data(self,sim_data: mh.SimData) -> None:
23
+ """Abstract method. Sets the SimData object that will be interpolated to
24
+ obtain sensor values. The purpose of this is to be able to apply the
25
+ same sensor array to an array of different simulations.
26
+
27
+ Parameters
28
+ ----------
29
+ sim_data : mh.SimData
30
+ Mooseherder SimData object. Contains a mesh and a simulated
31
+ physical field.
32
+ """
33
+ pass
34
+
35
+ @abstractmethod
36
+ def get_sim_data(self) -> mh.SimData:
37
+ """Abstract method. Gets the simulation data object associated with this
38
+ field. Used by pyvale visualisation tools to display simulation data
39
+ with simulated sensor values.
40
+
41
+ Returns
42
+ -------
43
+ mh.SimData
44
+ Mooseherder SimData object. Contains a mesh and a simulated
45
+ physical field.
46
+ """
47
+ pass
48
+
49
+ @abstractmethod
50
+ def get_time_steps(self) -> np.ndarray:
51
+ """Abstract method. Gets a 1D array of time steps from the simulation
52
+ data.
53
+
54
+ Returns
55
+ -------
56
+ np.ndarray
57
+ 1D array of simulation time steps. shape=(num_time_steps,)
58
+ """
59
+ pass
60
+
61
+ @abstractmethod
62
+ def get_visualiser(self) -> pv.UnstructuredGrid:
63
+ """Abstract method. Gets a pyvista unstructured grid object for
64
+ visualisation purposes.
65
+
66
+ Returns
67
+ -------
68
+ pv.UnstructuredGrid
69
+ Pyvista unstructured grid object containing only a mesh without any
70
+ physical field data attached.
71
+ """
72
+ pass
73
+
74
+ @abstractmethod
75
+ def get_all_components(self) -> tuple[str,...]:
76
+ """Gets the string keys for the component of the physical field. For
77
+ example: a scalar field might just have ('temperature',) whereas a
78
+ vector field might have ('disp_x','disp_y','disp_z').
79
+
80
+ Returns
81
+ -------
82
+ tuple[str,...]
83
+ Tuple containing the string keys for all components of the physical
84
+ field.
85
+ """
86
+ pass
87
+
88
+ @abstractmethod
89
+ def get_component_index(self,component: str) -> int:
90
+ """Gets the index for a component of the physical field. Used for
91
+ getting the index of a component in the sensor measurement array.
92
+
93
+ Parameters
94
+ ----------
95
+ component : str
96
+ String key for the field component (e.g. 'temperature' or 'disp_x').
97
+
98
+ Returns
99
+ -------
100
+ int
101
+ Index for the selected field component
102
+ """
103
+ pass
104
+
105
+ @abstractmethod
106
+ def sample_field(self,
107
+ points: np.ndarray,
108
+ times: np.ndarray | None = None,
109
+ angles: tuple[Rotation,...] | None = None,
110
+ ) -> np.ndarray:
111
+ """Samples (interpolates) the simulation field at the specified
112
+ positions, times, and angles.
113
+
114
+ Parameters
115
+ ----------
116
+ points : np.ndarray
117
+ Spatial points to be sampled with the rows indicating the point
118
+ number of the columns indicating the X,Y and Z coordinates.
119
+ times : np.ndarray | None, optional
120
+ Times to sample the underlying simulation. If None then the
121
+ simulation time steps are used and no temporal interpolation is
122
+ performed, by default None.
123
+ angles : tuple[Rotation,...] | None, optional
124
+ Angles to rotate the sampled values into with rotations specified
125
+ with respect to the simulation world coordinates. If a single
126
+ rotation is specified then all points are assumed to have the same
127
+ angle and are batch processed for speed. If None then no rotation is
128
+ performed, by default None.
129
+
130
+ Returns
131
+ -------
132
+ np.ndarray
133
+ An array of sampled (interpolated) values with the following
134
+ dimensions: shape=(num_points,num_components,num_time_steps).
135
+ """
136
+ pass
@@ -0,0 +1,154 @@
1
+ """
2
+ ================================================================================
3
+ pyvale: the python validation engine
4
+ License: MIT
5
+ Copyright (C) 2025 The Computer Aided Validation Team
6
+ ================================================================================
7
+ """
8
+ import warnings
9
+ import numpy as np
10
+ import pyvista as pv
11
+ from pyvista import CellType
12
+ import mooseherder as mh
13
+
14
+
15
+ def simdata_to_pyvista(sim_data: mh.SimData,
16
+ components: tuple[str,...] | None,
17
+ spat_dim: int
18
+ ) -> tuple[pv.UnstructuredGrid,pv.UnstructuredGrid]:
19
+ """Converts the mesh and field data in a `SimData` object into a pyvista
20
+ UnstructuredGrid for sampling (interpolating) the data and visualisation.
21
+
22
+ Parameters
23
+ ----------
24
+ sim_data : mh.SimData
25
+ Object containing a mesh and associated field data from a simulation.
26
+ components : tuple[str,...] | None
27
+ String keys for the components of the field to extract from the
28
+ simulation data.
29
+ spat_dim : int
30
+ Number of spatial dimensions (2 or 3) used to determine the element
31
+ types in the mesh from the number of nodes per element.
32
+
33
+ Returns
34
+ -------
35
+ tuple[pv.UnstructuredGrid,pv.UnstructuredGrid]
36
+ The first UnstructuredGrid has the field components attached as dataset
37
+ arrays. The second has no field data attached for visualisation.
38
+ """
39
+ flat_connect = np.array([],dtype=np.int64)
40
+ cell_types = np.array([],dtype=np.int64)
41
+
42
+ for cc in sim_data.connect:
43
+ # NOTE: need the -1 here to make element numbers 0 indexed!
44
+ this_connect = np.copy(sim_data.connect[cc])-1
45
+ (nodes_per_elem,n_elems) = this_connect.shape
46
+
47
+ this_cell_type = _get_pyvista_cell_type(nodes_per_elem,spat_dim)
48
+
49
+ # VTK and exodus have different winding for 3D higher order quads
50
+ this_connect = _exodus_to_pyvista_connect(this_cell_type,this_connect)
51
+
52
+ this_connect = this_connect.T.flatten()
53
+ idxs = np.arange(0,n_elems*nodes_per_elem,nodes_per_elem,dtype=np.int64)
54
+
55
+ this_connect = np.insert(this_connect,idxs,nodes_per_elem)
56
+
57
+ cell_types = np.hstack((cell_types,np.full(n_elems,this_cell_type)))
58
+ flat_connect = np.hstack((flat_connect,this_connect),dtype=np.int64)
59
+
60
+ cells = flat_connect
61
+
62
+ points = sim_data.coords
63
+ pv_grid = pv.UnstructuredGrid(cells, cell_types, points)
64
+ pv_grid_vis = pv.UnstructuredGrid(cells, cell_types, points)
65
+
66
+ if components is not None and sim_data.node_vars is not None:
67
+ for cc in components:
68
+ pv_grid[cc] = sim_data.node_vars[cc]
69
+
70
+ return (pv_grid,pv_grid_vis)
71
+
72
+
73
+ def _get_pyvista_cell_type(nodes_per_elem: int, spat_dim: int) -> CellType:
74
+ """Helper function to identify the pyvista element type in the mesh.
75
+
76
+ Parameters
77
+ ----------
78
+ nodes_per_elem : int
79
+ Number of nodes per element.
80
+ spat_dim : int
81
+ Number of spatial dimensions in the mesh (2 or 3).
82
+
83
+ Returns
84
+ -------
85
+ CellType
86
+ Enumeration describing the element type in pyvista.
87
+ """
88
+ cell_type = 0
89
+
90
+ if spat_dim == 2:
91
+ if nodes_per_elem == 4:
92
+ cell_type = CellType.QUAD
93
+ elif nodes_per_elem == 3:
94
+ cell_type = CellType.TRIANGLE
95
+ elif nodes_per_elem == 6:
96
+ cell_type = CellType.QUADRATIC_TRIANGLE
97
+ elif nodes_per_elem == 7:
98
+ cell_type = CellType.BIQUADRATIC_TRIANGLE
99
+ elif nodes_per_elem == 8:
100
+ cell_type = CellType.QUADRATIC_QUAD
101
+ elif nodes_per_elem == 9:
102
+ cell_type = CellType.BIQUADRATIC_QUAD
103
+ else:
104
+ warnings.warn(f"Cell type 2D with {nodes_per_elem} "
105
+ + "nodes not recognised. Defaulting to 4 node QUAD")
106
+ cell_type = CellType.QUAD
107
+ else:
108
+ if nodes_per_elem == 8:
109
+ cell_type = CellType.HEXAHEDRON
110
+ elif nodes_per_elem == 4:
111
+ cell_type = CellType.TETRA
112
+ elif nodes_per_elem == 10:
113
+ cell_type = CellType.QUADRATIC_TETRA
114
+ elif nodes_per_elem == 20:
115
+ cell_type = CellType.QUADRATIC_HEXAHEDRON
116
+ elif nodes_per_elem == 27:
117
+ cell_type = CellType.TRIQUADRATIC_HEXAHEDRON
118
+ else:
119
+ warnings.warn(f"Cell type 3D with {nodes_per_elem} "
120
+ + "nodes not recognised. Defaulting to 8 node HEX")
121
+ cell_type = CellType.HEXAHEDRON
122
+
123
+ return cell_type
124
+
125
+
126
+ def _exodus_to_pyvista_connect(cell_type: CellType, connect: np.ndarray) -> np.ndarray:
127
+ copy_connect = np.copy(connect)
128
+
129
+ # NOTE: it looks like VTK does not support TET14
130
+ # VTK and exodus have different winding for 3D higher order quads
131
+ if cell_type == CellType.QUADRATIC_HEXAHEDRON:
132
+ connect[12:16,:] = copy_connect[16:20,:]
133
+ connect[16:20,:] = copy_connect[12:16,:]
134
+ elif cell_type == CellType.TRIQUADRATIC_HEXAHEDRON:
135
+ connect[12:16,:] = copy_connect[16:20,:]
136
+ connect[16:20,:] = copy_connect[12:16,:]
137
+ connect[20:24,:] = copy_connect[23:27,:]
138
+ connect[24,:] = copy_connect[21,:]
139
+ connect[25,:] = copy_connect[22,:]
140
+ connect[26,:] = copy_connect[20,:]
141
+
142
+ return connect
143
+
144
+ def scale_length_units(sim_data: mh.SimData,
145
+ disp_comps: tuple[str,...],
146
+ scale: float) -> mh.SimData:
147
+
148
+ sim_data.coords = sim_data.coords*scale
149
+ for cc in disp_comps:
150
+ sim_data.node_vars[cc] = sim_data.node_vars[cc]*scale
151
+
152
+ return sim_data
153
+
154
+
@@ -0,0 +1,112 @@
1
+ """
2
+ ================================================================================
3
+ pyvale: the python validation engine
4
+ License: MIT
5
+ Copyright (C) 2025 The Computer Aided Validation Team
6
+ ================================================================================
7
+ """
8
+ import numpy as np
9
+ import pyvista as pv
10
+ from pyvale.core.field import IField
11
+ from pyvale.core.sensordata import SensorData
12
+ from pyvale.core.integratorfactory import build_spatial_averager
13
+
14
+
15
+ def sample_field_with_sensor_data(field: IField, sensor_data: SensorData
16
+ ) -> np.ndarray:
17
+ """Samples (interpolates) an `IField` object using the parameters specified
18
+ in the `SensorData` object.
19
+
20
+ Parameters
21
+ ----------
22
+ field : IField
23
+ The simulated physical field that the sensors will samples from. This is
24
+ normally a `FieldScalar`, `FieldVector` or `FieldTensor`.
25
+ sensor_data : SensorData
26
+ Contains sensor array parameters including: number of sensors, positions
27
+ and sample times. See the `SensorData` class for more information.
28
+
29
+ Returns
30
+ -------
31
+ np.ndarray
32
+ Array of sampled sensor measurements with shape=(num_sensors,
33
+ num_field_components,num_time_steps).
34
+ """
35
+ if sensor_data.spatial_averager is None:
36
+ return field.sample_field(sensor_data.positions,
37
+ sensor_data.sample_times,
38
+ sensor_data.angles)
39
+
40
+ spatial_integrator = build_spatial_averager(field,sensor_data)
41
+ return spatial_integrator.calc_averages()
42
+
43
+
44
+ # NOTE: sampling outside the bounds of the sample returns a value of 0
45
+ def sample_pyvista_grid(components: tuple[str,...],
46
+ pyvista_grid: pv.UnstructuredGrid,
47
+ sim_time_steps: np.ndarray,
48
+ points: np.ndarray,
49
+ sample_times: np.ndarray | None = None
50
+ ) -> np.ndarray:
51
+ """Function for sampling (interpolating) a pyvista grid object containing
52
+ simulated field data. The pyvista sample method uses VTK to perform the
53
+ spatial interpolation using the element shape functions. If the sampling
54
+ time steps are not the same as the simulation time then a linear
55
+ interpolation over time is performed using numpy.
56
+
57
+ NOTE: sampling outside the mesh bounds of the sample returns a value of 0.
58
+
59
+ Parameters
60
+ ----------
61
+ components : tuple[str,...]
62
+ String keys for the components to be sampled in the pyvista grid object.
63
+ Useful for only interpolating the field components of interest for speed
64
+ and memory reduction.
65
+ pyvista_grid : pv.UnstructuredGrid
66
+ Pyvista grid object containing the simulation mesh and the components of
67
+ the physical field that will be sampled.
68
+ sim_time_steps : np.ndarray
69
+ Simulation time steps corresponding to the fields in the pyvista grid
70
+ object.
71
+ points : np.ndarray
72
+ Coordinates of the points at which to sample the pyvista grid object.
73
+ shape=(num_points,3) where the columns are the X, Y and Z coordinates of
74
+ the sample points in simulation world coordintes.
75
+ sample_times : np.ndarray | None, optional
76
+ Array of time steps at which to sample the pyvista grid. If None then no
77
+ temporal interpolation is performed and the sample times are assumed to
78
+ be the simulation time steps.
79
+
80
+ Returns
81
+ -------
82
+ np.ndarray
83
+ Array of sampled sensor measurements with shape=(num_sensors,
84
+ num_field_components,num_time_steps).
85
+ """
86
+ pv_points = pv.PolyData(points)
87
+ sample_data = pv_points.sample(pyvista_grid)
88
+
89
+ n_comps = len(components)
90
+ (n_sensors,n_time_steps) = np.array(sample_data[components[0]]).shape
91
+ sample_at_sim_time = np.empty((n_sensors,n_comps,n_time_steps))
92
+
93
+ for ii,cc in enumerate(components):
94
+ sample_at_sim_time[:,ii,:] = np.array(sample_data[cc])
95
+
96
+ if sample_times is None:
97
+ return sample_at_sim_time
98
+
99
+ def sample_time_interp(x):
100
+ return np.interp(sample_times, sim_time_steps, x)
101
+
102
+ n_time_steps = sample_times.shape[0]
103
+ sample_at_spec_time = np.empty((n_sensors,n_comps,n_time_steps))
104
+
105
+ for ii,cc in enumerate(components):
106
+ sample_at_spec_time[:,ii,:] = np.apply_along_axis(sample_time_interp,-1,
107
+ sample_at_sim_time[:,ii,:])
108
+
109
+ return sample_at_spec_time
110
+
111
+
112
+
@@ -0,0 +1,167 @@
1
+ """
2
+ ================================================================================
3
+ pyvale: the python validation engine
4
+ License: MIT
5
+ Copyright (C) 2025 The Computer Aided Validation Team
6
+ ================================================================================
7
+ """
8
+ import numpy as np
9
+ import pyvista as pv
10
+ from scipy.spatial.transform import Rotation
11
+ import mooseherder as mh
12
+
13
+ from pyvale.core.field import IField
14
+ from pyvale.core.fieldconverter import simdata_to_pyvista
15
+ from pyvale.core.fieldsampler import sample_pyvista_grid
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","_spat_dims","_sim_data","_pyvista_grid",
24
+ "_pyvista_vis")
25
+
26
+ def __init__(self,
27
+ sim_data: mh.SimData,
28
+ field_key: str,
29
+ spat_dims: int) -> None:
30
+ """Initialiser for the `FieldScalar` class.
31
+
32
+ Parameters
33
+ ----------
34
+ sim_data : mh.SimData
35
+ Simulation data object containing the mesh and field to interpolate.
36
+ field_key : str
37
+ String key for the scalar field component in the `SimData` object.
38
+ spat_dims : int
39
+ Number of spatial dimensions (2 or 3) used for identifying element
40
+ types.
41
+ """
42
+
43
+ self._field_key = field_key
44
+ self._spat_dims = spat_dims
45
+
46
+ self._sim_data = sim_data
47
+ (self._pyvista_grid,self._pyvista_vis) = simdata_to_pyvista(
48
+ self._sim_data,
49
+ (self._field_key,),
50
+ self._spat_dims
51
+ )
52
+
53
+ def set_sim_data(self, sim_data: mh.SimData) -> None:
54
+ """Sets the `SimData` object that will be interpolated to obtain sensor
55
+ values. The purpose of this is to be able to apply the same sensor array
56
+ to an array of different simulations by setting a different `SimData`.
57
+
58
+ Parameters
59
+ ----------
60
+ sim_data : mh.SimData
61
+ Mooseherder SimData object. Contains a mesh and a simulated
62
+ physical field.
63
+ """
64
+ self._sim_data = sim_data
65
+ (self._pyvista_grid,self._pyvista_vis) = simdata_to_pyvista(
66
+ sim_data,
67
+ (self._field_key,),
68
+ self._spat_dims
69
+ )
70
+
71
+ def get_sim_data(self) -> mh.SimData:
72
+ """Gets the simulation data object associated with this field. Used by
73
+ pyvale visualisation tools to display simulation data with simulated
74
+ sensor values.
75
+
76
+ Returns
77
+ -------
78
+ mh.SimData
79
+ Mooseherder SimData object. Contains a mesh and a simulated
80
+ physical field.
81
+ """
82
+ return self._sim_data
83
+
84
+ def get_time_steps(self) -> np.ndarray:
85
+ """Gets a 1D array of time steps from the simulation data.
86
+
87
+ Returns
88
+ -------
89
+ np.ndarray
90
+ 1D array of simulation time steps. shape=(num_time_steps,)
91
+ """
92
+ return self._sim_data.time
93
+
94
+ def get_visualiser(self) -> pv.UnstructuredGrid:
95
+ """Gets a pyvista unstructured grid object for visualisation purposes.
96
+
97
+ Returns
98
+ -------
99
+ pv.UnstructuredGrid
100
+ Pyvista unstructured grid object containing only a mesh without any
101
+ physical field data attached.
102
+ """
103
+ return self._pyvista_vis
104
+
105
+ def get_all_components(self) -> tuple[str, ...]:
106
+ """Gets the string key for the component of the physical field. A scalar
107
+ field only has a single component so a tuple of length 1 is returned.
108
+
109
+ Returns
110
+ -------
111
+ tuple[str,...]
112
+ Tuple containing the string key for the physical field.
113
+ """
114
+ return (self._field_key,)
115
+
116
+ def get_component_index(self, comp: str) -> int:
117
+ """Gets the index for a component of the physical field. Used for
118
+ getting the index of a component in the sensor measurement array.
119
+
120
+ Parameters
121
+ ----------
122
+ component : str
123
+ String key for the field component (e.g. 'temperature' or 'disp_x').
124
+
125
+ Returns
126
+ -------
127
+ int
128
+ Index for the selected field component
129
+ """
130
+ return 0 # scalar fields only have one component!
131
+
132
+ def sample_field(self,
133
+ points: np.ndarray,
134
+ times: np.ndarray | None = None,
135
+ angles: tuple[Rotation,...] | None = None,
136
+ ) -> np.ndarray:
137
+ """Samples (interpolates) the simulation field at the specified
138
+ positions, times, and angles.
139
+
140
+ Parameters
141
+ ----------
142
+ points : np.ndarray
143
+ Spatial points to be sampled with the rows indicating the point
144
+ number of the columns indicating the X,Y and Z coordinates.
145
+ times : np.ndarray | None, optional
146
+ Times to sample the underlying simulation. If None then the
147
+ simulation time steps are used and no temporal interpolation is
148
+ performed, by default None.
149
+ angles : tuple[Rotation,...] | None, optional
150
+ Angles to rotate the sampled values into with rotations specified
151
+ with respect to the simulation world coordinates. If a single
152
+ rotation is specified then all points are assumed to have the same
153
+ angle and are batch processed for speed. If None then no rotation is
154
+ performed, by default None.
155
+
156
+ Returns
157
+ -------
158
+ np.ndarray
159
+ An array of sampled (interpolated) values with the following
160
+ dimensions: shape=(num_points,num_components,num_time_steps).
161
+ """
162
+ return sample_pyvista_grid((self._field_key,),
163
+ self._pyvista_grid,
164
+ self._sim_data.time,
165
+ points,
166
+ times)
167
+