pyvale 2025.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pyvale might be problematic. Click here for more details.

Files changed (157) hide show
  1. pyvale/__init__.py +75 -0
  2. pyvale/core/__init__.py +7 -0
  3. pyvale/core/analyticmeshgen.py +59 -0
  4. pyvale/core/analyticsimdatafactory.py +63 -0
  5. pyvale/core/analyticsimdatagenerator.py +160 -0
  6. pyvale/core/camera.py +146 -0
  7. pyvale/core/cameradata.py +64 -0
  8. pyvale/core/cameradata2d.py +82 -0
  9. pyvale/core/cameratools.py +328 -0
  10. pyvale/core/cython/rastercyth.c +32267 -0
  11. pyvale/core/cython/rastercyth.py +636 -0
  12. pyvale/core/dataset.py +250 -0
  13. pyvale/core/errorcalculator.py +112 -0
  14. pyvale/core/errordriftcalc.py +146 -0
  15. pyvale/core/errorintegrator.py +339 -0
  16. pyvale/core/errorrand.py +614 -0
  17. pyvale/core/errorsysdep.py +331 -0
  18. pyvale/core/errorsysfield.py +407 -0
  19. pyvale/core/errorsysindep.py +905 -0
  20. pyvale/core/experimentsimulator.py +99 -0
  21. pyvale/core/field.py +136 -0
  22. pyvale/core/fieldconverter.py +154 -0
  23. pyvale/core/fieldsampler.py +112 -0
  24. pyvale/core/fieldscalar.py +167 -0
  25. pyvale/core/fieldtensor.py +221 -0
  26. pyvale/core/fieldtransform.py +384 -0
  27. pyvale/core/fieldvector.py +215 -0
  28. pyvale/core/generatorsrandom.py +528 -0
  29. pyvale/core/imagedef2d.py +566 -0
  30. pyvale/core/integratorfactory.py +241 -0
  31. pyvale/core/integratorquadrature.py +192 -0
  32. pyvale/core/integratorrectangle.py +88 -0
  33. pyvale/core/integratorspatial.py +90 -0
  34. pyvale/core/integratortype.py +44 -0
  35. pyvale/core/optimcheckfuncs.py +153 -0
  36. pyvale/core/raster.py +31 -0
  37. pyvale/core/rastercy.py +76 -0
  38. pyvale/core/rasternp.py +604 -0
  39. pyvale/core/rendermesh.py +156 -0
  40. pyvale/core/sensorarray.py +179 -0
  41. pyvale/core/sensorarrayfactory.py +210 -0
  42. pyvale/core/sensorarraypoint.py +280 -0
  43. pyvale/core/sensordata.py +72 -0
  44. pyvale/core/sensordescriptor.py +101 -0
  45. pyvale/core/sensortools.py +143 -0
  46. pyvale/core/visualexpplotter.py +151 -0
  47. pyvale/core/visualimagedef.py +71 -0
  48. pyvale/core/visualimages.py +75 -0
  49. pyvale/core/visualopts.py +180 -0
  50. pyvale/core/visualsimanimator.py +83 -0
  51. pyvale/core/visualsimplotter.py +182 -0
  52. pyvale/core/visualtools.py +81 -0
  53. pyvale/core/visualtraceplotter.py +256 -0
  54. pyvale/data/__init__.py +7 -0
  55. pyvale/data/case13_out.e +0 -0
  56. pyvale/data/case16_out.e +0 -0
  57. pyvale/data/case17_out.e +0 -0
  58. pyvale/data/case18_1_out.e +0 -0
  59. pyvale/data/case18_2_out.e +0 -0
  60. pyvale/data/case18_3_out.e +0 -0
  61. pyvale/data/case25_out.e +0 -0
  62. pyvale/data/case26_out.e +0 -0
  63. pyvale/data/optspeckle_2464x2056px_spec5px_8bit_gblur1px.tiff +0 -0
  64. pyvale/examples/__init__.py +7 -0
  65. pyvale/examples/analyticdatagen/__init__.py +7 -0
  66. pyvale/examples/analyticdatagen/ex1_1_scalarvisualisation.py +38 -0
  67. pyvale/examples/analyticdatagen/ex1_2_scalarcasebuild.py +46 -0
  68. pyvale/examples/analyticdatagen/ex2_1_analyticsensors.py +83 -0
  69. pyvale/examples/ex1_1_thermal2d.py +89 -0
  70. pyvale/examples/ex1_2_thermal2d.py +111 -0
  71. pyvale/examples/ex1_3_thermal2d.py +113 -0
  72. pyvale/examples/ex1_4_thermal2d.py +89 -0
  73. pyvale/examples/ex1_5_thermal2d.py +105 -0
  74. pyvale/examples/ex2_1_thermal3d .py +87 -0
  75. pyvale/examples/ex2_2_thermal3d.py +51 -0
  76. pyvale/examples/ex2_3_thermal3d.py +109 -0
  77. pyvale/examples/ex3_1_displacement2d.py +47 -0
  78. pyvale/examples/ex3_2_displacement2d.py +79 -0
  79. pyvale/examples/ex3_3_displacement2d.py +104 -0
  80. pyvale/examples/ex3_4_displacement2d.py +105 -0
  81. pyvale/examples/ex4_1_strain2d.py +57 -0
  82. pyvale/examples/ex4_2_strain2d.py +79 -0
  83. pyvale/examples/ex4_3_strain2d.py +100 -0
  84. pyvale/examples/ex5_1_multiphysics2d.py +78 -0
  85. pyvale/examples/ex6_1_multiphysics2d_expsim.py +118 -0
  86. pyvale/examples/ex6_2_multiphysics3d_expsim.py +158 -0
  87. pyvale/examples/features/__init__.py +7 -0
  88. pyvale/examples/features/ex_animation_tools_3dmonoblock.py +83 -0
  89. pyvale/examples/features/ex_area_avg.py +89 -0
  90. pyvale/examples/features/ex_calibration_error.py +108 -0
  91. pyvale/examples/features/ex_chain_field_errs.py +141 -0
  92. pyvale/examples/features/ex_field_errs.py +78 -0
  93. pyvale/examples/features/ex_sensor_single_angle_batch.py +110 -0
  94. pyvale/examples/imagedef2d/ex_imagedef2d_todisk.py +86 -0
  95. pyvale/examples/rasterisation/ex_rastenp.py +154 -0
  96. pyvale/examples/rasterisation/ex_rastercyth_oneframe.py +220 -0
  97. pyvale/examples/rasterisation/ex_rastercyth_static_cypara.py +194 -0
  98. pyvale/examples/rasterisation/ex_rastercyth_static_pypara.py +193 -0
  99. pyvale/simcases/case00_HEX20.i +242 -0
  100. pyvale/simcases/case00_HEX27.i +242 -0
  101. pyvale/simcases/case00_TET10.i +242 -0
  102. pyvale/simcases/case00_TET14.i +242 -0
  103. pyvale/simcases/case01.i +101 -0
  104. pyvale/simcases/case02.i +156 -0
  105. pyvale/simcases/case03.i +136 -0
  106. pyvale/simcases/case04.i +181 -0
  107. pyvale/simcases/case05.i +234 -0
  108. pyvale/simcases/case06.i +305 -0
  109. pyvale/simcases/case07.geo +135 -0
  110. pyvale/simcases/case07.i +87 -0
  111. pyvale/simcases/case08.geo +144 -0
  112. pyvale/simcases/case08.i +153 -0
  113. pyvale/simcases/case09.geo +204 -0
  114. pyvale/simcases/case09.i +87 -0
  115. pyvale/simcases/case10.geo +204 -0
  116. pyvale/simcases/case10.i +257 -0
  117. pyvale/simcases/case11.geo +337 -0
  118. pyvale/simcases/case11.i +147 -0
  119. pyvale/simcases/case12.geo +388 -0
  120. pyvale/simcases/case12.i +329 -0
  121. pyvale/simcases/case13.i +140 -0
  122. pyvale/simcases/case14.i +159 -0
  123. pyvale/simcases/case15.geo +337 -0
  124. pyvale/simcases/case15.i +150 -0
  125. pyvale/simcases/case16.geo +391 -0
  126. pyvale/simcases/case16.i +357 -0
  127. pyvale/simcases/case17.geo +135 -0
  128. pyvale/simcases/case17.i +144 -0
  129. pyvale/simcases/case18.i +254 -0
  130. pyvale/simcases/case18_1.i +254 -0
  131. pyvale/simcases/case18_2.i +254 -0
  132. pyvale/simcases/case18_3.i +254 -0
  133. pyvale/simcases/case19.geo +252 -0
  134. pyvale/simcases/case19.i +99 -0
  135. pyvale/simcases/case20.geo +252 -0
  136. pyvale/simcases/case20.i +250 -0
  137. pyvale/simcases/case21.geo +74 -0
  138. pyvale/simcases/case21.i +155 -0
  139. pyvale/simcases/case22.geo +82 -0
  140. pyvale/simcases/case22.i +140 -0
  141. pyvale/simcases/case23.geo +164 -0
  142. pyvale/simcases/case23.i +140 -0
  143. pyvale/simcases/case24.geo +79 -0
  144. pyvale/simcases/case24.i +123 -0
  145. pyvale/simcases/case25.geo +82 -0
  146. pyvale/simcases/case25.i +140 -0
  147. pyvale/simcases/case26.geo +166 -0
  148. pyvale/simcases/case26.i +140 -0
  149. pyvale/simcases/run_1case.py +61 -0
  150. pyvale/simcases/run_all_cases.py +69 -0
  151. pyvale/simcases/run_build_case.py +64 -0
  152. pyvale/simcases/run_example_cases.py +69 -0
  153. pyvale-2025.4.0.dist-info/METADATA +140 -0
  154. pyvale-2025.4.0.dist-info/RECORD +157 -0
  155. pyvale-2025.4.0.dist-info/WHEEL +5 -0
  156. pyvale-2025.4.0.dist-info/licenses/LICENSE +21 -0
  157. pyvale-2025.4.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,151 @@
1
+ """
2
+ ================================================================================
3
+ pyvale: the python validation engine
4
+ License: MIT
5
+ Copyright (C) 2025 The Computer Aided Validation Team
6
+ ================================================================================
7
+ """
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+ import matplotlib.pyplot as plt
12
+ from pyvale.core.visualopts import (PlotOptsGeneral,
13
+ TraceOptsExperiment)
14
+ from pyvale.core.experimentsimulator import ExperimentSimulator
15
+
16
+ #TODO: Docstrings
17
+
18
+ def plot_exp_traces(exp_sim: ExperimentSimulator,
19
+ component: str,
20
+ sens_array_num: int,
21
+ sim_num: int,
22
+ trace_opts: TraceOptsExperiment | None = None,
23
+ plot_opts: PlotOptsGeneral | None = None) -> tuple[Any,Any]:
24
+
25
+ if trace_opts is None:
26
+ trace_opts = TraceOptsExperiment()
27
+
28
+ if plot_opts is None:
29
+ plot_opts = PlotOptsGeneral()
30
+
31
+ descriptor = exp_sim.sensor_arrays[sens_array_num].descriptor
32
+ comp_ind = exp_sim.sensor_arrays[sens_array_num].field.get_component_index(component)
33
+ samp_time = exp_sim.sensor_arrays[sens_array_num].get_sample_times()
34
+ num_sens = exp_sim.sensor_arrays[sens_array_num].get_measurement_shape()[0]
35
+
36
+ exp_data = exp_sim.get_data()
37
+ exp_stats = exp_sim.get_stats()
38
+
39
+ if trace_opts.sensors_to_plot is None:
40
+ sensors_to_plot = range(num_sens)
41
+ else:
42
+ sensors_to_plot = trace_opts.sensors_to_plot
43
+
44
+ #---------------------------------------------------------------------------
45
+ # Figure canvas setup
46
+ fig, ax = plt.subplots(figsize=plot_opts.single_fig_size_portrait,
47
+ layout='constrained')
48
+ fig.set_dpi(plot_opts.resolution)
49
+
50
+ #---------------------------------------------------------------------------
51
+ # Plot all simulated experimental points
52
+ if trace_opts.plot_all_exp_points:
53
+ for ss in sensors_to_plot:
54
+ for ee in range(exp_sim.num_exp_per_sim):
55
+ ax.plot(samp_time,
56
+ exp_data[sens_array_num][sim_num,ee,ss,comp_ind,:],
57
+ "+",
58
+ lw=plot_opts.lw,
59
+ ms=plot_opts.ms,
60
+ color=plot_opts.colors[ss % plot_opts.n_colors])
61
+
62
+ for ss in sensors_to_plot:
63
+ if trace_opts.centre == "median":
64
+ ax.plot(samp_time,
65
+ exp_stats[sens_array_num].median[sim_num,ss,comp_ind,:],
66
+ trace_opts.exp_mean_line,
67
+ lw=plot_opts.lw,
68
+ ms=plot_opts.ms,
69
+ color=plot_opts.colors[ss % plot_opts.n_colors])
70
+ else:
71
+ ax.plot(samp_time,
72
+ exp_stats[sens_array_num].mean[sim_num,ss,comp_ind,:],
73
+ trace_opts.exp_mean_line,
74
+ lw=plot_opts.lw,
75
+ ms=plot_opts.ms,
76
+ color=plot_opts.colors[ss % plot_opts.n_colors])
77
+
78
+ if trace_opts is not None:
79
+ upper = np.zeros_like(exp_stats[sens_array_num].min)
80
+ lower = np.zeros_like(exp_stats[sens_array_num].min)
81
+
82
+ if trace_opts.fill_between == 'max':
83
+ upper = exp_stats[sens_array_num].min
84
+ lower = exp_stats[sens_array_num].max
85
+
86
+ elif trace_opts.fill_between == 'quartile':
87
+ upper = exp_stats[sens_array_num].q25
88
+ lower = exp_stats[sens_array_num].q75
89
+
90
+ elif trace_opts.fill_between == '2std':
91
+ upper = exp_stats[sens_array_num].mean + \
92
+ 2*exp_stats[sens_array_num].std
93
+ lower = exp_stats[sens_array_num].mean - \
94
+ 2*exp_stats[sens_array_num].std
95
+
96
+ elif trace_opts.fill_between == '3std':
97
+ upper = exp_stats[sens_array_num].mean + \
98
+ 3*exp_stats[sens_array_num].std
99
+ lower = exp_stats[sens_array_num].mean - \
100
+ 3*exp_stats[sens_array_num].std
101
+
102
+ ax.fill_between(samp_time,
103
+ upper[sim_num,ss,comp_ind,:],
104
+ lower[sim_num,ss,comp_ind,:],
105
+ color=plot_opts.colors[ss % plot_opts.n_colors],
106
+ alpha=0.2)
107
+
108
+ #---------------------------------------------------------------------------
109
+ # Plot simulation and truth line
110
+ if trace_opts.sim_line is not None:
111
+ sim_time = exp_sim.sensor_arrays[sens_array_num].field.get_time_steps()
112
+ sim_vals = exp_sim.sensor_arrays[sens_array_num].field.sample_field(
113
+ exp_sim.sensor_arrays[sens_array_num].positions)
114
+
115
+ for ss in sensors_to_plot:
116
+ ax.plot(sim_time,
117
+ sim_vals[ss,comp_ind,:],
118
+ trace_opts.sim_line,
119
+ lw=plot_opts.lw,
120
+ ms=plot_opts.ms)
121
+
122
+ if trace_opts.truth_line is not None:
123
+ truth = exp_sim.sensor_arrays[sens_array_num].get_truth()
124
+ for ss in sensors_to_plot:
125
+ ax.plot(samp_time,
126
+ truth[ss,comp_ind,:],
127
+ trace_opts.truth_line,
128
+ lw=plot_opts.lw,
129
+ ms=plot_opts.ms,
130
+ color=plot_opts.colors[ss % plot_opts.n_colors])
131
+
132
+ #---------------------------------------------------------------------------
133
+ # Axis / legend labels and options
134
+ ax.set_xlabel(trace_opts.time_label,
135
+ fontsize=plot_opts.font_ax_size, fontname=plot_opts.font_name)
136
+ ax.set_ylabel(descriptor.create_label(comp_ind),
137
+ fontsize=plot_opts.font_ax_size, fontname=plot_opts.font_name)
138
+
139
+ if trace_opts.time_min_max is None:
140
+ ax.set_xlim((np.min(samp_time),np.max(samp_time))) # type: ignore
141
+ else:
142
+ ax.set_xlim(trace_opts.time_min_max)
143
+
144
+ trace_opts.legend = False
145
+ if trace_opts.legend:
146
+ ax.legend(prop={"size":plot_opts.font_leg_size},loc='best')
147
+
148
+ plt.grid(True)
149
+ plt.draw()
150
+
151
+ return (fig,ax)
@@ -0,0 +1,71 @@
1
+ """
2
+ ================================================================================
3
+ pyvale: the python validation engine
4
+ License: MIT
5
+ Copyright (C) 2025 The Computer Aided Validation Team
6
+ ================================================================================
7
+ """
8
+ import numpy as np
9
+ import matplotlib.pyplot as plt
10
+
11
+ class ImageDefDiags:
12
+ @staticmethod
13
+ def plot_speckle_image(image: np.ndarray,
14
+ title: str = "",
15
+ cmap: str = "gray") -> None:
16
+ fig, ax = plt.subplots()
17
+ cset = plt.imshow(image,cmap=plt.get_cmap(cmap),origin='lower')
18
+ ax.set_aspect('equal','box')
19
+ ax.set_title(title,fontsize=12)
20
+ fig.colorbar(cset)
21
+
22
+ @staticmethod
23
+ def plot_vector_image(image: np.ndarray,
24
+ title: str = "",
25
+ cmap: str = "plasma") -> None:
26
+ fig, ax = plt.subplots()
27
+ cset = plt.imshow(image,cmap=plt.get_cmap(cmap),origin='lower')
28
+ ax.set_aspect('equal','box')
29
+ ax.set_title(title,fontsize=12)
30
+ fig.colorbar(cset)
31
+
32
+
33
+ @staticmethod
34
+ def plot_image_xy(image: np.ndarray,
35
+ extent: tuple[float,float,float,float],
36
+ title: str = "",
37
+ cmap: str = "plasma",
38
+ ax_units: str = "mm") -> None:
39
+
40
+ fig, ax = plt.subplots()
41
+ cset = plt.imshow(image,
42
+ aspect='auto',interpolation='none',
43
+ origin='lower',cmap=plt.get_cmap(cmap),
44
+ extent=extent)
45
+ ax.set_aspect('equal','box')
46
+ ax.set_title(title,fontsize=12)
47
+ ax.set_xlabel(f'x [{ax_units}]',fontsize=12)
48
+ ax.set_ylabel(f'y [{ax_units}]',fontsize=12)
49
+ fig.colorbar(cset)
50
+
51
+ @staticmethod
52
+ def plot_all_diags(def_image: np.ndarray,
53
+ def_mask: np.ndarray | None,
54
+ def_image_subpx: np.ndarray,
55
+ subpx_disp_x: np.ndarray,
56
+ subpx_disp_y: np.ndarray,
57
+ subpx_grid_xm: np.ndarray,
58
+ subpx_grid_ym: np.ndarray) -> None:
59
+ image_map = "gray"
60
+ vector_map = "plasma"
61
+
62
+ if def_mask is not None:
63
+ ImageDefDiags.plot_image('Def. Mask',def_mask,image_map)
64
+
65
+ ImageDefDiags.plot_image('Subpx Def. Image',def_image_subpx,image_map)
66
+ ImageDefDiags.plot_image('Def. Image',def_image,image_map)
67
+
68
+ ext = tuple(np.array([subpx_grid_xm.min(),subpx_grid_xm.max(),
69
+ subpx_grid_ym.min(),subpx_grid_ym.max()])*10**3)
70
+ ImageDefDiags.plot_image_xy('Sub Pixel Disp X',subpx_disp_x,ext,V_CMAP)
71
+ ImageDefDiags.plot_image_xy('Sub Pixel Disp Y',subpx_disp_y,ext,V_CMAP)
@@ -0,0 +1,75 @@
1
+ """
2
+ ================================================================================
3
+ pyvale: the python validation engine
4
+ License: MIT
5
+ Copyright (C) 2025 The Computer Aided Validation Team
6
+ ================================================================================
7
+ """
8
+ from typing import Any
9
+ import numpy as np
10
+ import matplotlib.pyplot as plt
11
+ from pyvale.core.camera import CameraBasic2D
12
+ from pyvale.core.visualopts import PlotOptsGeneral
13
+
14
+ # NOTE: This module is a feature under developement.
15
+
16
+ # TODO: this only works for a 2D camera, maybe this should be deprecated
17
+ def plot_measurement_image(camera: CameraBasic2D,
18
+ component: str,
19
+ time_step: int = -1,
20
+ plot_opts: PlotOptsGeneral | None = None
21
+ ) -> tuple[Any,Any]:
22
+
23
+ if plot_opts is None:
24
+ plot_opts = PlotOptsGeneral()
25
+
26
+ comp_ind = camera.get_field().get_component_index(component)
27
+ meas_image = camera.get_measurement_images()[:,:,comp_ind,time_step]
28
+ descriptor = camera.get_descriptor()
29
+
30
+ (fig, ax) = plt.subplots(figsize=plot_opts.single_fig_size_square,
31
+ layout='constrained')
32
+ fig.set_dpi(plot_opts.resolution)
33
+
34
+ cset = plt.imshow(meas_image,
35
+ cmap=plt.get_cmap(plot_opts.cmap_seq),
36
+ origin='lower')
37
+ ax.set_aspect('equal','box')
38
+
39
+ fig.colorbar(cset,
40
+ label=descriptor.create_label_flat(comp_ind))
41
+
42
+ title = f"Time: {camera.get_sample_times()[time_step]}s"
43
+ ax.set_title(title,fontsize=plot_opts.font_head_size)
44
+ ax.set_xlabel(r"x ($px$)",
45
+ fontsize=plot_opts.font_ax_size, fontname=plot_opts.font_name)
46
+ ax.set_ylabel(r"y ($px$)",
47
+ fontsize=plot_opts.font_ax_size, fontname=plot_opts.font_name)
48
+
49
+ return (fig,ax)
50
+
51
+
52
+ def plot_field_image(image: np.ndarray,
53
+ title_str: str | None = None,
54
+ plot_opts: PlotOptsGeneral | None = None
55
+ ) -> tuple[Any,Any]:
56
+
57
+ if plot_opts is None:
58
+ plot_opts = PlotOptsGeneral()
59
+
60
+ (fig, ax) = plt.subplots(figsize=plot_opts.single_fig_size_square,
61
+ layout='constrained')
62
+ fig.set_dpi(plot_opts.resolution)
63
+ cset = plt.imshow(image,
64
+ cmap=plt.get_cmap(plot_opts.cmap_seq))
65
+ #origin='lower')
66
+ ax.set_aspect('equal','box')
67
+ fig.colorbar(cset)
68
+ if title_str is not None:
69
+ ax.set_title(title_str,fontsize=plot_opts.font_head_size)
70
+ ax.set_xlabel(r"x ($px$)",
71
+ fontsize=plot_opts.font_ax_size, fontname=plot_opts.font_name)
72
+ ax.set_ylabel(r"y ($px$)",
73
+ fontsize=plot_opts.font_ax_size, fontname=plot_opts.font_name)
74
+
75
+ return (fig,ax)
@@ -0,0 +1,180 @@
1
+ """
2
+ ================================================================================
3
+ pyvale: the python validation engine
4
+ License: MIT
5
+ Copyright (C) 2025 The Computer Aided Validation Team
6
+ ================================================================================
7
+ """
8
+ from pathlib import Path
9
+ import enum
10
+ from dataclasses import dataclass
11
+ import numpy as np
12
+ import matplotlib as plt
13
+
14
+
15
+ @dataclass(slots=True)
16
+ class PlotOptsGeneral:
17
+ """ Dataclass for controlling the properties of figures and graphs such as
18
+ figure size, resolution, font sizes, marker sizes, line widths and
19
+ colormaps.
20
+ """
21
+ aspect_ratio: float = 1.62
22
+ # These are in inches because of matplotlib
23
+ a4_width: float = 8.25
24
+ a4_height: float = 11.75
25
+ a4_margin_width: float = 0.5
26
+ a4_margin_height: float = 0.5
27
+ a4_print_width: float = a4_width-2*a4_margin_width
28
+ a4_print_height: float = a4_height-2*a4_margin_height
29
+
30
+ single_fig_scale: float = 0.5
31
+
32
+ single_fig_size_square: tuple[float,float] = (
33
+ a4_print_width*single_fig_scale,
34
+ a4_print_width*single_fig_scale
35
+ )
36
+ single_fig_size_portrait: tuple[float,float] = (
37
+ a4_print_width*single_fig_scale/aspect_ratio,
38
+ a4_print_width*single_fig_scale
39
+ )
40
+ single_fig_size_landscape: tuple[float,float] = (
41
+ a4_print_width*single_fig_scale,
42
+ a4_print_width*single_fig_scale/aspect_ratio
43
+ )
44
+
45
+ resolution: float = 300.0
46
+
47
+ font_name: str = 'Liberation Sans'
48
+ font_def_weight: str = 'normal'
49
+ font_def_size: float = 8.0
50
+ font_tick_size: float = 8.0
51
+ font_head_size: float = 9.0
52
+ font_ax_size: float = 8.0
53
+ font_leg_size: float = 8.0
54
+
55
+ ms: float = 3.2
56
+ lw: float = 0.8
57
+
58
+ cmap_seq: str = "cividis"
59
+ cmap_div: str = "RdBu"
60
+
61
+ colors = plt.rcParams['axes.prop_cycle'].by_key()['color']
62
+ n_colors = len(plt.rcParams['axes.prop_cycle'].by_key()['color'])
63
+
64
+ def __post_init__(self) -> None:
65
+ plt.rc('font', size=self.font_def_size)
66
+ plt.rc('axes', titlesize=self.font_def_size)
67
+
68
+
69
+ @dataclass(slots=True)
70
+ class TraceOptsSensor:
71
+ """Dataclass for controlling the properties of sensor trace plots.
72
+ """
73
+ legend: bool = True
74
+
75
+ x_label: str = r"x [$m$]"
76
+ y_label: str = r"y [$m$]"
77
+ z_label: str = r"z [$m$]"
78
+ time_label: str = r"Time, $t$ [$s$]"
79
+
80
+ truth_line: str | None = "-"
81
+ sim_line: str | None = "-"
82
+ meas_line: str = "--o"
83
+
84
+ sensors_to_plot: np.ndarray | None = None
85
+ time_min_max: tuple[float,float] | None = None
86
+
87
+
88
+ @dataclass(slots=True)
89
+ class TraceOptsExperiment:
90
+ """Dataclass for contorlling the properties of sensor trace plots from
91
+ batches of simulated experiments.
92
+ """
93
+ legend: bool = True
94
+
95
+ x_label: str = r"x [$m$]"
96
+ y_label: str = r"y [$m$]"
97
+ z_label: str = r"z [$m$]"
98
+ time_label: str = r"Time, $t$ [$s$]"
99
+
100
+ truth_line: str | None = None
101
+ sim_line: str | None = None
102
+ exp_mean_line: str = "-"
103
+ exp_marker_line: str = "+"
104
+
105
+ sensors_to_plot: np.ndarray | None = None
106
+ time_min_max: tuple[float,float] | None = None
107
+
108
+ centre: str = "mean"
109
+ plot_all_exp_points: bool = False
110
+ fill_between: str | None = "3std"
111
+
112
+
113
+ @dataclass(slots=True)
114
+ class VisOptsSimSensors:
115
+ """Dataclass for controlling displays of the simulation mesh and sensor
116
+ locations using pyvista.
117
+ """
118
+ # pyvista ops
119
+ window_size_px: tuple[int,int] = (1280,800)
120
+ camera_position: np.ndarray | str = "xy"
121
+ show_edges: bool = True
122
+ interactive: bool = True
123
+
124
+ font_colour: str = "black"
125
+ background_colour: str = "white" # "white"
126
+
127
+ time_label_font_size: float = 12
128
+ time_label_position: str = "upper_left"
129
+ time_label_show: bool = True
130
+
131
+ colour_bar_font_size: float = 18
132
+ colour_bar_show: bool = True
133
+ colour_bar_lims: tuple[float,float] | None = None
134
+ colour_bar_vertical: bool = True
135
+
136
+ # pyvale ops
137
+ show_perturbed_pos: bool = True
138
+ sens_colour_nom: str = "red"
139
+ sens_colour_pert: str = "blue"
140
+ sens_point_size: float = 20
141
+ sens_label_font_size: float = 30
142
+ sens_label_colour: str = "grey"
143
+
144
+
145
+ class EImageType(enum.Enum):
146
+ PNG = enum.auto()
147
+ SVG = enum.auto()
148
+
149
+ @dataclass(slots=True)
150
+ class VisOptsImageSave:
151
+ """Dataclass for image save options.
152
+ """
153
+ path: Path | None = None
154
+ image_type: EImageType = EImageType.PNG
155
+ transparent_background: bool = False
156
+
157
+
158
+ class EAnimationType(enum.Enum):
159
+ MP4 = enum.auto()
160
+ GIF = enum.auto()
161
+
162
+ @dataclass(slots=True)
163
+ class VisOptsAnimation:
164
+ """Dataclass for animation save options.
165
+ """
166
+ frames_per_second: float = 10.0
167
+ off_screen: bool = False
168
+
169
+ # save options
170
+ save_animation: EAnimationType | None = None
171
+ save_path: Path | None = None
172
+
173
+
174
+
175
+
176
+
177
+
178
+
179
+
180
+
@@ -0,0 +1,83 @@
1
+ """
2
+ ================================================================================
3
+ pyvale: the python validation engine
4
+ License: MIT
5
+ Copyright (C) 2025 The Computer Aided Validation Team
6
+ ================================================================================
7
+ """
8
+ import numpy as np
9
+ #import vtk #NOTE: has to be here to fix latex bug in pyvista/vtk
10
+ # See: https://github.com/pyvista/pyvista/discussions/2928
11
+ #NOTE: causes output to console to be suppressed unfortunately
12
+ import pyvista as pv
13
+ from pyvale.core.sensorarraypoint import SensorArrayPoint
14
+ from pyvale.core.visualopts import VisOptsSimSensors, VisOptsAnimation
15
+ from pyvale.core.visualtools import (create_pv_plotter,
16
+ get_colour_lims,
17
+ set_animation_writer)
18
+ from pyvale.core.visualsimplotter import (add_sensor_points_nom,
19
+ add_sensor_points_pert,
20
+ add_sim_field)
21
+
22
+ #TODO: Docstrings
23
+
24
+ def animate_sim_with_sensors(sensor_array: SensorArrayPoint,
25
+ component: str,
26
+ time_steps: np.ndarray | None = None,
27
+ vis_opts: VisOptsSimSensors | None = None,
28
+ anim_opts: VisOptsAnimation | None = None,
29
+ ) -> pv.Plotter:
30
+
31
+ if vis_opts is None:
32
+ vis_opts = VisOptsSimSensors()
33
+
34
+ if anim_opts is None:
35
+ anim_opts = VisOptsAnimation()
36
+
37
+ if time_steps is None:
38
+ time_steps = np.arange(0,sensor_array.get_sample_times().shape[0])
39
+
40
+ sim_data = sensor_array.field.get_sim_data()
41
+ vis_opts.colour_bar_lims = get_colour_lims(
42
+ sim_data.node_vars[component][:,time_steps],
43
+ vis_opts.colour_bar_lims)
44
+
45
+ #---------------------------------------------------------------------------
46
+ pv_plot = create_pv_plotter(vis_opts)
47
+
48
+ pv_plot = add_sensor_points_pert(pv_plot,sensor_array,vis_opts)
49
+ pv_plot = add_sensor_points_nom(pv_plot,sensor_array,vis_opts)
50
+ (pv_plot,sim_vis) = add_sim_field(pv_plot,
51
+ sensor_array,
52
+ component,
53
+ time_step = 0,
54
+ vis_opts = vis_opts)
55
+
56
+ pv_plot.camera_position = vis_opts.camera_position
57
+ pv_plot.show(auto_close=False,interactive=False)
58
+
59
+ pv_plot = set_animation_writer(pv_plot,anim_opts)
60
+
61
+ #---------------------------------------------------------------------------
62
+ for tt in time_steps:
63
+ # Updates the field plotted on the mesh
64
+ sim_vis[component] = sim_data.node_vars[component][:,tt]
65
+
66
+ if vis_opts.time_label_show:
67
+ pv_plot.add_text(f"Time: {sim_data.time[tt]} " + \
68
+ f"{sensor_array.descriptor.time_units}",
69
+ position=vis_opts.time_label_position,
70
+ font_size=vis_opts.time_label_font_size,
71
+ name='time-label')
72
+
73
+ if anim_opts.save_animation is not None:
74
+ pv_plot.write_frame()
75
+
76
+ pv_plot.show(auto_close=False,interactive=vis_opts.interactive)
77
+ return pv_plot
78
+
79
+
80
+
81
+
82
+
83
+