pyvale 2025.4.1__py3-none-any.whl → 2025.5.2__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 +18 -3
- pyvale/analyticmeshgen.py +1 -0
- pyvale/analyticsimdatafactory.py +18 -13
- pyvale/analyticsimdatagenerator.py +105 -72
- pyvale/blendercalibrationdata.py +15 -0
- pyvale/blenderlightdata.py +26 -0
- pyvale/blendermaterialdata.py +15 -0
- pyvale/blenderrenderdata.py +30 -0
- pyvale/blenderscene.py +488 -0
- pyvale/blendertools.py +420 -0
- pyvale/camera.py +6 -5
- pyvale/cameradata.py +25 -7
- pyvale/cameradata2d.py +6 -4
- pyvale/camerastereo.py +217 -0
- pyvale/cameratools.py +206 -11
- pyvale/cython/rastercyth.py +6 -2
- pyvale/data/cal_target.tiff +0 -0
- pyvale/dataset.py +73 -14
- pyvale/errorcalculator.py +8 -10
- pyvale/errordriftcalc.py +10 -9
- pyvale/errorintegrator.py +19 -21
- pyvale/errorrand.py +33 -39
- pyvale/errorsyscalib.py +134 -0
- pyvale/errorsysdep.py +19 -22
- pyvale/errorsysfield.py +49 -41
- pyvale/errorsysindep.py +79 -175
- 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 +3 -2
- pyvale/examples/{rasterisation → renderrasterisation}/ex_rastercyth_oneframe.py +2 -2
- pyvale/examples/{rasterisation → renderrasterisation}/ex_rastercyth_static_cypara.py +3 -8
- pyvale/examples/{rasterisation → renderrasterisation}/ex_rastercyth_static_pypara.py +6 -7
- pyvale/examples/{ex1_4_thermal2d.py → visualisation/ex1_1_plot_traces.py} +32 -16
- pyvale/examples/{features/ex_animation_tools_3dmonoblock.py → visualisation/ex2_1_animate_sim.py} +37 -31
- pyvale/experimentsimulator.py +107 -30
- pyvale/field.py +2 -9
- pyvale/fieldconverter.py +98 -22
- pyvale/fieldsampler.py +2 -2
- pyvale/fieldscalar.py +10 -10
- pyvale/fieldtensor.py +15 -17
- pyvale/fieldtransform.py +7 -2
- pyvale/fieldvector.py +6 -7
- pyvale/generatorsrandom.py +25 -47
- pyvale/imagedef2d.py +6 -2
- pyvale/integratorfactory.py +2 -2
- pyvale/integratorquadrature.py +50 -24
- pyvale/integratorrectangle.py +85 -7
- pyvale/integratorspatial.py +4 -4
- pyvale/integratortype.py +3 -3
- pyvale/output.py +17 -0
- pyvale/pyvaleexceptions.py +11 -0
- pyvale/raster.py +6 -5
- pyvale/rastercy.py +6 -4
- pyvale/rasternp.py +6 -4
- pyvale/rendermesh.py +6 -2
- pyvale/sensorarray.py +2 -2
- pyvale/sensorarrayfactory.py +52 -65
- pyvale/sensorarraypoint.py +29 -30
- pyvale/sensordata.py +2 -2
- pyvale/sensordescriptor.py +138 -25
- pyvale/sensortools.py +3 -3
- pyvale/simtools.py +67 -0
- pyvale/visualexpplotter.py +99 -57
- pyvale/visualimagedef.py +11 -7
- pyvale/visualimages.py +6 -4
- pyvale/visualopts.py +372 -58
- pyvale/visualsimanimator.py +42 -13
- pyvale/visualsimsensors.py +318 -0
- pyvale/visualtools.py +69 -13
- pyvale/visualtraceplotter.py +52 -165
- {pyvale-2025.4.1.dist-info → pyvale-2025.5.2.dist-info}/METADATA +17 -14
- pyvale-2025.5.2.dist-info/RECORD +172 -0
- {pyvale-2025.4.1.dist-info → pyvale-2025.5.2.dist-info}/WHEEL +1 -1
- pyvale/examples/analyticdatagen/__init__.py +0 -5
- pyvale/examples/ex1_1_thermal2d.py +0 -86
- pyvale/examples/ex1_2_thermal2d.py +0 -108
- pyvale/examples/ex1_3_thermal2d.py +0 -110
- pyvale/examples/ex1_5_thermal2d.py +0 -102
- pyvale/examples/ex2_1_thermal3d .py +0 -84
- pyvale/examples/ex2_2_thermal3d.py +0 -51
- pyvale/examples/ex2_3_thermal3d.py +0 -106
- pyvale/examples/ex3_1_displacement2d.py +0 -44
- pyvale/examples/ex3_2_displacement2d.py +0 -76
- pyvale/examples/ex3_3_displacement2d.py +0 -101
- pyvale/examples/ex3_4_displacement2d.py +0 -102
- pyvale/examples/ex4_1_strain2d.py +0 -54
- pyvale/examples/ex4_2_strain2d.py +0 -76
- pyvale/examples/ex4_3_strain2d.py +0 -97
- pyvale/examples/ex5_1_multiphysics2d.py +0 -75
- pyvale/examples/ex6_1_multiphysics2d_expsim.py +0 -115
- pyvale/examples/ex6_2_multiphysics3d_expsim.py +0 -160
- pyvale/examples/features/__init__.py +0 -5
- 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/optimcheckfuncs.py +0 -153
- pyvale/visualsimplotter.py +0 -182
- pyvale-2025.4.1.dist-info/RECORD +0 -163
- {pyvale-2025.4.1.dist-info → pyvale-2025.5.2.dist-info}/licenses/LICENSE +0 -0
- {pyvale-2025.4.1.dist-info → pyvale-2025.5.2.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,182 @@
|
|
|
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 3D
|
|
9
|
+
--------------------------------------------------------------------------------
|
|
10
|
+
In this example we build a custom tensor field sensor array (i.e. a strain gauge
|
|
11
|
+
array) in 3D. We will also demonstrate how to specify sensor angles and field
|
|
12
|
+
errors based on sensor angles.
|
|
13
|
+
|
|
14
|
+
Note that this tutorial assumes you are familiar with the use of pyvale for
|
|
15
|
+
scalar fields as described in the first set of examples.
|
|
16
|
+
|
|
17
|
+
Test case: point strain sensors on a 2D plate with hole loaded in tension
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
import matplotlib.pyplot as plt
|
|
22
|
+
from scipy.spatial.transform import Rotation
|
|
23
|
+
import mooseherder as mh
|
|
24
|
+
import pyvale as pyv
|
|
25
|
+
|
|
26
|
+
def main() -> None:
|
|
27
|
+
|
|
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 tensor field sensor we now have a third
|
|
35
|
+
# displacement field component here for scaling. Note that you don't need to
|
|
36
|
+
# scale the displacements here if you only want to analyse strains.
|
|
37
|
+
disp_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=disp_comps)
|
|
41
|
+
|
|
42
|
+
# Here is the main difference when creating a tensor field sensor array. We
|
|
43
|
+
# create a tensor field where we need to specify the normal and deviatoric
|
|
44
|
+
# component string keys as they appear in our `SimData` object. We have a 3D
|
|
45
|
+
# simulation here so we have 3 normal components and 3 deviatoric (shear).
|
|
46
|
+
field_name = "strain"
|
|
47
|
+
norm_comps = ("strain_xx","strain_yy","strain_zz")
|
|
48
|
+
dev_comps = ("strain_xy","strain_yz","strain_xz")
|
|
49
|
+
strain_field = pyv.FieldTensor(sim_data,
|
|
50
|
+
field_name=field_name,
|
|
51
|
+
norm_comps=norm_comps,
|
|
52
|
+
dev_comps=dev_comps,
|
|
53
|
+
elem_dims=3)
|
|
54
|
+
|
|
55
|
+
# Here we manually define our sensor positions to place a sensor on the
|
|
56
|
+
# centre of each face of our 10mm cube. From here everything is the same as
|
|
57
|
+
# for our 2D vector field sensor arrays.
|
|
58
|
+
sensor_positions = np.array(((5.0,0.0,5.0), # bottom
|
|
59
|
+
(5.0,10.0,5.0), # top
|
|
60
|
+
(5.0,5.0,0.0), # xy face
|
|
61
|
+
(5.0,5.0,10.0), # xy face
|
|
62
|
+
(0.0,5.0,5.0), # yz face
|
|
63
|
+
(10.0,5.0,5.0),)) # yz face
|
|
64
|
+
|
|
65
|
+
# We set custom sensor sampling times here but we could also set this to
|
|
66
|
+
# None to have the sensors sample at the simulation time steps.
|
|
67
|
+
sample_times = np.linspace(0.0,np.max(sim_data.time),50)
|
|
68
|
+
|
|
69
|
+
# We are going to manually specify the sensor angles for all our sensors.
|
|
70
|
+
sens_angles = (Rotation.from_euler("zyx", [0, 0, 0], degrees=True),
|
|
71
|
+
Rotation.from_euler("zyx", [0, 0, 0], degrees=True),
|
|
72
|
+
Rotation.from_euler("zyx", [45, 0, 0], degrees=True),
|
|
73
|
+
Rotation.from_euler("zyx", [45, 0, 0], degrees=True),
|
|
74
|
+
Rotation.from_euler("zyx", [0, 0, 45], degrees=True),
|
|
75
|
+
Rotation.from_euler("zyx", [0, 0, 45], degrees=True),)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
sens_data = pyv.SensorData(positions=sensor_positions,
|
|
79
|
+
sample_times=sample_times,
|
|
80
|
+
angles=sens_angles)
|
|
81
|
+
|
|
82
|
+
# Here we create a descriptor that will be used to label visualisations of
|
|
83
|
+
# the sensor locations and time traces for our sensors. For the strain
|
|
84
|
+
# gauges we are modelling here we could also use the descriptor factory to
|
|
85
|
+
# get these defaults.
|
|
86
|
+
descriptor = pyv.SensorDescriptor(name="Strain",
|
|
87
|
+
symbol=r"\varepsilon",
|
|
88
|
+
units=r"-",
|
|
89
|
+
tag="SG",
|
|
90
|
+
components=('xx','yy','zz','xy','yz','xz'))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
straingauge_array = pyv.SensorArrayPoint(sens_data,
|
|
94
|
+
strain_field,
|
|
95
|
+
descriptor)
|
|
96
|
+
|
|
97
|
+
# We can add any errors we like to our error chain. Here we add some basic
|
|
98
|
+
# percentage errors.
|
|
99
|
+
error_chain = []
|
|
100
|
+
error_chain.append(pyv.ErrSysUnif(low=-0.1e-3,high=0.1e-3))
|
|
101
|
+
error_chain.append(pyv.ErrRandNormPercent(std_percent=1.0))
|
|
102
|
+
|
|
103
|
+
# Now we add a field error to perturb the positions of each sensor on its
|
|
104
|
+
# relevant face and then add a +/- 2deg angle error.
|
|
105
|
+
|
|
106
|
+
pos_uncert = 0.1 # units = mm
|
|
107
|
+
pos_rand_xyz = (pyv.GenNormal(std=pos_uncert),
|
|
108
|
+
pyv.GenNormal(std=pos_uncert),
|
|
109
|
+
pyv.GenNormal(std=pos_uncert))
|
|
110
|
+
|
|
111
|
+
angle_uncert = 2.0
|
|
112
|
+
angle_rand_zyx = (pyv.GenUniform(low=-angle_uncert,high=angle_uncert), # units = deg
|
|
113
|
+
pyv.GenUniform(low=-angle_uncert,high=angle_uncert),
|
|
114
|
+
pyv.GenUniform(low=-angle_uncert,high=angle_uncert))
|
|
115
|
+
|
|
116
|
+
# We are going to lock position perturbation so that the sensors stay on the
|
|
117
|
+
# faces of the cube they are positioned on.
|
|
118
|
+
pos_lock = np.full(sensor_positions.shape,False,dtype=bool)
|
|
119
|
+
pos_lock[0:2,1] = True # Block translation in y
|
|
120
|
+
pos_lock[2:4,2] = True # Block translation in z
|
|
121
|
+
pos_lock[4:6,0] = True # Block translation in x
|
|
122
|
+
|
|
123
|
+
# We are also going to lock angular perturbation so that each sensor is only
|
|
124
|
+
# allowed to rotate on the plane it is on.
|
|
125
|
+
angle_lock = np.full(sensor_positions.shape,True,dtype=bool)
|
|
126
|
+
angle_lock[0:2,1] = False # Allow rotation about y
|
|
127
|
+
angle_lock[2:4,0] = False # Allow rotation about z
|
|
128
|
+
angle_lock[4:6,2] = False # Alloq rotation about x
|
|
129
|
+
|
|
130
|
+
field_error_data = pyv.ErrFieldData(pos_rand_xyz=pos_rand_xyz,
|
|
131
|
+
pos_lock_xyz=pos_lock,
|
|
132
|
+
ang_rand_zyx=angle_rand_zyx,
|
|
133
|
+
ang_lock_zyx=angle_lock)
|
|
134
|
+
sys_err_field = pyv.ErrSysField(strain_field,field_error_data)
|
|
135
|
+
error_chain.append(sys_err_field)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
error_int = pyv.ErrIntegrator(error_chain,
|
|
139
|
+
sens_data,
|
|
140
|
+
straingauge_array.get_measurement_shape())
|
|
141
|
+
straingauge_array.set_error_integrator(error_int)
|
|
142
|
+
|
|
143
|
+
# We run our virtual sensor simulation as normal. The only thing to note is
|
|
144
|
+
# that the second dimension of our measurement array will contain our tensor
|
|
145
|
+
# components in the order they are specified in the tuples with the normal
|
|
146
|
+
# components first followed by the deviatoric.
|
|
147
|
+
measurements = straingauge_array.calc_measurements()
|
|
148
|
+
|
|
149
|
+
# We print some of the results for one of the sensors so we can see the
|
|
150
|
+
# effect of the field errors.
|
|
151
|
+
print(80*"-")
|
|
152
|
+
|
|
153
|
+
sens_print: int = 0
|
|
154
|
+
time_print: int = 5
|
|
155
|
+
comp_print: int = 1 # strain_yy based on order in tuple
|
|
156
|
+
|
|
157
|
+
print("ROTATED SENSORS WITH ANGLE ERRORS:")
|
|
158
|
+
print(f"These are the last {time_print} virtual measurements of sensor "
|
|
159
|
+
+ f"{sens_print} for {norm_comps[comp_print]}:")
|
|
160
|
+
|
|
161
|
+
pyv.print_measurements(sens_array=straingauge_array,
|
|
162
|
+
sensors=(sens_print,sens_print+1),
|
|
163
|
+
components=(comp_print,comp_print+1),
|
|
164
|
+
time_steps=(measurements.shape[2]-time_print,
|
|
165
|
+
measurements.shape[2]))
|
|
166
|
+
print(80*"-")
|
|
167
|
+
|
|
168
|
+
# We can plot a given component of our tensor field and display our sensor
|
|
169
|
+
# locations with respect to the field.
|
|
170
|
+
plot_field = "strain_yy"
|
|
171
|
+
pv_plot = pyv.plot_point_sensors_on_sim(straingauge_array,plot_field)
|
|
172
|
+
pv_plot.show(cpos="xy")
|
|
173
|
+
|
|
174
|
+
# We can also plot time traces for all components of the tensor field.
|
|
175
|
+
for cc in (norm_comps+dev_comps):
|
|
176
|
+
pyv.plot_time_traces(straingauge_array,cc)
|
|
177
|
+
|
|
178
|
+
plt.show()
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
if __name__ == "__main__":
|
|
182
|
+
main()
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# pyvale: the python validation engine
|
|
3
|
+
# License: MIT
|
|
4
|
+
# Copyright (C) 2025 The Computer Aided Validation Team
|
|
5
|
+
# ==============================================================================
|
|
6
|
+
|
|
7
|
+
"""Pyvale example: Multi-physics experiment simulation in 2D
|
|
8
|
+
--------------------------------------------------------------------------------
|
|
9
|
+
In previous examples we have built our virtual sensor array and used this to
|
|
10
|
+
run a single simulated experiment. However, we will generally want to run many
|
|
11
|
+
simulated experiments and perform statistical analysis on the results. In this
|
|
12
|
+
example we demonstrate how pyvale can be used to run a set of simulated
|
|
13
|
+
experiments with a series of sensor arrays one measuring temperature and the
|
|
14
|
+
other measuring strain. We also show how this analysis can be performed over a
|
|
15
|
+
set of input physics simulations.
|
|
16
|
+
|
|
17
|
+
Note that this tutorial assumes you are familiar with the use of pyvale for
|
|
18
|
+
scalar and tensor fields as described in the previous examples.
|
|
19
|
+
|
|
20
|
+
Test case: thermo-mechanical analysis of a 2D plate with a temperature gradient.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import numpy as np
|
|
24
|
+
import matplotlib.pyplot as plt
|
|
25
|
+
import mooseherder as mh
|
|
26
|
+
import pyvale as pyv
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def main() -> None:
|
|
30
|
+
# Here we get a list of paths to a set of 3 simulations in this case the
|
|
31
|
+
# simulation is a 2D plate with a heat flux on one edge and a heat transfer
|
|
32
|
+
# coefficient on the other. The mechanical deformation is a result of
|
|
33
|
+
# thermal expansion. The 3 simulation cases cover a nominal thermal
|
|
34
|
+
# and a perturbation of +/-10%.
|
|
35
|
+
data_paths = pyv.DataSet.thermomechanical_2d_experiment_paths()
|
|
36
|
+
elem_dims: int = 2
|
|
37
|
+
|
|
38
|
+
# We now loop over the paths and load each into a `SimData` object. We then
|
|
39
|
+
# scale our length units to mm and append the simulation to a list which we
|
|
40
|
+
# will use to perform our simulated experiments.
|
|
41
|
+
disp_comps = ("disp_x","disp_y")
|
|
42
|
+
sim_list = []
|
|
43
|
+
for pp in data_paths:
|
|
44
|
+
sim_data = mh.ExodusReader(pp).read_all_sim_data()
|
|
45
|
+
sim_data = pyv.scale_length_units(scale=1000.0,
|
|
46
|
+
sim_data=sim_data,
|
|
47
|
+
disp_comps=disp_comps)
|
|
48
|
+
sim_list.append(sim_data)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# We will use the same sampling times for both the thermal and strain
|
|
52
|
+
# sensor arrays as well as the same positions.
|
|
53
|
+
sample_times = np.linspace(0.0,np.max(sim_data.time),50)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# We place 4 thermal sensors along the mid line of the plate in the
|
|
57
|
+
# direction of the temperature gradient.
|
|
58
|
+
n_sens = (4,1,1)
|
|
59
|
+
x_lims = (0.0,100.0)
|
|
60
|
+
y_lims = (0.0,50.0)
|
|
61
|
+
z_lims = (0.0,0.0)
|
|
62
|
+
tc_sens_pos = pyv.create_sensor_pos_array(n_sens,x_lims,y_lims,z_lims)
|
|
63
|
+
|
|
64
|
+
tc_sens_data = pyv.SensorData(positions=tc_sens_pos,
|
|
65
|
+
sample_times=sample_times)
|
|
66
|
+
|
|
67
|
+
# We use the sensor array factory to give use thermocouples with basic 2%
|
|
68
|
+
# errors with uniform systematic error and normal random error. Note that
|
|
69
|
+
# we need to provide a `SimData` object to create our sensor array but when
|
|
70
|
+
# we run our experiment the field object that relies on this will switch the
|
|
71
|
+
# sim data for the required simulation in our list.
|
|
72
|
+
tc_field_name = "temperature"
|
|
73
|
+
tc_array = pyv.SensorArrayFactory \
|
|
74
|
+
.thermocouples_basic_errs(sim_list[0],
|
|
75
|
+
tc_sens_data,
|
|
76
|
+
elem_dims=elem_dims,
|
|
77
|
+
field_name=tc_field_name,
|
|
78
|
+
errs_pc=2.0)
|
|
79
|
+
|
|
80
|
+
# We place 3 strain gauges along the direction of the temperature gradient.
|
|
81
|
+
n_sens = (3,1,1)
|
|
82
|
+
sg_sens_pos = pyv.create_sensor_pos_array(n_sens,x_lims,y_lims,z_lims)
|
|
83
|
+
sg_sens_data = pyv.SensorData(positions=sg_sens_pos,
|
|
84
|
+
sample_times=sample_times)
|
|
85
|
+
|
|
86
|
+
# We use the factory to give us a basic strain gauge array as well.
|
|
87
|
+
sg_field_name = "strain"
|
|
88
|
+
sg_norm_comps = ("strain_xx","strain_yy")
|
|
89
|
+
sg_dev_comps = ("strain_xy",)
|
|
90
|
+
sg_array = pyv.SensorArrayFactory \
|
|
91
|
+
.strain_gauges_basic_errs(sim_list[0],
|
|
92
|
+
sg_sens_data,
|
|
93
|
+
elem_dims=elem_dims,
|
|
94
|
+
field_name=sg_field_name,
|
|
95
|
+
norm_comps=sg_norm_comps,
|
|
96
|
+
dev_comps=sg_dev_comps,
|
|
97
|
+
errs_pc=2.0)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
# Now we have our list of simulation and the two sensor arrays we want to
|
|
101
|
+
# apply to the simulations. We create a list of our two sensor arrays and
|
|
102
|
+
# use this to create an experiment simulator while specifying how many
|
|
103
|
+
# simulate experiments we want to run per simulation and sensor array.
|
|
104
|
+
sensor_arrays = [tc_array,sg_array]
|
|
105
|
+
exp_sim = pyv.ExperimentSimulator(sim_list,
|
|
106
|
+
sensor_arrays,
|
|
107
|
+
num_exp_per_sim=1000)
|
|
108
|
+
|
|
109
|
+
# We can now run our experiments for all our sensor arrays. We are returned
|
|
110
|
+
# a list of numpy arrays. The index in the list corresponds to the position
|
|
111
|
+
# of the sensor array in the list. So if we want our thermocouple results we
|
|
112
|
+
# want exp_data[0] and for our strain gauges exp_data[1]. The numpy array
|
|
113
|
+
# has the following shape:
|
|
114
|
+
# (n_sims,n_exps,n_sensors,n_field_comps,n_time_steps)
|
|
115
|
+
exp_data = exp_sim.run_experiments()
|
|
116
|
+
|
|
117
|
+
# We can also calculate summary statistics for each sensor array which is
|
|
118
|
+
# returned as a list where the position corresponds to the sensor array as
|
|
119
|
+
# in our experimental data. The experiment stats object contains numpy
|
|
120
|
+
# arrays for each statistic that is collapsed over the number of
|
|
121
|
+
# experiments. The statistics we can acces include: mean, standard deviation
|
|
122
|
+
# minimum, maximum, median, median absolute deviation and the 25% and 75%
|
|
123
|
+
# quartiles. See the `ExperimentStats` data class for details.
|
|
124
|
+
exp_stats = exp_sim.calc_stats()
|
|
125
|
+
|
|
126
|
+
# We will index into and print the shape of our exp_data and exp_stats
|
|
127
|
+
# lists to demonstrate how this works in practice:
|
|
128
|
+
print(80*"=")
|
|
129
|
+
print("exp_data and exp_stats are lists where the index is the sensor array")
|
|
130
|
+
print("position in the list as field components are not consistent dims.\n")
|
|
131
|
+
|
|
132
|
+
print(80*"-")
|
|
133
|
+
print("Thermal sensor array @ exp_data[0]")
|
|
134
|
+
print(80*"-")
|
|
135
|
+
print("shape=(n_sims,n_exps,n_sensors,n_field_comps,n_time_steps)")
|
|
136
|
+
print(f"{exp_data[0].shape=}")
|
|
137
|
+
print()
|
|
138
|
+
print("Stats are calculated over all experiments (axis=1)")
|
|
139
|
+
print("shape=(n_sims,n_sensors,n_field_comps,n_time_steps)")
|
|
140
|
+
print(f"{exp_stats[0].max.shape=}")
|
|
141
|
+
print()
|
|
142
|
+
print(80*"-")
|
|
143
|
+
print("Mechanical sensor array @ exp_data[1]")
|
|
144
|
+
print(80*"-")
|
|
145
|
+
print("shape=(n_sims,n_exps,n_sensors,n_field_comps,n_time_steps)")
|
|
146
|
+
print(f"{exp_data[1].shape=}")
|
|
147
|
+
print()
|
|
148
|
+
print("shape=(n_sims,n_sensors,n_field_comps,n_time_steps)")
|
|
149
|
+
print(f"{exp_stats[1].max.shape=}")
|
|
150
|
+
print(80*"=")
|
|
151
|
+
|
|
152
|
+
# We also have specific plotting tools which allow us to visualise the
|
|
153
|
+
# uncertainty bounds for our sensor traces. The defaults plot options show
|
|
154
|
+
# the mean sensor trace and uncertainty bounds of 3 times the stanard
|
|
155
|
+
# deviation. In the next example we will see how to control these plots.
|
|
156
|
+
# For now we will plot the temperature traces for the first simulation and
|
|
157
|
+
# the strain traces for the third simulation in our list of SimData objects.
|
|
158
|
+
(fig,ax) = pyv.plot_exp_traces(exp_sim,
|
|
159
|
+
component="temperature",
|
|
160
|
+
sens_array_num=0,
|
|
161
|
+
sim_num=0)
|
|
162
|
+
|
|
163
|
+
(fig,ax) = pyv.plot_exp_traces(exp_sim,
|
|
164
|
+
component="strain_yy",
|
|
165
|
+
sens_array_num=1,
|
|
166
|
+
sim_num=2)
|
|
167
|
+
plt.show()
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
if __name__ == "__main__":
|
|
171
|
+
main()
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# pyvale: the python validation engine
|
|
3
|
+
# License: MIT
|
|
4
|
+
# Copyright (C) 2025 The Computer Aided Validation Team
|
|
5
|
+
# ==============================================================================
|
|
6
|
+
|
|
7
|
+
"""Pyvale example: Multi-physics experiment simulation in 3D
|
|
8
|
+
--------------------------------------------------------------------------------
|
|
9
|
+
In the previous example we performed a series of simulated experiments on a set
|
|
10
|
+
of 2D multi-physics simulations. Here we use a 3D thermo-mechanical analysis of
|
|
11
|
+
a divertor armour heatsink to show how we can run simulated experiments in 3D.
|
|
12
|
+
|
|
13
|
+
Note that this tutorial assumes you are familiar with the use of pyvale for
|
|
14
|
+
scalar and tensor fields as described in the previous examples.
|
|
15
|
+
|
|
16
|
+
Test case: thermo-mechanical analysis of a divertor heatsink in 3D
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
import numpy as np
|
|
21
|
+
import matplotlib.pyplot as plt
|
|
22
|
+
import mooseherder as mh
|
|
23
|
+
import pyvale as pyv
|
|
24
|
+
|
|
25
|
+
def main() -> None:
|
|
26
|
+
# First we get the path to simulation output file and then read the
|
|
27
|
+
# simulation into a `SimData` object. In this case our simulation is a
|
|
28
|
+
# thermomechanical model of a divertor heatsink.
|
|
29
|
+
sim_path = pyv.DataSet.thermomechanical_3d_path()
|
|
30
|
+
sim_data = mh.ExodusReader(sim_path).read_all_sim_data()
|
|
31
|
+
elem_dims: int = 3
|
|
32
|
+
# We scale our length and displacement units to mm to help with
|
|
33
|
+
# visualisation.
|
|
34
|
+
disp_comps = ("disp_x","disp_y","disp_z")
|
|
35
|
+
sim_data = pyv.scale_length_units(scale=1000.0,
|
|
36
|
+
sim_data=sim_data,
|
|
37
|
+
disp_comps=disp_comps)
|
|
38
|
+
|
|
39
|
+
# If we are going to save figures showing where our sensors are and their
|
|
40
|
+
# simulated traces we need to create a directory. Set the flag below to
|
|
41
|
+
# save the figures when you run the script
|
|
42
|
+
save_figs = False
|
|
43
|
+
save_tag = "thermomech3d"
|
|
44
|
+
fig_save_path = Path.cwd()/"images"
|
|
45
|
+
if not fig_save_path.is_dir():
|
|
46
|
+
fig_save_path.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
|
|
48
|
+
# We specify manual sensor sampling times but we could also set this to None
|
|
49
|
+
# for the sensors to sample at the simulation time steps.
|
|
50
|
+
sample_times = np.linspace(0.0,np.max(sim_data.time),50)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
x_lims = (12.5,12.5)
|
|
54
|
+
y_lims = (0.0,33.0)
|
|
55
|
+
z_lims = (0.0,12.0)
|
|
56
|
+
n_sens = (1,4,1)
|
|
57
|
+
tc_sens_pos = pyv.create_sensor_pos_array(n_sens,x_lims,y_lims,z_lims)
|
|
58
|
+
|
|
59
|
+
tc_sens_data = pyv.SensorData(positions=tc_sens_pos,
|
|
60
|
+
sample_times=sample_times)
|
|
61
|
+
|
|
62
|
+
# We use the sensor array factory to create our thermocouple array with no
|
|
63
|
+
# errors.
|
|
64
|
+
tc_field_name = "temperature"
|
|
65
|
+
tc_array = pyv.SensorArrayFactory \
|
|
66
|
+
.thermocouples_no_errs(sim_data,
|
|
67
|
+
tc_sens_data,
|
|
68
|
+
elem_dims=elem_dims,
|
|
69
|
+
field_name=tc_field_name)
|
|
70
|
+
|
|
71
|
+
# Now we build our error chain starting with some basic errors on the order
|
|
72
|
+
# of 1 degree.
|
|
73
|
+
tc_err_chain = []
|
|
74
|
+
tc_err_chain.append(pyv.ErrSysUnif(low=1.0,high=1.0))
|
|
75
|
+
tc_err_chain.append(pyv.ErrRandNorm(std=1.0))
|
|
76
|
+
|
|
77
|
+
# Now we add positioning error for our thermocouples.
|
|
78
|
+
tc_pos_uncert = 0.1 # units = mm
|
|
79
|
+
tc_pos_rand = (pyv.GenNormal(std=tc_pos_uncert),
|
|
80
|
+
pyv.GenNormal(std=tc_pos_uncert),
|
|
81
|
+
pyv.GenNormal(std=tc_pos_uncert))
|
|
82
|
+
|
|
83
|
+
# We block translation in x so the thermocouples stay attached.
|
|
84
|
+
tc_pos_lock = np.full(tc_sens_pos.shape,False,dtype=bool)
|
|
85
|
+
tc_pos_lock[:,0] = True
|
|
86
|
+
|
|
87
|
+
tc_field_err_data = pyv.ErrFieldData(pos_rand_xyz=tc_pos_rand,
|
|
88
|
+
pos_lock_xyz=tc_pos_lock)
|
|
89
|
+
tc_err_chain.append(pyv.ErrSysField(tc_array.get_field(),
|
|
90
|
+
tc_field_err_data))
|
|
91
|
+
# We have finished our error chain so we can build our error integrator and
|
|
92
|
+
# attach it to our thermocouple array.
|
|
93
|
+
tc_error_int = pyv.ErrIntegrator(tc_err_chain,
|
|
94
|
+
tc_sens_data,
|
|
95
|
+
tc_array.get_measurement_shape())
|
|
96
|
+
tc_array.set_error_integrator(tc_error_int)
|
|
97
|
+
|
|
98
|
+
# We visualise our thermcouple locations on our mesh to make sure they are
|
|
99
|
+
# in the correct positions.
|
|
100
|
+
pv_plot = pyv.plot_point_sensors_on_sim(tc_array,"temperature")
|
|
101
|
+
pv_plot.camera_position = [(59.354, 43.428, 69.946),
|
|
102
|
+
(-2.858, 13.189, 4.523),
|
|
103
|
+
(-0.215, 0.948, -0.233)]
|
|
104
|
+
if save_figs:
|
|
105
|
+
pv_plot.save_graphic(fig_save_path/(save_tag+"_tc_vis.svg"))
|
|
106
|
+
pv_plot.screenshot(fig_save_path/(save_tag+"_tc_vis.png"))
|
|
107
|
+
|
|
108
|
+
pv_plot.show()
|
|
109
|
+
|
|
110
|
+
# Now we have finished with our thermocouple array we can move on to our
|
|
111
|
+
# strain gauge array.
|
|
112
|
+
|
|
113
|
+
# We use the same sampling time but we are going to place the strain gauges
|
|
114
|
+
# down the side of the monoblock where the pipe passes through.
|
|
115
|
+
x_lims = (9.4,9.4)
|
|
116
|
+
y_lims = (0.0,33.0)
|
|
117
|
+
z_lims = (12.0,12.0)
|
|
118
|
+
n_sens = (1,4,1)
|
|
119
|
+
sg_sens_pos = pyv.create_sensor_pos_array(n_sens,x_lims,y_lims,z_lims)
|
|
120
|
+
|
|
121
|
+
sg_sens_data = pyv.SensorData(positions=sg_sens_pos,
|
|
122
|
+
sample_times=sample_times)
|
|
123
|
+
|
|
124
|
+
# We use the sensor array factory to give us a strain gauge array with no
|
|
125
|
+
# errors.
|
|
126
|
+
sg_field_name = "strain"
|
|
127
|
+
sg_norm_comps = ("strain_xx","strain_yy","strain_zz")
|
|
128
|
+
sg_dev_comps = ("strain_xy","strain_yz","strain_xz")
|
|
129
|
+
sg_array = pyv.SensorArrayFactory \
|
|
130
|
+
.strain_gauges_no_errs(sim_data,
|
|
131
|
+
sg_sens_data,
|
|
132
|
+
elem_dims=elem_dims,
|
|
133
|
+
field_name=sg_field_name,
|
|
134
|
+
norm_comps=sg_norm_comps,
|
|
135
|
+
dev_comps=sg_dev_comps)
|
|
136
|
+
|
|
137
|
+
# Now we build our error chain starting with some basic errors on the order
|
|
138
|
+
# of 1 percent.
|
|
139
|
+
sg_err_chain = []
|
|
140
|
+
sg_err_chain.append(pyv.ErrSysUnifPercent(low_percent=1.0,high_percent=1.0))
|
|
141
|
+
sg_err_chain.append(pyv.ErrRandNormPercent(std_percent=1.0))
|
|
142
|
+
|
|
143
|
+
# We are going to add +/-2 degree rotation uncertainty to our strain gauges.
|
|
144
|
+
angle_uncert = 2.0
|
|
145
|
+
angle_rand_zyx = (pyv.GenUniform(low=-angle_uncert,high=angle_uncert), # units = deg
|
|
146
|
+
pyv.GenUniform(low=-angle_uncert,high=angle_uncert),
|
|
147
|
+
pyv.GenUniform(low=-angle_uncert,high=angle_uncert))
|
|
148
|
+
|
|
149
|
+
# We only allow rotation on the face the strain gauges are on
|
|
150
|
+
angle_lock = np.full(sg_sens_pos.shape,True,dtype=bool)
|
|
151
|
+
angle_lock[:,0] = False # Allow rotation about z
|
|
152
|
+
|
|
153
|
+
sg_field_err_data = pyv.ErrFieldData(ang_rand_zyx=angle_rand_zyx,
|
|
154
|
+
ang_lock_zyx=angle_lock)
|
|
155
|
+
sg_err_chain.append(pyv.ErrSysField(sg_array.get_field(),
|
|
156
|
+
sg_field_err_data))
|
|
157
|
+
|
|
158
|
+
# We have finished our error chain so we can build our error integrator and
|
|
159
|
+
# attach it to our thermocouple array.
|
|
160
|
+
sg_error_int = pyv.ErrIntegrator(sg_err_chain,
|
|
161
|
+
sg_sens_data,
|
|
162
|
+
sg_array.get_measurement_shape())
|
|
163
|
+
sg_array.set_error_integrator(sg_error_int)
|
|
164
|
+
|
|
165
|
+
# Now we visualise the strain gauge locations to make sure they are where
|
|
166
|
+
# we expect them to be.
|
|
167
|
+
pv_plot = pyv.plot_point_sensors_on_sim(sg_array,"strain_yy")
|
|
168
|
+
pv_plot.camera_position = [(59.354, 43.428, 69.946),
|
|
169
|
+
(-2.858, 13.189, 4.523),
|
|
170
|
+
(-0.215, 0.948, -0.233)]
|
|
171
|
+
if save_figs:
|
|
172
|
+
pv_plot.save_graphic(fig_save_path/(save_tag+"_sg_vis.svg"))
|
|
173
|
+
pv_plot.screenshot(fig_save_path/(save_tag+"_sg_vis.png"))
|
|
174
|
+
|
|
175
|
+
pv_plot.show()
|
|
176
|
+
|
|
177
|
+
# We have both our sensor arrays so we will create and run our experiment.
|
|
178
|
+
# Here we only have a single input simulation in our list and we only run
|
|
179
|
+
# 100 simulated experiments as we are going to plot all simulated data
|
|
180
|
+
# points on our traces. Note that if you are running more than 100
|
|
181
|
+
# experiments here you will need to set the trace plots below to not show
|
|
182
|
+
# all points on the graph.
|
|
183
|
+
sim_list = [sim_data,]
|
|
184
|
+
sensor_arrays = [tc_array,sg_array]
|
|
185
|
+
exp_sim = pyv.ExperimentSimulator(sim_list,
|
|
186
|
+
sensor_arrays,
|
|
187
|
+
num_exp_per_sim=100)
|
|
188
|
+
|
|
189
|
+
# We run our experiments and calculate summary statistics as in the previous
|
|
190
|
+
# example
|
|
191
|
+
exp_data = exp_sim.run_experiments()
|
|
192
|
+
exp_stats = exp_sim.calc_stats()
|
|
193
|
+
|
|
194
|
+
# We print the lengths of our exp_data and exp_stats lists along with the
|
|
195
|
+
# shape of the numpy arrays they contain so we can index into them easily.
|
|
196
|
+
print(80*"=")
|
|
197
|
+
print("exp_data and exp_stats are lists where the index is the sensor array")
|
|
198
|
+
print("position in the list as field components are not consistent dims:")
|
|
199
|
+
print(f"{len(exp_data)=}")
|
|
200
|
+
print(f"{len(exp_stats)=}")
|
|
201
|
+
print()
|
|
202
|
+
print(80*"-")
|
|
203
|
+
print("Thermal sensor array @ exp_data[0]")
|
|
204
|
+
print(80*"-")
|
|
205
|
+
print("shape=(n_sims,n_exps,n_sensors,n_field_comps,n_time_steps)")
|
|
206
|
+
print(f"{exp_data[0].shape=}")
|
|
207
|
+
print()
|
|
208
|
+
print("Stats are calculated over all experiments (axis=1)")
|
|
209
|
+
print("shape=(n_sims,n_sensors,n_field_comps,n_time_steps)")
|
|
210
|
+
print(f"{exp_stats[0].max.shape=}")
|
|
211
|
+
print()
|
|
212
|
+
print(80*"-")
|
|
213
|
+
print("Mechanical sensor array @ exp_data[1]")
|
|
214
|
+
print(80*"-")
|
|
215
|
+
print("shape=(n_sims,n_exps,n_sensors,n_field_comps,n_time_steps)")
|
|
216
|
+
print(f"{exp_data[1].shape=}")
|
|
217
|
+
print()
|
|
218
|
+
print("shape=(n_sims,n_sensors,n_field_comps,n_time_steps)")
|
|
219
|
+
print(f"{exp_stats[1].max.shape=}")
|
|
220
|
+
print(80*"=")
|
|
221
|
+
|
|
222
|
+
# Finally, we are going to plot the simulated sensor traces but we are going
|
|
223
|
+
# to control some of the plotting options using the options data class here.
|
|
224
|
+
# We set the plot to show all simulated experiment data points and to plot
|
|
225
|
+
# the median as the centre line and to fill between the min and max values.
|
|
226
|
+
# Note that the default here is to plot the mean and fill between 3 times
|
|
227
|
+
# the standard deviation.
|
|
228
|
+
trace_opts = pyv.TraceOptsExperiment(plot_all_exp_points=True,
|
|
229
|
+
centre=pyv.EExpVisCentre.MEDIAN,
|
|
230
|
+
fill_between=pyv.EExpVisBounds.MINMAX)
|
|
231
|
+
|
|
232
|
+
(fig,ax) = pyv.plot_exp_traces(exp_sim,
|
|
233
|
+
component="temperature",
|
|
234
|
+
sens_array_num=0,
|
|
235
|
+
sim_num=0,
|
|
236
|
+
trace_opts=trace_opts)
|
|
237
|
+
if save_figs:
|
|
238
|
+
fig.savefig(fig_save_path/(save_tag+"_tc_traces.png"),
|
|
239
|
+
dpi=300, format='png', bbox_inches='tight')
|
|
240
|
+
|
|
241
|
+
(fig,ax) = pyv.plot_exp_traces(exp_sim,
|
|
242
|
+
component="strain_yy",
|
|
243
|
+
sens_array_num=1,
|
|
244
|
+
sim_num=0,
|
|
245
|
+
trace_opts=trace_opts)
|
|
246
|
+
if save_figs:
|
|
247
|
+
fig.savefig(fig_save_path/(save_tag+"_sg_traces.png"),
|
|
248
|
+
dpi=300, format='png', bbox_inches='tight')
|
|
249
|
+
plt.show()
|
|
250
|
+
|
|
251
|
+
if __name__ == "__main__":
|
|
252
|
+
main()
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Copyright (C) 2025 The Computer Aided Validation Team
|
|
8
|
-
================================================================================
|
|
9
|
-
'''
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# pyvale: the python validation engine
|
|
3
|
+
# License: MIT
|
|
4
|
+
# Copyright (C) 2025 The Computer Aided Validation Team
|
|
5
|
+
# ==============================================================================
|
|
6
|
+
|
|
10
7
|
import matplotlib.pyplot as plt
|
|
11
8
|
import pyvale
|
|
12
9
|
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Copyright (C) 2025 The Computer Aided Validation Team
|
|
8
|
-
================================================================================
|
|
9
|
-
'''
|
|
1
|
+
# ==============================================================================
|
|
2
|
+
# pyvale: the python validation engine
|
|
3
|
+
# License: MIT
|
|
4
|
+
# Copyright (C) 2025 The Computer Aided Validation Team
|
|
5
|
+
# ==============================================================================
|
|
6
|
+
|
|
10
7
|
import numpy as np
|
|
11
8
|
import matplotlib.pyplot as plt
|
|
12
9
|
import sympy
|
|
@@ -14,7 +11,7 @@ import pyvale
|
|
|
14
11
|
|
|
15
12
|
def main() -> None:
|
|
16
13
|
|
|
17
|
-
case_data = pyvale.
|
|
14
|
+
case_data = pyvale.AnalyticData2D()
|
|
18
15
|
case_data.length_x = 10.0
|
|
19
16
|
case_data.length_y = 7.5
|
|
20
17
|
n_elem_mult = 10
|
|
@@ -30,7 +27,7 @@ def main() -> None:
|
|
|
30
27
|
case_data.offsets_time = (0.0,)
|
|
31
28
|
|
|
32
29
|
|
|
33
|
-
data_gen = pyvale.
|
|
30
|
+
data_gen = pyvale.AnalyticSimDataGen(case_data)
|
|
34
31
|
sim_data = data_gen.generate_sim_data()
|
|
35
32
|
|
|
36
33
|
(grid_x,grid_y,grid_field) = data_gen.get_visualisation_grid()
|