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

Files changed (50) hide show
  1. AOT_biomaps/AOT_Acoustic/AcousticEnums.py +64 -0
  2. AOT_biomaps/AOT_Acoustic/AcousticTools.py +221 -0
  3. AOT_biomaps/AOT_Acoustic/FocusedWave.py +244 -0
  4. AOT_biomaps/AOT_Acoustic/IrregularWave.py +66 -0
  5. AOT_biomaps/AOT_Acoustic/PlaneWave.py +43 -0
  6. AOT_biomaps/AOT_Acoustic/StructuredWave.py +392 -0
  7. AOT_biomaps/AOT_Acoustic/__init__.py +15 -0
  8. AOT_biomaps/AOT_Acoustic/_mainAcoustic.py +978 -0
  9. AOT_biomaps/AOT_Experiment/Focus.py +55 -0
  10. AOT_biomaps/AOT_Experiment/Tomography.py +505 -0
  11. AOT_biomaps/AOT_Experiment/__init__.py +9 -0
  12. AOT_biomaps/AOT_Experiment/_mainExperiment.py +532 -0
  13. AOT_biomaps/AOT_Optic/Absorber.py +24 -0
  14. AOT_biomaps/AOT_Optic/Laser.py +70 -0
  15. AOT_biomaps/AOT_Optic/OpticEnums.py +17 -0
  16. AOT_biomaps/AOT_Optic/__init__.py +10 -0
  17. AOT_biomaps/AOT_Optic/_mainOptic.py +204 -0
  18. AOT_biomaps/AOT_Recon/AOT_Optimizers/DEPIERRO.py +191 -0
  19. AOT_biomaps/AOT_Recon/AOT_Optimizers/LS.py +106 -0
  20. AOT_biomaps/AOT_Recon/AOT_Optimizers/MAPEM.py +456 -0
  21. AOT_biomaps/AOT_Recon/AOT_Optimizers/MLEM.py +333 -0
  22. AOT_biomaps/AOT_Recon/AOT_Optimizers/PDHG.py +221 -0
  23. AOT_biomaps/AOT_Recon/AOT_Optimizers/__init__.py +5 -0
  24. AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Huber.py +90 -0
  25. AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Quadratic.py +86 -0
  26. AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/RelativeDifferences.py +59 -0
  27. AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/__init__.py +3 -0
  28. AOT_biomaps/AOT_Recon/AlgebraicRecon.py +1023 -0
  29. AOT_biomaps/AOT_Recon/AnalyticRecon.py +154 -0
  30. AOT_biomaps/AOT_Recon/BayesianRecon.py +230 -0
  31. AOT_biomaps/AOT_Recon/DeepLearningRecon.py +35 -0
  32. AOT_biomaps/AOT_Recon/PrimalDualRecon.py +210 -0
  33. AOT_biomaps/AOT_Recon/ReconEnums.py +375 -0
  34. AOT_biomaps/AOT_Recon/ReconTools.py +273 -0
  35. AOT_biomaps/AOT_Recon/__init__.py +11 -0
  36. AOT_biomaps/AOT_Recon/_mainRecon.py +288 -0
  37. AOT_biomaps/Config.py +95 -0
  38. AOT_biomaps/Settings.py +45 -13
  39. AOT_biomaps/__init__.py +271 -18
  40. aot_biomaps-2.9.233.dist-info/METADATA +22 -0
  41. aot_biomaps-2.9.233.dist-info/RECORD +43 -0
  42. {AOT_biomaps-2.1.3.dist-info → aot_biomaps-2.9.233.dist-info}/WHEEL +1 -1
  43. AOT_biomaps/AOT_Acoustic.py +0 -1881
  44. AOT_biomaps/AOT_Experiment.py +0 -541
  45. AOT_biomaps/AOT_Optic.py +0 -219
  46. AOT_biomaps/AOT_Reconstruction.py +0 -1416
  47. AOT_biomaps/config.py +0 -54
  48. AOT_biomaps-2.1.3.dist-info/METADATA +0 -20
  49. AOT_biomaps-2.1.3.dist-info/RECORD +0 -11
  50. {AOT_biomaps-2.1.3.dist-info → aot_biomaps-2.9.233.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1023 @@
1
+ from ._mainRecon import Recon
2
+ from .ReconEnums import ReconType, OptimizerType, ProcessType, SparsingType
3
+ from .AOT_Optimizers import MLEM, LS
4
+ from AOT_biomaps.Config import config
5
+
6
+ import os
7
+ import subprocess
8
+ import numpy as np
9
+ import matplotlib.pyplot as plt
10
+ import matplotlib.animation as animation
11
+ from IPython.display import HTML
12
+ from datetime import datetime
13
+ from tempfile import gettempdir
14
+ import cupy as cp
15
+ import cupyx.scipy.sparse as cpsparse
16
+ import gc
17
+ from tqdm import trange
18
+
19
+ class AlgebraicRecon(Recon):
20
+ """
21
+ This class implements the Algebraic reconstruction process.
22
+ It currently does not perform any operations but serves as a template for future implementations.
23
+ """
24
+ def __init__(self, opti = OptimizerType.MLEM, numIterations = 10000, numSubsets = 1, isSavingEachIteration=True, maxSaves = 5000, alpha = None, denominatorThreshold = 1e-6, useSparseSMatrix=True, sparseType = SparsingType.CSR, sparseThreshold=0.1, device = None, **kwargs):
25
+ super().__init__(**kwargs)
26
+ self.reconType = ReconType.Algebraic
27
+ self.optimizer = opti
28
+ self.reconPhantom = []
29
+ self.reconLaser = []
30
+ self.indices = []
31
+ self.numIterations = numIterations
32
+ self.numSubsets = numSubsets
33
+ self.isSavingEachIteration = isSavingEachIteration
34
+ self.maxSaves = maxSaves
35
+ self.denominatorThreshold = denominatorThreshold
36
+ self.alpha = alpha # Regularization parameter for LS
37
+ self.device = device
38
+ self.SMatrix_sparse = None # Sparse system matrix
39
+ self.sparseThreshold = sparseThreshold
40
+ self.useSparseSMatrix = useSparseSMatrix # Whether to use sparse SMatrix in optimizers
41
+ self.sparseType = sparseType
42
+ self.Z_dim = None # Used for sparse matrix reconstruction
43
+
44
+ if self.numIterations <= 0:
45
+ raise ValueError("Number of iterations must be greater than 0.")
46
+ if self.numSubsets <= 0:
47
+ raise ValueError("Number of subsets must be greater than 0.")
48
+ if type(self.numIterations) is not int:
49
+ raise TypeError("Number of iterations must be an integer.")
50
+ if type(self.numSubsets) is not int:
51
+ raise TypeError("Number of subsets must be an integer.")
52
+
53
+ print("Generating system matrix (processing acoustic fields)...")
54
+ self.SMatrix = self._sparseSMatrix()
55
+
56
+ # PUBLIC METHODS
57
+
58
+ def run(self, processType = ProcessType.PYTHON, withTumor= True, show_logs=True):
59
+ """
60
+ This method is a placeholder for the Algebraic reconstruction process.
61
+ It currently does not perform any operations but serves as a template for future implementations.
62
+ """
63
+ if(processType == ProcessType.CASToR):
64
+ self._AlgebraicReconCASToR(withTumor=withTumor, show_logs=show_logs)
65
+ elif(processType == ProcessType.PYTHON):
66
+ self._AlgebraicReconPython(withTumor=withTumor, show_logs=show_logs)
67
+ else:
68
+ raise ValueError(f"Unknown Algebraic reconstruction type: {processType}")
69
+
70
+ def sparse_SMatrix(self):
71
+ if self.sparseType == SparsingType.CSR:
72
+ self.SMatrix_sparse, self.Z_dim = self._sparseSMatrix_CSR(self.experiment.AcousticFields, threshold=self.sparseThreshold)
73
+ if self.sparseType == SparsingType.COO:
74
+ raise NotImplementedError("COO sparse matrix not implemented yet.")
75
+
76
+ def plot_MSE(self, isSaving=True, log_scale_x=False, log_scale_y=False, show_logs=True):
77
+ """
78
+ Plot the Mean Squared Error (MSE) of the reconstruction.
79
+
80
+ Parameters:
81
+ isSaving: bool, whether to save the plot.
82
+ log_scale_x: bool, if True, use logarithmic scale for the x-axis.
83
+ log_scale_y: bool, if True, use logarithmic scale for the y-axis.
84
+ Returns:
85
+ None
86
+ """
87
+ if not self.MSE:
88
+ raise ValueError("MSE is empty. Please calculate MSE first.")
89
+
90
+ best_idx = self.indices[np.argmin(self.MSE)]
91
+ if show_logs:
92
+ print(f"Lowest MSE = {np.min(self.MSE):.4f} at iteration {best_idx+1}")
93
+ # Plot MSE curve
94
+ plt.figure(figsize=(7, 5))
95
+ plt.plot(self.indices, self.MSE, 'r-', label="MSE curve")
96
+ # Add blue dashed lines
97
+ plt.axhline(np.min(self.MSE), color='blue', linestyle='--', label=f"Min MSE = {np.min(self.MSE):.4f}")
98
+ plt.axvline(best_idx, color='blue', linestyle='--', label=f"Iteration = {best_idx+1}")
99
+ plt.xlabel("Iteration")
100
+ plt.ylabel("MSE")
101
+ plt.title("MSE vs. Iteration")
102
+ if log_scale_x:
103
+ plt.xscale('log')
104
+ if log_scale_y:
105
+ plt.yscale('log')
106
+ plt.legend()
107
+ plt.grid(True, which="both", ls="-")
108
+ plt.tight_layout()
109
+ if isSaving and self.saveDir is not None:
110
+ now = datetime.now()
111
+ date_str = now.strftime("%Y_%d_%m_%y")
112
+ scale_str = ""
113
+ if log_scale_x and log_scale_y:
114
+ scale_str = "_loglog"
115
+ elif log_scale_x:
116
+ scale_str = "_logx"
117
+ elif log_scale_y:
118
+ scale_str = "_logy"
119
+ SavingFolder = os.path.join(self.saveDir, f'{self.SMatrix.shape[3]}_SCANS_MSE_plot_{self.optimizer.name}_{scale_str}{date_str}.png')
120
+ plt.savefig(SavingFolder, dpi=300)
121
+ if show_logs:
122
+ print(f"MSE plot saved to {SavingFolder}")
123
+
124
+ plt.show()
125
+
126
+ def show_MSE_bestRecon(self, isSaving=True, show_logs=True):
127
+ if not self.MSE:
128
+ raise ValueError("MSE is empty. Please calculate MSE first.")
129
+
130
+
131
+ best_idx = np.argmin(self.MSE)
132
+ best_recon = self.reconPhantom[best_idx]
133
+
134
+ # Crée la figure et les axes
135
+ fig, axs = plt.subplots(1, 3, figsize=(15, 5))
136
+
137
+ # Left: Best reconstructed image (normalized)
138
+ im0 = axs[0].imshow(best_recon,
139
+ extent=(self.experiment.params.general['Xrange'][0]*1000, self.experiment.params.general['Xrange'][1]*1000,
140
+ self.experiment.params.general['Zrange'][1]*1000, self.experiment.params.general['Zrange'][0]*1000),
141
+ cmap='hot', aspect='equal', vmin=0, vmax=1)
142
+ axs[0].set_title(f"Min MSE Reconstruction\nIter {self.indices[best_idx]}, MSE={np.min(self.MSE):.4f}")
143
+ axs[0].set_xlabel("x (mm)", fontsize=12)
144
+ axs[0].set_ylabel("z (mm)", fontsize=12)
145
+ axs[0].tick_params(axis='both', which='major', labelsize=8)
146
+
147
+ # Middle: Ground truth (normalized)
148
+ im1 = axs[1].imshow(self.experiment.OpticImage.phantom,
149
+ extent=(self.experiment.params.general['Xrange'][0]*1000, self.experiment.params.general['Xrange'][1]*1000,
150
+ self.experiment.params.general['Zrange'][1]*1000, self.experiment.params.general['Zrange'][0]*1000),
151
+ cmap='hot', aspect='equal', vmin=0, vmax=1)
152
+ axs[1].set_title(r"Ground Truth ($\lambda$)")
153
+ axs[1].set_xlabel("x (mm)", fontsize=12)
154
+ axs[1].set_ylabel("z (mm)", fontsize=12)
155
+ axs[1].tick_params(axis='both', which='major', labelsize=8)
156
+ axs[1].tick_params(axis='y', which='both', left=False, right=False, labelleft=False)
157
+
158
+ # Right: Reconstruction at last iteration
159
+ lastRecon = self.reconPhantom[-1]
160
+ if self.experiment.OpticImage.phantom.shape != lastRecon.shape:
161
+ lastRecon = lastRecon.T
162
+ im2 = axs[2].imshow(lastRecon,
163
+ extent=(self.experiment.params.general['Xrange'][0]*1000, self.experiment.params.general['Xrange'][1]*1000,
164
+ self.experiment.params.general['Zrange'][1]*1000, self.experiment.params.general['Zrange'][0]*1000),
165
+ cmap='hot', aspect='equal', vmin=0, vmax=1)
166
+ axs[2].set_title(f"Last Reconstruction\nIter {self.numIterations * self.numSubsets}, MSE={np.mean((self.experiment.OpticImage.phantom - lastRecon) ** 2):.4f}")
167
+ axs[2].set_xlabel("x (mm)", fontsize=12)
168
+ axs[2].set_ylabel("z (mm)", fontsize=12)
169
+ axs[2].tick_params(axis='both', which='major', labelsize=8)
170
+
171
+ # Ajoute une colorbar horizontale centrée en dessous des trois plots
172
+ fig.subplots_adjust(bottom=0.2)
173
+ cbar_ax = fig.add_axes([0.25, 0.08, 0.5, 0.03])
174
+ cbar = fig.colorbar(im2, cax=cbar_ax, orientation='horizontal')
175
+ cbar.set_label('Normalized Intensity', fontsize=12)
176
+ cbar.ax.tick_params(labelsize=8)
177
+
178
+ plt.subplots_adjust(wspace=0.3)
179
+
180
+ if isSaving and self.saveDir is not None:
181
+ now = datetime.now()
182
+ date_str = now.strftime("%Y_%d_%m_%y")
183
+ savePath = os.path.join(self.saveDir, 'results')
184
+ if not os.path.exists(savePath):
185
+ os.makedirs(savePath)
186
+ SavingFolder = os.path.join(self.saveDir, f'{self.SMatrix.shape[3]}_SCANS_comparison_MSE_BestANDLastRecon_{self.optimizer.name}_{date_str}.png')
187
+ plt.savefig(SavingFolder, dpi=300, bbox_inches='tight')
188
+ if show_logs:
189
+ print(f"MSE plot saved to {SavingFolder}")
190
+
191
+ plt.show()
192
+
193
+ def show_theta_animation(self, vmin=None, vmax=None, total_duration_ms=3000, save_path=None, max_frames=1000, isPropMSE=True, show_logs=True):
194
+ """
195
+ Show theta iteration animation with speed proportional to MSE acceleration.
196
+ In "propMSE" mode: slow down when MSE changes rapidly, speed up when MSE stagnates.
197
+
198
+ Parameters:
199
+ vmin, vmax: color limits (optional)
200
+ total_duration_ms: total duration of the animation in milliseconds
201
+ save_path: path to save animation (e.g., 'theta.gif')
202
+ max_frames: maximum number of frames to include (default: 1000)
203
+ isPropMSE: if True, use adaptive speed based on MSE (default: True)
204
+ """
205
+ import matplotlib as mpl
206
+ mpl.rcParams['animation.embed_limit'] = 200
207
+
208
+ if len(self.reconPhantom) == 0 or len(self.reconPhantom) < 2:
209
+ raise ValueError("Not enough theta matrices available for animation.")
210
+
211
+ if isPropMSE and (self.MSE is None or len(self.MSE) == 0):
212
+ raise ValueError("MSE is empty or not calculated. Please calculate MSE first.")
213
+
214
+ frames = np.array(self.reconPhantom)
215
+ mse = np.array(self.MSE)
216
+
217
+ # Sous-échantillonnage initial
218
+ step = max(1, len(frames) // max_frames)
219
+ frames_subset = frames[::step]
220
+ indices_subset = self.indices[::step]
221
+ mse_subset = mse[::step]
222
+
223
+ if vmin is None:
224
+ vmin = np.min(frames_subset)
225
+ if vmax is None:
226
+ vmax = np.max(frames_subset)
227
+
228
+ fig, ax = plt.subplots(figsize=(4, 4), dpi=100)
229
+ im = ax.imshow(
230
+ frames_subset[0],
231
+ extent=(
232
+ self.experiment.params.general['Xrange'][0],
233
+ self.experiment.params.general['Xrange'][1],
234
+ self.experiment.params.general['Zrange'][1],
235
+ self.experiment.params.general['Zrange'][0]
236
+ ),
237
+ vmin=vmin,
238
+ vmax=vmax,
239
+ aspect='equal',
240
+ cmap='hot'
241
+ )
242
+ title = ax.set_title(f"Iteration {indices_subset[0]}")
243
+ ax.set_xlabel("x (mm)")
244
+ ax.set_ylabel("z (mm)")
245
+ plt.tight_layout()
246
+
247
+ if isPropMSE:
248
+ # Calcule la dérivée première (variation du MSE)
249
+ mse_diff = np.gradient(mse_subset)
250
+ # Calcule la dérivée seconde (accélération du MSE)
251
+ mse_accel = np.gradient(mse_diff)
252
+ # Normalise l'accélération entre 0 et 1 (en valeur absolue)
253
+ mse_accel_normalized = np.abs(mse_accel)
254
+ mse_accel_normalized /= (np.max(mse_accel_normalized) + 1e-10)
255
+
256
+ # Prépare les frames pour le mode "propMSE"
257
+ all_frames = []
258
+ all_indices = []
259
+
260
+ for i in range(len(frames_subset)):
261
+ # Nombre de duplications inversement proportionnel à l'accélération (pour ralentir quand MSE change vite)
262
+ # Plus l'accélération est élevée, plus on duplique (pour ralentir)
263
+ num_duplicates = max(1, int(1 + 9 * mse_accel_normalized[i]))
264
+ all_frames.extend([frames_subset[i]] * num_duplicates)
265
+ all_indices.extend([indices_subset[i]] * num_duplicates)
266
+
267
+ # Ajuste le nombre total de frames pour respecter la durée
268
+ target_frames = int(total_duration_ms / 10) # 10 ms par frame
269
+ if len(all_frames) > target_frames:
270
+ step_prop = len(all_frames) // target_frames
271
+ all_frames = all_frames[::step_prop]
272
+ all_indices = all_indices[::step_prop]
273
+
274
+ else: # Mode "linéaire"
275
+ all_frames = frames_subset
276
+ all_indices = indices_subset
277
+
278
+ def update(frame_idx):
279
+ im.set_array(all_frames[frame_idx])
280
+ title.set_text(f"Iteration {all_indices[frame_idx]}")
281
+ return [im, title]
282
+
283
+ ani = animation.FuncAnimation(
284
+ fig,
285
+ update,
286
+ frames=len(all_frames),
287
+ interval=10, # 10 ms par frame
288
+ blit=False,
289
+ )
290
+
291
+ if save_path:
292
+ if save_path.endswith(".gif"):
293
+ ani.save(save_path, writer=animation.PillowWriter(fps=100))
294
+ elif save_path.endswith(".mp4"):
295
+ ani.save(save_path, writer="ffmpeg", fps=30)
296
+ if show_logs:
297
+ print(f"Animation saved to {save_path}")
298
+
299
+ plt.close(fig)
300
+ return HTML(ani.to_jshtml())
301
+
302
+ def plot_SSIM(self, isSaving=True, log_scale_x=False, log_scale_y=False, show_logs=True):
303
+ if not self.SSIM:
304
+ raise ValueError("SSIM is empty. Please calculate SSIM first.")
305
+
306
+ best_idx = self.indices[np.argmax(self.SSIM)]
307
+ if show_logs:
308
+ print(f"Highest SSIM = {np.max(self.SSIM):.4f} at iteration {best_idx+1}")
309
+ # Plot SSIM curve
310
+ plt.figure(figsize=(7, 5))
311
+ plt.plot(self.indices, self.SSIM, 'r-', label="SSIM curve")
312
+ # Add blue dashed lines
313
+ plt.axhline(np.max(self.SSIM), color='blue', linestyle='--', label=f"Max SSIM = {np.max(self.SSIM):.4f}")
314
+ plt.axvline(best_idx, color='blue', linestyle='--', label=f"Iteration = {best_idx}")
315
+ plt.xlabel("Iteration")
316
+ plt.ylabel("SSIM")
317
+ plt.title("SSIM vs. Iteration")
318
+ if log_scale_x:
319
+ plt.xscale('log')
320
+ if log_scale_y:
321
+ plt.yscale('log')
322
+ plt.legend()
323
+ plt.grid(True, which="both", ls="-")
324
+ plt.tight_layout()
325
+ if isSaving and self.saveDir is not None:
326
+ now = datetime.now()
327
+ date_str = now.strftime("%Y_%d_%m_%y")
328
+ scale_str = ""
329
+ if log_scale_x and log_scale_y:
330
+ scale_str = "_loglog"
331
+ elif log_scale_x:
332
+ scale_str = "_logx"
333
+ elif log_scale_y:
334
+ scale_str = "_logy"
335
+ SavingFolder = os.path.join(self.saveDir, f'{self.SMatrix.shape[3]}_SCANS_SSIM_plot_{self.optimizer.name}_{scale_str}{date_str}.png')
336
+ plt.savefig(SavingFolder, dpi=300)
337
+ if show_logs:
338
+ print(f"SSIM plot saved to {SavingFolder}")
339
+
340
+ plt.show()
341
+
342
+ def show_SSIM_bestRecon(self, isSaving=True, show_logs=True):
343
+
344
+ if not self.SSIM:
345
+ raise ValueError("SSIM is empty. Please calculate SSIM first.")
346
+
347
+ best_idx = np.argmax(self.SSIM)
348
+ best_recon = self.reconPhantom[best_idx]
349
+
350
+ # ----------------- Plotting -----------------
351
+ _, axs = plt.subplots(1, 3, figsize=(15, 5)) # 1 row, 3 columns
352
+
353
+ # Left: Best reconstructed image (normalized)
354
+ im0 = axs[0].imshow(best_recon,
355
+ extent=(self.experiment.params.general['Xrange'][0], self.experiment.params.general['Xrange'][1], self.experiment.params.general['Zrange'][1], self.experiment.params.general['Zrange'][0]),
356
+ cmap='hot', aspect='equal', vmin=0, vmax=1)
357
+ axs[0].set_title(f"Max SSIM Reconstruction\nIter {self.indices[best_idx]}, SSIM={np.min(self.MSE):.4f}")
358
+ axs[0].set_xlabel("x (mm)")
359
+ axs[0].set_ylabel("z (mm)")
360
+ plt.colorbar(im0, ax=axs[0])
361
+
362
+ # Middle: Ground truth (normalized)
363
+ im1 = axs[1].imshow(self.experiment.OpticImage.laser.intensity,
364
+ extent=(self.experiment.params.general['Xrange'][0], self.experiment.params.general['Xrange'][1], self.experiment.params.general['Zrange'][1], self.experiment.params.general['Zrange'][0]),
365
+ cmap='hot', aspect='equal', vmin=0, vmax=1)
366
+ axs[1].set_title(r"Ground Truth ($\lambda$)")
367
+ axs[1].set_xlabel("x (mm)")
368
+ axs[1].set_ylabel("z (mm)")
369
+ plt.colorbar(im1, ax=axs[1])
370
+
371
+ # Right: Reconstruction at iter 350
372
+ lastRecon = self.reconPhantom[-1]
373
+ im2 = axs[2].imshow(lastRecon,
374
+ extent=(self.experiment.params.general['Xrange'][0], self.experiment.params.general['Xrange'][1], self.experiment.params.general['Zrange'][1], self.experiment.params.general['Zrange'][0]),
375
+ cmap='hot', aspect='equal', vmin=0, vmax=1)
376
+ axs[2].set_title(f"Last Reconstruction\nIter {self.numIterations * self.numSubsets}, SSIM={self.SSIM[-1]:.4f}")
377
+ axs[2].set_xlabel("x (mm)")
378
+ axs[2].set_ylabel("z (mm)")
379
+ plt.colorbar(im2, ax=axs[2])
380
+
381
+ plt.tight_layout()
382
+ if isSaving:
383
+ now = datetime.now()
384
+ date_str = now.strftime("%Y_%d_%m_%y")
385
+ SavingFolder = os.path.join(self.saveDir, f'{self.SMatrix.shape[3]}_SCANS_comparison_SSIM_BestANDLastRecon_{self.optimizer.name}_{date_str}.png')
386
+ plt.savefig(SavingFolder, dpi=300)
387
+ if show_logs:
388
+ print(f"SSIM plot saved to {SavingFolder}")
389
+ plt.show()
390
+
391
+ def plot_CRC_vs_Noise(self, use_ROI=True, fin=None, isSaving=True, show_logs=True):
392
+ """
393
+ Plot CRC (Contrast Recovery Coefficient) vs Noise for each iteration.
394
+ """
395
+ if self.reconLaser is None or self.reconLaser == []:
396
+ raise ValueError("Reconstructed laser is empty. Run reconstruction first.")
397
+ if isinstance(self.reconLaser, list) and len(self.reconLaser) == 1:
398
+ raise ValueError("Reconstructed Image without tumor is a single frame. Run reconstruction with isSavingEachIteration=True to get a sequence of frames.")
399
+ if self.reconPhantom is None or self.reconPhantom == []:
400
+ raise ValueError("Reconstructed phantom is empty. Run reconstruction first.")
401
+ if isinstance(self.reconPhantom, list) and len(self.reconPhantom) == 1:
402
+ raise ValueError("Reconstructed Image with tumor is a single frame. Run reconstruction with isSavingEachIteration=True to get a sequence of frames.")
403
+
404
+ if fin is None:
405
+ fin = len(self.reconPhantom) - 1
406
+
407
+ iter_range = self.indices
408
+
409
+ if self.CRC is None:
410
+ self.calculateCRC(use_ROI=use_ROI)
411
+
412
+ noise_values = []
413
+
414
+ for i in iter_range:
415
+ recon_without_tumor = self.reconLaser[i].T
416
+ # Noise
417
+ noise = np.mean(np.abs(recon_without_tumor - self.experiment.OpticImage.laser.intensity))
418
+ noise_values.append(noise)
419
+
420
+ plt.figure(figsize=(6, 5))
421
+ plt.plot(noise_values, self.CRC, 'o-', label=self.optimizer.name)
422
+ for i, (x, y) in zip(iter_range, zip(noise_values, self.CRC)):
423
+ plt.text(x, y, str(i), fontsize=5.5, ha='left', va='bottom')
424
+
425
+ plt.xlabel("Noise (mean absolute error)")
426
+ plt.ylabel("CRC (Contrast Recovery Coefficient)")
427
+
428
+ plt.xscale('log')
429
+ plt.yscale('log')
430
+
431
+ plt.title("CRC vs Noise over Iterations")
432
+ plt.grid(True)
433
+ plt.legend()
434
+ if isSaving:
435
+ now = datetime.now()
436
+ date_str = now.strftime("%Y_%d_%m_%y")
437
+ SavingFolder = os.path.join(self.saveDir, f'{self.SMatrix.shape[3]}_SCANS_CRCvsNOISE_{self.optimizer.name}_{date_str}.png')
438
+ plt.savefig(SavingFolder, dpi=300)
439
+ if show_logs:
440
+ print(f"CRCvsNOISE plot saved to {SavingFolder}")
441
+ plt.show()
442
+
443
+ def show_reconstruction_progress(self, start=0, fin=None, save_path=None, with_tumor=True, show_logs=True):
444
+ """
445
+ Show the reconstruction progress for either with or without tumor.
446
+ If isPropMSE is True, the frame selection is adapted to MSE changes.
447
+ Otherwise, indices are evenly spaced between start and fin.
448
+
449
+ Parameters:
450
+ start: int, starting iteration index
451
+ fin: int, ending iteration index (inclusive)
452
+ duration: int, duration of the animation in milliseconds
453
+ save_path: str, path to save the figure (optional)
454
+ with_tumor: bool, if True, show reconstruction with tumor; else without (default: True)
455
+ isPropMSE: bool, if True, use adaptive speed based on MSE (default: True)
456
+ """
457
+ import matplotlib as mpl
458
+ mpl.rcParams['animation.embed_limit'] = 200
459
+
460
+ if fin is None:
461
+ fin = len(self.reconPhantom) - 1 if with_tumor else len(self.reconLaser) - 1
462
+
463
+ # Check data availability
464
+ if with_tumor:
465
+ if self.reconPhantom is None or self.reconPhantom == []:
466
+ raise ValueError("Reconstructed phantom is empty. Run reconstruction first.")
467
+ if isinstance(self.reconPhantom, list) and len(self.reconPhantom) == 1:
468
+ raise ValueError("Reconstructed Image with tumor is a single frame. Run reconstruction with isSavingEachIteration=True.")
469
+ recon_list = self.reconPhantom
470
+ ground_truth = self.experiment.OpticImage.phantom
471
+ title_suffix = "with_tumor"
472
+ else:
473
+ if self.reconLaser is None or self.reconLaser == []:
474
+ raise ValueError("Reconstructed laser is empty. Run reconstruction first.")
475
+ if isinstance(self.reconLaser, list) and len(self.reconLaser) == 1:
476
+ raise ValueError("Reconstructed Image without tumor is a single frame. Run reconstruction with isSavingEachIteration=True.")
477
+ recon_list = self.reconLaser
478
+ ground_truth = self.experiment.OpticImage.laser.intensity
479
+ title_suffix = "without_tumor"
480
+
481
+ # Collect data for all iterations
482
+ recon_list_data = []
483
+ diff_abs_list = []
484
+ mse_list = []
485
+ noise_list = []
486
+
487
+ for i in range(start, fin + 1):
488
+ recon = recon_list[i]
489
+ diff_abs = np.abs(recon - ground_truth)
490
+ mse = np.mean((ground_truth.flatten() - recon.flatten())**2)
491
+ noise = np.mean(np.abs(recon - ground_truth))
492
+
493
+ recon_list_data.append(recon)
494
+ diff_abs_list.append(diff_abs)
495
+ mse_list.append(mse)
496
+ noise_list.append(noise)
497
+
498
+ # Calculate global min/max for difference images
499
+ global_min_diff = np.min([d.min() for d in diff_abs_list[1:]])
500
+ global_max_diff = np.max([d.max() for d in diff_abs_list[1:]])
501
+
502
+ # Evenly spaced indices
503
+ num_frames = min(5, fin - start + 1)
504
+ all_indices = np.linspace(start, fin, num_frames, dtype=int).tolist()
505
+
506
+ # Plot
507
+ nrows = min(5, len(all_indices))
508
+ ncols = 3 # Recon, |Recon - GT|, Ground Truth
509
+ vmin, vmax = 0, 1
510
+
511
+ fig, axs = plt.subplots(nrows=nrows, ncols=ncols, figsize=(12, 3 * nrows))
512
+
513
+ for i, iter_idx in enumerate(all_indices[:nrows]):
514
+ idx_in_list = iter_idx - start # Index in the collected data lists
515
+ recon = recon_list_data[idx_in_list]
516
+ diff_abs = diff_abs_list[idx_in_list]
517
+ mse_val = mse_list[idx_in_list]
518
+ noise = noise_list[idx_in_list]
519
+
520
+ im0 = axs[i, 0].imshow(recon, cmap='hot', vmin=vmin, vmax=vmax, aspect='equal')
521
+ axs[i, 0].set_title(f"Reconstruction\nIter {self.indices[iter_idx]}, MSE={mse_val:.2e}", fontsize=10)
522
+ axs[i, 0].axis('off')
523
+ plt.colorbar(im0, ax=axs[i, 0])
524
+
525
+ im1 = axs[i, 1].imshow(diff_abs, cmap='viridis',
526
+ vmin=global_min_diff,
527
+ vmax=global_max_diff,
528
+ aspect='equal')
529
+ axs[i, 1].set_title(f"|Recon - Ground Truth|\nNoise={noise:.2e}", fontsize=10)
530
+ axs[i, 1].axis('off')
531
+ plt.colorbar(im1, ax=axs[i, 1])
532
+
533
+ im2 = axs[i, 2].imshow(ground_truth, cmap='hot', vmin=vmin, vmax=vmax, aspect='equal')
534
+ axs[i, 2].set_title(r"Ground Truth", fontsize=10)
535
+ axs[i, 2].axis('off')
536
+ plt.colorbar(im2, ax=axs[i, 2])
537
+
538
+ plt.tight_layout()
539
+
540
+ if save_path:
541
+ # Add suffix to filename based on with_tumor parameter
542
+ if '.' in save_path:
543
+ name, ext = save_path.rsplit('.', 1)
544
+ save_path = f"{name}_{title_suffix}.{ext}"
545
+ else:
546
+ save_path = f"{save_path}_{title_suffix}"
547
+ plt.savefig(save_path, dpi=300)
548
+ if show_logs:
549
+ print(f"Figure saved to: {save_path}")
550
+
551
+ plt.show()
552
+
553
+ def checkExistingFile(self, date = None):
554
+ """
555
+ Check if the reconstruction file already exists, based on current instance parameters.
556
+
557
+ Args:
558
+ withTumor (bool): If True, checks reconPhantom.npy; otherwise, checks reconLaser.npy.
559
+ overwrite (bool): If False, returns False if the file exists.
560
+
561
+ Returns:
562
+ tuple: (bool: whether to save, str: the filepath)
563
+ """
564
+ if self.saveDir is None:
565
+ raise ValueError("Save directory is not specified.")
566
+ if date is None:
567
+ date = datetime.now().strftime("%d%m")
568
+ results_dir = os.path.join(self.saveDir, f'results_{date}_{self.optimizer.value}')
569
+ if not os.path.exists(results_dir):
570
+ os.makedirs(results_dir)
571
+
572
+ if os.path.exists(os.path.join(results_dir,"indices.npy")):
573
+ return (True, results_dir)
574
+
575
+ return (False, results_dir)
576
+
577
+ def load(self, withTumor=True, results_date=None, optimizer=None, filePath=None, show_logs=True):
578
+ """
579
+ Load the reconstruction results (reconPhantom or reconLaser) and indices as lists of 2D np arrays into self.
580
+ If the loaded file is a 3D array, it is split into a list of 2D arrays.
581
+ Args:
582
+ withTumor: If True, loads reconPhantom (with tumor), else reconLaser (without tumor).
583
+ results_date: Date string (format "ddmm") to specify which results to load. If None, uses the most recent date in saveDir.
584
+ optimizer: Optimizer name (as string or enum) to filter results. If None, uses the current optimizer of the instance.
585
+ filePath: Optional. If provided, loads directly from this path (overrides saveDir and results_date).
586
+ """
587
+ if filePath is not None:
588
+ # Mode chargement direct depuis un fichier
589
+ recon_key = 'reconPhantom' if withTumor else 'reconLaser'
590
+ recon_path = filePath
591
+ if not os.path.exists(recon_path):
592
+ raise FileNotFoundError(f"No reconstruction file found at {recon_path}.")
593
+ # Charge le fichier (3D ou liste de 2D)
594
+ data = np.load(recon_path, allow_pickle=True)
595
+ # Découpe en liste de 2D si c'est un tableau 3D
596
+ if isinstance(data, np.ndarray) and data.ndim == 3:
597
+ if withTumor:
598
+ self.reconPhantom = [data[i, :, :] for i in range(data.shape[0])]
599
+ else:
600
+ self.reconLaser = [data[i, :, :] for i in range(data.shape[0])]
601
+ else:
602
+ # Sinon, suppose que c'est déjà une liste de 2D
603
+ if withTumor:
604
+ self.reconPhantom = data
605
+ else:
606
+ self.reconLaser = data
607
+ # Essayer de charger les indices
608
+ base_dir, _ = os.path.split(recon_path)
609
+ indices_path = os.path.join(base_dir, 'indices.npy')
610
+ if os.path.exists(indices_path):
611
+ indices_data = np.load(indices_path, allow_pickle=True)
612
+ if isinstance(indices_data, np.ndarray) and indices_data.ndim == 3:
613
+ self.indices = [indices_data[i, :, :] for i in range(indices_data.shape[0])]
614
+ else:
615
+ self.indices = indices_data
616
+ else:
617
+ self.indices = None
618
+
619
+ if show_logs:
620
+ print(f"Loaded reconstruction results and indices from {recon_path}")
621
+ else:
622
+ # Mode chargement depuis le répertoire de résultats
623
+ if self.saveDir is None:
624
+ raise ValueError("Save directory is not specified. Please set saveDir before loading.")
625
+ # Use current optimizer and potential function if not provided
626
+ opt_name = optimizer.value if optimizer is not None else self.optimizer.value
627
+ # Build the base directory pattern
628
+ dir_pattern = f'results_*_{opt_name}'
629
+ # Add parameters to the pattern based on the optimizer
630
+ if optimizer is None:
631
+ optimizer = self.optimizer
632
+ if optimizer == OptimizerType.PPGMLEM:
633
+ beta_str = f'_Beta_{self.beta}'
634
+ delta_str = f'_Delta_{self.delta}'
635
+ gamma_str = f'_Gamma_{self.gamma}'
636
+ sigma_str = f'_Sigma_{self.sigma}'
637
+ dir_pattern += f'{beta_str}{delta_str}{gamma_str}{sigma_str}'
638
+ elif optimizer in (OptimizerType.PGC, OptimizerType.DEPIERRO95):
639
+ beta_str = f'_Beta_{self.beta}'
640
+ sigma_str = f'_Sigma_{self.sigma}'
641
+ dir_pattern += f'{beta_str}{sigma_str}'
642
+ # Find the most recent results directory if no date is specified
643
+ if results_date is None:
644
+ dirs = [d for d in os.listdir(self.saveDir) if os.path.isdir(os.path.join(self.saveDir, d)) and dir_pattern in d]
645
+ if not dirs:
646
+ raise FileNotFoundError(f"No matching results directory found for pattern '{dir_pattern}' in {self.saveDir}.")
647
+ dirs.sort(reverse=True) # Most recent first
648
+ results_dir = os.path.join(self.saveDir, dirs[0])
649
+ else:
650
+ results_dir = os.path.join(self.saveDir, f'results_{results_date}_{opt_name}')
651
+ if optimizer == OptimizerType.MLEM:
652
+ pass
653
+ elif optimizer == OptimizerType.LS:
654
+ results_dir += f'_Alpha_{self.alpha}'
655
+ if not os.path.exists(results_dir):
656
+ raise FileNotFoundError(f"Directory {results_dir} does not exist.")
657
+ # Load reconstruction results
658
+ recon_key = 'reconPhantom' if withTumor else 'reconLaser'
659
+ recon_path = os.path.join(results_dir, f'{recon_key}.npy')
660
+ if not os.path.exists(recon_path):
661
+ raise FileNotFoundError(f"No reconstruction file found at {recon_path}.")
662
+ data = np.load(recon_path, allow_pickle=True)
663
+ if isinstance(data, np.ndarray) and data.ndim == 3:
664
+ if withTumor:
665
+ self.reconPhantom = [data[i, :, :] for i in range(data.shape[0])]
666
+ else:
667
+ self.reconLaser = [data[i, :, :] for i in range(data.shape[0])]
668
+ else:
669
+ if withTumor:
670
+ self.reconPhantom = data
671
+ else:
672
+ self.reconLaser = data
673
+ # Load saved indices as list of 2D arrays
674
+ indices_path = os.path.join(results_dir, 'indices.npy')
675
+ if not os.path.exists(indices_path):
676
+ raise FileNotFoundError(f"No indices file found at {indices_path}.")
677
+ indices_data = np.load(indices_path, allow_pickle=True)
678
+ if isinstance(indices_data, np.ndarray) and indices_data.ndim == 3:
679
+ self.indices = [indices_data[i, :, :] for i in range(indices_data.shape[0])]
680
+ else:
681
+ self.indices = indices_data
682
+ if show_logs:
683
+ print(f"Loaded reconstruction results and indices from {results_dir}")
684
+
685
+ def normalizeSMatrix(self):
686
+ self.SMatrix = self.SMatrix / (float(self.experiment.params.acoustic['voltage'])*float(self.experiment.params.acoustic['sensitivity']))
687
+
688
+ # PRIVATE METHODS
689
+
690
+ def _sparseSMatrix_CSR(AcousticFields, threshold_factor=0.1, normalize=False):
691
+ """
692
+ Construit une matrice sparse CSR par morceaux sans concaténation intermédiaire.
693
+ Libère toute la mémoire temporaire à chaque étape.
694
+ """
695
+ device_index = config.select_best_gpu()
696
+ # Configuration GPU
697
+ cp.cuda.Device(device_index).use()
698
+ dtype = cp.float32
699
+ dtype_indices = cp.int32
700
+
701
+ # Mesure mémoire initiale
702
+ total_mem, free_mem = cp.cuda.Device(device_index).mem_info
703
+ initial_mem = total_mem - free_mem
704
+ print(f"VRAM initiale: {initial_mem / 1024**3:.3f} Go")
705
+
706
+ N = len(AcousticFields)
707
+ if N == 0:
708
+ raise ValueError("Aucun champ acoustique fourni.")
709
+
710
+ # Déterminer les dimensions
711
+ field = AcousticFields[0].field
712
+ if isinstance(field, np.ndarray):
713
+ field = cp.asarray(field, dtype=dtype)
714
+ T, Z, X = field.shape
715
+ TN, ZX = T * N, Z * X
716
+
717
+ # Création d'une matrice CSR vide
718
+ SMatrix = cpsparse.csr_matrix((TN, ZX), dtype=dtype)
719
+
720
+ try:
721
+ # Traitement field par field
722
+ for n in trange(N, desc="Sparsing fields", unit="field"):
723
+ field = AcousticFields[n].field
724
+ if isinstance(field, np.ndarray):
725
+ field = cp.asarray(field, dtype=dtype)
726
+
727
+ # Liste pour stocker les données du field courant
728
+ field_rows = []
729
+ field_cols = []
730
+ field_values = []
731
+
732
+ for t in range(T):
733
+ field_t = field[t]
734
+ threshold = threshold_factor * cp.max(cp.abs(field_t))
735
+ mask = cp.abs(field_t) > threshold
736
+
737
+ if not cp.any(mask):
738
+ continue
739
+
740
+ z_idx, x_idx = cp.where(mask)
741
+ rows = cp.full_like(z_idx, t * N + n, dtype=dtype_indices)
742
+ cols = z_idx * X + x_idx
743
+ vals = field_t[mask].astype(dtype)
744
+
745
+ if normalize:
746
+ max_val = cp.max(cp.abs(vals)) if vals.size > 0 else 1.0
747
+ vals = vals / max_val if max_val > 0 else vals
748
+
749
+ field_rows.append(rows)
750
+ field_cols.append(cols)
751
+ field_values.append(vals)
752
+
753
+ # Libération immédiate
754
+ del rows, cols, vals, z_idx, x_idx, mask, field_t
755
+ cp.get_default_memory_pool().free_all_blocks()
756
+
757
+ # Si des données pour ce field
758
+ if field_rows:
759
+ # Création d'une matrice COO temporaire pour ce field
760
+ temp_rows = cp.concatenate(field_rows)
761
+ temp_cols = cp.concatenate(field_cols)
762
+ temp_values = cp.concatenate(field_values)
763
+
764
+ # Création d'une matrice COO puis conversion en CSR
765
+ field_matrix = cpsparse.coo_matrix(
766
+ (temp_values, (temp_rows, temp_cols)),
767
+ shape=(TN, ZX),
768
+ dtype=dtype
769
+ ).tocsr()
770
+
771
+ # Ajout à la matrice globale
772
+ SMatrix += field_matrix
773
+
774
+ # Libération mémoire
775
+ del field_rows, field_cols, field_values
776
+ del temp_rows, temp_cols, temp_values, field_matrix
777
+ cp.get_default_memory_pool().free_all_blocks()
778
+ gc.collect()
779
+
780
+ # Libération du field
781
+ del field
782
+ cp.get_default_memory_pool().free_all_blocks()
783
+ gc.collect()
784
+
785
+ # Optimisation finale de la matrice
786
+ SMatrix.sum_duplicates()
787
+ SMatrix.eliminate_zeros()
788
+
789
+ # Calcul des métriques
790
+ nnz = SMatrix.nnz
791
+ density = nnz / (TN * ZX)
792
+ size_bytes = nnz * (4 + 4) # 4 octets pour int32 + 4 pour float32
793
+
794
+ print(f"Dimensions: {TN} x {ZX}, NNZ={nnz:,} (density={density:.2%}) using {size_bytes / 1024**3:.2f} Go of VRAM")
795
+
796
+ # Dernière libération avant le return
797
+ cp.get_default_memory_pool().free_all_blocks()
798
+ gc.collect()
799
+
800
+ return {
801
+ 'DATA': SMatrix,
802
+ 'density': density,
803
+ 'size_bytes': size_bytes
804
+ }, Z
805
+
806
+ except Exception as e:
807
+ print(f"Erreur: {str(e)}")
808
+ del SMatrix
809
+ cp.get_default_memory_pool().free_all_blocks()
810
+ gc.collect()
811
+ raise
812
+
813
+
814
+ def _AlgebraicReconPython(self,withTumor, show_logs):
815
+
816
+ if withTumor:
817
+ if self.experiment.AOsignal_withTumor is None:
818
+ raise ValueError("AO signal with tumor is not available. Please generate AO signal with tumor the experiment first in the experiment object.")
819
+ else:
820
+ if self.experiment.AOsignal_withoutTumor is None:
821
+ raise ValueError("AO signal without tumor is not available. Please generate AO signal without tumor the experiment first in the experiment object.")
822
+
823
+ if self.useSparseSMatrix and self.SMatrix_sparse is None:
824
+ raise ValueError("Sparse SMatrix is not available. Please generate sparse SMatrix first.")
825
+
826
+ if self.optimizer.value == OptimizerType.MLEM.value:
827
+ if withTumor:
828
+ self.reconPhantom, self.indices = MLEM(SMatrix=self.SMatrix,
829
+ y=self.experiment.AOsignal_withTumor,
830
+ numIterations=self.numIterations,
831
+ isSavingEachIteration=self.isSavingEachIteration,
832
+ withTumor=withTumor,
833
+ device=self.device,
834
+ use_numba=self.isMultiCPU,
835
+ denominator_threshold=self.denominatorThreshold,
836
+ max_saves=self.maxSaves,
837
+ show_logs=show_logs,
838
+ useSparseSMatrix=self.useSparseSMatrix,
839
+ Z=self.Z_dim
840
+ )
841
+ else:
842
+ self.reconLaser, self.indices = MLEM(SMatrix=self.SMatrix,
843
+ y=self.experiment.AOsignal_withoutTumor,
844
+ numIterations=self.numIterations,
845
+ isSavingEachIteration=self.isSavingEachIteration,
846
+ withTumor=withTumor,
847
+ device=self.device,
848
+ use_numba=self.isMultiCPU,
849
+ denominator_threshold=self.denominatorThreshold,
850
+ max_saves=self.maxSaves,
851
+ show_logs=show_logs,
852
+ useSparseSMatrix=self.useSparseSMatrix,
853
+ Z=self.Z_dim
854
+ )
855
+ elif self.optimizer.value == OptimizerType.LS.value:
856
+ if self.alpha is None:
857
+ raise ValueError("Alpha (regularization parameter) must be set for LS reconstruction.")
858
+ if withTumor:
859
+ self.reconPhantom, self.indices = LS(SMatrix=self.SMatrix,
860
+ y=self.experiment.AOsignal_withTumor,
861
+ numIterations=self.numIterations,
862
+ isSavingEachIteration=self.isSavingEachIteration,
863
+ withTumor=withTumor,
864
+ alpha=self.alpha,
865
+ max_saves=self.maxSaves,
866
+ show_logs=show_logs
867
+ )
868
+ else:
869
+ self.reconLaser, self.indices = LS(SMatrix=self.SMatrix,
870
+ y=self.experiment.AOsignal_withoutTumor,
871
+ numIterations=self.numIterations,
872
+ isSavingEachIteration=self.isSavingEachIteration,
873
+ withTumor=withTumor,
874
+ alpha=self.alpha,
875
+ max_saves=self.maxSaves,
876
+ show_logs=show_logs
877
+ )
878
+ else:
879
+ raise ValueError(f"Only MLEM and LS are supported for simple algebraic reconstruction. {self.optimizer.value} need Bayesian reconstruction")
880
+
881
+ def _AlgebraicReconCASToR(self,withTumor, show_logs):
882
+ # Définir les chemins
883
+ smatrix = os.path.join(self.saveDir, "system_matrix")
884
+ if withTumor:
885
+ fileName = 'AOSignals_withTumor.cdh'
886
+ else:
887
+ fileName = 'AOSignals_withoutTumor.cdh'
888
+
889
+ # Vérifier et générer les fichiers d'entrée si nécessaire
890
+ if not os.path.isfile(os.path.join(self.saveDir, fileName)):
891
+ if show_logs:
892
+ print(f"Fichier .cdh manquant. Génération de {fileName}...")
893
+ self.experiment.saveAOsignals_Castor(self.saveDir)
894
+
895
+ # Vérifier/générer la matrice système
896
+ if not os.path.isdir(smatrix):
897
+ os.makedirs(smatrix, exist_ok=True)
898
+ if not os.listdir(smatrix):
899
+ if show_logs:
900
+ print("Matrice système manquante. Génération...")
901
+ self.experiment.saveAcousticFields(self.saveDir)
902
+
903
+ # Vérifier que le fichier .cdh existe (redondant mais sûr)
904
+ if not os.path.isfile(os.path.join(self.saveDir, fileName)):
905
+ raise FileNotFoundError(f"Le fichier .cdh n'existe toujours pas : {fileName}")
906
+
907
+ # Créer le dossier de sortie
908
+ os.makedirs(os.path.join(self.saveDir, 'results', 'recon'), exist_ok=True)
909
+
910
+ # Configuration de l'environnement pour CASToR
911
+ env = os.environ.copy()
912
+ env.update({
913
+ "CASTOR_DIR": self.experiment.params.reconstruction['castor_executable'],
914
+ "CASTOR_CONFIG": os.path.join(self.experiment.params.reconstruction['castor_executable'], "config"),
915
+ "CASTOR_64bits": "1",
916
+ "CASTOR_OMP": "1",
917
+ "CASTOR_SIMD": "1",
918
+ "CASTOR_ROOT": "1",
919
+ })
920
+
921
+ # Construire la commande
922
+ cmd = [
923
+ os.path.join(self.experiment.params.reconstruction['castor_executable'], "bin", "castor-recon"),
924
+ "-df", os.path.join(self.saveDir, fileName),
925
+ "-opti", self.optimizer.value,
926
+ "-it", f"{self.numIterations}:{self.numSubsets}",
927
+ "-proj", "matrix",
928
+ "-dout", os.path.join(self.saveDir, 'results', 'recon'),
929
+ "-th", str(os.cpu_count()),
930
+ "-vb", "5",
931
+ "-proj-comp", "1",
932
+ "-ignore-scanner",
933
+ "-data-type", "AOT",
934
+ "-ignore-corr", "cali,fdur",
935
+ "-system-matrix", smatrix,
936
+ ]
937
+
938
+ # Afficher la commande (pour débogage)
939
+ if show_logs:
940
+ print("Commande CASToR :")
941
+ print(" ".join(cmd))
942
+
943
+ # Chemin du script temporaire
944
+ recon_script_path = os.path.join(gettempdir(), 'recon.sh')
945
+
946
+ # Écrire le script bash
947
+ with open(recon_script_path, 'w') as f:
948
+ f.write("#!/bin/bash\n")
949
+ f.write(f"export PATH={env['CASTOR_DIR']}/bin:$PATH\n") # Ajoute le chemin de CASToR au PATH
950
+ f.write(f"export LD_LIBRARY_PATH={env['CASTOR_DIR']}/lib:$LD_LIBRARY_PATH\n") # Ajoute les bibliothèques si nécessaire
951
+ f.write(" ".join(cmd) + "\n")
952
+
953
+ # Rendre le script exécutable et l'exécuter
954
+ subprocess.run(["chmod", "+x", recon_script_path], check=True)
955
+ if show_logs:
956
+ print(f"Exécution de la reconstruction avec CASToR...")
957
+ result = subprocess.run(recon_script_path, env=env, check=True, capture_output=True, text=True)
958
+
959
+ # Afficher la sortie de CASToR (pour débogage)
960
+ if show_logs:
961
+ print("Sortie CASToR :")
962
+ print(result.stdout)
963
+ if result.stderr:
964
+ print("Erreurs :")
965
+ print(result.stderr)
966
+
967
+ if show_logs:
968
+ print("Reconstruction terminée avec succès.")
969
+ self.load_reconCASToR(withTumor=withTumor)
970
+
971
+ # STATIC METHODS
972
+ @staticmethod
973
+ def plot_mse_comparison(recon_list, labels=None):
974
+ """
975
+ Affiche les courbes de MSE pour chaque reconstruction dans recon_list.
976
+
977
+ Args:
978
+ recon_list (list): Liste d'objets recon (doivent avoir les attributs 'indices' et 'MSE').
979
+ labels (list, optional): Liste des labels pour chaque courbe. Si None, utilise "Recon i".
980
+ """
981
+ if labels is None:
982
+ labels = [f"Recon {i+1}" for i in range(len(recon_list))]
983
+
984
+ plt.figure(figsize=(4.5, 3.5))
985
+ colors = ['red', 'green', 'blue', 'orange', 'purple'] # Ajoute d'autres couleurs si nécessaire
986
+
987
+ for i, recon in enumerate(recon_list):
988
+ color = colors[i % len(colors)]
989
+ label = labels[i] if i < len(labels) else f"Recon {i+1}"
990
+
991
+ # Trouve l'index et la valeur minimale du MSE
992
+ best_idx = recon.indices[np.argmin(recon.MSE)]
993
+ min_mse = np.min(recon.MSE)
994
+
995
+ # Trace la courbe de MSE
996
+ plt.plot(recon.indices, recon.MSE, f'{color}-', label=label)
997
+ # Ligne horizontale pour le min MSE
998
+ plt.axhline(min_mse, color=color, linestyle='--', alpha=0.5)
999
+ # Ligne verticale pour l'itération du min MSE
1000
+ plt.axvline(best_idx, color=color, linestyle='--', alpha=0.5)
1001
+
1002
+ plt.xlabel("Iteration")
1003
+ plt.ylabel("MSE")
1004
+ plt.title("MSE vs. Iteration (Comparison)")
1005
+ plt.xscale('log')
1006
+ plt.yscale('log')
1007
+ plt.grid(True, which="both", ls="-")
1008
+
1009
+ # Légende personnalisée
1010
+ handles = []
1011
+ for i, recon in enumerate(recon_list):
1012
+ color = colors[i % len(colors)]
1013
+ best_idx = recon.indices[np.argmin(recon.MSE)]
1014
+ min_mse = np.min(recon.MSE)
1015
+ handles.append(
1016
+ plt.Line2D([0], [0], color=color,
1017
+ label=f"{labels[i] if labels and i < len(labels) else f'Recon {i+1}'} (min={min_mse:.4f} @ it.{best_idx+1})")
1018
+ )
1019
+
1020
+ plt.legend(handles=handles, loc='upper right')
1021
+ plt.tight_layout()
1022
+ plt.show()
1023
+