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.
Files changed (50) hide show
  1. AOT_biomaps/AOT_Acoustic/AcousticEnums.py +64 -0
  2. AOT_biomaps/AOT_Acoustic/AcousticTools.py +221 -0
  3. AOT_biomaps/AOT_Acoustic/FocusedWave.py +244 -0
  4. AOT_biomaps/AOT_Acoustic/IrregularWave.py +66 -0
  5. AOT_biomaps/AOT_Acoustic/PlaneWave.py +43 -0
  6. AOT_biomaps/AOT_Acoustic/StructuredWave.py +392 -0
  7. AOT_biomaps/AOT_Acoustic/__init__.py +15 -0
  8. AOT_biomaps/AOT_Acoustic/_mainAcoustic.py +978 -0
  9. AOT_biomaps/AOT_Experiment/Focus.py +55 -0
  10. AOT_biomaps/AOT_Experiment/Tomography.py +505 -0
  11. AOT_biomaps/AOT_Experiment/__init__.py +9 -0
  12. AOT_biomaps/AOT_Experiment/_mainExperiment.py +532 -0
  13. AOT_biomaps/AOT_Optic/Absorber.py +24 -0
  14. AOT_biomaps/AOT_Optic/Laser.py +70 -0
  15. AOT_biomaps/AOT_Optic/OpticEnums.py +17 -0
  16. AOT_biomaps/AOT_Optic/__init__.py +10 -0
  17. AOT_biomaps/AOT_Optic/_mainOptic.py +204 -0
  18. AOT_biomaps/AOT_Recon/AOT_Optimizers/DEPIERRO.py +191 -0
  19. AOT_biomaps/AOT_Recon/AOT_Optimizers/LS.py +106 -0
  20. AOT_biomaps/AOT_Recon/AOT_Optimizers/MAPEM.py +456 -0
  21. AOT_biomaps/AOT_Recon/AOT_Optimizers/MLEM.py +333 -0
  22. AOT_biomaps/AOT_Recon/AOT_Optimizers/PDHG.py +221 -0
  23. AOT_biomaps/AOT_Recon/AOT_Optimizers/__init__.py +5 -0
  24. AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Huber.py +90 -0
  25. AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Quadratic.py +86 -0
  26. AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/RelativeDifferences.py +59 -0
  27. AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/__init__.py +3 -0
  28. AOT_biomaps/AOT_Recon/AlgebraicRecon.py +1023 -0
  29. AOT_biomaps/AOT_Recon/AnalyticRecon.py +154 -0
  30. AOT_biomaps/AOT_Recon/BayesianRecon.py +230 -0
  31. AOT_biomaps/AOT_Recon/DeepLearningRecon.py +35 -0
  32. AOT_biomaps/AOT_Recon/PrimalDualRecon.py +210 -0
  33. AOT_biomaps/AOT_Recon/ReconEnums.py +375 -0
  34. AOT_biomaps/AOT_Recon/ReconTools.py +273 -0
  35. AOT_biomaps/AOT_Recon/__init__.py +11 -0
  36. AOT_biomaps/AOT_Recon/_mainRecon.py +288 -0
  37. AOT_biomaps/Config.py +95 -0
  38. AOT_biomaps/Settings.py +45 -13
  39. AOT_biomaps/__init__.py +271 -18
  40. aot_biomaps-2.9.233.dist-info/METADATA +22 -0
  41. aot_biomaps-2.9.233.dist-info/RECORD +43 -0
  42. {AOT_biomaps-2.1.3.dist-info → aot_biomaps-2.9.233.dist-info}/WHEEL +1 -1
  43. AOT_biomaps/AOT_Acoustic.py +0 -1881
  44. AOT_biomaps/AOT_Experiment.py +0 -541
  45. AOT_biomaps/AOT_Optic.py +0 -219
  46. AOT_biomaps/AOT_Reconstruction.py +0 -1416
  47. AOT_biomaps/config.py +0 -54
  48. AOT_biomaps-2.1.3.dist-info/METADATA +0 -20
  49. AOT_biomaps-2.1.3.dist-info/RECORD +0 -11
  50. {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
+ """