aot-biomaps 2.9.519__tar.gz

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.
Files changed (65) hide show
  1. aot_biomaps-2.9.519/AOT_biomaps/AOT_Acoustic/AcousticEnums.py +68 -0
  2. aot_biomaps-2.9.519/AOT_biomaps/AOT_Acoustic/AcousticTools.py +356 -0
  3. aot_biomaps-2.9.519/AOT_biomaps/AOT_Acoustic/FocusedWave.py +306 -0
  4. aot_biomaps-2.9.519/AOT_biomaps/AOT_Acoustic/IrregularWave.py +80 -0
  5. aot_biomaps-2.9.519/AOT_biomaps/AOT_Acoustic/PlaneWave.py +42 -0
  6. aot_biomaps-2.9.519/AOT_biomaps/AOT_Acoustic/StructuredWave.py +479 -0
  7. aot_biomaps-2.9.519/AOT_biomaps/AOT_Acoustic/__init__.py +12 -0
  8. aot_biomaps-2.9.519/AOT_biomaps/AOT_Acoustic/_mainAcoustic.py +867 -0
  9. aot_biomaps-2.9.519/AOT_biomaps/AOT_Experiment/ExperimentTools.py +250 -0
  10. aot_biomaps-2.9.519/AOT_biomaps/AOT_Experiment/Focus.py +153 -0
  11. aot_biomaps-2.9.519/AOT_biomaps/AOT_Experiment/Tomography.py +1233 -0
  12. aot_biomaps-2.9.519/AOT_biomaps/AOT_Experiment/__init__.py +8 -0
  13. aot_biomaps-2.9.519/AOT_biomaps/AOT_Experiment/_mainExperiment.py +621 -0
  14. aot_biomaps-2.9.519/AOT_biomaps/AOT_Medium/BubbleMedium.py +134 -0
  15. aot_biomaps-2.9.519/AOT_biomaps/AOT_Medium/HomogeneousMedium.py +104 -0
  16. aot_biomaps-2.9.519/AOT_biomaps/AOT_Medium/MediumEnums.py +12 -0
  17. aot_biomaps-2.9.519/AOT_biomaps/AOT_Medium/PVAMedium.py +116 -0
  18. aot_biomaps-2.9.519/AOT_biomaps/AOT_Medium/__init__.py +9 -0
  19. aot_biomaps-2.9.519/AOT_biomaps/AOT_Medium/_mainMedium.py +167 -0
  20. aot_biomaps-2.9.519/AOT_biomaps/AOT_Optic/Absorber.py +24 -0
  21. aot_biomaps-2.9.519/AOT_biomaps/AOT_Optic/Laser.py +80 -0
  22. aot_biomaps-2.9.519/AOT_biomaps/AOT_Optic/OpticEnums.py +17 -0
  23. aot_biomaps-2.9.519/AOT_biomaps/AOT_Optic/__init__.py +10 -0
  24. aot_biomaps-2.9.519/AOT_biomaps/AOT_Optic/_mainOptic.py +205 -0
  25. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/AOT_Kernels.py +741 -0
  26. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/AOT_Optimizers/DEPIERRO.py +197 -0
  27. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/AOT_Optimizers/LBFGS.py +173 -0
  28. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/AOT_Optimizers/LS.py +524 -0
  29. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/AOT_Optimizers/MAPEM.py +478 -0
  30. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/AOT_Optimizers/MLEM.py +384 -0
  31. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/AOT_Optimizers/PDHG.py +398 -0
  32. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/AOT_Optimizers/__init__.py +6 -0
  33. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Huber.py +102 -0
  34. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Quadratic.py +88 -0
  35. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/RelativeDifferences.py +95 -0
  36. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/__init__.py +3 -0
  37. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/SparseSMatrix_CSR.py +545 -0
  38. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/SparseSMatrix_SELL.py +644 -0
  39. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/__init__.py +10 -0
  40. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/AOT_biomaps_kernels.cubin +0 -0
  41. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/AlgebraicRecon.py +1017 -0
  42. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/AnalyticRecon.py +411 -0
  43. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/BayesianRecon.py +234 -0
  44. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/DeepLearningRecon.py +35 -0
  45. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/PrimalDualRecon.py +268 -0
  46. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/ReconEnums.py +386 -0
  47. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/ReconTools.py +492 -0
  48. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/SparseMatrixWrapper.py +213 -0
  49. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/__init__.py +12 -0
  50. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/_mainRecon.py +340 -0
  51. aot_biomaps-2.9.519/AOT_biomaps/AOT_Recon/test_kernels.py +182 -0
  52. aot_biomaps-2.9.519/AOT_biomaps/Config.py +87 -0
  53. aot_biomaps-2.9.519/AOT_biomaps/Settings.py +83 -0
  54. aot_biomaps-2.9.519/AOT_biomaps/__init__.py +90 -0
  55. aot_biomaps-2.9.519/AOT_biomaps/__main__.py +209 -0
  56. aot_biomaps-2.9.519/PKG-INFO +205 -0
  57. aot_biomaps-2.9.519/README.md +157 -0
  58. aot_biomaps-2.9.519/aot_biomaps.egg-info/PKG-INFO +205 -0
  59. aot_biomaps-2.9.519/aot_biomaps.egg-info/SOURCES.txt +118 -0
  60. aot_biomaps-2.9.519/aot_biomaps.egg-info/dependency_links.txt +1 -0
  61. aot_biomaps-2.9.519/aot_biomaps.egg-info/entry_points.txt +2 -0
  62. aot_biomaps-2.9.519/aot_biomaps.egg-info/requires.txt +15 -0
  63. aot_biomaps-2.9.519/aot_biomaps.egg-info/top_level.txt +1 -0
  64. aot_biomaps-2.9.519/setup.cfg +4 -0
  65. aot_biomaps-2.9.519/setup.py +150 -0
@@ -0,0 +1,68 @@
1
+ from enum import Enum
2
+
3
+ class TypeSim(Enum):
4
+ """
5
+ Enum for the type of simulation to be performed.
6
+
7
+ Selection of simulation types:
8
+ - KWAVE: k-Wave simulation.
9
+ - FIELD2: Field II simulation.
10
+ - HYDRO: Hydrophone acquisition.
11
+ """
12
+ KWAVE = 'k-wave'
13
+ """k-Wave simulation."""
14
+
15
+ FIELD2 = 'Field2'
16
+ """Field II simulation."""
17
+
18
+ HYDRO = 'Hydrophone'
19
+ """Hydrophone acquisition."""
20
+
21
+ SIMPLE_SIM = 'SimpleSim'
22
+ """Simple simulation for testing purposes."""
23
+
24
+ class Dim(Enum):
25
+ """
26
+ Enum for the dimension of the acoustic field.
27
+
28
+ Selection of dimensions:
29
+ - D2: 2D field.
30
+ - D3: 3D field.
31
+ """
32
+ D2 = '2D'
33
+ """2D field."""
34
+ D3 = '3D'
35
+ """3D field."""
36
+
37
+ class FormatSave(Enum):
38
+ """
39
+ Enum for different file formats to save the acoustic field.
40
+
41
+ Selection of file formats:
42
+ - HDR_IMG: Interfile format (.hdr and .img).
43
+ - H5: HDF5 format (.h5).
44
+ - NPY: NumPy format (.npy).
45
+ """
46
+ HDR_IMG = '.hdr'
47
+ """Interfile format (.hdr and .img)."""
48
+ H5 = '.h5'
49
+ """HDF5 format (.h5)."""
50
+ NPY = '.npy'
51
+ """NumPy format (.npy)."""
52
+
53
+ class WaveType(Enum):
54
+ """
55
+ Enum for different types of acoustic waves.
56
+
57
+ Selection of wave types:
58
+ - FocusedWave: A wave type where the energy is focused at a specific point.
59
+ - StructuredWave: A wave type characterized by a specific pattern or structure.
60
+ - PlaneWave: A wave type where the wavefronts are parallel and travel in a single direction.
61
+ """
62
+ FocusedWave = 'focus'
63
+ """A wave type where the energy is focused at a specific point."""
64
+ StructuredWave = 'structured'
65
+ """A wave type characterized by a specific pattern or structure."""
66
+ PlaneWave = 'plane'
67
+ """A wave type where the wavefronts are parallel and travel in a single direction."""
68
+
@@ -0,0 +1,356 @@
1
+ from scipy.signal import hilbert
2
+ from scipy.ndimage import zoom
3
+ from scipy.io import loadmat as scipy_loadmat
4
+ import os
5
+ import numpy as np
6
+ from scipy.stats import linregress
7
+
8
+ # Optional cupy import for GPU acceleration
9
+ try:
10
+ import cupy as cp
11
+ CUPY_AVAILABLE = True
12
+ except ImportError:
13
+ CUPY_AVAILABLE = False
14
+
15
+
16
+ def loadmat(param_path_mat):
17
+ """
18
+ Load a .mat file (MATLAB format).
19
+
20
+ Args:
21
+ param_path_mat: Path to the .mat file.
22
+
23
+ Returns:
24
+ Dictionary containing the variables from the file.
25
+ """
26
+ try:
27
+ return scipy_loadmat(param_path_mat)
28
+ except Exception:
29
+ raise ValueError(f"Could not load {param_path_mat}. Consider using scipy.io.loadmat or h5py for HDF5 files.")
30
+
31
+ def reshape_field(field, factor, device=None):
32
+ """
33
+ Downsample a 3D or 4D field using scipy interpolation.
34
+ Args:
35
+ field: Input field (numpy array).
36
+ factor: Downsampling factor (tuple of ints).
37
+ device: Ignored (kept for backward compatibility).
38
+ Returns:
39
+ Downsampled field (numpy array).
40
+ """
41
+ if field is None:
42
+ raise ValueError("Acoustic field is not generated. Please generate the field first.")
43
+
44
+ if not isinstance(field, np.ndarray):
45
+ field = np.asarray(field, dtype=np.float32)
46
+
47
+ if len(factor) == 3:
48
+ if field.ndim != 3:
49
+ raise ValueError("Expected 3D field.")
50
+ # Use scipy.ndimage.zoom for downsampling
51
+ zoom_factors = [1.0 / f for f in factor]
52
+ downsampled = zoom(field, zoom_factors, order=1) # order=1 for linear interpolation
53
+
54
+ elif len(factor) == 4:
55
+ if field.ndim != 4:
56
+ raise ValueError("Expected 4D field.")
57
+ zoom_factors = [1.0 / f for f in factor]
58
+ downsampled = zoom(field, zoom_factors, order=1)
59
+
60
+ else:
61
+ raise ValueError("Unsupported dimension. Only 3D and 4D fields are supported.")
62
+
63
+ return downsampled.astype(np.float32)
64
+
65
+ def calculate_envelope_squared_cpu(field):
66
+ """
67
+ Compute the squared envelope of the acoustic field on CPU in a vectorized way.
68
+ Optimized for 3D (T, X, Z) or 4D (T, X, Y, Z) arrays.
69
+
70
+ Args:
71
+ field: Acoustic field (numpy.ndarray). Expected shape: (T, X, Z) or (T, X, Y, Z).
72
+
73
+ Returns:
74
+ envelope_sq (numpy.ndarray): Squared envelope of the acoustic field.
75
+ """
76
+ try:
77
+ if field is None:
78
+ raise ValueError("Acoustic field is not generated.")
79
+
80
+ if not isinstance(field, np.ndarray):
81
+ field = np.asarray(field, dtype=np.float32)
82
+
83
+ if len(field.shape) not in [3, 4]:
84
+ raise ValueError("Field must be 3D (T, X, Z) or 4D (T, X, Y, Z).")
85
+
86
+ # Vectorized Hilbert transform along the time axis (axis=0)
87
+ analytic_signal = hilbert(field, axis=0)
88
+ envelope_sq = np.abs(analytic_signal) ** 2
89
+
90
+ return envelope_sq.astype(np.float32)
91
+
92
+ except Exception as e:
93
+ print(f"Error in calculate_envelope_squared_cpu: {e}")
94
+ raise
95
+
96
+ def calculate_envelope_squared_gpu(field, chunk_size=100):
97
+ """
98
+ Compute the squared envelope of the acoustic field on GPU using CuPy.
99
+ Returns the result on CPU (numpy.ndarray) and frees GPU memory.
100
+
101
+ Args:
102
+ field: Acoustic field (numpy.ndarray or cupy.ndarray) with shape (T, X, Z) or (T, X, Y, Z).
103
+ chunk_size: Number of spatial elements to process at once (to avoid OOM).
104
+
105
+ Returns:
106
+ envelope_sq (numpy.ndarray): Squared envelope on CPU.
107
+ """
108
+ if not CUPY_AVAILABLE:
109
+ print("CuPy not available. Falling back to CPU.")
110
+ return calculate_envelope_squared_cpu(field)
111
+
112
+ try:
113
+ # 1. Dimensions
114
+ T = field.shape[0]
115
+ spatial_dims = field.shape[1:]
116
+ total_spatial_size = int(cp.prod(cp.array(spatial_dims)))
117
+
118
+ # Prepare Hilbert filter (once)
119
+ n_fft = T
120
+ h = cp.zeros(n_fft, dtype=cp.float32)
121
+ if n_fft % 2 == 0:
122
+ h[0] = h[n_fft // 2] = 1
123
+ h[1:n_fft // 2] = 2
124
+ else:
125
+ h[0] = 1
126
+ h[1:(n_fft + 1) // 2] = 2
127
+ h = h[:, cp.newaxis] # For broadcasting
128
+
129
+ # Flatten spatial dimensions for easy iteration
130
+ field_flat = field.reshape(T, -1)
131
+ n_spatial = field_flat.shape[1]
132
+
133
+ envelope_sq = cp.empty((T, n_spatial), dtype=cp.float32)
134
+
135
+ # 2. Process in chunks
136
+ for i in range(0, n_spatial, chunk_size):
137
+ end = min(i + chunk_size, n_spatial)
138
+
139
+ # Transfer only the current chunk
140
+ chunk = cp.asarray(field_flat[:, i:end], dtype=cp.float32)
141
+
142
+ # FFT, filter, IFFT
143
+ chunk_fft = cp.fft.fft(chunk, axis=0)
144
+ chunk_analytic = cp.fft.ifft(chunk_fft * h, axis=0)
145
+
146
+ # Store squared envelope directly
147
+ envelope_sq[:, i:end] = cp.abs(chunk_analytic) ** 2
148
+
149
+ # Free chunk memory immediately
150
+ del chunk, chunk_fft, chunk_analytic
151
+
152
+ # 3. Reshape and return to CPU
153
+ return cp.asnumpy(envelope_sq.reshape(T, *spatial_dims))
154
+
155
+ except cp.cuda.memory.OutOfMemoryError:
156
+ print("⚠️ Insufficient GPU memory. Falling back to CPU.")
157
+ return calculate_envelope_squared_cpu(field)
158
+ except Exception as e:
159
+ print(f"Error in calculate_envelope_squared_gpu: {e}")
160
+ raise
161
+
162
+
163
+ def calculate_envelope_squared(field, device=None):
164
+ """
165
+ Compute the squared envelope of the acoustic field.
166
+ Automatically uses GPU if available and requested, otherwise falls back to CPU.
167
+
168
+ Args:
169
+ field: Acoustic field (numpy.ndarray or cupy.ndarray) with shape (T, X, Z) or (T, X, Y, Z).
170
+ device: 'gpu' to use GPU (if available), 'cpu' to force CPU, None for auto-detection.
171
+
172
+ Returns:
173
+ envelope_sq (numpy.ndarray): Squared envelope of the acoustic field.
174
+ """
175
+ if device == 'gpu' and CUPY_AVAILABLE:
176
+ return calculate_envelope_squared_gpu(field)
177
+ else:
178
+ return calculate_envelope_squared_cpu(field)
179
+
180
+
181
+ def getPattern(pathFile):
182
+ """
183
+ Extract the pattern from a file path.
184
+
185
+ Args:
186
+ pathFile (str): Path to the file containing the pattern.
187
+
188
+ Returns:
189
+ str: The pattern string.
190
+ """
191
+ try:
192
+ # Pattern between first _ and last _
193
+ pattern = os.path.basename(pathFile).split('_')[1:-1]
194
+ pattern_str = ''.join(pattern)
195
+ return pattern_str
196
+ except Exception as e:
197
+ print(f"Error reading pattern from file: {e}")
198
+ return None
199
+
200
+ def detect_space_0_and_space_1(hex_string):
201
+ """
202
+ Detect the longest sequences of 0s and 1s in a hex string.
203
+
204
+ Args:
205
+ hex_string: Hexadecimal string.
206
+
207
+ Returns:
208
+ tuple: (space_0, space_1) where space_0 is the length of the longest 0 sequence,
209
+ and space_1 is the length of the longest 1 sequence.
210
+ """
211
+ binary_string = bin(int(hex_string, 16))[2:].zfill(len(hex_string) * 4)
212
+
213
+ # Find longest sequence of consecutive 0s
214
+ zeros_groups = [len(s) for s in binary_string.split('1')]
215
+ space_0 = max(zeros_groups) if zeros_groups else 0
216
+
217
+ # Find longest sequence of consecutive 1s
218
+ ones_groups = [len(s) for s in binary_string.split('0')]
219
+ space_1 = max(ones_groups) if ones_groups else 0
220
+
221
+ return space_0, space_1
222
+
223
+ def getAngle(pathFile):
224
+ """
225
+ Extract the angle from a file path.
226
+
227
+ Args:
228
+ pathFile (str): Path to the file containing the angle.
229
+
230
+ Returns:
231
+ int: The angle in degrees.
232
+ """
233
+ try:
234
+ # Angle between last _ and .
235
+ angle_str = os.path.basename(pathFile).split('_')[-1].replace('.', '')
236
+ if angle_str.startswith('0'):
237
+ angle_str = angle_str[1:]
238
+ elif angle_str.startswith('1'):
239
+ angle_str = '-' + angle_str[1:]
240
+ else:
241
+ raise ValueError("Invalid angle format in file name.")
242
+ return int(angle_str)
243
+ except Exception as e:
244
+ print(f"Error reading angle from file: {e}")
245
+ return None
246
+
247
+ def getFrequency(fileName, num_elements, dx):
248
+ """
249
+ Calculate the spatial frequency from a file name.
250
+
251
+ Args:
252
+ fileName: File name containing the pattern.
253
+ num_elements: Number of elements in the probe.
254
+ dx: Element spacing in meters.
255
+
256
+ Returns:
257
+ int: Spatial frequency in mm^-1.
258
+ """
259
+ profile = hex_to_binary_profile(fileName[6:-4], num_elements)
260
+
261
+ if set(fileName[6:-4].lower().replace(" ", "")) == {'f'}:
262
+ fs_key = 0.0 # fs_key in mm^-1 (0.0 mm^-1)
263
+ else:
264
+ ft_prof = np.fft.fft(profile)
265
+ idx_max = np.argmax(np.abs(ft_prof[1:len(profile)//2])) + 1
266
+ freqs = np.fft.fftfreq(len(profile), d=dx)
267
+
268
+ # freqs is in m^-1 because dx is in meters
269
+ fs_m_inv = abs(freqs[idx_max])
270
+
271
+ fs_key = fs_m_inv # Spatial frequency in mm^-1
272
+ return int(fs_key / (1/(len(profile)*dx)))
273
+
274
+ def format_angle(a):
275
+ """
276
+ Format an angle as a string for file naming.
277
+
278
+ Args:
279
+ a: Angle in degrees.
280
+
281
+ Returns:
282
+ str: Formatted angle string (e.g., "045" or "145" for -45).
283
+ """
284
+ return f"{'1' if a < 0 else '0'}{abs(int(a)):02d}"
285
+
286
+ def next_power_of_2(n):
287
+ """
288
+ Calculate the next power of 2 greater than or equal to n.
289
+
290
+ Args:
291
+ n: Input integer.
292
+
293
+ Returns:
294
+ int: Next power of 2 >= n.
295
+ """
296
+ return int(2 ** np.ceil(np.log2(n)))
297
+
298
+ def hex_to_binary_profile(hex_string, n_piezos=192):
299
+ """
300
+ Convert a hex string to a binary profile array.
301
+
302
+ Args:
303
+ hex_string: Hexadecimal string representing the pattern.
304
+ n_piezos: Number of piezos in the probe (default: 192).
305
+
306
+ Returns:
307
+ numpy.ndarray: Binary profile as an array of 0s and 1s.
308
+ """
309
+ hex_string = hex_string.strip().replace(" ", "").replace("\n", "")
310
+ if set(hex_string.lower()) == {'f'}:
311
+ return np.ones(n_piezos, dtype=int)
312
+
313
+ try:
314
+ n_char = len(hex_string)
315
+ n_bits = n_char * 4
316
+ binary_str = bin(int(hex_string, 16))[2:].zfill(n_bits)
317
+ if len(binary_str) < n_piezos:
318
+ # Pad or truncate to match the actual probe size
319
+ binary_str = binary_str.ljust(n_piezos, '0')
320
+ elif len(binary_str) > n_piezos:
321
+ binary_str = binary_str[:n_piezos]
322
+ return np.array([int(b) for b in binary_str])
323
+ except ValueError:
324
+ return np.zeros(n_piezos, dtype=int)
325
+
326
+ def calculate_angle_from_delays(delays, c=1540):
327
+ """
328
+ Calculate the angle of incidence θ (in degrees) from an array of 192 delays.
329
+ Uses linear regression to estimate the slope of the delays.
330
+
331
+ Args:
332
+ delays: Array of 192 delays (in seconds).
333
+ c: Speed of sound (m/s).
334
+
335
+ Returns:
336
+ theta: Angle in degrees (positive to the right, negative to the left).
337
+ """
338
+ pitch = 0.2e-3 # Element spacing (m)
339
+ x = np.linspace(-(192-1)/2 * pitch, (192-1)/2 * pitch, 192) # Element positions (m)
340
+
341
+ # Linear regression to estimate the slope (sinθ / c)
342
+ slope, _, _, _, _ = linregress(x, delays)
343
+
344
+ # Calculate the angle (in degrees)
345
+ theta = np.rad2deg(np.arcsin(slope * c))
346
+
347
+ # Determine the sign based on the position of the maximum delay
348
+ max_index = np.argmax(delays)
349
+ if max_index < 95: # Left
350
+ theta = -abs(theta)
351
+ elif max_index > 95: # Right
352
+ theta = abs(theta)
353
+ else: # Center (θ ≈ 0)
354
+ theta = 0.0
355
+
356
+ return int(np.round(theta, 0))