AOT-biomaps 2.9.188__tar.gz → 2.9.354__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.
Files changed (61) hide show
  1. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/AcousticTools.py +0 -2
  2. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/StructuredWave.py +2 -2
  3. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/_mainAcoustic.py +11 -6
  4. aot_biomaps-2.9.354/AOT_biomaps/AOT_Experiment/ExperimentTools.py +69 -0
  5. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Experiment/Tomography.py +318 -12
  6. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Experiment/_mainExperiment.py +95 -55
  7. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_Optimizers/DEPIERRO.py +48 -13
  8. aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_Optimizers/LS.py +496 -0
  9. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_Optimizers/MAPEM.py +118 -38
  10. aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_Optimizers/MLEM.py +551 -0
  11. aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_Optimizers/PDHG.py +652 -0
  12. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/RelativeDifferences.py +10 -14
  13. aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/SparseSMatrix_CSR.py +274 -0
  14. aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/SparseSMatrix_SELL.py +360 -0
  15. aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/__init__.py +2 -0
  16. aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_biomaps_kernels.cubin +0 -0
  17. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AlgebraicRecon.py +243 -113
  18. aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AnalyticRecon.py +419 -0
  19. aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/BayesianRecon.py +230 -0
  20. aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/PrimalDualRecon.py +263 -0
  21. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/ReconEnums.py +27 -2
  22. aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/ReconTools.py +692 -0
  23. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/__init__.py +1 -0
  24. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/_mainRecon.py +66 -55
  25. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/__init__.py +4 -11
  26. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps.egg-info/PKG-INFO +2 -1
  27. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps.egg-info/SOURCES.txt +6 -1
  28. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps.egg-info/requires.txt +1 -0
  29. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/PKG-INFO +2 -1
  30. aot_biomaps-2.9.354/setup.py +378 -0
  31. aot_biomaps-2.9.188/AOT_biomaps/AOT_Recon/AOT_Optimizers/LS.py +0 -103
  32. aot_biomaps-2.9.188/AOT_biomaps/AOT_Recon/AOT_Optimizers/MLEM.py +0 -262
  33. aot_biomaps-2.9.188/AOT_biomaps/AOT_Recon/AOT_Optimizers/PDHG.py +0 -221
  34. aot_biomaps-2.9.188/AOT_biomaps/AOT_Recon/AnalyticRecon.py +0 -154
  35. aot_biomaps-2.9.188/AOT_biomaps/AOT_Recon/BayesianRecon.py +0 -295
  36. aot_biomaps-2.9.188/AOT_biomaps/AOT_Recon/PrimalDualRecon.py +0 -200
  37. aot_biomaps-2.9.188/AOT_biomaps/AOT_Recon/ReconTools.py +0 -272
  38. aot_biomaps-2.9.188/setup.py +0 -202
  39. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/AcousticEnums.py +0 -0
  40. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/FocusedWave.py +0 -0
  41. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/IrregularWave.py +0 -0
  42. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/PlaneWave.py +0 -0
  43. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/__init__.py +0 -0
  44. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Experiment/Focus.py +0 -0
  45. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Experiment/__init__.py +0 -0
  46. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Optic/Absorber.py +0 -0
  47. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Optic/Laser.py +0 -0
  48. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Optic/OpticEnums.py +0 -0
  49. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Optic/__init__.py +0 -0
  50. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Optic/_mainOptic.py +0 -0
  51. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_Optimizers/__init__.py +0 -0
  52. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Huber.py +0 -0
  53. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Quadratic.py +0 -0
  54. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/__init__.py +0 -0
  55. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/DeepLearningRecon.py +0 -0
  56. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/Config.py +0 -0
  57. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/Settings.py +0 -0
  58. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps.egg-info/dependency_links.txt +0 -0
  59. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps.egg-info/top_level.txt +0 -0
  60. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/README.md +0 -0
  61. {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/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.
@@ -292,8 +292,8 @@ class StructuredWave(AcousticField):
292
292
  f"!number format := short float\n"
293
293
  f"!number of bytes per pixel := 4\n"
294
294
  f"scaling factor (mm/pixel) [1] := {self.params['dx'] * 1000}\n"
295
- f"scaling factor (mm/pixel) [2] := {self.params['dx'] * 1000}\n"
296
- f"scaling factor (s/pixel) [3] := {1 / self.params['f_AQ']}\n"
295
+ f"scaling factor (mm/pixel) [2] := {self.params['dz'] * 1000}\n"
296
+ f"scaling factor (s/pixel) [3] := {1 / self.params['f_saving']}\n"
297
297
  f"first pixel offset (mm) [1] := {self.params['Xrange'][0] * 1e3}\n"
298
298
  f"first pixel offset (mm) [2] := {self.params['Zrange'][0] * 1e3}\n"
299
299
  f"first pixel offset (s) [3] := 0\n"
@@ -19,10 +19,12 @@ from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D
19
19
  from kwave.kspaceFirstOrder2D import kspaceFirstOrder2D
20
20
  from kwave.options.simulation_options import SimulationOptions
21
21
  from kwave.options.simulation_execution_options import SimulationExecutionOptions
22
+ from AOT_biomaps.Settings import Params
22
23
 
23
24
  from tempfile import gettempdir
24
25
  from math import ceil
25
26
  from abc import ABC, abstractmethod
27
+ import logging
26
28
 
27
29
 
28
30
 
@@ -75,7 +77,7 @@ class AcousticField(ABC):
75
77
  print(f"Initialization error: {e}")
76
78
  raise
77
79
  if params != None:
78
- if type(params) != AOT_biomaps.Settings.Params:
80
+ if type(params) != Params:
79
81
  raise TypeError("params must be an instance of the Params class")
80
82
 
81
83
  self.params = {
@@ -225,6 +227,7 @@ class AcousticField(ABC):
225
227
  Generate the acoustic field based on the specified simulation type and parameters.
226
228
  """
227
229
  try:
230
+ logging.getLogger('root').setLevel(logging.ERROR)
228
231
  if self.params['typeSim'] == TypeSim.FIELD2.value:
229
232
  raise NotImplementedError("FIELD2 simulation is not implemented yet.")
230
233
  elif self.params['typeSim'] == TypeSim.KWAVE.value:
@@ -265,7 +268,7 @@ class AcousticField(ABC):
265
268
  print(f"Error in save_field method: {e}")
266
269
  raise
267
270
 
268
- def load_field(self, folderPath, formatSave=FormatSave.HDR_IMG):
271
+ def load_field(self, folderPath, formatSave=FormatSave.HDR_IMG, nameBlock=None):
269
272
  """
270
273
  Load the acoustic field from a file in the specified format.
271
274
 
@@ -286,7 +289,7 @@ class AcousticField(ABC):
286
289
  raise NotImplementedError("3D KWAVE field loading is not implemented yet.")
287
290
  elif formatSave.value == FormatSave.H5.value:
288
291
  if self.params["dim"] == Dim.D2.value:
289
- self._load_field_h5(folderPath)
292
+ self._load_field_h5(folderPath,nameBlock)
290
293
  elif self.params["dim"] == Dim.D3.value:
291
294
  raise NotImplementedError("H5 KWAVE field loading is not implemented yet.")
292
295
  elif formatSave.value == FormatSave.NPY.value:
@@ -652,7 +655,7 @@ class AcousticField(ABC):
652
655
  """
653
656
  pass
654
657
 
655
- def _load_field_h5(self, filePath):
658
+ def _load_field_h5(self, filePath,nameBlock):
656
659
  """
657
660
  Load the 2D acoustic field from an H5 file.
658
661
 
@@ -663,8 +666,10 @@ class AcousticField(ABC):
663
666
  - field (numpy.ndarray): The loaded acoustic field.
664
667
  """
665
668
  try:
666
- with h5py.File(filePath+self.getName_field()+".h5", 'r') as f:
667
- self.field = f['data'][:]
669
+ if nameBlock is None:
670
+ nameBlock = 'data'
671
+ with h5py.File(os.path.join(filePath, self.getName_field()+".h5"), 'r') as f:
672
+ self.field = f[nameBlock][:]
668
673
  except Exception as e:
669
674
  print(f"Error in _load_field_h5 method: {e}")
670
675
  raise
@@ -0,0 +1,69 @@
1
+ import numpy as np
2
+
3
+ def calc_mat_os(xm, fx, bool_active_list, signal_type):
4
+ """
5
+ xm : vecteur des positions réelles des éléments (en m)
6
+ fx : fréquence spatiale (en m^-1)
7
+ signal_type : 'cos' ou 'sin'
8
+ """
9
+ num_els = len(xm)
10
+ num_cols = bool_active_list.shape[1]
11
+
12
+ if signal_type == 'cos':
13
+ mask = (np.cos(2 * np.pi * fx * xm) > 0).astype(float)
14
+ elif signal_type == 'sin':
15
+ mask = (np.sin(2 * np.pi * fx * xm) > 0).astype(float)
16
+ else:
17
+ mask = np.ones(num_els) # Sécurité
18
+
19
+ return np.tile(mask[:, np.newaxis], (1, num_cols))
20
+
21
+ def convert_to_hex_list(matrix):
22
+ """
23
+ Convertit une matrice binaire en liste de strings hexa (paquets de 4 bits).
24
+ Chaque colonne devient une chaîne de caractères.
25
+ """
26
+ n_els, n_scans = matrix.shape
27
+
28
+ # 1. Padding pour s'assurer que n_els est multiple de 4
29
+ remainder = n_els % 4
30
+ if remainder != 0:
31
+ padding = np.zeros((4 - remainder, n_scans))
32
+ matrix = np.vstack([matrix, padding])
33
+
34
+ # 2. Reshape pour isoler des blocs de 4 bits (nibbles)
35
+ # Shape résultante : (Nombre de blocs, 4 bits, Nombre de scans)
36
+ blocks = matrix.reshape(-1, 4, n_scans)
37
+
38
+ # 3. Calcul de la valeur décimale de chaque bloc (0 à 15)
39
+ # On considère le premier élément comme le bit de poids faible (LSB)
40
+ weights = np.array([1, 2, 4, 8]).reshape(1, 4, 1)
41
+ dec_values = np.sum(blocks * weights, axis=1).astype(int)
42
+
43
+ # 4. Conversion en caractères Hexadécimaux
44
+ # On définit la table de conversion pour la rapidité
45
+ hex_table = np.array(list("0123456789abcdef"))
46
+ hex_matrix = hex_table[dec_values]
47
+
48
+ # 5. Assemblage des chaînes (de l'élément N vers 0 pour l'ordre Shift Register standard)
49
+ return ["".join(hex_matrix[::-1, col]) for col in range(n_scans)]
50
+
51
+ def hex_to_binary_profile(hex_string, n_piezos=192):
52
+ hex_string = hex_string.strip().replace(" ", "").replace("\n", "")
53
+ if set(hex_string.lower()) == {'f'}:
54
+ return np.ones(n_piezos, dtype=int)
55
+
56
+ try:
57
+ n_char = len(hex_string)
58
+ n_bits = n_char * 4
59
+ binary_str = bin(int(hex_string, 16))[2:].zfill(n_bits)
60
+ if len(binary_str) < n_piezos:
61
+ # Tronquer/padder en fonction de la taille réelle de la sonde
62
+ binary_str = binary_str.ljust(n_piezos, '0')
63
+ elif len(binary_str) > n_piezos:
64
+ binary_str = binary_str[:n_piezos]
65
+ return np.array([int(b) for b in binary_str])
66
+ except ValueError:
67
+ return np.zeros(n_piezos, dtype=int)
68
+
69
+
@@ -2,16 +2,24 @@ 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
11
+ import h5py
12
+ from scipy.io import loadmat, savemat
13
+
10
14
 
11
15
  class Tomography(Experiment):
12
16
  def __init__(self, **kwargs):
13
17
  super().__init__(**kwargs)
14
18
  self.patterns = None
19
+ self.theta = []
20
+ self.decimations = []
21
+ self.ActiveList = []
22
+ self.DelayLaw = []
15
23
 
16
24
  # PUBLIC METHODS
17
25
  def check(self):
@@ -47,7 +55,7 @@ class Tomography(Experiment):
47
55
  return False, f"OpticImage phantom shape {self.OpticImage.phantom.shape} does not match AcousticFields shape {self.AcousticFields[0].field.shape[1:]}."
48
56
  return True, "Experiment is correctly initialized."
49
57
 
50
- def generateAcousticFields(self, fieldDataPath=None, show_log=True):
58
+ def generateAcousticFields(self, fieldDataPath=None, show_log=True, nameBlock=None):
51
59
  """
52
60
  Generate the acoustic fields for simulation.
53
61
  Args:
@@ -57,7 +65,30 @@ class Tomography(Experiment):
57
65
  systemMatrix: A numpy array of the generated fields.
58
66
  """
59
67
  if self.TypeAcoustic.value == WaveType.StructuredWave.value:
60
- self.AcousticFields = self._generateAcousticFields_STRUCT_CPU(fieldDataPath, show_log)
68
+ self.AcousticFields = self._generateAcousticFields_STRUCT_CPU(fieldDataPath, show_log, nameBlock)
69
+ for i in range(len(self.AcousticFields)):
70
+ profile = hex_to_binary_profile(self.AcousticFields[i].getName_field()[6:-4], self.params.acoustic['num_elements'])
71
+ self.ActiveList.append(profile)
72
+ angle = self.AcousticFields[i].angle
73
+ self.theta.append(angle)
74
+ Delay = 1000 * (1/self.params.acoustic['c0']) * np.sin(np.deg2rad(angle)) * np.arange(1, self.params.acoustic['num_elements'] + 1) * self.params.acoustic['element_width']
75
+ self.DelayLaw.append(Delay - np.min(Delay))
76
+
77
+ if set(self.AcousticFields[i].getName_field()[6:-4].lower().replace(" ", "")) == {'f'}:
78
+ fs_key = 0.0 # fs_key est en mm^-1 (0.0 mm^-1)
79
+ else:
80
+ ft_prof = np.fft.fft(profile)
81
+ idx_max = np.argmax(np.abs(ft_prof[1:len(profile)//2])) + 1
82
+ freqs = np.fft.fftfreq(len(profile), d=self.params.general['dx'])
83
+
84
+ # freqs est en m^-1 car delta_x est en mètres.
85
+ fs_m_inv = abs(freqs[idx_max])
86
+
87
+ fs_key = fs_m_inv # Fréquence spatiale en mm^-1
88
+
89
+ # fs = n * dfx => n = fs / dfx with dfx = 1/(N*delta_x)
90
+ self.decimations.append(int(fs_key / (1/(len(profile)*self.params.general['dx']))))
91
+
61
92
  else:
62
93
  raise ValueError("Unsupported wave type.")
63
94
 
@@ -271,7 +302,7 @@ class Tomography(Experiment):
271
302
  line = f"({coords}, {angles})\n"
272
303
  file.write(line)
273
304
 
274
- def generateActiveList(self, N):
305
+ def generateActiveList(self, N = None, decimations = None, angles = None):
275
306
  """
276
307
  Génère une liste de patterns d'activation équilibrés et réguliers.
277
308
  Args:
@@ -279,13 +310,139 @@ class Tomography(Experiment):
279
310
  Returns:
280
311
  list: Liste de strings au format "hex_angle".
281
312
  """
282
- if N < 1:
283
- raise ValueError("N must be a positive integer.")
284
- self.patterns = self._generate_patterns(N)
285
- if not self._check_patterns(self.patterns):
286
- raise ValueError("Generated patterns failed validation.")
313
+ if decimations is not None and angles is not None:
314
+ self.patterns = self._genereate_patterns_from_decimations(decimations, angles)
315
+ elif N is not None and N > 1:
316
+ self.patterns = self._generate_patterns(N)
317
+ if not self._check_patterns(self.patterns):
318
+ raise ValueError("Generated patterns failed validation.")
319
+ else:
320
+ raise ValueError("Either N (>=2) or both decimations and angles must be provided for pattern generation.")
321
+
322
+ def saveAOsignals_matlab(self, filePath):
323
+
324
+ savemat(filePath, {'data': self.AOsignal_withTumor, 'thetas': self.theta, 'decimations': self.decimations, 'ActiveList' : self.ActiveList, 'DelayLaw': self.DelayLaw})
325
+
326
+ def selectAngles(self, angles):
327
+
328
+ if self.AOsignal_withTumor is None and self.AOsignal_withoutTumor is None:
329
+ raise ValueError("AO signals are not initialized. Please load or generate the AO signals first.")
330
+ if self.AcousticFields is None or len(self.AcousticFields) == 0:
331
+ raise ValueError("AcousticFields is not initialized. Please generate the system matrix first.")
332
+ newAcousticFields = []
333
+ index = []
334
+ for i,field in enumerate(self.AcousticFields):
335
+ if field.angle in angles:
336
+ newAcousticFields.append(field)
337
+ index.append(i)
338
+ if self.AOsignal_withTumor is not None:
339
+ self.AOsignal_withTumor = self.AOsignal_withTumor[:, index]
340
+ if self.AOsignal_withoutTumor is not None:
341
+ self.AOsignal_withoutTumor = self.AOsignal_withoutTumor[:, index]
342
+ self.AcousticFields = newAcousticFields
343
+
344
+ def selectPatterns(self, pattern_names):
345
+ if self.AOsignal_withTumor is None and self.AOsignal_withoutTumor is None:
346
+ raise ValueError("AO signals are not initialized. Please load or generate the AO signals first.")
347
+ if self.AcousticFields is None or len(self.AcousticFields) == 0:
348
+ raise ValueError("AcousticFields is not initialized. Please generate the system matrix first.")
349
+ newAcousticFields = []
350
+ index = []
351
+ for i,field in enumerate(self.AcousticFields):
352
+ if field.pattern.activeList in pattern_names:
353
+ newAcousticFields.append(field)
354
+ index.append(i)
355
+ if self.AOsignal_withTumor is not None:
356
+ self.AOsignal_withTumor = self.AOsignal_withTumor[:, index]
357
+ if self.AOsignal_withoutTumor is not None:
358
+ self.AOsignal_withoutTumor = self.AOsignal_withoutTumor[:, index]
359
+ self.AcousticFields = newAcousticFields
360
+
361
+ def selectRandom(self,N):
362
+ if self.AOsignal_withTumor is None and self.AOsignal_withoutTumor is None:
363
+ raise ValueError("AO signals are not initialized. Please load or generate the AO signals first.")
364
+ if self.AcousticFields is None or len(self.AcousticFields) == 0:
365
+ raise ValueError("AcousticFields is not initialized. Please generate the system matrix first.")
366
+ if N > len(self.AcousticFields):
367
+ raise ValueError("N is larger than the number of available AcousticFields.")
368
+ indices = np.random.choice(len(self.AcousticFields), size=N, replace=False)
369
+ newAcousticFields = [self.AcousticFields[i] for i in indices]
370
+ if self.AOsignal_withTumor is not None:
371
+ self.AOsignal_withTumor = self.AOsignal_withTumor[:, indices]
372
+ if self.AOsignal_withoutTumor is not None:
373
+ self.AOsignal_withoutTumor = self.AOsignal_withoutTumor[:, indices]
374
+ self.AcousticFields = newAcousticFields
375
+
376
+ def _genereate_patterns_from_decimations(self, decimations, angles):
377
+ if isinstance(decimations, list):
378
+ decimations = np.array(decimations)
379
+ if isinstance(angles, list):
380
+ angles = np.array(angles)
381
+
382
+ angles = np.sort(angles)
383
+ decimations = np.sort(decimations)
384
+
385
+ num_elements = self.params.acoustic['num_elements']
386
+ Width = self.params.acoustic['element_width'] # en m
387
+ kerf = self.params.acoustic.get('kerf', 0.00000) # en m
388
+ Nactuators = self.params.acoustic['num_elements']
389
+
390
+ # --- Calcul du nombre de Scans ---
391
+ if 0 in decimations:
392
+ Nscans = 4 * len(angles) * (len(decimations) - 1) + len(angles)
393
+ else:
394
+ Nscans = 4 * len(angles) * len(decimations)
395
+
396
+ ActiveLIST = np.ones((num_elements, Nscans))
397
+
398
+ # --- Calcul des positions centrées des éléments (en m) ---
399
+ Xc = (Width + (Nactuators - 1) * (kerf + Width)) / 2
400
+ Xm = np.array([Width * (i - 1) + Width / 2 - Xc for i in range(1, Nactuators + 1)])
401
+
402
+ # --- Gestion de l'onde plane (tous les piezo ON) au début ---
403
+ if 0 in decimations:
404
+ I_plane = np.arange(len(angles))
405
+ ActiveLIST[:, I_plane] = 1 # Tous les piezo ON pour les len(angles) premières colonnes
287
406
 
288
- def _generate_patterns(self, N):
407
+ # --- Traitement des décimations non nulles ---
408
+ active_decimations = decimations[decimations != 0]
409
+ dFx = 1 / (Nactuators * Width) # fx de base (en m^-1)
410
+
411
+ for i_dec in range(len(active_decimations)):
412
+ # Décalage des indices pour placer les motifs modulés après l'onde plane
413
+ I = np.arange(len(angles)) + len(angles) + (i_dec * 4 * len(angles))
414
+
415
+ Icos = I
416
+ Incos = I + 1* len(angles)
417
+ Isin = I + 3 * len(angles)
418
+ Insin = I + 2 * len(angles)
419
+
420
+ fx = dFx * active_decimations[i_dec]
421
+
422
+ # Appliquer les motifs modulés
423
+ ActiveLIST[:, Icos] = calc_mat_os(Xm, fx, ActiveLIST[:, Icos[:1]], 'cos')
424
+ ActiveLIST[:, Incos] = 1 - ActiveLIST[:, Icos]
425
+ ActiveLIST[:, Isin] = calc_mat_os(Xm, fx, ActiveLIST[:, Isin[:1]], 'sin')
426
+ ActiveLIST[:, Insin] = 1 - ActiveLIST[:, Isin]
427
+
428
+ # --- Conversion au format attendu ---
429
+ hexa_list = convert_to_hex_list(ActiveLIST)
430
+
431
+ def format_angle(a):
432
+ return f"{'1' if a < 0 else '0'}{abs(a):02d}"
433
+
434
+ patterns = []
435
+ print(f"Generating {Nscans} patterns from decimations and angles...")
436
+ for i in range(Nscans):
437
+ angle_val = angles[i % len(angles)]
438
+ hex_pattern = hexa_list[i]
439
+ pair = f"{hex_pattern}_{format_angle(angle_val)}"
440
+ patterns.append({"fileName": pair})
441
+
442
+ return patterns
443
+
444
+
445
+ def _generate_patterns(self, N,angles = None):
289
446
  def format_angle(a):
290
447
  return f"{'1' if a < 0 else '0'}{abs(a):02d}"
291
448
 
@@ -296,7 +453,13 @@ class Tomography(Experiment):
296
453
  return hex_string
297
454
 
298
455
  num_elements = self.params.acoustic['num_elements']
299
- angle_choices = list(range(-20, 21))
456
+ if angles is None:
457
+ angle_choices = list(range(-20, 21))
458
+ else:
459
+ # convert np.array to list if necessary
460
+ if isinstance(angles, np.ndarray):
461
+ angles = angles.tolist()
462
+ angle_choices = angles
300
463
 
301
464
  # 1. Trouver TOUS les diviseurs PAIRS de num_elements (y compris num_elements)
302
465
  divs = [d for d in range(2, num_elements + 1) if num_elements % d == 0 and d % 2 == 0]
@@ -388,8 +551,82 @@ class Tomography(Experiment):
388
551
 
389
552
  return True
390
553
 
554
+ def applyApodisation(self, alpha=0.3, divergence_deg=0.5):
555
+ """
556
+ Applique une apodisation dynamique sur les champs acoustiques stockés dans l'objet.
557
+ L'apodisation suit l'angle d'émission et la divergence naturelle du faisceau pour
558
+ supprimer les lobes de diffraction (artefacts de bord) sans toucher au signal utile.
559
+ Args:
560
+ probe_width (float): Largeur physique active de la sonde (ex: 40e-3 pour 40mm).
561
+ alpha (float): Paramètre de Tukey (0.0=rectangle, 1.0=hann). 0.3 est un bon compromis.
562
+ divergence_deg (float): Angle d'ouverture du masque pour suivre l'élargissement du faisceau.
563
+ 0.0 = Droit, 0.5 = Légère ouverture (conseillé).
564
+ """
565
+ print(f"Applying apodization (Alpha={alpha}, Div={divergence_deg}°) on {len(self.AcousticFields)} fields...")
566
+
567
+ probe_width = self.params.acoustic['num_elements'] * self.params.acoustic['element_width']
568
+
569
+ for i in trange(len(self.AcousticFields), desc="Apodisation"):
570
+ # 1. Récupération des données et de l'angle
571
+ field = self.AcousticFields[i].field # Peut être (Z, X) ou (Time, Z, X)
572
+ angle = self.AcousticFields[i].angle # L'angle de l'onde plane
573
+
574
+ # 2. Récupération ou construction des axes physiques
575
+ nz, nx = field.shape[-2:]
576
+
577
+ if hasattr(self, 'x_axis') and self.x_axis is not None:
578
+ x_axis = self.x_axis
579
+ else:
580
+ # Génération par défaut centrée sur 0 (ex: -20mm à +20mm)
581
+ x_axis = np.linspace(-probe_width/2, probe_width/2, nx)
582
+
583
+ if hasattr(self, 'z_axis') and self.z_axis is not None:
584
+ z_axis = self.z_axis
585
+ else:
586
+ # Génération par défaut (ex: 0 à 40mm, basé sur un pitch standard ou arbitraire)
587
+ estimated_depth = 40e-3 # Valeur arbitraire si inconnue
588
+ z_axis = np.linspace(0, estimated_depth, nz)
589
+
590
+ # 3. Préparation des grilles pour le masque
591
+ Z, X = np.meshgrid(z_axis, x_axis, indexing='ij')
592
+
593
+ # 4. Calcul de la géométrie orientée (Steering)
594
+ angle_rad = np.deg2rad(angle)
595
+ X_aligned = X - Z * np.tan(angle_rad)
596
+
597
+ # 5. Calcul de la largeur dynamique du masque (Divergence)
598
+ div_rad = np.deg2rad(divergence_deg)
599
+ current_half_width = (probe_width / 2.0) + Z * np.tan(div_rad)
600
+
601
+ # 6. Normalisation et création du masque Tukey
602
+ X_norm = np.divide(X_aligned, current_half_width, out=np.zeros_like(X_aligned), where=current_half_width!=0)
603
+
604
+ mask = np.zeros_like(X_norm)
605
+ plateau_threshold = 1.0 * (1 - alpha)
606
+
607
+ # Zone centrale (plateau = 1)
608
+ mask[np.abs(X_norm) <= plateau_threshold] = 1.0
609
+
610
+ # Zone de transition (cosinus)
611
+ transition_indices = (np.abs(X_norm) > plateau_threshold) & (np.abs(X_norm) <= 1.0)
612
+ if np.any(transition_indices):
613
+ x_trans = np.abs(X_norm[transition_indices]) - plateau_threshold
614
+ width_trans = 1.0 * alpha
615
+ mask[transition_indices] = 0.5 * (1 + np.cos(np.pi * x_trans / width_trans))
616
+
617
+ # 7. Application du masque (Gestion 2D vs 3D)
618
+ if field.ndim == 3:
619
+ field_apodized = field * mask[np.newaxis, :, :]
620
+ else:
621
+ field_apodized = field * mask
622
+
623
+ # 8. Mise à jour de l'objet
624
+ self.AcousticFields[i].field = field_apodized
625
+
626
+ print("Apodisation done.")
627
+
391
628
  # PRIVATE METHODS
392
- def _generateAcousticFields_STRUCT_CPU(self, fieldDataPath=None, show_log=False):
629
+ def _generateAcousticFields_STRUCT_CPU(self, fieldDataPath=None, show_log=False, nameBlock=None):
393
630
  if self.patterns is None:
394
631
  raise ValueError("patterns is not initialized. Please load or generate the active list first.")
395
632
  listAcousticFields = []
@@ -415,7 +652,7 @@ class Tomography(Experiment):
415
652
  if pathField is not None and os.path.exists(pathField):
416
653
  progress_bar.set_postfix_str(f"Loading field - {AcousticField.getName_field()} -- Memory used: {memory.percent}%")
417
654
  try:
418
- AcousticField.load_field(fieldDataPath, self.FormatSave)
655
+ AcousticField.load_field(fieldDataPath, self.FormatSave,nameBlock)
419
656
  except:
420
657
  progress_bar.set_postfix_str(f"Error loading field -> Generating field - {AcousticField.getName_field()} -- Memory used: {memory.percent}% ---- processing on {config.get_process().upper()} ----")
421
658
  AcousticField.generate_field(show_log=show_log)
@@ -432,4 +669,73 @@ class Tomography(Experiment):
432
669
  AcousticField.save_field(fieldDataPath)
433
670
  listAcousticFields.append(AcousticField)
434
671
  progress_bar.set_postfix_str("")
672
+
435
673
  return listAcousticFields
674
+
675
+ def load_experimentalAO(self, pathAO, withTumor = True, h5name='AOsignal'):
676
+ """
677
+ Load experimental AO signals from specified file paths.
678
+ Args:
679
+ path_withTumor: Path to the AO signal with tumor.
680
+ path_withoutTumor: Path to the AO signal without tumor.
681
+ """
682
+ if not os.path.exists(pathAO):
683
+ raise FileNotFoundError(f"File {pathAO} not found.")
684
+
685
+ if pathAO.endswith('.npy'):
686
+ ao_signal = np.load(pathAO)
687
+ elif pathAO.endswith('.h5'):
688
+ with h5py.File(pathAO, 'r') as f:
689
+ if h5name not in f:
690
+ raise KeyError(f"Dataset '{h5name}' not found in the HDF5 file.")
691
+ ao_signal = f[h5name][:]
692
+ elif pathAO.endswith('.mat'):
693
+ mat_data = loadmat(pathAO)
694
+ if h5name not in mat_data:
695
+ raise KeyError(f"Dataset '{h5name}' not found in the .mat file.")
696
+ ao_signal = mat_data[h5name]
697
+ elif pathAO.endswith('.hdr'):
698
+ ao_signal = self._loadAOSignal(pathAO)
699
+ else:
700
+ raise ValueError("Unsupported file format. Supported formats are: .npy, .h5, .mat, .hdr")
701
+
702
+ if withTumor:
703
+ self.AOsignal_withTumor = ao_signal
704
+ else:
705
+ self.AOsignal_withoutTumor = ao_signal
706
+
707
+ def check_experimentalAO(self, activeListPath, withTumor=True):
708
+ """
709
+ Check if the experimental AO signals are correctly initialized.
710
+ """
711
+ if withTumor:
712
+ if self.AOsignal_withTumor is None:
713
+ raise ValueError("Experimental AOsignal with tumor is not initialized. Please load the experimental AO signal with tumor first.")
714
+ else:
715
+ if self.AOsignal_withoutTumor is None:
716
+ raise ValueError("Experimental AOsignal without tumor is not initialized. Please load the experimental AO signal without tumor first.")
717
+ if self.AcousticFields is not None:
718
+ # get min time shape between all AO signals
719
+ print()
720
+
721
+ if self.AcousticFields[0].field.shape[0] > self.AOsignal_withTumor.shape[0]:
722
+ self.cutAcousticFields(max_t=self.AOsignal_withTumor.shape[0]/float(self.params.acoustic['f_saving']))
723
+ else:
724
+ for i in range(len(self.AcousticFields)):
725
+ min_time_shape = min(self.AcousticFields[i].field.shape[0])
726
+ if withTumor:
727
+ self.AOsignal_withTumor = self.AOsignal_withTumor[:min_time_shape, :]
728
+ else:
729
+ self.AOsignal_withoutTumor = self.AOsignal_withoutTumor[:min_time_shape, :]
730
+
731
+ for field in self.AcousticFields:
732
+ if activeListPath is not None:
733
+ with open(activeListPath, 'r') as file:
734
+ lines = file.readlines()
735
+ expected_name = lines[self.AcousticFields.index(field)].strip()
736
+ nameField = field.getName_field()
737
+ if nameField.startswith("field_"):
738
+ nameField = nameField[len("field_"):]
739
+ if nameField != expected_name:
740
+ raise ValueError(f"Field name {nameField} does not match the expected name {expected_name} from the active list.")
741
+ print("Experimental AO signals are correctly initialized.")