AOT-biomaps 2.9.339__py3-none-any.whl → 2.9.373__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.

@@ -217,3 +217,20 @@ def next_power_of_2(n):
217
217
  """Calculate the next power of 2 greater than or equal to n."""
218
218
  return int(2 ** np.ceil(np.log2(n)))
219
219
 
220
+ def hex_to_binary_profile(hex_string, n_piezos=192):
221
+ hex_string = hex_string.strip().replace(" ", "").replace("\n", "")
222
+ if set(hex_string.lower()) == {'f'}:
223
+ return np.ones(n_piezos, dtype=int)
224
+
225
+ try:
226
+ n_char = len(hex_string)
227
+ n_bits = n_char * 4
228
+ binary_str = bin(int(hex_string, 16))[2:].zfill(n_bits)
229
+ if len(binary_str) < n_piezos:
230
+ # Tronquer/padder en fonction de la taille réelle de la sonde
231
+ binary_str = binary_str.ljust(n_piezos, '0')
232
+ elif len(binary_str) > n_piezos:
233
+ binary_str = binary_str[:n_piezos]
234
+ return np.array([int(b) for b in binary_str])
235
+ except ValueError:
236
+ return np.zeros(n_piezos, dtype=int)
@@ -2,6 +2,7 @@ from AOT_biomaps.Config import config
2
2
  from ._mainAcoustic import AcousticField
3
3
  from .AcousticEnums import WaveType
4
4
  from .AcousticTools import detect_space_0_and_space_1, getAngle
5
+ from .AcousticTools import hex_to_binary_profile
5
6
 
6
7
  import os
7
8
  import numpy as np
@@ -156,7 +157,20 @@ class StructuredWave(AcousticField):
156
157
  int: Decimation frequency.
157
158
  """
158
159
  try:
159
- return 1/(self.pattern.space_0 + self.pattern.space_1)/self.params['element_width']
160
+ profile = hex_to_binary_profile(self.getName_field()[6:-4], self.params['num_elements'])
161
+
162
+ if set(self.getName_field()[6:-4].lower().replace(" ", "")) == {'f'}:
163
+ fs_key = 0.0 # fs_key est en mm^-1 (0.0 mm^-1)
164
+ else:
165
+ ft_prof = np.fft.fft(profile)
166
+ idx_max = np.argmax(np.abs(ft_prof[1:len(profile)//2])) + 1
167
+ freqs = np.fft.fftfreq(len(profile), d=self.params['dx'])
168
+
169
+ # freqs est en m^-1 car delta_x est en mètres.
170
+ fs_m_inv = abs(freqs[idx_max])
171
+
172
+ fs_key = fs_m_inv # Fréquence spatiale en mm^-1
173
+ return int(fs_key / (1/(len(profile)*self.params['dx'])))
160
174
  except Exception as e:
161
175
  print(f"Error calculating decimation frequency: {e}")
162
176
  return None
@@ -93,6 +93,8 @@ class AcousticField(ABC):
93
93
  'num_elements': params.acoustic['num_elements'],
94
94
  'element_width': params.acoustic['element_width'],
95
95
  'element_height': params.acoustic['element_height'],
96
+ 'height_phantom': params.acoustic['phantom']['height'] if 'phantom' in params.acoustic and 'height' in params.acoustic['phantom'] else None,
97
+ 'width_phantom': params.acoustic['phantom']['width'] if 'phantom' in params.acoustic and 'width' in params.acoustic['phantom'] else None,
96
98
  'Xrange': params.general['Xrange'],
97
99
  'Yrange': params.general['Yrange'],
98
100
  'Zrange': params.general['Zrange'],
@@ -104,6 +106,7 @@ class AcousticField(ABC):
104
106
  'Nx': int(np.round((params.general['Xrange'][1] - params.general['Xrange'][0])/params.general['dx'])),
105
107
  'Ny': int(np.round((params.general['Yrange'][1] - params.general['Yrange'][0])/params.general['dy'])) if params.general['Yrange'] is not None else 1,
106
108
  'Nz': int(np.round((params.general['Zrange'][1] - params.general['Zrange'][0])/params.general['dz'])),
109
+ 'Nt': params.general['Nt'] if 'Nt' in params.general else None,
107
110
  'probeWidth': params.acoustic['num_elements'] * params.acoustic['element_width'],
108
111
  'IsAbsorbingMedium': params.acoustic['isAbsorbingMedium'],
109
112
  }
@@ -114,8 +117,11 @@ class AcousticField(ABC):
114
117
 
115
118
  self.params['f_AQ'] = int(1/self.kgrid.dt)
116
119
  else:
117
- Nt = ceil((self.params['Zrange'][1] - self.params['Zrange'][0])*float(params.acoustic['f_AQ']) / self.params['c0'])
118
-
120
+ if self.params['Nt'] is None:
121
+ Nt = ceil((self.params['Zrange'][1] - self.params['Zrange'][0])*float(params.acoustic['f_AQ']) / self.params['c0'])
122
+ self.params['Nt'] = Nt
123
+ else:
124
+ Nt = self.params['Nt']
119
125
  self.kgrid.setTime(Nt,1/float(params.acoustic['f_AQ']))
120
126
  self.params['f_AQ'] = int(float(params.acoustic['f_AQ']))
121
127
 
@@ -505,13 +511,25 @@ class AcousticField(ABC):
505
511
  try:
506
512
  # --- 1. Grid setup ---
507
513
  dx = self.params['dx']
508
- if dx >= self.params['element_width']*2:
514
+ if dx >= self.params['element_width']:
509
515
  dx = self.params['element_width'] / 2
510
- Nx = int(round((self.params['Xrange'][1] - self.params['Xrange'][0]) / dx))
511
- Nz = int(round((self.params['Zrange'][1] - self.params['Zrange'][0]) / dx))
516
+ if self.params['width_phantom'] is not None:
517
+ Nx = int(np.round((self.params['width_phantom'])/dx))
518
+ else:
519
+ Nx = int(round((self.params['Xrange'][1] - self.params['Xrange'][0]) / dx))
520
+ if self.params['height_phantom'] is not None:
521
+ Nz = int(np.round((self.params['height_phantom'])/dx))
522
+ else:
523
+ Nz = int(round((self.params['Zrange'][1] - self.params['Zrange'][0]) / dx))
512
524
  else:
513
- Nx = self.params['Nx']
514
- Nz = self.params['Nz']
525
+ if self.params['width_phantom'] is not None:
526
+ Nx = int(np.round((self.params['width_phantom'])/self.params['dx']))
527
+ else:
528
+ Nx = int(round((self.params['Xrange'][1] - self.params['Xrange'][0]) / self.params['dx']))
529
+ if self.params['height_phantom'] is not None:
530
+ Nz = int(np.round((self.params['height_phantom'])/self.params['dz']))
531
+ else:
532
+ Nz = int(round((self.params['Zrange'][1] - self.params['Zrange'][0]) / self.params['dz']))
515
533
 
516
534
  # --- 2. Time and space factors ---
517
535
  self.factorT = int(np.ceil(self.params['f_AQ'] / self.params['f_saving']))
@@ -530,17 +548,14 @@ class AcousticField(ABC):
530
548
  sensor.mask = np.ones((Nx, Nz))
531
549
 
532
550
  # --- 5. PML setup ---
533
- total_size_x = next_power_of_2(Nx)
534
551
  total_size_z = next_power_of_2(Nz)
535
- pml_x_size = (total_size_x - Nx) // 2
536
552
  pml_z_size = (total_size_z - Nz) // 2
537
- pml_x_size = max(pml_x_size, 50) # Ensure a minimum PML size of 50 grid points to avoid parasitic reflections
538
553
  pml_z_size = max(pml_z_size, 50) # Ensure a minimum PML size of 50 grid points to avoid parasitic reflections
539
554
 
540
555
  # --- 6. Simulation options ---
541
556
  simulation_options = SimulationOptions(
542
557
  pml_inside=False,
543
- pml_size=[pml_x_size, pml_z_size],
558
+ pml_size=[0, pml_z_size],
544
559
  use_sg=False,
545
560
  save_to_disk=True,
546
561
  input_filename=os.path.join(gettempdir(), "KwaveIN.h5"),
@@ -583,11 +598,23 @@ class AcousticField(ABC):
583
598
  dx = self.params['dx']
584
599
  if dx >= self.params['element_width']:
585
600
  dx = self.params['element_width'] / 2
586
- Nx = int(round((self.params['Xrange'][1] - self.params['Xrange'][0]) / dx))
587
- Nz = int(round((self.params['Zrange'][1] - self.params['Zrange'][0]) / dx))
601
+ if self.params['width_phantom'] is not None:
602
+ Nx = int(np.round((self.params['width_phantom'])/dx))
603
+ else:
604
+ Nx = int(round((self.params['Xrange'][1] - self.params['Xrange'][0]) / dx))
605
+ if self.params['height_phantom'] is not None:
606
+ Nz = int(np.round((self.params['height_phantom'])/dx))
607
+ else:
608
+ Nz = int(round((self.params['Zrange'][1] - self.params['Zrange'][0]) / dx))
588
609
  else:
589
- Nx = self.params['Nx']
590
- Nz = self.params['Nz']
610
+ if self.params['width_phantom'] is not None:
611
+ Nx = int(np.round((self.params['width_phantom'])/self.params['dx']))
612
+ else:
613
+ Nx = int(round((self.params['Xrange'][1] - self.params['Xrange'][0]) / self.params['dx']))
614
+ if self.params['height_phantom'] is not None:
615
+ Nz = int(np.round((self.params['height_phantom'])/self.params['dz']))
616
+ else:
617
+ Nz = int(round((self.params['Zrange'][1] - self.params['Zrange'][0]) / self.params['dz']))
591
618
 
592
619
  # --- 2. Time and space factors (common) ---
593
620
  factorT = int(np.ceil(self.params['f_AQ'] / self.params['f_saving']))
@@ -1,32 +1,22 @@
1
1
  import numpy as np
2
2
 
3
- def calc_mat_os(xm, fx, dx, bool_active_list, signal_type):
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
+ """
4
9
  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)
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)
14
16
  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]))
17
+ mask = np.ones(num_els) # Sécurité
18
+
19
+ return np.tile(mask[:, np.newaxis], (1, num_cols))
30
20
 
31
21
  def convert_to_hex_list(matrix):
32
22
  """
@@ -18,6 +18,8 @@ class Tomography(Experiment):
18
18
  self.patterns = None
19
19
  self.theta = []
20
20
  self.decimations = []
21
+ self.ActiveList = []
22
+ self.DelayLaw = []
21
23
 
22
24
  # PUBLIC METHODS
23
25
  def check(self):
@@ -66,8 +68,11 @@ class Tomography(Experiment):
66
68
  self.AcousticFields = self._generateAcousticFields_STRUCT_CPU(fieldDataPath, show_log, nameBlock)
67
69
  for i in range(len(self.AcousticFields)):
68
70
  profile = hex_to_binary_profile(self.AcousticFields[i].getName_field()[6:-4], self.params.acoustic['num_elements'])
71
+ self.ActiveList.append(profile)
69
72
  angle = self.AcousticFields[i].angle
70
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))
71
76
 
72
77
  if set(self.AcousticFields[i].getName_field()[6:-4].lower().replace(" ", "")) == {'f'}:
73
78
  fs_key = 0.0 # fs_key est en mm^-1 (0.0 mm^-1)
@@ -83,6 +88,7 @@ class Tomography(Experiment):
83
88
 
84
89
  # fs = n * dfx => n = fs / dfx with dfx = 1/(N*delta_x)
85
90
  self.decimations.append(int(fs_key / (1/(len(profile)*self.params.general['dx']))))
91
+
86
92
  else:
87
93
  raise ValueError("Unsupported wave type.")
88
94
 
@@ -314,20 +320,8 @@ class Tomography(Experiment):
314
320
  raise ValueError("Either N (>=2) or both decimations and angles must be provided for pattern generation.")
315
321
 
316
322
  def saveAOsignals_matlab(self, filePath):
317
- ActiveList = []
318
- DelayLaw = []
319
- c = self.params.acoustic['c0']
320
- NbElemts = self.params.acoustic['num_elements']
321
- pitch = self.params.acoustic['element_width']
322
-
323
- for i in range(len(self.AcousticFields)):
324
- profile = hex_to_binary_profile(self.AcousticFields[i].getName_field()[6:-4], NbElemts)
325
- ActiveList.append(profile)
326
- angle = self.AcousticFields[i].angle
327
- Delay = 1000 * (1/c) * np.sin(np.deg2rad(angle)) * np.arange(1, NbElemts + 1) * pitch
328
- DelayLaw.append(Delay - np.min(Delay))
329
323
 
330
- savemat(filePath, {'data': self.AOsignal_withTumor, 'thetas': self.theta, 'decimations': self.decimations, 'ActiveList' : ActiveList, 'DelayLaw': DelayLaw})
324
+ savemat(filePath, {'data': self.AOsignal_withTumor, 'thetas': self.theta, 'decimations': self.decimations, 'ActiveList' : self.ActiveList, 'DelayLaw': self.DelayLaw})
331
325
 
332
326
  def selectAngles(self, angles):
333
327
 
@@ -346,6 +340,27 @@ class Tomography(Experiment):
346
340
  if self.AOsignal_withoutTumor is not None:
347
341
  self.AOsignal_withoutTumor = self.AOsignal_withoutTumor[:, index]
348
342
  self.AcousticFields = newAcousticFields
343
+ self.theta = [field.angle for field in newAcousticFields]
344
+ self.decimations = [field.f_s for field in newAcousticFields]
345
+
346
+ def selectDecimations(self, decimations):
347
+ if self.AOsignal_withTumor is None and self.AOsignal_withoutTumor is None:
348
+ raise ValueError("AO signals are not initialized. Please load or generate the AO signals first.")
349
+ if self.AcousticFields is None or len(self.AcousticFields) == 0:
350
+ raise ValueError("AcousticFields is not initialized. Please generate the system matrix first.")
351
+ newAcousticFields = []
352
+ index = []
353
+ for i, field in enumerate(self.AcousticFields):
354
+ if field.f_s in decimations:
355
+ newAcousticFields.append(field)
356
+ index.append(i)
357
+ if self.AOsignal_withTumor is not None:
358
+ self.AOsignal_withTumor = self.AOsignal_withTumor[:, index]
359
+ if self.AOsignal_withoutTumor is not None:
360
+ self.AOsignal_withoutTumor = self.AOsignal_withoutTumor[:, index]
361
+ self.AcousticFields = newAcousticFields
362
+ self.decimations = [field.f_s for field in newAcousticFields]
363
+ self.theta = [field.angle for field in newAcousticFields]
349
364
 
350
365
  def selectPatterns(self, pattern_names):
351
366
  if self.AOsignal_withTumor is None and self.AOsignal_withoutTumor is None:
@@ -380,75 +395,73 @@ class Tomography(Experiment):
380
395
  self.AcousticFields = newAcousticFields
381
396
 
382
397
  def _genereate_patterns_from_decimations(self, decimations, angles):
383
- if isinstance(decimations, list): decimations = np.array(decimations)
384
- if isinstance(angles, list): angles = np.array(angles)
398
+ if isinstance(decimations, list):
399
+ decimations = np.array(decimations)
400
+ if isinstance(angles, list):
401
+ angles = np.array(angles)
385
402
 
386
403
  angles = np.sort(angles)
387
404
  decimations = np.sort(decimations)
388
405
 
389
406
  num_elements = self.params.acoustic['num_elements']
390
- dx_mm = self.params.general['dx'] * 1e3
407
+ Width = self.params.acoustic['element_width'] # en m
408
+ kerf = self.params.acoustic.get('kerf', 0.00000) # en m
409
+ Nactuators = self.params.acoustic['num_elements']
391
410
 
392
411
  # --- Calcul du nombre de Scans ---
393
412
  if 0 in decimations:
394
- Nscans = 4 * angles.shape[0] * (decimations.shape[0] - 1) + angles.shape[0]
395
- offSet = angles.shape[0]
413
+ Nscans = 4 * len(angles) * (len(decimations) - 1) + len(angles)
396
414
  else:
397
- Nscans = 4 * angles.shape[0] * decimations.shape[0]
398
- offSet = 0
415
+ Nscans = 4 * len(angles) * len(decimations)
399
416
 
400
417
  ActiveLIST = np.ones((num_elements, Nscans))
401
- Xm = np.arange(1, num_elements + 1) * dx_mm
402
- dFx = 1 / (num_elements * dx_mm)
403
418
 
404
- # On traite séparément les décimations non nulles pour la boucle
419
+ # --- Calcul des positions centrées des éléments (en m) ---
420
+ Xc = (Width + (Nactuators - 1) * (kerf + Width)) / 2
421
+ Xm = np.array([Width * (i - 1) + Width / 2 - Xc for i in range(1, Nactuators + 1)])
422
+
423
+ # --- Gestion de l'onde plane (tous les piezo ON) au début ---
424
+ if 0 in decimations:
425
+ I_plane = np.arange(len(angles))
426
+ ActiveLIST[:, I_plane] = 1 # Tous les piezo ON pour les len(angles) premières colonnes
427
+
428
+ # --- Traitement des décimations non nulles ---
405
429
  active_decimations = decimations[decimations != 0]
430
+ dFx = 1 / (Nactuators * Width) # fx de base (en m^-1)
406
431
 
407
432
  for i_dec in range(len(active_decimations)):
408
- idx_base = (np.arange(len(angles))) + (i_dec * 4 * len(angles)) + offSet
409
-
410
- Icos = idx_base
411
- Incos = idx_base + len(angles)
412
- Isin = idx_base + 2 * len(angles)
413
- Insin = idx_base + 3 * len(angles)
433
+ # Décalage des indices pour placer les motifs modulés après l'onde plane
434
+ I = np.arange(len(angles)) + len(angles) + (i_dec * 4 * len(angles))
414
435
 
415
- fx = dFx * active_decimations[i_dec]
436
+ Icos = I
437
+ Incos = I + 1* len(angles)
438
+ Isin = I + 3 * len(angles)
439
+ Insin = I + 2 * len(angles)
416
440
 
417
- # Remplissage des 4 phases
418
- valid_icos = Icos[Icos < Nscans]
419
- if valid_icos.size > 0:
420
- ActiveLIST[:, valid_icos] = calc_mat_os(Xm, fx, dx_mm, ActiveLIST[:, valid_icos], 'cos')
421
- if (Incos < Nscans).any():
422
- ActiveLIST[:, Incos[Incos < Nscans]] = 1 - ActiveLIST[:, valid_icos]
441
+ fx = dFx * active_decimations[i_dec]
423
442
 
424
- valid_isin = Isin[Isin < Nscans]
425
- if valid_isin.size > 0:
426
- ActiveLIST[:, valid_isin] = calc_mat_os(Xm, fx, dx_mm, ActiveLIST[:, valid_isin], 'sin')
427
- if (Insin < Nscans).any():
428
- ActiveLIST[:, Insin[Insin < Nscans]] = 1 - ActiveLIST[:, valid_isin]
443
+ # Appliquer les motifs modulés
444
+ ActiveLIST[:, Icos] = calc_mat_os(Xm, fx, ActiveLIST[:, Icos[:1]], 'cos')
445
+ ActiveLIST[:, Incos] = 1 - ActiveLIST[:, Icos]
446
+ ActiveLIST[:, Isin] = calc_mat_os(Xm, fx, ActiveLIST[:, Isin[:1]], 'sin')
447
+ ActiveLIST[:, Insin] = 1 - ActiveLIST[:, Isin]
429
448
 
430
449
  # --- Conversion au format attendu ---
431
- # 1. On convertit toute la matrice en liste de strings Hexa
432
450
  hexa_list = convert_to_hex_list(ActiveLIST)
433
451
 
434
- # 2. Fonction interne de formatage d'angle (pour coller à votre ancien code)
435
452
  def format_angle(a):
436
453
  return f"{'1' if a < 0 else '0'}{abs(a):02d}"
437
454
 
438
- # 3. Construction de la liste de dictionnaires
439
455
  patterns = []
440
456
  print(f"Generating {Nscans} patterns from decimations and angles...")
441
457
  for i in range(Nscans):
442
- # On retrouve l'angle correspondant à l'index i
443
- # La logique est cyclique sur la taille de 'angles'
444
458
  angle_val = angles[i % len(angles)]
445
-
446
459
  hex_pattern = hexa_list[i]
447
460
  pair = f"{hex_pattern}_{format_angle(angle_val)}"
448
461
  patterns.append({"fileName": pair})
449
462
 
450
463
  return patterns
451
-
464
+
452
465
  def _generate_patterns(self, N,angles = None):
453
466
  def format_angle(a):
454
467
  return f"{'1' if a < 0 else '0'}{abs(a):02d}"