AOT-biomaps 2.9.356__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']))
@@ -340,6 +340,27 @@ class Tomography(Experiment):
340
340
  if self.AOsignal_withoutTumor is not None:
341
341
  self.AOsignal_withoutTumor = self.AOsignal_withoutTumor[:, index]
342
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]
343
364
 
344
365
  def selectPatterns(self, pattern_names):
345
366
  if self.AOsignal_withTumor is None and self.AOsignal_withoutTumor is None:
@@ -440,8 +461,7 @@ class Tomography(Experiment):
440
461
  patterns.append({"fileName": pair})
441
462
 
442
463
  return patterns
443
-
444
-
464
+
445
465
  def _generate_patterns(self, N,angles = None):
446
466
  def format_angle(a):
447
467
  return f"{'1' if a < 0 else '0'}{abs(a):02d}"
@@ -18,91 +18,6 @@ class AnalyticRecon(Recon):
18
18
  self.Lc = Lc # in meters
19
19
  self.AOsignal_demoldulated = None
20
20
 
21
- def parse_and_demodulate(self, withTumor=True):
22
-
23
- if withTumor:
24
- AOsignal = self.experiment.AOsignal_withTumor
25
- else:
26
- AOsignal = self.experiment.AOsignal_withoutTumor
27
- delta_x = self.experiment.params.general['dx'] # en m
28
- n_piezos = self.experiment.params.acoustic['num_elements']
29
- demodulated_data = {}
30
- structured_buffer = {}
31
-
32
- for i in trange(len(self.experiment.AcousticFields), desc="Demodulating AO signals"):
33
- label = self.experiment.AcousticFields[i].getName_field()
34
-
35
- parts = label.split("_")
36
- hex_pattern = parts[0]
37
- angle_code = parts[-1]
38
-
39
- # Angle
40
- if angle_code.startswith("1"):
41
- angle_deg = -int(angle_code[1:])
42
- else:
43
- angle_deg = int(angle_code)
44
- angle_rad = np.deg2rad(angle_deg)
45
-
46
- # Onde Plane (f_s = 0)
47
- if set(hex_pattern.lower().replace(" ", "")) == {'f'}:
48
- fs_key = 0.0 # fs_key est en mm^-1 (0.0 mm^-1)
49
- demodulated_data[(fs_key, angle_rad)] = np.array(AOsignal[:,i])
50
- continue
51
-
52
- # Onde Structurée
53
- profile = hex_to_binary_profile(hex_pattern, n_piezos)
54
-
55
- # Calcul FS (Fréquence de Structuration)
56
- ft_prof = np.fft.fft(profile)
57
- # On regarde uniquement la partie positive non DC
58
- idx_max = np.argmax(np.abs(ft_prof[1:len(profile)//2])) + 1
59
- freqs = np.fft.fftfreq(len(profile), d=delta_x)
60
-
61
- # freqs est en m^-1 car delta_x est en mètres.
62
- fs_m_inv = abs(freqs[idx_max])
63
-
64
- # *** CORRECTION 1: Conversion de f_s en mm^-1 (mm^-1 est utilisé dans iRadon) ***
65
- fs_key = fs_m_inv / 1000.0 # Fréquence spatiale en mm^-1
66
-
67
-
68
- if fs_key == 0: continue
69
-
70
- # Calcul de la Phase (Shift)
71
- phase = get_phase_deterministic(profile)
72
-
73
- # Stockage par (fs, theta) et phase
74
- key = (fs_key, angle_rad)
75
- if key not in structured_buffer:
76
- structured_buffer[key] = {}
77
-
78
- # La moyenne est nécessaire si plusieurs acquisitions ont la même phase (pour le SNR)
79
- if phase in structured_buffer[key]:
80
- structured_buffer[key][phase] = (structured_buffer[key][phase] + np.array(AOsignal[:,i])) / 2
81
- else:
82
- structured_buffer[key][phase] = np.array(AOsignal[:,i])
83
-
84
-
85
-
86
- for (fs, theta), phases in structured_buffer.items():
87
- s0 = phases.get(0.0, 0)
88
- s_pi_2 = phases.get(np.pi/2, 0)
89
- s_pi = phases.get(np.pi, 0)
90
- s_3pi_2 = phases.get(3*np.pi/2, 0)
91
-
92
- # Assurer que les zéros sont des vecteurs de la bonne taille
93
- example = next(val for val in phases.values() if not isinstance(val, int))
94
- if isinstance(s0, int): s0 = np.zeros_like(example)
95
- if isinstance(s_pi, int): s_pi = np.zeros_like(example)
96
- if isinstance(s_pi_2, int): s_pi_2 = np.zeros_like(example)
97
- if isinstance(s_3pi_2, int): s_3pi_2 = np.zeros_like(example)
98
-
99
- real = s0 - s_pi
100
- imag = s_pi_2 - s_3pi_2
101
-
102
- demodulated_data[(fs, theta)] = (real - 1j * imag) / (2/np.pi)
103
-
104
- return demodulated_data
105
-
106
21
  def run(self, processType = ProcessType.PYTHON, withTumor= True):
107
22
  """
108
23
  This method is a placeholder for the analytic reconstruction process.
@@ -126,8 +41,13 @@ class AnalyticRecon(Recon):
126
41
  Parameters:
127
42
  analyticType: The type of analytic reconstruction to perform (default is iFOURIER).
128
43
  """
44
+ if withTumor:
45
+ AOsignal = self.experiment.AOsignal_withTumor
46
+ else:
47
+ AOsignal = self.experiment.AOsignal_withoutTumor
48
+
129
49
  d_t = 1 / float(self.experiment.params.acoustic['f_saving'])
130
- t_array = np.arange(0, self.experiment.AOsignal_withTumor.shape[0])*d_t
50
+ t_array = np.arange(0, AOsignal.shape[0])*d_t
131
51
  Z = t_array * self.experiment.params.acoustic['c0']
132
52
  X_m = np.arange(0, self.experiment.params.acoustic['num_elements'])* self.experiment.params.general['dx']
133
53
  dfX = 1 / (X_m[1] - X_m[0]) / len(X_m)
@@ -135,7 +55,7 @@ class AnalyticRecon(Recon):
135
55
  self.AOsignal_demoldulated = self.parse_and_demodulate(withTumor=True)
136
56
  if self.analyticType == AnalyticType.iFOURIER:
137
57
  self.reconPhantom = self._iFourierRecon(
138
- R = self.experiment.AOsignal_withTumor,
58
+ R = AOsignal,
139
59
  z = Z,
140
60
  X_m=X_m,
141
61
  theta=self.experiment.theta,
@@ -148,7 +68,7 @@ class AnalyticRecon(Recon):
148
68
 
149
69
  elif self.analyticType == AnalyticType.iRADON:
150
70
  self.reconPhantom = self._iRadonRecon(
151
- R=self.experiment.AOsignal_withTumor,
71
+ R=AOsignal,
152
72
  z=Z,
153
73
  X_m=X_m,
154
74
  theta=self.experiment.theta,
@@ -165,7 +85,7 @@ class AnalyticRecon(Recon):
165
85
  self.AOsignal_demoldulated = self.parse_and_demodulate(withTumor=False)
166
86
  if self.analyticType == AnalyticType.iFOURIER:
167
87
  self.reconLaser = self._iFourierRecon(
168
- R = self.experiment.AOsignal_withoutTumor,
88
+ R = AOsignal ,
169
89
  z = Z,
170
90
  X_m=X_m,
171
91
  theta=self.experiment.theta,
@@ -177,7 +97,7 @@ class AnalyticRecon(Recon):
177
97
  )
178
98
  elif self.analyticType == AnalyticType.iRADON:
179
99
  self.reconLaser = self._iRadonRecon(
180
- R=self.experiment.AOsignal_withoutTumor,
100
+ R=AOsignal ,
181
101
  z=Z,
182
102
  X_m=X_m,
183
103
  theta=self.experiment.theta,
@@ -203,7 +123,14 @@ class AnalyticRecon(Recon):
203
123
  ActiveLIST,
204
124
  withTumor,
205
125
  ):
206
- # --- 1. Préparation des données ---
126
+ """
127
+ Reconstruction d'image utilisant la méthode iFourier (GPU).
128
+ Normalisation physique complète incluse.
129
+ """
130
+
131
+ # ======================================================
132
+ # 1. Préparation GPU
133
+ # ======================================================
207
134
  R = cp.asarray(R)
208
135
  z = cp.asarray(z)
209
136
  X_m = cp.asarray(X_m)
@@ -211,28 +138,26 @@ class AnalyticRecon(Recon):
211
138
  decimation = cp.asarray(decimation)
212
139
  DelayLAWS = cp.asarray(DelayLAWS)
213
140
  ActiveLIST = cp.asarray(ActiveLIST)
214
- # Normalisation DelayLAWS (ms → s si besoin)
215
- DelayLAWS_s = cp.where(cp.max(DelayLAWS) > 1e-3, DelayLAWS / 1000.0, DelayLAWS)
216
- # Regroupement tirs (décimation, angle)
217
- ScanParam_cpu = cp.asnumpy(
218
- cp.stack([decimation, cp.round(theta, 4)], axis=1)
219
- )
220
141
 
221
- _, ia_cpu, ib_cpu = np.unique(
222
- ScanParam_cpu, axis=0, return_index=True, return_inverse=True
223
- )
142
+ # Normalisation DelayLAWS (ms -> s si nécessaire)
143
+ DelayLAWS_s = cp.where(cp.max(DelayLAWS) > 1e-3, DelayLAWS / 1000.0, DelayLAWS)
224
144
 
145
+ # Regroupement tirs (CPU pour np.unique plus rapide)
146
+ ScanParam_cpu = cp.asnumpy(cp.stack([decimation, cp.round(theta, 4)], axis=1))
147
+ _, ia_cpu, ib_cpu = np.unique(ScanParam_cpu, axis=0, return_index=True, return_inverse=True)
225
148
  ia = cp.asarray(ia_cpu)
226
149
  ib = cp.asarray(ib_cpu)
227
150
 
228
-
229
- # --- 2. Structuration complexe ---
230
- # add_sincos_gpu doit être l'équivalent GPU de add_sincos_cpu
151
+ # ======================================================
152
+ # 2. Structuration complexe
153
+ # ======================================================
231
154
  F_complex_cpu, theta_u_cpu, decim_u_cpu = add_sincos_cpu(
232
- cp.asnumpy(R),
233
- cp.asnumpy(decimation),
234
- np.radians(cp.asnumpy(theta))
155
+ cp.asnumpy(R),
156
+ cp.asnumpy(decimation),
157
+ np.radians(cp.asnumpy(theta))
235
158
  )
159
+
160
+ # Calcul des centres de rotation
236
161
  M0 = EvalDelayLawOS_center(
237
162
  X_m,
238
163
  theta_u_cpu,
@@ -241,29 +166,38 @@ class AnalyticRecon(Recon):
241
166
  c
242
167
  )
243
168
 
244
- # Transfert GPU (UNE FOIS)
169
+ # Transfert GPU
245
170
  F_complex = cp.asarray(F_complex_cpu)
246
171
  theta_u = cp.asarray(theta_u_cpu)
247
172
  decim_u = cp.asarray(decim_u_cpu)
173
+ M0_gpu = cp.asarray(M0)
248
174
 
175
+ # ======================================================
176
+ # 3. Paramètres de la grille
177
+ # ======================================================
249
178
  Nz = z.size
250
179
  Nx = X_m.size
251
180
  dx = X_m[1] - X_m[0]
252
-
253
181
  X_grid, Z_grid = cp.meshgrid(X_m, z)
254
182
  idx0_x = Nx // 2
255
- # Angles uniques après compression
256
- angles_group, ia_u, ib_u = cp.unique(
257
- theta_u, return_index=True, return_inverse=True
258
- )
183
+
184
+ # Angles uniques
185
+ angles_group, ia_u, ib_u = cp.unique(theta_u, return_index=True, return_inverse=True)
259
186
  Ntheta = angles_group.size
260
187
 
188
+ # Initialisation reconstruction
261
189
  I_final = cp.zeros((Nz, Nx), dtype=cp.complex64)
262
190
 
263
- # --- 3. Boucle InverseFourierX ---
264
- for i_ang in trange(Ntheta, desc=f"AOT-BioMaps -- Analytic Reconstruction Tomography: iFourier ({'with tumor' if withTumor else 'without tumor'}) ---- processing on single GPU ----", unit="angle"):
191
+ # ======================================================
192
+ # 4. Boucle Inverse Fourier X
193
+ # ======================================================
194
+ for i_ang in trange(
195
+ Ntheta,
196
+ desc=f"AOT-BioMaps -- iFourier ({'with tumor' if withTumor else 'without tumor'}) -- GPU",
197
+ unit="angle"
198
+ ):
265
199
 
266
- # Grille (z, fx)
200
+ # Grille Fourier locale (z, fx)
267
201
  F_fx_z = cp.zeros((Nz, Nx), dtype=cp.complex64)
268
202
 
269
203
  # Indices correspondant à cet angle
@@ -278,7 +212,7 @@ class AnalyticRecon(Recon):
278
212
  if 0 <= ip < Nx:
279
213
  F_fx_z[:, ip] = trace_z
280
214
 
281
- # Mapping négatif (symétrie hermitienne Matlab)
215
+ # Mapping négatif (symétrie hermitienne MATLAB)
282
216
  if n != 0:
283
217
  im = idx0_x - n
284
218
  if 0 <= im < Nx:
@@ -289,21 +223,30 @@ class AnalyticRecon(Recon):
289
223
  # Correction DC
290
224
  F_fx_z[:, idx0_x] *= 0.5
291
225
 
292
- # --- Inverse Fourier X (GPU) ---
226
+ # Inverse Fourier X (GPU) + facteur Nx pour correspondance MATLAB
293
227
  I_spatial = ifourierx_gpu(F_fx_z, dx) * Nx
294
228
 
295
- # --- Rotation spatiale GPU ---
229
+ # Rotation spatiale autour du centre M0
296
230
  I_rot = rotate_theta_gpu(
297
231
  X_grid,
298
232
  Z_grid,
299
233
  I_spatial,
300
234
  -angles_group[i_ang],
301
- M0[i_ang, :]
235
+ M0_gpu[i_ang, :]
302
236
  )
303
237
 
304
- # Somme incohérente (OriginIm = sum)
238
+ # Somme incohérente
305
239
  I_final += I_rot
306
- I_final /= Ntheta
240
+
241
+ # ======================================================
242
+ # 5. Normalisation physique finale
243
+ # ======================================================
244
+ Ntheta_total = len(theta_u)
245
+ Ntirs_complex = (R.shape[1] - Ntheta_total) / 4.0 # 4 phases par tir
246
+
247
+ I_final /= (Ntheta_total * Ntirs_complex)
248
+ I_final *= dx # normalisation physique sur l’axe x
249
+
307
250
  return cp.real(I_final).get()
308
251
 
309
252
  def _iRadonRecon(
@@ -319,23 +262,24 @@ class AnalyticRecon(Recon):
319
262
  DelayLAWS,
320
263
  ActiveLIST,
321
264
  withTumor,
322
- ):
265
+ ):
323
266
  """
324
267
  Reconstruction d'image utilisant la méthode iRadon.
325
-
326
- :return: Image reconstruite.
268
+ Normalisation physique correcte (phases, angles, dz).
327
269
  """
328
-
270
+
329
271
  # ======================================================
330
- # 1. AddSinCos (structuration)
272
+ # 1. AddSinCos (structuration) — CPU volontairement
331
273
  # ======================================================
332
274
  theta = np.radians(theta)
333
275
  F_ct_kx, theta_u, decim_u = add_sincos_cpu(R, decimation, theta)
276
+
334
277
  ScanParam = np.stack([decimation, theta], axis=1)
335
278
  _, ia, _ = np.unique(ScanParam, axis=0, return_index=True, return_inverse=True)
279
+
336
280
  ActiveLIST = np.asarray(ActiveLIST).T
337
281
  DelayLAWS = np.asarray(DelayLAWS).T
338
- ActiveLIST_unique = ActiveLIST[:,ia]
282
+ ActiveLIST_unique = ActiveLIST[:, ia]
339
283
 
340
284
  # ======================================================
341
285
  # 2. FFT z
@@ -343,8 +287,7 @@ class AnalyticRecon(Recon):
343
287
  z_gpu = cp.asarray(z)
344
288
  Fin = fourierz_gpu(z, F_ct_kx)
345
289
 
346
-
347
- dz = float(z[1] - z[0])
290
+ dz = float(z[1] - z[0]) # <<< Δz PHYSIQUE
348
291
  fz = cp.fft.fftshift(cp.fft.fftfreq(len(z), d=dz))
349
292
 
350
293
  Nz, Nk = Fin.shape
@@ -354,7 +297,7 @@ class AnalyticRecon(Recon):
354
297
  # ======================================================
355
298
  decim_gpu = cp.asarray(decim_u)
356
299
  I0 = decim_gpu == 0
357
- F0 = Fin * I0[None, :] # I0 broadcastée sur les lignes
300
+ F0 = Fin * I0[None, :]
358
301
 
359
302
  DEC, FZ = cp.meshgrid(decim_gpu, fz)
360
303
 
@@ -381,40 +324,53 @@ class AnalyticRecon(Recon):
381
324
  Xc = float(np.mean(X_m))
382
325
 
383
326
  # ======================================================
384
- # 6. Calcul du centre M0 pour chaque angle
327
+ # 6. Centre de rotation M0
385
328
  # ======================================================
386
- M0 = EvalDelayLawOS_center(X_m,theta, DelayLAWS[:, ia], ActiveLIST_unique, c)
329
+ M0 = EvalDelayLawOS_center(X_m, theta, DelayLAWS[:, ia], ActiveLIST_unique, c)
387
330
  M0_gpu = cp.asarray(M0)
388
331
 
389
332
  # ======================================================
390
333
  # 7. Rétroprojection
391
334
  # ======================================================
392
- Irec_list = []
393
335
  Irec = cp.zeros_like(X, dtype=cp.complex64)
394
- for i in trange(len(theta_u), desc=f"AOT-BioMaps -- Analytic Reconstruction Tomography: iRadon ({'with tumor' if withTumor else 'without tumor'}) ---- processing on single GPU ----", unit="angle"):
336
+
337
+ for i in trange(
338
+ len(theta_u),
339
+ desc=f"AOT-BioMaps -- iRadon ({'with tumor' if withTumor else 'without tumor'}) -- GPU",
340
+ unit="angle"
341
+ ):
395
342
  th = float(theta_u[i])
396
-
397
- # calcul de T, S, h0...
398
- T = (X - M0_gpu[i,0]) * cp.sin(th) + (Z - M0_gpu[i,1]) * cp.cos(th) + M0_gpu[i,1]
399
- S = (X - Xc) * cp.cos(th) - (Z - M0_gpu[i,1]) * cp.sin(th)
400
- h0 = cp.exp(1j * 2*cp.pi * decim_u[i] * df0x * S)
401
-
402
- # interpolation GPU
403
- Tind = (T - z_gpu[0]) / (z_gpu[1] - z_gpu[0])
343
+
344
+ T = (X - M0_gpu[i, 0]) * cp.sin(th) + (Z - M0_gpu[i, 1]) * cp.cos(th) + M0_gpu[i, 1]
345
+ S = (X - Xc) * cp.cos(th) - (Z - M0_gpu[i, 1]) * cp.sin(th)
346
+ h0 = cp.exp(1j * 2 * cp.pi * decim_u[i] * df0x * S)
347
+
348
+ # interpolation linéaire en z
349
+ Tind = (T - z_gpu[0]) / dz
404
350
  i0 = cp.floor(Tind).astype(cp.int32)
405
351
  i1 = i0 + 1
406
- i0 = cp.clip(i0, 0, Nz-1)
407
- i1 = cp.clip(i1, 0, Nz-1)
352
+ i0 = cp.clip(i0, 0, Nz - 1)
353
+ i1 = cp.clip(i1, 0, Nz - 1)
408
354
  w = Tind - i0
409
355
 
410
- proj_sup = (1-w)*Fsup[i0, i] + w*Fsup[i1, i]
411
- proj_inf = (1-w)*Finf[i0, i] + w*Finf[i1, i]
356
+ proj_sup = (1 - w) * Fsup[i0, i] + w * Fsup[i1, i]
357
+ proj_inf = (1 - w) * Finf[i0, i] + w * Finf[i1, i]
358
+
359
+ # >>> SOMME BRUTE (correcte)
360
+ Irec += 2 * h0 * proj_sup + proj_inf
361
+
362
+ # ======================================================
363
+ # 8. NORMALISATION PHYSIQUE GLOBALE
364
+ # ======================================================
365
+
366
+ Ntheta = len(theta_u)
412
367
 
413
- Irec += 2*h0*proj_sup + proj_inf
414
- Irec /= i+1
415
- # Ajouter à la liste
416
- Irec_list.append(cp.real(Irec).get())
417
-
368
+ # nombre de tirs complexes indépendants (4 phases)
369
+ Ntirs_complex = (R.shape[1] - Ntheta) / 4.0
418
370
 
419
- return Irec_list
371
+ # normalisation finale
372
+ Irec /= (Ntheta * Ntirs_complex)
373
+ print(f"dz normalization: {dz}")
374
+ Irec *= dz
420
375
 
376
+ return cp.real(Irec).get()
@@ -1,12 +1,13 @@
1
1
  from AOT_biomaps.Config import config
2
- from AOT_biomaps.AOT_Experiment.Tomography import Tomography
2
+ from AOT_biomaps.AOT_Experiment.Tomography import Tomography, hex_to_binary_profile
3
3
  from .ReconEnums import ReconType
4
- from .ReconTools import mse, ssim
4
+ from .ReconTools import mse, ssim, get_phase_deterministic
5
5
 
6
6
  import os
7
7
  import numpy as np
8
8
  import matplotlib.pyplot as plt
9
9
  from abc import ABC, abstractmethod
10
+ from tqdm import trange
10
11
 
11
12
 
12
13
  class Recon(ABC):
@@ -166,8 +167,14 @@ class Recon(ABC):
166
167
  ssim_value = ssim(self.experiment.OpticImage.phantom, theta, data_range=data_range)
167
168
  self.SSIM.append(ssim_value)
168
169
 
169
-
170
- def show(self, withTumor=True, savePath=None):
170
+ def show(self, withTumor=True, savePath=None, scale='same'):
171
+ """
172
+ Display the reconstructed images.
173
+ Args:
174
+ withTumor (bool): If True, displays reconPhantom. If False, displays reconLaser. Default is True.
175
+ savePath (str): Path to save the figure. If None, the figure is not saved. Default is None.
176
+ scale (str): Scale for the aspect ratio of the plots. Default is 'same'. Options are 'same' or 'auto'.
177
+ """
171
178
  if withTumor:
172
179
  if self.reconPhantom is None:
173
180
  raise ValueError("Reconstructed phantom with tumor is empty. Run reconstruction first.")
@@ -181,12 +188,19 @@ class Recon(ABC):
181
188
  fig, axs = plt.subplots(1, 1, figsize=(10, 10))
182
189
  else:
183
190
  fig, axs = plt.subplots(1, 2, figsize=(20, 10))
191
+ if scale == 'same':
192
+ vmin = 0
193
+ vmax = 1
194
+ elif scale == 'auto':
195
+ vmin = np.min(self.experiment.OpticImage.phantom)
196
+ vmax = np.max(self.experiment.OpticImage.phantom)
197
+
184
198
  # Phantom original
185
199
  im1 = axs[1].imshow(
186
200
  self.experiment.OpticImage.phantom,
187
201
  cmap='hot',
188
- vmin=0,
189
- vmax=1,
202
+ vmin=vmin,
203
+ vmax=vmax,
190
204
  extent=(
191
205
  self.experiment.params.general['Xrange'][0],
192
206
  self.experiment.params.general['Xrange'][1],
@@ -199,12 +213,18 @@ class Recon(ABC):
199
213
  axs[1].set_xlabel("x (mm)", fontsize=12)
200
214
  axs[1].set_ylabel("z (mm)", fontsize=12)
201
215
  axs[1].tick_params(axis='both', which='major', labelsize=8)
216
+ if scale == 'same':
217
+ vmin = 0
218
+ vmax = 1
219
+ elif scale == 'auto':
220
+ vmin = np.min(image)
221
+ vmax = np.max(image)
202
222
  # Phantom reconstruit
203
223
  im0 = axs[0].imshow(
204
224
  image,
205
225
  cmap='hot',
206
- vmin=0,
207
- vmax=1,
226
+ vmin=vmin,
227
+ vmax=vmax,
208
228
  extent=(
209
229
  self.experiment.params.general['Xrange'][0],
210
230
  self.experiment.params.general['Xrange'][1],
@@ -288,4 +308,87 @@ class Recon(ABC):
288
308
 
289
309
  plt.show()
290
310
 
311
+ def parse_and_demodulate(self, withTumor=True):
312
+
313
+ if withTumor:
314
+ AOsignal = self.experiment.AOsignal_withTumor
315
+ else:
316
+ AOsignal = self.experiment.AOsignal_withoutTumor
317
+ delta_x = self.experiment.params.general['dx'] # en m
318
+ n_piezos = self.experiment.params.acoustic['num_elements']
319
+ demodulated_data = {}
320
+ structured_buffer = {}
321
+
322
+ for i in trange(len(self.experiment.AcousticFields), desc="Demodulating AO signals"):
323
+ label = self.experiment.AcousticFields[i].getName_field()
324
+
325
+ parts = label.split("_")
326
+ hex_pattern = parts[0]
327
+ angle_code = parts[-1]
328
+
329
+ # Angle
330
+ if angle_code.startswith("1"):
331
+ angle_deg = -int(angle_code[1:])
332
+ else:
333
+ angle_deg = int(angle_code)
334
+ angle_rad = np.deg2rad(angle_deg)
335
+
336
+ # Onde Plane (f_s = 0)
337
+ if set(hex_pattern.lower().replace(" ", "")) == {'f'}:
338
+ fs_key = 0.0 # fs_key est en mm^-1 (0.0 mm^-1)
339
+ demodulated_data[(fs_key, angle_rad)] = np.array(AOsignal[:,i])
340
+ continue
341
+
342
+ # Onde Structurée
343
+ profile = hex_to_binary_profile(hex_pattern, n_piezos)
344
+
345
+ # Calcul FS (Fréquence de Structuration)
346
+ ft_prof = np.fft.fft(profile)
347
+ # On regarde uniquement la partie positive non DC
348
+ idx_max = np.argmax(np.abs(ft_prof[1:len(profile)//2])) + 1
349
+ freqs = np.fft.fftfreq(len(profile), d=delta_x)
350
+
351
+ # freqs est en m^-1 car delta_x est en mètres.
352
+ fs_m_inv = abs(freqs[idx_max])
353
+
354
+ # *** CORRECTION 1: Conversion de f_s en mm^-1 (mm^-1 est utilisé dans iRadon) ***
355
+ fs_key = fs_m_inv / 1000.0 # Fréquence spatiale en mm^-1
356
+
357
+
358
+ if fs_key == 0: continue
359
+
360
+ # Calcul de la Phase (Shift)
361
+ phase = get_phase_deterministic(profile)
362
+
363
+ # Stockage par (fs, theta) et phase
364
+ key = (fs_key, angle_rad)
365
+ if key not in structured_buffer:
366
+ structured_buffer[key] = {}
367
+
368
+ # La moyenne est nécessaire si plusieurs acquisitions ont la même phase (pour le SNR)
369
+ if phase in structured_buffer[key]:
370
+ structured_buffer[key][phase] = (structured_buffer[key][phase] + np.array(AOsignal[:,i])) / 2
371
+ else:
372
+ structured_buffer[key][phase] = np.array(AOsignal[:,i])
373
+
291
374
 
375
+
376
+ for (fs, theta), phases in structured_buffer.items():
377
+ s0 = phases.get(0.0, 0)
378
+ s_pi_2 = phases.get(np.pi/2, 0)
379
+ s_pi = phases.get(np.pi, 0)
380
+ s_3pi_2 = phases.get(3*np.pi/2, 0)
381
+
382
+ # Assurer que les zéros sont des vecteurs de la bonne taille
383
+ example = next(val for val in phases.values() if not isinstance(val, int))
384
+ if isinstance(s0, int): s0 = np.zeros_like(example)
385
+ if isinstance(s_pi, int): s_pi = np.zeros_like(example)
386
+ if isinstance(s_pi_2, int): s_pi_2 = np.zeros_like(example)
387
+ if isinstance(s_3pi_2, int): s_3pi_2 = np.zeros_like(example)
388
+
389
+ real = s0 - s_pi
390
+ imag = s_pi_2 - s_3pi_2
391
+
392
+ demodulated_data[(fs, theta)] = (real - 1j * imag) / (2/np.pi)
393
+
394
+ return demodulated_data
AOT_biomaps/__init__.py CHANGED
@@ -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.356'
88
+ __version__ = '2.9.373'
89
89
  __process__ = config.get_process()
90
90
 
91
91
  def initialize(process=None):
@@ -202,6 +202,23 @@ def initialize(process=None):
202
202
 
203
203
 
204
204
 
205
+
206
+
207
+
208
+
209
+
210
+
211
+
212
+
213
+
214
+
215
+
216
+
217
+
218
+
219
+
220
+
221
+
205
222
 
206
223
 
207
224
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: AOT_biomaps
3
- Version: 2.9.356
3
+ Version: 2.9.373
4
4
  Summary: Acousto-Optic Tomography
5
5
  Home-page: https://github.com/LucasDuclos/AcoustoOpticTomography
6
6
  Author: Lucas Duclos
@@ -1,17 +1,17 @@
1
1
  AOT_biomaps/Config.py,sha256=ghEOP1n8aO1pR-su13wMeAZAxZRfry5hH67NbtZ8SqI,3614
2
2
  AOT_biomaps/Settings.py,sha256=v8fPhnvvcfBJP29m1RLOTEr3jndGLGwbUiORXmsj2Bo,2853
3
- AOT_biomaps/__init__.py,sha256=prD0ZZNE6dc-NWf_N0QIPLTrsRD3fAtK78ptZscj3Yo,4374
3
+ AOT_biomaps/__init__.py,sha256=fgpEUPXUQbaCtMxY_uHJtjrQTvQBwltYoAQtHk1zCuM,4408
4
4
  AOT_biomaps/AOT_Acoustic/AcousticEnums.py,sha256=s5kXa6jKzbS4btwbubrVcynLOr0yg5tth5vL_FGfbMk,1802
5
- AOT_biomaps/AOT_Acoustic/AcousticTools.py,sha256=h2sCtGVcDtyLtEF1q7sLZmuWivWmesVGUBPnW-ndQqc,7535
5
+ AOT_biomaps/AOT_Acoustic/AcousticTools.py,sha256=7kuWIIGyzZPQrzRI0zVvdwNUp7qKUE67yCYOMzSb0Ug,8283
6
6
  AOT_biomaps/AOT_Acoustic/FocusedWave.py,sha256=3kGKKDx_3Msy5COYqIwzROPORGWvNjw8UsDanBfkMXE,11037
7
7
  AOT_biomaps/AOT_Acoustic/IrregularWave.py,sha256=yZhtxkR6zlciRcEpdTR0BAhvgQl40XHKFaF8f4VXarE,3035
8
8
  AOT_biomaps/AOT_Acoustic/PlaneWave.py,sha256=xza-rj5AUWDecLkGDxRcULrwZVWeBvGnEP2d51TyR04,1447
9
- AOT_biomaps/AOT_Acoustic/StructuredWave.py,sha256=jTLVlOhYLWJb5MxZPxhq3OFVlz2McoyMPBmfLvnekDU,18209
9
+ AOT_biomaps/AOT_Acoustic/StructuredWave.py,sha256=DRTjD-zrmX12FHrvwOeEo-Rk1fHYm9gfCcebz4WhtXc,18930
10
10
  AOT_biomaps/AOT_Acoustic/__init__.py,sha256=t9M2rRqa_L9pk7W2FeELTkHEMuP4DBr4gBRldMqsQbg,491
11
- AOT_biomaps/AOT_Acoustic/_mainAcoustic.py,sha256=RdmhRF1i0KAlpsP7_wnZ7F4J27br3eUc4XR91Qq7C64,44158
11
+ AOT_biomaps/AOT_Acoustic/_mainAcoustic.py,sha256=M0CKApSCuKiCupj9pf9uOEhuuzJVNSlLvhgTpBQN5Q8,46128
12
12
  AOT_biomaps/AOT_Experiment/ExperimentTools.py,sha256=aFvJw6J_jfFVTDFnG7J3a61SHEgORdZKZS0UI82VMaY,2637
13
13
  AOT_biomaps/AOT_Experiment/Focus.py,sha256=B2nBawmv-NG2AWJx9zgQ8GlN6aFB9FwTSqX-M-phKXg,3193
14
- AOT_biomaps/AOT_Experiment/Tomography.py,sha256=GGbXVUnRhvSRLL1QEg5ZgAhtWIg9RfH7aADJ5u-78HQ,36383
14
+ AOT_biomaps/AOT_Experiment/Tomography.py,sha256=9mJDwV9WVphoX8drL7MgN3WhS6fjYwS6HWQD3x1CrVs,37625
15
15
  AOT_biomaps/AOT_Experiment/__init__.py,sha256=H9zMLeBLA6uhbaHohAa-2u5mDDxqJi8oE5c6tShdQp8,308
16
16
  AOT_biomaps/AOT_Experiment/_mainExperiment.py,sha256=zSfuNrsz7nhiKrGIdK6CAXjlI2T6qYC5-JXHFgPNzhc,24674
17
17
  AOT_biomaps/AOT_Optic/Absorber.py,sha256=jEodzRy7gkEH-wbazVasRQiri0dU16BfapmR-qnTSvM,867
@@ -21,14 +21,14 @@ AOT_biomaps/AOT_Optic/__init__.py,sha256=HSUVhfz0NzwHHZZ9KP9Xyfu33IgP_rYJX86J-gE
21
21
  AOT_biomaps/AOT_Optic/_mainOptic.py,sha256=Wk63CcgWbU-ygMfjNK80islaUbGGJpTXgZY3_C2KQNY,8179
22
22
  AOT_biomaps/AOT_Recon/AOT_biomaps_kernels.cubin,sha256=JWy-bdtBTZdnNlDbJGZKwXyF-2u1wICtmlOC_YxEL6o,82528
23
23
  AOT_biomaps/AOT_Recon/AlgebraicRecon.py,sha256=CGBXZyYEZ3TOTFOKSt-h7NGuFbuI9PNr3YTWTbSLxDo,46832
24
- AOT_biomaps/AOT_Recon/AnalyticRecon.py,sha256=uZp4Va9z04dG7neZFGAmmAUygs8JyfrDuOr1_lKIERc,16651
24
+ AOT_biomaps/AOT_Recon/AnalyticRecon.py,sha256=z4VJtqL-atoZLflG7vQKHNeTXXjhYG_us8l0xfo5XsU,14514
25
25
  AOT_biomaps/AOT_Recon/BayesianRecon.py,sha256=RnnPa-tTcvirwiNPnCRZnSM4NWeEEltYET-piBbp34g,12671
26
26
  AOT_biomaps/AOT_Recon/DeepLearningRecon.py,sha256=RfVcEsi4GeGqJn0_SPxwQPQx6IQjin79WKh2UarMRLI,1383
27
27
  AOT_biomaps/AOT_Recon/PrimalDualRecon.py,sha256=JbFhxiyUoSTnlJgHbOWIfUUwhwfZoi39RJMnfkagegY,16504
28
28
  AOT_biomaps/AOT_Recon/ReconEnums.py,sha256=KAf55RqHAr2ilt6pxFrUBGQOn-7HA8NP6TyL-1FNiXo,19714
29
29
  AOT_biomaps/AOT_Recon/ReconTools.py,sha256=CV2BwdEwvNd3B02G5LYoKsRGlONwIupuv617S2AOWZE,25322
30
30
  AOT_biomaps/AOT_Recon/__init__.py,sha256=xs_argJqXKFl76xP7-jiUc1ynOEEtY7XZ0gDxD5uVZc,246
31
- AOT_biomaps/AOT_Recon/_mainRecon.py,sha256=MvDnfsiJ7v-UmtCFmA1vmKGzV9zd2xdnt27CAWBubko,13470
31
+ AOT_biomaps/AOT_Recon/_mainRecon.py,sha256=hubU5SWEeAv94k_DNbdze4wfUeBw-4smboqRyzlmy_k,18058
32
32
  AOT_biomaps/AOT_Recon/AOT_Optimizers/DEPIERRO.py,sha256=qA1n722GLQJH3V8HcLr5q_GxEwBS_NRlIT3E6JZk-Ag,9479
33
33
  AOT_biomaps/AOT_Recon/AOT_Optimizers/LS.py,sha256=bCu1rKzFXPbYQ7jV3L3E_jVQpb6LIEC5MIlN1-mCNdY,22814
34
34
  AOT_biomaps/AOT_Recon/AOT_Optimizers/MAPEM.py,sha256=vQLCB0L4FSXJKn2_6kdIdWrI6WZ82KuqUh7CSqBGVuo,25766
@@ -42,7 +42,7 @@ AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/__init__.py,sha256=RwrJdLOFbAFBFnRx
42
42
  AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/SparseSMatrix_CSR.py,sha256=RACc2P5oxmp0uPLAGnNj9mEtAxa_OlepNgCawKij3jI,12062
43
43
  AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/SparseSMatrix_SELL.py,sha256=ti3dZQsb_Uu62C7Bn65Z-yf-R5NKCFsmnBT5GlLd_HY,15138
44
44
  AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/__init__.py,sha256=8nou-hqjQjuCTLhoL5qv4EM_lMPFviAZAZKSPhi84jE,67
45
- aot_biomaps-2.9.356.dist-info/METADATA,sha256=eiJaF8-8bnGyUaK14BMcAiv645TibZb5uk8thsji2bE,700
46
- aot_biomaps-2.9.356.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
- aot_biomaps-2.9.356.dist-info/top_level.txt,sha256=6STF-lT4kaAnBHJYCripmN5mZABoHjMuY689JdiDphk,12
48
- aot_biomaps-2.9.356.dist-info/RECORD,,
45
+ aot_biomaps-2.9.373.dist-info/METADATA,sha256=pJCjnmUoIHGqV_s_popa7BNlrTn4LPP3h7mxRqlUiTQ,700
46
+ aot_biomaps-2.9.373.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
47
+ aot_biomaps-2.9.373.dist-info/top_level.txt,sha256=6STF-lT4kaAnBHJYCripmN5mZABoHjMuY689JdiDphk,12
48
+ aot_biomaps-2.9.373.dist-info/RECORD,,