AOT-biomaps 2.9.15__tar.gz → 2.9.354__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.
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Acoustic/AcousticTools.py +219 -0
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Acoustic/FocusedWave.py +244 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/PlaneWave.py +2 -2
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/StructuredWave.py +48 -209
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/_mainAcoustic.py +231 -27
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Experiment/ExperimentTools.py +69 -0
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Experiment/Tomography.py +741 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Experiment/_mainExperiment.py +203 -70
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Optic/_mainOptic.py +204 -0
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_Optimizers/DEPIERRO.py +191 -0
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_Optimizers/LS.py +496 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_Optimizers/MAPEM.py +173 -68
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_Optimizers/MLEM.py +551 -0
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_Optimizers/PDHG.py +652 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_Optimizers/__init__.py +2 -1
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/RelativeDifferences.py +10 -14
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/SparseSMatrix_CSR.py +274 -0
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/SparseSMatrix_SELL.py +360 -0
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/__init__.py +2 -0
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_biomaps_kernels.cubin +0 -0
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AlgebraicRecon.py +950 -0
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AnalyticRecon.py +419 -0
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/BayesianRecon.py +230 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/DeepLearningRecon.py +4 -1
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/PrimalDualRecon.py +263 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/ReconEnums.py +51 -2
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/ReconTools.py +692 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/__init__.py +1 -0
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/_mainRecon.py +291 -0
- aot_biomaps-2.9.354/AOT_biomaps/Config.py +95 -0
- aot_biomaps-2.9.354/AOT_biomaps/__init__.py +220 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps.egg-info/PKG-INFO +3 -4
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps.egg-info/SOURCES.txt +7 -1
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps.egg-info/requires.txt +2 -3
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/PKG-INFO +3 -4
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/README.md +4 -25
- aot_biomaps-2.9.354/setup.py +378 -0
- aot_biomaps-2.9.15/AOT_biomaps/AOT_Acoustic/AcousticTools.py +0 -258
- aot_biomaps-2.9.15/AOT_biomaps/AOT_Acoustic/FocusedWave.py +0 -329
- aot_biomaps-2.9.15/AOT_biomaps/AOT_Experiment/Tomography.py +0 -315
- aot_biomaps-2.9.15/AOT_biomaps/AOT_Optic/_mainOptic.py +0 -138
- aot_biomaps-2.9.15/AOT_biomaps/AOT_Recon/AOT_Optimizers/DEPIERRO.py +0 -132
- aot_biomaps-2.9.15/AOT_biomaps/AOT_Recon/AOT_Optimizers/MLEM.py +0 -275
- aot_biomaps-2.9.15/AOT_biomaps/AOT_Recon/AOT_Optimizers/PDHG.py +0 -182
- aot_biomaps-2.9.15/AOT_biomaps/AOT_Recon/AlgebraicRecon.py +0 -629
- aot_biomaps-2.9.15/AOT_biomaps/AOT_Recon/AnalyticRecon.py +0 -151
- aot_biomaps-2.9.15/AOT_biomaps/AOT_Recon/BayesianRecon.py +0 -156
- aot_biomaps-2.9.15/AOT_biomaps/AOT_Recon/PrimalDualRecon.py +0 -66
- aot_biomaps-2.9.15/AOT_biomaps/AOT_Recon/ReconTools.py +0 -272
- aot_biomaps-2.9.15/AOT_biomaps/AOT_Recon/_mainRecon.py +0 -149
- aot_biomaps-2.9.15/AOT_biomaps/Config.py +0 -70
- aot_biomaps-2.9.15/AOT_biomaps/__init__.py +0 -65
- aot_biomaps-2.9.15/setup.py +0 -40
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/AcousticEnums.py +0 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/IrregularWave.py +0 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/__init__.py +0 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Experiment/Focus.py +0 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Experiment/__init__.py +0 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Optic/Absorber.py +0 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Optic/Laser.py +0 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Optic/OpticEnums.py +0 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Optic/__init__.py +0 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Huber.py +0 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Quadratic.py +0 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/__init__.py +0 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps/Settings.py +0 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps.egg-info/dependency_links.txt +0 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/AOT_biomaps.egg-info/top_level.txt +0 -0
- {aot_biomaps-2.9.15 → aot_biomaps-2.9.354}/setup.cfg +0 -0
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
from AOT_biomaps.Config import config
|
|
2
|
+
|
|
3
|
+
from scipy.signal import hilbert
|
|
4
|
+
import os
|
|
5
|
+
import h5py
|
|
6
|
+
import numpy as np
|
|
7
|
+
import torch
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
9
|
+
|
|
10
|
+
def loadmat(param_path_mat):
|
|
11
|
+
"""
|
|
12
|
+
Charge un fichier .mat (format HDF5) sans SciPy.
|
|
13
|
+
Args:
|
|
14
|
+
param_path_mat: Chemin vers le fichier .mat.
|
|
15
|
+
Returns:
|
|
16
|
+
Dictionnaire contenant les variables du fichier.
|
|
17
|
+
"""
|
|
18
|
+
with h5py.File(param_path_mat, 'r') as f:
|
|
19
|
+
data = {}
|
|
20
|
+
for key in f.keys():
|
|
21
|
+
# Récupère les données et convertit en numpy array si nécessaire
|
|
22
|
+
item = f[key]
|
|
23
|
+
if isinstance(item, h5py.Dataset):
|
|
24
|
+
data[key] = item[()] # Convertit en numpy array
|
|
25
|
+
elif isinstance(item, h5py.Group):
|
|
26
|
+
# Pour les structures MATLAB (nested)
|
|
27
|
+
data[key] = {}
|
|
28
|
+
for subkey in item:
|
|
29
|
+
data[key][subkey] = item[subkey][()]
|
|
30
|
+
return data
|
|
31
|
+
|
|
32
|
+
def reshape_field(field, factor, device=None):
|
|
33
|
+
"""
|
|
34
|
+
Downsample a 3D or 4D field using PyTorch interpolation (auto-detects GPU/CPU).
|
|
35
|
+
Args:
|
|
36
|
+
field: Input field (numpy array or torch.Tensor).
|
|
37
|
+
factor: Downsampling factor (tuple of ints).
|
|
38
|
+
device: Force device ('cpu' or 'cuda'). If None, auto-detects GPU.
|
|
39
|
+
Returns:
|
|
40
|
+
Downsampled field (same type as input: numpy array or torch.Tensor).
|
|
41
|
+
"""
|
|
42
|
+
# Check input
|
|
43
|
+
if field is None:
|
|
44
|
+
raise ValueError("Acoustic field is not generated. Please generate the field first.")
|
|
45
|
+
|
|
46
|
+
# Auto-detect device if not specified
|
|
47
|
+
if device is None:
|
|
48
|
+
device = 'cuda' if torch.cuda.is_available() else 'cpu'
|
|
49
|
+
else:
|
|
50
|
+
device = device.lower()
|
|
51
|
+
if device not in ['cpu', 'cuda']:
|
|
52
|
+
raise ValueError("Device must be 'cpu' or 'cuda'.")
|
|
53
|
+
|
|
54
|
+
# Convert to torch.Tensor if needed
|
|
55
|
+
if isinstance(field, np.ndarray):
|
|
56
|
+
field = torch.from_numpy(field)
|
|
57
|
+
elif not isinstance(field, torch.Tensor):
|
|
58
|
+
raise TypeError("Input must be a numpy array or torch.Tensor.")
|
|
59
|
+
|
|
60
|
+
# Move to the target device
|
|
61
|
+
field = field.to(device)
|
|
62
|
+
|
|
63
|
+
# Add batch and channel dimensions (required by torch.interpolate)
|
|
64
|
+
if len(factor) == 3:
|
|
65
|
+
if field.dim() != 3:
|
|
66
|
+
raise ValueError("Expected 3D field.")
|
|
67
|
+
field = field.unsqueeze(0).unsqueeze(0) # (1, 1, D, H, W)
|
|
68
|
+
|
|
69
|
+
# Calculate new shape
|
|
70
|
+
new_shape = [
|
|
71
|
+
field.shape[2] // factor[0],
|
|
72
|
+
field.shape[3] // factor[1],
|
|
73
|
+
field.shape[4] // factor[2]
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
# Trilinear interpolation
|
|
77
|
+
downsampled = torch.nn.functional.interpolate(
|
|
78
|
+
field,
|
|
79
|
+
size=new_shape,
|
|
80
|
+
mode='trilinear',
|
|
81
|
+
align_corners=True
|
|
82
|
+
)
|
|
83
|
+
downsampled = downsampled.squeeze(0).squeeze(0) # Remove batch/channel dims
|
|
84
|
+
|
|
85
|
+
elif len(factor) == 4:
|
|
86
|
+
if field.dim() != 4:
|
|
87
|
+
raise ValueError("Expected 4D field.")
|
|
88
|
+
field = field.unsqueeze(0).unsqueeze(0) # (1, 1, T, D, H, W)
|
|
89
|
+
|
|
90
|
+
new_shape = [
|
|
91
|
+
field.shape[2] // factor[0],
|
|
92
|
+
field.shape[3] // factor[1],
|
|
93
|
+
field.shape[4] // factor[2],
|
|
94
|
+
field.shape[5] // factor[3]
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
# Tetra-linear interpolation
|
|
98
|
+
downsampled = torch.nn.functional.interpolate(
|
|
99
|
+
field,
|
|
100
|
+
size=new_shape,
|
|
101
|
+
mode='trilinear', # PyTorch uses 'trilinear' for both 3D and 4D
|
|
102
|
+
align_corners=True
|
|
103
|
+
)
|
|
104
|
+
downsampled = downsampled.squeeze(0).squeeze(0)
|
|
105
|
+
|
|
106
|
+
else:
|
|
107
|
+
raise ValueError("Unsupported dimension. Only 3D and 4D fields are supported.")
|
|
108
|
+
|
|
109
|
+
# Convert back to numpy if input was numpy
|
|
110
|
+
if isinstance(field, np.ndarray):
|
|
111
|
+
return downsampled.cpu().numpy()
|
|
112
|
+
else:
|
|
113
|
+
return downsampled.cpu().numpy()
|
|
114
|
+
|
|
115
|
+
def calculate_envelope_squared(field):
|
|
116
|
+
"""
|
|
117
|
+
Calcule l'enveloppe au carré du champ acoustique en utilisant scipy.signal.hilbert (CPU uniquement).
|
|
118
|
+
|
|
119
|
+
Args:
|
|
120
|
+
field: Champ acoustique (numpy.ndarray) de forme (T, X, Z) ou (T, X, Y, Z).
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
envelope (numpy.ndarray): Enveloppe au carré du champ acoustique.
|
|
124
|
+
"""
|
|
125
|
+
try:
|
|
126
|
+
if field is None:
|
|
127
|
+
raise ValueError("Le champ acoustique n'est pas généré. Veuillez d'abord générer le champ.")
|
|
128
|
+
|
|
129
|
+
if not isinstance(field, np.ndarray):
|
|
130
|
+
if hasattr(field, 'cpu'):
|
|
131
|
+
field = field.cpu().numpy() # Si c'est un tenseur PyTorch sur GPU/CPU
|
|
132
|
+
else:
|
|
133
|
+
field = np.array(field) # Conversion générique
|
|
134
|
+
|
|
135
|
+
if len(field.shape) not in [3, 4]:
|
|
136
|
+
raise ValueError("Le champ acoustique doit être un tableau 3D (T, X, Z) ou 4D (T, X, Y, Z).")
|
|
137
|
+
|
|
138
|
+
# Calcul de l'enveloppe avec scipy.signal.hilbert
|
|
139
|
+
if len(field.shape) == 3:
|
|
140
|
+
T, X, Z = field.shape
|
|
141
|
+
envelope = np.zeros_like(field)
|
|
142
|
+
for x in range(X):
|
|
143
|
+
for z in range(Z):
|
|
144
|
+
envelope[:, x, z] = np.abs(hilbert(field[:, x, z], axis=0)) ** 2
|
|
145
|
+
elif len(field.shape) == 4:
|
|
146
|
+
T, X, Y, Z = field.shape
|
|
147
|
+
envelope = np.zeros_like(field)
|
|
148
|
+
for x in range(X):
|
|
149
|
+
for y in range(Y):
|
|
150
|
+
for z in range(Z):
|
|
151
|
+
envelope[:, x, y, z] = np.abs(hilbert(field[:, x, y, z], axis=0)) ** 2
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
return envelope
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
print(f"Erreur dans calculate_envelope_squared: {e}")
|
|
158
|
+
raise
|
|
159
|
+
|
|
160
|
+
def getPattern(pathFile):
|
|
161
|
+
"""
|
|
162
|
+
Get the pattern from a file path.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
pathFile (str): Path to the file containing the pattern.
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
str: The pattern string.
|
|
169
|
+
"""
|
|
170
|
+
try:
|
|
171
|
+
# Pattern between first _ and last _
|
|
172
|
+
pattern = os.path.basename(pathFile).split('_')[1:-1]
|
|
173
|
+
pattern_str = ''.join(pattern)
|
|
174
|
+
return pattern_str
|
|
175
|
+
except Exception as e:
|
|
176
|
+
print(f"Error reading pattern from file: {e}")
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
def detect_space_0_and_space_1(hex_string):
|
|
180
|
+
binary_string = bin(int(hex_string, 16))[2:].zfill(len(hex_string) * 4)
|
|
181
|
+
|
|
182
|
+
# Trouver la plus longue séquence de 0 consécutifs
|
|
183
|
+
zeros_groups = [len(s) for s in binary_string.split('1')]
|
|
184
|
+
space_0 = max(zeros_groups) if zeros_groups else 0
|
|
185
|
+
|
|
186
|
+
# Trouver la plus longue séquence de 1 consécutifs
|
|
187
|
+
ones_groups = [len(s) for s in binary_string.split('0')]
|
|
188
|
+
space_1 = max(ones_groups) if ones_groups else 0
|
|
189
|
+
|
|
190
|
+
return space_0, space_1
|
|
191
|
+
|
|
192
|
+
def getAngle(pathFile):
|
|
193
|
+
"""
|
|
194
|
+
Get the angle from a file path.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
pathFile (str): Path to the file containing the angle.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
int: The angle in degrees.
|
|
201
|
+
"""
|
|
202
|
+
try:
|
|
203
|
+
# Angle between last _ and .
|
|
204
|
+
angle_str = os.path.basename(pathFile).split('_')[-1].replace('.', '')
|
|
205
|
+
if angle_str.startswith('0'):
|
|
206
|
+
angle_str = angle_str[1:]
|
|
207
|
+
elif angle_str.startswith('1'):
|
|
208
|
+
angle_str = '-' + angle_str[1:]
|
|
209
|
+
else:
|
|
210
|
+
raise ValueError("Invalid angle format in file name.")
|
|
211
|
+
return int(angle_str)
|
|
212
|
+
except Exception as e:
|
|
213
|
+
print(f"Error reading angle from file: {e}")
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
def next_power_of_2(n):
|
|
217
|
+
"""Calculate the next power of 2 greater than or equal to n."""
|
|
218
|
+
return int(2 ** np.ceil(np.log2(n)))
|
|
219
|
+
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
from ._mainAcoustic import AcousticField
|
|
2
|
+
from .AcousticEnums import WaveType
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import numpy as np
|
|
6
|
+
import matplotlib.pyplot as plt
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FocusedWave(AcousticField):
|
|
11
|
+
|
|
12
|
+
def __init__(self, focal_line, **kwargs):
|
|
13
|
+
"""
|
|
14
|
+
Initialize the FocusedWave object.
|
|
15
|
+
|
|
16
|
+
Parameters:
|
|
17
|
+
- focal_line (tuple): The focal line coordinates (x) in meters.
|
|
18
|
+
- **kwargs: Additional keyword arguments for AcousticField initialization.
|
|
19
|
+
"""
|
|
20
|
+
super().__init__(**kwargs)
|
|
21
|
+
self.waveType = WaveType.FocusedWave
|
|
22
|
+
self.kgrid.setTime(int(self.kgrid.Nt*2),self.kgrid.dt) # Extend the time grid to allow for delays
|
|
23
|
+
self.focal_line = focal_line
|
|
24
|
+
self.delayedSignal = self._apply_delay()
|
|
25
|
+
|
|
26
|
+
def getName_field(self):
|
|
27
|
+
"""
|
|
28
|
+
Generate the name for the field file based on the focal line.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
str: File name for the system matrix file.
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
return f"field_focused_X{self.focal_line*1000:.2f}"
|
|
35
|
+
except Exception as e:
|
|
36
|
+
print(f"Error generating file name: {e}")
|
|
37
|
+
return None
|
|
38
|
+
|
|
39
|
+
def _apply_delay(self, dx=None):
|
|
40
|
+
try:
|
|
41
|
+
if dx is None:
|
|
42
|
+
dx = self.params['dx']
|
|
43
|
+
|
|
44
|
+
# 1. Grid and element setup (utilise les paramètres utilisateur)
|
|
45
|
+
element_width_grid_points = int(round(self.params['element_width'] / dx))
|
|
46
|
+
total_grid_points = self.params['num_elements'] * element_width_grid_points
|
|
47
|
+
element_positions = np.linspace(
|
|
48
|
+
self.params['Xrange'][0] + self.params['element_width']/2,
|
|
49
|
+
self.params['Xrange'][1] - self.params['element_width'],
|
|
50
|
+
self.params['num_elements']
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# 2. Select active elements (utilise les paramètres utilisateur)
|
|
54
|
+
center_idx = np.argmin(np.abs(element_positions - self.focal_line))
|
|
55
|
+
half_N = self.params['N_piezoFocal'] // 2
|
|
56
|
+
start_idx = max(0, center_idx - half_N)
|
|
57
|
+
end_idx = min(self.params['num_elements'] - 1, center_idx + half_N - 1)
|
|
58
|
+
active_elements = np.arange(start_idx, end_idx + 1)
|
|
59
|
+
active_element_positions = element_positions[active_elements]
|
|
60
|
+
|
|
61
|
+
# 3. Calculate relative positions
|
|
62
|
+
x_rel = active_element_positions - self.focal_line
|
|
63
|
+
a = np.max(np.abs(x_rel))
|
|
64
|
+
|
|
65
|
+
# 4. Calculate the parabolic delays (utilise les paramètres utilisateur)
|
|
66
|
+
tau_max = (a**2) / (2 * self.params['Foc'] * self.params['c0'])
|
|
67
|
+
delays = tau_max * (1 - (x_rel / a)**2)
|
|
68
|
+
|
|
69
|
+
# 5. Force the delays at the edges to be zero
|
|
70
|
+
tau_edge = delays[0]
|
|
71
|
+
delays -= tau_edge
|
|
72
|
+
|
|
73
|
+
# 6. Convert delays to samples
|
|
74
|
+
delay_samples = np.round(delays / self.kgrid.dt).astype(int)
|
|
75
|
+
max_delay_samples = np.max(delay_samples)
|
|
76
|
+
print(f"Max delay (samples): {max_delay_samples}")
|
|
77
|
+
|
|
78
|
+
# 7. Initialize output
|
|
79
|
+
delayed_signals = np.zeros((total_grid_points, len(self.burst) + max_delay_samples))
|
|
80
|
+
|
|
81
|
+
# 8. Apply delays
|
|
82
|
+
for elem_idx, elem_delay in zip(active_elements, delay_samples):
|
|
83
|
+
start_grid = elem_idx * element_width_grid_points
|
|
84
|
+
end_grid = start_grid + element_width_grid_points
|
|
85
|
+
for grid_idx in range(start_grid, end_grid):
|
|
86
|
+
if elem_delay >= 0 and elem_delay + len(self.burst) <= delayed_signals.shape[1]:
|
|
87
|
+
delayed_signals[grid_idx, elem_delay:elem_delay + len(self.burst)] = self.burst
|
|
88
|
+
|
|
89
|
+
return delayed_signals
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print(f"Error applying delay: {e}")
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def plot_delay(self):
|
|
97
|
+
"""
|
|
98
|
+
Plot the time of the maximum of each delayed signal to visualize the wavefront.
|
|
99
|
+
"""
|
|
100
|
+
try:
|
|
101
|
+
# Find the index of the maximum for each delayed signal
|
|
102
|
+
max_indices = np.argmax(self.delayedSignal, axis=1)
|
|
103
|
+
element_indices = np.linspace(0, self.params['num_elements'] - 1, self.delayedSignal.shape[0])
|
|
104
|
+
# Convert indices to time
|
|
105
|
+
max_times = max_indices / self.params['f_AQ'] * 1e6
|
|
106
|
+
|
|
107
|
+
# Détermine la valeur minimale des temps de maximum (pour les éléments actifs)
|
|
108
|
+
min_active_time = np.min(max_times[max_times > 0])
|
|
109
|
+
|
|
110
|
+
# Plot the times of the maxima
|
|
111
|
+
plt.figure(figsize=(10, 6))
|
|
112
|
+
plt.plot(element_indices, max_times, 'o-')
|
|
113
|
+
plt.title('Time of Maximum for Each Delayed Signal')
|
|
114
|
+
plt.xlabel('Transducer Element Index')
|
|
115
|
+
plt.ylabel('Time of Maximum (µs)')
|
|
116
|
+
plt.grid(True)
|
|
117
|
+
|
|
118
|
+
# Ajuste l'échelle de l'axe Y pour commencer à la valeur minimale des éléments actifs
|
|
119
|
+
plt.ylim(bottom=min_active_time * 0.95) # Ajoute une marge de 5% pour plus de lisibilité
|
|
120
|
+
plt.show()
|
|
121
|
+
except Exception as e:
|
|
122
|
+
print(f"Error plotting max times: {e}")
|
|
123
|
+
|
|
124
|
+
def _SetUpSource(self, source, Nx, dx, factorT):
|
|
125
|
+
"""
|
|
126
|
+
Set up source for both 2D and 3D focused waves.
|
|
127
|
+
"""
|
|
128
|
+
element_width_grid_points = int(round(self.params['element_width'] / dx))
|
|
129
|
+
|
|
130
|
+
if source.p_mask.ndim == 2:
|
|
131
|
+
# --- 2D ---
|
|
132
|
+
element_positions = np.linspace(
|
|
133
|
+
self.params['Xrange'][0] + self.params['element_width'] / 2,
|
|
134
|
+
self.params['Xrange'][1] - self.params['element_width'] / 2,
|
|
135
|
+
self.params['num_elements']
|
|
136
|
+
)
|
|
137
|
+
center_idx = np.argmin(np.abs(element_positions - self.focal_line))
|
|
138
|
+
start_idx = max(0, center_idx - self.params['N_piezoFocal'] // 2)
|
|
139
|
+
end_idx = min(self.params['num_elements'] - 1, start_idx + self.params['N_piezoFocal'] - 1)
|
|
140
|
+
selected_indices = np.arange(start_idx, end_idx + 1)
|
|
141
|
+
|
|
142
|
+
current_position = (Nx - self.params['num_elements'] * element_width_grid_points) // 2
|
|
143
|
+
active_grid_indices = []
|
|
144
|
+
for i in range(self.params['num_elements']):
|
|
145
|
+
x_start = current_position
|
|
146
|
+
x_end = current_position + element_width_grid_points
|
|
147
|
+
if i in selected_indices:
|
|
148
|
+
source.p_mask[x_start:x_end, 0] = 1
|
|
149
|
+
active_grid_indices.extend(range(x_start, x_end))
|
|
150
|
+
current_position += element_width_grid_points
|
|
151
|
+
|
|
152
|
+
delayed_signals = self._apply_delay(dx=dx) if factorT != 1 else self.delayedSignal
|
|
153
|
+
source.p = float(self.params['voltage']) * float(self.params['sensitivity']) * delayed_signals[active_grid_indices, :]
|
|
154
|
+
|
|
155
|
+
elif source.p_mask.ndim == 3:
|
|
156
|
+
# --- 3D ---
|
|
157
|
+
center_index_x = Nx // 2
|
|
158
|
+
center_index_y = self.params['Ny'] // 2
|
|
159
|
+
|
|
160
|
+
for i in range(self.params['num_elements']):
|
|
161
|
+
x_pos = center_index_x - (self.params['num_elements'] // 2) * element_width_grid_points + i * element_width_grid_points
|
|
162
|
+
source.p_mask[x_pos, center_index_y, 0] = 1
|
|
163
|
+
|
|
164
|
+
delayed_signals = self._apply_delay()
|
|
165
|
+
source.p = float(self.params['voltage']) * float(self.params['sensitivity']) * delayed_signals.T
|
|
166
|
+
|
|
167
|
+
def _save2D_HDR_IMG(self, filePath):
|
|
168
|
+
"""
|
|
169
|
+
Save the acoustic field to .img and .hdr files.
|
|
170
|
+
|
|
171
|
+
Parameters:
|
|
172
|
+
- filePath (str): Path to the folder where files will be saved.
|
|
173
|
+
"""
|
|
174
|
+
try:
|
|
175
|
+
t_ex = 1 / self.params['f_US']
|
|
176
|
+
x_focal, z_focal = self.focal_point
|
|
177
|
+
|
|
178
|
+
# Define file names (img and hdr)
|
|
179
|
+
file_name = f"field_focused_{x_focal:.2f}_{z_focal:.2f}"
|
|
180
|
+
|
|
181
|
+
img_path = os.path.join(filePath, file_name + ".img")
|
|
182
|
+
hdr_path = os.path.join(filePath, file_name + ".hdr")
|
|
183
|
+
|
|
184
|
+
# Save the acoustic field to the .img file
|
|
185
|
+
with open(img_path, "wb") as f_img:
|
|
186
|
+
self.field.astype('float32').tofile(f_img)
|
|
187
|
+
|
|
188
|
+
# Generate headerFieldGlob
|
|
189
|
+
headerFieldGlob = (
|
|
190
|
+
f"!INTERFILE :=\n"
|
|
191
|
+
f"modality : AOT\n"
|
|
192
|
+
f"voxels number transaxial: {self.field.shape[2]}\n"
|
|
193
|
+
f"voxels number transaxial 2: {self.field.shape[1]}\n"
|
|
194
|
+
f"voxels number axial: {1}\n"
|
|
195
|
+
f"field of view transaxial: {(self.params['Xrange'][1] - self.params['Xrange'][0]) * 1000}\n"
|
|
196
|
+
f"field of view transaxial 2: {(self.params['Zrange'][1] - self.params['Zrange'][0]) * 1000}\n"
|
|
197
|
+
f"field of view axial: {1}\n"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
# Generate header
|
|
201
|
+
header = (
|
|
202
|
+
f"!INTERFILE :=\n"
|
|
203
|
+
f"!imaging modality := AOT\n\n"
|
|
204
|
+
f"!GENERAL DATA :=\n"
|
|
205
|
+
f"!data offset in bytes := 0\n"
|
|
206
|
+
f"!name of data file := system_matrix/{file_name}.img\n\n"
|
|
207
|
+
f"!GENERAL IMAGE DATA\n"
|
|
208
|
+
f"!total number of images := {self.field.shape[0]}\n"
|
|
209
|
+
f"imagedata byte order := LITTLEENDIAN\n"
|
|
210
|
+
f"!number of frame groups := 1\n\n"
|
|
211
|
+
f"!STATIC STUDY (General) :=\n"
|
|
212
|
+
f"number of dimensions := 3\n"
|
|
213
|
+
f"!matrix size [1] := {self.field.shape[2]}\n"
|
|
214
|
+
f"!matrix size [2] := {self.field.shape[1]}\n"
|
|
215
|
+
f"!matrix size [3] := {self.field.shape[0]}\n"
|
|
216
|
+
f"!number format := short float\n"
|
|
217
|
+
f"!number of bytes per pixel := 4\n"
|
|
218
|
+
f"scaling factor (mm/pixel) [1] := {self.params['dx'] * 1000}\n"
|
|
219
|
+
f"scaling factor (mm/pixel) [2] := {self.params['dx'] * 1000}\n"
|
|
220
|
+
f"scaling factor (s/pixel) [3] := {1 / self.params['f_AQ']}\n"
|
|
221
|
+
f"first pixel offset (mm) [1] := {self.params['Xrange'][0] * 1e3}\n"
|
|
222
|
+
f"first pixel offset (mm) [2] := {self.params['Zrange'][0] * 1e3}\n"
|
|
223
|
+
f"first pixel offset (s) [3] := 0\n"
|
|
224
|
+
f"data rescale offset := 0\n"
|
|
225
|
+
f"data rescale slope := 1\n"
|
|
226
|
+
f"quantification units := 1\n\n"
|
|
227
|
+
f"!SPECIFIC PARAMETERS :=\n"
|
|
228
|
+
f"focal point (x, z) := {x_focal}, {z_focal}\n"
|
|
229
|
+
f"number of US transducers := {self.params['num_elements']}\n"
|
|
230
|
+
f"delay (s) := 0\n"
|
|
231
|
+
f"us frequency (Hz) := {self.params['f_US']}\n"
|
|
232
|
+
f"excitation duration (s) := {t_ex}\n"
|
|
233
|
+
f"!END OF INTERFILE :=\n"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Save the .hdr file
|
|
237
|
+
with open(hdr_path, "w") as f_hdr:
|
|
238
|
+
f_hdr.write(header)
|
|
239
|
+
|
|
240
|
+
with open(os.path.join(filePath, "field.hdr"), "w") as f_hdr2:
|
|
241
|
+
f_hdr2.write(headerFieldGlob)
|
|
242
|
+
except Exception as e:
|
|
243
|
+
print(f"Error saving HDR/IMG files: {e}")
|
|
244
|
+
|
|
@@ -3,7 +3,7 @@ from .AcousticEnums import WaveType
|
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class PlaneWave(StructuredWave):
|
|
6
|
-
def __init__(self,
|
|
6
|
+
def __init__(self, angle_deg, **kwargs):
|
|
7
7
|
"""
|
|
8
8
|
Initialize the PlaneWave object.
|
|
9
9
|
|
|
@@ -12,7 +12,7 @@ class PlaneWave(StructuredWave):
|
|
|
12
12
|
**kwargs: Additional keyword arguments.
|
|
13
13
|
"""
|
|
14
14
|
try:
|
|
15
|
-
super().__init__(
|
|
15
|
+
super().__init__(angle_deg=angle_deg, fileName=None, space_0=0, space_1=192, move_head_0_2tail=0, move_tail_1_2head=0, **kwargs)
|
|
16
16
|
self.waveType = WaveType.PlaneWave
|
|
17
17
|
except Exception as e:
|
|
18
18
|
print(f"Error initializing PlaneWave: {e}")
|