AOT-biomaps 2.9.312__tar.gz → 2.9.332__tar.gz

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 (53) hide show
  1. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Acoustic/AcousticTools.py +0 -2
  2. aot_biomaps-2.9.332/AOT_biomaps/AOT_Experiment/ExperimentTools.py +79 -0
  3. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Experiment/Tomography.py +254 -9
  4. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/AOT_Optimizers/MLEM.py +2 -1
  5. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/AnalyticRecon.py +93 -0
  6. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/ReconTools.py +46 -4
  7. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/__init__.py +21 -1
  8. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps.egg-info/PKG-INFO +1 -1
  9. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps.egg-info/SOURCES.txt +1 -0
  10. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/PKG-INFO +1 -1
  11. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/setup.py +21 -1
  12. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Acoustic/AcousticEnums.py +0 -0
  13. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Acoustic/FocusedWave.py +0 -0
  14. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Acoustic/IrregularWave.py +0 -0
  15. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Acoustic/PlaneWave.py +0 -0
  16. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Acoustic/StructuredWave.py +0 -0
  17. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Acoustic/__init__.py +0 -0
  18. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Acoustic/_mainAcoustic.py +0 -0
  19. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Experiment/Focus.py +0 -0
  20. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Experiment/__init__.py +0 -0
  21. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Experiment/_mainExperiment.py +0 -0
  22. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Optic/Absorber.py +0 -0
  23. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Optic/Laser.py +0 -0
  24. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Optic/OpticEnums.py +0 -0
  25. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Optic/__init__.py +0 -0
  26. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Optic/_mainOptic.py +0 -0
  27. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/AOT_Optimizers/DEPIERRO.py +0 -0
  28. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/AOT_Optimizers/LS.py +0 -0
  29. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/AOT_Optimizers/MAPEM.py +0 -0
  30. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/AOT_Optimizers/PDHG.py +0 -0
  31. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/AOT_Optimizers/__init__.py +0 -0
  32. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Huber.py +0 -0
  33. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Quadratic.py +0 -0
  34. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/RelativeDifferences.py +0 -0
  35. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/__init__.py +0 -0
  36. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/SparseSMatrix_CSR.py +0 -0
  37. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/SparseSMatrix_SELL.py +0 -0
  38. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/__init__.py +0 -0
  39. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/AOT_biomaps_kernels.cubin +0 -0
  40. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/AlgebraicRecon.py +0 -0
  41. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/BayesianRecon.py +0 -0
  42. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/DeepLearningRecon.py +0 -0
  43. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/PrimalDualRecon.py +0 -0
  44. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/ReconEnums.py +0 -0
  45. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/__init__.py +0 -0
  46. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/AOT_Recon/_mainRecon.py +0 -0
  47. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/Config.py +0 -0
  48. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps/Settings.py +0 -0
  49. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps.egg-info/dependency_links.txt +0 -0
  50. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps.egg-info/requires.txt +0 -0
  51. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/AOT_biomaps.egg-info/top_level.txt +0 -0
  52. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/README.md +0 -0
  53. {aot_biomaps-2.9.312 → aot_biomaps-2.9.332}/setup.cfg +0 -0
@@ -157,8 +157,6 @@ def calculate_envelope_squared(field):
157
157
  print(f"Erreur dans calculate_envelope_squared: {e}")
158
158
  raise
159
159
 
160
-
161
-
162
160
  def getPattern(pathFile):
163
161
  """
164
162
  Get the pattern from a file path.
@@ -0,0 +1,79 @@
1
+ import numpy as np
2
+
3
+ def calc_mat_os(xm, fx, dx, bool_active_list, signal_type):
4
+ num_els = len(xm)
5
+
6
+ # Cas limite : Fréquence nulle (Décimation 0)
7
+ if fx == 0:
8
+ if signal_type == 'cos':
9
+ # cos(0) = 1 -> Tout est actif
10
+ mask = np.ones(num_els, dtype=bool)
11
+ else:
12
+ # sin(0) = 0 -> Tout est inactif
13
+ mask = np.zeros(num_els, dtype=bool)
14
+ else:
15
+ # Calcul normal pour fx > 0
16
+ half_period_elements = round(1 / (2 * fx * dx))
17
+
18
+ # Sécurité : si fx est tellement grand que half_period < 1
19
+ half_period_elements = max(1, half_period_elements)
20
+
21
+ indices = np.arange(num_els)
22
+ if signal_type == 'cos':
23
+ mask = ((indices // half_period_elements) % 2 == 0)
24
+ else:
25
+ # Déphasage de 90° pour le sinus : on décale d'une demi-demi-période
26
+ shift = half_period_elements // 2
27
+ mask = (((indices + shift) // half_period_elements) % 2 == 0)
28
+
29
+ return np.tile(mask[:, np.newaxis], (1, bool_active_list.shape[1]))
30
+
31
+ def convert_to_hex_list(matrix):
32
+ """
33
+ Convertit une matrice binaire en liste de strings hexa (paquets de 4 bits).
34
+ Chaque colonne devient une chaîne de caractères.
35
+ """
36
+ n_els, n_scans = matrix.shape
37
+
38
+ # 1. Padding pour s'assurer que n_els est multiple de 4
39
+ remainder = n_els % 4
40
+ if remainder != 0:
41
+ padding = np.zeros((4 - remainder, n_scans))
42
+ matrix = np.vstack([matrix, padding])
43
+
44
+ # 2. Reshape pour isoler des blocs de 4 bits (nibbles)
45
+ # Shape résultante : (Nombre de blocs, 4 bits, Nombre de scans)
46
+ blocks = matrix.reshape(-1, 4, n_scans)
47
+
48
+ # 3. Calcul de la valeur décimale de chaque bloc (0 à 15)
49
+ # On considère le premier élément comme le bit de poids faible (LSB)
50
+ weights = np.array([1, 2, 4, 8]).reshape(1, 4, 1)
51
+ dec_values = np.sum(blocks * weights, axis=1).astype(int)
52
+
53
+ # 4. Conversion en caractères Hexadécimaux
54
+ # On définit la table de conversion pour la rapidité
55
+ hex_table = np.array(list("0123456789abcdef"))
56
+ hex_matrix = hex_table[dec_values]
57
+
58
+ # 5. Assemblage des chaînes (de l'élément N vers 0 pour l'ordre Shift Register standard)
59
+ return ["".join(hex_matrix[::-1, col]) for col in range(n_scans)]
60
+
61
+ def hex_to_binary_profile(hex_string, n_piezos=192):
62
+ hex_string = hex_string.strip().replace(" ", "").replace("\n", "")
63
+ if set(hex_string.lower()) == {'f'}:
64
+ return np.ones(n_piezos, dtype=int)
65
+
66
+ try:
67
+ n_char = len(hex_string)
68
+ n_bits = n_char * 4
69
+ binary_str = bin(int(hex_string, 16))[2:].zfill(n_bits)
70
+ if len(binary_str) < n_piezos:
71
+ # Tronquer/padder en fonction de la taille réelle de la sonde
72
+ binary_str = binary_str.ljust(n_piezos, '0')
73
+ elif len(binary_str) > n_piezos:
74
+ binary_str = binary_str[:n_piezos]
75
+ return np.array([int(b) for b in binary_str])
76
+ except ValueError:
77
+ return np.zeros(n_piezos, dtype=int)
78
+
79
+
@@ -2,18 +2,43 @@ from ._mainExperiment import Experiment
2
2
  from AOT_biomaps.AOT_Acoustic.AcousticEnums import WaveType
3
3
  from AOT_biomaps.AOT_Acoustic.StructuredWave import StructuredWave
4
4
  from AOT_biomaps.Config import config
5
+ from AOT_biomaps.AOT_Experiment.ExperimentTools import calc_mat_os, convert_to_hex_list, hex_to_binary_profile
5
6
  import os
6
7
  import psutil
7
8
  import numpy as np
8
9
  import matplotlib.pyplot as plt
9
10
  from tqdm import trange
10
11
  import h5py
11
- from scipy.io import loadmat
12
+ from scipy.io import loadmat, savemat
13
+
12
14
 
13
15
  class Tomography(Experiment):
14
16
  def __init__(self, **kwargs):
15
17
  super().__init__(**kwargs)
16
18
  self.patterns = None
19
+ self.theta = []
20
+ self.decimations = []
21
+
22
+
23
+ for i in range(len(self.AcousticFields)):
24
+ profile = hex_to_binary_profile(self.AcousticFields[i].getName_field()[6:-4], self.params.acoustic['num_elements'])
25
+ angle = self.AcousticFields[i].angle
26
+ self.theta.append(angle)
27
+
28
+ if set(self.AcousticFields[i].getName_field()[6:-4].lower().replace(" ", "")) == {'f'}:
29
+ fs_key = 0.0 # fs_key est en mm^-1 (0.0 mm^-1)
30
+ else:
31
+ ft_prof = np.fft.fft(profile)
32
+ idx_max = np.argmax(np.abs(ft_prof[1:len(profile)//2])) + 1
33
+ freqs = np.fft.fftfreq(len(profile), d=self.params.general['dx'])
34
+
35
+ # freqs est en m^-1 car delta_x est en mètres.
36
+ fs_m_inv = abs(freqs[idx_max])
37
+
38
+ fs_key = fs_m_inv # Fréquence spatiale en mm^-1
39
+
40
+ # fs = n * dfx => n = fs / dfx with dfx = 1/(N*delta_x)
41
+ self.decimations.append(int(fs_key / (1/(len(profile)*self.params.general['dx']))))
17
42
 
18
43
  # PUBLIC METHODS
19
44
  def check(self):
@@ -273,7 +298,7 @@ class Tomography(Experiment):
273
298
  line = f"({coords}, {angles})\n"
274
299
  file.write(line)
275
300
 
276
- def generateActiveList(self, N):
301
+ def generateActiveList(self, N = None, decimations = None, angles = None):
277
302
  """
278
303
  Génère une liste de patterns d'activation équilibrés et réguliers.
279
304
  Args:
@@ -281,13 +306,153 @@ class Tomography(Experiment):
281
306
  Returns:
282
307
  list: Liste de strings au format "hex_angle".
283
308
  """
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.")
309
+ if decimations is not None and angles is not None:
310
+ self.patterns = self._genereate_patterns_from_decimations(decimations, angles)
311
+ elif N is not None and N > 1:
312
+ self.patterns = self._generate_patterns(N)
313
+ if not self._check_patterns(self.patterns):
314
+ raise ValueError("Generated patterns failed validation.")
315
+ else:
316
+ raise ValueError("Either N (>=2) or both decimations and angles must be provided for pattern generation.")
317
+
318
+
319
+ def saveAOsignals_matlab(self, filePath):
320
+ ActiveList = []
321
+ DelayLaw = []
322
+ c = self.params.acoustic['c0']
323
+ NbElemts = self.params.acoustic['num_elements']
324
+ pitch = self.params.acoustic['width']
325
+
326
+ for i in range(len(self.AcousticFields)):
327
+ profile = hex_to_binary_profile(self.AcousticFields[i].getName_field()[6:-4], NbElemts)
328
+ ActiveList.append(profile)
329
+ angle = self.AcousticFields[i].angle
330
+ Delay = 1000 * (1/c) * np.sin(np.deg2rad(angle)) * np.arange(1, NbElemts + 1) * pitch
331
+ DelayLaw.append(Delay - np.min(Delay))
332
+
333
+ savemat(filePath, {'data': self.AOsignal_withTumor, 'thetas': self.theta, 'decimations': self.decimations, 'ActiveList' : ActiveList, 'DelayLaw': DelayLaw})
334
+
335
+ def selectAngles(self, angles):
336
+
337
+ if self.AOsignal_withTumor is None and self.AOsignal_withoutTumor is None:
338
+ raise ValueError("AO signals are not initialized. Please load or generate the AO signals first.")
339
+ if self.AcousticFields is None or len(self.AcousticFields) == 0:
340
+ raise ValueError("AcousticFields is not initialized. Please generate the system matrix first.")
341
+ newAcousticFields = []
342
+ index = []
343
+ for i,field in enumerate(self.AcousticFields):
344
+ if field.angle in angles:
345
+ newAcousticFields.append(field)
346
+ index.append(i)
347
+ if self.AOsignal_withTumor is not None:
348
+ self.AOsignal_withTumor = self.AOsignal_withTumor[:, index]
349
+ if self.AOsignal_withoutTumor is not None:
350
+ self.AOsignal_withoutTumor = self.AOsignal_withoutTumor[:, index]
351
+ self.AcousticFields = newAcousticFields
352
+
353
+ def selectPatterns(self, pattern_names):
354
+ if self.AOsignal_withTumor is None and self.AOsignal_withoutTumor is None:
355
+ raise ValueError("AO signals are not initialized. Please load or generate the AO signals first.")
356
+ if self.AcousticFields is None or len(self.AcousticFields) == 0:
357
+ raise ValueError("AcousticFields is not initialized. Please generate the system matrix first.")
358
+ newAcousticFields = []
359
+ index = []
360
+ for i,field in enumerate(self.AcousticFields):
361
+ if field.pattern.activeList in pattern_names:
362
+ newAcousticFields.append(field)
363
+ index.append(i)
364
+ if self.AOsignal_withTumor is not None:
365
+ self.AOsignal_withTumor = self.AOsignal_withTumor[:, index]
366
+ if self.AOsignal_withoutTumor is not None:
367
+ self.AOsignal_withoutTumor = self.AOsignal_withoutTumor[:, index]
368
+ self.AcousticFields = newAcousticFields
369
+
370
+ def selectRandom(self,N):
371
+ if self.AOsignal_withTumor is None and self.AOsignal_withoutTumor is None:
372
+ raise ValueError("AO signals are not initialized. Please load or generate the AO signals first.")
373
+ if self.AcousticFields is None or len(self.AcousticFields) == 0:
374
+ raise ValueError("AcousticFields is not initialized. Please generate the system matrix first.")
375
+ if N > len(self.AcousticFields):
376
+ raise ValueError("N is larger than the number of available AcousticFields.")
377
+ indices = np.random.choice(len(self.AcousticFields), size=N, replace=False)
378
+ newAcousticFields = [self.AcousticFields[i] for i in indices]
379
+ if self.AOsignal_withTumor is not None:
380
+ self.AOsignal_withTumor = self.AOsignal_withTumor[:, indices]
381
+ if self.AOsignal_withoutTumor is not None:
382
+ self.AOsignal_withoutTumor = self.AOsignal_withoutTumor[:, indices]
383
+ self.AcousticFields = newAcousticFields
384
+
385
+ def _genereate_patterns_from_decimations(self, decimations, angles):
386
+ if isinstance(decimations, list): decimations = np.array(decimations)
387
+ if isinstance(angles, list): angles = np.array(angles)
388
+
389
+ angles = np.sort(angles)
390
+ decimations = np.sort(decimations)
391
+
392
+ num_elements = self.params.acoustic['num_elements']
393
+ dx_mm = self.params.general['dx'] * 1e3
394
+
395
+ # --- Calcul du nombre de Scans ---
396
+ if 0 in decimations:
397
+ Nscans = 4 * angles.shape[0] * (decimations.shape[0] - 1) + angles.shape[0]
398
+ offSet = angles.shape[0]
399
+ else:
400
+ Nscans = 4 * angles.shape[0] * decimations.shape[0]
401
+ offSet = 0
402
+
403
+ ActiveLIST = np.ones((num_elements, Nscans))
404
+ Xm = np.arange(1, num_elements + 1) * dx_mm
405
+ dFx = 1 / (num_elements * dx_mm)
406
+
407
+ # On traite séparément les décimations non nulles pour la boucle
408
+ active_decimations = decimations[decimations != 0]
409
+
410
+ for i_dec in range(len(active_decimations)):
411
+ idx_base = (np.arange(len(angles))) + (i_dec * 4 * len(angles)) + offSet
412
+
413
+ Icos = idx_base
414
+ Incos = idx_base + len(angles)
415
+ Isin = idx_base + 2 * len(angles)
416
+ Insin = idx_base + 3 * len(angles)
417
+
418
+ fx = dFx * active_decimations[i_dec]
419
+
420
+ # Remplissage des 4 phases
421
+ valid_icos = Icos[Icos < Nscans]
422
+ if valid_icos.size > 0:
423
+ ActiveLIST[:, valid_icos] = calc_mat_os(Xm, fx, dx_mm, ActiveLIST[:, valid_icos], 'cos')
424
+ if (Incos < Nscans).any():
425
+ ActiveLIST[:, Incos[Incos < Nscans]] = 1 - ActiveLIST[:, valid_icos]
426
+
427
+ valid_isin = Isin[Isin < Nscans]
428
+ if valid_isin.size > 0:
429
+ ActiveLIST[:, valid_isin] = calc_mat_os(Xm, fx, dx_mm, ActiveLIST[:, valid_isin], 'sin')
430
+ if (Insin < Nscans).any():
431
+ ActiveLIST[:, Insin[Insin < Nscans]] = 1 - ActiveLIST[:, valid_isin]
432
+
433
+ # --- Conversion au format attendu ---
434
+ # 1. On convertit toute la matrice en liste de strings Hexa
435
+ hexa_list = convert_to_hex_list(ActiveLIST)
436
+
437
+ # 2. Fonction interne de formatage d'angle (pour coller à votre ancien code)
438
+ def format_angle(a):
439
+ return f"{'1' if a < 0 else '0'}{abs(a):02d}"
440
+
441
+ # 3. Construction de la liste de dictionnaires
442
+ patterns = []
443
+ print(f"Generating {Nscans} patterns from decimations and angles...")
444
+ for i in range(Nscans):
445
+ # On retrouve l'angle correspondant à l'index i
446
+ # La logique est cyclique sur la taille de 'angles'
447
+ angle_val = angles[i % len(angles)]
448
+
449
+ hex_pattern = hexa_list[i]
450
+ pair = f"{hex_pattern}_{format_angle(angle_val)}"
451
+ patterns.append({"fileName": pair})
289
452
 
290
- def _generate_patterns(self, N):
453
+ return patterns
454
+
455
+ def _generate_patterns(self, N,angles = None):
291
456
  def format_angle(a):
292
457
  return f"{'1' if a < 0 else '0'}{abs(a):02d}"
293
458
 
@@ -298,7 +463,13 @@ class Tomography(Experiment):
298
463
  return hex_string
299
464
 
300
465
  num_elements = self.params.acoustic['num_elements']
301
- angle_choices = list(range(-20, 21))
466
+ if angles is None:
467
+ angle_choices = list(range(-20, 21))
468
+ else:
469
+ # convert np.array to list if necessary
470
+ if isinstance(angles, np.ndarray):
471
+ angles = angles.tolist()
472
+ angle_choices = angles
302
473
 
303
474
  # 1. Trouver TOUS les diviseurs PAIRS de num_elements (y compris num_elements)
304
475
  divs = [d for d in range(2, num_elements + 1) if num_elements % d == 0 and d % 2 == 0]
@@ -390,6 +561,80 @@ class Tomography(Experiment):
390
561
 
391
562
  return True
392
563
 
564
+ def applyApodisation(self, alpha=0.3, divergence_deg=0.5):
565
+ """
566
+ Applique une apodisation dynamique sur les champs acoustiques stockés dans l'objet.
567
+ L'apodisation suit l'angle d'émission et la divergence naturelle du faisceau pour
568
+ supprimer les lobes de diffraction (artefacts de bord) sans toucher au signal utile.
569
+ Args:
570
+ probe_width (float): Largeur physique active de la sonde (ex: 40e-3 pour 40mm).
571
+ alpha (float): Paramètre de Tukey (0.0=rectangle, 1.0=hann). 0.3 est un bon compromis.
572
+ divergence_deg (float): Angle d'ouverture du masque pour suivre l'élargissement du faisceau.
573
+ 0.0 = Droit, 0.5 = Légère ouverture (conseillé).
574
+ """
575
+ print(f"Applying apodization (Alpha={alpha}, Div={divergence_deg}°) on {len(self.AcousticFields)} fields...")
576
+
577
+ probe_width = self.params.acoustic['num_elements'] * self.params.acoustic['element_width']
578
+
579
+ for i in trange(len(self.AcousticFields), desc="Apodisation"):
580
+ # 1. Récupération des données et de l'angle
581
+ field = self.AcousticFields[i].field # Peut être (Z, X) ou (Time, Z, X)
582
+ angle = self.AcousticFields[i].angle # L'angle de l'onde plane
583
+
584
+ # 2. Récupération ou construction des axes physiques
585
+ nz, nx = field.shape[-2:]
586
+
587
+ if hasattr(self, 'x_axis') and self.x_axis is not None:
588
+ x_axis = self.x_axis
589
+ else:
590
+ # Génération par défaut centrée sur 0 (ex: -20mm à +20mm)
591
+ x_axis = np.linspace(-probe_width/2, probe_width/2, nx)
592
+
593
+ if hasattr(self, 'z_axis') and self.z_axis is not None:
594
+ z_axis = self.z_axis
595
+ else:
596
+ # Génération par défaut (ex: 0 à 40mm, basé sur un pitch standard ou arbitraire)
597
+ estimated_depth = 40e-3 # Valeur arbitraire si inconnue
598
+ z_axis = np.linspace(0, estimated_depth, nz)
599
+
600
+ # 3. Préparation des grilles pour le masque
601
+ Z, X = np.meshgrid(z_axis, x_axis, indexing='ij')
602
+
603
+ # 4. Calcul de la géométrie orientée (Steering)
604
+ angle_rad = np.deg2rad(angle)
605
+ X_aligned = X - Z * np.tan(angle_rad)
606
+
607
+ # 5. Calcul de la largeur dynamique du masque (Divergence)
608
+ div_rad = np.deg2rad(divergence_deg)
609
+ current_half_width = (probe_width / 2.0) + Z * np.tan(div_rad)
610
+
611
+ # 6. Normalisation et création du masque Tukey
612
+ X_norm = np.divide(X_aligned, current_half_width, out=np.zeros_like(X_aligned), where=current_half_width!=0)
613
+
614
+ mask = np.zeros_like(X_norm)
615
+ plateau_threshold = 1.0 * (1 - alpha)
616
+
617
+ # Zone centrale (plateau = 1)
618
+ mask[np.abs(X_norm) <= plateau_threshold] = 1.0
619
+
620
+ # Zone de transition (cosinus)
621
+ transition_indices = (np.abs(X_norm) > plateau_threshold) & (np.abs(X_norm) <= 1.0)
622
+ if np.any(transition_indices):
623
+ x_trans = np.abs(X_norm[transition_indices]) - plateau_threshold
624
+ width_trans = 1.0 * alpha
625
+ mask[transition_indices] = 0.5 * (1 + np.cos(np.pi * x_trans / width_trans))
626
+
627
+ # 7. Application du masque (Gestion 2D vs 3D)
628
+ if field.ndim == 3:
629
+ field_apodized = field * mask[np.newaxis, :, :]
630
+ else:
631
+ field_apodized = field * mask
632
+
633
+ # 8. Mise à jour de l'objet
634
+ self.AcousticFields[i].field = field_apodized
635
+
636
+ print("Apodisation done.")
637
+
393
638
  # PRIVATE METHODS
394
639
  def _generateAcousticFields_STRUCT_CPU(self, fieldDataPath=None, show_log=False, nameBlock=None):
395
640
  if self.patterns is None:
@@ -547,4 +547,5 @@ def MLEM_sparseSELL_pycuda(
547
547
  try:
548
548
  SMatrix.ctx.pop()
549
549
  except Exception:
550
- pass
550
+ pass
551
+
@@ -1,8 +1,13 @@
1
1
  from ._mainRecon import Recon
2
2
  from .ReconEnums import ReconType, AnalyticType, ProcessType
3
+ from AOT_biomaps.AOT_Experiment.Tomography import hex_to_binary_profile
4
+ from .ReconTools import get_phase_deterministic
3
5
 
4
6
  import numpy as np
5
7
  from tqdm import trange
8
+ import torch
9
+ import tqdm
10
+
6
11
 
7
12
  class AnalyticRecon(Recon):
8
13
  def __init__(self, analyticType, **kwargs):
@@ -10,6 +15,94 @@ class AnalyticRecon(Recon):
10
15
  self.reconType = ReconType.Analytic
11
16
  self.analyticType = analyticType
12
17
 
18
+
19
+
20
+ def parse_and_demodulate(self, withTumor=True):
21
+
22
+ if withTumor:
23
+ AOsignal = self.experiment.AOsignal_withTumor
24
+ else:
25
+ AOsignal = self.experiment.AOsignal_withoutTumor
26
+ delta_x = self.params.acoustic['dx'] # en m
27
+ n_piezos = self.params.acoustic['num_elements']
28
+ demodulated_data = {}
29
+ structured_buffer = {}
30
+
31
+ for i in tqdm(range(len(self.experiment.AcousticFields)), desc="Demodulating AO signals"):
32
+ label = self.experiment.AcousticFields[i].getName_field()
33
+
34
+ parts = label.split("_")
35
+ hex_pattern = parts[0]
36
+ angle_code = parts[-1]
37
+
38
+ # Angle
39
+ if angle_code.startswith("1"):
40
+ angle_deg = -int(angle_code[1:])
41
+ else:
42
+ angle_deg = int(angle_code)
43
+ angle_rad = np.deg2rad(angle_deg)
44
+
45
+ # Onde Plane (f_s = 0)
46
+ if set(hex_pattern.lower().replace(" ", "")) == {'f'}:
47
+ fs_key = 0.0 # fs_key est en mm^-1 (0.0 mm^-1)
48
+ demodulated_data[(fs_key, angle_rad)] = np.array(AOsignal[i])
49
+ continue
50
+
51
+ # Onde Structurée
52
+ profile = hex_to_binary_profile(hex_pattern, n_piezos)
53
+
54
+ # Calcul FS (Fréquence de Structuration)
55
+ ft_prof = np.fft.fft(profile)
56
+ # On regarde uniquement la partie positive non DC
57
+ idx_max = np.argmax(np.abs(ft_prof[1:len(profile)//2])) + 1
58
+ freqs = np.fft.fftfreq(len(profile), d=delta_x)
59
+
60
+ # freqs est en m^-1 car delta_x est en mètres.
61
+ fs_m_inv = abs(freqs[idx_max])
62
+
63
+ # *** CORRECTION 1: Conversion de f_s en mm^-1 (mm^-1 est utilisé dans iRadon) ***
64
+ fs_key = fs_m_inv / 1000.0 # Fréquence spatiale en mm^-1
65
+
66
+
67
+ if fs_key == 0: continue
68
+
69
+ # Calcul de la Phase (Shift)
70
+ phase = get_phase_deterministic(profile)
71
+
72
+ # Stockage par (fs, theta) et phase
73
+ key = (fs_key, angle_rad)
74
+ if key not in structured_buffer:
75
+ structured_buffer[key] = {}
76
+
77
+ # La moyenne est nécessaire si plusieurs acquisitions ont la même phase (pour le SNR)
78
+ if phase in structured_buffer[key]:
79
+ structured_buffer[key][phase] = (structured_buffer[key][phase] + np.array(AOsignal[i])) / 2
80
+ else:
81
+ structured_buffer[key][phase] = np.array(AOsignal[i])
82
+
83
+
84
+
85
+ for (fs, theta), phases in structured_buffer.items():
86
+ s0 = phases.get(0.0, 0)
87
+ s_pi_2 = phases.get(np.pi/2, 0)
88
+ s_pi = phases.get(np.pi, 0)
89
+ s_3pi_2 = phases.get(3*np.pi/2, 0)
90
+
91
+ # Assurer que les zéros sont des vecteurs de la bonne taille
92
+ example = next(val for val in phases.values() if not isinstance(val, int))
93
+ if isinstance(s0, int): s0 = np.zeros_like(example)
94
+ if isinstance(s_pi, int): s_pi = np.zeros_like(example)
95
+ if isinstance(s_pi_2, int): s_pi_2 = np.zeros_like(example)
96
+ if isinstance(s_3pi_2, int): s_3pi_2 = np.zeros_like(example)
97
+
98
+ real = s0 - s_pi
99
+ imag = s_pi_2 - s_3pi_2
100
+
101
+
102
+ demodulated_data[(fs, theta)] = (real - 1j * imag) / (2/np.pi)
103
+
104
+ return demodulated_data
105
+
13
106
  def run(self, processType = ProcessType.PYTHON, withTumor= True):
14
107
  """
15
108
  This method is a placeholder for the analytic reconstruction process.
@@ -6,6 +6,7 @@ import pycuda.driver as drv
6
6
  from numba import njit, prange
7
7
  from torch_sparse import coalesce
8
8
  from scipy.signal.windows import hann
9
+ from itertools import groupby
9
10
 
10
11
  def load_recon(hdr_path):
11
12
  """
@@ -221,7 +222,6 @@ def calculate_memory_requirement(SMatrix, y):
221
222
  # --- 3. Final Result ---
222
223
  return total_bytes / (1024 ** 3)
223
224
 
224
-
225
225
  def check_gpu_memory(device_index, required_memory, show_logs=True):
226
226
  """Check if enough memory is available on the specified GPU."""
227
227
  free_memory, _ = torch.cuda.mem_get_info(f"cuda:{device_index}")
@@ -252,7 +252,6 @@ def _backward_projection(SMatrix, e_p, c_p):
252
252
  total += SMatrix[_t, _z, _x, _n] * e_p[_t, _n]
253
253
  c_p[_z, _x] = total
254
254
 
255
-
256
255
  def _build_adjacency_sparse(Z, X, device, corner=(0.5 - np.sqrt(2) / 4) / np.sqrt(2), face=0.5 - np.sqrt(2) / 4,dtype=torch.float32):
257
256
  rows, cols, weights = [], [], []
258
257
  for z in range(Z):
@@ -273,7 +272,6 @@ def _build_adjacency_sparse(Z, X, device, corner=(0.5 - np.sqrt(2) / 4) / np.sqr
273
272
  index, values = coalesce(index, values, m=Z*X, n=Z*X)
274
273
  return index, values
275
274
 
276
-
277
275
  def power_method(P, PT, data, Z, X, n_it=10):
278
276
  x = torch.randn(Z * X, device=data.device)
279
277
  x = x / torch.norm(x)
@@ -486,4 +484,48 @@ def power_method_estimate_L__SELL(SMatrix, stream, n_it=20, block_size=256):
486
484
  g.free()
487
485
  except:
488
486
  pass
489
- return max(L_sq, 1e-6)
487
+ return max(L_sq, 1e-6)
488
+
489
+ def get_phase_deterministic(profile):
490
+ """
491
+ Détermine la phase en se basant sur la valeur initiale (0 ou 1) et l'état
492
+ de décalage (is_shifted) de la séquence binaire.
493
+
494
+ ATTENTION: Cette fonction est conservée mais la logique est souvent simplifiée
495
+ en pratique si les labels garantissent les phases 0, pi/2, pi, 3pi/2.
496
+ """
497
+ runs = [(k, sum(1 for _ in g)) for k, g in groupby(profile)]
498
+ if not runs: return 0.0
499
+
500
+ nominal_half_period = max([r[1] for r in runs])
501
+ if nominal_half_period == 0: return 0.0
502
+
503
+ first_val = runs[0][0] # 0 ou 1
504
+ first_len = runs[0][1]
505
+ # Détection de cycle 50%
506
+ is_shifted = (0.3 < first_len / nominal_half_period < 0.7)
507
+
508
+ # --- LOGIQUE DE MAPPAGE DE PHASE SIMPLIFIÉE (idx 1 à 4) ---
509
+
510
+ if first_val == 0:
511
+ if is_shifted:
512
+ idx = 3 # C1/C3 décalé (phi_1 ou phi_3)
513
+ else:
514
+ idx = 4 # C2/C4 non décalé
515
+ else: # first_val == 1
516
+ if is_shifted:
517
+ idx = 1 # C1/C3 décalé (phi_1 ou phi_3)
518
+ else:
519
+ idx = 2 # C2/C4 non décalé
520
+
521
+ # On utilise les phases de quadrature 0, pi/2, pi, 3pi/2
522
+ if idx == 1:
523
+ phase = 0
524
+ elif idx == 2 :
525
+ phase = np.pi/2
526
+ elif idx == 3 :
527
+ phase = np.pi
528
+ elif idx == 4 :
529
+ phase = 3*np.pi/2
530
+
531
+ return phase
@@ -85,7 +85,7 @@ from .AOT_Recon.AOT_PotentialFunctions.RelativeDifferences import *
85
85
  from .Config import config
86
86
  from .Settings import *
87
87
 
88
- __version__ = '2.9.312'
88
+ __version__ = '2.9.332'
89
89
  __process__ = config.get_process()
90
90
 
91
91
  def initialize(process=None):
@@ -156,6 +156,26 @@ def initialize(process=None):
156
156
 
157
157
 
158
158
 
159
+
160
+
161
+
162
+
163
+
164
+
165
+
166
+
167
+
168
+
169
+
170
+
171
+
172
+
173
+
174
+
175
+
176
+
177
+
178
+
159
179
 
160
180
 
161
181
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AOT_biomaps
3
- Version: 2.9.312
3
+ Version: 2.9.332
4
4
  Summary: Acousto-Optic Tomography
5
5
  Home-page: https://github.com/LucasDuclos/AcoustoOpticTomography
6
6
  Author: Lucas Duclos
@@ -16,6 +16,7 @@ AOT_biomaps/AOT_Acoustic/PlaneWave.py
16
16
  AOT_biomaps/AOT_Acoustic/StructuredWave.py
17
17
  AOT_biomaps/AOT_Acoustic/__init__.py
18
18
  AOT_biomaps/AOT_Acoustic/_mainAcoustic.py
19
+ AOT_biomaps/AOT_Experiment/ExperimentTools.py
19
20
  AOT_biomaps/AOT_Experiment/Focus.py
20
21
  AOT_biomaps/AOT_Experiment/Tomography.py
21
22
  AOT_biomaps/AOT_Experiment/__init__.py
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AOT_biomaps
3
- Version: 2.9.312
3
+ Version: 2.9.332
4
4
  Summary: Acousto-Optic Tomography
5
5
  Home-page: https://github.com/LucasDuclos/AcoustoOpticTomography
6
6
  Author: Lucas Duclos
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='AOT_biomaps',
5
- version='2.9.312',
5
+ version='2.9.332',
6
6
  packages=find_packages(),
7
7
  include_package_data=True,
8
8
 
@@ -314,6 +314,26 @@ setup(
314
314
 
315
315
 
316
316
 
317
+
318
+
319
+
320
+
321
+
322
+
323
+
324
+
325
+
326
+
327
+
328
+
329
+
330
+
331
+
332
+
333
+
334
+
335
+
336
+
317
337
 
318
338
 
319
339
 
File without changes
File without changes