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