pyvale 2025.5.3__cp311-cp311-macosx_13_0_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.

Files changed (175) hide show
  1. pyvale/.dylibs/libomp.dylib +0 -0
  2. pyvale/__init__.py +89 -0
  3. pyvale/analyticmeshgen.py +102 -0
  4. pyvale/analyticsimdatafactory.py +91 -0
  5. pyvale/analyticsimdatagenerator.py +323 -0
  6. pyvale/blendercalibrationdata.py +15 -0
  7. pyvale/blenderlightdata.py +26 -0
  8. pyvale/blendermaterialdata.py +15 -0
  9. pyvale/blenderrenderdata.py +30 -0
  10. pyvale/blenderscene.py +488 -0
  11. pyvale/blendertools.py +420 -0
  12. pyvale/camera.py +146 -0
  13. pyvale/cameradata.py +69 -0
  14. pyvale/cameradata2d.py +84 -0
  15. pyvale/camerastereo.py +217 -0
  16. pyvale/cameratools.py +522 -0
  17. pyvale/cython/rastercyth.c +32211 -0
  18. pyvale/cython/rastercyth.cpython-311-darwin.so +0 -0
  19. pyvale/cython/rastercyth.py +640 -0
  20. pyvale/data/__init__.py +5 -0
  21. pyvale/data/cal_target.tiff +0 -0
  22. pyvale/data/case00_HEX20_out.e +0 -0
  23. pyvale/data/case00_HEX27_out.e +0 -0
  24. pyvale/data/case00_HEX8_out.e +0 -0
  25. pyvale/data/case00_TET10_out.e +0 -0
  26. pyvale/data/case00_TET14_out.e +0 -0
  27. pyvale/data/case00_TET4_out.e +0 -0
  28. pyvale/data/case13_out.e +0 -0
  29. pyvale/data/case16_out.e +0 -0
  30. pyvale/data/case17_out.e +0 -0
  31. pyvale/data/case18_1_out.e +0 -0
  32. pyvale/data/case18_2_out.e +0 -0
  33. pyvale/data/case18_3_out.e +0 -0
  34. pyvale/data/case25_out.e +0 -0
  35. pyvale/data/case26_out.e +0 -0
  36. pyvale/data/optspeckle_2464x2056px_spec5px_8bit_gblur1px.tiff +0 -0
  37. pyvale/dataset.py +325 -0
  38. pyvale/errorcalculator.py +109 -0
  39. pyvale/errordriftcalc.py +146 -0
  40. pyvale/errorintegrator.py +336 -0
  41. pyvale/errorrand.py +607 -0
  42. pyvale/errorsyscalib.py +134 -0
  43. pyvale/errorsysdep.py +327 -0
  44. pyvale/errorsysfield.py +414 -0
  45. pyvale/errorsysindep.py +808 -0
  46. pyvale/examples/__init__.py +5 -0
  47. pyvale/examples/basics/ex1_1_basicscalars_therm2d.py +131 -0
  48. pyvale/examples/basics/ex1_2_sensormodel_therm2d.py +158 -0
  49. pyvale/examples/basics/ex1_3_customsens_therm3d.py +216 -0
  50. pyvale/examples/basics/ex1_4_basicerrors_therm3d.py +153 -0
  51. pyvale/examples/basics/ex1_5_fielderrs_therm3d.py +168 -0
  52. pyvale/examples/basics/ex1_6_caliberrs_therm2d.py +133 -0
  53. pyvale/examples/basics/ex1_7_spatavg_therm2d.py +123 -0
  54. pyvale/examples/basics/ex2_1_basicvectors_disp2d.py +112 -0
  55. pyvale/examples/basics/ex2_2_vectorsens_disp2d.py +111 -0
  56. pyvale/examples/basics/ex2_3_sensangle_disp2d.py +139 -0
  57. pyvale/examples/basics/ex2_4_chainfielderrs_disp2d.py +196 -0
  58. pyvale/examples/basics/ex2_5_vectorfields3d_disp3d.py +109 -0
  59. pyvale/examples/basics/ex3_1_basictensors_strain2d.py +114 -0
  60. pyvale/examples/basics/ex3_2_tensorsens2d_strain2d.py +111 -0
  61. pyvale/examples/basics/ex3_3_tensorsens3d_strain3d.py +182 -0
  62. pyvale/examples/basics/ex4_1_expsim2d_thermmech2d.py +171 -0
  63. pyvale/examples/basics/ex4_2_expsim3d_thermmech3d.py +252 -0
  64. pyvale/examples/genanalyticdata/ex1_1_scalarvisualisation.py +35 -0
  65. pyvale/examples/genanalyticdata/ex1_2_scalarcasebuild.py +43 -0
  66. pyvale/examples/genanalyticdata/ex2_1_analyticsensors.py +80 -0
  67. pyvale/examples/imagedef2d/ex_imagedef2d_todisk.py +79 -0
  68. pyvale/examples/renderblender/ex1_1_blenderscene.py +121 -0
  69. pyvale/examples/renderblender/ex1_2_blenderdeformed.py +119 -0
  70. pyvale/examples/renderblender/ex2_1_stereoscene.py +128 -0
  71. pyvale/examples/renderblender/ex2_2_stereodeformed.py +131 -0
  72. pyvale/examples/renderblender/ex3_1_blendercalibration.py +120 -0
  73. pyvale/examples/renderrasterisation/ex_rastenp.py +153 -0
  74. pyvale/examples/renderrasterisation/ex_rastercyth_oneframe.py +218 -0
  75. pyvale/examples/renderrasterisation/ex_rastercyth_static_cypara.py +187 -0
  76. pyvale/examples/renderrasterisation/ex_rastercyth_static_pypara.py +190 -0
  77. pyvale/examples/visualisation/ex1_1_plot_traces.py +102 -0
  78. pyvale/examples/visualisation/ex2_1_animate_sim.py +89 -0
  79. pyvale/experimentsimulator.py +175 -0
  80. pyvale/field.py +128 -0
  81. pyvale/fieldconverter.py +351 -0
  82. pyvale/fieldsampler.py +111 -0
  83. pyvale/fieldscalar.py +166 -0
  84. pyvale/fieldtensor.py +218 -0
  85. pyvale/fieldtransform.py +388 -0
  86. pyvale/fieldvector.py +213 -0
  87. pyvale/generatorsrandom.py +505 -0
  88. pyvale/imagedef2d.py +569 -0
  89. pyvale/integratorfactory.py +240 -0
  90. pyvale/integratorquadrature.py +217 -0
  91. pyvale/integratorrectangle.py +165 -0
  92. pyvale/integratorspatial.py +89 -0
  93. pyvale/integratortype.py +43 -0
  94. pyvale/output.py +17 -0
  95. pyvale/pyvaleexceptions.py +11 -0
  96. pyvale/raster.py +31 -0
  97. pyvale/rastercy.py +77 -0
  98. pyvale/rasternp.py +603 -0
  99. pyvale/rendermesh.py +147 -0
  100. pyvale/sensorarray.py +178 -0
  101. pyvale/sensorarrayfactory.py +196 -0
  102. pyvale/sensorarraypoint.py +278 -0
  103. pyvale/sensordata.py +71 -0
  104. pyvale/sensordescriptor.py +213 -0
  105. pyvale/sensortools.py +142 -0
  106. pyvale/simcases/case00_HEX20.i +242 -0
  107. pyvale/simcases/case00_HEX27.i +242 -0
  108. pyvale/simcases/case00_HEX8.i +242 -0
  109. pyvale/simcases/case00_TET10.i +242 -0
  110. pyvale/simcases/case00_TET14.i +242 -0
  111. pyvale/simcases/case00_TET4.i +242 -0
  112. pyvale/simcases/case01.i +101 -0
  113. pyvale/simcases/case02.i +156 -0
  114. pyvale/simcases/case03.i +136 -0
  115. pyvale/simcases/case04.i +181 -0
  116. pyvale/simcases/case05.i +234 -0
  117. pyvale/simcases/case06.i +305 -0
  118. pyvale/simcases/case07.geo +135 -0
  119. pyvale/simcases/case07.i +87 -0
  120. pyvale/simcases/case08.geo +144 -0
  121. pyvale/simcases/case08.i +153 -0
  122. pyvale/simcases/case09.geo +204 -0
  123. pyvale/simcases/case09.i +87 -0
  124. pyvale/simcases/case10.geo +204 -0
  125. pyvale/simcases/case10.i +257 -0
  126. pyvale/simcases/case11.geo +337 -0
  127. pyvale/simcases/case11.i +147 -0
  128. pyvale/simcases/case12.geo +388 -0
  129. pyvale/simcases/case12.i +329 -0
  130. pyvale/simcases/case13.i +140 -0
  131. pyvale/simcases/case14.i +159 -0
  132. pyvale/simcases/case15.geo +337 -0
  133. pyvale/simcases/case15.i +150 -0
  134. pyvale/simcases/case16.geo +391 -0
  135. pyvale/simcases/case16.i +357 -0
  136. pyvale/simcases/case17.geo +135 -0
  137. pyvale/simcases/case17.i +144 -0
  138. pyvale/simcases/case18.i +254 -0
  139. pyvale/simcases/case18_1.i +254 -0
  140. pyvale/simcases/case18_2.i +254 -0
  141. pyvale/simcases/case18_3.i +254 -0
  142. pyvale/simcases/case19.geo +252 -0
  143. pyvale/simcases/case19.i +99 -0
  144. pyvale/simcases/case20.geo +252 -0
  145. pyvale/simcases/case20.i +250 -0
  146. pyvale/simcases/case21.geo +74 -0
  147. pyvale/simcases/case21.i +155 -0
  148. pyvale/simcases/case22.geo +82 -0
  149. pyvale/simcases/case22.i +140 -0
  150. pyvale/simcases/case23.geo +164 -0
  151. pyvale/simcases/case23.i +140 -0
  152. pyvale/simcases/case24.geo +79 -0
  153. pyvale/simcases/case24.i +123 -0
  154. pyvale/simcases/case25.geo +82 -0
  155. pyvale/simcases/case25.i +140 -0
  156. pyvale/simcases/case26.geo +166 -0
  157. pyvale/simcases/case26.i +140 -0
  158. pyvale/simcases/run_1case.py +61 -0
  159. pyvale/simcases/run_all_cases.py +69 -0
  160. pyvale/simcases/run_build_case.py +64 -0
  161. pyvale/simcases/run_example_cases.py +69 -0
  162. pyvale/simtools.py +67 -0
  163. pyvale/visualexpplotter.py +191 -0
  164. pyvale/visualimagedef.py +74 -0
  165. pyvale/visualimages.py +76 -0
  166. pyvale/visualopts.py +493 -0
  167. pyvale/visualsimanimator.py +111 -0
  168. pyvale/visualsimsensors.py +318 -0
  169. pyvale/visualtools.py +136 -0
  170. pyvale/visualtraceplotter.py +142 -0
  171. pyvale-2025.5.3.dist-info/METADATA +144 -0
  172. pyvale-2025.5.3.dist-info/RECORD +175 -0
  173. pyvale-2025.5.3.dist-info/WHEEL +6 -0
  174. pyvale-2025.5.3.dist-info/licenses/LICENSE +21 -0
  175. pyvale-2025.5.3.dist-info/top_level.txt +1 -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()