pyvale 2025.5.3__cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.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 +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-x86_64-linux-gnu.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.libs/libgomp-a34b3233.so.1.0.0 +0 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# pyvale: the python validation engine
|
|
3
|
+
# License: MIT
|
|
4
|
+
# Copyright (C) 2025 The Computer Aided Validation Team
|
|
5
|
+
# ==============================================================================
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Pyvale example: Field-based systematic errors
|
|
9
|
+
--------------------------------------------------------------------------------
|
|
10
|
+
In this example we give an overview of field-based systematic errors. Field
|
|
11
|
+
errors require additional interpolation of the underlying physical field such as
|
|
12
|
+
uncertainty in a sensors position or sampling time. For this example we will
|
|
13
|
+
focus on field error sources that perturb sensor locations and sampling times.
|
|
14
|
+
In later examples we will analyse sensor orientation for vector and tensor
|
|
15
|
+
fields.
|
|
16
|
+
|
|
17
|
+
Note that field errors are more computationally intensive than basic errors as
|
|
18
|
+
they require additional interpolations of the underlying physical field.
|
|
19
|
+
|
|
20
|
+
Test case: Scalar field point sensors (thermocouples) on a 3D thermal simulation
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
import numpy as np
|
|
25
|
+
import matplotlib.pyplot as plt
|
|
26
|
+
import mooseherder as mh
|
|
27
|
+
import pyvale as pyv
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def main() -> None:
|
|
31
|
+
# First we use everything we learned from the first three examples to build
|
|
32
|
+
# a thermocouple sensor array for the same 3D thermal simulation we have
|
|
33
|
+
# analysed in the previous examples. Then we will look at a new type of
|
|
34
|
+
# systematic error called a field error which requires additional
|
|
35
|
+
# interpolation of the underlying physical field to be measured.
|
|
36
|
+
data_path = pyv.DataSet.thermal_3d_path()
|
|
37
|
+
sim_data = mh.ExodusReader(data_path).read_all_sim_data()
|
|
38
|
+
sim_data = pyv.scale_length_units(scale=1000.0,
|
|
39
|
+
sim_data=sim_data,
|
|
40
|
+
disp_comps=None)
|
|
41
|
+
|
|
42
|
+
n_sens = (1,4,1)
|
|
43
|
+
x_lims = (12.5,12.5)
|
|
44
|
+
y_lims = (0.0,33.0)
|
|
45
|
+
z_lims = (0.0,12.0)
|
|
46
|
+
sens_pos = pyv.create_sensor_pos_array(n_sens,x_lims,y_lims,z_lims)
|
|
47
|
+
|
|
48
|
+
sample_times = np.linspace(0.0,np.max(sim_data.time),50) # | None
|
|
49
|
+
|
|
50
|
+
sensor_data = pyv.SensorData(positions=sens_pos,
|
|
51
|
+
sample_times=sample_times)
|
|
52
|
+
|
|
53
|
+
field_key: str = "temperature"
|
|
54
|
+
tc_array = pyv.SensorArrayFactory \
|
|
55
|
+
.thermocouples_no_errs(sim_data,
|
|
56
|
+
sensor_data,
|
|
57
|
+
elem_dims=3,
|
|
58
|
+
field_name=field_key)
|
|
59
|
+
|
|
60
|
+
# Now we will create a field error data class which we will use to build our
|
|
61
|
+
# field error. This controls which sensor parameters will be perturbed such
|
|
62
|
+
# as: position, time and orientation. Here we will perturb the sensor
|
|
63
|
+
# positions on the face of the block using a normal distribution and we will
|
|
64
|
+
# also perturb the measurement times.
|
|
65
|
+
|
|
66
|
+
# We can apply a constant offset to each sensors position in x,y,z by
|
|
67
|
+
# providing a shape=(num_sensors,coord[x,y,z]) array. Here we apply a
|
|
68
|
+
# constant offset in the y and z direction for all sensors.
|
|
69
|
+
pos_offset_xyz = np.array((0.0,1.0,1.0),dtype=np.float64)
|
|
70
|
+
pos_offset_xyz = np.tile(pos_offset_xyz,(sens_pos.shape[0],1))
|
|
71
|
+
|
|
72
|
+
# We can also apply a constant offset to the sampling times for all sensors
|
|
73
|
+
time_offset = np.full((sample_times.shape[0],),0.1)
|
|
74
|
+
|
|
75
|
+
# Using the `Gen*` random generators in pyvale we can randomly perturb the
|
|
76
|
+
# position or sampling times of our virtual sensors.
|
|
77
|
+
pos_rand = pyv.GenNormal(std=1.0) # units = mm
|
|
78
|
+
time_rand = pyv.GenNormal(std=0.1) # units = s
|
|
79
|
+
|
|
80
|
+
# Now we put everything into our field error data class ready to build our
|
|
81
|
+
# field error object. Have a look at the other parameters in this data class
|
|
82
|
+
# to geta feel for the other types of supported field errors. We will look
|
|
83
|
+
# at the orientation and area averaging errors when we look at vector and
|
|
84
|
+
# tensor fields in later examples.
|
|
85
|
+
field_err_data = pyv.ErrFieldData(
|
|
86
|
+
pos_offset_xyz=pos_offset_xyz,
|
|
87
|
+
time_offset=time_offset,
|
|
88
|
+
pos_rand_xyz=(None,pos_rand,pos_rand),
|
|
89
|
+
time_rand=time_rand
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Adding our field error to our error chain is exactly the same as the basic
|
|
93
|
+
# errors we have seen previously. We can also combine field errors with
|
|
94
|
+
# basic errors and place them anywhere in our error chain. We can even chain
|
|
95
|
+
# field errors together which we will look at in the next example. For now
|
|
96
|
+
# we will just have a single field error so we can easily visualise what
|
|
97
|
+
# this type of error does.
|
|
98
|
+
err_chain = []
|
|
99
|
+
|
|
100
|
+
# A field error needs to know which field it should interpolate for error
|
|
101
|
+
# calculations so we provide the field from the sensor array as well as the
|
|
102
|
+
# field error error data class.
|
|
103
|
+
err_chain.append(pyv.ErrSysField(tc_array.get_field(),
|
|
104
|
+
field_err_data))
|
|
105
|
+
err_int = pyv.ErrIntegrator(err_chain,
|
|
106
|
+
sensor_data,
|
|
107
|
+
tc_array.get_measurement_shape())
|
|
108
|
+
tc_array.set_error_integrator(err_int)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# Now we can run the sensor simulation and display the results to see what
|
|
112
|
+
# our field error has done.
|
|
113
|
+
measurements = tc_array.calc_measurements()
|
|
114
|
+
|
|
115
|
+
print(80*"-")
|
|
116
|
+
|
|
117
|
+
sens_print: int = 0
|
|
118
|
+
time_print: int = 5
|
|
119
|
+
comp_print: int = 0
|
|
120
|
+
|
|
121
|
+
print(f"These are the last {time_print} virtual measurements of sensor "
|
|
122
|
+
+ f"{sens_print}:")
|
|
123
|
+
|
|
124
|
+
pyv.print_measurements(sens_array=tc_array,
|
|
125
|
+
sensors=(sens_print,sens_print+1),
|
|
126
|
+
components=(comp_print,comp_print+1),
|
|
127
|
+
time_steps=(measurements.shape[2]-time_print,
|
|
128
|
+
measurements.shape[2]))
|
|
129
|
+
print(80*"-")
|
|
130
|
+
|
|
131
|
+
# We are going to save some figures to disk as well as displaying them
|
|
132
|
+
# interactively so we create a directory for this:
|
|
133
|
+
output_path = Path.cwd() / "pyvale-output"
|
|
134
|
+
if not output_path.is_dir():
|
|
135
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# If we analyse the time traces we can see offsets in the sensor value and
|
|
139
|
+
# the sampling times which we expect from our field error setup.
|
|
140
|
+
(fig,ax) = pyv.plot_time_traces(tc_array,field_key)
|
|
141
|
+
|
|
142
|
+
save_traces = output_path/"field_ex1_5_sensortraces.png"
|
|
143
|
+
fig.savefig(save_traces, dpi=300, bbox_inches="tight")
|
|
144
|
+
fig.savefig(save_traces.with_suffix(".svg"), dpi=300, bbox_inches="tight")
|
|
145
|
+
|
|
146
|
+
plt.show()
|
|
147
|
+
|
|
148
|
+
# It is also possible to view the perturbed sensor locations on the
|
|
149
|
+
# simulation mesh if we create a plot after running the sensor simulation.
|
|
150
|
+
pv_plot = pyv.plot_point_sensors_on_sim(tc_array,field_key)
|
|
151
|
+
pv_plot.camera_position = [(59.354, 43.428, 69.946),
|
|
152
|
+
(-2.858, 13.189, 4.523),
|
|
153
|
+
(-0.215, 0.948, -0.233)]
|
|
154
|
+
|
|
155
|
+
save_render = output_path / "fielderrs_ex1_5_sensorlocs.svg"
|
|
156
|
+
pv_plot.save_graphic(save_render) # only for .svg .eps .ps .pdf .tex
|
|
157
|
+
pv_plot.screenshot(save_render.with_suffix(".png"))
|
|
158
|
+
|
|
159
|
+
pv_plot.show()
|
|
160
|
+
|
|
161
|
+
# We have saved an image of the sensor traces and the perturbed locations of
|
|
162
|
+
# the sensors to the `pyvale-output` directory in your current working
|
|
163
|
+
# directory. Analyse these figures side by side to show that the location of
|
|
164
|
+
# the perturbed sensor locations matches the expected sensor traces.
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
if __name__ == '__main__':
|
|
168
|
+
main()
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# pyvale: the python validation engine
|
|
3
|
+
# License: MIT
|
|
4
|
+
# Copyright (C) 2025 The Computer Aided Validation Team
|
|
5
|
+
# ==============================================================================
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Pyvale example: Sensor calibration systematic errors
|
|
9
|
+
--------------------------------------------------------------------------------
|
|
10
|
+
In this example we show how pyvale can simulate sensor calibration errors with
|
|
11
|
+
user defined calibration functions.
|
|
12
|
+
|
|
13
|
+
Test case: Scalar field point sensors (thermocouples) on a 2D thermal simulation
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import matplotlib.pyplot as plt
|
|
18
|
+
import mooseherder as mh
|
|
19
|
+
import pyvale as pyv
|
|
20
|
+
|
|
21
|
+
# First we need to define some calibration functions. These functions must take
|
|
22
|
+
# a numpy array and return a numpy array of the same shape. We start by
|
|
23
|
+
# defining what we think our calibration is called `assumed_calib()` and then
|
|
24
|
+
# we also need to define the ground truth calibration `truth_calib()` so that
|
|
25
|
+
# we can calculate the error between them. The calibration functions shown below
|
|
26
|
+
# are simplified versions of the typical calibration curves for a K-type
|
|
27
|
+
# thermocouple.
|
|
28
|
+
def assumed_calib(signal: np.ndarray) -> np.ndarray:
|
|
29
|
+
return 24.3*signal + 0.616
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def truth_calib(signal: np.ndarray) -> np.ndarray:
|
|
33
|
+
return -0.01897 + 25.41881*signal - 0.42456*signal**2 + 0.04365*signal**3
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def main() -> None:
|
|
37
|
+
|
|
38
|
+
# We are first going to do a quick analytical calculation for the minimum
|
|
39
|
+
# and maximum systematic error we expect between our assumed and true
|
|
40
|
+
# calibration. For our true calibration we know this holds between 0 and 6mV
|
|
41
|
+
# so we perform the calculation over this range and print the min/max
|
|
42
|
+
# expected error over this range.
|
|
43
|
+
n_cal_divs = 10000
|
|
44
|
+
signal_calib_range = np.array((0.0,6.0),dtype=np.float64)
|
|
45
|
+
milli_volts = np.linspace(signal_calib_range[0],
|
|
46
|
+
signal_calib_range[1],
|
|
47
|
+
n_cal_divs)
|
|
48
|
+
temp_truth = truth_calib(milli_volts)
|
|
49
|
+
temp_assumed = assumed_calib(milli_volts)
|
|
50
|
+
calib_error = temp_assumed - temp_truth
|
|
51
|
+
|
|
52
|
+
print()
|
|
53
|
+
print(80*"-")
|
|
54
|
+
print(f"Max calibrated temperature: {np.min(temp_truth)} degC")
|
|
55
|
+
print(f"Min calibrated temperature: {np.max(temp_truth)} degC")
|
|
56
|
+
print()
|
|
57
|
+
print(f"Calibration error over signal:"
|
|
58
|
+
+ f" {signal_calib_range[0]} to {signal_calib_range[1]} mV")
|
|
59
|
+
print(f"Max calib error: {np.max(calib_error)}")
|
|
60
|
+
print(f"Min calib error: {np.min(calib_error)}")
|
|
61
|
+
print(80*"-")
|
|
62
|
+
print()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# Now let's go back and build the 2D thermal plate with simulated
|
|
66
|
+
# thermocouples that we analysed in the first two examples. We use this
|
|
67
|
+
# simulation as the temperatures are within our calibrated range.
|
|
68
|
+
data_path = pyv.DataSet.thermal_2d_path()
|
|
69
|
+
sim_data = mh.ExodusReader(data_path).read_all_sim_data()
|
|
70
|
+
sim_data = pyv.scale_length_units(scale=1000.0,
|
|
71
|
+
sim_data=sim_data,
|
|
72
|
+
disp_comps=None)
|
|
73
|
+
|
|
74
|
+
n_sens = (4,1,1)
|
|
75
|
+
x_lims = (0.0,100.0)
|
|
76
|
+
y_lims = (0.0,50.0)
|
|
77
|
+
z_lims = (0.0,0.0)
|
|
78
|
+
sens_pos = pyv.create_sensor_pos_array(n_sens,x_lims,y_lims,z_lims)
|
|
79
|
+
|
|
80
|
+
sample_times = np.linspace(0.0,np.max(sim_data.time),50) # | None
|
|
81
|
+
|
|
82
|
+
sensor_data = pyv.SensorData(positions=sens_pos,
|
|
83
|
+
sample_times=sample_times)
|
|
84
|
+
|
|
85
|
+
field_key: str = "temperature"
|
|
86
|
+
tc_array = pyv.SensorArrayFactory \
|
|
87
|
+
.thermocouples_no_errs(sim_data,
|
|
88
|
+
sensor_data,
|
|
89
|
+
elem_dims=2,
|
|
90
|
+
field_name=field_key)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# With our assumed and true calibration functions we can build our
|
|
94
|
+
# calibration error object and add it to our error chain as normal. Note
|
|
95
|
+
# that the truth calibration function must be inverted numerically so to
|
|
96
|
+
# increase accuracy the number of divisions can be increased. However, 1e4
|
|
97
|
+
# divisions should be suitable for most applications.
|
|
98
|
+
cal_err = pyv.ErrSysCalibration(assumed_calib,
|
|
99
|
+
truth_calib,
|
|
100
|
+
signal_calib_range,
|
|
101
|
+
n_cal_divs=10000)
|
|
102
|
+
sys_err_int = pyv.ErrIntegrator([cal_err],
|
|
103
|
+
sensor_data,
|
|
104
|
+
tc_array.get_measurement_shape())
|
|
105
|
+
tc_array.set_error_integrator(sys_err_int)
|
|
106
|
+
|
|
107
|
+
# Now we run our sensor simulation to see what our calibration does.
|
|
108
|
+
measurements = tc_array.calc_measurements()
|
|
109
|
+
|
|
110
|
+
print(80*"-")
|
|
111
|
+
|
|
112
|
+
sens_print: int = 0
|
|
113
|
+
time_print: int = 5
|
|
114
|
+
comp_print: int = 0
|
|
115
|
+
|
|
116
|
+
print(f"These are the last {time_print} virtual measurements of sensor "
|
|
117
|
+
+ f"{sens_print}:")
|
|
118
|
+
|
|
119
|
+
pyv.print_measurements(sens_array=tc_array,
|
|
120
|
+
sensors=(sens_print,sens_print+1),
|
|
121
|
+
components=(comp_print,comp_print+1),
|
|
122
|
+
time_steps=(measurements.shape[2]-time_print,
|
|
123
|
+
measurements.shape[2]))
|
|
124
|
+
print(80*"-")
|
|
125
|
+
|
|
126
|
+
pyv.plot_time_traces(tc_array,field_key)
|
|
127
|
+
plt.show()
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
main()
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# pyvale: the python validation engine
|
|
3
|
+
# License: MIT
|
|
4
|
+
# Copyright (C) 2025 The Computer Aided Validation Team
|
|
5
|
+
# ==============================================================================
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Pyvale example: Sensor spatial averaging and averaging errors
|
|
9
|
+
--------------------------------------------------------------------------------
|
|
10
|
+
In this example we show how pyvale can simulate sensor spatial averaging for
|
|
11
|
+
ground truth calculations as well as for calculating systematic errors.
|
|
12
|
+
|
|
13
|
+
Test case: Scalar field point sensors (thermocouples) on a 2D thermal simulation
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import numpy as np
|
|
17
|
+
import matplotlib.pyplot as plt
|
|
18
|
+
import mooseherder as mh
|
|
19
|
+
import pyvale as pyv
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def main() -> None:
|
|
23
|
+
# First we are going to build a custom sensor array so we can control how
|
|
24
|
+
# the ground truth is extracted for a sensor using area averaging. Note that
|
|
25
|
+
# the default is an ideal point sensor with no spatial averaging. Later we
|
|
26
|
+
# will add area averaging as a systematic error. Note that it is possible to
|
|
27
|
+
# have an ideal point sensor with no area averaging for the truth and then
|
|
28
|
+
# add an area averaging error. It is also possible to have a truth that is
|
|
29
|
+
# area averaged without and area averaging error. The first part of this is
|
|
30
|
+
# the same as the 3D thermal example we have used previously then we control
|
|
31
|
+
# the area averaging using the sensor data object.
|
|
32
|
+
data_path = pyv.DataSet.thermal_2d_path()
|
|
33
|
+
sim_data = mh.ExodusReader(data_path).read_all_sim_data()
|
|
34
|
+
sim_data = pyv.scale_length_units(scale=1000.0,
|
|
35
|
+
sim_data=sim_data,
|
|
36
|
+
disp_comps=None)
|
|
37
|
+
|
|
38
|
+
descriptor = pyv.SensorDescriptorFactory.temperature_descriptor()
|
|
39
|
+
|
|
40
|
+
field_key = "temperature"
|
|
41
|
+
t_field = pyv.FieldScalar(sim_data,
|
|
42
|
+
field_key=field_key,
|
|
43
|
+
elem_dims=2)
|
|
44
|
+
|
|
45
|
+
n_sens = (4,1,1)
|
|
46
|
+
x_lims = (0.0,100.0)
|
|
47
|
+
y_lims = (0.0,50.0)
|
|
48
|
+
z_lims = (0.0,0.0)
|
|
49
|
+
sens_pos = pyv.create_sensor_pos_array(n_sens,x_lims,y_lims,z_lims)
|
|
50
|
+
|
|
51
|
+
sample_times = np.linspace(0.0,np.max(sim_data.time),50) # | None
|
|
52
|
+
|
|
53
|
+
# This is where we control the setup of the area averaging. We need to
|
|
54
|
+
# specify the sensor dimensions and the type of numerical spatial
|
|
55
|
+
# integration to use. Here we specify a square sensor in x and y with 4
|
|
56
|
+
# point Gaussian quadrature integration. It is worth noting that increasing
|
|
57
|
+
# the number of integration points will increase computational cost as each
|
|
58
|
+
# additional integration point requires an additional interpolation of the
|
|
59
|
+
# physical field.
|
|
60
|
+
sensor_dims = np.array([20.0,20.0,0]) # units = mm
|
|
61
|
+
sensor_data = pyv.SensorData(positions=sens_pos,
|
|
62
|
+
sample_times=sample_times,
|
|
63
|
+
spatial_averager=pyv.EIntSpatialType.QUAD4PT,
|
|
64
|
+
spatial_dims=sensor_dims)
|
|
65
|
+
|
|
66
|
+
# We have added spatial averaging to our sensor data so we can now create
|
|
67
|
+
# our sensor array as we have done in previous examples.
|
|
68
|
+
tc_array = pyv.SensorArrayPoint(sensor_data,
|
|
69
|
+
t_field,
|
|
70
|
+
descriptor)
|
|
71
|
+
|
|
72
|
+
# We are also going to create a field error that includes area averaging as
|
|
73
|
+
# an error. We do this by adding the option to our field error data class
|
|
74
|
+
# specifying rectangular integration with 1 point.
|
|
75
|
+
area_avg_err_data = pyv.ErrFieldData(
|
|
76
|
+
spatial_averager=pyv.EIntSpatialType.RECT1PT,
|
|
77
|
+
spatial_dims=np.array((5.0,5.0)),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
# We add the field error to our error chain as normal. We could combine it
|
|
81
|
+
# with any of our other error models but we will isolate it for now so we
|
|
82
|
+
# can see what it does.
|
|
83
|
+
err_chain = []
|
|
84
|
+
err_chain.append(pyv.ErrSysField(t_field,
|
|
85
|
+
area_avg_err_data))
|
|
86
|
+
error_int = pyv.ErrIntegrator(err_chain,
|
|
87
|
+
sensor_data,
|
|
88
|
+
tc_array.get_measurement_shape())
|
|
89
|
+
tc_array.set_error_integrator(error_int)
|
|
90
|
+
|
|
91
|
+
# Now we run our sensor simulation to see how spatial averaging changes our
|
|
92
|
+
measurements = tc_array.calc_measurements()
|
|
93
|
+
|
|
94
|
+
print(80*"-")
|
|
95
|
+
|
|
96
|
+
sens_print: int = 0
|
|
97
|
+
time_print: int = 5
|
|
98
|
+
comp_print: int = 0
|
|
99
|
+
|
|
100
|
+
print(f"These are the last {time_print} virtual measurements of sensor "
|
|
101
|
+
+ f"{sens_print}:")
|
|
102
|
+
|
|
103
|
+
pyv.print_measurements(sens_array=tc_array,
|
|
104
|
+
sensors=(sens_print,sens_print+1),
|
|
105
|
+
components=(comp_print,comp_print+1),
|
|
106
|
+
time_steps=(measurements.shape[2]-time_print,
|
|
107
|
+
measurements.shape[2]))
|
|
108
|
+
print(80*"-")
|
|
109
|
+
|
|
110
|
+
pyv.plot_time_traces(tc_array,field_key)
|
|
111
|
+
plt.show()
|
|
112
|
+
|
|
113
|
+
# From here you now have everything you need to build your own sensor
|
|
114
|
+
# simulations for scalar field sensors using pyvale. In the next examples
|
|
115
|
+
# we will look at sensors applied to vector (e.g. displacement) and tensor
|
|
116
|
+
# fields (e.g. strain). If you don't need to sample vector or tensor fields
|
|
117
|
+
# then skip ahead to the examples on experiment simulation where you will
|
|
118
|
+
# learn how to perform Monte-Carlo sensor uncertainty quantification
|
|
119
|
+
# simulations and to analyse the results with pyvale.
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
if __name__ == "__main__":
|
|
123
|
+
main()
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# pyvale: the python validation engine
|
|
3
|
+
# License: MIT
|
|
4
|
+
# Copyright (C) 2025 The Computer Aided Validation Team
|
|
5
|
+
# ==============================================================================
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Pyvale example: Basic vector field (displacement) sensors
|
|
9
|
+
--------------------------------------------------------------------------------
|
|
10
|
+
In this example we use the sensor array factory to build a set of displacement
|
|
11
|
+
sensors that can sample the displacement vector field from a solid mechanics
|
|
12
|
+
simulation. In the next example we will examine how we can build custom vector
|
|
13
|
+
field sensors as we did for scalar field in the first set of examples.
|
|
14
|
+
|
|
15
|
+
Note that this tutorial assumes you are familiar with the use of pyvale for
|
|
16
|
+
scalar fields as described in the first set of examples.
|
|
17
|
+
|
|
18
|
+
Test case: point displacement sensors on a 2D plate with hole loaded in tension
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
import matplotlib.pyplot as plt
|
|
23
|
+
import mooseherder as mh
|
|
24
|
+
import pyvale as pyv
|
|
25
|
+
|
|
26
|
+
def main() -> None:
|
|
27
|
+
# Here we load a pre-packaged dataset from pyvale that is the output of a
|
|
28
|
+
# MOOSE simulation in exodus format. The simulation is a linear elastic
|
|
29
|
+
# rectangular plate with a central hole that is loaded in tension (we will
|
|
30
|
+
# see a visualisation of the mesh and results later).
|
|
31
|
+
data_path = pyv.DataSet.mechanical_2d_path()
|
|
32
|
+
# We use `mooseherder` to load the exodus file into a `SimData` object.
|
|
33
|
+
sim_data = mh.ExodusReader(data_path).read_all_sim_data()
|
|
34
|
+
|
|
35
|
+
# We scale our SI simulation to mm including the displacement fields which
|
|
36
|
+
# are also in length units. The string keys we have provided here must match
|
|
37
|
+
# the variable names you have in your SimData object.
|
|
38
|
+
field_name = "disp"
|
|
39
|
+
field_comps = ("disp_x","disp_y")
|
|
40
|
+
sim_data = pyv.scale_length_units(scale=1000.0,
|
|
41
|
+
sim_data=sim_data,
|
|
42
|
+
disp_comps=field_comps)
|
|
43
|
+
|
|
44
|
+
# Creating a displacement field point sensor array is similar to what we
|
|
45
|
+
# have already done for scalar fields we just need to specify the string
|
|
46
|
+
# keys for the displacement fields in the sim data object we have loaded.
|
|
47
|
+
# For 2D vector fields we expect to have 2 components which are typically:
|
|
48
|
+
# ("disp_x","disp_y"). For 3D vector fields we have 3 field components which
|
|
49
|
+
# are typically: ("disp_x","disp_y","disp_z").
|
|
50
|
+
n_sens = (2,3,1)
|
|
51
|
+
x_lims = (0.0,100.0)
|
|
52
|
+
y_lims = (0.0,150.0)
|
|
53
|
+
z_lims = (0.0,0.0)
|
|
54
|
+
sens_pos = pyv.create_sensor_pos_array(n_sens,x_lims,y_lims,z_lims)
|
|
55
|
+
|
|
56
|
+
sens_data = pyv.SensorData(positions=sens_pos)
|
|
57
|
+
|
|
58
|
+
disp_sens_array = pyv.SensorArrayFactory \
|
|
59
|
+
.disp_sensors_basic_errs(sim_data,
|
|
60
|
+
sens_data,
|
|
61
|
+
elem_dims=2,
|
|
62
|
+
field_name=field_name,
|
|
63
|
+
field_comps=field_comps,
|
|
64
|
+
errs_pc=2.0)
|
|
65
|
+
|
|
66
|
+
# We run our sensor simulation as normal but we note that the second
|
|
67
|
+
# dimension of our measurement array will have the two vector components in
|
|
68
|
+
# the order we specified them in the field keys.
|
|
69
|
+
measurements = disp_sens_array.calc_measurements()
|
|
70
|
+
|
|
71
|
+
# Here we print the shape of the measurement array so we can see that the
|
|
72
|
+
# second dimension contains both our vector components. We also print some
|
|
73
|
+
# of the sensor measurements for the first vector component.
|
|
74
|
+
print("\n"+80*"-")
|
|
75
|
+
print("For a virtual sensor: measurement = truth + sysematic error + random error")
|
|
76
|
+
print(f"measurements.shape = {measurements.shape} = "+
|
|
77
|
+
"(n_sensors,n_field_components,n_timesteps)\n")
|
|
78
|
+
print("The truth, systematic error and random error arrays have the same "+
|
|
79
|
+
"shape.")
|
|
80
|
+
|
|
81
|
+
print(80*"-")
|
|
82
|
+
|
|
83
|
+
sens_print: int = 0
|
|
84
|
+
time_print: int = 5
|
|
85
|
+
comp_print: int = 0
|
|
86
|
+
|
|
87
|
+
print(f"These are the last {time_print} virtual measurements of sensor "
|
|
88
|
+
+ f"{sens_print}:")
|
|
89
|
+
|
|
90
|
+
pyv.print_measurements(sens_array=disp_sens_array,
|
|
91
|
+
sensors=(sens_print,sens_print+1),
|
|
92
|
+
components=(comp_print,comp_print+1),
|
|
93
|
+
time_steps=(measurements.shape[2]-time_print,
|
|
94
|
+
measurements.shape[2]))
|
|
95
|
+
print(80*"-")
|
|
96
|
+
|
|
97
|
+
# Now that we have multiple field components we can plot each of them on the
|
|
98
|
+
# simulation mesh and visulise the sensor locations with respect to these
|
|
99
|
+
# fields.
|
|
100
|
+
for ff in field_comps:
|
|
101
|
+
pv_plot = pyv.plot_point_sensors_on_sim(disp_sens_array,ff)
|
|
102
|
+
pv_plot.show(cpos="xy")
|
|
103
|
+
|
|
104
|
+
# We can also plot the traces for each component of the displacement field.
|
|
105
|
+
for ff in field_comps:
|
|
106
|
+
pyv.plot_time_traces(disp_sens_array,ff)
|
|
107
|
+
|
|
108
|
+
plt.show()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
if __name__ == "__main__":
|
|
112
|
+
main()
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# pyvale: the python validation engine
|
|
3
|
+
# License: MIT
|
|
4
|
+
# Copyright (C) 2025 The Computer Aided Validation Team
|
|
5
|
+
# ==============================================================================
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Pyvale example: Custom vector field sensors
|
|
9
|
+
--------------------------------------------------------------------------------
|
|
10
|
+
In this example we build a custom vector field sensor array which mimics the
|
|
11
|
+
sensor array we built with the factory in the previous example.
|
|
12
|
+
|
|
13
|
+
Note that this tutorial assumes you are familiar with the use of pyvale for
|
|
14
|
+
scalar fields as described in the first set of examples.
|
|
15
|
+
|
|
16
|
+
Test case: point displacement sensors on a 2D plate with hole loaded in tension
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import numpy as np
|
|
20
|
+
import matplotlib.pyplot as plt
|
|
21
|
+
import mooseherder as mh
|
|
22
|
+
import pyvale as pyv
|
|
23
|
+
|
|
24
|
+
def main() -> None:
|
|
25
|
+
|
|
26
|
+
# First we load the same 2D solid mechanics simulation we had previously as
|
|
27
|
+
# a `SimData` object and then we scale everything to millimeters.
|
|
28
|
+
data_path = pyv.DataSet.mechanical_2d_path()
|
|
29
|
+
sim_data = mh.ExodusReader(data_path).read_all_sim_data()
|
|
30
|
+
field_name = "disp"
|
|
31
|
+
field_comps = ("disp_x","disp_y")
|
|
32
|
+
sim_data = pyv.scale_length_units(scale=1000.0,
|
|
33
|
+
sim_data=sim_data,
|
|
34
|
+
disp_comps=field_comps)
|
|
35
|
+
|
|
36
|
+
# This is the key different between building a vector field sensor vs a
|
|
37
|
+
# scalar field sensor. Here we create a vector field object which we will
|
|
38
|
+
# pass to our sensor array. In later examples we will see that the process
|
|
39
|
+
# is the same for tensor fields (e.g. strain) where we create a tensor field
|
|
40
|
+
# object and pass this to our sensor array. One thing to note is that the
|
|
41
|
+
# number of field components will be different here for a 2D vs 3D
|
|
42
|
+
# simulation. Also, it is worth noting that the element dimensions
|
|
43
|
+
# parameter does not need to match the number of field components. For
|
|
44
|
+
# example: it is possible to have a surface mesh (elem_dims=2) where we
|
|
45
|
+
# have all 3 components of the displacement field.
|
|
46
|
+
disp_field = pyv.FieldVector(sim_data,field_name,field_comps,elem_dims=2)
|
|
47
|
+
|
|
48
|
+
# As we saw previously for scalar fields we define our sensor data object
|
|
49
|
+
# which determines how many point sensors we have and their sampling times.
|
|
50
|
+
# For vector field sensors we can also define the sensor orientation here
|
|
51
|
+
# which we will demonstrate in the next example.
|
|
52
|
+
n_sens = (2,3,1)
|
|
53
|
+
x_lims = (0.0,100.0)
|
|
54
|
+
y_lims = (0.0,150.0)
|
|
55
|
+
z_lims = (0.0,0.0)
|
|
56
|
+
sens_pos = pyv.create_sensor_pos_array(n_sens,x_lims,y_lims,z_lims)
|
|
57
|
+
|
|
58
|
+
# We set custom sampling times here but we could also set this to None so
|
|
59
|
+
# that the sensors sample at the simulation time steps.
|
|
60
|
+
sample_times = np.linspace(0.0,np.max(sim_data.time),50)
|
|
61
|
+
|
|
62
|
+
sens_data = pyv.SensorData(positions=sens_pos,
|
|
63
|
+
sample_times=sample_times)
|
|
64
|
+
|
|
65
|
+
# We can optionally define a custom sensor descriptor for our vector field
|
|
66
|
+
# sensor which will be used for labelling sensor placement visualisation or
|
|
67
|
+
# for time traces. It is also possible to use the sensor descriptor factory
|
|
68
|
+
# to get the same sensor descriptor object with these defaults.
|
|
69
|
+
descriptor = pyv.SensorDescriptor(name="Disp.",
|
|
70
|
+
symbol=r"u",
|
|
71
|
+
units=r"mm",
|
|
72
|
+
tag="DS",
|
|
73
|
+
components=("x","y","z"))
|
|
74
|
+
|
|
75
|
+
# The point sensor array class is generic and will take any field class
|
|
76
|
+
# that implements the field interface. So here we just pass in the vector
|
|
77
|
+
# field to create our vector field sensor array.
|
|
78
|
+
disp_sens_array = pyv.SensorArrayPoint(sens_data,
|
|
79
|
+
disp_field,
|
|
80
|
+
descriptor)
|
|
81
|
+
|
|
82
|
+
# We can add errors to our error simulation chain in exactly the same way as
|
|
83
|
+
# we did for scalar fields. We will add some simple errors for now but in
|
|
84
|
+
# the next example we will look at some field errors to do with sensor
|
|
85
|
+
# orientation that
|
|
86
|
+
error_chain = []
|
|
87
|
+
error_chain.append(pyv.ErrSysUnif(low=-0.01,high=0.01)) # units = mm
|
|
88
|
+
error_chain.append(pyv.ErrRandNorm(std=0.01)) # units = mm
|
|
89
|
+
error_int = pyv.ErrIntegrator(error_chain,
|
|
90
|
+
sens_data,
|
|
91
|
+
disp_sens_array.get_measurement_shape())
|
|
92
|
+
disp_sens_array.set_error_integrator(error_int)
|
|
93
|
+
|
|
94
|
+
disp_sens_array.calc_measurements()
|
|
95
|
+
|
|
96
|
+
# Now that we have multiple field components we can plot each of them on the
|
|
97
|
+
# simulation mesh and visulise the sensor locations with respect to these
|
|
98
|
+
# fields.
|
|
99
|
+
for ff in field_comps:
|
|
100
|
+
pv_plot = pyv.plot_point_sensors_on_sim(disp_sens_array,ff)
|
|
101
|
+
pv_plot.show(cpos="xy")
|
|
102
|
+
|
|
103
|
+
# We can also plot the traces for each component of the displacement field.
|
|
104
|
+
for ff in field_comps:
|
|
105
|
+
pyv.plot_time_traces(disp_sens_array,ff)
|
|
106
|
+
|
|
107
|
+
plt.show()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
main()
|