pyvale 2025.4.1__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.

Files changed (126) hide show
  1. pyvale/__init__.py +18 -3
  2. pyvale/analyticmeshgen.py +1 -0
  3. pyvale/analyticsimdatafactory.py +18 -13
  4. pyvale/analyticsimdatagenerator.py +105 -72
  5. pyvale/blendercalibrationdata.py +15 -0
  6. pyvale/blenderlightdata.py +26 -0
  7. pyvale/blendermaterialdata.py +15 -0
  8. pyvale/blenderrenderdata.py +30 -0
  9. pyvale/blenderscene.py +488 -0
  10. pyvale/blendertools.py +420 -0
  11. pyvale/camera.py +6 -5
  12. pyvale/cameradata.py +25 -7
  13. pyvale/cameradata2d.py +6 -4
  14. pyvale/camerastereo.py +217 -0
  15. pyvale/cameratools.py +206 -11
  16. pyvale/cython/rastercyth.py +6 -2
  17. pyvale/data/cal_target.tiff +0 -0
  18. pyvale/dataset.py +73 -14
  19. pyvale/errorcalculator.py +8 -10
  20. pyvale/errordriftcalc.py +10 -9
  21. pyvale/errorintegrator.py +19 -21
  22. pyvale/errorrand.py +33 -39
  23. pyvale/errorsyscalib.py +134 -0
  24. pyvale/errorsysdep.py +19 -22
  25. pyvale/errorsysfield.py +49 -41
  26. pyvale/errorsysindep.py +79 -175
  27. pyvale/examples/basics/ex1_1_basicscalars_therm2d.py +131 -0
  28. pyvale/examples/basics/ex1_2_sensormodel_therm2d.py +158 -0
  29. pyvale/examples/basics/ex1_3_customsens_therm3d.py +216 -0
  30. pyvale/examples/basics/ex1_4_basicerrors_therm3d.py +153 -0
  31. pyvale/examples/basics/ex1_5_fielderrs_therm3d.py +168 -0
  32. pyvale/examples/basics/ex1_6_caliberrs_therm2d.py +133 -0
  33. pyvale/examples/basics/ex1_7_spatavg_therm2d.py +123 -0
  34. pyvale/examples/basics/ex2_1_basicvectors_disp2d.py +112 -0
  35. pyvale/examples/basics/ex2_2_vectorsens_disp2d.py +111 -0
  36. pyvale/examples/basics/ex2_3_sensangle_disp2d.py +139 -0
  37. pyvale/examples/basics/ex2_4_chainfielderrs_disp2d.py +196 -0
  38. pyvale/examples/basics/ex2_5_vectorfields3d_disp3d.py +109 -0
  39. pyvale/examples/basics/ex3_1_basictensors_strain2d.py +114 -0
  40. pyvale/examples/basics/ex3_2_tensorsens2d_strain2d.py +111 -0
  41. pyvale/examples/basics/ex3_3_tensorsens3d_strain3d.py +182 -0
  42. pyvale/examples/basics/ex4_1_expsim2d_thermmech2d.py +171 -0
  43. pyvale/examples/basics/ex4_2_expsim3d_thermmech3d.py +252 -0
  44. pyvale/examples/{analyticdatagen → genanalyticdata}/ex1_1_scalarvisualisation.py +6 -9
  45. pyvale/examples/{analyticdatagen → genanalyticdata}/ex1_2_scalarcasebuild.py +8 -11
  46. pyvale/examples/{analyticdatagen → genanalyticdata}/ex2_1_analyticsensors.py +9 -12
  47. pyvale/examples/imagedef2d/ex_imagedef2d_todisk.py +8 -15
  48. pyvale/examples/renderblender/ex1_1_blenderscene.py +121 -0
  49. pyvale/examples/renderblender/ex1_2_blenderdeformed.py +119 -0
  50. pyvale/examples/renderblender/ex2_1_stereoscene.py +128 -0
  51. pyvale/examples/renderblender/ex2_2_stereodeformed.py +131 -0
  52. pyvale/examples/renderblender/ex3_1_blendercalibration.py +120 -0
  53. pyvale/examples/{rasterisation → renderrasterisation}/ex_rastenp.py +3 -2
  54. pyvale/examples/{rasterisation → renderrasterisation}/ex_rastercyth_oneframe.py +2 -2
  55. pyvale/examples/{rasterisation → renderrasterisation}/ex_rastercyth_static_cypara.py +3 -8
  56. pyvale/examples/{rasterisation → renderrasterisation}/ex_rastercyth_static_pypara.py +6 -7
  57. pyvale/examples/{ex1_4_thermal2d.py → visualisation/ex1_1_plot_traces.py} +32 -16
  58. pyvale/examples/{features/ex_animation_tools_3dmonoblock.py → visualisation/ex2_1_animate_sim.py} +37 -31
  59. pyvale/experimentsimulator.py +107 -30
  60. pyvale/field.py +2 -9
  61. pyvale/fieldconverter.py +98 -22
  62. pyvale/fieldsampler.py +2 -2
  63. pyvale/fieldscalar.py +10 -10
  64. pyvale/fieldtensor.py +15 -17
  65. pyvale/fieldtransform.py +7 -2
  66. pyvale/fieldvector.py +6 -7
  67. pyvale/generatorsrandom.py +25 -47
  68. pyvale/imagedef2d.py +6 -2
  69. pyvale/integratorfactory.py +2 -2
  70. pyvale/integratorquadrature.py +50 -24
  71. pyvale/integratorrectangle.py +85 -7
  72. pyvale/integratorspatial.py +4 -4
  73. pyvale/integratortype.py +3 -3
  74. pyvale/output.py +17 -0
  75. pyvale/pyvaleexceptions.py +11 -0
  76. pyvale/raster.py +6 -5
  77. pyvale/rastercy.py +6 -4
  78. pyvale/rasternp.py +6 -4
  79. pyvale/rendermesh.py +6 -2
  80. pyvale/sensorarray.py +2 -2
  81. pyvale/sensorarrayfactory.py +52 -65
  82. pyvale/sensorarraypoint.py +29 -30
  83. pyvale/sensordata.py +2 -2
  84. pyvale/sensordescriptor.py +138 -25
  85. pyvale/sensortools.py +3 -3
  86. pyvale/simtools.py +67 -0
  87. pyvale/visualexpplotter.py +99 -57
  88. pyvale/visualimagedef.py +11 -7
  89. pyvale/visualimages.py +6 -4
  90. pyvale/visualopts.py +372 -58
  91. pyvale/visualsimanimator.py +42 -13
  92. pyvale/visualsimsensors.py +318 -0
  93. pyvale/visualtools.py +69 -13
  94. pyvale/visualtraceplotter.py +52 -165
  95. {pyvale-2025.4.1.dist-info → pyvale-2025.5.1.dist-info}/METADATA +17 -14
  96. pyvale-2025.5.1.dist-info/RECORD +172 -0
  97. {pyvale-2025.4.1.dist-info → pyvale-2025.5.1.dist-info}/WHEEL +1 -1
  98. pyvale/examples/analyticdatagen/__init__.py +0 -5
  99. pyvale/examples/ex1_1_thermal2d.py +0 -86
  100. pyvale/examples/ex1_2_thermal2d.py +0 -108
  101. pyvale/examples/ex1_3_thermal2d.py +0 -110
  102. pyvale/examples/ex1_5_thermal2d.py +0 -102
  103. pyvale/examples/ex2_1_thermal3d .py +0 -84
  104. pyvale/examples/ex2_2_thermal3d.py +0 -51
  105. pyvale/examples/ex2_3_thermal3d.py +0 -106
  106. pyvale/examples/ex3_1_displacement2d.py +0 -44
  107. pyvale/examples/ex3_2_displacement2d.py +0 -76
  108. pyvale/examples/ex3_3_displacement2d.py +0 -101
  109. pyvale/examples/ex3_4_displacement2d.py +0 -102
  110. pyvale/examples/ex4_1_strain2d.py +0 -54
  111. pyvale/examples/ex4_2_strain2d.py +0 -76
  112. pyvale/examples/ex4_3_strain2d.py +0 -97
  113. pyvale/examples/ex5_1_multiphysics2d.py +0 -75
  114. pyvale/examples/ex6_1_multiphysics2d_expsim.py +0 -115
  115. pyvale/examples/ex6_2_multiphysics3d_expsim.py +0 -160
  116. pyvale/examples/features/__init__.py +0 -5
  117. pyvale/examples/features/ex_area_avg.py +0 -89
  118. pyvale/examples/features/ex_calibration_error.py +0 -108
  119. pyvale/examples/features/ex_chain_field_errs.py +0 -141
  120. pyvale/examples/features/ex_field_errs.py +0 -78
  121. pyvale/examples/features/ex_sensor_single_angle_batch.py +0 -110
  122. pyvale/optimcheckfuncs.py +0 -153
  123. pyvale/visualsimplotter.py +0 -182
  124. pyvale-2025.4.1.dist-info/RECORD +0 -163
  125. {pyvale-2025.4.1.dist-info → pyvale-2025.5.1.dist-info}/licenses/LICENSE +0 -0
  126. {pyvale-2025.4.1.dist-info → pyvale-2025.5.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,139 @@
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 angles for vector fields
9
+ --------------------------------------------------------------------------------
10
+ In this example we demonstrate how to setup vector field sensors at custom
11
+ orientations with respect to the simulation coordinate system. We first build a
12
+ sensor array aligned with the simulation coords in the same way as the previous
13
+ example. We then build a sensor array with the sensors rotated and compare this
14
+ to the case with no rotation.
15
+
16
+ Note that this tutorial assumes you are familiar with the use of pyvale for
17
+ scalar fields as described in the first set of examples.
18
+
19
+ Test case: point displacement sensors on a 2D plate with hole loaded in tension
20
+ """
21
+
22
+ import numpy as np
23
+ import matplotlib.pyplot as plt
24
+ from scipy.spatial.transform import Rotation
25
+ import mooseherder as mh
26
+ import pyvale as pyv
27
+
28
+ def main() -> None:
29
+ # First we are going to setup the same displacement sensor array on the 2D
30
+ # solid mechanics test case we have used previously. This will serve as a
31
+ # baseline with no sensor rotation.
32
+
33
+ data_path = pyv.DataSet.mechanical_2d_path()
34
+ sim_data = mh.ExodusReader(data_path).read_all_sim_data()
35
+
36
+ field_name = "disp"
37
+ field_comps = ("disp_x","disp_y")
38
+ sim_data = pyv.scale_length_units(scale=1000.0,
39
+ sim_data=sim_data,
40
+ disp_comps=field_comps)
41
+
42
+ descriptor = pyv.SensorDescriptorFactory.displacement_descriptor()
43
+
44
+ disp_field = pyv.FieldVector(sim_data,field_name,field_comps,elem_dims=2)
45
+
46
+ n_sens = (2,3,1)
47
+ x_lims = (0.0,100.0)
48
+ y_lims = (0.0,150.0)
49
+ z_lims = (0.0,0.0)
50
+ sens_pos = pyv.create_sensor_pos_array(n_sens,x_lims,y_lims,z_lims)
51
+
52
+
53
+ sample_times = np.linspace(0.0,np.max(sim_data.time),50)
54
+
55
+ sens_data_norot = pyv.SensorData(positions=sens_pos,
56
+ sample_times=sample_times)
57
+
58
+ disp_sens_norot = pyv.SensorArrayPoint(sens_data_norot,
59
+ disp_field,
60
+ descriptor)
61
+
62
+ disp_sens_norot.calc_measurements()
63
+
64
+ # To create our sensor array with rotated sensors we need to add a tuple of
65
+ # scipy rotation objects to our sensor data class. This tuple must be the
66
+ # same length as the number of sensors in the sensor array. Note that it is
67
+ # also possible to specify a single rotation in the tuple in this case all
68
+ # sensors are assumed to have the same rotation and they are batch processed
69
+ # to increase speed. Here we will define our rotations to all be the same
70
+ # rotation in degrees about the z axis which is the out of plane axis for
71
+ # our current test case.
72
+ sens_angles = sens_pos.shape[0] * \
73
+ (Rotation.from_euler("zyx", [45, 0, 0], degrees=True),)
74
+
75
+ # We could have also use a single element tuple to have all sensors have the
76
+ # angle and batch process them:
77
+ sens_angles = (Rotation.from_euler("zyx", [45, 0, 0], degrees=True),)
78
+
79
+
80
+ sens_data_rot = pyv.SensorData(positions=sens_pos,
81
+ sample_times=sample_times,
82
+ angles=sens_angles)
83
+
84
+ disp_sens_rot = pyv.SensorArrayPoint(sens_data_rot,
85
+ disp_field,
86
+ descriptor)
87
+
88
+ # We can also use a field error to add uncertainty to the sensors angle.
89
+ # We can apply a specific offset to each sensor or provide a random
90
+ # generator to perturb the sensors orientation. Note that the offset and
91
+ # the random generator should provide the perturbation in degrees.
92
+ angle_offset = np.zeros_like(sens_pos)
93
+ angle_offset[:,0] = 2.0 # only rotate about z in 2D
94
+ angle_rand = (pyv.GenNormal(std=2.0),None,None)
95
+ angle_error_data = pyv.ErrFieldData(ang_offset_zyx=angle_offset,
96
+ ang_rand_zyx=angle_rand)
97
+
98
+
99
+ sys_err_rot = pyv.ErrSysField(disp_field,angle_error_data)
100
+ sys_err_int = pyv.ErrIntegrator([sys_err_rot],
101
+ sens_data_rot,
102
+ disp_sens_rot.get_measurement_shape())
103
+ disp_sens_rot.set_error_integrator(sys_err_int)
104
+
105
+ measurements = disp_sens_rot.calc_measurements()
106
+
107
+
108
+ # We print some of the results for one of the sensors so we can see the
109
+ # effect of the field errors.
110
+ print(80*"-")
111
+
112
+ sens_print: int = 0
113
+ time_print: int = 5
114
+ comp_print: int = 0
115
+
116
+ print("ROTATED SENSORS WITH ANGLE ERRORS:")
117
+ print(f"These are the last {time_print} virtual measurements of sensor "
118
+ + f"{sens_print} for {field_comps[comp_print]}:")
119
+
120
+ pyv.print_measurements(sens_array=disp_sens_rot,
121
+ sensors=(sens_print,sens_print+1),
122
+ components=(comp_print,comp_print+1),
123
+ time_steps=(measurements.shape[2]-time_print,
124
+ measurements.shape[2]))
125
+ print(80*"-")
126
+
127
+ # We can now plot the traces for the non-rotated and rotated sensors to
128
+ # compare them:
129
+ for ff in field_comps:
130
+ (_,ax) = pyv.plot_time_traces(disp_sens_norot,ff)
131
+ ax.set_title("No Rotation")
132
+ (_,ax) = pyv.plot_time_traces(disp_sens_rot,ff)
133
+ ax.set_title("Rotated with Errors")
134
+
135
+ plt.show()
136
+
137
+
138
+ if __name__ == "__main__":
139
+ main()
@@ -0,0 +1,196 @@
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: Chaining field errors
9
+ ----------------------------------------------------------------------------
10
+ In this example we show how field errors can be chained together and accumulated
11
+ allowing for successive perturbations in postion, sampling time and orientation.
12
+ It is more computationally efficient to provide a single field error object as
13
+ this will perform all perturbations in a single step allowing for a single new
14
+ interpolation of the underlying physical field. However, in some cases it can
15
+ be useful to separate the sensor parameter perturbations to determine which is
16
+ contributing most to the total error.
17
+
18
+ Note that this tutorial assumes you are familiar with the use of pyvale for
19
+ scalar fields as described in the first set of examples.
20
+
21
+ Test case: point displacement sensors on a 2D plate with hole loaded in tension
22
+ """
23
+
24
+ import numpy as np
25
+ import matplotlib.pyplot as plt
26
+ import mooseherder as mh
27
+ import pyvale as pyv
28
+
29
+ def main() -> None:
30
+ # We start by building the same displacement sensor array applied to a 2D
31
+ # solid mechanics simulation that we have analysed previously.
32
+ data_path = pyv.DataSet.mechanical_2d_path()
33
+ sim_data = mh.ExodusReader(data_path).read_all_sim_data()
34
+ field_name = "disp"
35
+ field_comps = ("disp_x","disp_y")
36
+ sim_data = pyv.scale_length_units(scale=1000.0,
37
+ sim_data=sim_data,
38
+ disp_comps=field_comps)
39
+
40
+ descriptor = pyv.SensorDescriptorFactory.displacement_descriptor()
41
+
42
+ disp_field = pyv.FieldVector(sim_data,field_name,field_comps,elem_dims=2)
43
+
44
+ n_sens = (2,3,1)
45
+ x_lims = (0.0,100.0)
46
+ y_lims = (0.0,150.0)
47
+ z_lims = (0.0,0.0)
48
+ sensor_positions = pyv.create_sensor_pos_array(n_sens,x_lims,y_lims,z_lims)
49
+
50
+ sample_times = np.linspace(0.0,np.max(sim_data.time),50)
51
+
52
+ sensor_data = pyv.SensorData(positions=sensor_positions,
53
+ sample_times=sample_times)
54
+
55
+ disp_sens_array = pyv.SensorArrayPoint(sensor_data,
56
+ disp_field,
57
+ descriptor)
58
+
59
+
60
+ # Now we will build a series of field errors that cause succesive offsets in
61
+ # sensor sampling time, sensor position and sensor orientation. That way
62
+ # we should be able to analyse the sensor data object at each point in the
63
+ # error chain to see how the sensor parameters have accumulated.
64
+
65
+ # We will apply a position offset of -1.0mm in the x and y axes.
66
+ pos_offset = -1.0*np.ones_like(sensor_positions)
67
+ pos_offset[:,2] = 0.0 # in 2d we only have offset in x and y so zero z
68
+ pos_error_data = pyv.ErrFieldData(pos_offset_xyz=pos_offset)
69
+
70
+ # We will apply a rotation offset about the z axis of 1 degree
71
+ angle_offset = np.zeros_like(sensor_positions)
72
+ angle_offset[:,0] = 1.0 # only rotate about z in 2D
73
+ angle_error_data = pyv.ErrFieldData(ang_offset_zyx=angle_offset)
74
+
75
+ time_offset = 2.0*np.ones_like(disp_sens_array.get_sample_times())
76
+ time_error_data = pyv.ErrFieldData(time_offset=time_offset)
77
+
78
+ # Now we add all our field errors to our error chain. We add each error
79
+ # twice to see how they accumulate with each other. We also need to set the
80
+ # error dependence to `DEPENDENT` so that the sensor state is accumulated
81
+ # over the error chain as field errors are `INDEPENDENT` by default.
82
+ err_chain = []
83
+ err_chain.append(pyv.ErrSysField(disp_field,
84
+ time_error_data,
85
+ pyv.EErrDep.DEPENDENT))
86
+ err_chain.append(pyv.ErrSysField(disp_field,
87
+ time_error_data,
88
+ pyv.EErrDep.DEPENDENT))
89
+
90
+ err_chain.append(pyv.ErrSysField(disp_field,
91
+ pos_error_data,
92
+ pyv.EErrDep.DEPENDENT))
93
+ err_chain.append(pyv.ErrSysField(disp_field,
94
+ pos_error_data,
95
+ pyv.EErrDep.DEPENDENT))
96
+
97
+ err_chain.append(pyv.ErrSysField(disp_field,
98
+ angle_error_data,
99
+ pyv.EErrDep.DEPENDENT))
100
+ err_chain.append(pyv.ErrSysField(disp_field,
101
+ angle_error_data,
102
+ pyv.EErrDep.DEPENDENT))
103
+
104
+ # Instead of setting the dependence for each individual error above we could
105
+ # also just use our error integration options to force all errors to be
106
+ # `DEPENDENT`. We also set the error integration options to store the errors
107
+ # for each step in the error chain so we can analyse the sensor data at each
108
+ # step of chain. This option also allows us to separate the contribution of
109
+ # each error in the chain to the total error rather than just being able to
110
+ # analyse the total systematic and total random error which is the default.
111
+ # Note that this option will use more memory.
112
+ err_int_opts = pyv.ErrIntOpts(force_dependence=pyv.EErrDep.DEPENDENT,
113
+ store_all_errs=True)
114
+
115
+ # Now we build our error integrator, add it to our sensor array and then run
116
+ # our sensor simulation to obtain some virtual measurements.
117
+ error_int = pyv.ErrIntegrator(err_chain,
118
+ sensor_data,
119
+ disp_sens_array.get_measurement_shape(),
120
+ err_int_opts)
121
+ disp_sens_array.set_error_integrator(error_int)
122
+
123
+ measurements = disp_sens_array.calc_measurements()
124
+
125
+
126
+ # Here we will print to the console the time, position and angle of from the
127
+ # sensor data objects at each point in the error chain. We should see each
128
+ # sensor parameter perturbed and accumulated throughout the chain:
129
+ sens_data_by_chain = error_int.get_sens_data_by_chain()
130
+ if sens_data_by_chain is not None:
131
+ for ii,ss in enumerate(sens_data_by_chain):
132
+ print(80*"-")
133
+ if ss is not None:
134
+ print(f"SensorData @ [{ii}]")
135
+ print("TIME")
136
+ print(ss.sample_times)
137
+ print()
138
+ print("POSITIONS")
139
+ print(ss.positions)
140
+ print()
141
+ print("ANGLES")
142
+ for aa in ss.angles:
143
+ print(aa.as_euler("zyx",degrees=True))
144
+ print()
145
+ print(80*"-")
146
+
147
+ # Try setting all the field errors to be `INDEPENDENT` using the error
148
+ # integration options above. You should see that the sensor parameters are
149
+ # not accumulated throughout the error chain.
150
+
151
+ # Here we print the final sampling time, sensor positions and sensor angles
152
+ # at the end of error chain.
153
+ print()
154
+ print(80*"=")
155
+ sens_data_accumulated = error_int.get_sens_data_accumulated()
156
+ print("TIME")
157
+ print(sens_data_accumulated.sample_times)
158
+ print()
159
+ print("POSITIONS")
160
+ print(sens_data_accumulated.positions)
161
+ print()
162
+ print("ANGLES")
163
+ for aa in sens_data_accumulated.angles:
164
+ print(aa.as_euler("zyx",degrees=True))
165
+ print()
166
+ print(80*"=")
167
+
168
+ # We print the results for one of the sensors so we can see what the errors
169
+ # are for the last few sampling times.
170
+ print(80*"-")
171
+
172
+ sens_print: int = 0
173
+ time_print: int = 5
174
+ comp_print: int = 0
175
+
176
+ print("ROTATED SENSORS WITH ANGLE ERRORS:")
177
+ print(f"These are the last {time_print} virtual measurements of sensor "
178
+ + f"{sens_print} for {field_comps[comp_print]}:")
179
+
180
+ pyv.print_measurements(sens_array=disp_sens_array,
181
+ sensors=(sens_print,sens_print+1),
182
+ components=(comp_print,comp_print+1),
183
+ time_steps=(measurements.shape[2]-time_print,
184
+ measurements.shape[2]))
185
+ print(80*"-")
186
+
187
+
188
+ # Finally, we plot the time traces for all field components.
189
+ for ff in field_comps:
190
+ pyv.plot_time_traces(disp_sens_array,ff)
191
+
192
+ plt.show()
193
+
194
+
195
+ if __name__ == "__main__":
196
+ main()
@@ -0,0 +1,109 @@
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: 3D vector field sensors
9
+ --------------------------------------------------------------------------------
10
+ In all our previous examples we have looked at a 2D solid mechanics simulation
11
+ and applied displacement sensors to the vector field. Here we will build a
12
+ custom vector field sensor array on a 3D simulation of a small linear elastic
13
+ cube loaded in tension with the addition of an applied thermal gradient.
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: Simple 3D cube thermo-mechanical in tension with temp gradient.
19
+ """
20
+
21
+ import numpy as np
22
+ import matplotlib.pyplot as plt
23
+ import mooseherder as mh
24
+ import pyvale as pyv
25
+
26
+
27
+ def main() -> None:
28
+ # Frist we load our simulation as a `SimData` object. In this case we are
29
+ # loading a 10mm cube loaded in tension in the y direction with the addition
30
+ # of a thermal gradient in the y direction.
31
+ data_path = pyv.DataSet.element_case_path(pyv.EElemTest.HEX20)
32
+ sim_data = mh.ExodusReader(data_path).read_all_sim_data()
33
+
34
+ # As we are creating a 3D vector field sensor we now have a third
35
+ # displacement field component here.
36
+ field_name = "disp"
37
+ field_comps = ("disp_x","disp_y","disp_z")
38
+ sim_data = pyv.scale_length_units(scale=1000.0,
39
+ sim_data=sim_data,
40
+ disp_comps=field_comps)
41
+
42
+ # We use a helper function to print the extent of the dimensions in our
43
+ # `SimData` object to help us locate our sensors on the cube.
44
+ pyv.print_dimensions(sim_data)
45
+
46
+ descriptor = pyv.SensorDescriptorFactory.displacement_descriptor()
47
+
48
+ # We pass in the string keys for the three vector field components as they
49
+ # appear in our `SimData` object as well as specifying that our elements are
50
+ # 3 dimensional.
51
+ disp_field = pyv.FieldVector(sim_data,field_name,field_comps,elem_dims=3)
52
+
53
+ # Here we manually define our sensor positions to place a sensor on the
54
+ # centre of each face of our 10mm cube. From here everything is the same as
55
+ # for our 2D vector field sensor arrays.
56
+ sensor_positions = np.array(((5.0,0.0,5.0),
57
+ (5.0,10.0,5.0),
58
+ (5.0,5.0,0.0),
59
+ (5.0,5.0,10.0),
60
+ (0.0,5.0,5.0),
61
+ (10.0,5.0,5.0),))
62
+
63
+ sample_times = np.linspace(0.0,np.max(sim_data.time),50)
64
+
65
+ sensor_data = pyv.SensorData(positions=sensor_positions,
66
+ sample_times=sample_times)
67
+
68
+ disp_sens_array = pyv.SensorArrayPoint(sensor_data,
69
+ disp_field,
70
+ descriptor)
71
+
72
+ measurements = disp_sens_array.calc_measurements()
73
+
74
+ # Let's have a look at the y displacement field in relation to the location
75
+ # of our displacement sensors.
76
+ pv_plot = pyv.plot_point_sensors_on_sim(disp_sens_array,"disp_y")
77
+ pv_plot.show()
78
+
79
+ # We print the results for one of the sensors so we can see what the errors
80
+ # are for the last few sampling times.
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} for {field_comps[comp_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
+
98
+ # Finally, we plot the time traces for all field components noting that we
99
+ # expect the bottom of the cube to be fixed, the top of the cube to have the
100
+ # maximum y displacement, and that all sensors on the sides of the cube
101
+ # should give the same results.
102
+ for ff in field_comps:
103
+ pyv.plot_time_traces(disp_sens_array,ff)
104
+
105
+ plt.show()
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()
@@ -0,0 +1,114 @@
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 tensor field sensors (strain gauges)
9
+ --------------------------------------------------------------------------------
10
+ In this example we use the sensor array factory to build a set of strain
11
+ sensors that can sample the strain tensor field from a solid mechanics
12
+ simulation. In the next example we will examine how we can build custom tensor
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 strain sensors on a 2D plate with hole loaded in tension
19
+ """
20
+
21
+ from pathlib import Path
22
+ import numpy as np
23
+ import matplotlib.pyplot as plt
24
+ import mooseherder as mh
25
+ from mooseherder import SimData
26
+ import pyvale as pyv
27
+
28
+ def main() -> None:
29
+ # First we load the same 2D solid mechanics simulation we used previously
30
+ # for vector displacement fields. Most of this setup code is similar to our
31
+ # vector field examples except we will need to specify the string keys for
32
+ # the normal a deviatoric components of our tensor field (as they appear in
33
+ # our `SimData` object).
34
+ data_path: Path = pyv.DataSet.mechanical_2d_path()
35
+ sim_data: SimData = mh.ExodusReader(data_path).read_all_sim_data()
36
+ sim_data: SimData = pyv.scale_length_units(scale=1000.0,
37
+ sim_data=sim_data,
38
+ disp_comps=("disp_x","disp_y"))
39
+
40
+ n_sens = (2,3,1)
41
+ x_lims = (0.0,100.0)
42
+ y_lims = (0.0,150.0)
43
+ z_lims = (0.0,0.0)
44
+ sens_pos = pyv.create_sensor_pos_array(n_sens,x_lims,y_lims,z_lims)
45
+
46
+ sample_times = np.linspace(0.0,np.max(sim_data.time),50)
47
+
48
+ sens_data = pyv.SensorData(positions=sens_pos,
49
+ sample_times=sample_times)
50
+
51
+ # This is where we need to specify the string keys for the normal and
52
+ # deviatoric components of our strain field. In 2D we have two normal
53
+ # normal components and one deviatoric. In 3D we will have 3 of each as we
54
+ # will see in a later example. Otherwise this is very similar to what we
55
+ # have seen previously for scalar and vector fields.
56
+ norm_comps = ("strain_xx","strain_yy")
57
+ dev_comps = ("strain_xy",)
58
+ straingauge_array = pyv.SensorArrayFactory \
59
+ .strain_gauges_basic_errs(sim_data,
60
+ sens_data,
61
+ elem_dims=2,
62
+ field_name="strain",
63
+ norm_comps=norm_comps,
64
+ dev_comps=dev_comps,
65
+ errs_pc=5.0)
66
+
67
+ # We run our virtual sensor simulation as normal. The only thing to note is
68
+ # that the second dimension of our measurement array will contain our tensor
69
+ # components in the order they are specified in the tuples with the normal
70
+ # components first followed by the deviatoric. In our case this will be
71
+ # (strain_xx,strain_yy,strain_xy).
72
+ measurements = straingauge_array.calc_measurements()
73
+
74
+ # Here we print the shape of the measurement array so we can see that the
75
+ # second dimension contains both our tensor components. We also print some
76
+ # of the sensor measurements for the first tensor component.
77
+ print("\n"+80*"-")
78
+ print("For a virtual sensor: measurement = truth + sysematic error + random error")
79
+ print(f"measurements.shape = {measurements.shape} = "+
80
+ "(n_sensors,n_field_components,n_timesteps)\n")
81
+ print("The truth, systematic error and random error arrays have the same "+
82
+ "shape.")
83
+
84
+ print(80*"-")
85
+
86
+ sens_print: int = 0
87
+ time_print: int = 5
88
+ comp_print: int = 0
89
+
90
+ print(f"These are the last {time_print} virtual measurements of sensor "
91
+ + f"{sens_print}:")
92
+
93
+ pyv.print_measurements(sens_array=straingauge_array,
94
+ sensors=(sens_print,sens_print+1),
95
+ components=(comp_print,comp_print+1),
96
+ time_steps=(measurements.shape[2]-time_print,
97
+ measurements.shape[2]))
98
+ print(80*"-")
99
+
100
+ # We can plot a given component of our tensor field and display our sensor
101
+ # locations with respect to the field.
102
+ plot_field = "strain_yy"
103
+ pv_plot = pyv.plot_point_sensors_on_sim(straingauge_array,plot_field)
104
+ pv_plot.show(cpos="xy")
105
+
106
+ # We can also plot time traces for all components of the tensor field.
107
+ for cc in (norm_comps+dev_comps):
108
+ pyv.plot_time_traces(straingauge_array,cc)
109
+
110
+ plt.show()
111
+
112
+
113
+ if __name__ == "__main__":
114
+ 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 tensor field sensors (strain gauges) in 2D
9
+ --------------------------------------------------------------------------------
10
+ In this example we build a custom tensor field sensor array (i.e. a strain gauge
11
+ array) in 2D.
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 strain 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
+ # First we load the same 2D solid mechanics simulation of a plate with a
26
+ # hole loaded in tension as a `SimData` object. We scale the units to mm
27
+ # from SI including the coordinates and displacement. Strain is unitless so
28
+ # we leave it alone.
29
+ data_path = pyv.DataSet.mechanical_2d_path()
30
+ sim_data = mh.ExodusReader(data_path).read_all_sim_data()
31
+ sim_data = pyv.scale_length_units(scale=1000.0,
32
+ sim_data=sim_data,
33
+ disp_comps=("disp_x","disp_y"))
34
+
35
+
36
+ # Here is the main difference when creating a tensor field sensor array. We
37
+ # create a tensor field where we need to specify the normal and deviatoric
38
+ # component string keys as they appear in our `SimData` object. We have a 2d
39
+ # simulation here so we have 2 normal components and 1 deviatoric (shear).
40
+ field_name = "strain"
41
+ norm_comps = ("strain_xx","strain_yy")
42
+ dev_comps = ("strain_xy",)
43
+ strain_field = pyv.FieldTensor(sim_data,
44
+ field_name=field_name,
45
+ norm_comps=norm_comps,
46
+ dev_comps=dev_comps,
47
+ elem_dims=2)
48
+
49
+ # The setup of our sensor data object is exactly the same as for any other
50
+ # point sensor array. We could optionally specify the sample time to be None
51
+ # in which case the sensor will sample at the simulation time steps.
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
+ sample_times = np.linspace(0.0,np.max(sim_data.time),50)
59
+
60
+ sens_data = pyv.SensorData(positions=sens_pos,
61
+ sample_times=sample_times)
62
+
63
+ # Here we create a descriptor that will be used to label visualisations of
64
+ # the sensor locations and time traces for our sensors. For the strain
65
+ # gauges we are modelling here we could also use the descriptor factory to
66
+ # get these defaults.
67
+ descriptor = pyv.SensorDescriptor(name="Strain",
68
+ symbol=r"\varepsilon",
69
+ units=r"-",
70
+ tag="SG",
71
+ components=("xx","yy","xy"))
72
+
73
+ # We build our point sensor array as normal.
74
+ straingauge_array = pyv.SensorArrayPoint(sens_data,
75
+ strain_field,
76
+ descriptor)
77
+
78
+
79
+ # We can add any errors we like to our error chain. Here we add some basic
80
+ # percentage errors.
81
+ error_chain = []
82
+ error_chain.append(pyv.ErrSysUnifPercent(low_percent=-2.0,high_percent=2.0))
83
+ error_chain.append(pyv.ErrRandNormPercent(std_percent=2.0))
84
+ error_int = pyv.ErrIntegrator(error_chain,
85
+ sens_data,
86
+ straingauge_array.get_measurement_shape())
87
+ straingauge_array.set_error_integrator(error_int)
88
+
89
+ # We run our virtual sensor simulation as normal. The only thing to note is
90
+ # that the second dimension of our measurement array will contain our tensor
91
+ # components in the order they are specified in the tuples with the normal
92
+ # components first followed by the deviatoric. In our case this will be
93
+ # (strain_xx,strain_yy,strain_xy).
94
+ measurements = straingauge_array.calc_measurements()
95
+
96
+ # We can plot a given component of our tensor field and display our sensor
97
+ # locations with respect to the field.
98
+ plot_field = "strain_yy"
99
+ pv_plot = pyv.plot_point_sensors_on_sim(straingauge_array,plot_field)
100
+ pv_plot.show(cpos="xy")
101
+
102
+ # We can also plot time traces for all components of the tensor field.
103
+ for cc in (norm_comps+dev_comps):
104
+ pyv.plot_time_traces(straingauge_array,cc)
105
+
106
+ plt.show()
107
+
108
+
109
+
110
+ if __name__ == "__main__":
111
+ main()