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,318 @@
1
+ # ==============================================================================
2
+ # pyvale: the python validation engine
3
+ # License: MIT
4
+ # Copyright (C) 2025 The Computer Aided Validation Team
5
+ # ==============================================================================
6
+
7
+ """
8
+ This module contains functions for visualising virtual sensors on a simulation
9
+ mesh with simulated fields using pyvista.
10
+ """
11
+
12
+ import vtk #NOTE: has to be here to fix latex bug in pyvista/vtk
13
+ # See: https://github.com/pyvista/pyvista/discussions/2928
14
+ #NOTE: causes output to console to be suppressed unfortunately
15
+ #NOTE: May 2025, the console suppression output is fixed but the vtk import is
16
+ #still required tro make latex work.
17
+ import pyvista as pv
18
+
19
+ import mooseherder as mh
20
+
21
+ from pyvale.sensorarraypoint import SensorArrayPoint
22
+ from pyvale.fieldconverter import simdata_to_pyvista
23
+ from pyvale.visualopts import (VisOptsSimSensors,VisOptsImageSave)
24
+ from pyvale.visualtools import (create_pv_plotter,
25
+ get_colour_lims,
26
+ save_pv_image)
27
+
28
+
29
+ # TODO: this needs to be updated to allow the user to plot at sensor times not
30
+ # just simulation times. This will require interpolation of the underlying
31
+ # simulation fields.
32
+ def add_sim_field(pv_plot: pv.Plotter,
33
+ sensor_array: SensorArrayPoint,
34
+ component: str,
35
+ time_step: int,
36
+ vis_opts: VisOptsSimSensors,
37
+ ) -> tuple[pv.Plotter,pv.UnstructuredGrid]:
38
+ """Adds a simulation field to a pyvista plot object which is visualised on
39
+ the mesh using a colormap.
40
+
41
+ Parameters
42
+ ----------
43
+ pv_plot : pv.Plotter
44
+ Handle to the pyvista plot object to add the simulation field to.
45
+ sensor_array : SensorArrayPoint
46
+ Sensor array associated with the field to be plotted.
47
+ component : str
48
+ String key for the field component to be shown.
49
+ time_step : int
50
+ Time step to plot based on the time steps in the underlying simulation
51
+ data object.
52
+ vis_opts : VisOptsSimSensors
53
+ Dataclass containing options for controlling the appearance of the
54
+ virtual sensors.
55
+
56
+ Returns
57
+ -------
58
+ tuple[pv.Plotter,pv.UnstructuredGrid]
59
+ Tuple containing a handle to the pyvista plotter which has had the field
60
+ visualisation added and the pyvistas unstructured grid that was used to
61
+ plot the field.
62
+ """
63
+ sim_vis = sensor_array._field.get_visualiser()
64
+ sim_data = sensor_array._field.get_sim_data()
65
+ sim_vis[component] = sim_data.node_vars[component][:,time_step]
66
+ comp_ind = sensor_array._field.get_component_index(component)
67
+
68
+ scalar_bar_args = {"title":sensor_array._descriptor.create_label(comp_ind),
69
+ "vertical":vis_opts.colour_bar_vertical,
70
+ "title_font_size":vis_opts.colour_bar_font_size,
71
+ "label_font_size":vis_opts.colour_bar_font_size}
72
+
73
+ pv_plot.add_mesh(sim_vis,
74
+ scalars=component,
75
+ label="sim-data",
76
+ show_edges=vis_opts.show_edges,
77
+ show_scalar_bar=vis_opts.colour_bar_show,
78
+ scalar_bar_args=scalar_bar_args,
79
+ lighting=False,
80
+ clim=vis_opts.colour_bar_lims)
81
+
82
+ if vis_opts.time_label_pos is not None:
83
+ pv_plot.add_text(f"Time: {sim_data.time[time_step]} " + \
84
+ f"{sensor_array._descriptor.time_units}",
85
+ position=vis_opts.time_label_pos,
86
+ font_size=vis_opts.time_label_font_size,
87
+ name='time-label')
88
+
89
+ return (pv_plot,sim_vis)
90
+
91
+
92
+ # TODO: this should be able to take a list of ISensorArray and plot all of them
93
+ # on the same mesh.
94
+ def add_sensor_points_nom(pv_plot: pv.Plotter,
95
+ sensor_array: SensorArrayPoint,
96
+ vis_opts: VisOptsSimSensors,
97
+ ) -> pv.Plotter:
98
+ """Adds points and tagged labels showing the virtual sensor locations on
99
+ the simulation mesh in the given pyvista plot object.
100
+
101
+ Parameters
102
+ ----------
103
+ pv_plot : pv.Plotter
104
+ Pyvista plotter used to display the virtual sensor locations.
105
+ sensor_array : SensorArrayPoint
106
+ Sensor array for which the virtual sensor location will be shown.
107
+ vis_opts : VisOptsSimSensors
108
+ Dataclass containing options for controlling the appearance of the
109
+ virtual sensors.
110
+
111
+ Returns
112
+ -------
113
+ pv.Plotter
114
+ Pyvista plotter which has had the virtual sensor locations added.
115
+ """
116
+ vis_sens_nominal = pv.PolyData(sensor_array._sensor_data.positions)
117
+ vis_sens_nominal["labels"] = sensor_array._descriptor.create_sensor_tags(
118
+ sensor_array.get_measurement_shape()[0])
119
+
120
+ # Add points to show sensor locations
121
+ pv_plot.add_point_labels(vis_sens_nominal,"labels",
122
+ font_size=vis_opts.sens_label_font_size,
123
+ shape_color=vis_opts.sens_label_colour,
124
+ point_color=vis_opts.sens_colour_nom,
125
+ render_points_as_spheres=True,
126
+ point_size=vis_opts.sens_point_size,
127
+ always_visible=True)
128
+
129
+ return pv_plot
130
+
131
+
132
+ def add_sensor_points_pert(pv_plot: pv.Plotter,
133
+ sensor_array: SensorArrayPoint,
134
+ vis_opts: VisOptsSimSensors,
135
+ ) -> pv.Plotter:
136
+ """Adds points showing the perturbed virtual sensor locations on
137
+ the simulation mesh in the given pyvista plot object. Note that this will
138
+ only work if field errors are added perturbing the sensor locations.
139
+
140
+ Parameters
141
+ ----------
142
+ pv_plot : pv.Plotter
143
+ Pyvista plotter used to display the virtual sensor locations.
144
+ sensor_array : SensorArrayPoint
145
+ Sensor array for which the virtual sensor location will be shown.
146
+ vis_opts : VisOptsSimSensors
147
+ Dataclass containing options for controlling the appearance of the
148
+ virtual sensors.
149
+
150
+ Returns
151
+ -------
152
+ pv.Plotter
153
+ Pyvista plotter which has had the virtual sensor locations added.
154
+ """
155
+ sens_data_perturbed = sensor_array.get_sensor_data_perturbed()
156
+
157
+ if sens_data_perturbed is not None and vis_opts.show_perturbed_pos:
158
+ vis_sens_perturbed = pv.PolyData(sens_data_perturbed.positions)
159
+ vis_sens_perturbed["labels"] = ["",]*sensor_array.get_measurement_shape()[0]
160
+
161
+ pv_plot.add_point_labels(vis_sens_perturbed,"labels",
162
+ font_size=vis_opts.sens_label_font_size,
163
+ shape_color=vis_opts.sens_label_colour,
164
+ point_color=vis_opts.sens_colour_pert,
165
+ render_points_as_spheres=True,
166
+ point_size=vis_opts.sens_point_size,
167
+ always_visible=True)
168
+
169
+ return pv_plot
170
+
171
+
172
+ def plot_sim_mesh(sim_data: mh.SimData,
173
+ elem_dims: int,
174
+ vis_opts: VisOptsSimSensors | None = None,
175
+ ) -> pv.Plotter:
176
+ """Plots the simulation mesh without any fields. Useful for visualising
177
+ mesh geometry.
178
+
179
+ Parameters
180
+ ----------
181
+ sim_data : mh.SimData
182
+ Sim data object containing the mesh to plot.
183
+ elem_dims : int
184
+ Number of dimensions for the elements to be plotted.
185
+ vis_opts : VisOptsSimSensors | None, optional
186
+ Dataclass containing options for controlling the appearance of the
187
+ virtual sensors, by default None. If None then a default options
188
+ dataclass is created.
189
+
190
+ Returns
191
+ -------
192
+ pv.Plotter
193
+ Handle to the pyvista plotter that is showing the mesh.
194
+ """
195
+ if vis_opts is None:
196
+ vis_opts = VisOptsSimSensors()
197
+
198
+ (_,sim_vis) = simdata_to_pyvista(sim_data=sim_data,
199
+ components=None,
200
+ elem_dims=elem_dims)
201
+
202
+ pv_plot = create_pv_plotter(vis_opts)
203
+ pv_plot.add_mesh(sim_vis,
204
+ label="sim-data",
205
+ show_edges=vis_opts.show_edges,
206
+ lighting=False)
207
+ return pv_plot
208
+
209
+
210
+ def plot_sim_data(sim_data: mh.SimData,
211
+ component: str,
212
+ elem_dims: int,
213
+ time_step: int = -1,
214
+ vis_opts: VisOptsSimSensors | None = None
215
+ ) -> pv.Plotter:
216
+ """Plots the simulation mesh showing the specified phyiscal field at the
217
+ time step specified.
218
+
219
+ Parameters
220
+ ----------
221
+ sim_data : mh.SimData
222
+ simulation data object containing the mesh and field data to show.
223
+ component : str
224
+ String key for accessing the nodal field to visualise in the sim data
225
+ object.
226
+ elem_dims : int
227
+ Number of dimensions for the elements to be plotted.
228
+ time_step : int, optional
229
+ Simulation time step number to plot, by default -1 (the last time step).
230
+ vis_opts : VisOptsSimSensors | None, optional
231
+ Dataclass containing options for controlling the appearance of the
232
+ virtual sensors, by default None. If None then a default options
233
+ dataclass is created.
234
+
235
+ Returns
236
+ -------
237
+ pv.Plotter
238
+ Handle to the pyvista plotter showing the simulation mesh and field.
239
+ """
240
+ if vis_opts is None:
241
+ vis_opts = VisOptsSimSensors()
242
+
243
+ (_,sim_vis) = simdata_to_pyvista(sim_data,
244
+ (component,),
245
+ elem_dims)
246
+
247
+ sim_vis[component] = sim_data.node_vars[component][:,time_step]
248
+
249
+ pv_plot = create_pv_plotter(vis_opts)
250
+ pv_plot.add_mesh(sim_vis,
251
+ scalars=component,
252
+ label="sim-data",
253
+ show_edges=vis_opts.show_edges,
254
+ show_scalar_bar=vis_opts.colour_bar_show,
255
+ lighting=False,
256
+ clim=vis_opts.colour_bar_lims)
257
+
258
+ return pv_plot
259
+
260
+
261
+ def plot_point_sensors_on_sim(sensor_array: SensorArrayPoint,
262
+ component: str,
263
+ time_step: int = -1,
264
+ vis_opts: VisOptsSimSensors | None = None,
265
+ image_save_opts: VisOptsImageSave | None = None,
266
+ ) -> pv.Plotter:
267
+ """Creates a visualisation of the virtual sensor locations on the simulation
268
+ mesh showing the underlying field the sensors are sampling at the specified
269
+ time step.
270
+
271
+ Parameters
272
+ ----------
273
+ sensor_array : SensorArrayPoint
274
+ Sensor array containing the sensors to plot and the field to display.
275
+ component : str
276
+ String key for accessing the nodal field to visualise in the sim data
277
+ object.
278
+ time_step : int, optional
279
+ Simulation time step number to plot, by default -1 (the last time step).
280
+ vis_opts : VisOptsSimSensors | None, optional
281
+ Dataclass containing options for controlling the appearance of the
282
+ virtual sensors, by default None. If None then a default options
283
+ dataclass is created.
284
+ image_save_opts : VisOptsImageSave | None, optional
285
+ Dataclass containing options for saving image of the virtual sensor
286
+ visualisation, by default None. If None a default options dataclass is
287
+ created.
288
+
289
+ Returns
290
+ -------
291
+ pv.Plotter
292
+ Handle to the pyvista plotter showing the sensor locations.
293
+ """
294
+ if vis_opts is None:
295
+ vis_opts = VisOptsSimSensors()
296
+
297
+ sim_data = sensor_array._field.get_sim_data()
298
+ vis_opts.colour_bar_lims = get_colour_lims(
299
+ sim_data.node_vars[component][:,time_step],
300
+ vis_opts.colour_bar_lims)
301
+
302
+ pv_plot = create_pv_plotter(vis_opts)
303
+
304
+ pv_plot = add_sensor_points_pert(pv_plot,sensor_array,vis_opts)
305
+ pv_plot = add_sensor_points_nom(pv_plot,sensor_array,vis_opts)
306
+ (pv_plot,_) = add_sim_field(pv_plot,
307
+ sensor_array,
308
+ component,
309
+ time_step,
310
+ vis_opts)
311
+
312
+ pv_plot.camera_position = vis_opts.camera_position
313
+
314
+ if image_save_opts is not None:
315
+ save_pv_image(pv_plot,image_save_opts)
316
+
317
+ return pv_plot
318
+
pyvale/visualtools.py ADDED
@@ -0,0 +1,136 @@
1
+ # ==============================================================================
2
+ # pyvale: the python validation engine
3
+ # License: MIT
4
+ # Copyright (C) 2025 The Computer Aided Validation Team
5
+ # ==============================================================================
6
+
7
+ """
8
+ This module contains utility functions used for creating pyvale visualisations.
9
+ """
10
+
11
+ from pathlib import Path
12
+ import numpy as np
13
+ import vtk #NOTE: has to be here to fix latex bug in pyvista/vtk
14
+ # See: https://github.com/pyvista/pyvista/discussions/2928
15
+ # NOTE: causes output to console to be suppressed unfortunately
16
+ # NOTE: May2025 still needs include but does not suppress console output
17
+ import pyvista as pv
18
+ from pyvale.visualopts import (VisOptsSimSensors,
19
+ VisOptsImageSave,
20
+ EImageType,
21
+ VisOptsAnimation,
22
+ EAnimationType)
23
+
24
+ def create_pv_plotter(vis_opts: VisOptsSimSensors) -> pv.Plotter:
25
+ """Creates a pyvista plotter based on the input options.
26
+
27
+ Parameters
28
+ ----------
29
+ vis_opts : VisOptsSimSensors
30
+ Dataclass containing the visualisation options for creating the plotter.
31
+
32
+ Returns
33
+ -------
34
+ pv.Plotter
35
+ Blank pyvista plotter object with the given settings.
36
+ """
37
+ pv_plot = pv.Plotter(window_size=vis_opts.window_size_px)
38
+ pv_plot.set_background(vis_opts.background_colour)
39
+ pv.global_theme.font.color = vis_opts.font_colour
40
+ pv_plot.add_axes_at_origin(labels_off=True)
41
+ return pv_plot
42
+
43
+
44
+ def get_colour_lims(component_data: np.ndarray,
45
+ colour_bar_lims: tuple[float,float] | None
46
+ ) -> tuple[float,float]:
47
+ """Gets the colourbar limits based on the input component data array.
48
+
49
+ Parameters
50
+ ----------
51
+ component_data : np.ndarray
52
+ Array of data for the field component of interest. Can be any shape as
53
+ the array is flattened for the limit calculations
54
+ colour_bar_lims : tuple[float,float] | None
55
+ Forces the colourbar limits to be the values give in the tuple. If None
56
+ then the colorbar limits are calculated based on the input data array.
57
+
58
+ Returns
59
+ -------
60
+ tuple[float,float]
61
+ Colourbar limits in the form: (min,max).
62
+ """
63
+ if colour_bar_lims is None:
64
+ min_comp = np.min(component_data.flatten())
65
+ max_comp = np.max(component_data.flatten())
66
+ colour_bar_lims = (min_comp,max_comp)
67
+
68
+ assert colour_bar_lims[1] > colour_bar_lims[0], ("Colourbar minimum must be"
69
+ + " smaller than the colourbar maximum.")
70
+
71
+ return colour_bar_lims
72
+
73
+
74
+ def save_pv_image(pv_plot: pv.Plotter,
75
+ image_save_opts: VisOptsImageSave) -> None:
76
+ """Saves an image of a pyvista visualisation to disk based on the input
77
+ options.
78
+
79
+ Parameters
80
+ ----------
81
+ pv_plot : pv.Plotter
82
+ Pyvista plotter object to save the image from.
83
+ image_save_opts : VisOptsImageSave
84
+ Dataclass containing the options to save the image.
85
+ """
86
+
87
+ if image_save_opts.path is None:
88
+ image_save_opts.path = Path.cwd() / "pyvale-image"
89
+
90
+ if image_save_opts.image_type == EImageType.PNG:
91
+ image_save_opts.path = image_save_opts.path.with_suffix(".png")
92
+ pv_plot.screenshot(image_save_opts.path,
93
+ image_save_opts.transparent_background)
94
+
95
+ elif image_save_opts.image_type == EImageType.SVG:
96
+ image_save_opts.path = image_save_opts.path.with_suffix(".svg")
97
+ pv_plot.save_graphic(image_save_opts.path)
98
+
99
+
100
+ def set_animation_writer(pv_plot: pv.Plotter,
101
+ anim_opts: VisOptsAnimation) -> pv.Plotter:
102
+ """Sets the animation writer and output path for a virtual sensor simulation
103
+ visualisation.
104
+
105
+ Parameters
106
+ ----------
107
+ pv_plot : pv.Plotter
108
+ Pyvistas plot object which will be used to create the animation.
109
+ anim_opts : VisOptsAnimation
110
+ Dataclass containing the options for creating the animation.
111
+
112
+ Returns
113
+ -------
114
+ pv.Plotter
115
+ Pyvista plotter with the given animation writer opened.
116
+ """
117
+ if anim_opts.save_animation is None:
118
+ return pv_plot
119
+
120
+ if anim_opts.save_path is None:
121
+ anim_opts.save_path = Path.cwd() / "pyvale-animation"
122
+
123
+ if anim_opts.save_animation == EAnimationType.GIF:
124
+ anim_opts.save_path = anim_opts.save_path.with_suffix(".gif")
125
+ pv_plot.open_gif(anim_opts.save_path,
126
+ loop=0,
127
+ fps=anim_opts.frames_per_second)
128
+
129
+ elif anim_opts.save_animation == EAnimationType.MP4:
130
+ anim_opts.save_path = anim_opts.save_path.with_suffix(".mp4")
131
+ pv_plot.open_movie(anim_opts.save_path,
132
+ anim_opts.frames_per_second)
133
+
134
+ return pv_plot
135
+
136
+
@@ -0,0 +1,142 @@
1
+ # ==============================================================================
2
+ # pyvale: the python validation engine
3
+ # License: MIT
4
+ # Copyright (C) 2025 The Computer Aided Validation Team
5
+ # ==============================================================================
6
+
7
+ from typing import Any
8
+ import numpy as np
9
+ import matplotlib.pyplot as plt
10
+ from pyvale.sensorarraypoint import SensorArrayPoint
11
+ from pyvale.visualopts import (PlotOptsGeneral,
12
+ TraceOptsSensor)
13
+
14
+
15
+
16
+ # TODO: this should probably take an ISensorarray
17
+ def plot_time_traces(sensor_array: SensorArrayPoint,
18
+ component: str | None = None,
19
+ trace_opts: TraceOptsSensor | None = None,
20
+ plot_opts: PlotOptsGeneral | None = None
21
+ ) -> tuple[Any,Any]:
22
+ """Plots time traces for the truth and virtual experiments of the sensors
23
+ in the given sensor array.
24
+
25
+ Parameters
26
+ ----------
27
+ sensor_array : SensorArrayPoint
28
+ _description
29
+ component : str | None
30
+ String key for the field component to plot, by default None. If None
31
+ then the first component in the measurement array is plotted
32
+ trace_opts : TraceOptsSensor | None, optional
33
+ Dataclass containing specific options for controlling the plot
34
+ appearance, by default None. If None the default options are used.
35
+ plot_opts : PlotOptsGeneral | None, optional
36
+ Dataclass containing general options for formatting plots and
37
+ visualisations, by default None. If None the default options are used.
38
+
39
+ Returns
40
+ -------
41
+ tuple[Any,Any]
42
+ A tuple containing a handle to the matplotlib figure and axis objects:
43
+ (fig,ax).
44
+ """
45
+ #---------------------------------------------------------------------------
46
+ field = sensor_array._field
47
+ samp_time = sensor_array.get_sample_times()
48
+ measurements = sensor_array.get_measurements()
49
+ num_sens = sensor_array._sensor_data.positions.shape[0]
50
+ descriptor = sensor_array._descriptor
51
+ sensors_perturbed = sensor_array.get_sensor_data_perturbed()
52
+
53
+ comp_ind = 0
54
+ if component is not None:
55
+ comp_ind = sensor_array._field.get_component_index(component)
56
+
57
+ #---------------------------------------------------------------------------
58
+ if plot_opts is None:
59
+ plot_opts = PlotOptsGeneral()
60
+
61
+ if trace_opts is None:
62
+ trace_opts = TraceOptsSensor()
63
+
64
+ if trace_opts.sensors_to_plot is None:
65
+ sensors_to_plot = range(num_sens)
66
+ else:
67
+ sensors_to_plot = trace_opts.sensors_to_plot
68
+
69
+ #---------------------------------------------------------------------------
70
+ # Figure canvas setup
71
+ fig, ax = plt.subplots(figsize=plot_opts.single_fig_size_landscape,
72
+ layout="constrained")
73
+ fig.set_dpi(plot_opts.resolution)
74
+
75
+ #---------------------------------------------------------------------------
76
+ # Plot simulation and truth lines
77
+ if trace_opts.sim_line is not None:
78
+ sim_time = field.get_time_steps()
79
+ sim_vals = field.sample_field(sensor_array._sensor_data.positions,
80
+ None,
81
+ sensor_array._sensor_data.angles)
82
+
83
+ for ii,ss in enumerate(sensors_to_plot):
84
+ ax.plot(sim_time,
85
+ sim_vals[ss,comp_ind,:],
86
+ trace_opts.sim_line,
87
+ lw=plot_opts.lw,
88
+ ms=plot_opts.ms,
89
+ color=plot_opts.colors[ii % plot_opts.colors_num])
90
+
91
+ if trace_opts.truth_line is not None:
92
+ truth = sensor_array.get_truth()
93
+ for ii,ss in enumerate(sensors_to_plot):
94
+ ax.plot(samp_time,
95
+ truth[ss,comp_ind,:],
96
+ trace_opts.truth_line,
97
+ lw=plot_opts.lw,
98
+ ms=plot_opts.ms,
99
+ color=plot_opts.colors[ii % plot_opts.colors_num])
100
+
101
+ sensor_tags = descriptor.create_sensor_tags(num_sens)
102
+ lines = []
103
+ for ii,ss in enumerate(sensors_to_plot):
104
+ sensor_time = samp_time
105
+ if sensors_perturbed is not None:
106
+ if sensors_perturbed.sample_times is not None:
107
+ sensor_time = sensors_perturbed.sample_times
108
+
109
+ line, = ax.plot(sensor_time,
110
+ measurements[ss,comp_ind,:],
111
+ trace_opts.meas_line,
112
+ label=sensor_tags[ss],
113
+ lw=plot_opts.lw,
114
+ ms=plot_opts.ms,
115
+ color=plot_opts.colors[ii % plot_opts.colors_num])
116
+
117
+ lines.append(line)
118
+
119
+ #---------------------------------------------------------------------------
120
+ # Axis / legend labels and options
121
+ ax.set_xlabel(trace_opts.time_label,
122
+ fontsize=plot_opts.font_ax_size, fontname=plot_opts.font_name)
123
+ ax.set_ylabel(descriptor.create_label(comp_ind),
124
+ fontsize=plot_opts.font_ax_size, fontname=plot_opts.font_name)
125
+
126
+ if trace_opts.time_min_max is None:
127
+ min_time = np.min((np.min(samp_time),np.min(sensor_time)))
128
+ max_time = np.max((np.max(samp_time),np.max(sensor_time)))
129
+ ax.set_xlim((min_time,max_time)) # type: ignore
130
+ else:
131
+ ax.set_xlim(trace_opts.time_min_max)
132
+
133
+ if trace_opts.legend_loc is not None:
134
+ ax.legend(handles=lines,
135
+ prop={"size":plot_opts.font_leg_size},
136
+ loc=trace_opts.legend_loc)
137
+
138
+ plt.grid(True)
139
+ plt.draw()
140
+
141
+ return (fig,ax)
142
+