AOT-biomaps 2.1.3__py3-none-any.whl → 2.9.233__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 AOT-biomaps might be problematic. Click here for more details.
- AOT_biomaps/AOT_Acoustic/AcousticEnums.py +64 -0
- AOT_biomaps/AOT_Acoustic/AcousticTools.py +221 -0
- AOT_biomaps/AOT_Acoustic/FocusedWave.py +244 -0
- AOT_biomaps/AOT_Acoustic/IrregularWave.py +66 -0
- AOT_biomaps/AOT_Acoustic/PlaneWave.py +43 -0
- AOT_biomaps/AOT_Acoustic/StructuredWave.py +392 -0
- AOT_biomaps/AOT_Acoustic/__init__.py +15 -0
- AOT_biomaps/AOT_Acoustic/_mainAcoustic.py +978 -0
- AOT_biomaps/AOT_Experiment/Focus.py +55 -0
- AOT_biomaps/AOT_Experiment/Tomography.py +505 -0
- AOT_biomaps/AOT_Experiment/__init__.py +9 -0
- AOT_biomaps/AOT_Experiment/_mainExperiment.py +532 -0
- AOT_biomaps/AOT_Optic/Absorber.py +24 -0
- AOT_biomaps/AOT_Optic/Laser.py +70 -0
- AOT_biomaps/AOT_Optic/OpticEnums.py +17 -0
- AOT_biomaps/AOT_Optic/__init__.py +10 -0
- AOT_biomaps/AOT_Optic/_mainOptic.py +204 -0
- AOT_biomaps/AOT_Recon/AOT_Optimizers/DEPIERRO.py +191 -0
- AOT_biomaps/AOT_Recon/AOT_Optimizers/LS.py +106 -0
- AOT_biomaps/AOT_Recon/AOT_Optimizers/MAPEM.py +456 -0
- AOT_biomaps/AOT_Recon/AOT_Optimizers/MLEM.py +333 -0
- AOT_biomaps/AOT_Recon/AOT_Optimizers/PDHG.py +221 -0
- AOT_biomaps/AOT_Recon/AOT_Optimizers/__init__.py +5 -0
- AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Huber.py +90 -0
- AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Quadratic.py +86 -0
- AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/RelativeDifferences.py +59 -0
- AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/__init__.py +3 -0
- AOT_biomaps/AOT_Recon/AlgebraicRecon.py +1023 -0
- AOT_biomaps/AOT_Recon/AnalyticRecon.py +154 -0
- AOT_biomaps/AOT_Recon/BayesianRecon.py +230 -0
- AOT_biomaps/AOT_Recon/DeepLearningRecon.py +35 -0
- AOT_biomaps/AOT_Recon/PrimalDualRecon.py +210 -0
- AOT_biomaps/AOT_Recon/ReconEnums.py +375 -0
- AOT_biomaps/AOT_Recon/ReconTools.py +273 -0
- AOT_biomaps/AOT_Recon/__init__.py +11 -0
- AOT_biomaps/AOT_Recon/_mainRecon.py +288 -0
- AOT_biomaps/Config.py +95 -0
- AOT_biomaps/Settings.py +45 -13
- AOT_biomaps/__init__.py +271 -18
- aot_biomaps-2.9.233.dist-info/METADATA +22 -0
- aot_biomaps-2.9.233.dist-info/RECORD +43 -0
- {AOT_biomaps-2.1.3.dist-info → aot_biomaps-2.9.233.dist-info}/WHEEL +1 -1
- AOT_biomaps/AOT_Acoustic.py +0 -1881
- AOT_biomaps/AOT_Experiment.py +0 -541
- AOT_biomaps/AOT_Optic.py +0 -219
- AOT_biomaps/AOT_Reconstruction.py +0 -1416
- AOT_biomaps/config.py +0 -54
- AOT_biomaps-2.1.3.dist-info/METADATA +0 -20
- AOT_biomaps-2.1.3.dist-info/RECORD +0 -11
- {AOT_biomaps-2.1.3.dist-info → aot_biomaps-2.9.233.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,64 @@
|
|
|
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
|
+
class Dim(Enum):
|
|
22
|
+
"""
|
|
23
|
+
Enum for the dimension of the acoustic field.
|
|
24
|
+
|
|
25
|
+
Selection of dimensions:
|
|
26
|
+
- D2: 2D field.
|
|
27
|
+
- D3: 3D field.
|
|
28
|
+
"""
|
|
29
|
+
D2 = '2D'
|
|
30
|
+
"""2D field."""
|
|
31
|
+
D3 = '3D'
|
|
32
|
+
"""3D field."""
|
|
33
|
+
|
|
34
|
+
class FormatSave(Enum):
|
|
35
|
+
"""
|
|
36
|
+
Enum for different file formats to save the acoustic field.
|
|
37
|
+
|
|
38
|
+
Selection of file formats:
|
|
39
|
+
- HDR_IMG: Interfile format (.hdr and .img).
|
|
40
|
+
- H5: HDF5 format (.h5).
|
|
41
|
+
- NPY: NumPy format (.npy).
|
|
42
|
+
"""
|
|
43
|
+
HDR_IMG = '.hdr'
|
|
44
|
+
"""Interfile format (.hdr and .img)."""
|
|
45
|
+
H5 = '.h5'
|
|
46
|
+
"""HDF5 format (.h5)."""
|
|
47
|
+
NPY = '.npy'
|
|
48
|
+
"""NumPy format (.npy)."""
|
|
49
|
+
|
|
50
|
+
class WaveType(Enum):
|
|
51
|
+
"""
|
|
52
|
+
Enum for different types of acoustic waves.
|
|
53
|
+
|
|
54
|
+
Selection of wave types:
|
|
55
|
+
- FocusedWave: A wave type where the energy is focused at a specific point.
|
|
56
|
+
- StructuredWave: A wave type characterized by a specific pattern or structure.
|
|
57
|
+
- PlaneWave: A wave type where the wavefronts are parallel and travel in a single direction.
|
|
58
|
+
"""
|
|
59
|
+
FocusedWave = 'focus'
|
|
60
|
+
"""A wave type where the energy is focused at a specific point."""
|
|
61
|
+
StructuredWave = 'structured'
|
|
62
|
+
"""A wave type characterized by a specific pattern or structure."""
|
|
63
|
+
PlaneWave = 'plane'
|
|
64
|
+
"""A wave type where the wavefronts are parallel and travel in a single direction."""
|
|
@@ -0,0 +1,221 @@
|
|
|
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
|
+
|
|
161
|
+
|
|
162
|
+
def getPattern(pathFile):
|
|
163
|
+
"""
|
|
164
|
+
Get the pattern from a file path.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
pathFile (str): Path to the file containing the pattern.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
str: The pattern string.
|
|
171
|
+
"""
|
|
172
|
+
try:
|
|
173
|
+
# Pattern between first _ and last _
|
|
174
|
+
pattern = os.path.basename(pathFile).split('_')[1:-1]
|
|
175
|
+
pattern_str = ''.join(pattern)
|
|
176
|
+
return pattern_str
|
|
177
|
+
except Exception as e:
|
|
178
|
+
print(f"Error reading pattern from file: {e}")
|
|
179
|
+
return None
|
|
180
|
+
|
|
181
|
+
def detect_space_0_and_space_1(hex_string):
|
|
182
|
+
binary_string = bin(int(hex_string, 16))[2:].zfill(len(hex_string) * 4)
|
|
183
|
+
|
|
184
|
+
# Trouver la plus longue séquence de 0 consécutifs
|
|
185
|
+
zeros_groups = [len(s) for s in binary_string.split('1')]
|
|
186
|
+
space_0 = max(zeros_groups) if zeros_groups else 0
|
|
187
|
+
|
|
188
|
+
# Trouver la plus longue séquence de 1 consécutifs
|
|
189
|
+
ones_groups = [len(s) for s in binary_string.split('0')]
|
|
190
|
+
space_1 = max(ones_groups) if ones_groups else 0
|
|
191
|
+
|
|
192
|
+
return space_0, space_1
|
|
193
|
+
|
|
194
|
+
def getAngle(pathFile):
|
|
195
|
+
"""
|
|
196
|
+
Get the angle from a file path.
|
|
197
|
+
|
|
198
|
+
Args:
|
|
199
|
+
pathFile (str): Path to the file containing the angle.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
int: The angle in degrees.
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
# Angle between last _ and .
|
|
206
|
+
angle_str = os.path.basename(pathFile).split('_')[-1].replace('.', '')
|
|
207
|
+
if angle_str.startswith('0'):
|
|
208
|
+
angle_str = angle_str[1:]
|
|
209
|
+
elif angle_str.startswith('1'):
|
|
210
|
+
angle_str = '-' + angle_str[1:]
|
|
211
|
+
else:
|
|
212
|
+
raise ValueError("Invalid angle format in file name.")
|
|
213
|
+
return int(angle_str)
|
|
214
|
+
except Exception as e:
|
|
215
|
+
print(f"Error reading angle from file: {e}")
|
|
216
|
+
return None
|
|
217
|
+
|
|
218
|
+
def next_power_of_2(n):
|
|
219
|
+
"""Calculate the next power of 2 greater than or equal to n."""
|
|
220
|
+
return int(2 ** np.ceil(np.log2(n)))
|
|
221
|
+
|
|
@@ -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
|
+
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from AOT_biomaps.AOT_Acoustic._mainAcoustic import AcousticField
|
|
2
|
+
from .AcousticEnums import WaveType, TypeSim
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
class IrregularWave(AcousticField):
|
|
7
|
+
"""
|
|
8
|
+
Class for irregular wave types, inheriting from AcousticField.
|
|
9
|
+
This class is a placeholder for future implementation of irregular wave types.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def __init__(self, **kwargs):
|
|
13
|
+
super().__init__(**kwargs)
|
|
14
|
+
self.waveType = WaveType.IrregularWave
|
|
15
|
+
self.params = {
|
|
16
|
+
'typeSim': TypeSim.IRREGULAR.value,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
def getName_field(self):
|
|
20
|
+
raise NotImplementedError("getName_field method not implemented for IrregularWave.")
|
|
21
|
+
|
|
22
|
+
def _generate_diverse_structurations(self,num_elements, num_sequences, num_frequencies):
|
|
23
|
+
"""
|
|
24
|
+
Génère num_sequences structurations irrégulières ON/OFF pour une sonde de num_elements éléments.
|
|
25
|
+
Chaque structuration contient exactement num_frequencies fréquences spatiales distinctes.
|
|
26
|
+
|
|
27
|
+
:param num_elements: Nombre total d'éléments piézoélectriques de la sonde.
|
|
28
|
+
:param num_sequences: Nombre total de structurations générées.
|
|
29
|
+
:param num_frequencies: Nombre de fréquences spatiales distinctes par structuration.
|
|
30
|
+
:return: Matrice de structuration de taille (num_sequences, num_elements)
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
# Définition des fréquences spatiales disponibles
|
|
34
|
+
max_freq = num_elements // 2 # Nyquist limit
|
|
35
|
+
available_frequencies = np.arange(1, max_freq + 1) # Fréquences possibles
|
|
36
|
+
|
|
37
|
+
# Matrice des structurations
|
|
38
|
+
structurations = np.zeros((num_sequences, num_elements), dtype=int)
|
|
39
|
+
|
|
40
|
+
# Sélectionner des fréquences uniques pour chaque structuration
|
|
41
|
+
chosen_frequencies = []
|
|
42
|
+
for _ in range(num_sequences):
|
|
43
|
+
freqs = np.random.choice(available_frequencies, size=num_frequencies, replace=False)
|
|
44
|
+
chosen_frequencies.append(freqs)
|
|
45
|
+
|
|
46
|
+
# Construire la structuration correspondante
|
|
47
|
+
structuration = np.zeros(num_elements)
|
|
48
|
+
for f in freqs:
|
|
49
|
+
structuration += np.cos(2 * np.pi * f * np.arange(num_elements) / num_elements) # Ajouter la fréquence
|
|
50
|
+
|
|
51
|
+
structuration = np.where(structuration >= 0, 1, 0) # Binarisation ON/OFF
|
|
52
|
+
structurations[_] = structuration
|
|
53
|
+
|
|
54
|
+
return structurations, chosen_frequencies
|
|
55
|
+
|
|
56
|
+
def getName_field(self):
|
|
57
|
+
raise NotImplementedError("getName_field method not implemented for IrregularWave.")
|
|
58
|
+
|
|
59
|
+
def _generate_2Dacoustic_field_KWAVE(self):
|
|
60
|
+
raise NotImplementedError("2D acoustic field generation not implemented for IrregularWave.")
|
|
61
|
+
|
|
62
|
+
def _generate_3Dacoustic_field_KWAVE(self):
|
|
63
|
+
raise NotImplementedError("3D acoustic field generation not implemented for IrregularWave.")
|
|
64
|
+
|
|
65
|
+
def _save2D_HDR_IMG(self, filePath):
|
|
66
|
+
raise NotImplementedError("HDR/IMG saving not implemented for IrregularWave.")
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from .StructuredWave import StructuredWave
|
|
2
|
+
from .AcousticEnums import WaveType
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class PlaneWave(StructuredWave):
|
|
6
|
+
def __init__(self, angle_deg, **kwargs):
|
|
7
|
+
"""
|
|
8
|
+
Initialize the PlaneWave object.
|
|
9
|
+
|
|
10
|
+
Args:
|
|
11
|
+
angle_deg (float): Angle in degrees.
|
|
12
|
+
**kwargs: Additional keyword arguments.
|
|
13
|
+
"""
|
|
14
|
+
try:
|
|
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
|
+
self.waveType = WaveType.PlaneWave
|
|
17
|
+
except Exception as e:
|
|
18
|
+
print(f"Error initializing PlaneWave: {e}")
|
|
19
|
+
raise
|
|
20
|
+
|
|
21
|
+
def _check_angle(self):
|
|
22
|
+
"""
|
|
23
|
+
Check if the angle is within the valid range.
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ValueError: If the angle is not between -20 and 20 degrees.
|
|
27
|
+
"""
|
|
28
|
+
if self.angle < -20 or self.angle > 20:
|
|
29
|
+
raise ValueError("Angle must be between -20 and 20 degrees.")
|
|
30
|
+
|
|
31
|
+
def getName_field(self):
|
|
32
|
+
"""
|
|
33
|
+
Generate the list of system matrix .hdr file paths for this wave.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
str: File path for the system matrix .hdr file.
|
|
37
|
+
"""
|
|
38
|
+
try:
|
|
39
|
+
angle_str = self._format_angle()
|
|
40
|
+
return f"field_{self.pattern.activeList}_{angle_str}"
|
|
41
|
+
except Exception as e:
|
|
42
|
+
print(f"Error generating file path: {e}")
|
|
43
|
+
return None
|