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,407 @@
1
+ """
2
+ ================================================================================
3
+ pyvale: the python validation engine
4
+ License: MIT
5
+ Copyright (C) 2025 The Computer Aided Validation Team
6
+ ================================================================================
7
+ """
8
+ import copy
9
+ from dataclasses import dataclass
10
+ import numpy as np
11
+ from scipy.spatial.transform import Rotation
12
+
13
+ from pyvale.core.field import IField
14
+ from pyvale.core.fieldsampler import sample_field_with_sensor_data
15
+ from pyvale.core.sensordata import SensorData
16
+ from pyvale.core.integratortype import EIntSpatialType
17
+ from pyvale.core.errorcalculator import (IErrCalculator,
18
+ EErrType,
19
+ EErrDependence)
20
+ from pyvale.core.errordriftcalc import IDriftCalculator
21
+ from pyvale.core.generatorsrandom import IGeneratorRandom
22
+
23
+
24
+ @dataclass(slots=True)
25
+ class ErrFieldData:
26
+ """Dataclass for controlling sensor parameter perturbations for field based
27
+ systematic errors (i.e. errors that require interpolation of the physical
28
+ field).
29
+ """
30
+
31
+ pos_offset_xyz: np.ndarray | None = None
32
+ """Array of offsets to apply to the sensor positions for error calculation.
33
+ shape=(num_sensors,3) where the columns represent the X, Y and Z offsets in
34
+ simulation world coordinates. If None then no position offset is applied.
35
+ """
36
+
37
+ ang_offset_zyx: np.ndarray | None = None
38
+ """Array of offsets to apply to the sensor angles for error calculation.
39
+ shape=(num_sensors,3) where the columns represent rotations about offsets
40
+ about the Z, Y and X axis of the sensor in sensor local coordinates. If None
41
+ then no angular offsets are applied.
42
+ """
43
+
44
+ time_offset: np.ndarray | None = None
45
+ """Array of offsets to apply to the sampling times for all sensors. shape=(
46
+ num_time_steps,). If None then no time offset is applied.
47
+ """
48
+
49
+ pos_rand_xyz: tuple[IGeneratorRandom | None,
50
+ IGeneratorRandom | None,
51
+ IGeneratorRandom | None] = (None,None,None)
52
+ """Tuple of random generators (implementations of `IGeneratorRandom`
53
+ interface) for perturbing the sensor positions. The generators perturb the
54
+ X, Y and Z coordinates in order. If None then that axis is not randomly
55
+ perturbed from the nominal sensor position.
56
+ """
57
+
58
+ ang_rand_zyx: tuple[IGeneratorRandom | None,
59
+ IGeneratorRandom | None,
60
+ IGeneratorRandom | None] = (None,None,None)
61
+ """Tuple of random generators (implementations of `IGeneratorRandom`
62
+ interface) for perturbing the sensor angles. The generators perturb
63
+ rotations about the the Z, Y and X axis in order. If None then that axis is
64
+ not randomly perturbed from the nominal sensor position.
65
+ """
66
+
67
+ time_rand: IGeneratorRandom | None = None
68
+ """Random generator for perturbing sensor array sampling times for the
69
+ purpose of calculating field based errors. If None then sensor sampling
70
+ times will not be perturbed from the nominal times.
71
+ """
72
+
73
+ spatial_averager: EIntSpatialType | None = None
74
+ """Type of spatial averaging to use for this sensor array for the purpose of
75
+ calculating field based errors. If None then no spatial averaging is
76
+ performed.
77
+ """
78
+
79
+ spatial_dims: np.ndarray | None = None
80
+ """The spatial dimension of the sensor in its local X,Y,Z coordinates for
81
+ the purpose of calculating field errors. Only used if spatial averager is
82
+ specified above. shape=(3,)
83
+ """
84
+
85
+ # DEV FEATURE: locks the coordinate even if offsets and random generators are specified
86
+ pos_lock_xyz: np.ndarray | None = None
87
+ ang_lock_zyx: np.ndarray | None = None
88
+
89
+ #TODO: implement drift for other dimensions, pos/angle
90
+ time_drift: IDriftCalculator | None = None
91
+ """Temporal drift calculation
92
+ """
93
+
94
+
95
+ class ErrSysField(IErrCalculator):
96
+ """Class for calculating field based systematic errors. Field based errors
97
+ are errors that require interpolation or sampling of the simulated physical
98
+ field such as perturbations of the sensor position or sampling time.
99
+
100
+ All perturbations to the sensor parameters (positions, sample times, angles
101
+ area averaging) are calculated first before performing a single
102
+ interpolation with the perturbed sensor state.
103
+
104
+ Implements the `IErrCalculator` interface.
105
+ """
106
+ __slots__ = ("_field","_sensor_data_perturbed","_field_err_data","_err_dep")
107
+
108
+ def __init__(self,
109
+ field: IField,
110
+ field_err_data: ErrFieldData,
111
+ err_dep: EErrDependence = EErrDependence.INDEPENDENT) -> None:
112
+ """Initialiser for the `ErrSysField` class.
113
+
114
+ Parameters
115
+ ----------
116
+ field : IField
117
+ The physical field to interpolate which will be an implementation of
118
+ the `IField` interface. This will be a `FieldScalar`, `FieldVector`
119
+ or `FieldTensor` object.
120
+ field_err_data : ErrFieldData
121
+ Dataclass specifying which sensor array parameters will be perturbed
122
+ and how they will be perturbed. See the `ErrFieldData` class for
123
+ more detail
124
+ err_dep : EErrDependence, optional
125
+ Error calculation dependence, by default EErrDependence.INDEPENDENT.
126
+ """
127
+ self._field = field
128
+ self._field_err_data = field_err_data
129
+ self._err_dep = err_dep
130
+ self._sensor_data_perturbed = SensorData()
131
+
132
+ def get_error_dep(self) -> EErrDependence:
133
+ """Gets the error dependence state for this error calculator. An
134
+ independent error is calculated based on the input truth values as the
135
+ error basis. A dependent error is calculated based on the accumulated
136
+ sensor reading from all preceeding errors in the chain.
137
+
138
+ Returns
139
+ -------
140
+ EErrDependence
141
+ Enumeration defining INDEPENDENT or DEPENDENT behaviour.
142
+ """
143
+ return self._err_dep
144
+
145
+ def set_error_dep(self, dependence: EErrDependence) -> None:
146
+ """Sets the error dependence state for this error calculator. An
147
+ independent error is calculated based on the input truth values as the
148
+ error basis. A dependent error is calculated based on the accumulated
149
+ sensor reading from all preceeding errors in the chain.
150
+
151
+ Parameters
152
+ ----------
153
+ dependence : EErrDependence
154
+ Enumeration defining INDEPENDENT or DEPENDENT behaviour.
155
+ """
156
+ self._err_dep = dependence
157
+
158
+ def get_error_type(self) -> EErrType:
159
+ """Gets the error type.
160
+
161
+ Returns
162
+ -------
163
+ EErrType
164
+ Enumeration definining RANDOM or SYSTEMATIC error types.
165
+ """
166
+ return EErrType.SYSTEMATIC
167
+
168
+ def get_perturbed_sensor_data(self) -> SensorData:
169
+
170
+ return self._sensor_data_perturbed
171
+
172
+ def calc_errs(self,
173
+ err_basis: np.ndarray,
174
+ sens_data: SensorData,
175
+ ) -> tuple[np.ndarray, SensorData]:
176
+ """Calculates the error array based on the size of the input. First
177
+ calculates the combined perturbed sensor state from all perturbations
178
+ specified in the `ErrFieldData` object and then performs a single
179
+ interpolation of the field to obtain the error array.
180
+
181
+ Parameters
182
+ ----------
183
+ err_basis : np.ndarray
184
+ Array of values with the same dimensions as the sensor measurement
185
+ matrix.
186
+ sens_data : SensorData
187
+ The accumulated sensor state data for all errors prior to this one.
188
+
189
+ Returns
190
+ -------
191
+ tuple[np.ndarray, SensorData]
192
+ Tuple containing the calculated error array and pass through of the
193
+ sensor data object as it is not modified by this class. The returned
194
+ error array has the same shape as the input error basis.
195
+ """
196
+ self._sensor_data_perturbed = copy.deepcopy(sens_data)
197
+ self._sensor_data_perturbed.spatial_averager = \
198
+ self._field_err_data.spatial_averager
199
+ self._sensor_data_perturbed.spatial_dims = \
200
+ self._field_err_data.spatial_dims
201
+
202
+ self._sensor_data_perturbed.positions = _perturb_sensor_positions(
203
+ self._sensor_data_perturbed.positions,
204
+ self._field_err_data.pos_offset_xyz,
205
+ self._field_err_data.pos_rand_xyz,
206
+ self._field_err_data.pos_lock_xyz,
207
+ )
208
+
209
+ self._sensor_data_perturbed.sample_times = _perturb_sample_times(
210
+ self._field.get_time_steps(),
211
+ self._sensor_data_perturbed.sample_times,
212
+ self._field_err_data.time_offset,
213
+ self._field_err_data.time_rand,
214
+ self._field_err_data.time_drift,
215
+ )
216
+
217
+ self._sensor_data_perturbed.angles = _perturb_sensor_angles(
218
+ sens_data.positions.shape[0],
219
+ self._sensor_data_perturbed.angles,
220
+ self._field_err_data.ang_offset_zyx,
221
+ self._field_err_data.ang_rand_zyx,
222
+ self._field_err_data.ang_lock_zyx,
223
+ )
224
+
225
+ sys_errs = sample_field_with_sensor_data(
226
+ self._field,
227
+ self._sensor_data_perturbed
228
+ ) - err_basis
229
+
230
+ return (sys_errs,self._sensor_data_perturbed)
231
+
232
+
233
+ def _perturb_sensor_positions(sens_pos_nominal: np.ndarray,
234
+ pos_offset_xyz: np.ndarray | None,
235
+ pos_rand_xyz: tuple[IGeneratorRandom | None,
236
+ IGeneratorRandom | None,
237
+ IGeneratorRandom | None] | None,
238
+ pos_loc_xyz: np.ndarray | None,
239
+ ) -> np.ndarray:
240
+ """Helper function for perturbing the sensor positions from their nominal
241
+ positions based on the user specified offset and random generators for each
242
+ axis.
243
+
244
+ Parameters
245
+ ----------
246
+ sens_pos_nominal : np.ndarray
247
+ Nominal sensor positions as an array with shape=(num_sensors,3) where
248
+ the columns represent the position in the X, Y and Z axes.
249
+ pos_offset_xyz : np.ndarray | None
250
+ Offsets to apply to the sensor positions as an array with shape=
251
+ (num_sensors,3) wherethe columns represent the position in the X, Y and
252
+ Z axes. If None then no offset is applied.
253
+ pos_rand_xyz : tuple[IGeneratorRandom | None,
254
+ IGeneratorRandom | None,
255
+ IGeneratorRandom | None] | None
256
+ Random generators for sensor position perturbations along the the X, Y
257
+ and Z axes. If None then no perturbation is applied.
258
+ pos_loc_xyz : np.ndarray | None
259
+ Boolean mask with shape=(num_sensors,3), where the mask is true the
260
+ coordinate is locked and will not perturb based on offset or rand above.
261
+
262
+ Returns
263
+ -------
264
+ np.ndarray
265
+ Array of perturbed sensors positions with shape=(num_sensors,3) where
266
+ the columns represent the position in the X, Y and Z axes.
267
+ """
268
+ sens_pos_perturbed = np.copy(sens_pos_nominal)
269
+
270
+ if pos_offset_xyz is not None:
271
+ sens_pos_perturbed = sens_pos_perturbed + pos_offset_xyz
272
+
273
+ if pos_rand_xyz is not None:
274
+ for ii,rng in enumerate(pos_rand_xyz):
275
+ if rng is not None:
276
+ sens_pos_perturbed[:,ii] = sens_pos_perturbed[:,ii] + \
277
+ rng.generate(shape=sens_pos_perturbed.shape[0])
278
+
279
+ if pos_loc_xyz is not None:
280
+ sens_pos_perturbed[pos_loc_xyz] = sens_pos_nominal[pos_loc_xyz]
281
+
282
+ return sens_pos_perturbed
283
+
284
+
285
+ def _perturb_sample_times(sim_time: np.ndarray,
286
+ time_nominal: np.ndarray | None,
287
+ time_offset: np.ndarray | None,
288
+ time_rand: IGeneratorRandom | None,
289
+ time_drift: IDriftCalculator | None
290
+ ) -> np.ndarray | None:
291
+ """Helper function for calculating perturbed sensor sampling times for the
292
+ purpose of calculating field based systematic errors.
293
+
294
+ Parameters
295
+ ----------
296
+ sim_time : np.ndarray
297
+ Simulation time steps for the underlying physical field.
298
+ time_nominal : np.ndarray | None
299
+ Nominal sensor sampling times. If None then the simulation time steps
300
+ are assumed to be the sampling times.
301
+ time_offset : np.ndarray | None
302
+ Array of time offsets to apply to all sensors. If None then no offsets
303
+ are applied.
304
+ time_rand : IGeneratorRandom | None
305
+ Random generator for perturbing the sampling times of all sensors. If
306
+ None then no random perturbation of sampling times occurs.
307
+ time_drift : IDriftCalculator | None
308
+ Drift function for calculating temporal sampling drift. If None then no
309
+ temporal drift is applied.
310
+
311
+ Returns
312
+ -------
313
+ np.ndarray | None
314
+ Array of perturbed sample times
315
+ """
316
+ if time_nominal is None:
317
+ if (time_offset is not None
318
+ or time_rand is not None
319
+ or time_drift is not None):
320
+ time_nominal = sim_time
321
+ else:
322
+ return None
323
+
324
+ time_perturbed = np.copy(time_nominal)
325
+
326
+ if time_offset is not None:
327
+ time_perturbed = time_perturbed + time_offset
328
+ if time_rand is not None:
329
+ time_perturbed = time_perturbed + time_rand.generate(
330
+ shape=time_nominal.shape)
331
+ if time_drift is not None:
332
+ time_perturbed = time_perturbed + time_drift.calc_drift(time_nominal)
333
+
334
+ return time_perturbed
335
+
336
+
337
+ def _perturb_sensor_angles(n_sensors: int,
338
+ angles_nominal: tuple[Rotation,...] | None,
339
+ angle_offsets_zyx: np.ndarray | None,
340
+ rand_ang_zyx: tuple[IGeneratorRandom | None,
341
+ IGeneratorRandom | None,
342
+ IGeneratorRandom | None] | None,
343
+ angle_loc_zyx: np.ndarray | None,
344
+ ) -> tuple[Rotation,...] | None:
345
+ """Helper function for perturbing sensor angles for the purpose of
346
+ calculating field based systematic errors.
347
+
348
+ Parameters
349
+ ----------
350
+ n_sensors : int
351
+ Number of sensors in the sensor array.
352
+ angles_nominal : tuple[Rotation,...] | None
353
+ The nominal angles of the sensors as a tuple of scipy Rotation objects.
354
+ This tuple should have length equal to the number of sensors. If None
355
+ then an initial orienation of [0,0,0] is assumed.
356
+ angle_offsets_zyx : np.ndarray | None
357
+ Angle offsets to apply to the sensor array as an array with shape=(
358
+ num_sensors,3) where the columns are the rotations about Z, Y and X in
359
+ degrees. If None then no offsets are applied.
360
+ rand_ang_zyx : tuple[IGeneratorRandom | None,
361
+ IGeneratorRandom | None,
362
+ IGeneratorRandom | None] | None
363
+ Random generators for perturbing sensor angles about the Z, Y and X axis
364
+ respectively. If None then no random perturbation to the sensor angle
365
+ occurs.
366
+ angle_loc_zyx : np.ndarray | None
367
+ Boolean mask with shape=(num_sensors,3), where the mask is true the
368
+ angle is locked and the sensor will not rotate about that axis despite
369
+ the offset of rand generators above,
370
+
371
+ Returns
372
+ -------
373
+ tuple[Rotation,...] | None
374
+ Rotation object giving each sensors perturbed angle. If None then the
375
+ no sensors have had their angles perturbed.
376
+ """
377
+ if angles_nominal is None:
378
+ if angle_offsets_zyx is not None or rand_ang_zyx is not None:
379
+ angles_nominal = n_sensors * \
380
+ (Rotation.from_euler("zyx",[0,0,0], degrees=True),)
381
+ else:
382
+ return None
383
+
384
+ angles_perturbed = [Rotation.from_euler("zyx",[0,0,0], degrees=True)] * \
385
+ len(angles_nominal)
386
+ for ii,rot_nom in enumerate(angles_nominal): # loop over sensors
387
+ # NOTE: adding angles here might not be correct
388
+ sensor_rot_angs = np.zeros((3,))
389
+
390
+ if angle_offsets_zyx is not None:
391
+ sensor_rot_angs = sensor_rot_angs + angle_offsets_zyx[ii,:]
392
+
393
+ if rand_ang_zyx is not None:
394
+ for jj,rand_ang in enumerate(rand_ang_zyx): # loop over components
395
+ if rand_ang is not None:
396
+ sensor_rot_angs[jj] = sensor_rot_angs[jj] + \
397
+ rand_ang.generate(shape=1)
398
+
399
+ if angle_loc_zyx is not None:
400
+ # No rotation about locked axes using mask
401
+ sensor_rot_angs[angle_loc_zyx[ii,:]] = 0.0
402
+
403
+ sensor_rot = Rotation.from_euler("zyx",sensor_rot_angs, degrees=True)
404
+ angles_perturbed[ii] = sensor_rot*rot_nom
405
+
406
+ return tuple(angles_perturbed)
407
+