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.
- 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,55 @@
|
|
|
1
|
+
from AOT_biomaps.AOT_Acoustic.AcousticEnums import WaveType
|
|
2
|
+
from ._mainExperiment import Experiment
|
|
3
|
+
|
|
4
|
+
class Focus(Experiment):
|
|
5
|
+
def __init__(self, **kwargs):
|
|
6
|
+
super().__init__(**kwargs)
|
|
7
|
+
|
|
8
|
+
# PUBLIC METHODS
|
|
9
|
+
|
|
10
|
+
def check(self):
|
|
11
|
+
"""
|
|
12
|
+
Check if the experiment is correctly initialized.
|
|
13
|
+
"""
|
|
14
|
+
if self.TypeAcoustic is None or self.TypeAcoustic.value != WaveType.FocusedWave.value:
|
|
15
|
+
return False, "acousticType must be provided and must be FocusedWave for Focus experiment"
|
|
16
|
+
if self.AcousticFields is None:
|
|
17
|
+
return False, "AcousticFields is not initialized. Please generate the system matrix first."
|
|
18
|
+
if self.AOsignal_withTumor is None:
|
|
19
|
+
return False, "AOsignal with tumor is not initialized. Please generate the AO signal with tumor first."
|
|
20
|
+
if self.AOsignal_withoutTumor is None:
|
|
21
|
+
return False, "AOsignal without tumor is not initialized. Please generate the AO signal without tumor first."
|
|
22
|
+
if self.OpticImage is None:
|
|
23
|
+
return False, "OpticImage is not initialized. Please generate the optic image first."
|
|
24
|
+
if self.AOsignal_withoutTumor.shape != self.AOsignal_withTumor.shape:
|
|
25
|
+
return False, "AOsignal with and without tumor must have the same shape."
|
|
26
|
+
for field in self.AcousticFields:
|
|
27
|
+
if field.field.shape[0] != self.AOsignal_withTumor.shape[0]:
|
|
28
|
+
return False, f"Field {field.getName_field()} has an invalid Time shape: {field.field.shape[0]}. Expected time shape to be {self.AOsignal_withTumor.shape[0]}."
|
|
29
|
+
if not all(field.field.shape == self.AcousticFields[0].field.shape for field in self.AcousticFields):
|
|
30
|
+
return False, "All AcousticFields must have the same shape."
|
|
31
|
+
if self.OpticImage is None:
|
|
32
|
+
return False, "OpticImage is not initialized. Please generate the optic image first."
|
|
33
|
+
if self.OpticImage.phantom is None:
|
|
34
|
+
return False, "OpticImage phantom is not initialized. Please generate the phantom first."
|
|
35
|
+
if self.OpticImage.laser is None:
|
|
36
|
+
return False, "OpticImage laser is not initialized. Please generate the laser first."
|
|
37
|
+
if self.OpticImage.laser.shape != self.OpticImage.phantom.shape:
|
|
38
|
+
return False, "OpticImage laser and phantom must have the same shape."
|
|
39
|
+
if self.OpticImage.phantom.shape[0] != self.AcousticFields[0].field.shape[1] or self.OpticImage.phantom.shape[1] != self.AcousticFields[0].field.shape[2]:
|
|
40
|
+
return False, f"OpticImage phantom shape {self.OpticImage.phantom.shape} does not match AcousticFields shape {self.AcousticFields[0].field.shape[1:]}."
|
|
41
|
+
|
|
42
|
+
return True, "Experiment is correctly initialized."
|
|
43
|
+
|
|
44
|
+
def generateAcousticFields(self, fieldDataPath, fieldParamPath, show_log = True):
|
|
45
|
+
"""
|
|
46
|
+
Generate the acoustic fields for simulation.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
fieldDataPath: Path to save the generated fields.
|
|
50
|
+
fieldParamPath: Path to the field parameters file.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
systemMatrix: A numpy array of the generated fields.
|
|
54
|
+
"""
|
|
55
|
+
pass
|
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
from ._mainExperiment import Experiment
|
|
2
|
+
from AOT_biomaps.AOT_Acoustic.AcousticEnums import WaveType
|
|
3
|
+
from AOT_biomaps.AOT_Acoustic.StructuredWave import StructuredWave
|
|
4
|
+
from AOT_biomaps.Config import config
|
|
5
|
+
import os
|
|
6
|
+
import psutil
|
|
7
|
+
import numpy as np
|
|
8
|
+
import matplotlib.pyplot as plt
|
|
9
|
+
from tqdm import trange
|
|
10
|
+
import h5py
|
|
11
|
+
from scipy.io import loadmat
|
|
12
|
+
|
|
13
|
+
class Tomography(Experiment):
|
|
14
|
+
def __init__(self, **kwargs):
|
|
15
|
+
super().__init__(**kwargs)
|
|
16
|
+
self.patterns = None
|
|
17
|
+
|
|
18
|
+
# PUBLIC METHODS
|
|
19
|
+
def check(self):
|
|
20
|
+
"""
|
|
21
|
+
Check if the experiment is correctly initialized.
|
|
22
|
+
"""
|
|
23
|
+
if self.TypeAcoustic is None or self.TypeAcoustic.value == WaveType.FocusedWave.value:
|
|
24
|
+
return False, "acousticType must be provided and cannot be FocusedWave for Tomography experiment"
|
|
25
|
+
if self.AcousticFields is None:
|
|
26
|
+
return False, "AcousticFields is not initialized. Please generate the system matrix first."
|
|
27
|
+
if self.AOsignal_withTumor is None:
|
|
28
|
+
return False, "AOsignal with tumor is not initialized. Please generate the AO signal with tumor first."
|
|
29
|
+
if self.AOsignal_withoutTumor is None:
|
|
30
|
+
return False, "AOsignal without tumor is not initialized. Please generate the AO signal without tumor first."
|
|
31
|
+
if self.OpticImage is None:
|
|
32
|
+
return False, "OpticImage is not initialized. Please generate the optic image first."
|
|
33
|
+
if self.AOsignal_withoutTumor.shape != self.AOsignal_withTumor.shape:
|
|
34
|
+
return False, "AOsignal with and without tumor must have the same shape."
|
|
35
|
+
for field in self.AcousticFields:
|
|
36
|
+
if field.field.shape[0] != self.AOsignal_withTumor.shape[0]:
|
|
37
|
+
return False, f"Field {field.getName_field()} has an invalid Time shape: {field.field.shape[0]}. Expected time shape to be {self.AOsignal_withTumor.shape[0]}."
|
|
38
|
+
if not all(field.field.shape == self.AcousticFields[0].field.shape for field in self.AcousticFields):
|
|
39
|
+
return False, "All AcousticFields must have the same shape."
|
|
40
|
+
if self.OpticImage is None:
|
|
41
|
+
return False, "OpticImage is not initialized. Please generate the optic image first."
|
|
42
|
+
if self.OpticImage.phantom is None:
|
|
43
|
+
return False, "OpticImage phantom is not initialized. Please generate the phantom first."
|
|
44
|
+
if self.OpticImage.laser is None:
|
|
45
|
+
return False, "OpticImage laser is not initialized. Please generate the laser first."
|
|
46
|
+
if self.OpticImage.laser.shape != self.OpticImage.phantom.shape:
|
|
47
|
+
return False, "OpticImage laser and phantom must have the same shape."
|
|
48
|
+
if self.OpticImage.phantom.shape[0] != self.AcousticFields[0].field.shape[1] or self.OpticImage.phantom.shape[1] != self.AcousticFields[0].field.shape[2]:
|
|
49
|
+
return False, f"OpticImage phantom shape {self.OpticImage.phantom.shape} does not match AcousticFields shape {self.AcousticFields[0].field.shape[1:]}."
|
|
50
|
+
return True, "Experiment is correctly initialized."
|
|
51
|
+
|
|
52
|
+
def generateAcousticFields(self, fieldDataPath=None, show_log=True, nameBlock=None):
|
|
53
|
+
"""
|
|
54
|
+
Generate the acoustic fields for simulation.
|
|
55
|
+
Args:
|
|
56
|
+
fieldDataPath: Path to save the generated fields.
|
|
57
|
+
show_log: Whether to show progress logs.
|
|
58
|
+
Returns:
|
|
59
|
+
systemMatrix: A numpy array of the generated fields.
|
|
60
|
+
"""
|
|
61
|
+
if self.TypeAcoustic.value == WaveType.StructuredWave.value:
|
|
62
|
+
self.AcousticFields = self._generateAcousticFields_STRUCT_CPU(fieldDataPath, show_log, nameBlock)
|
|
63
|
+
else:
|
|
64
|
+
raise ValueError("Unsupported wave type.")
|
|
65
|
+
|
|
66
|
+
def show_pattern(self):
|
|
67
|
+
if self.AcousticFields is None:
|
|
68
|
+
raise ValueError("AcousticFields is not initialized. Please generate the system matrix first.")
|
|
69
|
+
|
|
70
|
+
# Collect and sort entries
|
|
71
|
+
entries = []
|
|
72
|
+
for field in self.AcousticFields:
|
|
73
|
+
if field.waveType != WaveType.StructuredWave:
|
|
74
|
+
raise TypeError("AcousticFields must be of type StructuredWave to plot pattern.")
|
|
75
|
+
pattern = field.pattern
|
|
76
|
+
entries.append((
|
|
77
|
+
(pattern.space_0, pattern.space_1, pattern.move_head_0_2tail, pattern.move_tail_1_2head),
|
|
78
|
+
pattern.activeList,
|
|
79
|
+
field.angle
|
|
80
|
+
))
|
|
81
|
+
|
|
82
|
+
entries.sort(key=lambda x: (
|
|
83
|
+
-(x[0][0] + x[0][1]),
|
|
84
|
+
-max(x[0][0], x[0][1]),
|
|
85
|
+
-x[0][0],
|
|
86
|
+
-x[0][2],
|
|
87
|
+
x[0][3]
|
|
88
|
+
))
|
|
89
|
+
|
|
90
|
+
# Extract data
|
|
91
|
+
hex_list = [hex_str for _, hex_str, _ in entries]
|
|
92
|
+
angle_list = [angle for _, _, angle in entries]
|
|
93
|
+
|
|
94
|
+
def hex_string_to_binary_column(hex_str):
|
|
95
|
+
bits = ''.join(f'{int(c, 16):04b}' for c in hex_str)
|
|
96
|
+
return np.array([int(b) for b in bits], dtype=np.uint8).reshape(-1, 1)
|
|
97
|
+
|
|
98
|
+
bit_columns = [hex_string_to_binary_column(h) for h in hex_list]
|
|
99
|
+
image = np.hstack(bit_columns)
|
|
100
|
+
print(image) # Doit être un tableau de 1 partout
|
|
101
|
+
|
|
102
|
+
height, width = image.shape
|
|
103
|
+
|
|
104
|
+
# Create figure with compact size
|
|
105
|
+
fig, ax = plt.subplots(figsize=(5, 4))
|
|
106
|
+
plt.subplots_adjust(left=0.1, right=0.95, top=0.9, bottom=0.2)
|
|
107
|
+
|
|
108
|
+
# Plot binary pattern
|
|
109
|
+
im = ax.imshow(image, cmap='binary', aspect='auto', interpolation='none', vmin=0, vmax=1)
|
|
110
|
+
|
|
111
|
+
ax.set_title("Scan Configuration", fontsize=12, pad=10, weight='bold')
|
|
112
|
+
ax.set_xlabel("Wave Index", fontsize=10, labelpad=8)
|
|
113
|
+
ax.set_ylabel("Transducer Activation", fontsize=10, labelpad=8)
|
|
114
|
+
yticks_positions = np.arange(0, height) # Positions des ticks (0 à 191)
|
|
115
|
+
yticks_labels = np.arange(1, height + 1) # Labels de 1 à 192
|
|
116
|
+
|
|
117
|
+
ax.set_yticks(yticks_positions)
|
|
118
|
+
ax.set_yticklabels(yticks_labels, fontsize=8)
|
|
119
|
+
# Plot angle markers (bigger and bolder)
|
|
120
|
+
angle_min, angle_max = -20.2, 20.2
|
|
121
|
+
center = height / 2
|
|
122
|
+
scale = height / (angle_max - angle_min)
|
|
123
|
+
for i, angle in enumerate(angle_list):
|
|
124
|
+
y = round(center - angle * scale)
|
|
125
|
+
if 0 <= y < height:
|
|
126
|
+
ax.plot(i, y - 0.5, 'ro', markersize=4, alpha=0.7) # Points rouges plus gros
|
|
127
|
+
|
|
128
|
+
ax.set_ylim(height - 0.5, -0.5)
|
|
129
|
+
|
|
130
|
+
# Twin axis for angles (with larger font)
|
|
131
|
+
ax2 = ax.twinx()
|
|
132
|
+
ax2.set_ylim(ax.get_ylim())
|
|
133
|
+
yticks_angle = np.linspace(20, -20, 5) # 5 ticks pour plus de détails
|
|
134
|
+
yticks_pos = np.interp(yticks_angle, [angle_min, angle_max], [height - 0.5, -0.5])
|
|
135
|
+
ax2.set_yticks(yticks_pos)
|
|
136
|
+
ax2.set_yticklabels([f"{a:.1f}°" for a in yticks_angle], fontsize=9, color='r')
|
|
137
|
+
ax2.set_ylabel("Angle [°]", fontsize=11, color='r', labelpad=10)
|
|
138
|
+
ax2.tick_params(axis='y', colors='r', labelsize=9, width=1.5, length=5)
|
|
139
|
+
|
|
140
|
+
# Make axes thicker
|
|
141
|
+
ax.spines['left'].set_linewidth(1.5)
|
|
142
|
+
ax.spines['bottom'].set_linewidth(1.5)
|
|
143
|
+
ax2.spines['right'].set_linewidth(1.5)
|
|
144
|
+
|
|
145
|
+
# Add grid (thicker lines)
|
|
146
|
+
ax.grid(True, linestyle='--', alpha=0.4, color='gray', linewidth=0.5)
|
|
147
|
+
ax.set_xticks(np.linspace(0, width-1, 6)) # Plus de ticks sur l'axe x
|
|
148
|
+
ax.set_yticks(np.linspace(0, height-1, 6)) # Plus de ticks sur l'axe y
|
|
149
|
+
ax.tick_params(axis='both', which='both', labelsize=8, width=1.5, length=4)
|
|
150
|
+
|
|
151
|
+
plt.tight_layout()
|
|
152
|
+
plt.show()
|
|
153
|
+
|
|
154
|
+
def plot_angle_frequency_distribution(self):
|
|
155
|
+
if self.patterns is None:
|
|
156
|
+
raise ValueError("patterns is not initialized. Please load or generate the active list first.")
|
|
157
|
+
|
|
158
|
+
num_elements = self.params.acoustic['num_elements']
|
|
159
|
+
divs = sorted([d for d in range(2, num_elements + 1) if num_elements % d == 0 and d % 2 == 0])
|
|
160
|
+
if num_elements not in divs:
|
|
161
|
+
divs.append(num_elements)
|
|
162
|
+
divs.sort()
|
|
163
|
+
|
|
164
|
+
angles = []
|
|
165
|
+
freqs = []
|
|
166
|
+
|
|
167
|
+
for p in self.patterns:
|
|
168
|
+
# Extraire la chaîne "hexa_XXX" depuis le dictionnaire
|
|
169
|
+
file_name = p["fileName"]
|
|
170
|
+
hex_part, angle_str = file_name.split('_') # Split sur le dictionnaire corrigé
|
|
171
|
+
|
|
172
|
+
# Récupérer l'angle
|
|
173
|
+
sign = -1 if angle_str[0] == '1' else 1
|
|
174
|
+
angle = sign * int(angle_str[1:])
|
|
175
|
+
angles.append(angle)
|
|
176
|
+
|
|
177
|
+
# Récupérer la fréquence spatiale
|
|
178
|
+
bits = np.array([int(b) for b in bin(int(hex_part, 16))[2:].zfill(num_elements)])
|
|
179
|
+
if np.all(bits == 1): # Cas "tous activés"
|
|
180
|
+
freqs.append(num_elements)
|
|
181
|
+
continue
|
|
182
|
+
|
|
183
|
+
for block_size in divs:
|
|
184
|
+
half_block = block_size // 2
|
|
185
|
+
block = np.array([0] * half_block + [1] * half_block)
|
|
186
|
+
reps = num_elements // block_size
|
|
187
|
+
pattern_check = np.tile(block, reps)
|
|
188
|
+
if any(np.array_equal(np.roll(pattern_check, shift), bits) for shift in range(block_size)):
|
|
189
|
+
freqs.append(block_size)
|
|
190
|
+
break
|
|
191
|
+
else:
|
|
192
|
+
freqs.append(None)
|
|
193
|
+
|
|
194
|
+
freqs = [f for f in freqs if f is not None]
|
|
195
|
+
|
|
196
|
+
# Plot
|
|
197
|
+
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
|
|
198
|
+
|
|
199
|
+
# Histogramme des angles
|
|
200
|
+
axes[0].hist(angles, bins=np.arange(-20.5, 21.5, 1), color='skyblue', edgecolor='black', rwidth=0.8)
|
|
201
|
+
axes[0].set_xlabel("Angle (°)")
|
|
202
|
+
axes[0].set_ylabel("Nombre de patterns")
|
|
203
|
+
axes[0].set_title("Distribution des angles")
|
|
204
|
+
axes[0].set_xticks(np.arange(-20, 21, 2))
|
|
205
|
+
|
|
206
|
+
# Histogramme des fréquences spatiales
|
|
207
|
+
unique_freqs, freq_counts = np.unique(freqs, return_counts=True)
|
|
208
|
+
x_pos = np.arange(len(divs))
|
|
209
|
+
for freq, count in zip(unique_freqs, freq_counts):
|
|
210
|
+
idx = divs.index(freq)
|
|
211
|
+
axes[1].bar(x_pos[idx], count, color='salmon', edgecolor='black', width=0.8)
|
|
212
|
+
|
|
213
|
+
axes[1].set_xticks(x_pos)
|
|
214
|
+
axes[1].set_xticklabels(divs)
|
|
215
|
+
axes[1].set_xlabel("Taille du bloc (fréquence spatiale)")
|
|
216
|
+
axes[1].set_ylabel("Nombre de patterns")
|
|
217
|
+
axes[1].set_title("Distribution des fréquences spatiales")
|
|
218
|
+
|
|
219
|
+
plt.tight_layout()
|
|
220
|
+
plt.show()
|
|
221
|
+
|
|
222
|
+
def loadActiveList(self, fieldParamPath):
|
|
223
|
+
if not os.path.exists(fieldParamPath):
|
|
224
|
+
raise FileNotFoundError(f"Field parameter file {fieldParamPath} not found.")
|
|
225
|
+
patterns = []
|
|
226
|
+
with open(fieldParamPath, 'r') as file:
|
|
227
|
+
lines = file.readlines()
|
|
228
|
+
for line in lines:
|
|
229
|
+
line = line.strip()
|
|
230
|
+
if not line:
|
|
231
|
+
continue
|
|
232
|
+
if "_" in line and all(c in "0123456789abcdefABCDEF" for c in line.split("_")[0]):
|
|
233
|
+
patterns.append({"fileName": line})
|
|
234
|
+
continue
|
|
235
|
+
try:
|
|
236
|
+
parsed = eval(line, {"__builtins__": None})
|
|
237
|
+
if isinstance(parsed, tuple) and len(parsed) == 2:
|
|
238
|
+
coords, angles = parsed
|
|
239
|
+
for angle in angles:
|
|
240
|
+
patterns.append({
|
|
241
|
+
"space_0": coords[0],
|
|
242
|
+
"space_1": coords[1],
|
|
243
|
+
"move_head_0_2tail": coords[2],
|
|
244
|
+
"move_tail_1_2head": coords[3],
|
|
245
|
+
"angle": angle
|
|
246
|
+
})
|
|
247
|
+
else:
|
|
248
|
+
raise ValueError("Ligne inattendue (pas un tuple de deux éléments)")
|
|
249
|
+
except Exception as e:
|
|
250
|
+
print(f"Erreur de parsing sur la ligne : {line}\n{e}")
|
|
251
|
+
self.patterns = patterns
|
|
252
|
+
|
|
253
|
+
def saveActiveList(self, filePath):
|
|
254
|
+
"""
|
|
255
|
+
Sauvegarde la liste des patterns dans un fichier texte.
|
|
256
|
+
Args:
|
|
257
|
+
filePath (str): Chemin du fichier de sortie.
|
|
258
|
+
"""
|
|
259
|
+
with open(filePath, 'w') as file:
|
|
260
|
+
for pattern in self.patterns:
|
|
261
|
+
if "fileName" in pattern:
|
|
262
|
+
# Cas 1 : Pattern simple (format "hexa_XXX")
|
|
263
|
+
file.write(f"{pattern['fileName']}\n")
|
|
264
|
+
else:
|
|
265
|
+
# Cas 2 : Pattern avec paramètres (format tuple)
|
|
266
|
+
coords = (
|
|
267
|
+
pattern["space_0"],
|
|
268
|
+
pattern["space_1"],
|
|
269
|
+
pattern["move_head_0_2tail"],
|
|
270
|
+
pattern["move_tail_1_2head"]
|
|
271
|
+
)
|
|
272
|
+
angles = [pattern["angle"]] # Supposons que chaque pattern a un seul angle
|
|
273
|
+
line = f"({coords}, {angles})\n"
|
|
274
|
+
file.write(line)
|
|
275
|
+
|
|
276
|
+
def generateActiveList(self, N):
|
|
277
|
+
"""
|
|
278
|
+
Génère une liste de patterns d'activation équilibrés et réguliers.
|
|
279
|
+
Args:
|
|
280
|
+
N (int): Nombre de patterns à générer.
|
|
281
|
+
Returns:
|
|
282
|
+
list: Liste de strings au format "hex_angle".
|
|
283
|
+
"""
|
|
284
|
+
if N < 1:
|
|
285
|
+
raise ValueError("N must be a positive integer.")
|
|
286
|
+
self.patterns = self._generate_patterns(N)
|
|
287
|
+
if not self._check_patterns(self.patterns):
|
|
288
|
+
raise ValueError("Generated patterns failed validation.")
|
|
289
|
+
|
|
290
|
+
def _generate_patterns(self, N):
|
|
291
|
+
def format_angle(a):
|
|
292
|
+
return f"{'1' if a < 0 else '0'}{abs(a):02d}"
|
|
293
|
+
|
|
294
|
+
def bits_to_hex(bits):
|
|
295
|
+
bit_string = ''.join(str(b) for b in bits)
|
|
296
|
+
bit_string = bit_string.zfill(len(bits))
|
|
297
|
+
hex_string = ''.join([f"{int(bit_string[i:i+4], 2):x}" for i in range(0, len(bit_string), 4)])
|
|
298
|
+
return hex_string
|
|
299
|
+
|
|
300
|
+
num_elements = self.params.acoustic['num_elements']
|
|
301
|
+
angle_choices = list(range(-20, 21))
|
|
302
|
+
|
|
303
|
+
# 1. Trouver TOUS les diviseurs PAIRS de num_elements (y compris num_elements)
|
|
304
|
+
divs = [d for d in range(2, num_elements + 1) if num_elements % d == 0 and d % 2 == 0]
|
|
305
|
+
if not divs:
|
|
306
|
+
print(f"Aucun diviseur pair trouvé pour num_elements = {num_elements}")
|
|
307
|
+
return []
|
|
308
|
+
|
|
309
|
+
# 2. Utiliser un ensemble pour suivre les patterns uniques
|
|
310
|
+
unique_patterns = set()
|
|
311
|
+
|
|
312
|
+
# 3. Générer jusqu'à N patterns uniques
|
|
313
|
+
while len(unique_patterns) < N:
|
|
314
|
+
# Tirer un diviseur aléatoire (y compris num_elements)
|
|
315
|
+
block_size = np.random.choice(divs)
|
|
316
|
+
|
|
317
|
+
if block_size == num_elements:
|
|
318
|
+
# Cas spécial : pattern "tous activés"
|
|
319
|
+
pattern_bits = np.ones(num_elements, dtype=int)
|
|
320
|
+
else:
|
|
321
|
+
# Cas général : pattern équilibré
|
|
322
|
+
half_block = block_size // 2
|
|
323
|
+
block = np.array([0] * half_block + [1] * half_block)
|
|
324
|
+
reps = num_elements // block_size
|
|
325
|
+
base_pattern = np.tile(block, reps)
|
|
326
|
+
# Tirer un décalage aléatoire
|
|
327
|
+
shift = np.random.randint(0, block_size)
|
|
328
|
+
pattern_bits = np.roll(base_pattern, shift)
|
|
329
|
+
|
|
330
|
+
# Convertir en hex et choisir un angle aléatoire
|
|
331
|
+
hex_pattern = bits_to_hex(pattern_bits)
|
|
332
|
+
angle = np.random.choice(angle_choices)
|
|
333
|
+
pair = f"{hex_pattern}_{format_angle(angle)}"
|
|
334
|
+
|
|
335
|
+
# Ajouter à l'ensemble (les doublons sont automatiquement ignorés)
|
|
336
|
+
unique_patterns.add(pair)
|
|
337
|
+
|
|
338
|
+
# 4. Convertir en liste de dictionnaires avec la clé "fileName"
|
|
339
|
+
patterns = [{"fileName": pair} for pair in unique_patterns]
|
|
340
|
+
|
|
341
|
+
# 5. Retourner exactement N patterns (on a déjà vérifié la taille avec while)
|
|
342
|
+
return patterns[:N] # Par sécurité, même si len(unique_patterns) == N
|
|
343
|
+
|
|
344
|
+
def _check_patterns(self, patterns):
|
|
345
|
+
# 1. Vérifier les doublons (basé sur "fileName")
|
|
346
|
+
file_names = [p["fileName"] for p in patterns]
|
|
347
|
+
if len(file_names) != len(set(file_names)):
|
|
348
|
+
# Trouver les doublons
|
|
349
|
+
from collections import Counter
|
|
350
|
+
file_counts = Counter(file_names)
|
|
351
|
+
duplicates = [fn for fn, count in file_counts.items() if count > 1]
|
|
352
|
+
for dup in duplicates:
|
|
353
|
+
print(f"Erreur : Doublon détecté pour {dup}")
|
|
354
|
+
return False
|
|
355
|
+
|
|
356
|
+
# 2. Vérifier chaque pattern individuellement
|
|
357
|
+
num_elements = self.params.acoustic['num_elements']
|
|
358
|
+
for pattern in patterns:
|
|
359
|
+
hex_part, angle_str = pattern["fileName"].split('_')
|
|
360
|
+
bits = np.array([int(b) for b in bin(int(hex_part, 16))[2:].zfill(num_elements)])
|
|
361
|
+
|
|
362
|
+
# Vérifier la longueur
|
|
363
|
+
if len(bits) != num_elements:
|
|
364
|
+
print(f"Erreur longueur: {pattern['fileName']}")
|
|
365
|
+
return False
|
|
366
|
+
|
|
367
|
+
# Cas spécial : pattern "tous activés"
|
|
368
|
+
if np.all(bits == 1):
|
|
369
|
+
continue
|
|
370
|
+
|
|
371
|
+
# Vérifier l'équilibre 0/1
|
|
372
|
+
if np.sum(bits) != num_elements // 2:
|
|
373
|
+
print(f"Erreur équilibre 0/1: {pattern['fileName']}")
|
|
374
|
+
return False
|
|
375
|
+
|
|
376
|
+
# Vérifier la régularité
|
|
377
|
+
valid = False
|
|
378
|
+
divs = [d for d in range(2, num_elements + 1) if num_elements % d == 0 and d % 2 == 0]
|
|
379
|
+
for block_size in divs:
|
|
380
|
+
half_block = block_size // 2
|
|
381
|
+
block = np.array([0] * half_block + [1] * half_block)
|
|
382
|
+
reps = num_elements // block_size
|
|
383
|
+
expected_pattern = np.tile(block, reps)
|
|
384
|
+
if any(np.array_equal(np.roll(expected_pattern, shift), bits) for shift in range(block_size)):
|
|
385
|
+
valid = True
|
|
386
|
+
break
|
|
387
|
+
if not valid:
|
|
388
|
+
print(f"Erreur régularité: {pattern['fileName']}")
|
|
389
|
+
return False
|
|
390
|
+
|
|
391
|
+
return True
|
|
392
|
+
|
|
393
|
+
# PRIVATE METHODS
|
|
394
|
+
def _generateAcousticFields_STRUCT_CPU(self, fieldDataPath=None, show_log=False, nameBlock=None):
|
|
395
|
+
if self.patterns is None:
|
|
396
|
+
raise ValueError("patterns is not initialized. Please load or generate the active list first.")
|
|
397
|
+
listAcousticFields = []
|
|
398
|
+
progress_bar = trange(0, len(self.patterns), desc="Generating acoustic fields")
|
|
399
|
+
for i in progress_bar:
|
|
400
|
+
memory = psutil.virtual_memory()
|
|
401
|
+
pattern = self.patterns[i]
|
|
402
|
+
if "fileName" in pattern:
|
|
403
|
+
AcousticField = StructuredWave(fileName=pattern["fileName"], params=self.params)
|
|
404
|
+
else:
|
|
405
|
+
AcousticField = StructuredWave(
|
|
406
|
+
angle_deg=pattern["angle"],
|
|
407
|
+
space_0=pattern["space_0"],
|
|
408
|
+
space_1=pattern["space_1"],
|
|
409
|
+
move_head_0_2tail=pattern["move_head_0_2tail"],
|
|
410
|
+
move_tail_1_2head=pattern["move_tail_1_2head"],
|
|
411
|
+
params=self.params
|
|
412
|
+
)
|
|
413
|
+
if fieldDataPath is None:
|
|
414
|
+
pathField = None
|
|
415
|
+
else:
|
|
416
|
+
pathField = os.path.join(fieldDataPath, AcousticField.getName_field() + self.FormatSave.value)
|
|
417
|
+
if pathField is not None and os.path.exists(pathField):
|
|
418
|
+
progress_bar.set_postfix_str(f"Loading field - {AcousticField.getName_field()} -- Memory used: {memory.percent}%")
|
|
419
|
+
try:
|
|
420
|
+
AcousticField.load_field(fieldDataPath, self.FormatSave,nameBlock)
|
|
421
|
+
except:
|
|
422
|
+
progress_bar.set_postfix_str(f"Error loading field -> Generating field - {AcousticField.getName_field()} -- Memory used: {memory.percent}% ---- processing on {config.get_process().upper()} ----")
|
|
423
|
+
AcousticField.generate_field(show_log=show_log)
|
|
424
|
+
if not os.path.exists(pathField):
|
|
425
|
+
progress_bar.set_postfix_str(f"Saving field - {AcousticField.getName_field()} -- Memory used: {memory.percent}%")
|
|
426
|
+
os.makedirs(os.path.dirname(pathField), exist_ok=True)
|
|
427
|
+
AcousticField.save_field(fieldDataPath)
|
|
428
|
+
elif pathField is None or not os.path.exists(pathField):
|
|
429
|
+
progress_bar.set_postfix_str(f"Generating field - {AcousticField.getName_field()} -- Memory used: {memory.percent}% ---- processing on {config.get_process().upper()} ----")
|
|
430
|
+
AcousticField.generate_field(show_log=show_log)
|
|
431
|
+
if pathField is not None and not os.path.exists(pathField):
|
|
432
|
+
progress_bar.set_postfix_str(f"Saving field - {AcousticField.getName_field()} -- Memory used: {memory.percent}%")
|
|
433
|
+
os.makedirs(os.path.dirname(pathField), exist_ok=True)
|
|
434
|
+
AcousticField.save_field(fieldDataPath)
|
|
435
|
+
listAcousticFields.append(AcousticField)
|
|
436
|
+
progress_bar.set_postfix_str("")
|
|
437
|
+
return listAcousticFields
|
|
438
|
+
|
|
439
|
+
def load_experimentalAO(self, pathAO, withTumor = True, h5name='AOsignal'):
|
|
440
|
+
"""
|
|
441
|
+
Load experimental AO signals from specified file paths.
|
|
442
|
+
Args:
|
|
443
|
+
path_withTumor: Path to the AO signal with tumor.
|
|
444
|
+
path_withoutTumor: Path to the AO signal without tumor.
|
|
445
|
+
"""
|
|
446
|
+
if not os.path.exists(pathAO):
|
|
447
|
+
raise FileNotFoundError(f"File {pathAO} not found.")
|
|
448
|
+
|
|
449
|
+
if pathAO.endswith('.npy'):
|
|
450
|
+
ao_signal = np.load(pathAO)
|
|
451
|
+
elif pathAO.endswith('.h5'):
|
|
452
|
+
with h5py.File(pathAO, 'r') as f:
|
|
453
|
+
if h5name not in f:
|
|
454
|
+
raise KeyError(f"Dataset '{h5name}' not found in the HDF5 file.")
|
|
455
|
+
ao_signal = f[h5name][:]
|
|
456
|
+
elif pathAO.endswith('.mat'):
|
|
457
|
+
mat_data = loadmat(pathAO)
|
|
458
|
+
if h5name not in mat_data:
|
|
459
|
+
raise KeyError(f"Dataset '{h5name}' not found in the .mat file.")
|
|
460
|
+
ao_signal = mat_data[h5name]
|
|
461
|
+
elif pathAO.endswith('.hdr'):
|
|
462
|
+
ao_signal = self._loadAOSignal(pathAO)
|
|
463
|
+
else:
|
|
464
|
+
raise ValueError("Unsupported file format. Supported formats are: .npy, .h5, .mat, .hdr")
|
|
465
|
+
|
|
466
|
+
if withTumor:
|
|
467
|
+
self.AOsignal_withTumor = ao_signal
|
|
468
|
+
else:
|
|
469
|
+
self.AOsignal_withoutTumor = ao_signal
|
|
470
|
+
|
|
471
|
+
def check_experimentalAO(self, activeListPath, withTumor=True):
|
|
472
|
+
"""
|
|
473
|
+
Check if the experimental AO signals are correctly initialized.
|
|
474
|
+
"""
|
|
475
|
+
if withTumor:
|
|
476
|
+
if self.AOsignal_withTumor is None:
|
|
477
|
+
raise ValueError("Experimental AOsignal with tumor is not initialized. Please load the experimental AO signal with tumor first.")
|
|
478
|
+
else:
|
|
479
|
+
if self.AOsignal_withoutTumor is None:
|
|
480
|
+
raise ValueError("Experimental AOsignal without tumor is not initialized. Please load the experimental AO signal without tumor first.")
|
|
481
|
+
if self.AcousticFields is not None:
|
|
482
|
+
# get min time shape between all AO signals
|
|
483
|
+
print()
|
|
484
|
+
|
|
485
|
+
if self.AcousticFields[0].field.shape[0] > self.AOsignal_withTumor.shape[0]:
|
|
486
|
+
self.cutAcousticFields(max_t=self.AOsignal_withTumor.shape[0]/float(self.params.acoustic['f_saving']))
|
|
487
|
+
else:
|
|
488
|
+
for i in range(len(self.AcousticFields)):
|
|
489
|
+
min_time_shape = min(self.AcousticFields[i].field.shape[0])
|
|
490
|
+
if withTumor:
|
|
491
|
+
self.AOsignal_withTumor = self.AOsignal_withTumor[:min_time_shape, :]
|
|
492
|
+
else:
|
|
493
|
+
self.AOsignal_withoutTumor = self.AOsignal_withoutTumor[:min_time_shape, :]
|
|
494
|
+
|
|
495
|
+
for field in self.AcousticFields:
|
|
496
|
+
if activeListPath is not None:
|
|
497
|
+
with open(activeListPath, 'r') as file:
|
|
498
|
+
lines = file.readlines()
|
|
499
|
+
expected_name = lines[self.AcousticFields.index(field)].strip()
|
|
500
|
+
nameField = field.getName_field()
|
|
501
|
+
if nameField.startswith("field_"):
|
|
502
|
+
nameField = nameField[len("field_"):]
|
|
503
|
+
if nameField != expected_name:
|
|
504
|
+
raise ValueError(f"Field name {nameField} does not match the expected name {expected_name} from the active list.")
|
|
505
|
+
print("Experimental AO signals are correctly initialized.")
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from ._mainExperiment import *
|
|
2
|
+
from .Focus import *
|
|
3
|
+
from .Tomography import *
|
|
4
|
+
|
|
5
|
+
# Docstring for the AOT_Experiment package
|
|
6
|
+
"""
|
|
7
|
+
AOT_Experiment is a package for experimental setups in Acousto-Optic Tomography.
|
|
8
|
+
It provides tools and classes for focusing and tomography in acousto-optic experiments.
|
|
9
|
+
"""
|