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,280 @@
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
+ from pyvale.core.field import IField
10
+ from pyvale.core.sensorarray import ISensorArray
11
+ from pyvale.core.errorintegrator import ErrIntegrator
12
+ from pyvale.core.sensordescriptor import SensorDescriptor
13
+ from pyvale.core.sensordata import SensorData
14
+ from pyvale.core.fieldsampler import sample_field_with_sensor_data
15
+
16
+
17
+ class SensorArrayPoint(ISensorArray):
18
+ """A class for creating arrays of point sensors applied to a simulated
19
+ physical field. Examples include: thermocouples used to measure temperature
20
+ (a scalar field) or strain gauges used to measure strain (a tensor field).
21
+ Implements the ISensorArray interface.
22
+
23
+ This class uses the `pyvale` sensor measurement simulation model. Here a
24
+ measurement is taken as: measurement = truth + random errors + systematic
25
+ errors. The truth value for each sensor is interpolated from the physical
26
+ field (an implementation of the `IField` interface, nominally a
27
+ `FieldScalar`, `FieldVector` or `FieldTensor` object).
28
+
29
+ The random and systematic errors are calculated by a user specified error
30
+ integrator (`ErrIntegrator` class). This class contains a chain of different
31
+ types of user selected errors (implementations of the `IErrCalculator`
32
+ interface). Further information can be found in the `ErrIntegrator` class
33
+ and in implementations of the `IErrCalculator` interface.
34
+
35
+ In `pyvale`, function and methods with `calc` in their name will cause
36
+ probability distributions to be resampled and any additional calculations,
37
+ such as interpolation, to be performed. Functions and methods with `get` in
38
+ the name will directly return the previously calculated values without
39
+ resampling probability distributions.
40
+
41
+ Calling the class method `calc_measurements()` will create and return an
42
+ array of simulated sensor measurements with the following shape=(num_sensors
43
+ ,num_field_component,num_time_steps). When calling `calc_measurements()` all
44
+ sensor errors that are based on probability distributions are resampled and
45
+ any required interpolations are performed (e.g. a random perturbation of the
46
+ sensor positions requiring interpolation at the perturbed sensor location).
47
+
48
+ Calling the class method `get_measurements()` just returns the previously
49
+ calculated set of sensor measurements without resampling of probability.
50
+ Distributions.
51
+
52
+ Without an error integrator this class can be used for interpolating
53
+ simulated physical fields quickly using finite element shape functions.
54
+ """
55
+
56
+ __slots__ = ("field","descriptor","sensor_data","_truth","_measurements",
57
+ "error_integrator")
58
+
59
+ def __init__(self,
60
+ sensor_data: SensorData,
61
+ field: IField,
62
+ descriptor: SensorDescriptor | None = None,
63
+ ) -> None:
64
+ """Initialiser for the `SensorArrayPoint` class.
65
+
66
+ Parameters
67
+ ----------
68
+ sensor_data : SensorData
69
+ Specifies sensor array parameters including: positions, sample times
70
+ , angles, and area averaging. See the `SensorData` dataclass for
71
+ details.
72
+ field : IField
73
+ The simulated physical field that the sensors will samples from.
74
+ This is normally a `FieldScalar`, `FieldVector` or `FieldTensor`.
75
+ descriptor : SensorDescriptor | None, optional
76
+ Contains descriptive information about the sensor array for display
77
+ and visualisations, by default None.
78
+ """
79
+ self.sensor_data = sensor_data
80
+ self.field = field
81
+ self.error_integrator = None
82
+
83
+ self.descriptor = SensorDescriptor()
84
+ if descriptor is not None:
85
+ self.descriptor = descriptor
86
+
87
+ self._truth = None
88
+ self._measurements = None
89
+
90
+ def get_sample_times(self) -> np.ndarray:
91
+ """Gets the times at which the sensors sample the given physical field.
92
+ This is specified by the user in the SensorData object or defaults to
93
+ the time steps in the underlying simulation if unspecified.
94
+
95
+ Returns
96
+ -------
97
+ np.ndarray
98
+ Sample times with shape: (num_time_steps,)
99
+ """
100
+ if self.sensor_data.sample_times is None:
101
+ return self.field.get_time_steps()
102
+
103
+ return self.sensor_data.sample_times
104
+
105
+ def get_measurement_shape(self) -> tuple[int,int,int]:
106
+ """Gets the shape of the sensor measurement array. shape=(num_sensors,
107
+ num_field_components,num_time_steps)
108
+
109
+ Returns
110
+ -------
111
+ tuple[int,int,int]
112
+ Shape of the measurement array. shape=(num_sensors,
113
+ num_field_components,num_time_steps)
114
+ """
115
+
116
+ return (self.sensor_data.positions.shape[0],
117
+ len(self.field.get_all_components()),
118
+ self.get_sample_times().shape[0])
119
+
120
+ def get_field(self) -> IField:
121
+ """Gets a reference to the physical field that this sensor array
122
+ is applied to.
123
+
124
+ Returns
125
+ -------
126
+ IField
127
+ Reference to an `IField` interface.
128
+ """
129
+ return self.field
130
+
131
+
132
+ def calc_truth_values(self) -> np.ndarray:
133
+ """Calculates the ground truth sensor values by interpolating the
134
+ simulated physical field using the sensor array parameters in the
135
+ `SensorData` object.
136
+
137
+ Returns
138
+ -------
139
+ np.ndarray
140
+ Array of ground truth sensor values. shape=(num_sensors,
141
+ num_field_components,num_time_steps).
142
+ """
143
+ self._truth = sample_field_with_sensor_data(self.field,
144
+ self.sensor_data)
145
+
146
+ return self._truth
147
+
148
+ def get_truth(self) -> np.ndarray:
149
+ """Gets the ground truth sensor values that were calculated previously.
150
+ If the ground truth values have not been calculated then
151
+ `calc_truth_values()` is called first.
152
+
153
+ Returns
154
+ -------
155
+ np.ndarray
156
+ Array of ground truth sensor values. shape=(num_sensors,
157
+ num_field_components,num_time_steps).
158
+ """
159
+ if self._truth is None:
160
+ self._truth = self.calc_truth_values()
161
+
162
+ return self._truth
163
+
164
+ def set_error_integrator(self, err_int: ErrIntegrator) -> None:
165
+ """Sets the error intergrator that will be used to calculate the sensor
166
+ array measurement errors when `calc_measurements()` is called. See the
167
+ `ErrIntegrator` class for further detail.
168
+
169
+ Parameters
170
+ ----------
171
+ err_int : ErrIntegrator
172
+ Error integration object with a chain of user defined sensor errors.
173
+ """
174
+ self.error_integrator = err_int
175
+
176
+ def get_sensor_data_perturbed(self) -> SensorData | None:
177
+ """Gets the final sensor array parameters after all errors in the error
178
+ integrator have been applied. If no error integrator is specified then
179
+ None is returned.
180
+
181
+ Returns
182
+ -------
183
+ SensorData | None
184
+ The accumulated sensor array parameters as a SensorData object.
185
+ Returns None if no error integrator has been specified.
186
+ """
187
+ if self.error_integrator is None:
188
+ return None
189
+
190
+ return self.error_integrator.get_sens_data_accumulated()
191
+
192
+ def get_errors_systematic(self) -> np.ndarray | None:
193
+ """Gets the systematic error array from the previously calculated sensor
194
+ measurements. Returns None is no error integrator has been specified.
195
+
196
+ Returns
197
+ -------
198
+ np.ndarray | None
199
+ Array of systematic errors for this sensor array. shape=(num_sensors
200
+ ,num_field_components,num_time_steps). Returns None if no error
201
+ integrator has been set.
202
+ """
203
+ if self.error_integrator is None:
204
+ return None
205
+
206
+ return self.error_integrator.get_errs_systematic()
207
+
208
+ def get_errors_random(self) -> np.ndarray | None:
209
+ """Gets the random error array from the previously calculated sensor
210
+ measurements. Returns None is no error integrator has been specified.
211
+
212
+ Returns
213
+ -------
214
+ np.ndarray | None
215
+ Array of random errors for this sensor array. shape=(num_sensors
216
+ ,num_field_components,num_time_steps). Returns None if no error
217
+ integrator has been set.
218
+ """
219
+ if self.error_integrator is None:
220
+ return None
221
+
222
+ return self.error_integrator.get_errs_random()
223
+
224
+ def get_errors_total(self) -> np.ndarray | None:
225
+ """Gets the total error array from the previously calculated sensor
226
+ measurements. Returns None is no error integrator has been specified.
227
+
228
+ Returns
229
+ -------
230
+ np.ndarray | None
231
+ Array of total errors for this sensor array. shape=(num_sensors
232
+ ,num_field_components,num_time_steps). Returns None if no error
233
+ integrator has been set.
234
+ """
235
+ if self.error_integrator is None:
236
+ return None
237
+
238
+ return self.error_integrator.get_errs_total()
239
+
240
+ def calc_measurements(self) -> np.ndarray:
241
+ """Calculates a set of sensor measurements using the specified sensor
242
+ array parameters and the error intergator if specified. Calculates
243
+ measurements as: measurement = truth + systematic errors + random errors
244
+ . The truth is calculated once and is interpolated from the input
245
+ simulation field. The errors are calculated based on the user specified
246
+ error chain in the error integrator object. If no error integrator is
247
+ specified then only the truth is returned. _description_ew simulated experiment
248
+ for this sensor array.
249
+
250
+ Returns
251
+ -------
252
+ np.ndarray
253
+ Array of sensor measurements including any simulated random and
254
+ systematic errors if an error integrator is specified. shape=(
255
+ num_sensors,num_field_components,num_time_steps).
256
+ """
257
+ if self.error_integrator is None:
258
+ self._measurements = self.get_truth()
259
+ else:
260
+ self._measurements = self.get_truth() + \
261
+ self.error_integrator.calc_errors_from_chain(self.get_truth())
262
+
263
+ return self._measurements
264
+
265
+ def get_measurements(self) -> np.ndarray:
266
+ """Returns the current set of simulated measurements if theses have been
267
+ calculated. If these have not been calculated then 'calc_measurements()'
268
+ is called and a set of measurements in then returned.
269
+
270
+ Returns
271
+ -------
272
+ np.ndarray
273
+ Array of sensor measurements including any simulated random and
274
+ systematic errors if an error integrator is specified. shape=(
275
+ num_sensors,num_field_components,num_time_steps).
276
+ """
277
+ if self._measurements is None:
278
+ self._measurements = self.calc_measurements()
279
+
280
+ return self._measurements
@@ -0,0 +1,72 @@
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 scipy.spatial.transform import Rotation
11
+ from pyvale.core.integratortype import EIntSpatialType
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class SensorData:
16
+ """Data class used for specifying sensor array parameters including:
17
+ position, sample times, angles (for vector/tensor fields), spatial averaging
18
+ and spatial dimensions of the sensor for spatial averaging. The number of
19
+ sensor positions specified determines the number of sensors in the array.
20
+ """
21
+
22
+ positions: np.ndarray | None = None
23
+ """Numpy array of sensor positions where each row is for an individual
24
+ sensor and the columns specify the X, Y and Z coordinates respectively. To
25
+ create a sensor array the positions must be specified and the number of rows
26
+ of the position array determines the number of sensors in the array.
27
+
28
+ shape=(num_sensors,3)
29
+ """
30
+
31
+ sample_times: np.ndarray | None = None
32
+ """Numpy array of times at which the sensors will take measurements (sample
33
+ the field). This does not need to be specified to create a sensor array and
34
+ if it is set to None then the sample times will be assumed to be the same as
35
+ the simulation time steps.
36
+
37
+ shape=(num_time_steps,)
38
+ """
39
+
40
+ angles: tuple[Rotation,...] | None = None
41
+ """The angles for each sensor in the array specified using scipy Rotation
42
+ objects. For scalar fields the rotation only has an effect if a spatial
43
+ averager is specified and the locations of the integration points are
44
+ rotated. For vector and tensor fields the field is transformed using this
45
+ rotation as well as rotating the positions of the integration points if a
46
+ spatial averager is specified.
47
+
48
+ Specifying a single rotation in the tuple will cause all sensors to have the
49
+ same rotation and they will be batch processed increasing speed. Otherwise
50
+ this tuple must have a length equal to the number of sensors (i.e. the
51
+ number of rows in the position array above).
52
+
53
+ shape=(num_sensor,) | (1,)
54
+ """
55
+
56
+ spatial_averager: EIntSpatialType | None = None
57
+ """Type of spatial averaging to use for this sensor array. If None then no
58
+ spatial averaging is performed and sensor values are taken directly from the
59
+ specified positions.
60
+ """
61
+
62
+ spatial_dims: np.ndarray | None = None
63
+ """The spatial dimension of the sensor array in its local X,Y,Z coordinates.
64
+ Only used if spatial averager is specified above.
65
+
66
+ shape=(3,)
67
+ """
68
+
69
+
70
+
71
+
72
+
@@ -0,0 +1,101 @@
1
+
2
+ """
3
+ ================================================================================
4
+ pyvale: the python validation engine
5
+ License: MIT
6
+ Copyright (C) 2025 The Computer Aided Validation Team
7
+ ================================================================================
8
+ """
9
+ from dataclasses import dataclass
10
+ import numpy as np
11
+
12
+ #TODO: Docstrings
13
+
14
+ @dataclass(slots=True)
15
+ class SensorDescriptor:
16
+ name: str = 'Measured Value'
17
+ units: str = r"-"
18
+ time_units: str = r"s"
19
+ symbol: str = r"m"
20
+ tag: str = 'S'
21
+ components: tuple[str,...] | None = None
22
+
23
+ def create_label(self, comp_ind: int | None = None) -> str:
24
+ label = ""
25
+ if self.name != "":
26
+ label = label + rf"{self.name} "
27
+
28
+
29
+ symbol = rf"${self.symbol}$ "
30
+ if comp_ind is not None and self.components is not None:
31
+ symbol = rf"${self.symbol}_{{{self.components[comp_ind]}}}$ "
32
+ if symbol != "":
33
+ label = label + symbol
34
+
35
+ if self.units != "":
36
+ label = label + "\n" + rf"[${self.units}$]"
37
+
38
+ return label
39
+
40
+ def create_label_flat(self, comp_ind: int | None = None) -> str:
41
+ label = ""
42
+ if self.name != "":
43
+ label = label + rf"{self.name} "
44
+
45
+
46
+ symbol = rf"${self.symbol}$ "
47
+ if comp_ind is not None and self.components is not None:
48
+ symbol = rf"${self.symbol}_{{{self.components[comp_ind]}}}$ "
49
+ if symbol != "":
50
+ label = label + symbol
51
+
52
+ if self.units != "":
53
+ label = label + " " + rf"[${self.units}$]"
54
+
55
+ return label
56
+
57
+ def create_sensor_tags(self,n_sensors: int) -> list[str]:
58
+ z_width = int(np.log10(n_sensors))+1
59
+
60
+ sensor_names = list()
61
+ for ss in range(n_sensors):
62
+ num_str = f'{ss+1}'.zfill(z_width)
63
+ sensor_names.append(f'{self.tag}{num_str}')
64
+
65
+ return sensor_names
66
+
67
+
68
+ class SensorDescriptorFactory:
69
+ @staticmethod
70
+ def temperature_descriptor() -> SensorDescriptor:
71
+ descriptor = SensorDescriptor()
72
+ descriptor.name = 'Temp.'
73
+ descriptor.symbol = 'T'
74
+ descriptor.units = r'^{\circ}C'
75
+ descriptor.tag = 'TC'
76
+ return descriptor
77
+
78
+ @staticmethod
79
+ def displacement_descriptor() -> SensorDescriptor:
80
+ descriptor = SensorDescriptor()
81
+ descriptor.name = 'Disp.'
82
+ descriptor.symbol = 'u'
83
+ descriptor.units = r'm'
84
+ descriptor.tag = 'DS'
85
+ descriptor.components = ('x','y','z')
86
+ return descriptor
87
+
88
+ @staticmethod
89
+ def strain_descriptor(spat_dims: int = 3) -> SensorDescriptor:
90
+ descriptor = SensorDescriptor()
91
+ descriptor.name = 'Strain'
92
+ descriptor.symbol = r'\varepsilon'
93
+ descriptor.units = r'-'
94
+ descriptor.tag = 'SG'
95
+
96
+ if spat_dims == 2:
97
+ descriptor.components = ('xx','yy','xy')
98
+ else:
99
+ descriptor.components = ('xx','yy','zz','xy','yz','xz')
100
+
101
+ return descriptor
@@ -0,0 +1,143 @@
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 mooseherder as mh
10
+ from pyvale.core.sensorarray import ISensorArray
11
+
12
+
13
+ def create_sensor_pos_array(num_sensors: tuple[int,int,int],
14
+ x_lims: tuple[float, float],
15
+ y_lims: tuple[float, float],
16
+ z_lims: tuple[float, float]) -> np.ndarray:
17
+ """Function or creating a uniform grid of sensors inside the specified
18
+ bounds and returning the positions in format that can be used to build a
19
+ `SensorData` object.
20
+
21
+ To create a line of sensors along the X axis set the number of sensors to 1
22
+ for all the Y and Z axes and then set the upper and lower limits of the Y
23
+ and Z axis to be the same value.
24
+
25
+ To create a plane of sensors in the X-Y plane set the number of sensors in
26
+ Z to 1 and set the upper and lower coordinates of the Z limit to the desired
27
+ Z location of the plane. Then set the number of sensors in X and Y as
28
+ desired along with the associated limits.
29
+
30
+ Parameters
31
+ ----------
32
+ n_sens : tuple[int,int,int]
33
+ Number of sensors to create in the X, Y and Z directions.
34
+ x_lims : tuple[float, float]
35
+ Limits of the X axis sensor locations.
36
+ y_lims : tuple[float, float]
37
+ Limits of the Y axis sensor locations.
38
+ z_lims : tuple[float, float]
39
+ Limits of the Z axis sensor locations.
40
+
41
+ Returns
42
+ -------
43
+ np.ndarray
44
+ Array of sensor positions with shape=(num_sensors,3) where num_sensors
45
+ is the product of integers in the num_sensors tuple. The columns are the
46
+ X, Y and Z locations of the sensors.
47
+ """
48
+ sens_pos_x = np.linspace(x_lims[0],x_lims[1],num_sensors[0]+2)[1:-1]
49
+ sens_pos_y = np.linspace(y_lims[0],y_lims[1],num_sensors[1]+2)[1:-1]
50
+ sens_pos_z = np.linspace(z_lims[0],z_lims[1],num_sensors[2]+2)[1:-1]
51
+
52
+ (sens_grid_x,sens_grid_y,sens_grid_z) = np.meshgrid(
53
+ sens_pos_x,sens_pos_y,sens_pos_z)
54
+
55
+ sens_pos_x = sens_grid_x.flatten()
56
+ sens_pos_y = sens_grid_y.flatten()
57
+ sens_pos_z = sens_grid_z.flatten()
58
+
59
+ sens_pos = np.vstack((sens_pos_x,sens_pos_y,sens_pos_z)).T
60
+ return sens_pos
61
+
62
+
63
+ def print_measurements(sens_array: ISensorArray,
64
+ sensors: tuple[int,int],
65
+ components: tuple[int,int],
66
+ time_steps: tuple[int,int]) -> None:
67
+ """Diagnostic function to print sensor measurements to the console. Also
68
+ prints the ground truth, the random and the systematic errors for the
69
+ specified sensor array. The sensors, components and time steps are specified
70
+ as slices of the measurement array.
71
+
72
+ Parameters
73
+ ----------
74
+ sens_array : ISensorArray
75
+ Sensor array to print measurement for.
76
+ sensors : tuple[int,int]
77
+ Range of sensors to print from the measurement array using the slice
78
+ specified by this tuple.
79
+ components : tuple[int,int]
80
+ Range of field components to print based on slicing the measurement
81
+ array with this tuple.
82
+ time_steps : tuple[int,int]
83
+ Range of time steps to print based on slicing the measurement array with
84
+ this tuple.
85
+ """
86
+ measurement = sens_array.get_measurements()
87
+ truth = sens_array.get_truth()
88
+ rand_errs = sens_array.get_errors_random()
89
+ sys_errs = sens_array.get_errors_systematic()
90
+ tot_errs = sens_array.get_errors_total()
91
+
92
+ print(f"\nmeasurement.shape = \n {measurement.shape}")
93
+ print_meas = measurement[sensors[0]:sensors[1],
94
+ components[0]:components[1],
95
+ time_steps[0]:time_steps[1]]
96
+ print(f"measurement = \n {print_meas}")
97
+
98
+ print_truth = truth[sensors[0]:sensors[1],
99
+ components[0]:components[1],
100
+ time_steps[0]:time_steps[1]]
101
+ print(f"truth = \n {print_truth}")
102
+
103
+ if rand_errs is not None:
104
+ print_randerrs = rand_errs[sensors[0]:sensors[1],
105
+ components[0]:components[1],
106
+ time_steps[0]:time_steps[1]]
107
+ print(f"random errors = \n {print_randerrs}")
108
+
109
+ if sys_errs is not None:
110
+ print_syserrs = sys_errs[sensors[0]:sensors[1],
111
+ components[0]:components[1],
112
+ time_steps[0]:time_steps[1]]
113
+ print(f"systematic errors = \n {print_syserrs}")
114
+
115
+ if tot_errs is not None:
116
+ print_toterrs = tot_errs[sensors[0]:sensors[1],
117
+ components[0]:components[1],
118
+ time_steps[0]:time_steps[1]]
119
+ print(f"total errors = \n {print_syserrs}")
120
+
121
+ print()
122
+
123
+
124
+ def print_dimensions(sim_data: mh.SimData) -> None:
125
+ """Diagnostic function for quickly finding the coordinate limits for from a
126
+ given simulation.
127
+
128
+ Parameters
129
+ ----------
130
+ sim_data : mh.SimData
131
+ Simulation data objects containing the nodal coordinates.
132
+ """
133
+ print(80*"-")
134
+ print("SimData Dimensions:")
135
+ print(f"x [min,max] = [{np.min(sim_data.coords[:,0])}," + \
136
+ f"{np.max(sim_data.coords[:,0])}]")
137
+ print(f"y [min,max] = [{np.min(sim_data.coords[:,1])}," + \
138
+ f"{np.max(sim_data.coords[:,1])}]")
139
+ print(f"z [min,max] = [{np.min(sim_data.coords[:,2])}," + \
140
+ f"{np.max(sim_data.coords[:,2])}]")
141
+ print(f"t [min,max] = [{np.min(sim_data.time)},{np.max(sim_data.time)}]")
142
+ print(80*"-")
143
+