pyvale 2025.4.1__py3-none-any.whl → 2025.5.1__py3-none-any.whl

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

Potentially problematic release.


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

Files changed (126) hide show
  1. pyvale/__init__.py +18 -3
  2. pyvale/analyticmeshgen.py +1 -0
  3. pyvale/analyticsimdatafactory.py +18 -13
  4. pyvale/analyticsimdatagenerator.py +105 -72
  5. pyvale/blendercalibrationdata.py +15 -0
  6. pyvale/blenderlightdata.py +26 -0
  7. pyvale/blendermaterialdata.py +15 -0
  8. pyvale/blenderrenderdata.py +30 -0
  9. pyvale/blenderscene.py +488 -0
  10. pyvale/blendertools.py +420 -0
  11. pyvale/camera.py +6 -5
  12. pyvale/cameradata.py +25 -7
  13. pyvale/cameradata2d.py +6 -4
  14. pyvale/camerastereo.py +217 -0
  15. pyvale/cameratools.py +206 -11
  16. pyvale/cython/rastercyth.py +6 -2
  17. pyvale/data/cal_target.tiff +0 -0
  18. pyvale/dataset.py +73 -14
  19. pyvale/errorcalculator.py +8 -10
  20. pyvale/errordriftcalc.py +10 -9
  21. pyvale/errorintegrator.py +19 -21
  22. pyvale/errorrand.py +33 -39
  23. pyvale/errorsyscalib.py +134 -0
  24. pyvale/errorsysdep.py +19 -22
  25. pyvale/errorsysfield.py +49 -41
  26. pyvale/errorsysindep.py +79 -175
  27. pyvale/examples/basics/ex1_1_basicscalars_therm2d.py +131 -0
  28. pyvale/examples/basics/ex1_2_sensormodel_therm2d.py +158 -0
  29. pyvale/examples/basics/ex1_3_customsens_therm3d.py +216 -0
  30. pyvale/examples/basics/ex1_4_basicerrors_therm3d.py +153 -0
  31. pyvale/examples/basics/ex1_5_fielderrs_therm3d.py +168 -0
  32. pyvale/examples/basics/ex1_6_caliberrs_therm2d.py +133 -0
  33. pyvale/examples/basics/ex1_7_spatavg_therm2d.py +123 -0
  34. pyvale/examples/basics/ex2_1_basicvectors_disp2d.py +112 -0
  35. pyvale/examples/basics/ex2_2_vectorsens_disp2d.py +111 -0
  36. pyvale/examples/basics/ex2_3_sensangle_disp2d.py +139 -0
  37. pyvale/examples/basics/ex2_4_chainfielderrs_disp2d.py +196 -0
  38. pyvale/examples/basics/ex2_5_vectorfields3d_disp3d.py +109 -0
  39. pyvale/examples/basics/ex3_1_basictensors_strain2d.py +114 -0
  40. pyvale/examples/basics/ex3_2_tensorsens2d_strain2d.py +111 -0
  41. pyvale/examples/basics/ex3_3_tensorsens3d_strain3d.py +182 -0
  42. pyvale/examples/basics/ex4_1_expsim2d_thermmech2d.py +171 -0
  43. pyvale/examples/basics/ex4_2_expsim3d_thermmech3d.py +252 -0
  44. pyvale/examples/{analyticdatagen → genanalyticdata}/ex1_1_scalarvisualisation.py +6 -9
  45. pyvale/examples/{analyticdatagen → genanalyticdata}/ex1_2_scalarcasebuild.py +8 -11
  46. pyvale/examples/{analyticdatagen → genanalyticdata}/ex2_1_analyticsensors.py +9 -12
  47. pyvale/examples/imagedef2d/ex_imagedef2d_todisk.py +8 -15
  48. pyvale/examples/renderblender/ex1_1_blenderscene.py +121 -0
  49. pyvale/examples/renderblender/ex1_2_blenderdeformed.py +119 -0
  50. pyvale/examples/renderblender/ex2_1_stereoscene.py +128 -0
  51. pyvale/examples/renderblender/ex2_2_stereodeformed.py +131 -0
  52. pyvale/examples/renderblender/ex3_1_blendercalibration.py +120 -0
  53. pyvale/examples/{rasterisation → renderrasterisation}/ex_rastenp.py +3 -2
  54. pyvale/examples/{rasterisation → renderrasterisation}/ex_rastercyth_oneframe.py +2 -2
  55. pyvale/examples/{rasterisation → renderrasterisation}/ex_rastercyth_static_cypara.py +3 -8
  56. pyvale/examples/{rasterisation → renderrasterisation}/ex_rastercyth_static_pypara.py +6 -7
  57. pyvale/examples/{ex1_4_thermal2d.py → visualisation/ex1_1_plot_traces.py} +32 -16
  58. pyvale/examples/{features/ex_animation_tools_3dmonoblock.py → visualisation/ex2_1_animate_sim.py} +37 -31
  59. pyvale/experimentsimulator.py +107 -30
  60. pyvale/field.py +2 -9
  61. pyvale/fieldconverter.py +98 -22
  62. pyvale/fieldsampler.py +2 -2
  63. pyvale/fieldscalar.py +10 -10
  64. pyvale/fieldtensor.py +15 -17
  65. pyvale/fieldtransform.py +7 -2
  66. pyvale/fieldvector.py +6 -7
  67. pyvale/generatorsrandom.py +25 -47
  68. pyvale/imagedef2d.py +6 -2
  69. pyvale/integratorfactory.py +2 -2
  70. pyvale/integratorquadrature.py +50 -24
  71. pyvale/integratorrectangle.py +85 -7
  72. pyvale/integratorspatial.py +4 -4
  73. pyvale/integratortype.py +3 -3
  74. pyvale/output.py +17 -0
  75. pyvale/pyvaleexceptions.py +11 -0
  76. pyvale/raster.py +6 -5
  77. pyvale/rastercy.py +6 -4
  78. pyvale/rasternp.py +6 -4
  79. pyvale/rendermesh.py +6 -2
  80. pyvale/sensorarray.py +2 -2
  81. pyvale/sensorarrayfactory.py +52 -65
  82. pyvale/sensorarraypoint.py +29 -30
  83. pyvale/sensordata.py +2 -2
  84. pyvale/sensordescriptor.py +138 -25
  85. pyvale/sensortools.py +3 -3
  86. pyvale/simtools.py +67 -0
  87. pyvale/visualexpplotter.py +99 -57
  88. pyvale/visualimagedef.py +11 -7
  89. pyvale/visualimages.py +6 -4
  90. pyvale/visualopts.py +372 -58
  91. pyvale/visualsimanimator.py +42 -13
  92. pyvale/visualsimsensors.py +318 -0
  93. pyvale/visualtools.py +69 -13
  94. pyvale/visualtraceplotter.py +52 -165
  95. {pyvale-2025.4.1.dist-info → pyvale-2025.5.1.dist-info}/METADATA +17 -14
  96. pyvale-2025.5.1.dist-info/RECORD +172 -0
  97. {pyvale-2025.4.1.dist-info → pyvale-2025.5.1.dist-info}/WHEEL +1 -1
  98. pyvale/examples/analyticdatagen/__init__.py +0 -5
  99. pyvale/examples/ex1_1_thermal2d.py +0 -86
  100. pyvale/examples/ex1_2_thermal2d.py +0 -108
  101. pyvale/examples/ex1_3_thermal2d.py +0 -110
  102. pyvale/examples/ex1_5_thermal2d.py +0 -102
  103. pyvale/examples/ex2_1_thermal3d .py +0 -84
  104. pyvale/examples/ex2_2_thermal3d.py +0 -51
  105. pyvale/examples/ex2_3_thermal3d.py +0 -106
  106. pyvale/examples/ex3_1_displacement2d.py +0 -44
  107. pyvale/examples/ex3_2_displacement2d.py +0 -76
  108. pyvale/examples/ex3_3_displacement2d.py +0 -101
  109. pyvale/examples/ex3_4_displacement2d.py +0 -102
  110. pyvale/examples/ex4_1_strain2d.py +0 -54
  111. pyvale/examples/ex4_2_strain2d.py +0 -76
  112. pyvale/examples/ex4_3_strain2d.py +0 -97
  113. pyvale/examples/ex5_1_multiphysics2d.py +0 -75
  114. pyvale/examples/ex6_1_multiphysics2d_expsim.py +0 -115
  115. pyvale/examples/ex6_2_multiphysics3d_expsim.py +0 -160
  116. pyvale/examples/features/__init__.py +0 -5
  117. pyvale/examples/features/ex_area_avg.py +0 -89
  118. pyvale/examples/features/ex_calibration_error.py +0 -108
  119. pyvale/examples/features/ex_chain_field_errs.py +0 -141
  120. pyvale/examples/features/ex_field_errs.py +0 -78
  121. pyvale/examples/features/ex_sensor_single_angle_batch.py +0 -110
  122. pyvale/optimcheckfuncs.py +0 -153
  123. pyvale/visualsimplotter.py +0 -182
  124. pyvale-2025.4.1.dist-info/RECORD +0 -163
  125. {pyvale-2025.4.1.dist-info → pyvale-2025.5.1.dist-info}/licenses/LICENSE +0 -0
  126. {pyvale-2025.4.1.dist-info → pyvale-2025.5.1.dist-info}/top_level.txt +0 -0
pyvale/camerastereo.py ADDED
@@ -0,0 +1,217 @@
1
+ # ==============================================================================
2
+ # pyvale: the python validation engine
3
+ # License: MIT
4
+ # Copyright (C) 2025 The Computer Aided Validation Team
5
+ # ==============================================================================
6
+
7
+ """
8
+ NOTE: This module is a feature under developement.
9
+ """
10
+
11
+ from typing import Self
12
+ from pathlib import Path
13
+ import numpy as np
14
+ import yaml
15
+ from scipy.spatial.transform import Rotation
16
+ from pyvale.cameradata import CameraData
17
+ from pyvale.pyvaleexceptions import BlenderError
18
+
19
+
20
+ class CameraStereo:
21
+ __slots__ = ("cam_data_0","cam_data_1","stereo_dist","stereo_rotation")
22
+
23
+ def __init__(self, cam_data_0: CameraData, cam_data_1: CameraData) -> None:
24
+ self.cam_data_0 = cam_data_0
25
+ self.cam_data_1 = cam_data_1
26
+
27
+ cam0_rot_matrix = Rotation.as_matrix(self.cam_data_0.rot_world)
28
+ cam1_rot_matrix = Rotation.as_matrix(self.cam_data_1.rot_world)
29
+ (self.stereo_rotation, _) = Rotation.align_vectors(cam0_rot_matrix,
30
+ cam1_rot_matrix)
31
+ dist = self.cam_data_0.pos_world - self.cam_data_1.pos_world
32
+ dist_rot = self.cam_data_0.rot_world.apply(dist)
33
+ inverse = self.stereo_rotation.inv().as_quat()
34
+ inverse[3] *= -1
35
+ inverse = Rotation.from_quat(inverse)
36
+ self.stereo_dist = inverse.apply(dist_rot)
37
+
38
+ @classmethod
39
+ def from_calibration(cls,
40
+ calib_path: Path,
41
+ pos_world_0: np.ndarray,
42
+ rot_world_0: Rotation,
43
+ focal_length: float) -> Self:
44
+ """A method to initialise the CameraStereo using a calibration file and
45
+ some additional parameters. This creates an instance of the CameraStereo
46
+ class from the calibration parameters.
47
+
48
+ Parameters
49
+ ----------
50
+ calib_path : Path
51
+ The path to the calibration file (in yaml format).
52
+ pos_world_0 : np.ndarray
53
+ The position of camera 0 in world coordinates.
54
+ rot_world_0 : Rotation
55
+ The rotation of camera 0 in world coordinates.
56
+ focal_length : float
57
+ The focal length of camera 0.
58
+
59
+ Returns
60
+ -------
61
+ Self
62
+ An instance of the CameraStereo class, given the specified parameters.
63
+ """
64
+ calib_params = yaml.safe_load(calib_path.read_text())
65
+ pixels_num_cam0 = np.array([calib_params['Cam0_Cx [pixels]']*2,
66
+ calib_params['Cam0_Cy [pixels]']*2])
67
+ pixels_num_cam1 = np.array([calib_params['Cam1_Cx [pixels]']*2,
68
+ calib_params['Cam1_Cy [pixels]']*2])
69
+ pixels_size = focal_length / calib_params["Cam0_Fx [pixels]"]
70
+ stereo_rotation = Rotation.from_euler("xyz", ([calib_params['Theta [deg]'],
71
+ calib_params['Phi [deg]'],
72
+ calib_params['Psi [deg]']]), degrees=True)
73
+ stereo_dist = np.array([calib_params["Tx [mm]"],
74
+ calib_params["Ty [mm]"],
75
+ calib_params["Tz [mm]"]])
76
+
77
+ rot_world_1 = stereo_rotation * rot_world_0
78
+
79
+ inverse = stereo_rotation.inv().as_quat()
80
+ inverse[3] *= -1
81
+ inverse = Rotation.from_quat(inverse)
82
+
83
+ dist_rot = inverse.inv().apply(stereo_dist)
84
+ dist = rot_world_0.inv().apply(dist_rot)
85
+ pos_world_1 = pos_world_0 - dist
86
+
87
+ cam_data_0 = CameraData(pixels_num=pixels_num_cam0,
88
+ pixels_size=np.array([pixels_size, pixels_size]),
89
+ pos_world=pos_world_0,
90
+ rot_world=rot_world_0,
91
+ roi_cent_world=np.array([0, 0, 0]),
92
+ focal_length=focal_length)
93
+ cam_data_1 = CameraData(pixels_num=pixels_num_cam1,
94
+ pixels_size=np.array([pixels_size, pixels_size]),
95
+ pos_world=pos_world_1,
96
+ rot_world=rot_world_1,
97
+ roi_cent_world=np.array([0, 0, 0]),
98
+ focal_length=focal_length)
99
+ camera_stereo = cls(cam_data_0, cam_data_1)
100
+
101
+ return camera_stereo
102
+
103
+ def save_calibration(self, base_dir: Path) -> None:
104
+ """A method to save a calibration file of the stereo system as a yaml.
105
+ This is so that the file can easily be read into python, but is also
106
+ user-readable.
107
+
108
+ Parameters
109
+ ----------
110
+ base_dir : Path
111
+ The base directory to which all files should be saved. The
112
+ calibration file will be saved in a sub-directory named "calibration"
113
+ within this directory.
114
+
115
+ Raises
116
+ ------
117
+ BlenderError
118
+ "The specified save directory does not exist"
119
+ """
120
+ stereo_rotation = self.stereo_rotation.as_euler("xyz", degrees=True)
121
+ calib_params = {
122
+ "Cam0_Fx [pixels]": float(self.cam_data_0.focal_length /
123
+ self.cam_data_0.pixels_size[0]),
124
+ "Cam0_Fy [pixels]": float(self.cam_data_0.focal_length /
125
+ self.cam_data_0.pixels_size[1]),
126
+ "Cam0_Fs [pixels]": 0,
127
+ "Cam0_Kappa 1": self.cam_data_0.k1,
128
+ "Cam0_Kappa 2": self.cam_data_0.k2,
129
+ "Cam0_Kappa 3": self.cam_data_0.k3,
130
+ "Cam0_P1": self.cam_data_0.p1,
131
+ "Cam0_P2": self.cam_data_0.p2,
132
+ "Cam0_Cx [pixels]": float(self.cam_data_0.c0),
133
+ "Cam0_Cy [pixels]": float(self.cam_data_0.c1),
134
+ "Cam1_Fx [pixels]": float(self.cam_data_1.focal_length /
135
+ self.cam_data_1.pixels_size[0]),
136
+ "Cam1_Fy [pixels]": float(self.cam_data_1.focal_length /
137
+ self.cam_data_1.pixels_size[1]),
138
+ "Cam1_Fs [pixels]": 0,
139
+ "Cam1_Kappa 1": self.cam_data_1.k1,
140
+ "Cam1_Kappa 2": self.cam_data_1.k2,
141
+ "Cam1_Kappa 3": self.cam_data_1.k3,
142
+ "Cam1_P1": self.cam_data_1.p1,
143
+ "Cam1_P2": self.cam_data_1.p2,
144
+ "Cam1_Cx [pixels]": float(self.cam_data_1.c0),
145
+ "Cam1_Cy [pixels]": float(self.cam_data_1.c1),
146
+ "Tx [mm]": float(self.stereo_dist[0]),
147
+ "Ty [mm]": float(self.stereo_dist[1]),
148
+ "Tz [mm]": float(self.stereo_dist[2]),
149
+ "Theta [deg]": float(stereo_rotation[0]),
150
+ "Phi [deg]": float(stereo_rotation[1]),
151
+ "Psi [deg]": float(stereo_rotation[2])
152
+ }
153
+ if not base_dir.is_dir():
154
+ raise BlenderError("The specified save directory does not exist")
155
+
156
+ save_dir = base_dir / "calibration"
157
+ if not save_dir.is_dir():
158
+ save_dir.mkdir(parents=True, exist_ok=True)
159
+
160
+ filepath = str(save_dir / "calibration.yaml")
161
+ calib_file = open(filepath, "w")
162
+ yaml.safe_dump(calib_params, calib_file)
163
+ calib_file.close()
164
+ print("Calibration file saved to:", (save_dir / "calibration.yaml"))
165
+
166
+ def save_calibration_mid(self, base_dir: Path) -> None:
167
+ """A method to save a calibration file of the stereo system in a MatchID
168
+ accepted format.
169
+
170
+ Parameters
171
+ ----------
172
+ base_dir : Path
173
+ The base directory to which all files should be saved. The
174
+ calibration file will be saved in a sub-directory named "calibration"
175
+ within this directory.
176
+
177
+ Raises
178
+ ------
179
+ BlenderError
180
+ "The specified save directory does not exist"
181
+ """
182
+ if not base_dir.is_dir():
183
+ raise BlenderError("The specified save directory does not exist")
184
+
185
+ save_dir = base_dir / "calibration"
186
+ if not save_dir.is_dir():
187
+ save_dir.mkdir(parents=True, exist_ok=True)
188
+
189
+ filepath = str(save_dir / "calibration.caldat")
190
+ with open(filepath, "w") as file:
191
+ file.write(f'Cam0_Fx [pixels]; {self.cam_data_0.focal_length/ self.cam_data_0.pixels_size[0]}\n')
192
+ file.write(f'Cam0_Fy [pixels]; {self.cam_data_0.focal_length/ self.cam_data_0.pixels_size[1]}\n')
193
+ file.write("Cam0_Fs [pixels];0\n")
194
+ file.write(f'Cam0_Kappa 1;{self.cam_data_0.k1}\n')
195
+ file.write(f'Cam0_Kappa 2;{self.cam_data_0.k2}\n')
196
+ file.write(f'Cam0_Kappa 3;{self.cam_data_0.k3}\n')
197
+ file.write(f'Cam0_P1;{self.cam_data_0.p1}\n')
198
+ file.write(f'Cam0_P2;{self.cam_data_0.p2}\n')
199
+ file.write(f'Cam0_Cx [pixels];{self.cam_data_0.c0}\n')
200
+ file.write(f'Cam0_Cy [pixels];{self.cam_data_0.c1}\n')
201
+ file.write(f'Cam1_Fx [pixels]; {self.cam_data_1.focal_length/ self.cam_data_1.pixels_size[0]}\n')
202
+ file.write(f'Cam1_Fy [pixels]; {self.cam_data_1.focal_length/ self.cam_data_1.pixels_size[1]}\n')
203
+ file.write("Cam1_Fs [pixels];0\n")
204
+ file.write(f'Cam1_Kappa 1;{self.cam_data_1.k1}\n')
205
+ file.write(f'Cam1_Kappa 2;{self.cam_data_1.k2}\n')
206
+ file.write(f'Cam1_Kappa 3;{self.cam_data_1.k3}\n')
207
+ file.write(f'Cam1_P1;{self.cam_data_1.p1}\n')
208
+ file.write(f'Cam1_P2;{self.cam_data_1.p2}\n')
209
+ file.write(f'Cam1_Cx [pixels];{self.cam_data_1.c0}\n')
210
+ file.write(f'Cam1_Cy [pixels];{self.cam_data_1.c1}\n')
211
+ file.write(f"Tx [mm];{self.stereo_dist[0]}\n")
212
+ file.write(f"Ty [mm];{self.stereo_dist[1]}\n")
213
+ file.write(f"Tz [mm];{self.stereo_dist[2]}\n")
214
+ stereo_rotation = self.stereo_rotation.as_euler("xyz", degrees=True)
215
+ file.write(f"Theta [deg];{stereo_rotation[0]}\n")
216
+ file.write(f"Phi [deg];{stereo_rotation[1]}\n")
217
+ file.write(f"Psi [deg];{stereo_rotation[2]}")
pyvale/cameratools.py CHANGED
@@ -1,23 +1,28 @@
1
- # ================================================================================
1
+ # ==============================================================================
2
2
  # pyvale: the python validation engine
3
3
  # License: MIT
4
4
  # Copyright (C) 2025 The Computer Aided Validation Team
5
- # ================================================================================
5
+ # ==============================================================================
6
+
7
+ """
8
+ NOTE: This module is a feature under developement.
9
+ """
6
10
 
7
11
  import warnings
8
12
  from pathlib import Path
9
13
  import numpy as np
10
14
  from scipy.signal import convolve2d
15
+ import copy
11
16
  from scipy.spatial.transform import Rotation
12
17
  import matplotlib.image as mplim
13
18
  from PIL import Image
14
19
  from pyvale.cameradata2d import CameraData2D
15
20
  from pyvale.sensordata import SensorData
21
+ from pyvale.cameradata import CameraData
22
+ from pyvale.camerastereo import CameraStereo
16
23
 
17
- # NOTE: This module is a feature under developement.
18
24
 
19
25
  class CameraTools:
20
- #-------------------------------------------------------------------------------
21
26
  @staticmethod
22
27
  def load_image(im_path: Path) -> np.ndarray:
23
28
 
@@ -55,7 +60,6 @@ class CameraTools:
55
60
 
56
61
  return num_str
57
62
 
58
- #-------------------------------------------------------------------------------
59
63
  @staticmethod
60
64
  def pixel_vec_px(pixels_count: np.ndarray) -> tuple[np.ndarray,np.ndarray]:
61
65
  px_vec_x = np.arange(0,pixels_count[0],1)
@@ -71,7 +75,6 @@ class CameraTools:
71
75
  (px_grid_x,px_grid_y) = CameraTools.pixel_grid_px(pixels_count)
72
76
  return (px_grid_x.flatten(),px_grid_y.flatten())
73
77
 
74
- #-------------------------------------------------------------------------------
75
78
  @staticmethod
76
79
  def subpixel_vec_px(pixels_count: np.ndarray,
77
80
  subsample: int = 2) -> tuple[np.ndarray,np.ndarray]:
@@ -91,7 +94,6 @@ class CameraTools:
91
94
  (px_grid_x,px_grid_y) = CameraTools.subpixel_grid_px(pixels_count,subsample)
92
95
  return (px_grid_x.flatten(),px_grid_y.flatten())
93
96
 
94
- #-------------------------------------------------------------------------------
95
97
  @staticmethod
96
98
  def pixel_vec_leng(field_of_view: np.ndarray,
97
99
  leng_per_px: float) -> tuple[np.ndarray,np.ndarray]:
@@ -115,7 +117,7 @@ class CameraTools:
115
117
  (px_grid_x,px_grid_y) = CameraTools.pixel_grid_leng(field_of_view,leng_per_px)
116
118
  return (px_grid_x.flatten(),px_grid_y.flatten())
117
119
 
118
- #-------------------------------------------------------------------------------
120
+
119
121
  @staticmethod
120
122
  def subpixel_vec_leng(field_of_view: np.ndarray,
121
123
  leng_per_px: float,
@@ -148,7 +150,6 @@ class CameraTools:
148
150
  subsample)
149
151
  return (px_grid_x.flatten(),px_grid_y.flatten())
150
152
 
151
- #-------------------------------------------------------------------------------
152
153
  @staticmethod
153
154
  def calc_resolution_from_sim_2d(pixels_count: np.ndarray,
154
155
  coords: np.ndarray,
@@ -210,7 +211,6 @@ class CameraTools:
210
211
  round(subsample/2)-1::subsample]
211
212
  return avg_image
212
213
 
213
- #---------------------------------------------------------------------------
214
214
  @staticmethod
215
215
  def build_sensor_data_from_camera_2d(cam_data: CameraData2D) -> SensorData:
216
216
  pixels_vectorised = CameraTools.vectorise_pixel_grid_leng(cam_data.field_of_view,
@@ -324,4 +324,199 @@ class CameraTools:
324
324
  return (roi_pos_world,cam_pos_world)
325
325
 
326
326
 
327
- #-------------------------------------------------------------------------------
327
+ #---------------------------------------------------------------------------
328
+ # Blender camera tools
329
+
330
+ @staticmethod
331
+ def calculate_FOV(cam_data: CameraData) -> tuple[float, float]:
332
+ """A method to calulate the camera's field of view in mm
333
+
334
+ Parameters
335
+ ----------
336
+ cam_data : CameraData
337
+ A dataclass containing the camera parameters
338
+
339
+ Returns
340
+ -------
341
+ tuple[float, float]
342
+ A tuple containing the field of view in mm in both x and y directions
343
+ """
344
+ FOV_x = (((cam_data.image_dist - cam_data.focal_length)
345
+ / cam_data.focal_length) *
346
+ (cam_data.pixels_size) *
347
+ cam_data.pixels_num[0])[0]
348
+ FOV_y = (cam_data.pixels_num[1] / cam_data.pixels_num[0]) * FOV_x
349
+ FOV_mm = (FOV_x, FOV_y)
350
+ return FOV_mm
351
+
352
+ @staticmethod
353
+ def blender_FOV(cam_data: CameraData) -> tuple[float, float]:
354
+ """A method to calculate the camera's field of view in mm using Blender's
355
+ method. This method differs due to one simplification.
356
+
357
+ Parameters
358
+ ----------
359
+ cam_data : CameraData
360
+ A dataclass containing the camera parameters
361
+
362
+ Returns
363
+ -------
364
+ tuple[float, float]
365
+ A tuple containing the FOV in x and y directions
366
+ """
367
+ FOV_x = (cam_data.pixels_num[0] * cam_data.pixels_size[0] * cam_data.image_dist) / cam_data.focal_length
368
+ FOV_y = (cam_data.pixels_num[1] / cam_data.pixels_num[0]) * FOV_x
369
+ FOV_blender = (FOV_x, FOV_y)
370
+ return FOV_blender
371
+
372
+ @staticmethod
373
+ def calculate_mm_px_resolution(cam_data: CameraData) -> float:
374
+ """Function to calculate the mm/px resolution of a camera
375
+
376
+ Parameters
377
+ ----------
378
+ cam_data : CameraData
379
+ A dataclass containing the camera parameters
380
+
381
+ Returns
382
+ -------
383
+ float
384
+ The mm/px resolution
385
+ """
386
+ FOV_mm = CameraTools.blender_FOV(cam_data)
387
+ resolution = FOV_mm[0] / cam_data.pixels_num[0]
388
+ return resolution
389
+
390
+ @staticmethod
391
+ def focal_length_from_resolution(pixels_size: np.ndarray,
392
+ working_dist: float,
393
+ resolution: float) -> float:
394
+ """A method to calculate the required focal length to achieve a certain
395
+ resolution. This is calculated given the pixel size and working distance.
396
+ This method can be used for a 2D setup or for camera 0 for a stereo setup.
397
+
398
+ Parameters
399
+ ----------
400
+ pixels_size : np.ndarray
401
+ The camera pixel size in the x and y directions (in mm).
402
+ working_dist : float
403
+ The working distance of the camera to the sample.
404
+ resolution : float
405
+ The desired resolution in mm/px.
406
+
407
+ Returns
408
+ -------
409
+ float
410
+ The focal length required to obtain the desired image resolution.
411
+ """
412
+ focal_length = working_dist / ((resolution / pixels_size[0]))
413
+ return focal_length
414
+
415
+ @staticmethod
416
+ def blender_camera_from_resolution(pixels_num: np.ndarray,
417
+ pixels_size: np.ndarray,
418
+ working_dist: float,
419
+ resolution: float) -> CameraData:
420
+ """A convenience function to create a camera object in Blender from its pixels,
421
+ the pixel size, the working distance and desired resolution.
422
+
423
+ Parameters
424
+ ----------
425
+ pixels_num : np.ndarray
426
+ The number of pixels in the camera, in the x and y directions.
427
+ pixels_size : np.ndarray
428
+ The camera pixels size in mm, in the x and y directions.
429
+ working_dist : float
430
+ The working distance of the camera.
431
+ resolution : float
432
+ The desired mm/px resolution
433
+
434
+ Returns
435
+ -------
436
+ CameraData
437
+ A dataclass containing the created camera's parameters.
438
+ """
439
+ focal_length = CameraTools.focal_length_from_resolution(pixels_size, working_dist, resolution)
440
+
441
+ cam_data = CameraData(pixels_num=pixels_num,
442
+ pixels_size=pixels_size,
443
+ pos_world=(0, 0, working_dist),
444
+ rot_world=Rotation.from_euler("xyz", [0, 0, 0]),
445
+ roi_cent_world=(0, 0, 0),
446
+ focal_length=focal_length)
447
+ return cam_data
448
+
449
+ @staticmethod
450
+ def symmetric_stereo_cameras(cam_data_0: CameraData,
451
+ stereo_angle:float) -> CameraStereo:
452
+ """A convenience function to set up a symmetric stereo camera system, given
453
+ an initial CameraData dataclass and a stereo angle. This assumes the basic
454
+ camera parameters are the same.
455
+
456
+ Parameters
457
+ ----------
458
+ cam_data_0 : CameraData
459
+ A dataclass containing the camera parameters for a single camera, which
460
+ will be camera 0.
461
+ stereo_angle : float
462
+ The stereo angle between the two cameras.
463
+
464
+ Returns
465
+ -------
466
+ CameraStereo
467
+ An instance of the CameraStereo class. This class contains
468
+ information about each of the cameras, as well as the extrinsic
469
+ parameters between them.
470
+ """
471
+ cam_data_1 = copy.deepcopy(cam_data_0)
472
+ base = 2 * cam_data_0.pos_world[2] * np.tan(np.radians(stereo_angle) / 2)
473
+
474
+ cam_data_0.pos_world[0] -= base / 2
475
+ cam_data_1.pos_world[0] += base / 2
476
+
477
+ cam_0_rot = (0, -np.radians(stereo_angle / 2), 0)
478
+ cam_0_rot = Rotation.from_euler("xyz", cam_0_rot, degrees=False)
479
+ cam_data_0.rot_world = cam_0_rot
480
+
481
+ cam_1_rot = (0, np.radians(stereo_angle / 2), 0)
482
+ cam_1_rot = Rotation.from_euler("xyz", cam_1_rot, degrees=False)
483
+ cam_data_1.rot_world = cam_1_rot
484
+
485
+ stereo_system = CameraStereo(cam_data_0, cam_data_1)
486
+
487
+ return stereo_system
488
+
489
+ @staticmethod
490
+ def faceon_stereo_cameras(cam_data_0: CameraData,
491
+ stereo_angle: float) -> CameraStereo:
492
+ # TODO: Correct docstring
493
+ """A convenience function to set up a face-on stereo camera system, given
494
+ an initial CameraData dataclass and a stereo angle. This assumes the basic
495
+ camera parameters are the same.
496
+
497
+ Parameters
498
+ ----------
499
+ cam_data_0 : CameraData
500
+ A dataclass containing the camera parameters for a single camera, which
501
+ will be camera 0.
502
+ stereo_angle : float
503
+ The stereo angle between the two cameras.
504
+
505
+ Returns
506
+ -------
507
+ CameraStereo
508
+ An instance of the CameraStereo class. This class contains
509
+ information about each of the cameras, as well as the extrinsic
510
+ parameters between them.
511
+ """
512
+ cam_data_1 = copy.deepcopy(cam_data_0)
513
+ base = cam_data_0.pos_world[2] * np.tan(np.radians(stereo_angle))
514
+ cam_data_1.pos_world[0] += base
515
+
516
+ rotation_angle = (0, np.radians(stereo_angle), 0)
517
+ rotation_angle = Rotation.from_euler("xyz", rotation_angle, degrees=False)
518
+ cam_data_1.rot_world = rotation_angle
519
+
520
+ stereo_system = CameraStereo(cam_data_0, cam_data_1)
521
+
522
+ return stereo_system
@@ -1,8 +1,12 @@
1
- # ================================================================================
1
+ # ==============================================================================
2
2
  # pyvale: the python validation engine
3
3
  # License: MIT
4
4
  # Copyright (C) 2025 The Computer Aided Validation Team
5
- # ================================================================================
5
+ # ==============================================================================
6
+
7
+ """
8
+ NOTE: this module is a feature under developement.
9
+ """
6
10
 
7
11
  import numpy as np
8
12
  import cython
Binary file
pyvale/dataset.py CHANGED
@@ -4,39 +4,63 @@
4
4
  # Copyright (C) 2025 The Computer Aided Validation Team
5
5
  #===============================================================================
6
6
 
7
+ """
8
+ Accesors for data that comes pre-packaged with pyvale for demonstrating its
9
+ functionality. This includes moose simulation outputs as exodus files, input
10
+ files for moose and gmsh for additional simulation cases, and images required
11
+ for testing the image deformation and digital image correlation modules.
12
+ """
13
+
7
14
  from enum import Enum
8
15
  from pathlib import Path
9
16
  from importlib.resources import files
10
17
 
11
18
 
19
+ SIM_CASE_COUNT = 26
20
+ """Constant describing the number of simulation test case input files for moose
21
+ and gmsh that come packaged with pyvale.
22
+ """
23
+
12
24
  class EElemTest(Enum):
25
+ """Enumeration used to specify different 3D element types for extracting
26
+ specific test simulation datasets.
27
+ """
28
+
13
29
  TET4 = "TET4"
30
+ """Tetrahedral element, linear with 4 nodes.
31
+ """
32
+
14
33
  TET10 = "TET10"
34
+ """Tetrahedral element, quadratic with 10 nodes.
35
+ """
36
+
15
37
  TET14 = "TET14"
38
+ """Tetrahedral element, quadratic with 14 nodes.
39
+ """
40
+
16
41
  HEX8 = "HEX8"
42
+ """Hexahedral element, linear with 8 nodes.
43
+ """
44
+
17
45
  HEX20 = "HEX20"
46
+ """Hexahedral element, quadratic with 20 nodes.
47
+ """
48
+
18
49
  HEX27 = "HEX27"
50
+ """Hexahedral element, quadratic with 27 nodes.
51
+ """
19
52
 
20
53
  def __str__(self):
21
54
  return self.value
22
55
 
23
56
 
24
- SIM_CASE_COUNT = 26
25
-
26
-
27
57
  class DataSetError(Exception):
28
58
  """Custom error class for file io errors associated with retrieving datasets
29
59
  and files packaged with pyvale.
30
60
  """
31
- pass
32
61
 
33
62
 
34
63
  class DataSet:
35
- """A static namespace class for handling datasets packaged with pyvale.
36
- Contains a series of static methods returning a Path object to each data
37
- file that is packaged with pyvale.
38
- """
39
-
40
64
  @staticmethod
41
65
  def sim_case_input_file_path(case_num: int) -> Path:
42
66
  """Gets the path to MOOSE input file (*.i) for a particular simulation
@@ -62,7 +86,7 @@ class DataSet:
62
86
  raise DataSetError("Simulation case number must be greater than 0")
63
87
  elif case_num > SIM_CASE_COUNT:
64
88
  raise DataSetError("Simulation case number must be less than " \
65
- + f"{SIM_CASE_COUNT}")
89
+ + f"{SIM_CASE_COUNT}")
66
90
 
67
91
  case_num_str = str(case_num).zfill(2)
68
92
  case_file = f"case{case_num_str}.i"
@@ -97,7 +121,7 @@ class DataSet:
97
121
  raise DataSetError("Simulation case number must be greater than 0")
98
122
  elif case_num > SIM_CASE_COUNT:
99
123
  raise DataSetError("Simulation case number must be less than " \
100
- + f"{SIM_CASE_COUNT}")
124
+ + f"{SIM_CASE_COUNT}")
101
125
 
102
126
  case_num_str = str(case_num).zfill(2)
103
127
  case_file = f"case{case_num_str}.geo"
@@ -248,19 +272,54 @@ class DataSet:
248
272
 
249
273
  @staticmethod
250
274
  def render_mechanical_3d_path() -> Path:
251
- """_summary_
275
+ """Path to a MOOSE simulation output in exodus format. This case is a
276
+ purely mechanical test case in 3D meant for testing image rendering
277
+ algorithms for digital image correlation simulation. The simulation
278
+ consists of a linear elastic thin plate with a hole loaded in tension.
279
+ The simulation uses linear tetrahedral elements for rendering tests.
252
280
 
253
281
  Returns
254
282
  -------
255
283
  Path
256
- _description_
284
+ Path to the exodus (*.e) output file for this simulation case.
257
285
  """
258
286
  return Path(files("pyvale.data").joinpath("case26_out.e"))
259
287
 
260
288
  @staticmethod
261
289
  def render_simple_block_path() -> Path:
290
+ """Path to a MOOSE simulation output in exodus format. This case is a
291
+ a simple rectangular block in 3D loaded in tension. It uses a minimum
292
+ number of elements and is intended purely for testing image rendering
293
+ algorithms. This simulation uses linear tetrahedral elements.
294
+
295
+ Returns
296
+ -------
297
+ Path
298
+ Path to the exodus (*.e) output file for this simulation case.
299
+ """
262
300
  return Path(files("pyvale.data").joinpath("case25_out.e"))
263
301
 
264
302
  @staticmethod
265
303
  def element_case_path(elem_type: EElemTest) -> Path:
266
- return Path(files("pyvale.data").joinpath(f"case00_{elem_type.value}_out.e"))
304
+ """Path to a MOOSE simulation output in exodus format. This case is a
305
+ 10mm cube undergoing thermo-mechanical loading solved for the
306
+ temperature displacement and strain fields. This case is solved using a
307
+ variety of tetrahedral and hexahedral elements with linear or quadratic
308
+ shapes functions. These simulation cases are intended for testing
309
+ purposes and contain a minimal number of elements.
310
+
311
+ Parameters
312
+ ----------
313
+ elem_type : EElemTest
314
+ Enumeration specifying the element type for this test case.
315
+
316
+ Returns
317
+ -------
318
+ Path
319
+ Path to the exodus (*.e) output file for this simulation case.
320
+ """
321
+ return Path(files("pyvale.data")
322
+ .joinpath(f"case00_{elem_type.value}_out.e"))
323
+
324
+
325
+