pyvale 2025.5.3__cp311-cp311-win32.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


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

Files changed (174) hide show
  1. pyvale/__init__.py +89 -0
  2. pyvale/analyticmeshgen.py +102 -0
  3. pyvale/analyticsimdatafactory.py +91 -0
  4. pyvale/analyticsimdatagenerator.py +323 -0
  5. pyvale/blendercalibrationdata.py +15 -0
  6. pyvale/blenderlightdata.py +26 -0
  7. pyvale/blendermaterialdata.py +15 -0
  8. pyvale/blenderrenderdata.py +30 -0
  9. pyvale/blenderscene.py +488 -0
  10. pyvale/blendertools.py +420 -0
  11. pyvale/camera.py +146 -0
  12. pyvale/cameradata.py +69 -0
  13. pyvale/cameradata2d.py +84 -0
  14. pyvale/camerastereo.py +217 -0
  15. pyvale/cameratools.py +522 -0
  16. pyvale/cython/rastercyth.c +32211 -0
  17. pyvale/cython/rastercyth.cp311-win32.pyd +0 -0
  18. pyvale/cython/rastercyth.py +640 -0
  19. pyvale/data/__init__.py +5 -0
  20. pyvale/data/cal_target.tiff +0 -0
  21. pyvale/data/case00_HEX20_out.e +0 -0
  22. pyvale/data/case00_HEX27_out.e +0 -0
  23. pyvale/data/case00_HEX8_out.e +0 -0
  24. pyvale/data/case00_TET10_out.e +0 -0
  25. pyvale/data/case00_TET14_out.e +0 -0
  26. pyvale/data/case00_TET4_out.e +0 -0
  27. pyvale/data/case13_out.e +0 -0
  28. pyvale/data/case16_out.e +0 -0
  29. pyvale/data/case17_out.e +0 -0
  30. pyvale/data/case18_1_out.e +0 -0
  31. pyvale/data/case18_2_out.e +0 -0
  32. pyvale/data/case18_3_out.e +0 -0
  33. pyvale/data/case25_out.e +0 -0
  34. pyvale/data/case26_out.e +0 -0
  35. pyvale/data/optspeckle_2464x2056px_spec5px_8bit_gblur1px.tiff +0 -0
  36. pyvale/dataset.py +325 -0
  37. pyvale/errorcalculator.py +109 -0
  38. pyvale/errordriftcalc.py +146 -0
  39. pyvale/errorintegrator.py +336 -0
  40. pyvale/errorrand.py +607 -0
  41. pyvale/errorsyscalib.py +134 -0
  42. pyvale/errorsysdep.py +327 -0
  43. pyvale/errorsysfield.py +414 -0
  44. pyvale/errorsysindep.py +808 -0
  45. pyvale/examples/__init__.py +5 -0
  46. pyvale/examples/basics/ex1_1_basicscalars_therm2d.py +131 -0
  47. pyvale/examples/basics/ex1_2_sensormodel_therm2d.py +158 -0
  48. pyvale/examples/basics/ex1_3_customsens_therm3d.py +216 -0
  49. pyvale/examples/basics/ex1_4_basicerrors_therm3d.py +153 -0
  50. pyvale/examples/basics/ex1_5_fielderrs_therm3d.py +168 -0
  51. pyvale/examples/basics/ex1_6_caliberrs_therm2d.py +133 -0
  52. pyvale/examples/basics/ex1_7_spatavg_therm2d.py +123 -0
  53. pyvale/examples/basics/ex2_1_basicvectors_disp2d.py +112 -0
  54. pyvale/examples/basics/ex2_2_vectorsens_disp2d.py +111 -0
  55. pyvale/examples/basics/ex2_3_sensangle_disp2d.py +139 -0
  56. pyvale/examples/basics/ex2_4_chainfielderrs_disp2d.py +196 -0
  57. pyvale/examples/basics/ex2_5_vectorfields3d_disp3d.py +109 -0
  58. pyvale/examples/basics/ex3_1_basictensors_strain2d.py +114 -0
  59. pyvale/examples/basics/ex3_2_tensorsens2d_strain2d.py +111 -0
  60. pyvale/examples/basics/ex3_3_tensorsens3d_strain3d.py +182 -0
  61. pyvale/examples/basics/ex4_1_expsim2d_thermmech2d.py +171 -0
  62. pyvale/examples/basics/ex4_2_expsim3d_thermmech3d.py +252 -0
  63. pyvale/examples/genanalyticdata/ex1_1_scalarvisualisation.py +35 -0
  64. pyvale/examples/genanalyticdata/ex1_2_scalarcasebuild.py +43 -0
  65. pyvale/examples/genanalyticdata/ex2_1_analyticsensors.py +80 -0
  66. pyvale/examples/imagedef2d/ex_imagedef2d_todisk.py +79 -0
  67. pyvale/examples/renderblender/ex1_1_blenderscene.py +121 -0
  68. pyvale/examples/renderblender/ex1_2_blenderdeformed.py +119 -0
  69. pyvale/examples/renderblender/ex2_1_stereoscene.py +128 -0
  70. pyvale/examples/renderblender/ex2_2_stereodeformed.py +131 -0
  71. pyvale/examples/renderblender/ex3_1_blendercalibration.py +120 -0
  72. pyvale/examples/renderrasterisation/ex_rastenp.py +153 -0
  73. pyvale/examples/renderrasterisation/ex_rastercyth_oneframe.py +218 -0
  74. pyvale/examples/renderrasterisation/ex_rastercyth_static_cypara.py +187 -0
  75. pyvale/examples/renderrasterisation/ex_rastercyth_static_pypara.py +190 -0
  76. pyvale/examples/visualisation/ex1_1_plot_traces.py +102 -0
  77. pyvale/examples/visualisation/ex2_1_animate_sim.py +89 -0
  78. pyvale/experimentsimulator.py +175 -0
  79. pyvale/field.py +128 -0
  80. pyvale/fieldconverter.py +351 -0
  81. pyvale/fieldsampler.py +111 -0
  82. pyvale/fieldscalar.py +166 -0
  83. pyvale/fieldtensor.py +218 -0
  84. pyvale/fieldtransform.py +388 -0
  85. pyvale/fieldvector.py +213 -0
  86. pyvale/generatorsrandom.py +505 -0
  87. pyvale/imagedef2d.py +569 -0
  88. pyvale/integratorfactory.py +240 -0
  89. pyvale/integratorquadrature.py +217 -0
  90. pyvale/integratorrectangle.py +165 -0
  91. pyvale/integratorspatial.py +89 -0
  92. pyvale/integratortype.py +43 -0
  93. pyvale/output.py +17 -0
  94. pyvale/pyvaleexceptions.py +11 -0
  95. pyvale/raster.py +31 -0
  96. pyvale/rastercy.py +77 -0
  97. pyvale/rasternp.py +603 -0
  98. pyvale/rendermesh.py +147 -0
  99. pyvale/sensorarray.py +178 -0
  100. pyvale/sensorarrayfactory.py +196 -0
  101. pyvale/sensorarraypoint.py +278 -0
  102. pyvale/sensordata.py +71 -0
  103. pyvale/sensordescriptor.py +213 -0
  104. pyvale/sensortools.py +142 -0
  105. pyvale/simcases/case00_HEX20.i +242 -0
  106. pyvale/simcases/case00_HEX27.i +242 -0
  107. pyvale/simcases/case00_HEX8.i +242 -0
  108. pyvale/simcases/case00_TET10.i +242 -0
  109. pyvale/simcases/case00_TET14.i +242 -0
  110. pyvale/simcases/case00_TET4.i +242 -0
  111. pyvale/simcases/case01.i +101 -0
  112. pyvale/simcases/case02.i +156 -0
  113. pyvale/simcases/case03.i +136 -0
  114. pyvale/simcases/case04.i +181 -0
  115. pyvale/simcases/case05.i +234 -0
  116. pyvale/simcases/case06.i +305 -0
  117. pyvale/simcases/case07.geo +135 -0
  118. pyvale/simcases/case07.i +87 -0
  119. pyvale/simcases/case08.geo +144 -0
  120. pyvale/simcases/case08.i +153 -0
  121. pyvale/simcases/case09.geo +204 -0
  122. pyvale/simcases/case09.i +87 -0
  123. pyvale/simcases/case10.geo +204 -0
  124. pyvale/simcases/case10.i +257 -0
  125. pyvale/simcases/case11.geo +337 -0
  126. pyvale/simcases/case11.i +147 -0
  127. pyvale/simcases/case12.geo +388 -0
  128. pyvale/simcases/case12.i +329 -0
  129. pyvale/simcases/case13.i +140 -0
  130. pyvale/simcases/case14.i +159 -0
  131. pyvale/simcases/case15.geo +337 -0
  132. pyvale/simcases/case15.i +150 -0
  133. pyvale/simcases/case16.geo +391 -0
  134. pyvale/simcases/case16.i +357 -0
  135. pyvale/simcases/case17.geo +135 -0
  136. pyvale/simcases/case17.i +144 -0
  137. pyvale/simcases/case18.i +254 -0
  138. pyvale/simcases/case18_1.i +254 -0
  139. pyvale/simcases/case18_2.i +254 -0
  140. pyvale/simcases/case18_3.i +254 -0
  141. pyvale/simcases/case19.geo +252 -0
  142. pyvale/simcases/case19.i +99 -0
  143. pyvale/simcases/case20.geo +252 -0
  144. pyvale/simcases/case20.i +250 -0
  145. pyvale/simcases/case21.geo +74 -0
  146. pyvale/simcases/case21.i +155 -0
  147. pyvale/simcases/case22.geo +82 -0
  148. pyvale/simcases/case22.i +140 -0
  149. pyvale/simcases/case23.geo +164 -0
  150. pyvale/simcases/case23.i +140 -0
  151. pyvale/simcases/case24.geo +79 -0
  152. pyvale/simcases/case24.i +123 -0
  153. pyvale/simcases/case25.geo +82 -0
  154. pyvale/simcases/case25.i +140 -0
  155. pyvale/simcases/case26.geo +166 -0
  156. pyvale/simcases/case26.i +140 -0
  157. pyvale/simcases/run_1case.py +61 -0
  158. pyvale/simcases/run_all_cases.py +69 -0
  159. pyvale/simcases/run_build_case.py +64 -0
  160. pyvale/simcases/run_example_cases.py +69 -0
  161. pyvale/simtools.py +67 -0
  162. pyvale/visualexpplotter.py +191 -0
  163. pyvale/visualimagedef.py +74 -0
  164. pyvale/visualimages.py +76 -0
  165. pyvale/visualopts.py +493 -0
  166. pyvale/visualsimanimator.py +111 -0
  167. pyvale/visualsimsensors.py +318 -0
  168. pyvale/visualtools.py +136 -0
  169. pyvale/visualtraceplotter.py +142 -0
  170. pyvale-2025.5.3.dist-info/METADATA +144 -0
  171. pyvale-2025.5.3.dist-info/RECORD +174 -0
  172. pyvale-2025.5.3.dist-info/WHEEL +5 -0
  173. pyvale-2025.5.3.dist-info/licenses/LICENSE +21 -0
  174. pyvale-2025.5.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,414 @@
1
+ # ==============================================================================
2
+ # pyvale: the python validation engine
3
+ # License: MIT
4
+ # Copyright (C) 2025 The Computer Aided Validation Team
5
+ # ==============================================================================
6
+
7
+ import copy
8
+ from dataclasses import dataclass
9
+ import numpy as np
10
+ from scipy.spatial.transform import Rotation
11
+
12
+ from pyvale.field import IField
13
+ from pyvale.fieldsampler import sample_field_with_sensor_data
14
+ from pyvale.sensordata import SensorData
15
+ from pyvale.integratortype import EIntSpatialType
16
+ from pyvale.errorcalculator import (IErrCalculator,
17
+ EErrType,
18
+ EErrDep)
19
+ from pyvale.errordriftcalc import IDriftCalculator
20
+ from pyvale.generatorsrandom import IGenRandom
21
+
22
+ # TODO:
23
+ # - Implement different perturbed sampling times for each sensor or allow all
24
+ # to lock to the same time step as it works now.
25
+ # - Need to check that we perform field rotations correctly for sensor angles.
26
+ # - This needs to be updated to take rotation objects for offsets and to build
27
+ # and compose rotations
28
+
29
+
30
+ @dataclass(slots=True)
31
+ class ErrFieldData:
32
+ """Dataclass for controlling sensor parameter perturbations for field based
33
+ systematic errors (i.e. errors that require interpolation of the physical
34
+ field).
35
+ """
36
+
37
+ pos_offset_xyz: np.ndarray | None = None
38
+ """Array of offsets to apply to the sensor positions for error calculation.
39
+ shape=(num_sensors,3) where the columns represent the X, Y and Z offsets in
40
+ simulation world coordinates. If None then no position offset is applied.
41
+ """
42
+
43
+ ang_offset_zyx: np.ndarray | None = None
44
+ """Array of offsets to apply to the sensor angles for error calculation.
45
+ shape=(num_sensors,3) where the columns represent rotations about offsets
46
+ about the Z, Y and X axis of the sensor in sensor local coordinates. If None
47
+ then no angular offsets are applied.
48
+ """
49
+
50
+ time_offset: np.ndarray | None = None
51
+ """Array of offsets to apply to the sampling times for all sensors. shape=(
52
+ num_time_steps,). If None then no time offset is applied.
53
+ """
54
+
55
+ pos_rand_xyz: tuple[IGenRandom | None,
56
+ IGenRandom | None,
57
+ IGenRandom | None] = (None,None,None)
58
+ """Tuple of random generators (implementations of `IGenRandom`
59
+ interface) for perturbing the sensor positions. The generators perturb the
60
+ X, Y and Z coordinates in order. If None then that axis is not randomly
61
+ perturbed from the nominal sensor position. Note that the random generators
62
+ should return position perturbations consistent with the simulation units.
63
+ """
64
+
65
+ ang_rand_zyx: tuple[IGenRandom | None,
66
+ IGenRandom | None,
67
+ IGenRandom | None] = (None,None,None)
68
+ """Tuple of random generators (implementations of `IGenRandom`
69
+ interface) for perturbing the sensor angles. The generators perturb
70
+ rotations about the the Z, Y and X axis in order. If None then that axis is
71
+ not randomly perturbed from the nominal sensor position.
72
+ """
73
+
74
+ time_rand: IGenRandom | None = None
75
+ """Random generator for perturbing sensor array sampling times for the
76
+ purpose of calculating field based errors. If None then sensor sampling
77
+ times will not be perturbed from the nominal times.
78
+ """
79
+
80
+ spatial_averager: EIntSpatialType | None = None
81
+ """Type of spatial averaging to use for this sensor array for the purpose of
82
+ calculating field based errors. If None then no spatial averaging is
83
+ performed.
84
+ """
85
+
86
+ spatial_dims: np.ndarray | None = None
87
+ """The spatial dimension of the sensor in its local X,Y,Z coordinates for
88
+ the purpose of calculating field errors. Only used if spatial averager is
89
+ specified above. shape=(3,)
90
+ """
91
+
92
+ # DEV FEATURE: locks the coordinate even if offsets and random generators
93
+ # are specified. These allow individual sensors to be locked when we only
94
+ # specify a random generator for each axis not each sensor.
95
+ pos_lock_xyz: np.ndarray | None = None
96
+ ang_lock_zyx: np.ndarray | None = None
97
+
98
+ # TODO: implement drift for other dimensions, pos/angle
99
+ time_drift: IDriftCalculator | None = None
100
+ """Temporal drift calculation
101
+ """
102
+
103
+
104
+ class ErrSysField(IErrCalculator):
105
+ """Class for calculating field based systematic errors. Field based errors
106
+ are errors that require interpolation or sampling of the simulated physical
107
+ field such as perturbations of the sensor position or sampling time.
108
+
109
+ All perturbations to the sensor parameters (positions, sample times, angles
110
+ area averaging) are calculated first before performing a single
111
+ interpolation with the perturbed sensor state.
112
+
113
+ Implements the `IErrCalculator` interface.
114
+ """
115
+ __slots__ = ("_field","_sensor_data_perturbed","_field_err_data","_err_dep")
116
+
117
+ def __init__(self,
118
+ field: IField,
119
+ field_err_data: ErrFieldData,
120
+ err_dep: EErrDep = EErrDep.INDEPENDENT) -> None:
121
+ """
122
+ Parameters
123
+ ----------
124
+ field : IField
125
+ The physical field to interpolate which will be an implementation of
126
+ the `IField` interface. This will be a `FieldScalar`, `FieldVector`
127
+ or `FieldTensor` object.
128
+ field_err_data : ErrFieldData
129
+ Dataclass specifying which sensor array parameters will be perturbed
130
+ and how they will be perturbed. See the `ErrFieldData` class for
131
+ more detail
132
+ err_dep : EErrDep, optional
133
+ Error calculation dependence, by default EErrDep.DEPENDENT.
134
+ """
135
+ self._field = field
136
+ self._field_err_data = field_err_data
137
+ self._err_dep = err_dep
138
+ self._sensor_data_perturbed = SensorData()
139
+
140
+ def get_error_dep(self) -> EErrDep:
141
+ """Gets the error dependence state for this error calculator. An
142
+ independent error is calculated based on the input truth values as the
143
+ error basis. A dependent error is calculated based on the accumulated
144
+ sensor reading from all preceeding errors in the chain.
145
+
146
+ Returns
147
+ -------
148
+ EErrDep
149
+ Enumeration defining INDEPENDENT or DEPENDENT behaviour.
150
+ """
151
+ return self._err_dep
152
+
153
+ def set_error_dep(self, dependence: EErrDep) -> None:
154
+ """Sets the error dependence state for this error calculator. An
155
+ independent error is calculated based on the input truth values as the
156
+ error basis. A dependent error is calculated based on the accumulated
157
+ sensor reading from all preceeding errors in the chain.
158
+
159
+ Parameters
160
+ ----------
161
+ dependence : EErrDep
162
+ Enumeration defining INDEPENDENT or DEPENDENT behaviour.
163
+ """
164
+ self._err_dep = dependence
165
+
166
+ def get_error_type(self) -> EErrType:
167
+ """Gets the error type.
168
+
169
+ Returns
170
+ -------
171
+ EErrType
172
+ Enumeration definining RANDOM or SYSTEMATIC error types.
173
+ """
174
+ return EErrType.SYSTEMATIC
175
+
176
+ def get_perturbed_sensor_data(self) -> SensorData:
177
+
178
+ return self._sensor_data_perturbed
179
+
180
+ def calc_errs(self,
181
+ err_basis: np.ndarray,
182
+ sens_data: SensorData,
183
+ ) -> tuple[np.ndarray, SensorData]:
184
+ """Calculates the error array based on the size of the input. First
185
+ calculates the combined perturbed sensor state from all perturbations
186
+ specified in the `ErrFieldData` object and then performs a single
187
+ interpolation of the field to obtain the error array.
188
+
189
+ Parameters
190
+ ----------
191
+ err_basis : np.ndarray
192
+ Array of values with the same dimensions as the sensor measurement
193
+ matrix.
194
+ sens_data : SensorData
195
+ The accumulated sensor state data for all errors prior to this one.
196
+
197
+ Returns
198
+ -------
199
+ tuple[np.ndarray, SensorData]
200
+ Tuple containing the calculated error array and pass through of the
201
+ sensor data object as it is not modified by this class. The returned
202
+ error array has the same shape as the input error basis.
203
+ """
204
+ self._sensor_data_perturbed = copy.deepcopy(sens_data)
205
+ self._sensor_data_perturbed.spatial_averager = \
206
+ self._field_err_data.spatial_averager
207
+ self._sensor_data_perturbed.spatial_dims = \
208
+ self._field_err_data.spatial_dims
209
+
210
+ self._sensor_data_perturbed.positions = _perturb_sensor_positions(
211
+ self._sensor_data_perturbed.positions,
212
+ self._field_err_data.pos_offset_xyz,
213
+ self._field_err_data.pos_rand_xyz,
214
+ self._field_err_data.pos_lock_xyz,
215
+ )
216
+
217
+ self._sensor_data_perturbed.sample_times = _perturb_sample_times(
218
+ self._field.get_time_steps(),
219
+ self._sensor_data_perturbed.sample_times,
220
+ self._field_err_data.time_offset,
221
+ self._field_err_data.time_rand,
222
+ self._field_err_data.time_drift,
223
+ )
224
+
225
+ self._sensor_data_perturbed.angles = _perturb_sensor_angles(
226
+ sens_data.positions.shape[0],
227
+ self._sensor_data_perturbed.angles,
228
+ self._field_err_data.ang_offset_zyx,
229
+ self._field_err_data.ang_rand_zyx,
230
+ self._field_err_data.ang_lock_zyx,
231
+ )
232
+
233
+ sys_errs = sample_field_with_sensor_data(
234
+ self._field,
235
+ self._sensor_data_perturbed
236
+ ) - err_basis
237
+
238
+ return (sys_errs,self._sensor_data_perturbed)
239
+
240
+
241
+ def _perturb_sensor_positions(sens_pos_nominal: np.ndarray,
242
+ pos_offset_xyz: np.ndarray | None,
243
+ pos_rand_xyz: tuple[IGenRandom | None,
244
+ IGenRandom | None,
245
+ IGenRandom | None] | None,
246
+ pos_loc_xyz: np.ndarray | None,
247
+ ) -> np.ndarray:
248
+ """Helper function for perturbing the sensor positions from their nominal
249
+ positions based on the user specified offset and random generators for each
250
+ axis.
251
+
252
+ Parameters
253
+ ----------
254
+ sens_pos_nominal : np.ndarray
255
+ Nominal sensor positions as an array with shape=(num_sensors,3) where
256
+ the columns represent the position in the X, Y and Z axes.
257
+ pos_offset_xyz : np.ndarray | None
258
+ Offsets to apply to the sensor positions as an array with shape=
259
+ (num_sensors,3) wherethe columns represent the position in the X, Y and
260
+ Z axes. If None then no offset is applied.
261
+ pos_rand_xyz : tuple[IGenRandom | None,
262
+ IGenRandom | None,
263
+ IGenRandom | None] | None
264
+ Random generators for sensor position perturbations along the the X, Y
265
+ and Z axes. If None then no perturbation is applied.
266
+ pos_loc_xyz : np.ndarray | None
267
+ Boolean mask with shape=(num_sensors,3), where the mask is true the
268
+ coordinate is locked and will not perturb based on offset or rand above.
269
+
270
+ Returns
271
+ -------
272
+ np.ndarray
273
+ Array of perturbed sensors positions with shape=(num_sensors,3) where
274
+ the columns represent the position in the X, Y and Z axes.
275
+ """
276
+ sens_pos_perturbed = np.copy(sens_pos_nominal)
277
+
278
+ if pos_offset_xyz is not None:
279
+ sens_pos_perturbed = sens_pos_perturbed + pos_offset_xyz
280
+
281
+ if pos_rand_xyz is not None:
282
+ for ii,rng in enumerate(pos_rand_xyz):
283
+ if rng is not None:
284
+ sens_pos_perturbed[:,ii] = sens_pos_perturbed[:,ii] + \
285
+ rng.generate(shape=sens_pos_perturbed.shape[0])
286
+
287
+ if pos_loc_xyz is not None:
288
+ sens_pos_perturbed[pos_loc_xyz] = sens_pos_nominal[pos_loc_xyz]
289
+
290
+ return sens_pos_perturbed
291
+
292
+
293
+ def _perturb_sample_times(sim_time: np.ndarray,
294
+ time_nominal: np.ndarray | None,
295
+ time_offset: np.ndarray | None,
296
+ time_rand: IGenRandom | None,
297
+ time_drift: IDriftCalculator | None
298
+ ) -> np.ndarray | None:
299
+ """Helper function for calculating perturbed sensor sampling times for the
300
+ purpose of calculating field based systematic errors.
301
+
302
+ Parameters
303
+ ----------
304
+ sim_time : np.ndarray
305
+ Simulation time steps for the underlying physical field.
306
+ time_nominal : np.ndarray | None
307
+ Nominal sensor sampling times. If None then the simulation time steps
308
+ are assumed to be the sampling times.
309
+ time_offset : np.ndarray | None
310
+ Array of time offsets to apply to all sensors. If None then no offsets
311
+ are applied.
312
+ time_rand : IGenRandom | None
313
+ Random generator for perturbing the sampling times of all sensors. If
314
+ None then no random perturbation of sampling times occurs.
315
+ time_drift : IDriftCalculator | None
316
+ Drift function for calculating temporal sampling drift. If None then no
317
+ temporal drift is applied.
318
+
319
+ Returns
320
+ -------
321
+ np.ndarray | None
322
+ Array of perturbed sample times
323
+ """
324
+ if time_nominal is None:
325
+ if (time_offset is not None
326
+ or time_rand is not None
327
+ or time_drift is not None):
328
+ time_nominal = sim_time
329
+ else:
330
+ return None
331
+
332
+ time_perturbed = np.copy(time_nominal)
333
+
334
+ if time_offset is not None:
335
+ time_perturbed = time_perturbed + time_offset
336
+ if time_rand is not None:
337
+ time_perturbed = time_perturbed + time_rand.generate(
338
+ shape=time_nominal.shape)
339
+ if time_drift is not None:
340
+ time_perturbed = time_perturbed + time_drift.calc_drift(time_nominal)
341
+
342
+ return time_perturbed
343
+
344
+
345
+ def _perturb_sensor_angles(n_sensors: int,
346
+ angles_nominal: tuple[Rotation,...] | None,
347
+ angle_offsets_zyx: np.ndarray | None,
348
+ rand_ang_zyx: tuple[IGenRandom | None,
349
+ IGenRandom | None,
350
+ IGenRandom | None] | None,
351
+ angle_loc_zyx: np.ndarray | None,
352
+ ) -> tuple[Rotation,...] | None:
353
+ """Helper function for perturbing sensor angles for the purpose of
354
+ calculating field based systematic errors.
355
+
356
+ Parameters
357
+ ----------
358
+ n_sensors : int
359
+ Number of sensors in the sensor array.
360
+ angles_nominal : tuple[Rotation,...] | None
361
+ The nominal angles of the sensors as a tuple of scipy Rotation objects.
362
+ This tuple should have length equal to the number of sensors. If None
363
+ then an initial orienation of [0,0,0] is assumed.
364
+ angle_offsets_zyx : np.ndarray | None
365
+ Angle offsets to apply to the sensor array as an array with shape=(
366
+ num_sensors,3) where the columns are the rotations about Z, Y and X in
367
+ degrees. If None then no offsets are applied.
368
+ rand_ang_zyx : tuple[IGenRandom | None,
369
+ IGenRandom | None,
370
+ IGenRandom | None] | None
371
+ Random generators for perturbing sensor angles about the Z, Y and X axis
372
+ respectively. If None then no random perturbation to the sensor angle
373
+ occurs.
374
+ angle_loc_zyx : np.ndarray | None
375
+ Boolean mask with shape=(num_sensors,3), where the mask is true the
376
+ angle is locked and the sensor will not rotate about that axis despite
377
+ the offset of rand generators above,
378
+
379
+ Returns
380
+ -------
381
+ tuple[Rotation,...] | None
382
+ Rotation object giving each sensors perturbed angle. If None then the
383
+ no sensors have had their angles perturbed.
384
+ """
385
+ if angles_nominal is None:
386
+ if angle_offsets_zyx is not None or rand_ang_zyx is not None:
387
+ angles_nominal = n_sensors * \
388
+ (Rotation.from_euler("zyx",[0,0,0], degrees=True),)
389
+ else:
390
+ return None
391
+
392
+ angles_perturbed = [Rotation.from_euler("zyx",[0,0,0], degrees=True)] * \
393
+ len(angles_nominal)
394
+ for ii,rot_nom in enumerate(angles_nominal): # loop over sensors
395
+ # NOTE: adding angles here might not be correct
396
+ sensor_rot_angs = np.zeros((3,))
397
+
398
+ if angle_offsets_zyx is not None:
399
+ sensor_rot_angs = sensor_rot_angs + angle_offsets_zyx[ii,:]
400
+
401
+ if rand_ang_zyx is not None:
402
+ for jj,rand_ang in enumerate(rand_ang_zyx): # loop over components
403
+ if rand_ang is not None:
404
+ sensor_rot_angs[jj] = sensor_rot_angs[jj] + \
405
+ rand_ang.generate(shape=1)
406
+
407
+ if angle_loc_zyx is not None:
408
+ # No rotation about locked axes using mask
409
+ sensor_rot_angs[angle_loc_zyx[ii,:]] = 0.0
410
+
411
+ sensor_rot = Rotation.from_euler("zyx",sensor_rot_angs, degrees=True)
412
+ angles_perturbed[ii] = sensor_rot*rot_nom
413
+
414
+ return tuple(angles_perturbed)