pyvale 2025.4.0__py3-none-any.whl → 2025.5.1__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 +78 -64
- pyvale/analyticmeshgen.py +102 -0
- pyvale/{core/analyticsimdatafactory.py → analyticsimdatafactory.py} +44 -16
- 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/{core/camera.py → camera.py} +15 -15
- pyvale/{core/cameradata.py → cameradata.py} +27 -22
- pyvale/{core/cameradata2d.py → cameradata2d.py} +8 -6
- pyvale/camerastereo.py +217 -0
- pyvale/{core/cameratools.py → cameratools.py} +220 -26
- pyvale/{core/cython → cython}/rastercyth.py +11 -7
- pyvale/data/__init__.py +5 -7
- 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/{core/dataset.py → dataset.py} +91 -16
- pyvale/{core/errorcalculator.py → errorcalculator.py} +13 -16
- pyvale/{core/errordriftcalc.py → errordriftcalc.py} +14 -14
- pyvale/{core/errorintegrator.py → errorintegrator.py} +25 -28
- pyvale/{core/errorrand.py → errorrand.py} +39 -46
- pyvale/errorsyscalib.py +134 -0
- pyvale/{core/errorsysdep.py → errorsysdep.py} +25 -29
- pyvale/{core/errorsysfield.py → errorsysfield.py} +59 -52
- pyvale/{core/errorsysindep.py → errorsysindep.py} +85 -182
- pyvale/examples/__init__.py +5 -7
- 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/{analyticdatagen → genanalyticdata}/ex1_1_scalarvisualisation.py +6 -9
- pyvale/examples/{analyticdatagen → genanalyticdata}/ex1_2_scalarcasebuild.py +8 -11
- pyvale/examples/{analyticdatagen → genanalyticdata}/ex2_1_analyticsensors.py +9 -12
- pyvale/examples/imagedef2d/ex_imagedef2d_todisk.py +8 -15
- 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/{rasterisation → renderrasterisation}/ex_rastenp.py +6 -7
- pyvale/examples/{rasterisation → renderrasterisation}/ex_rastercyth_oneframe.py +5 -7
- pyvale/examples/{rasterisation → renderrasterisation}/ex_rastercyth_static_cypara.py +6 -13
- pyvale/examples/{rasterisation → renderrasterisation}/ex_rastercyth_static_pypara.py +9 -12
- pyvale/examples/{ex1_4_thermal2d.py → visualisation/ex1_1_plot_traces.py} +33 -20
- pyvale/examples/{features/ex_animation_tools_3dmonoblock.py → visualisation/ex2_1_animate_sim.py} +37 -31
- pyvale/experimentsimulator.py +175 -0
- pyvale/{core/field.py → field.py} +6 -14
- pyvale/fieldconverter.py +351 -0
- pyvale/{core/fieldsampler.py → fieldsampler.py} +9 -10
- pyvale/{core/fieldscalar.py → fieldscalar.py} +17 -18
- pyvale/{core/fieldtensor.py → fieldtensor.py} +23 -26
- pyvale/{core/fieldtransform.py → fieldtransform.py} +9 -5
- pyvale/{core/fieldvector.py → fieldvector.py} +14 -16
- pyvale/{core/generatorsrandom.py → generatorsrandom.py} +29 -52
- pyvale/{core/imagedef2d.py → imagedef2d.py} +11 -8
- pyvale/{core/integratorfactory.py → integratorfactory.py} +12 -13
- pyvale/{core/integratorquadrature.py → integratorquadrature.py} +57 -32
- pyvale/integratorrectangle.py +165 -0
- pyvale/{core/integratorspatial.py → integratorspatial.py} +9 -10
- pyvale/{core/integratortype.py → integratortype.py} +7 -8
- pyvale/output.py +17 -0
- pyvale/pyvaleexceptions.py +11 -0
- pyvale/{core/raster.py → raster.py} +8 -8
- pyvale/{core/rastercy.py → rastercy.py} +11 -10
- pyvale/{core/rasternp.py → rasternp.py} +12 -13
- pyvale/{core/rendermesh.py → rendermesh.py} +10 -19
- pyvale/{core/sensorarray.py → sensorarray.py} +7 -8
- pyvale/{core/sensorarrayfactory.py → sensorarrayfactory.py} +64 -78
- pyvale/{core/sensorarraypoint.py → sensorarraypoint.py} +39 -41
- pyvale/{core/sensordata.py → sensordata.py} +7 -8
- pyvale/sensordescriptor.py +213 -0
- pyvale/{core/sensortools.py → sensortools.py} +8 -9
- pyvale/simcases/case00_HEX20.i +5 -5
- pyvale/simcases/case00_HEX27.i +5 -5
- pyvale/simcases/case00_HEX8.i +242 -0
- pyvale/simcases/case00_TET10.i +2 -2
- pyvale/simcases/case00_TET14.i +2 -2
- pyvale/simcases/case00_TET4.i +242 -0
- pyvale/simcases/run_1case.py +1 -1
- pyvale/simtools.py +67 -0
- pyvale/visualexpplotter.py +191 -0
- pyvale/{core/visualimagedef.py → visualimagedef.py} +13 -10
- pyvale/{core/visualimages.py → visualimages.py} +10 -9
- pyvale/visualopts.py +493 -0
- pyvale/{core/visualsimanimator.py → visualsimanimator.py} +47 -19
- pyvale/visualsimsensors.py +318 -0
- pyvale/visualtools.py +136 -0
- pyvale/visualtraceplotter.py +142 -0
- {pyvale-2025.4.0.dist-info → pyvale-2025.5.1.dist-info}/METADATA +17 -14
- pyvale-2025.5.1.dist-info/RECORD +172 -0
- {pyvale-2025.4.0.dist-info → pyvale-2025.5.1.dist-info}/WHEEL +1 -1
- pyvale/core/__init__.py +0 -7
- pyvale/core/analyticmeshgen.py +0 -59
- pyvale/core/analyticsimdatagenerator.py +0 -160
- pyvale/core/cython/rastercyth.c +0 -32267
- pyvale/core/experimentsimulator.py +0 -99
- pyvale/core/fieldconverter.py +0 -154
- pyvale/core/integratorrectangle.py +0 -88
- pyvale/core/optimcheckfuncs.py +0 -153
- pyvale/core/sensordescriptor.py +0 -101
- pyvale/core/visualexpplotter.py +0 -151
- pyvale/core/visualopts.py +0 -180
- pyvale/core/visualsimplotter.py +0 -182
- pyvale/core/visualtools.py +0 -81
- pyvale/core/visualtraceplotter.py +0 -256
- pyvale/examples/analyticdatagen/__init__.py +0 -7
- pyvale/examples/ex1_1_thermal2d.py +0 -89
- pyvale/examples/ex1_2_thermal2d.py +0 -111
- pyvale/examples/ex1_3_thermal2d.py +0 -113
- pyvale/examples/ex1_5_thermal2d.py +0 -105
- pyvale/examples/ex2_1_thermal3d .py +0 -87
- pyvale/examples/ex2_2_thermal3d.py +0 -51
- pyvale/examples/ex2_3_thermal3d.py +0 -109
- pyvale/examples/ex3_1_displacement2d.py +0 -47
- pyvale/examples/ex3_2_displacement2d.py +0 -79
- pyvale/examples/ex3_3_displacement2d.py +0 -104
- pyvale/examples/ex3_4_displacement2d.py +0 -105
- pyvale/examples/ex4_1_strain2d.py +0 -57
- pyvale/examples/ex4_2_strain2d.py +0 -79
- pyvale/examples/ex4_3_strain2d.py +0 -100
- pyvale/examples/ex5_1_multiphysics2d.py +0 -78
- pyvale/examples/ex6_1_multiphysics2d_expsim.py +0 -118
- pyvale/examples/ex6_2_multiphysics3d_expsim.py +0 -158
- pyvale/examples/features/__init__.py +0 -7
- pyvale/examples/features/ex_area_avg.py +0 -89
- pyvale/examples/features/ex_calibration_error.py +0 -108
- pyvale/examples/features/ex_chain_field_errs.py +0 -141
- pyvale/examples/features/ex_field_errs.py +0 -78
- pyvale/examples/features/ex_sensor_single_angle_batch.py +0 -110
- pyvale-2025.4.0.dist-info/RECORD +0 -157
- {pyvale-2025.4.0.dist-info → pyvale-2025.5.1.dist-info}/licenses/LICENSE +0 -0
- {pyvale-2025.4.0.dist-info → pyvale-2025.5.1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,158 @@
|
|
|
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 model & `get_measurements()` vs `calc_measurements()`
|
|
9
|
+
--------------------------------------------------------------------------------
|
|
10
|
+
In this example we explain the pyvale virtual sensor measurement model. For a
|
|
11
|
+
virtual sensor in pyvale a measurement is defined as measurement = truth +
|
|
12
|
+
systematic error + random error. Sources of systematic errors include: spatial/
|
|
13
|
+
temporal averaging, uncertainty in position / sampling time / orientation,
|
|
14
|
+
digitisation, saturation, and calibration. Sources of random error are generally
|
|
15
|
+
due to measurement noise characterised by a given probability distribution.
|
|
16
|
+
|
|
17
|
+
Random errors can be mitigated by performing multiple experiments and averaging.
|
|
18
|
+
However, systematic errors cannot easily be accounted for without a forward
|
|
19
|
+
model of the source of the error. Characterising the contribution of systematic
|
|
20
|
+
errors to the total measurement error is a key application of `pyvale`.
|
|
21
|
+
|
|
22
|
+
between the `get_measurements()` and `calc_measurements()` methods for a sensor
|
|
23
|
+
array. Calling `get_measurements()` retrieves the results for the current
|
|
24
|
+
simulated experiment whereas calling `calc_measurements()` will generate a new
|
|
25
|
+
simulated experiment by sampling / calculating the systematic and random errors.
|
|
26
|
+
|
|
27
|
+
Test case: Scalar field point sensors (thermocouples) on a 2D thermal simulation
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
import matplotlib.pyplot as plt
|
|
31
|
+
import mooseherder as mh
|
|
32
|
+
import pyvale as pyv
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def main() -> None:
|
|
36
|
+
# The first part of this example is the similar to basics example 1.1, so
|
|
37
|
+
# feel free to skip to after the first call to `calc_measurements()`.
|
|
38
|
+
|
|
39
|
+
# Here we load a pre-generated MOOSE finite element simulation dataset that
|
|
40
|
+
# comes packaged with pyvale. The simulation is a 2D rectangular plate with
|
|
41
|
+
# a bi-directional temperature gradient.
|
|
42
|
+
data_path = pyv.DataSet.thermal_2d_path()
|
|
43
|
+
sim_data = mh.ExodusReader(data_path).read_all_sim_data()
|
|
44
|
+
field_key: str = "temperature"
|
|
45
|
+
# Scale to mm to make 3D visualisation scaling easier as pyvista scales
|
|
46
|
+
# everything to unity
|
|
47
|
+
sim_data = pyv.scale_length_units(scale=1000.0,
|
|
48
|
+
sim_data=sim_data,
|
|
49
|
+
disp_comps=None)
|
|
50
|
+
|
|
51
|
+
# We now use a helper function to create a grid of sensor locations but we
|
|
52
|
+
# could have also manually built the numpy array of sensor locations which
|
|
53
|
+
# has the shape=(num_sensors,coord[x,y,z]).
|
|
54
|
+
n_sens = (4,1,1)
|
|
55
|
+
x_lims = (0.0,100.0)
|
|
56
|
+
y_lims = (0.0,50.0)
|
|
57
|
+
z_lims = (0.0,0.0)
|
|
58
|
+
sens_pos = pyv.create_sensor_pos_array(n_sens,x_lims,y_lims,z_lims)
|
|
59
|
+
|
|
60
|
+
# This dataclass contains the parameters to build our sensor array. We can
|
|
61
|
+
# also customise the output frequency, the sensor area and the sensor
|
|
62
|
+
# orientation. For now we will use the defaults which assumes an ideal point
|
|
63
|
+
# sensor sampling at the simulation time steps.
|
|
64
|
+
sens_data = pyv.SensorData(positions=sens_pos)
|
|
65
|
+
|
|
66
|
+
# Now that we have our sensor locations we can use the sensor factory to
|
|
67
|
+
# build a basic thermocouple array with some useful defaults. In later
|
|
68
|
+
# examples we will see how to customise sensor parameters and errors.
|
|
69
|
+
# This basic thermocouple array includes a 5% systematic and random error -
|
|
70
|
+
# We are specifically using exaggerated errors here for visualisation.
|
|
71
|
+
tc_array = pyv.SensorArrayFactory \
|
|
72
|
+
.thermocouples_basic_errs(sim_data,
|
|
73
|
+
sens_data,
|
|
74
|
+
elem_dims=2,
|
|
75
|
+
field_name=field_key,
|
|
76
|
+
errs_pc=5.0)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# We have built our sensor array so now we can call `calc_measurements()` to
|
|
80
|
+
# generate simulated sensor traces.
|
|
81
|
+
measurements = tc_array.calc_measurements()
|
|
82
|
+
|
|
83
|
+
# From here we are going to experiment with repeated calls to
|
|
84
|
+
# `calc_measurements()` and `get_measurements()` for our sensor array. We
|
|
85
|
+
# will print the results to the console as well as plotting time traces of
|
|
86
|
+
# the simulated sensor output. All further explanations are in the print
|
|
87
|
+
# statements below.
|
|
88
|
+
|
|
89
|
+
print("\n"+80*"-")
|
|
90
|
+
print("For a sensor array: "
|
|
91
|
+
+ "measurement = truth + sysematic error + random error")
|
|
92
|
+
print(f"\nmeasurements.shape = {measurements.shape} = "
|
|
93
|
+
+ "(n_sensors,n_field_components,n_timesteps)\n")
|
|
94
|
+
print("Here we have a scalar temperature field so only 1 field component.")
|
|
95
|
+
print("The truth, systematic error and random error arrays all have the "+
|
|
96
|
+
"same shape.")
|
|
97
|
+
|
|
98
|
+
sens_print: int = 0
|
|
99
|
+
time_print: int = 5
|
|
100
|
+
comp_print: int = 0
|
|
101
|
+
|
|
102
|
+
print(80*"-")
|
|
103
|
+
print(f"Looking at the last {time_print} virtual measurements" +
|
|
104
|
+
f" of sensor {sens_print}:")
|
|
105
|
+
|
|
106
|
+
pyv.print_measurements(sens_array=tc_array,
|
|
107
|
+
sensors=(sens_print,sens_print+1),
|
|
108
|
+
components=(comp_print,comp_print+1),
|
|
109
|
+
time_steps=(measurements.shape[2]-time_print,
|
|
110
|
+
measurements.shape[2]))
|
|
111
|
+
print(80*"-")
|
|
112
|
+
print("If we call the `calc_measurements()` method then the errors are "
|
|
113
|
+
+ "re-calculated.")
|
|
114
|
+
measurements = tc_array.calc_measurements()
|
|
115
|
+
|
|
116
|
+
pyv.print_measurements(sens_array=tc_array,
|
|
117
|
+
sensors=(sens_print,sens_print+1),
|
|
118
|
+
components=(comp_print,comp_print+1),
|
|
119
|
+
time_steps=(measurements.shape[2]-time_print,
|
|
120
|
+
measurements.shape[2]))
|
|
121
|
+
|
|
122
|
+
(fig,ax) = pyv.plot_time_traces(tc_array,field_key)
|
|
123
|
+
ax.set_title("Exp 1: called calc_measurements()")
|
|
124
|
+
|
|
125
|
+
print(80*"-")
|
|
126
|
+
print("If we call the `get_measurements()` method then the errors are the "
|
|
127
|
+
+ "same:")
|
|
128
|
+
measurements = tc_array.get_measurements()
|
|
129
|
+
|
|
130
|
+
pyv.print_measurements(sens_array=tc_array,
|
|
131
|
+
sensors=(sens_print,sens_print+1),
|
|
132
|
+
components=(comp_print,comp_print+1),
|
|
133
|
+
time_steps=(measurements.shape[2]-time_print,
|
|
134
|
+
measurements.shape[2]))
|
|
135
|
+
|
|
136
|
+
(fig,ax) = pyv.plot_time_traces(tc_array,field_key)
|
|
137
|
+
ax.set_title("Exp 2: called get_measurements()")
|
|
138
|
+
|
|
139
|
+
print(80*"-")
|
|
140
|
+
print("If we call the `calc_measurements()` method again we generate / "
|
|
141
|
+
"sample new errors:")
|
|
142
|
+
measurements = tc_array.calc_measurements()
|
|
143
|
+
|
|
144
|
+
pyv.print_measurements(sens_array=tc_array,
|
|
145
|
+
sensors=(sens_print,sens_print+1),
|
|
146
|
+
components=(comp_print,comp_print+1),
|
|
147
|
+
time_steps=(measurements.shape[2]-time_print,
|
|
148
|
+
measurements.shape[2]))
|
|
149
|
+
|
|
150
|
+
(fig,ax) = pyv.plot_time_traces(tc_array,field_key)
|
|
151
|
+
ax.set_title("Exp 3: called calc_measurements()")
|
|
152
|
+
|
|
153
|
+
print(80*"-")
|
|
154
|
+
|
|
155
|
+
plt.show()
|
|
156
|
+
|
|
157
|
+
if __name__ == "__main__":
|
|
158
|
+
main()
|
|
@@ -0,0 +1,216 @@
|
|
|
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: Building a point sensor array from scratch with custom errors
|
|
9
|
+
--------------------------------------------------------------------------------
|
|
10
|
+
Here we build a custom point sensor array from scratch that is similar to the
|
|
11
|
+
pre-built thermocouple array from example 1.1. For this example we switch to a
|
|
12
|
+
3D thermal simulation of a fusion heatsink component.
|
|
13
|
+
|
|
14
|
+
Test case: Scalar field point sensors (thermocouples) on a 3D thermal simulation
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import numpy as np
|
|
19
|
+
import matplotlib.pyplot as plt
|
|
20
|
+
import mooseherder as mh
|
|
21
|
+
import pyvale as pyv
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def main() -> None:
|
|
25
|
+
# To build our custom point sensor array we need to at minimum provide a
|
|
26
|
+
# `IField` (i.e. `FieldScaler`, `FieldVector`, `FieldTensor`) and a
|
|
27
|
+
# `SensorData` object. For labelling visualisations (e.g. axis labels and
|
|
28
|
+
# unit labels) we can also provide a `SensorDescriptor` object.
|
|
29
|
+
# Once we have built our `SensorArrayPoint` object from these we can then
|
|
30
|
+
# attach custom chains of different types of random and systematic errors
|
|
31
|
+
# to be evaluated when we run our measurement simulation. This example is
|
|
32
|
+
# based on the same thermal example we have used in the last two examples so
|
|
33
|
+
# we start by loading our simulation data:
|
|
34
|
+
|
|
35
|
+
data_path = pyv.DataSet.thermal_3d_path()
|
|
36
|
+
sim_data = mh.ExodusReader(data_path).read_all_sim_data()
|
|
37
|
+
# Scale to mm to make 3D visualisation scaling easier as pyvista scales
|
|
38
|
+
# everything to unity
|
|
39
|
+
sim_data = pyv.scale_length_units(scale=1000.0,
|
|
40
|
+
sim_data=sim_data,
|
|
41
|
+
disp_comps=None)
|
|
42
|
+
|
|
43
|
+
# We are going to build a custom temperature sensor so we need a scalar
|
|
44
|
+
# field object to perform interpolation to the sensor locations at the
|
|
45
|
+
# desired sampling times.
|
|
46
|
+
field_key: str = "temperature"
|
|
47
|
+
t_field = pyv.FieldScalar(sim_data,
|
|
48
|
+
field_key=field_key,
|
|
49
|
+
elem_dims=3)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# Next we need to create our `SensorData` object which will set the position
|
|
53
|
+
# and sampling times of our sensors. We use the same helper function we used
|
|
54
|
+
# previously to create a uniformly spaced grid of sensors in space
|
|
55
|
+
n_sens = (1,4,1)
|
|
56
|
+
x_lims = (12.5,12.5)
|
|
57
|
+
y_lims = (0.0,33.0)
|
|
58
|
+
z_lims = (0.0,12.0)
|
|
59
|
+
sens_pos = pyv.create_sensor_pos_array(n_sens,x_lims,y_lims,z_lims)
|
|
60
|
+
|
|
61
|
+
# We are also going to specify the times at which we would like to simulate
|
|
62
|
+
# measurements. Setting this to `None` will default the measurements times
|
|
63
|
+
# to match the simulation time steps.
|
|
64
|
+
sample_times = np.linspace(0.0,np.max(sim_data.time),50)
|
|
65
|
+
|
|
66
|
+
sensor_data = pyv.SensorData(positions=sens_pos,
|
|
67
|
+
sample_times=sample_times)
|
|
68
|
+
|
|
69
|
+
# Finally, we can create a `SensorDescriptor` which will be used to label
|
|
70
|
+
# the visualisation and sensor trace plots we have seen in previous
|
|
71
|
+
# examples.
|
|
72
|
+
use_auto_descriptor: str = "blank"
|
|
73
|
+
if use_auto_descriptor == "manual":
|
|
74
|
+
descriptor = pyv.SensorDescriptor(name="Temperature",
|
|
75
|
+
symbol="T",
|
|
76
|
+
units = r"^{\circ}C",
|
|
77
|
+
tag = "TC")
|
|
78
|
+
elif use_auto_descriptor == "factory":
|
|
79
|
+
descriptor = pyv.SensorDescriptorFactory.temperature_descriptor()
|
|
80
|
+
else:
|
|
81
|
+
descriptor = pyv.SensorDescriptor()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# We can now build our custom point sensor array. This sensor array has no
|
|
85
|
+
# errors so if we call `get_measurements()` or `calc_measurements()` we will
|
|
86
|
+
# be able to extract the simulation truth values at the sensor locations.
|
|
87
|
+
tc_array = pyv.SensorArrayPoint(sensor_data,
|
|
88
|
+
t_field,
|
|
89
|
+
descriptor)
|
|
90
|
+
|
|
91
|
+
# This is a new 3D simulation we are analysing so we should visualise the
|
|
92
|
+
# sensor locations before we run our measurement simulation. We use the same
|
|
93
|
+
# code as we did in example 1.1 to display the sensor locations.
|
|
94
|
+
|
|
95
|
+
# We are also going to save some figures to disk as well as displaying them
|
|
96
|
+
# interactively so we create a directory for this:
|
|
97
|
+
output_path = Path.cwd() / "pyvale-output"
|
|
98
|
+
if not output_path.is_dir():
|
|
99
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
100
|
+
|
|
101
|
+
pv_plot = pyv.plot_point_sensors_on_sim(tc_array,field_key)
|
|
102
|
+
|
|
103
|
+
pv_plot.camera_position = [(59.354, 43.428, 69.946),
|
|
104
|
+
(-2.858, 13.189, 4.523),
|
|
105
|
+
(-0.215, 0.948, -0.233)]
|
|
106
|
+
|
|
107
|
+
save_render = output_path / "customsensors_ex1_3_sensorlocs.svg"
|
|
108
|
+
pv_plot.save_graphic(save_render) # only for .svg .eps .ps .pdf .tex
|
|
109
|
+
pv_plot.screenshot(save_render.with_suffix(".png"))
|
|
110
|
+
|
|
111
|
+
pv_plot.show()
|
|
112
|
+
|
|
113
|
+
# If we want to simulate sources of uncertainty for our sensor array we need
|
|
114
|
+
# to add an `ErrIntegrator` to our sensor array using the method
|
|
115
|
+
# `set_error_integrator()`. We provide our `ErrIntegrator` a list of error
|
|
116
|
+
# objects which will be evaluated in the order specified in the list.
|
|
117
|
+
#
|
|
118
|
+
# In pyvale errors have a type specified as: random / systematic
|
|
119
|
+
# (`EErrorType`) and a dependence `EErrDependence` as: independent /
|
|
120
|
+
# dependent. When analysing errors all random all systematic errors are
|
|
121
|
+
# grouped and summed together.
|
|
122
|
+
#
|
|
123
|
+
# The error dependence determines if an error is
|
|
124
|
+
# calculated based on the truth (independent) or the accumulated measurement
|
|
125
|
+
# based on all previous errors in the chain (dependent). Some errors are
|
|
126
|
+
# purely independent such as random noise with a normal distribution with a
|
|
127
|
+
# set standard devitation. An example of an error that is dependent would be
|
|
128
|
+
# saturation which must be place last in the error chain and will clamp the
|
|
129
|
+
# final sensor value to be within the specified bounds.
|
|
130
|
+
#
|
|
131
|
+
# pyvale provides a library of different random `ErrRand*` and systematic
|
|
132
|
+
# `ErrSys*` errors which can be found listed in the docs. In the next
|
|
133
|
+
# example we will explore the error library but for now we will specify some
|
|
134
|
+
# common error types. Try experimenting with the code below to turn the
|
|
135
|
+
# different error types off and on to see how it changes the virtual sensor
|
|
136
|
+
# measurements.
|
|
137
|
+
errors_on = {"sys": True,
|
|
138
|
+
"rand": True}
|
|
139
|
+
|
|
140
|
+
error_chain = []
|
|
141
|
+
if errors_on["sys"]:
|
|
142
|
+
# This systematic error is just a constant offset of -5 to all simulated
|
|
143
|
+
# measurements. Note that error values should be specified in the same
|
|
144
|
+
# units as the simulation.
|
|
145
|
+
error_chain.append(pyv.ErrSysOffset(offset=-10.0))
|
|
146
|
+
|
|
147
|
+
# This systematic error samples from a uniform probability distribution.
|
|
148
|
+
error_chain.append(pyv.ErrSysUnif(low=-10.0,
|
|
149
|
+
high=10.0))
|
|
150
|
+
|
|
151
|
+
if errors_on["rand"]:
|
|
152
|
+
# This random error is generated by sampling from a normal distribution
|
|
153
|
+
# with the given standard deviation in simulation units.
|
|
154
|
+
error_chain.append(pyv.ErrRandNorm(std=5.0))
|
|
155
|
+
|
|
156
|
+
# This random error is generated as a percentage sampled from uniform
|
|
157
|
+
# probability distribution
|
|
158
|
+
error_chain.append(pyv.ErrRandUnifPercent(low_percent=-5.0,
|
|
159
|
+
high_percent=5.0))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
# By default pyvale does not store all individual error source
|
|
163
|
+
# calculations (i.e. only the total random and total systematic error are
|
|
164
|
+
# stored) to save memory but this can be changed using `ErrIntOpts`. This
|
|
165
|
+
# can also be used to force all errors to behave if they
|
|
166
|
+
|
|
167
|
+
if len(error_chain) > 0:
|
|
168
|
+
err_int_opts = pyv.ErrIntOpts()
|
|
169
|
+
error_integrator = pyv.ErrIntegrator(error_chain,
|
|
170
|
+
sensor_data,
|
|
171
|
+
tc_array.get_measurement_shape(),
|
|
172
|
+
err_int_opts=err_int_opts)
|
|
173
|
+
tc_array.set_error_integrator(error_integrator)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# Now that we have added our error chain we can run a simulation to sample
|
|
177
|
+
# from all our error sources.
|
|
178
|
+
measurements = tc_array.calc_measurements()
|
|
179
|
+
|
|
180
|
+
# We display the simulation results by printing to the console and by
|
|
181
|
+
# plotting the sensor times traces. Try experimenting with the errors above
|
|
182
|
+
# to see how the results change.
|
|
183
|
+
print("\n"+80*"-")
|
|
184
|
+
print("For a virtual sensor: measurement = truth + sysematic error + random error")
|
|
185
|
+
print(f"measurements.shape = {measurements.shape} = "+
|
|
186
|
+
"(n_sensors,n_field_components,n_timesteps)\n")
|
|
187
|
+
print("The truth, systematic error and random error arrays have the same "+
|
|
188
|
+
"shape.")
|
|
189
|
+
|
|
190
|
+
print(80*"-")
|
|
191
|
+
|
|
192
|
+
sens_print: int = 0
|
|
193
|
+
time_print: int = 5
|
|
194
|
+
comp_print: int = 0
|
|
195
|
+
|
|
196
|
+
print(f"These are the last {time_print} virtual measurements of sensor "
|
|
197
|
+
+ f"{sens_print}:")
|
|
198
|
+
|
|
199
|
+
pyv.print_measurements(sens_array=tc_array,
|
|
200
|
+
sensors=(sens_print,sens_print+1),
|
|
201
|
+
components=(comp_print,comp_print+1),
|
|
202
|
+
time_steps=(measurements.shape[2]-time_print,
|
|
203
|
+
measurements.shape[2]))
|
|
204
|
+
print(80*"-")
|
|
205
|
+
|
|
206
|
+
(fig,ax) = pyv.plot_time_traces(tc_array,field_key)
|
|
207
|
+
|
|
208
|
+
save_traces = output_path/"customsensors_ex1_3_sensortraces.png"
|
|
209
|
+
fig.savefig(save_traces, dpi=300, bbox_inches="tight")
|
|
210
|
+
fig.savefig(save_traces.with_suffix(".svg"), dpi=300, bbox_inches="tight")
|
|
211
|
+
|
|
212
|
+
plt.show()
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
if __name__ == "__main__":
|
|
216
|
+
main()
|
|
@@ -0,0 +1,153 @@
|
|
|
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: Overview of the basic error library
|
|
9
|
+
--------------------------------------------------------------------------------
|
|
10
|
+
Building on what we learned in examples 1.1-1.3 we now have a look at the basic
|
|
11
|
+
error library for pyvale. The sensor error models in pyvale are grouped into the
|
|
12
|
+
types of random (`ErrRand*`) and systematic (`ErrSys*`). In this example we will
|
|
13
|
+
consider probability distribution based sampled errors, constant offsets and
|
|
14
|
+
basic systematic errors such as digitisation / saturation.
|
|
15
|
+
|
|
16
|
+
In the next examples we will consider more advanced error sources including:
|
|
17
|
+
field errors that perturb the sensor parameters (e.g. location, sampling time
|
|
18
|
+
and orientation) requiring re-interpolation of the underlying field data; and
|
|
19
|
+
calibration errors.
|
|
20
|
+
|
|
21
|
+
Test case: Scalar field point sensors (thermocouples) on a 3D thermal simulation
|
|
22
|
+
|
|
23
|
+
Advanced users: It is also possible to write custom errors by writing your own
|
|
24
|
+
class that implements the `IErrCalculator` abstract base class and then add them
|
|
25
|
+
to your error chain.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
import numpy as np
|
|
29
|
+
import matplotlib.pyplot as plt
|
|
30
|
+
import mooseherder as mh
|
|
31
|
+
import pyvale as pyv
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def main() -> None:
|
|
35
|
+
# First we use everything we learned from the first three examples to build
|
|
36
|
+
# a thermocouple sensor array for the same 3D thermal simulation we have
|
|
37
|
+
# analysed in the previous example.
|
|
38
|
+
data_path = pyv.DataSet.thermal_3d_path()
|
|
39
|
+
sim_data = mh.ExodusReader(data_path).read_all_sim_data()
|
|
40
|
+
sim_data = pyv.scale_length_units(scale=1000.0,
|
|
41
|
+
sim_data=sim_data,
|
|
42
|
+
disp_comps=None)
|
|
43
|
+
n_sens = (1,4,1)
|
|
44
|
+
x_lims = (12.5,12.5)
|
|
45
|
+
y_lims = (0.0,33.0)
|
|
46
|
+
z_lims = (0.0,12.0)
|
|
47
|
+
sens_pos = pyv.create_sensor_pos_array(n_sens,x_lims,y_lims,z_lims)
|
|
48
|
+
|
|
49
|
+
sample_times = np.linspace(0.0,np.max(sim_data.time),50) # | None
|
|
50
|
+
|
|
51
|
+
sensor_data = pyv.SensorData(positions=sens_pos,
|
|
52
|
+
sample_times=sample_times)
|
|
53
|
+
|
|
54
|
+
field_key: str = "temperature"
|
|
55
|
+
tc_array = pyv.SensorArrayFactory \
|
|
56
|
+
.thermocouples_no_errs(sim_data,
|
|
57
|
+
sensor_data,
|
|
58
|
+
elem_dims=3,
|
|
59
|
+
field_name=field_key)
|
|
60
|
+
|
|
61
|
+
# Now we have our thermocouple array applied to our simulation without any
|
|
62
|
+
# errors we can build a custom chain of basic errors. Here we will start by
|
|
63
|
+
# adding a series of systematic errors that are independent:
|
|
64
|
+
err_chain = []
|
|
65
|
+
|
|
66
|
+
# For probability sampling systematic errors the distribution is sampled to
|
|
67
|
+
# provide an offset which is assumed to be constant over all sensor sampling
|
|
68
|
+
# times. This is different to random errors which are sampled to provide a
|
|
69
|
+
# different error for each sensor and time step.
|
|
70
|
+
|
|
71
|
+
# These systematic errors provide a constant offset to all measurements in
|
|
72
|
+
# simulation units or as a percentage.
|
|
73
|
+
err_chain.append(pyv.ErrSysOffset(offset=-10.0))
|
|
74
|
+
err_chain.append(pyv.ErrSysOffsetPercent(offset_percent=-1.0))
|
|
75
|
+
|
|
76
|
+
# These systematic errors are sampled from a uniform or normal probability
|
|
77
|
+
# distribution either in simulation units or as a percentage.
|
|
78
|
+
err_chain.append(pyv.ErrSysUnif(low=-1.0,
|
|
79
|
+
high=1.0))
|
|
80
|
+
err_chain.append(pyv.ErrSysUnifPercent(low_percent=-1.0,
|
|
81
|
+
high_percent=1.0))
|
|
82
|
+
err_chain.append(pyv.ErrSysNorm(std=1.0))
|
|
83
|
+
err_chain.append(pyv.ErrSysNormPercent(std_percent=1.0))
|
|
84
|
+
|
|
85
|
+
# pyvale includes a series of random number generator objects that wrap the
|
|
86
|
+
# random number generators from numpy. These are named `Gen*` and can be
|
|
87
|
+
# used with an `ErrSysGen` or an `ErrSysGenPercent` object to create custom
|
|
88
|
+
# probability distribution sampling errors:
|
|
89
|
+
sys_gen = pyv.GenTriangular(left=-1.0,
|
|
90
|
+
mode=0.0,
|
|
91
|
+
right=1.0)
|
|
92
|
+
err_chain.append(pyv.ErrSysGen(sys_gen))
|
|
93
|
+
|
|
94
|
+
# We can also build the equivalent of `ErrSysUnifPercent` above using a
|
|
95
|
+
# `Gen` object inserted into an `ErrSysGenPercent` object:
|
|
96
|
+
unif_gen = pyv.GenUniform(low=-1.0,
|
|
97
|
+
high=1.0)
|
|
98
|
+
err_chain.append(pyv.ErrSysGenPercent(unif_gen))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# We can also add a series of random errors in a similar manner to the
|
|
102
|
+
# systematic errors above noting that these will generate a new error for
|
|
103
|
+
# each sensor and each time step whereas the systematic error sampling
|
|
104
|
+
# provides a constant shift over all sampling times for each sensor.
|
|
105
|
+
err_chain.append(pyv.ErrRandNorm(std = 2.0))
|
|
106
|
+
err_chain.append(pyv.ErrRandNormPercent(std_percent=2.0))
|
|
107
|
+
err_chain.append(pyv.ErrRandUnif(low=-2.0,high=2.0))
|
|
108
|
+
err_chain.append(pyv.ErrRandUnifPercent(low_percent=-2.0,
|
|
109
|
+
high_percent=2.0))
|
|
110
|
+
rand_gen = pyv.GenTriangular(left=-5.0,
|
|
111
|
+
mode=0.0,
|
|
112
|
+
right=5.0)
|
|
113
|
+
err_chain.append(pyv.ErrRandGenerator(rand_gen))
|
|
114
|
+
|
|
115
|
+
# Finally we add some dependent systematic errors including rounding errors,
|
|
116
|
+
# digitisation and saturation. Note that the saturation error must be placed
|
|
117
|
+
# last in the error chain. Try changing some of these values to see how the
|
|
118
|
+
# sensor traces change - particularly the saturation error.
|
|
119
|
+
err_chain.append(pyv.ErrSysRoundOff(pyv.ERoundMethod.ROUND,0.1))
|
|
120
|
+
err_chain.append(pyv.ErrSysDigitisation(bits_per_unit=2**16/100))
|
|
121
|
+
err_chain.append(pyv.ErrSysSaturation(meas_min=0.0,meas_max=400.0))
|
|
122
|
+
|
|
123
|
+
err_int = pyv.ErrIntegrator(err_chain,
|
|
124
|
+
sensor_data,
|
|
125
|
+
tc_array.get_measurement_shape())
|
|
126
|
+
tc_array.set_error_integrator(err_int)
|
|
127
|
+
|
|
128
|
+
# Now we can run the sensor simulation and display the results to see the
|
|
129
|
+
# different error sources as we have done in previous examples.
|
|
130
|
+
measurements = tc_array.calc_measurements()
|
|
131
|
+
|
|
132
|
+
print(80*"-")
|
|
133
|
+
|
|
134
|
+
sens_print: int = 0
|
|
135
|
+
time_print: int = 5
|
|
136
|
+
comp_print: int = 0
|
|
137
|
+
|
|
138
|
+
print(f"These are the last {time_print} virtual measurements of sensor "
|
|
139
|
+
+ f"{sens_print}:")
|
|
140
|
+
|
|
141
|
+
pyv.print_measurements(sens_array=tc_array,
|
|
142
|
+
sensors=(sens_print,sens_print+1),
|
|
143
|
+
components=(comp_print,comp_print+1),
|
|
144
|
+
time_steps=(measurements.shape[2]-time_print,
|
|
145
|
+
measurements.shape[2]))
|
|
146
|
+
print(80*"-")
|
|
147
|
+
|
|
148
|
+
pyv.plot_time_traces(tc_array,field_key)
|
|
149
|
+
plt.show()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
if __name__ == '__main__':
|
|
153
|
+
main()
|