pyvale 2025.5.3__cp311-cp311-macosx_14_0_arm64.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.
- pyvale/.dylibs/libomp.dylib +0 -0
- pyvale/__init__.py +89 -0
- pyvale/analyticmeshgen.py +102 -0
- pyvale/analyticsimdatafactory.py +91 -0
- pyvale/analyticsimdatagenerator.py +323 -0
- pyvale/blendercalibrationdata.py +15 -0
- pyvale/blenderlightdata.py +26 -0
- pyvale/blendermaterialdata.py +15 -0
- pyvale/blenderrenderdata.py +30 -0
- pyvale/blenderscene.py +488 -0
- pyvale/blendertools.py +420 -0
- pyvale/camera.py +146 -0
- pyvale/cameradata.py +69 -0
- pyvale/cameradata2d.py +84 -0
- pyvale/camerastereo.py +217 -0
- pyvale/cameratools.py +522 -0
- pyvale/cython/rastercyth.c +32211 -0
- pyvale/cython/rastercyth.cpython-311-darwin.so +0 -0
- pyvale/cython/rastercyth.py +640 -0
- pyvale/data/__init__.py +5 -0
- pyvale/data/cal_target.tiff +0 -0
- pyvale/data/case00_HEX20_out.e +0 -0
- pyvale/data/case00_HEX27_out.e +0 -0
- pyvale/data/case00_HEX8_out.e +0 -0
- pyvale/data/case00_TET10_out.e +0 -0
- pyvale/data/case00_TET14_out.e +0 -0
- pyvale/data/case00_TET4_out.e +0 -0
- pyvale/data/case13_out.e +0 -0
- pyvale/data/case16_out.e +0 -0
- pyvale/data/case17_out.e +0 -0
- pyvale/data/case18_1_out.e +0 -0
- pyvale/data/case18_2_out.e +0 -0
- pyvale/data/case18_3_out.e +0 -0
- pyvale/data/case25_out.e +0 -0
- pyvale/data/case26_out.e +0 -0
- pyvale/data/optspeckle_2464x2056px_spec5px_8bit_gblur1px.tiff +0 -0
- pyvale/dataset.py +325 -0
- pyvale/errorcalculator.py +109 -0
- pyvale/errordriftcalc.py +146 -0
- pyvale/errorintegrator.py +336 -0
- pyvale/errorrand.py +607 -0
- pyvale/errorsyscalib.py +134 -0
- pyvale/errorsysdep.py +327 -0
- pyvale/errorsysfield.py +414 -0
- pyvale/errorsysindep.py +808 -0
- pyvale/examples/__init__.py +5 -0
- pyvale/examples/basics/ex1_1_basicscalars_therm2d.py +131 -0
- pyvale/examples/basics/ex1_2_sensormodel_therm2d.py +158 -0
- pyvale/examples/basics/ex1_3_customsens_therm3d.py +216 -0
- pyvale/examples/basics/ex1_4_basicerrors_therm3d.py +153 -0
- pyvale/examples/basics/ex1_5_fielderrs_therm3d.py +168 -0
- pyvale/examples/basics/ex1_6_caliberrs_therm2d.py +133 -0
- pyvale/examples/basics/ex1_7_spatavg_therm2d.py +123 -0
- pyvale/examples/basics/ex2_1_basicvectors_disp2d.py +112 -0
- pyvale/examples/basics/ex2_2_vectorsens_disp2d.py +111 -0
- pyvale/examples/basics/ex2_3_sensangle_disp2d.py +139 -0
- pyvale/examples/basics/ex2_4_chainfielderrs_disp2d.py +196 -0
- pyvale/examples/basics/ex2_5_vectorfields3d_disp3d.py +109 -0
- pyvale/examples/basics/ex3_1_basictensors_strain2d.py +114 -0
- pyvale/examples/basics/ex3_2_tensorsens2d_strain2d.py +111 -0
- pyvale/examples/basics/ex3_3_tensorsens3d_strain3d.py +182 -0
- pyvale/examples/basics/ex4_1_expsim2d_thermmech2d.py +171 -0
- pyvale/examples/basics/ex4_2_expsim3d_thermmech3d.py +252 -0
- pyvale/examples/genanalyticdata/ex1_1_scalarvisualisation.py +35 -0
- pyvale/examples/genanalyticdata/ex1_2_scalarcasebuild.py +43 -0
- pyvale/examples/genanalyticdata/ex2_1_analyticsensors.py +80 -0
- pyvale/examples/imagedef2d/ex_imagedef2d_todisk.py +79 -0
- pyvale/examples/renderblender/ex1_1_blenderscene.py +121 -0
- pyvale/examples/renderblender/ex1_2_blenderdeformed.py +119 -0
- pyvale/examples/renderblender/ex2_1_stereoscene.py +128 -0
- pyvale/examples/renderblender/ex2_2_stereodeformed.py +131 -0
- pyvale/examples/renderblender/ex3_1_blendercalibration.py +120 -0
- pyvale/examples/renderrasterisation/ex_rastenp.py +153 -0
- pyvale/examples/renderrasterisation/ex_rastercyth_oneframe.py +218 -0
- pyvale/examples/renderrasterisation/ex_rastercyth_static_cypara.py +187 -0
- pyvale/examples/renderrasterisation/ex_rastercyth_static_pypara.py +190 -0
- pyvale/examples/visualisation/ex1_1_plot_traces.py +102 -0
- pyvale/examples/visualisation/ex2_1_animate_sim.py +89 -0
- pyvale/experimentsimulator.py +175 -0
- pyvale/field.py +128 -0
- pyvale/fieldconverter.py +351 -0
- pyvale/fieldsampler.py +111 -0
- pyvale/fieldscalar.py +166 -0
- pyvale/fieldtensor.py +218 -0
- pyvale/fieldtransform.py +388 -0
- pyvale/fieldvector.py +213 -0
- pyvale/generatorsrandom.py +505 -0
- pyvale/imagedef2d.py +569 -0
- pyvale/integratorfactory.py +240 -0
- pyvale/integratorquadrature.py +217 -0
- pyvale/integratorrectangle.py +165 -0
- pyvale/integratorspatial.py +89 -0
- pyvale/integratortype.py +43 -0
- pyvale/output.py +17 -0
- pyvale/pyvaleexceptions.py +11 -0
- pyvale/raster.py +31 -0
- pyvale/rastercy.py +77 -0
- pyvale/rasternp.py +603 -0
- pyvale/rendermesh.py +147 -0
- pyvale/sensorarray.py +178 -0
- pyvale/sensorarrayfactory.py +196 -0
- pyvale/sensorarraypoint.py +278 -0
- pyvale/sensordata.py +71 -0
- pyvale/sensordescriptor.py +213 -0
- pyvale/sensortools.py +142 -0
- pyvale/simcases/case00_HEX20.i +242 -0
- pyvale/simcases/case00_HEX27.i +242 -0
- pyvale/simcases/case00_HEX8.i +242 -0
- pyvale/simcases/case00_TET10.i +242 -0
- pyvale/simcases/case00_TET14.i +242 -0
- pyvale/simcases/case00_TET4.i +242 -0
- pyvale/simcases/case01.i +101 -0
- pyvale/simcases/case02.i +156 -0
- pyvale/simcases/case03.i +136 -0
- pyvale/simcases/case04.i +181 -0
- pyvale/simcases/case05.i +234 -0
- pyvale/simcases/case06.i +305 -0
- pyvale/simcases/case07.geo +135 -0
- pyvale/simcases/case07.i +87 -0
- pyvale/simcases/case08.geo +144 -0
- pyvale/simcases/case08.i +153 -0
- pyvale/simcases/case09.geo +204 -0
- pyvale/simcases/case09.i +87 -0
- pyvale/simcases/case10.geo +204 -0
- pyvale/simcases/case10.i +257 -0
- pyvale/simcases/case11.geo +337 -0
- pyvale/simcases/case11.i +147 -0
- pyvale/simcases/case12.geo +388 -0
- pyvale/simcases/case12.i +329 -0
- pyvale/simcases/case13.i +140 -0
- pyvale/simcases/case14.i +159 -0
- pyvale/simcases/case15.geo +337 -0
- pyvale/simcases/case15.i +150 -0
- pyvale/simcases/case16.geo +391 -0
- pyvale/simcases/case16.i +357 -0
- pyvale/simcases/case17.geo +135 -0
- pyvale/simcases/case17.i +144 -0
- pyvale/simcases/case18.i +254 -0
- pyvale/simcases/case18_1.i +254 -0
- pyvale/simcases/case18_2.i +254 -0
- pyvale/simcases/case18_3.i +254 -0
- pyvale/simcases/case19.geo +252 -0
- pyvale/simcases/case19.i +99 -0
- pyvale/simcases/case20.geo +252 -0
- pyvale/simcases/case20.i +250 -0
- pyvale/simcases/case21.geo +74 -0
- pyvale/simcases/case21.i +155 -0
- pyvale/simcases/case22.geo +82 -0
- pyvale/simcases/case22.i +140 -0
- pyvale/simcases/case23.geo +164 -0
- pyvale/simcases/case23.i +140 -0
- pyvale/simcases/case24.geo +79 -0
- pyvale/simcases/case24.i +123 -0
- pyvale/simcases/case25.geo +82 -0
- pyvale/simcases/case25.i +140 -0
- pyvale/simcases/case26.geo +166 -0
- pyvale/simcases/case26.i +140 -0
- pyvale/simcases/run_1case.py +61 -0
- pyvale/simcases/run_all_cases.py +69 -0
- pyvale/simcases/run_build_case.py +64 -0
- pyvale/simcases/run_example_cases.py +69 -0
- pyvale/simtools.py +67 -0
- pyvale/visualexpplotter.py +191 -0
- pyvale/visualimagedef.py +74 -0
- pyvale/visualimages.py +76 -0
- pyvale/visualopts.py +493 -0
- pyvale/visualsimanimator.py +111 -0
- pyvale/visualsimsensors.py +318 -0
- pyvale/visualtools.py +136 -0
- pyvale/visualtraceplotter.py +142 -0
- pyvale-2025.5.3.dist-info/METADATA +144 -0
- pyvale-2025.5.3.dist-info/RECORD +175 -0
- pyvale-2025.5.3.dist-info/WHEEL +6 -0
- pyvale-2025.5.3.dist-info/licenses/LICENSE +21 -0
- pyvale-2025.5.3.dist-info/top_level.txt +1 -0
pyvale/errorsysfield.py
ADDED
|
@@ -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)
|