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