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.
- pyvale/.dylibs/libomp.dylib +0 -0
- pyvale/__init__.py +89 -0
- pyvale/analyticmeshgen.py +102 -0
- pyvale/analyticsimdatafactory.py +91 -0
- pyvale/analyticsimdatagenerator.py +323 -0
- pyvale/blendercalibrationdata.py +15 -0
- pyvale/blenderlightdata.py +26 -0
- pyvale/blendermaterialdata.py +15 -0
- pyvale/blenderrenderdata.py +30 -0
- pyvale/blenderscene.py +488 -0
- pyvale/blendertools.py +420 -0
- pyvale/camera.py +146 -0
- pyvale/cameradata.py +69 -0
- pyvale/cameradata2d.py +84 -0
- pyvale/camerastereo.py +217 -0
- pyvale/cameratools.py +522 -0
- pyvale/cython/rastercyth.c +32211 -0
- pyvale/cython/rastercyth.cpython-311-darwin.so +0 -0
- pyvale/cython/rastercyth.py +640 -0
- pyvale/data/__init__.py +5 -0
- pyvale/data/cal_target.tiff +0 -0
- pyvale/data/case00_HEX20_out.e +0 -0
- pyvale/data/case00_HEX27_out.e +0 -0
- pyvale/data/case00_HEX8_out.e +0 -0
- pyvale/data/case00_TET10_out.e +0 -0
- pyvale/data/case00_TET14_out.e +0 -0
- pyvale/data/case00_TET4_out.e +0 -0
- pyvale/data/case13_out.e +0 -0
- pyvale/data/case16_out.e +0 -0
- pyvale/data/case17_out.e +0 -0
- pyvale/data/case18_1_out.e +0 -0
- pyvale/data/case18_2_out.e +0 -0
- pyvale/data/case18_3_out.e +0 -0
- pyvale/data/case25_out.e +0 -0
- pyvale/data/case26_out.e +0 -0
- pyvale/data/optspeckle_2464x2056px_spec5px_8bit_gblur1px.tiff +0 -0
- pyvale/dataset.py +325 -0
- pyvale/errorcalculator.py +109 -0
- pyvale/errordriftcalc.py +146 -0
- pyvale/errorintegrator.py +336 -0
- pyvale/errorrand.py +607 -0
- pyvale/errorsyscalib.py +134 -0
- pyvale/errorsysdep.py +327 -0
- pyvale/errorsysfield.py +414 -0
- pyvale/errorsysindep.py +808 -0
- pyvale/examples/__init__.py +5 -0
- 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/genanalyticdata/ex1_1_scalarvisualisation.py +35 -0
- pyvale/examples/genanalyticdata/ex1_2_scalarcasebuild.py +43 -0
- pyvale/examples/genanalyticdata/ex2_1_analyticsensors.py +80 -0
- pyvale/examples/imagedef2d/ex_imagedef2d_todisk.py +79 -0
- 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/renderrasterisation/ex_rastenp.py +153 -0
- pyvale/examples/renderrasterisation/ex_rastercyth_oneframe.py +218 -0
- pyvale/examples/renderrasterisation/ex_rastercyth_static_cypara.py +187 -0
- pyvale/examples/renderrasterisation/ex_rastercyth_static_pypara.py +190 -0
- pyvale/examples/visualisation/ex1_1_plot_traces.py +102 -0
- pyvale/examples/visualisation/ex2_1_animate_sim.py +89 -0
- pyvale/experimentsimulator.py +175 -0
- pyvale/field.py +128 -0
- pyvale/fieldconverter.py +351 -0
- pyvale/fieldsampler.py +111 -0
- pyvale/fieldscalar.py +166 -0
- pyvale/fieldtensor.py +218 -0
- pyvale/fieldtransform.py +388 -0
- pyvale/fieldvector.py +213 -0
- pyvale/generatorsrandom.py +505 -0
- pyvale/imagedef2d.py +569 -0
- pyvale/integratorfactory.py +240 -0
- pyvale/integratorquadrature.py +217 -0
- pyvale/integratorrectangle.py +165 -0
- pyvale/integratorspatial.py +89 -0
- pyvale/integratortype.py +43 -0
- pyvale/output.py +17 -0
- pyvale/pyvaleexceptions.py +11 -0
- pyvale/raster.py +31 -0
- pyvale/rastercy.py +77 -0
- pyvale/rasternp.py +603 -0
- pyvale/rendermesh.py +147 -0
- pyvale/sensorarray.py +178 -0
- pyvale/sensorarrayfactory.py +196 -0
- pyvale/sensorarraypoint.py +278 -0
- pyvale/sensordata.py +71 -0
- pyvale/sensordescriptor.py +213 -0
- pyvale/sensortools.py +142 -0
- pyvale/simcases/case00_HEX20.i +242 -0
- pyvale/simcases/case00_HEX27.i +242 -0
- pyvale/simcases/case00_HEX8.i +242 -0
- pyvale/simcases/case00_TET10.i +242 -0
- pyvale/simcases/case00_TET14.i +242 -0
- pyvale/simcases/case00_TET4.i +242 -0
- pyvale/simcases/case01.i +101 -0
- pyvale/simcases/case02.i +156 -0
- pyvale/simcases/case03.i +136 -0
- pyvale/simcases/case04.i +181 -0
- pyvale/simcases/case05.i +234 -0
- pyvale/simcases/case06.i +305 -0
- pyvale/simcases/case07.geo +135 -0
- pyvale/simcases/case07.i +87 -0
- pyvale/simcases/case08.geo +144 -0
- pyvale/simcases/case08.i +153 -0
- pyvale/simcases/case09.geo +204 -0
- pyvale/simcases/case09.i +87 -0
- pyvale/simcases/case10.geo +204 -0
- pyvale/simcases/case10.i +257 -0
- pyvale/simcases/case11.geo +337 -0
- pyvale/simcases/case11.i +147 -0
- pyvale/simcases/case12.geo +388 -0
- pyvale/simcases/case12.i +329 -0
- pyvale/simcases/case13.i +140 -0
- pyvale/simcases/case14.i +159 -0
- pyvale/simcases/case15.geo +337 -0
- pyvale/simcases/case15.i +150 -0
- pyvale/simcases/case16.geo +391 -0
- pyvale/simcases/case16.i +357 -0
- pyvale/simcases/case17.geo +135 -0
- pyvale/simcases/case17.i +144 -0
- pyvale/simcases/case18.i +254 -0
- pyvale/simcases/case18_1.i +254 -0
- pyvale/simcases/case18_2.i +254 -0
- pyvale/simcases/case18_3.i +254 -0
- pyvale/simcases/case19.geo +252 -0
- pyvale/simcases/case19.i +99 -0
- pyvale/simcases/case20.geo +252 -0
- pyvale/simcases/case20.i +250 -0
- pyvale/simcases/case21.geo +74 -0
- pyvale/simcases/case21.i +155 -0
- pyvale/simcases/case22.geo +82 -0
- pyvale/simcases/case22.i +140 -0
- pyvale/simcases/case23.geo +164 -0
- pyvale/simcases/case23.i +140 -0
- pyvale/simcases/case24.geo +79 -0
- pyvale/simcases/case24.i +123 -0
- pyvale/simcases/case25.geo +82 -0
- pyvale/simcases/case25.i +140 -0
- pyvale/simcases/case26.geo +166 -0
- pyvale/simcases/case26.i +140 -0
- pyvale/simcases/run_1case.py +61 -0
- pyvale/simcases/run_all_cases.py +69 -0
- pyvale/simcases/run_build_case.py +64 -0
- pyvale/simcases/run_example_cases.py +69 -0
- pyvale/simtools.py +67 -0
- pyvale/visualexpplotter.py +191 -0
- pyvale/visualimagedef.py +74 -0
- pyvale/visualimages.py +76 -0
- pyvale/visualopts.py +493 -0
- pyvale/visualsimanimator.py +111 -0
- pyvale/visualsimsensors.py +318 -0
- pyvale/visualtools.py +136 -0
- pyvale/visualtraceplotter.py +142 -0
- pyvale-2025.5.3.dist-info/METADATA +144 -0
- pyvale-2025.5.3.dist-info/RECORD +175 -0
- pyvale-2025.5.3.dist-info/WHEEL +6 -0
- pyvale-2025.5.3.dist-info/licenses/LICENSE +21 -0
- 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
|
+
|