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.

@@ -1,108 +1,23 @@
1
1
  from ._mainRecon import Recon
2
2
  from .ReconEnums import ReconType, AnalyticType, ProcessType
3
3
  from AOT_biomaps.AOT_Experiment.Tomography import hex_to_binary_profile
4
- from .ReconTools import get_phase_deterministic
4
+ from .ReconTools import fourierz_gpu, get_phase_deterministic, add_sincos_cpu, EvalDelayLawOS_center, ifourierx_gpu, rotate_theta_gpu, filter_radon_gpu, ifourierz_gpu
5
5
 
6
6
  import numpy as np
7
7
  from tqdm import trange
8
- import torch
9
- import tqdm
8
+ import cupy as cp
10
9
 
11
10
 
12
11
  class AnalyticRecon(Recon):
13
- def __init__(self, analyticType, **kwargs):
12
+ def __init__(self, analyticType, Lc = None,**kwargs):
14
13
  super().__init__(**kwargs)
15
14
  self.reconType = ReconType.Analytic
16
15
  self.analyticType = analyticType
16
+ if self.analyticType == AnalyticType.iRADON and Lc is None:
17
+ raise ValueError("Lc parameter must be provided for iRADON analytic reconstruction.")
18
+ self.Lc = Lc # in meters
17
19
  self.AOsignal_demoldulated = None
18
20
 
19
-
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,109 +41,336 @@ 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
+
49
+ d_t = 1 / float(self.experiment.params.acoustic['f_saving'])
50
+ t_array = np.arange(0, AOsignal.shape[0])*d_t
51
+ Z = t_array * self.experiment.params.acoustic['c0']
52
+ X_m = np.arange(0, self.experiment.params.acoustic['num_elements'])* self.experiment.params.general['dx']
53
+ dfX = 1 / (X_m[1] - X_m[0]) / len(X_m)
129
54
  if withTumor:
130
55
  self.AOsignal_demoldulated = self.parse_and_demodulate(withTumor=True)
131
56
  if self.analyticType == AnalyticType.iFOURIER:
132
- self.reconPhantom = self._iFourierRecon(self.experiment.AOsignal_withTumor)
57
+ self.reconPhantom = self._iFourierRecon(
58
+ R = AOsignal,
59
+ z = Z,
60
+ X_m=X_m,
61
+ theta=self.experiment.theta,
62
+ decimation=self.experiment.decimations,
63
+ c=self.experiment.params.acoustic['c0'],
64
+ DelayLAWS=self.experiment.DelayLaw,
65
+ ActiveLIST=self.experiment.ActiveList,
66
+ withTumor=True,
67
+ )
68
+
133
69
  elif self.analyticType == AnalyticType.iRADON:
134
- self.reconPhantom = self._iRadonRecon(self.experiment.AOsignal_withTumor)
70
+ self.reconPhantom = self._iRadonRecon(
71
+ R=AOsignal,
72
+ z=Z,
73
+ X_m=X_m,
74
+ theta=self.experiment.theta,
75
+ decimation=self.experiment.decimations,
76
+ df0x=dfX,
77
+ Lc =self.Lc,
78
+ c=self.experiment.params.acoustic['c0'],
79
+ DelayLAWS=self.experiment.DelayLaw,
80
+ ActiveLIST=self.experiment.ActiveList,
81
+ withTumor=True)
135
82
  else:
136
83
  raise ValueError(f"Unknown analytic type: {self.analyticType}")
137
84
  else:
138
85
  self.AOsignal_demoldulated = self.parse_and_demodulate(withTumor=False)
139
86
  if self.analyticType == AnalyticType.iFOURIER:
140
- self.reconLaser = self._iFourierRecon(self.experiment.AOsignal_withoutTumor)
87
+ self.reconLaser = self._iFourierRecon(
88
+ R = AOsignal ,
89
+ z = Z,
90
+ X_m=X_m,
91
+ theta=self.experiment.theta,
92
+ decimation=self.experiment.decimations,
93
+ c=self.experiment.params.acoustic['c0'],
94
+ DelayLAWS=self.experiment.DelayLaw,
95
+ ActiveLIST=self.experiment.ActiveList,
96
+ withTumor=False,
97
+ )
141
98
  elif self.analyticType == AnalyticType.iRADON:
142
- self.reconLaser = self._iRadonRecon(self.experiment.AOsignal_withoutTumor)
99
+ self.reconLaser = self._iRadonRecon(
100
+ R=AOsignal ,
101
+ z=Z,
102
+ X_m=X_m,
103
+ theta=self.experiment.theta,
104
+ decimation=self.experiment.decimations,
105
+ df0x=dfX,
106
+ Lc = self.Lc,
107
+ c=self.experiment.params.acoustic['c0'],
108
+ DelayLAWS=self.experiment.DelayLaw,
109
+ ActiveLIST=self.experiment.ActiveList,
110
+ withTumor=False)
143
111
  else:
144
112
  raise ValueError(f"Unknown analytic type: {self.analyticType}")
145
113
 
146
- def _iFourierRecon(self, AOsignal):
114
+ def _iFourierRecon(
115
+ self,
116
+ R,
117
+ z,
118
+ X_m,
119
+ theta,
120
+ decimation,
121
+ c,
122
+ DelayLAWS,
123
+ ActiveLIST,
124
+ withTumor,
125
+ ):
147
126
  """
148
- Reconstruction d'image utilisant la transformation de Fourier inverse.
149
- :param AOsignal: Signal dans le domaine temporel (shape: N_t, N_theta).
150
- :return: Image reconstruite dans le domaine spatial.
127
+ Reconstruction d'image utilisant la méthode iFourier (GPU).
128
+ Normalisation physique complète incluse.
151
129
  """
152
- theta = np.array([af.angle for af in self.experiment.AcousticFields])
153
- f_s = np.array([af.f_s for af in self.experiment.AcousticFields])
154
- dt = self.experiment.dt
155
- f_t = np.fft.fftfreq(AOsignal.shape[0], d=dt) # fréquences temporelles
156
- x = self.experiment.OpticImage.laser.x
157
- z = self.experiment.OpticImage.laser.z
158
- X, Z = np.meshgrid(x, z, indexing='ij') # grille spatiale (Nx, Nz)
159
-
160
- # Transformée de Fourier du signal
161
- s_tilde = np.fft.fft(AOsignal, axis=0) # shape: (N_t, N_theta)
162
-
163
- # Initialisation de l'image reconstruite
164
- I_rec = np.zeros((len(x), len(z)), dtype=complex)
165
-
166
- # Boucle sur les angles
167
- for i, th in enumerate(trange(len(theta), desc="AOT-BioMaps -- iFourier Reconstruction")):
168
- # Coordonnées tournées
169
- X_prime = X * np.cos(th) + Z * np.sin(th)
170
- Z_prime = -X * np.sin(th) + Z * np.cos(th)
171
-
172
- # Pour chaque fréquence temporelle f_t[j]
173
- for j in range(len(f_t)):
174
- # Phase: exp(2jπ (X_prime * f_s[i] + Z_prime * f_t[j]))
175
- phase = 2j * np.pi * (X_prime * f_s[i] + Z_prime * f_t[j])
176
- # Contribution de cette fréquence
177
- I_rec += s_tilde[j, i] * np.exp(phase) * dt # Pondération par dt pour l'intégration
178
-
179
- # Normalisation
180
- I_rec /= len(theta)
181
- return np.abs(I_rec)
182
-
183
-
184
- def _iRadonRecon(self, AOsignal):
130
+
131
+ # ======================================================
132
+ # 1. Préparation GPU
133
+ # ======================================================
134
+ R = cp.asarray(R)
135
+ z = cp.asarray(z)
136
+ X_m = cp.asarray(X_m)
137
+ theta = cp.asarray(theta)
138
+ decimation = cp.asarray(decimation)
139
+ DelayLAWS = cp.asarray(DelayLAWS)
140
+ ActiveLIST = cp.asarray(ActiveLIST)
141
+
142
+ # Normalisation DelayLAWS (ms -> s si nécessaire)
143
+ DelayLAWS_s = cp.where(cp.max(DelayLAWS) > 1e-3, DelayLAWS / 1000.0, DelayLAWS)
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)
148
+ ia = cp.asarray(ia_cpu)
149
+ ib = cp.asarray(ib_cpu)
150
+
151
+ # ======================================================
152
+ # 2. Structuration complexe
153
+ # ======================================================
154
+ F_complex_cpu, theta_u_cpu, decim_u_cpu = add_sincos_cpu(
155
+ cp.asnumpy(R),
156
+ cp.asnumpy(decimation),
157
+ np.radians(cp.asnumpy(theta))
158
+ )
159
+
160
+ # Calcul des centres de rotation
161
+ M0 = EvalDelayLawOS_center(
162
+ X_m,
163
+ theta_u_cpu,
164
+ DelayLAWS_s.T[:, ia],
165
+ ActiveLIST.T[:, ia],
166
+ c
167
+ )
168
+
169
+ # Transfert GPU
170
+ F_complex = cp.asarray(F_complex_cpu)
171
+ theta_u = cp.asarray(theta_u_cpu)
172
+ decim_u = cp.asarray(decim_u_cpu)
173
+ M0_gpu = cp.asarray(M0)
174
+
175
+ # ======================================================
176
+ # 3. Paramètres de la grille
177
+ # ======================================================
178
+ Nz = z.size
179
+ Nx = X_m.size
180
+ dx = X_m[1] - X_m[0]
181
+ X_grid, Z_grid = cp.meshgrid(X_m, z)
182
+ idx0_x = Nx // 2
183
+
184
+ # Angles uniques
185
+ angles_group, ia_u, ib_u = cp.unique(theta_u, return_index=True, return_inverse=True)
186
+ Ntheta = angles_group.size
187
+
188
+ # Initialisation reconstruction
189
+ I_final = cp.zeros((Nz, Nx), dtype=cp.complex64)
190
+
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
+ ):
199
+
200
+ # Grille Fourier locale (z, fx)
201
+ F_fx_z = cp.zeros((Nz, Nx), dtype=cp.complex64)
202
+
203
+ # Indices correspondant à cet angle
204
+ indices = cp.where(ib_u == i_ang)[0]
205
+
206
+ for idx in indices:
207
+ n = int(decim_u[idx])
208
+ trace_z = F_complex[:, idx]
209
+
210
+ # Mapping positif
211
+ ip = idx0_x + n
212
+ if 0 <= ip < Nx:
213
+ F_fx_z[:, ip] = trace_z
214
+
215
+ # Mapping négatif (symétrie hermitienne MATLAB)
216
+ if n != 0:
217
+ im = idx0_x - n
218
+ if 0 <= im < Nx:
219
+ col_conj = cp.zeros(Nz, dtype=cp.complex64)
220
+ col_conj[1:] = cp.conj(trace_z[:-1])
221
+ F_fx_z[:, im] = col_conj
222
+
223
+ # Correction DC
224
+ F_fx_z[:, idx0_x] *= 0.5
225
+
226
+ # Inverse Fourier X (GPU) + facteur Nx pour correspondance MATLAB
227
+ I_spatial = ifourierx_gpu(F_fx_z, dx) * Nx
228
+
229
+ # Rotation spatiale autour du centre M0
230
+ I_rot = rotate_theta_gpu(
231
+ X_grid,
232
+ Z_grid,
233
+ I_spatial,
234
+ -angles_group[i_ang],
235
+ M0_gpu[i_ang, :]
236
+ )
237
+
238
+ # Somme incohérente
239
+ I_final += I_rot
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
+
250
+ return cp.real(I_final).get()
251
+
252
+ def _iRadonRecon(
253
+ self,
254
+ R,
255
+ z,
256
+ X_m,
257
+ theta,
258
+ decimation,
259
+ df0x,
260
+ Lc,
261
+ c,
262
+ DelayLAWS,
263
+ ActiveLIST,
264
+ withTumor,
265
+ ):
185
266
  """
186
267
  Reconstruction d'image utilisant la méthode iRadon.
187
-
188
- :return: Image reconstruite.
268
+ Normalisation physique correcte (phases, angles, dz).
189
269
  """
190
- @staticmethod
191
- def trapz(y, x):
192
- """Compute the trapezoidal rule for integration."""
193
- return np.sum((y[:-1] + y[1:]) * (x[1:] - x[:-1]) / 2)
194
-
195
- # Initialisation de l'image reconstruite
196
- I_rec = np.zeros((len(self.experiment.OpticImage.laser.x), len(self.experiment.OpticImage.laser.z)), dtype=complex)
197
-
198
- # Transformation de Fourier du signal
199
- s_tilde = np.fft.fft(AOsignal, axis=0)
200
-
201
- # Extraction des angles et des fréquences spatiales
202
- theta = [acoustic_field.angle for acoustic_field in self.experiment.AcousticFields]
203
- f_s = [acoustic_field.f_s for acoustic_field in self.experiment.AcousticFields]
204
-
205
- # Calcul des coordonnées transformées et intégrales
206
- with trange(len(theta) * 2, desc="AOT-BioMaps -- Analytic Reconstruction Tomography: iRadon") as pbar:
207
- for i in range(len(theta)):
208
- pbar.set_description("AOT-BioMaps -- Analytic Reconstruction Tomography: iRadon (Processing frequency contributions) ---- processing on single CPU ----")
209
- th = theta[i]
210
- x_prime = self.experiment.OpticImage.x[:, np.newaxis] * np.cos(th) - self.experiment.OpticImage.z[np.newaxis, :] * np.sin(th)
211
- z_prime = self.experiment.OpticImage.z[np.newaxis, :] * np.cos(th) + self.experiment.OpticImage.x[:, np.newaxis] * np.sin(th)
212
-
213
- # Première intégrale : partie réelle
214
- for j in range(len(f_s)):
215
- fs = f_s[j]
216
- integrand = s_tilde[i, j] * np.exp(2j * np.pi * (x_prime * fs + z_prime * fs))
217
- integral = self.trapz(integrand * fs, fs)
218
- I_rec += 2 * np.real(integral)
219
- pbar.update(1)
220
-
221
- for i in range(len(theta)):
222
- pbar.set_description("AOT-BioMaps -- Analytic Reconstruction Tomography: iRadon (Processing central contributions) ---- processing on single CPU ----")
223
- th = theta[i]
224
- x_prime = self.experiment.OpticImage.x[:, np.newaxis] * np.cos(th) - self.experiment.OpticImage.z[np.newaxis, :] * np.sin(th)
225
- z_prime = self.experiment.OpticImage.z[np.newaxis, :] * np.cos(th) + self.experiment.OpticImage.x[:, np.newaxis] * np.sin(th)
226
-
227
- # Filtrer les fréquences spatiales pour ne garder que celles inférieures ou égales à f_s_max
228
- filtered_f_s = np.array([fs for fs in f_s if fs <= self.f_s_max])
229
- integrand = s_tilde[i, np.where(np.array(f_s) == 0)[0][0]] * np.exp(2j * np.pi * z_prime * filtered_f_s)
230
- integral = self.trapz(integrand * filtered_f_s, filtered_f_s)
231
- I_rec += integral
232
- pbar.update(1)
233
-
234
- return np.abs(I_rec)
270
+
271
+ # ======================================================
272
+ # 1. AddSinCos (structuration) CPU volontairement
273
+ # ======================================================
274
+ theta = np.radians(theta)
275
+ F_ct_kx, theta_u, decim_u = add_sincos_cpu(R, decimation, theta)
276
+
277
+ ScanParam = np.stack([decimation, theta], axis=1)
278
+ _, ia, _ = np.unique(ScanParam, axis=0, return_index=True, return_inverse=True)
279
+
280
+ ActiveLIST = np.asarray(ActiveLIST).T
281
+ DelayLAWS = np.asarray(DelayLAWS).T
282
+ ActiveLIST_unique = ActiveLIST[:, ia]
283
+
284
+ # ======================================================
285
+ # 2. FFT z
286
+ # ======================================================
287
+ z_gpu = cp.asarray(z)
288
+ Fin = fourierz_gpu(z, F_ct_kx)
289
+
290
+ dz = float(z[1] - z[0]) # <<< Δz PHYSIQUE
291
+ fz = cp.fft.fftshift(cp.fft.fftfreq(len(z), d=dz))
292
+
293
+ Nz, Nk = Fin.shape
294
+
295
+ # ======================================================
296
+ # 3. Filtrage OS exact
297
+ # ======================================================
298
+ decim_gpu = cp.asarray(decim_u)
299
+ I0 = decim_gpu == 0
300
+ F0 = Fin * I0[None, :]
301
+
302
+ DEC, FZ = cp.meshgrid(decim_gpu, fz)
303
+
304
+ Hinf = cp.abs(FZ) < cp.abs(DEC) * df0x
305
+ Hsup = FZ >= 0
306
+
307
+ Fc = 1 / Lc
308
+ FILTER = filter_radon_gpu(fz, Fc)[:, None]
309
+
310
+ Finf = F0 * FILTER[:, :F0.shape[1]] * Hinf[:, :F0.shape[1]]
311
+ Fsup = Fin * FILTER * Hsup
312
+
313
+ # ======================================================
314
+ # 4. Retour espace z
315
+ # ======================================================
316
+ Finf = ifourierz_gpu(z, Finf)
317
+ Fsup = ifourierz_gpu(z, Fsup)
318
+
319
+ # ======================================================
320
+ # 5. Grille image
321
+ # ======================================================
322
+ X_gpu = cp.asarray(X_m)
323
+ X, Z = cp.meshgrid(X_gpu, z_gpu)
324
+ Xc = float(np.mean(X_m))
325
+
326
+ # ======================================================
327
+ # 6. Centre de rotation M0
328
+ # ======================================================
329
+ M0 = EvalDelayLawOS_center(X_m, theta, DelayLAWS[:, ia], ActiveLIST_unique, c)
330
+ M0_gpu = cp.asarray(M0)
331
+
332
+ # ======================================================
333
+ # 7. Rétroprojection
334
+ # ======================================================
335
+ Irec = cp.zeros_like(X, dtype=cp.complex64)
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
+ ):
342
+ th = float(theta_u[i])
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
350
+ i0 = cp.floor(Tind).astype(cp.int32)
351
+ i1 = i0 + 1
352
+ i0 = cp.clip(i0, 0, Nz - 1)
353
+ i1 = cp.clip(i1, 0, Nz - 1)
354
+ w = Tind - i0
355
+
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)
367
+
368
+ # nombre de tirs complexes indépendants (4 phases)
369
+ Ntirs_complex = (R.shape[1] - Ntheta) / 4.0
370
+
371
+ # normalisation finale
372
+ Irec /= (Ntheta * Ntirs_complex)
373
+ print(f"dz normalization: {dz}")
374
+ Irec *= dz
375
+
376
+ return cp.real(Irec).get()