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
@@ -1,541 +0,0 @@
1
-
2
- from .Settings import *
3
- from .AOT_Optic import *
4
- from .AOT_Acoustic import *
5
- import os
6
- import ast
7
- import matplotlib.pyplot as plt
8
- import matplotlib.animation as animation
9
- import numpy as np
10
- import matplotlib as mpl
11
- from datetime import datetime
12
- from tqdm import trange
13
- from enum import Enum
14
- from .config import config
15
-
16
- class Experiment():
17
- def __init__(self, params):
18
- self.params = params
19
- self.OpticImage = AOT_biomaps.AOT_Optic.Phantom(params=params)
20
- self.AOsignal = None
21
- self.AcousticFields = None
22
-
23
- if type(params) != AOT_biomaps.Settings.Params:
24
- raise TypeError("params must be an instance of the Params class")
25
-
26
- class Focus(Experiment):
27
- def __init__(self, **kwargs):
28
- super().__init__(**kwargs)
29
-
30
- class Tomography(Experiment):
31
-
32
- def __init__(self, acousticType = AOT_biomaps.AOT_Acoustic.WaveType.StructuredWave ,fieldParamPath=None, fieldDataPath = None, formatSave = AOT_biomaps.AOT_Acoustic.FormatSave.HDR_IMG, **kwargs):
33
- super().__init__(**kwargs)
34
-
35
- if fieldParamPath is None:
36
- raise ValueError("fieldParamPath must be provided for Tomography experiment")
37
- if type(acousticType).__name__ != "WaveType":
38
- raise TypeError("acousticType must be an instance of the WaveType class")
39
- self.TypeAcoustic = acousticType
40
- self.FormatSave = formatSave
41
- self.fieldDataPath = fieldDataPath
42
- self.AcousticFields = self._generate_system_matrix(fieldDataPath, fieldParamPath)
43
- self.OpticImage = AOT_biomaps.AOT_Optic.Phantom(params=self.params)
44
- self.AOsignal = self._generate_AO_signal()
45
-
46
- if type(self.params)!= AOT_biomaps.Settings.Params:
47
- raise TypeError("params must be an instance of the Params class")
48
-
49
- def _generate_system_matrix(self, fieldDataPath, fieldParamPath):
50
- """
51
- Generate the system matrix for the structured wave.
52
-
53
- Args:
54
- fieldDataPath: Path to save the generated fields.
55
- fieldParamPath: Path to the field parameters file.
56
- params: Parameters object containing simulation settings.
57
-
58
- Returns:
59
- systemMatrix: A numpy array of the generated fields.
60
- """
61
- if self.TypeAcoustic.value == AOT_biomaps.AOT_Acoustic.WaveType.StructuredWave.value:
62
- return self._generate_system_matrixSTRUCT(fieldDataPath, fieldParamPath)
63
- else:
64
- raise ValueError("Unsupported wave type.")
65
-
66
- def _generate_system_matrixSTRUCT(self, fieldDataPath, fieldParamPath):
67
- if not os.path.exists(fieldParamPath):
68
- raise FileNotFoundError(f"Field parameter file {fieldParamPath} not found.")
69
- print(fieldDataPath)
70
- if not fieldDataPath is None:
71
- os.makedirs(fieldDataPath, exist_ok=True)
72
- listAcousticFields = []
73
- patternList = []
74
- with open(fieldParamPath, 'r') as file:
75
- lines = file.readlines()
76
- for line in lines:
77
- line = line.strip()
78
- if not line:
79
- continue # skip empty lines
80
-
81
- try:
82
- # Sécurise l'évaluation en supprimant accès à builtins
83
- parsed = eval(line, {"__builtins__": None})
84
-
85
- if isinstance(parsed, tuple) and len(parsed) == 2:
86
- coords, angles = parsed
87
- for angle in angles:
88
- patternList.append([*coords, angle])
89
- else:
90
- raise ValueError("Ligne inattendue (pas un tuple de deux éléments)")
91
-
92
- except Exception as e:
93
- print(f"Erreur de parsing sur la ligne : {line}\n{e}")
94
-
95
- if config.get_process() == 'gpu':
96
- print("Using GPU for computation.")
97
- else:
98
- print("Using CPU for computation.")
99
- progress_bar = trange(1,len(patternList), desc="Generating system matrix")
100
-
101
- for i in progress_bar:
102
- pattern = patternList[i]
103
- if len(pattern) != 5:
104
- raise ValueError(f"Invalid pattern format: {pattern}. Expected 5 values.")
105
- # Initialisation de l'objet AcousticField
106
- AcousticField = AOT_biomaps.AOT_Acoustic.StructuredWave(
107
- angle_deg=pattern[4],
108
- space_0=pattern[0],
109
- space_1=pattern[1],
110
- move_head_0_2tail=pattern[2],
111
- move_tail_1_2head=pattern[3],
112
- params=self.params
113
- )
114
-
115
- if fieldDataPath is None:
116
- pathField = None
117
- else:
118
- pathField = os.path.join(fieldDataPath, os.path.basename(AcousticField.getName_field() + self.FormatSave.value))
119
-
120
- if not pathField is None and os.path.exists(pathField):
121
- progress_bar.set_postfix_str(f"Loading field - {AcousticField.getName_field()}")
122
- AcousticField.load_field(fieldDataPath, self.FormatSave)
123
-
124
- elif pathField is None or not os.path.exists(pathField):
125
- progress_bar.set_postfix_str(f"Generating field - {AcousticField.getName_field()}")
126
- AcousticField.generate_field()
127
-
128
- progress_bar.set_postfix_str(f"Calculating squared envelope of the field - {AcousticField.getName_field()}")
129
- AcousticField.calculate_envelope_squared()
130
-
131
- progress_bar.set_postfix_str(f"Saving system matrix - {AcousticField.getName_field()}")
132
-
133
- if not pathField is None and not os.path.exists(pathField):
134
- os.makedirs(os.path.dirname(pathField), exist_ok=True)
135
- AcousticField.save_field(fieldDataPath)
136
-
137
- listAcousticFields.append(AcousticField)
138
- # Réinitialiser le texte de la barre de progression pour l'itération suivante
139
- progress_bar.set_postfix_str("")
140
-
141
- return listAcousticFields
142
-
143
- def _generate_AO_signal(self):
144
- if self.AcousticFields is None:
145
- raise ValueError("AcousticFields is not initialized. Please generate the system matrix first.")
146
- # for field in self.AcousticFields:
147
- # print(field)
148
- #check if all AcousticFields.field have the same shape
149
- if not all(field.enveloppe.shape == self.AcousticFields[0].enveloppe.shape for field in self.AcousticFields):
150
- minShape = min([field.enveloppe.shape[0] for field in self.AcousticFields])
151
- self.cutAcousticFields(minShape*self.params['fs_aq'])
152
- else:
153
- shape_field = self.AcousticFields[0].enveloppe.shape
154
-
155
- AOsignal = np.zeros((shape_field[0], len(self.AcousticFields)), dtype=np.float32)
156
-
157
- for i in trange(len(self.AcousticFields), desc="Generating AO Signal"):
158
- for t in range(self.AcousticFields[i].enveloppe.shape[0]):
159
- interaction = self.OpticImage.phantom * self.AcousticFields[i].enveloppe[t, :, :]
160
- AOsignal[t,i] = np.sum(interaction)
161
-
162
- return AOsignal
163
-
164
-
165
- def cutAcousticFields(self,max_t,min_t=0,saveFields=False):
166
-
167
- min_sample = int(np.floor(min_t * float(self.params.acoustic['f_AQ'])))
168
- max_sample = int(np.floor(max_t * float(self.params.acoustic['f_AQ'])))
169
-
170
- print(f"Cutting fields from {min_sample} to {max_sample} samples.")
171
- if min_sample < 0 or max_sample < 0:
172
- raise ValueError("min_sample and max_sample must be non-negative integers.")
173
- if min_sample >= max_sample:
174
- raise ValueError("min_sample must be less than max_sample.")
175
-
176
- if not self.AcousticFields:
177
- raise ValueError("AcousticFields is empty. Cannot cut fields.")
178
-
179
- for i in trange(len(self.AcousticFields), desc="Cutting Acoustic Fields"):
180
- field = self.AcousticFields[i]
181
- if field.field.shape[0] < max_sample:
182
- raise ValueError(f"Field {field.getName_field()} has an invalid shape: {field.enveloppe.shape}. Expected shape to be at least ({max_sample},).")
183
- self.AcousticFields[i].enveloppe = field.enveloppe[min_sample:max_sample, :, :]
184
- self.AcousticFields[i].field = field.field[min_sample:max_sample, :, :]
185
-
186
- self.AOsignal = self._generate_AO_signal()
187
-
188
- if saveFields:
189
- self._saveSMatrixForCastor(self.fieldDataPath)
190
-
191
-
192
- def _saveSMatrixForCastor(self, save_directory):
193
- for i in trange(len(self.AcousticFields), desc="Saving Acoustic Fields"):
194
- field = self.AcousticFields[i]
195
- field.save_field(save_directory,formatSave = self.FormatSave)
196
-
197
- def _saveAOsignalForCastor(self,save_directory):
198
- """
199
- Sauvegarde le signal AO au format .cdf et .cdh (comme dans le script MATLAB)
200
-
201
- :param AOsignal: np.ndarray de taille (times, angles)
202
- :param save_directory: chemin de sauvegarde
203
- :param set_id: identifiant du set
204
- :param n_experiment: identifiant de l'expérience
205
- :param param: dictionnaire contenant les paramètres nécessaires (comme fs_aq, Nt, angles, etc.)
206
- """
207
-
208
- # Noms des fichiers de sortie
209
- cdf_location = os.path.join(save_directory, "AOSignals.cdf")
210
- cdh_location = os.path.join(save_directory, "AOSignals.cdh")
211
- info_location = os.path.join(save_directory, "info.txt")
212
-
213
- # Calcul des angles (en degrés) si nécessaire
214
-
215
- nScan = self.AOsignal.shape[1] # Nombre de scans ou d'événements
216
-
217
- # **1. Sauvegarde du fichier .cdf**
218
- with open(cdf_location, "wb") as fileID:
219
- for j in range(self.AOsignal.shape[1]):
220
- active_list = self.AcousticFields[j].pattern.activeList
221
- angle = self.AcousticFields[j].angle
222
- # Écrire les identifiants hexadécimaux
223
- active_list_str = ''.join(map(str, active_list))
224
-
225
- nb_padded_zeros = (4 - len(active_list_str) % 4) % 4 # Calcul du nombre de 0 nécessaires
226
- active_list_str += '0' * nb_padded_zeros # Ajout des zéros à la fin de la chaîne
227
-
228
- # Regrouper par paquets de 4 bits et convertir chaque paquet en hexadécimal
229
- active_list_hex = ''.join([hex(int(active_list_str[i:i+4], 2))[2:] for i in range(0, len(active_list_str), 4)])
230
- for i in range(0, len(active_list_hex), 2): # Chaque 2 caractères hex représentent 1 octet
231
- byte_value = int(active_list_hex[i:i + 2], 16) # Convertit l'hexadécimal en entier
232
- fileID.write(byte_value.to_bytes(1, byteorder='big')) # Écriture en big endian
233
-
234
- fileID.write(np.int8(angle).tobytes())
235
-
236
- # Écrire le signal AO correspondant (times x 1) en single (float32)
237
- fileID.write(self.AOsignal[:, j].astype(np.float32).tobytes())
238
-
239
- # **2. Sauvegarde du fichier .cdh**
240
- header_content = (
241
- f"Data filename: AOSignals.cdf\n"
242
- f"Number of events: {nScan}\n"
243
- f"Number of acquisitions per event: {self.AOsignal.shape[1]}\n"
244
- f"Start time (s): 0\n"
245
- f"Duration (s): 1\n"
246
- f"Acquisition frequency (Hz): {1/self.AcousticFields[0].kgrid.dt}\n"
247
- f"Data mode: histogram\n"
248
- f"Data type: AOT\n"
249
- f"Number of US transducers: {self.params['num_elements']}"
250
- )
251
- with open(cdh_location, "w") as fileID:
252
- fileID.write(header_content)
253
-
254
- with open(info_location, "w") as fileID:
255
- for field in self.AcousticFields:
256
- fileID.write(field.getName_field() + "\n")
257
-
258
- print(f"Fichiers .cdf, .cdh et info.txt sauvegardés dans {save_directory}")
259
-
260
- def load_AOSignal(self,cdh_file):
261
- # Lire les métadonnées depuis le fichier .cdh
262
- with open(cdh_file, "r") as file:
263
- cdh_content = file.readlines()
264
-
265
- # Extraire les dimensions des données à partir des métadonnées
266
- n_events = int([line.split(":")[1].strip() for line in cdh_content if "Number of events" in line][0])
267
- n_acquisitions = int([line.split(":")[1].strip() for line in cdh_content if "Number of acquisitions per event" in line][0])
268
-
269
- # Initialiser la matrice pour stocker les données
270
- AOsignal_matrix = np.zeros((n_events, n_acquisitions), dtype=np.float32)
271
-
272
- # Lire les données binaires depuis le fichier .cdf
273
- with open(cdh_file.replace(".cdh", ".cdf"), "rb") as file:
274
- for event in range(n_events):
275
- # Lire et ignorer la chaîne hexadécimale (active_list)
276
- num_elements = int([line.split(":")[1].strip() for line in cdh_content if "Number of US transducers" in line][0])
277
- hex_length = (num_elements + 3) // 4 # Nombre de caractères hex nécessaires
278
- file.read(hex_length // 2) # Ignorer la chaîne hexadécimale
279
-
280
- # Lire l'angle (int8)
281
- angle = int.from_bytes(file.read(1), byteorder='big', signed=True)
282
-
283
- # Lire le signal AO correspondant (float32)
284
- signal = np.frombuffer(file.read(n_acquisitions * 4), dtype=np.float32) # 4 octets par float32
285
-
286
- # Stocker le signal dans la matrice
287
- AOsignal_matrix[event, :] = signal
288
-
289
- self.AOsignal = AOsignal_matrix
290
-
291
- def plot_animations_A_matrix(self, wave_name=None, desired_duration_ms = 5000, save_dir=None):
292
- """
293
- Plot synchronized animations of A_matrix slices for selected angles.
294
-
295
- Args:
296
- A_matrix: 4D numpy array (time, z, x, angles)
297
- z: array of z-axis positions
298
- x: array of x-axis positions
299
- angles_to_plot: list of angles to visualize
300
- wave_name: optional name for labeling the subplots (e.g., "wave1")
301
- step: time step between frames (default every 10 frames)
302
- save_dir: directory to save the animation gif; if None, animation will not be saved
303
-
304
- Returns:
305
- ani: Matplotlib FuncAnimation object
306
- """
307
-
308
- # Set the maximum embedded animation size to 100 MB
309
- mpl.rcParams['animation.embed_limit'] = 100
310
-
311
- if save_dir is not None:
312
- os.makedirs(save_dir, exist_ok=True)
313
-
314
- num_plots = len(self.AcousticFields)
315
-
316
- # Automatically adjust layout
317
- if num_plots <= 5:
318
- nrows, ncols = 1, num_plots
319
- else:
320
- ncols = 5
321
- nrows = (num_plots + ncols - 1) // ncols
322
-
323
- # Create figure and subplots
324
- fig, axes = plt.subplots(nrows, ncols, figsize=(5 * ncols, 5.3 * nrows))
325
- if isinstance(axes, plt.Axes):
326
- axes = np.array([axes])
327
- axes = axes.flatten()
328
-
329
- ims = []
330
-
331
- # Set main title
332
- fig.suptitle(f"System Matrix Animation {wave_name}",
333
- fontsize=12, y=0.98)
334
-
335
- # Ensure start_idx_in_A_matrix exists
336
- if not hasattr(self, 'start_idx_in_A_matrix'):
337
- raise AttributeError("StructuredWave must have attribute 'start_idx_in_A_matrix' to locate A_matrix slices.")
338
-
339
- for idx in range(num_plots):
340
- ax = axes[idx]
341
-
342
- im = ax.imshow(self.AcousticFields[0, :, :, idx],
343
- # extent=(x[0]*1000, x[-1]*1000, z[-1]*1000, z[0]*1000), vmax = 0.2*np.max(A_matrix[:,:,:,global_index]),
344
- extent=(self.params['Xrange'][0], self.params['Xrange'][1], self.params['Zrange'][1],self.params['Zrange'][0]), vmax = 1,
345
- aspect='equal', cmap='jet', animated=True)
346
- ax.set_xlabel("x (mm)", fontsize=8)
347
- ax.set_ylabel("z (mm)", fontsize=8)
348
- ims.append((im, ax, idx))
349
-
350
- # Remove unused axes if any
351
- for j in range(num_plots, len(axes)):
352
- fig.delaxes(axes[j])
353
-
354
- # Adjust layout to leave space for main title
355
- plt.tight_layout(rect=[0, 0, 1, 0.95])
356
-
357
- # Unified update function for all subplots
358
- def update(frame):
359
- artists = []
360
- for im, ax, idx in ims:
361
- im.set_array(self.AcousticFields[frame, :, :,idx ])
362
- fig.suptitle(f"System Matrix Animation {wave_name} t = {frame * 25e-6 * 1000:.2f} ms", fontsize=10)
363
- artists.append(im)
364
- return artists
365
-
366
- interval = desired_duration_ms / self.AcousticFields.shape[0]
367
- # Create animation
368
- ani = animation.FuncAnimation(
369
- fig, update,
370
- frames=range(0, self.AcousticFields.shape[0]),
371
- interval=interval, blit=True
372
- )
373
-
374
- # Save animation if needed
375
- if save_dir is not None:
376
- now = datetime.now()
377
- date_str = now.strftime("%Y_%d_%m_%y")
378
- save_filename = f"AcousticField_{wave_name}_{date_str}.gif"
379
- save_path = os.path.join(save_dir, save_filename)
380
- ani.save(save_path, writer='pillow', fps=20)
381
- print(f"Saved: {save_path}")
382
-
383
- plt.close(fig)
384
-
385
- return ani
386
-
387
- def plot_animated_y_A_LAMBDA(self, fileOfAcousticField, save_dir=None, desired_duration_ms = 5000):
388
-
389
- mpl.rcParams['animation.embed_limit'] = 100
390
-
391
- pattern_str = AOT_biomaps.AOT_Acoustic.StructuredWave.getPattern(fileOfAcousticField)
392
- angle = AOT_biomaps.AOT_Acoustic.StructuredWave.getAngle(fileOfAcousticField)
393
-
394
- fieldToPlot = None
395
-
396
- for field in self.AcousticFields:
397
- if field.get_path() == fileOfAcousticField:
398
- break
399
- fieldToPlot = field
400
- idx = self.AcousticFields.index(field)
401
- else:
402
- raise ValueError(f"Field {fileOfAcousticField} not found in AcousticFields.")
403
-
404
- if wave_name is None:
405
- wave_name = f"Pattern structure {pattern_str}"
406
-
407
- fig, axs = plt.subplots(1, 2, figsize=(6 * 2, 5.3 * 1))
408
-
409
- if isinstance(axs, plt.Axes):
410
- axs = np.array([axs])
411
-
412
- fig.suptitle(f"AO Signal Animation {wave_name} | Angle {angle}°", fontsize=12, y=0.98)
413
-
414
- # Left: LAMBDA at bottom
415
- axs[0].imshow(self.OpticImage.T, cmap='hot', alpha=1, origin='upper',
416
- extent=(self.params['Xrange'][0], self.params['Xrange'][1], self.params['Zrange'][1], self.params['Zrange'][0]),
417
- aspect='equal')
418
-
419
- # Acoustic field drawn on top of LAMBDA
420
- im_field = axs[0].imshow(fieldToPlot[0, :, :, idx], cmap='jet', origin='upper',
421
- extent=(self.params['Xrange'][0], self.params['Xrange'][1], self.params['Zrange'][1], self.params['Zrange'][0]),
422
- vmax = 1, vmin = 0.01, alpha=0.8,
423
- aspect='equal')
424
-
425
- axs[0].set_title(f"{wave_name} | Angle {angle}° | t = 0.00 ms", fontsize=10)
426
- axs[0].set_xlabel("x (mm)", fontsize=8)
427
- axs[0].set_ylabel("z (mm)", fontsize=8)
428
-
429
- # Center: AO signal y
430
- time_axis = np.arange(self.AOsignal.shape[0]) * 25e-6 * 1000 # in ms
431
- line_y, = axs[1].plot(time_axis, self.AOsignal[:, idx])
432
- # vertical_line = axs[1].axvline(x=time_axis[0], color='r', linestyle='--')
433
- vertical_line, = axs[1].plot([time_axis[0], time_axis[0]], [0, self.AOsignal[0, idx]], 'r--')
434
- axs[1].set_xlabel("Time (ms)", fontsize=8)
435
- axs[1].set_ylabel("Value", fontsize=8)
436
- axs[1].set_title(f"{wave_name} | Angle {angle}° | t = 0.00 ms", fontsize=10)
437
-
438
- plt.tight_layout(rect=[0, 0, 1, 0.95])
439
-
440
- def update(frame):
441
- current_time_ms = frame * 25e-6 * 1000
442
-
443
- # Apply masking to suppress background
444
- frame_data = fieldToPlot[frame, :, :, idx]
445
- masked_data = np.where(frame_data > 0.02, frame_data, np.nan)
446
- im_field.set_data(masked_data)
447
-
448
- axs[0].set_title(f"{wave_name} | Angle {angle}° | t = {current_time_ms:.2f} ms", fontsize=10)
449
-
450
- # Copy partial y signal
451
- y_vals = self.AOsignal[:, idx]
452
- y_copy = np.full_like(y_vals, np.nan)
453
- y_copy[:frame + 1] = y_vals[:frame + 1]
454
- line_y.set_data(time_axis, y_copy)
455
-
456
- # Red vertical line
457
- vertical_line.set_data([time_axis[frame], time_axis[frame]], [0, y_vals[frame]])
458
-
459
- axs[1].set_title(f"{wave_name} | Angle {angle}° | t = {current_time_ms:.2f} ms", fontsize=10)
460
-
461
- return [im_field, vertical_line, line_y]
462
-
463
- interval = desired_duration_ms / fieldToPlot.shape[0]
464
-
465
- # Create the animation
466
- ani = animation.FuncAnimation(
467
- fig, update,
468
- frames=range(0, self.AcousticFields.shape[0]),
469
- interval=interval, blit=True
470
- )
471
-
472
- if save_dir is not None:
473
- now = datetime.now()
474
- date_str = now.strftime("%Y_%d_%m_%y")
475
- os.makedirs(save_dir, exist_ok=True)
476
- save_filename = f"A_y_LAMBDA_overlay_{pattern_str}_{angle}_{date_str}.gif"
477
- save_path = os.path.join(save_dir, save_filename)
478
- ani.save(save_path, writer='pillow', fps=20)
479
- print(f"Saved: {save_path}")
480
-
481
- plt.close(fig)
482
-
483
- return ani
484
-
485
- def plot_AO_signal_y(self, save_dir=None, wave_name=None):
486
- """
487
- Plot AO signals y(t) for selected angles.
488
-
489
- Args:
490
- y: 2D numpy array (time, angles)
491
- angles_to_plot: list of angles to visualize
492
- save_dir: directory to save the figure; if None, only display
493
- wave_name: optional name for labeling; default uses pattern structure
494
- """
495
-
496
- # Time axis in milliseconds
497
- time_axis = np.arange(self.AOsignal.shape[0]) * 25e-6 * 1000
498
-
499
- # Set up layout
500
- num_plots = self.AOsignal.shape[1]
501
-
502
- if num_plots <= 5:
503
- nrows, ncols = 1, num_plots
504
- else:
505
- ncols = 5
506
- nrows = (num_plots + ncols - 1) // ncols
507
-
508
- fig, axes = plt.subplots(nrows, ncols, figsize=(5 * ncols, 5.3 * nrows))
509
- if isinstance(axes, plt.Axes):
510
- axes = np.array([axes])
511
- axes = axes.flatten()
512
-
513
- # Set main title
514
- fig.suptitle(f"AO Signal Plot {wave_name}", fontsize=12, y=0.98)
515
-
516
-
517
- for idx in range(num_plots):
518
- ax = axes[idx]
519
- ax.plot(time_axis, self.AOsignal[:,idx])
520
- ax.set_xlabel("Time (ms)", fontsize=8)
521
- ax.set_ylabel("Value", fontsize=8)
522
-
523
- # Remove unused axes
524
- for j in range(num_plots, len(axes)):
525
- fig.delaxes(axes[j])
526
-
527
- plt.tight_layout(rect=[0, 0, 1, 0.95])
528
-
529
- # Save if needed
530
- if save_dir is not None:
531
- now = datetime.now()
532
- date_str = now.strftime("%Y_%d_%m_%y")
533
- os.makedirs(save_dir, exist_ok=True)
534
- save_filename = f"Static_y_Plot{wave_name}_{date_str}.png"
535
- save_path = os.path.join(save_dir, save_filename)
536
- plt.savefig(save_path, dpi=200)
537
- print(f"Saved: {save_path}")
538
-
539
- plt.show()
540
- plt.close(fig)
541
-