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.

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,532 @@
1
+ from AOT_biomaps.Settings import Params
2
+ from AOT_biomaps.AOT_Optic._mainOptic import Phantom
3
+ from AOT_biomaps.AOT_Acoustic.AcousticEnums import WaveType, FormatSave
4
+ from AOT_biomaps.AOT_Acoustic.StructuredWave import StructuredWave
5
+
6
+ from abc import ABC, abstractmethod
7
+ import os
8
+ import numpy as np
9
+ import torch
10
+ import torch.nn.functional as F
11
+ from tqdm import trange
12
+ from datetime import datetime
13
+ import matplotlib.pyplot as plt
14
+ import matplotlib.animation as animation
15
+ import matplotlib as mpl
16
+ import copy
17
+
18
+ class Experiment(ABC):
19
+ def __init__(self, params, acousticType=WaveType.StructuredWave, formatSave=FormatSave.HDR_IMG):
20
+ self.params = params
21
+ self.OpticImage = None
22
+ self.AcousticFields = None
23
+ self.AOsignal_withTumor = None
24
+ self.AOsignal_withoutTumor = None
25
+
26
+ if type(acousticType).__name__ != "WaveType":
27
+ raise TypeError("acousticType must be an instance of the WaveType class")
28
+
29
+ self.FormatSave = formatSave
30
+ self.TypeAcoustic = acousticType
31
+
32
+ if type(self.params) != Params:
33
+ raise TypeError("params must be an instance of the Params class")
34
+
35
+ def copy(self):
36
+ """Retourne une copie profonde de l'objet."""
37
+ return copy.deepcopy(self)
38
+
39
+ def generatePhantom(self):
40
+ """
41
+ Generate the phantom for the experiment.
42
+ This method initializes the OpticImage attribute with a Phantom instance.
43
+ """
44
+ self.OpticImage = Phantom(params=self.params)
45
+
46
+ @abstractmethod
47
+ def generateAcousticFields(self, fieldDataPath, fieldParamPath, show_log=True):
48
+ """
49
+ Generate the acoustic fields for simulation.
50
+ Args:
51
+ fieldDataPath: Path to save the generated fields.
52
+ fieldParamPath: Path to the field parameters file.
53
+ Returns:
54
+ systemMatrix: A numpy array of the generated fields.
55
+ """
56
+ pass
57
+
58
+ def cutAcousticFields(self, max_t, min_t=0):
59
+
60
+ max_t = float(max_t)
61
+ min_t = float(min_t)
62
+
63
+ min_sample = int(np.floor(min_t * float(self.params.acoustic['f_saving'])))
64
+ max_sample = int(np.floor(max_t * float(self.params.acoustic['f_saving'])))
65
+
66
+ if min_sample < 0 or max_sample < 0:
67
+ raise ValueError("min_sample and max_sample must be non-negative integers.")
68
+ if min_sample >= max_sample:
69
+ raise ValueError("min_sample must be less than max_sample.")
70
+
71
+ if not self.AcousticFields:
72
+ raise ValueError("AcousticFields is empty. Cannot cut fields.")
73
+
74
+ for i in trange(len(self.AcousticFields), desc=f"Cutting Acoustic Fields ({min_sample} to {max_sample} samples)"):
75
+ field = self.AcousticFields[i]
76
+ if field.field.shape[0] < max_sample:
77
+ raise ValueError(f"Field {field.getName_field()} has an invalid shape: {field.field.shape}. Expected shape to be at least ({max_sample},).")
78
+ self.AcousticFields[i].field = field.field[min_sample:max_sample, :, :]
79
+
80
+ def addNoise(self, noiseType='gaussian', noiseLvl=0.1, withTumor=True):
81
+ """
82
+ Ajoute du bruit (gaussien ou poisson) au signal AO sélectionné.
83
+
84
+ Args:
85
+ noiseType (str): Type de bruit à ajouter ('gaussian' ou 'poisson').
86
+ noiseLvl (float): Niveau de bruit (écart-type pour le bruit gaussien, facteur multiplicatif pour le bruit de Poisson).
87
+ withTumor (bool): Si True, ajoute le bruit au signal avec tumeur, sinon au signal sans tumeur.
88
+ """
89
+ if withTumor and self.AOsignal_withTumor is None:
90
+ raise ValueError("AO signal with tumor is not generated. Please generate it first.")
91
+ if not withTumor and self.AOsignal_withoutTumor is None:
92
+ raise ValueError("AO signal without tumor is not generated. Please generate it first.")
93
+
94
+ if withTumor:
95
+ AOsignals = self.AOsignal_withTumor
96
+ else:
97
+ AOsignals = self.AOsignal_withoutTumor
98
+
99
+ noiseSignals = np.zeros_like(AOsignals)
100
+ for i in trange(AOsignals.shape[1], desc=f"Adding {noiseType} noise to AO signal {'with' if withTumor else 'without'} tumor"):
101
+ AOsignal = AOsignals[:, i]
102
+ if noiseType.lower() == 'gaussian':
103
+ noise = np.random.normal(0, noiseLvl*np.max(AOsignal), AOsignal.shape)
104
+ noisy_signal = AOsignal + noise
105
+ elif noiseType.lower() == 'poisson':
106
+ # Pour le bruit de Poisson, on utilise souvent un facteur multiplicatif
107
+ # car le bruit de Poisson est proportionnel à la racine carrée du signal.
108
+ # Ici, on multiplie le signal par un facteur aléatoire centré autour de 1.
109
+ noise = np.random.poisson(noiseLvl * np.abs(AOsignal)) / (noiseLvl * np.abs(AOsignal).max())
110
+ noisy_signal = AOsignal * noise
111
+ else:
112
+ raise ValueError("noiseType must be either 'gaussian' or 'poisson'.")
113
+ noisy_signal = np.clip(noisy_signal, a_min=0, a_max=None) # Assurer que le signal reste non négatif
114
+ noiseSignals[:, i] = noisy_signal
115
+ return noiseSignals
116
+
117
+ def reduceDims(self, mode='avg'):
118
+ """
119
+ Réduit les dimensions T, X, Z d'un numpy array (T, X, Z) par 2 en utilisant une convolution.
120
+ Retourne un numpy array et met à jour les paramètres numériques.
121
+ """
122
+ for i in trange(len(self.AcousticFields),
123
+ desc="Downsampling Acoustic Fields (T, X, Z → T//2, X//2, Z//2)"):
124
+ # Conversion en tenseur PyTorch
125
+ field = self.AcousticFields[i].field
126
+ if not isinstance(field, torch.Tensor):
127
+ field = torch.from_numpy(field)
128
+
129
+ # Vérification de la forme (doit être 3D : T, X, Z)
130
+ if field.dim() != 3:
131
+ raise ValueError(f"Forme non supportée : {field.shape}. Attendu (T, X, Z).")
132
+
133
+ # Ajout des dimensions pour conv3d : (1, 1, T, X, Z)
134
+ x = field.unsqueeze(0).unsqueeze(0)
135
+
136
+ # Réduction par convolution 3D
137
+ if mode == 'avg':
138
+ x_down = F.avg_pool3d(x, kernel_size=(2, 2, 2), stride=(2, 2, 2))
139
+ else: # mode == 'max'
140
+ x_down = F.max_pool3d(x, kernel_size=(2, 2, 2), stride=(2, 2, 2))
141
+
142
+ # Conversion en numpy array et suppression des dimensions ajoutées
143
+ self.AcousticFields[i].field = x_down.squeeze(0).squeeze(0).cpu().numpy()
144
+
145
+ # Fonction utilitaire pour convertir et mettre à jour un paramètre
146
+ def convert_and_update(param_dict, key, operation):
147
+ if key in param_dict:
148
+ if isinstance(param_dict[key], str):
149
+ param_dict[key] = float(param_dict[key])
150
+ param_dict[key] = operation(param_dict[key])
151
+
152
+ # Mise à jour des paramètres
153
+ convert_and_update(self.params.acoustic, 'f_saving', lambda x: x / 2)
154
+ for param in ['dx', 'dy', 'dz']:
155
+ convert_and_update(self.params.general, param, lambda x: x * 2)
156
+
157
+ def normalizeAOsignals(self, withTumor=True):
158
+ if withTumor and self.AOsignal_withTumor is None:
159
+ raise ValueError("AO signal with tumor is not generated. Please generate it first.")
160
+ if not withTumor and self.AOsignal_withoutTumor is None:
161
+ raise ValueError("AO signal without tumor is not generated. Please generate it first.")
162
+ if withTumor:
163
+ self.AOsignal_withTumor = self.AOsignal_withTumor - np.min(self.AOsignal_withTumor)/(np.max(self.AOsignal_withTumor)-np.min(self.AOsignal_withTumor))
164
+ else:
165
+ self.AOsignal_withoutTumor = self.AOsignal_withoutTumor - np.min(self.AOsignal_withoutTumor)/(np.max(self.AOsignal_withoutTumor)-np.min(self.AOsignal_withoutTumor))
166
+
167
+ def saveAcousticFields(self, save_directory):
168
+ progress_bar = trange(len(self.AcousticFields), desc="Saving Acoustic Fields")
169
+ for i in progress_bar:
170
+ progress_bar.set_postfix_str(f"-- {self.AcousticFields[i].getName_field()}")
171
+ self.AcousticFields[i].save_field(save_directory, formatSave=self.FormatSave)
172
+
173
+ def show_animated_Acoustic(self, wave_name=None, desired_duration_ms=5000, save_dir=None):
174
+ """
175
+ Plot synchronized animations of A_matrix slices for selected angles.
176
+ Args:
177
+ wave_name: optional name for labeling the subplots (e.g., "wave1")
178
+ desired_duration_ms: Total duration of the animation in milliseconds.
179
+ save_dir: directory to save the animation gif; if None, animation will not be saved
180
+ Returns:
181
+ ani: Matplotlib FuncAnimation object
182
+ """
183
+ mpl.rcParams['animation.embed_limit'] = 100
184
+ if save_dir is not None:
185
+ os.makedirs(save_dir, exist_ok=True)
186
+
187
+ num_plots = len(self.AcousticFields)
188
+ if num_plots <= 5:
189
+ nrows, ncols = 1, num_plots
190
+ else:
191
+ ncols = 5
192
+ nrows = (num_plots + ncols - 1) // ncols
193
+
194
+ fig, axes = plt.subplots(nrows, ncols, figsize=(5 * ncols, 5.3 * nrows))
195
+ if isinstance(axes, plt.Axes):
196
+ axes = np.array([axes])
197
+ axes = axes.flatten()
198
+ ims = []
199
+
200
+ fig.suptitle(f"System Matrix Animation {wave_name}", fontsize=12, y=0.98)
201
+
202
+ for idx in range(num_plots):
203
+ ax = axes[idx]
204
+ im = ax.imshow(self.AcousticFields[0, :, :, idx],
205
+ extent=(self.params['Xrange'][0], self.params['Xrange'][1], self.params['Zrange'][1], self.params['Zrange'][0]),
206
+ vmax=1, aspect='equal', cmap='jet', animated=True)
207
+ ax.set_xlabel("x (mm)", fontsize=8)
208
+ ax.set_ylabel("z (mm)", fontsize=8)
209
+ ims.append((im, ax, idx))
210
+
211
+ for j in range(num_plots, len(axes)):
212
+ fig.delaxes(axes[j])
213
+
214
+ plt.tight_layout(rect=[0, 0, 1, 0.95])
215
+
216
+ def update(frame):
217
+ artists = []
218
+ for im, ax, idx in ims:
219
+ im.set_array(self.AcousticFields[frame, :, :, idx])
220
+ fig.suptitle(f"System Matrix Animation {wave_name} t = {frame * 25e-6 * 1000:.2f} ms", fontsize=10)
221
+ artists.append(im)
222
+ return artists
223
+
224
+ interval = desired_duration_ms / self.AcousticFields.shape[0]
225
+ ani = animation.FuncAnimation(
226
+ fig, update,
227
+ frames=range(0, self.AcousticFields.shape[0]),
228
+ interval=interval, blit=True
229
+ )
230
+
231
+ if save_dir is not None:
232
+ now = datetime.now()
233
+ date_str = now.strftime("%Y_%d_%m_%y")
234
+ save_filename = f"AcousticField_{wave_name}_{date_str}.gif"
235
+ save_path = os.path.join(save_dir, save_filename)
236
+ ani.save(save_path, writer='pillow', fps=20)
237
+ print(f"Saved: {save_path}")
238
+
239
+ plt.close(fig)
240
+ return ani
241
+
242
+ def generateAOsignal(self, withTumor=True, AOsignalDataPath=None):
243
+
244
+ if AOsignalDataPath is not None:
245
+ if not os.path.exists(AOsignalDataPath):
246
+ raise FileNotFoundError(f"AO file {AOsignalDataPath} not found.")
247
+ if withTumor:
248
+ self.AOsignal_withTumor = self._loadAOSignal(AOsignalDataPath)
249
+ if self.AOsignal_withTumor.shape[0] != self.AcousticFields[0].field.shape[0]:
250
+ print(f"AO signal shape {self.AOsignal_withTumor.shape} does not match the expected shape {self.AcousticFields[0].field.shape}. Resizing Acoustic fields...")
251
+ self.cutAcousticFields(max_t=self.AOsignal_withTumor.shape[0] / float(self.params.acoustic['f_saving']), min_t=0)
252
+ else:
253
+ self.AOsignal_withoutTumor = self._loadAOSignal(AOsignalDataPath)
254
+ if self.AOsignal_withoutTumor.shape[0] != self.AcousticFields[0].field.shape[0]:
255
+ print(f"AO signal shape {self.AOsignal_withoutTumor.shape} does not match the expected shape {self.AcousticFields[0].field.shape}. Resizing Acoustic fields...")
256
+ self.cutAcousticFields(max_t=self.AOsignal_withoutTumor.shape[0] / float(self.params.acoustic['f_saving']), min_t=0)
257
+ else:
258
+ if self.AcousticFields is None:
259
+ raise ValueError("AcousticFields is not initialized. Please generate the system matrix first.")
260
+
261
+ if self.OpticImage is None:
262
+ raise ValueError("OpticImage is not initialized. Please generate the phantom first.")
263
+
264
+ if not all(field.field.shape == self.AcousticFields[0].field.shape for field in self.AcousticFields):
265
+ minShape = min([field.field.shape[0] for field in self.AcousticFields])
266
+ self.cutAcousticFields(max_t=minShape * self.params['fs_aq'])
267
+ else:
268
+ shape_field = self.AcousticFields[0].field.shape
269
+
270
+ AOsignal = np.zeros((shape_field[0], len(self.AcousticFields)), dtype=np.float32)
271
+
272
+ if withTumor:
273
+ description = "Generating AO Signal with Tumor"
274
+ else:
275
+ description = "Generating AO Signal without Tumor"
276
+
277
+ for i in trange(len(self.AcousticFields), desc=description):
278
+ for t in range(self.AcousticFields[i].field.shape[0]):
279
+ if withTumor:
280
+ interaction = self.OpticImage.phantom * self.AcousticFields[i].field[t, :, :]
281
+ else:
282
+ interaction = self.OpticImage.laser.intensity * self.AcousticFields[i].field[t, :, :]
283
+ AOsignal[t, i] = np.sum(interaction)
284
+
285
+ if withTumor:
286
+ self.AOsignal_withTumor = AOsignal
287
+ else:
288
+ self.AOsignal_withoutTumor = AOsignal
289
+
290
+ @staticmethod
291
+ def _loadAOSignal(AOsignalPath):
292
+ if AOsignalPath.endswith(".cdh"):
293
+ with open(AOsignalPath, "r") as file:
294
+ cdh_content = file.readlines()
295
+
296
+ cdf_path = AOsignalPath.replace(".cdh", ".cdf")
297
+
298
+ # Extraire les paramètres depuis le fichier .cdh
299
+ n_scans = int([line.split(":")[1].strip() for line in cdh_content if "Number of events" in line][0])
300
+ n_acquisitions_per_event = int([line.split(":")[1].strip() for line in cdh_content if "Number of acquisitions per event" in line][0])
301
+ num_elements = int([line.split(":")[1].strip() for line in cdh_content if "Number of US transducers" in line][0])
302
+
303
+ # Initialisation des structures
304
+ AO_signal = np.zeros((n_acquisitions_per_event, n_scans), dtype=np.float32)
305
+ active_lists = []
306
+ angles = []
307
+
308
+ # Lecture du fichier binaire
309
+ with open(cdf_path, "rb") as file:
310
+ for j in trange(n_scans, desc="Lecture des événements"):
311
+ # Lire l'activeList : 48 caractères hex = 24 bytes
312
+ active_list_bytes = file.read(24)
313
+ active_list_hex = active_list_bytes.hex()
314
+ active_lists.append(active_list_hex)
315
+
316
+ # Lire l'angle (1 byte signé)
317
+ angle_byte = file.read(1)
318
+ angle = np.frombuffer(angle_byte, dtype=np.int8)[0]
319
+ angles.append(angle)
320
+
321
+ # Lire le signal AO (float32)
322
+ data = np.frombuffer(file.read(n_acquisitions_per_event * 4), dtype=np.float32)
323
+ if len(data) != n_acquisitions_per_event:
324
+ raise ValueError(f"Erreur à l'événement {j} : attendu {n_acquisitions_per_event}, obtenu {len(data)}")
325
+ AO_signal[:, j] = data
326
+
327
+ return AO_signal
328
+
329
+
330
+ elif AOsignalPath.endswith(".npy"):
331
+ return np.load(AOsignalPath) # Supposé déjà au bon format
332
+ else:
333
+ raise ValueError("Format de fichier non supporté. Utilisez .cdh/.cdf ou .npy.")
334
+
335
+ def saveAOsignals_Castor(self, save_directory, withTumor=True):
336
+ if withTumor:
337
+ AO_signal = self.AOsignal_withTumor
338
+ cdf_location = os.path.join(save_directory, "AOSignals_withTumor.cdf")
339
+ cdh_location = os.path.join(save_directory, "AOSignals_withTumor.cdh")
340
+ else:
341
+ AO_signal = self.AOsignal_withoutTumor
342
+ cdf_location = os.path.join(save_directory, "AOSignals_withoutTumor.cdf")
343
+ cdh_location = os.path.join(save_directory, "AOSignals_withoutTumor.cdh")
344
+
345
+ info_location = os.path.join(save_directory, "info.txt")
346
+ nScan = AO_signal.shape[1]
347
+
348
+ with open(cdf_location, "wb") as fileID:
349
+ for j in range(AO_signal.shape[1]):
350
+ active_list_hex = self.AcousticFields[j].pattern.activeList
351
+ for i in range(0, len(active_list_hex), 2):
352
+ byte_value = int(active_list_hex[i:i+2], 16)
353
+ fileID.write(byte_value.to_bytes(1, byteorder='big'))
354
+ angle = self.AcousticFields[j].angle
355
+ fileID.write(np.int8(angle).tobytes())
356
+ fileID.write(AO_signal[:, j].astype(np.float32).tobytes())
357
+
358
+ header_content = (
359
+ f"Data filename: {'AOSignals_withTumor.cdf' if withTumor else 'AOSignals_withoutTumor.cdf'}\n"
360
+ f"Number of events: {nScan}\n"
361
+ f"Number of acquisitions per event: {AO_signal.shape[0]}\n"
362
+ f"Start time (s): 0\n"
363
+ f"Duration (s): 1\n"
364
+ f"Acquisition frequency (Hz): {self.params.acoustic['f_saving']}\n"
365
+ f"Data mode: histogram\n"
366
+ f"Data type: AOT\n"
367
+ f"Number of US transducers: {self.params.acoustic['num_elements']}"
368
+ )
369
+
370
+ with open(cdh_location, "w") as fileID:
371
+ fileID.write(header_content)
372
+
373
+ with open(info_location, "w") as fileID:
374
+ for field in self.AcousticFields:
375
+ fileID.write(field.getName_field() + "\n")
376
+
377
+ print(f"Fichiers .cdf, .cdh et info.txt sauvegardés dans {save_directory}")
378
+
379
+ def showAOsignal(self, withTumor=True, save_dir=None, wave_name=None):
380
+ if withTumor and self.AOsignal_withTumor is None:
381
+ raise ValueError("AO signal with tumor is not generated. Please generate it first.")
382
+ if not withTumor and self.AOsignal_withoutTumor is None:
383
+ raise ValueError("AO signal without tumor is not generated. Please generate it first.")
384
+
385
+ if withTumor:
386
+ AOsignal = self.AOsignal_withTumor
387
+ else:
388
+ AOsignal = self.AOsignal_withoutTumor
389
+
390
+ time_axis = np.arange(AOsignal.shape[0]) / float(self.params.acoustic['f_AQ']) * 1e6
391
+
392
+ num_plots = AOsignal.shape[1]
393
+ if num_plots <= 5:
394
+ nrows, ncols = 1, num_plots
395
+ else:
396
+ ncols = 5
397
+ nrows = (num_plots + ncols - 1) // ncols
398
+
399
+ fig, axes = plt.subplots(nrows, ncols, figsize=(5 * ncols, 5.3 * nrows))
400
+ if isinstance(axes, plt.Axes):
401
+ axes = np.array([axes])
402
+ axes = axes.flatten()
403
+
404
+ if wave_name is None:
405
+ title = "AO Signal -- all plots"
406
+ else:
407
+ title = f"AO Signal -- {wave_name}"
408
+
409
+ fig.suptitle(title, fontsize=12, y=0.98)
410
+
411
+ for idx in range(num_plots):
412
+ ax = axes[idx]
413
+ ax.plot(time_axis, AOsignal[:, idx])
414
+ ax.set_xlabel("Time (µs)", fontsize=8)
415
+ ax.set_ylabel("Value", fontsize=8)
416
+
417
+ for j in range(num_plots, len(axes)):
418
+ fig.delaxes(axes[j])
419
+
420
+ plt.tight_layout(rect=[0, 0, 1, 0.95])
421
+
422
+ if save_dir is not None:
423
+ now = datetime.now()
424
+ date_str = now.strftime("%Y_%d_%m_%y")
425
+ os.makedirs(save_dir, exist_ok=True)
426
+ save_filename = f"Static_y_Plot{wave_name}_{date_str}.png"
427
+ save_path = os.path.join(save_dir, save_filename)
428
+ plt.savefig(save_path, dpi=200)
429
+ print(f"Saved: {save_path}")
430
+
431
+ plt.show()
432
+ plt.close(fig)
433
+
434
+ def show_animated_all(self, fileOfAcousticField=None, save_dir=None, desired_duration_ms=5000):
435
+ mpl.rcParams['animation.embed_limit'] = 100
436
+ pattern_str = StructuredWave.getPattern(fileOfAcousticField)
437
+ angle = StructuredWave.getAngle(fileOfAcousticField)
438
+ fieldToPlot = None
439
+
440
+ for field in self.AcousticFields:
441
+ if field.get_path() == fileOfAcousticField:
442
+ fieldToPlot = field
443
+ idx = self.AcousticFields.index(field)
444
+ break
445
+ else:
446
+ raise ValueError(f"Field {fileOfAcousticField} not found in AcousticFields.")
447
+
448
+ if wave_name is None:
449
+ wave_name = f"Pattern structure {pattern_str}"
450
+
451
+ fig, axs = plt.subplots(1, 2, figsize=(6 * 2, 5.3 * 1))
452
+ if isinstance(axs, plt.Axes):
453
+ axs = np.array([axs])
454
+
455
+ fig.suptitle(f"AO Signal Animation {wave_name} | Angle {angle}°", fontsize=12, y=0.98)
456
+
457
+ axs[0].imshow(self.OpticImage.T, cmap='hot', alpha=1, origin='upper',
458
+ extent=(self.params['Xrange'][0], self.params['Xrange'][1], self.params['Zrange'][1], self.params['Zrange'][0]),
459
+ aspect='equal')
460
+
461
+ im_field = axs[0].imshow(fieldToPlot[0, :, :, idx], cmap='jet', origin='upper',
462
+ extent=(self.params['Xrange'][0], self.params['Xrange'][1], self.params['Zrange'][1], self.params['Zrange'][0]),
463
+ vmax=1, vmin=0.01, alpha=0.8, aspect='equal')
464
+
465
+ axs[0].set_title(f"{wave_name} | Angle {angle}° | t = 0.00 ms", fontsize=10)
466
+ axs[0].set_xlabel("x (mm)", fontsize=8)
467
+ axs[0].set_ylabel("z (mm)", fontsize=8)
468
+
469
+ time_axis = np.arange(self.AOsignal.shape[0]) * 25e-6 * 1000
470
+ line_y, = axs[1].plot(time_axis, self.AOsignal[:, idx])
471
+ vertical_line, = axs[1].plot([time_axis[0], time_axis[0]], [0, self.AOsignal[0, idx]], 'r--')
472
+
473
+ axs[1].set_xlabel("Time (ms)", fontsize=8)
474
+ axs[1].set_ylabel("Value", fontsize=8)
475
+ axs[1].set_title(f"{wave_name} | Angle {angle}° | t = 0.00 ms", fontsize=10)
476
+
477
+ plt.tight_layout(rect=[0, 0, 1, 0.95])
478
+
479
+ def update(frame):
480
+ current_time_ms = frame * 25e-6 * 1000
481
+ frame_data = fieldToPlot[frame, :, :, idx]
482
+ masked_data = np.where(frame_data > 0.02, frame_data, np.nan)
483
+ im_field.set_data(masked_data)
484
+ axs[0].set_title(f"{wave_name} | Angle {angle}° | t = {current_time_ms:.2f} ms", fontsize=10)
485
+
486
+ y_vals = self.AOsignal[:, idx]
487
+ y_copy = np.full_like(y_vals, np.nan)
488
+ y_copy[:frame + 1] = y_vals[:frame + 1]
489
+ line_y.set_data(time_axis, y_copy)
490
+
491
+ vertical_line.set_data([time_axis[frame], time_axis[frame]], [0, y_vals[frame]])
492
+ axs[1].set_title(f"{wave_name} | Angle {angle}° | t = {current_time_ms:.2f} ms", fontsize=10)
493
+
494
+ return [im_field, vertical_line, line_y]
495
+
496
+ interval = desired_duration_ms / fieldToPlot.shape[0]
497
+ ani = animation.FuncAnimation(
498
+ fig, update,
499
+ frames=range(0, self.AcousticFields.shape[0]),
500
+ interval=interval, blit=True
501
+ )
502
+
503
+ if save_dir is not None:
504
+ now = datetime.now()
505
+ date_str = now.strftime("%Y_%d_%m_%y")
506
+ os.makedirs(save_dir, exist_ok=True)
507
+ save_filename = f"A_y_LAMBDA_overlay_{pattern_str}_{angle}_{date_str}.gif"
508
+ save_path = os.path.join(save_dir, save_filename)
509
+ ani.save(save_path, writer='pillow', fps=20)
510
+ print(f"Saved: {save_path}")
511
+
512
+ plt.close(fig)
513
+ return ani
514
+
515
+ def showPhantom(self, withROI=False):
516
+ """
517
+ Displays the optical phantom with absorbers.
518
+ """
519
+ try:
520
+ if withROI:
521
+ self.OpticImage.show_ROI()
522
+ else:
523
+ self.OpticImage.show_phantom()
524
+ except Exception as e:
525
+ raise RuntimeError(f"Error plotting phantom: {e}")
526
+
527
+ @abstractmethod
528
+ def check(self):
529
+ """
530
+ Check if the experiment is correctly initialized.
531
+ """
532
+ pass
@@ -0,0 +1,24 @@
1
+
2
+ class Absorber:
3
+ def __init__(self, name, type, center, radius, amplitude):
4
+ """
5
+ Initializes an absorber with the given parameters.
6
+ :param name: Name of the absorber.
7
+ :param type: Type of the absorber.
8
+ :param center: Center of the absorber.
9
+ :param radius: Radius of the absorber.
10
+ :param amplitude: Amplitude of the absorber.
11
+ """
12
+ self.name = name
13
+ self.type = type
14
+ self.center = center
15
+ self.radius = radius
16
+ self.amplitude = amplitude
17
+
18
+ def __repr__(self):
19
+ """
20
+ String representation of the absorber.
21
+ :return: String representing the absorber.
22
+ """
23
+ return (f"Absorber(name={self.name}, type={self.type}, "
24
+ f"center={self.center}, radius={self.radius}, amplitude={self.amplitude})")
@@ -0,0 +1,70 @@
1
+ from .OpticEnums import OpticFieldType
2
+
3
+ import numpy as np
4
+
5
+ class Laser:
6
+ def __init__(self, params):
7
+ """
8
+ Initializes the laser with the given parameters.
9
+ :param params: Configuration parameters for the laser.
10
+ """
11
+ try:
12
+ self.x = np.arange(params.general['Xrange'][0], params.general['Xrange'][1], params.general['dx']) * 1000
13
+ self.z = np.arange(params.general['Zrange'][0], params.general['Zrange'][1], params.general['dz']) * 1000
14
+ self.shape = OpticFieldType(params.optic['laser']['shape'].capitalize())
15
+ self.center = params.optic['laser']['center']
16
+ self.w0 = params.optic['laser']['w0'] * 1000
17
+ self._set_intensity()
18
+ except KeyError as e:
19
+ raise ValueError(f"Missing parameter: {e}")
20
+ except ValueError as e:
21
+ raise ValueError(f"Invalid laser shape: {e}")
22
+
23
+ def _set_intensity(self):
24
+ """
25
+ Sets the intensity of the beam based on its shape.
26
+ """
27
+ try:
28
+ if self.shape == OpticFieldType.GAUSSIAN:
29
+ self.intensity = self._gaussian_beam()
30
+ elif self.shape == OpticFieldType.UNIFORM:
31
+ raise NotImplementedError("Uniform beam not implemented yet.")
32
+ elif self.shape == OpticFieldType.SPHERICAL:
33
+ raise NotImplementedError("Spherical beam not implemented yet.")
34
+ else:
35
+ raise ValueError("Unknown beam shape.")
36
+ except Exception as e:
37
+ raise RuntimeError(f"Error setting intensity: {e}")
38
+
39
+ def _gaussian_beam(self):
40
+ """
41
+ Generates a Gaussian laser beam in the XZ plane.
42
+ :return: Intensity matrix of the Gaussian beam.
43
+ """
44
+ try:
45
+ if self.center == 'center':
46
+ x0 = (self.x[0] + self.x[-1]) / 2
47
+ z0 = (self.z[0] + self.z[-1]) / 2
48
+ else:
49
+ x0 = self.center[0] * 1000
50
+ z0 = self.center[1] * 1000
51
+ X, Z = np.meshgrid(self.x, self.z, indexing='ij')
52
+ return np.exp(-2 * ((X - x0)**2 + (Z - z0)**2) / self.w0**2)
53
+ except Exception as e:
54
+ raise RuntimeError(f"Error generating Gaussian beam: {e}")
55
+
56
+ def show_laser(self):
57
+ """
58
+ Displays the laser intensity distribution.
59
+ """
60
+ try:
61
+ import matplotlib.pyplot as plt
62
+ plt.imshow(self.intensity, extent=(self.x[0], self.x[-1] + 1, self.z[-1], self.z[0]), aspect='auto', cmap='hot')
63
+ plt.colorbar(label='Intensity')
64
+ plt.xlabel('X (mm)', fontsize=20)
65
+ plt.ylabel('Z (mm)', fontsize=20)
66
+ plt.tick_params(axis='both', which='major', labelsize=20)
67
+ plt.title('Laser Intensity Distribution')
68
+ plt.show()
69
+ except Exception as e:
70
+ raise RuntimeError(f"Error plotting laser intensity: {e}")
@@ -0,0 +1,17 @@
1
+ from enum import Enum
2
+
3
+ class OpticFieldType(Enum):
4
+ """
5
+ Enumeration of available optic field types.
6
+
7
+ Selection of optic field types:
8
+ - GAUSSIAN: A Gaussian optic field type.
9
+ - UNIFORM: A uniform optic field type.
10
+ - SPHERICAL: A spherical optic field type.
11
+ """
12
+ GAUSSIAN = "Gaussian"
13
+ """A Gaussian optic field type."""
14
+ UNIFORM = "Uniform"
15
+ """A uniform optic field type."""
16
+ SPHERICAL = "Spherical"
17
+ """A spherical optic field type."""
@@ -0,0 +1,10 @@
1
+ from ._mainOptic import *
2
+ from .Absorber import *
3
+ from .Laser import *
4
+ from .OpticEnums import *
5
+
6
+ # Docstring for the AOT_Optic package
7
+ """
8
+ AOT_Optic is a package for optical components in Acousto-Optic Tomography.
9
+ It provides tools and classes for working with optical elements such as absorbers and lasers.
10
+ """