AOT-biomaps 2.9.167__py3-none-any.whl → 2.9.273__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.
- AOT_biomaps/AOT_Acoustic/StructuredWave.py +2 -2
- AOT_biomaps/AOT_Acoustic/_mainAcoustic.py +14 -7
- AOT_biomaps/AOT_Experiment/Tomography.py +74 -4
- AOT_biomaps/AOT_Experiment/_mainExperiment.py +95 -55
- AOT_biomaps/AOT_Recon/AOT_Optimizers/DEPIERRO.py +48 -13
- AOT_biomaps/AOT_Recon/AOT_Optimizers/LS.py +9 -6
- AOT_biomaps/AOT_Recon/AOT_Optimizers/MAPEM.py +118 -38
- AOT_biomaps/AOT_Recon/AOT_Optimizers/MLEM.py +307 -102
- AOT_biomaps/AOT_Recon/AOT_Optimizers/PDHG.py +1 -1
- AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/RelativeDifferences.py +10 -14
- AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/SparseSMatrix_CSR.py +281 -0
- AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/SparseSMatrix_SELL.py +296 -0
- AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/__init__.py +2 -0
- AOT_biomaps/AOT_Recon/AOT_biomaps_kernels.cubin +0 -0
- AOT_biomaps/AOT_Recon/AlgebraicRecon.py +262 -149
- AOT_biomaps/AOT_Recon/AnalyticRecon.py +27 -42
- AOT_biomaps/AOT_Recon/BayesianRecon.py +84 -151
- AOT_biomaps/AOT_Recon/DeepLearningRecon.py +1 -1
- AOT_biomaps/AOT_Recon/PrimalDualRecon.py +69 -62
- AOT_biomaps/AOT_Recon/ReconEnums.py +27 -2
- AOT_biomaps/AOT_Recon/ReconTools.py +120 -12
- AOT_biomaps/AOT_Recon/__init__.py +1 -0
- AOT_biomaps/AOT_Recon/_mainRecon.py +73 -59
- AOT_biomaps/__init__.py +4 -71
- {aot_biomaps-2.9.167.dist-info → aot_biomaps-2.9.273.dist-info}/METADATA +2 -1
- aot_biomaps-2.9.273.dist-info/RECORD +47 -0
- aot_biomaps-2.9.167.dist-info/RECORD +0 -43
- {aot_biomaps-2.9.167.dist-info → aot_biomaps-2.9.273.dist-info}/WHEEL +0 -0
- {aot_biomaps-2.9.167.dist-info → aot_biomaps-2.9.273.dist-info}/top_level.txt +0 -0
|
@@ -292,8 +292,8 @@ class StructuredWave(AcousticField):
|
|
|
292
292
|
f"!number format := short float\n"
|
|
293
293
|
f"!number of bytes per pixel := 4\n"
|
|
294
294
|
f"scaling factor (mm/pixel) [1] := {self.params['dx'] * 1000}\n"
|
|
295
|
-
f"scaling factor (mm/pixel) [2] := {self.params['
|
|
296
|
-
f"scaling factor (s/pixel) [3] := {1 / self.params['
|
|
295
|
+
f"scaling factor (mm/pixel) [2] := {self.params['dz'] * 1000}\n"
|
|
296
|
+
f"scaling factor (s/pixel) [3] := {1 / self.params['f_saving']}\n"
|
|
297
297
|
f"first pixel offset (mm) [1] := {self.params['Xrange'][0] * 1e3}\n"
|
|
298
298
|
f"first pixel offset (mm) [2] := {self.params['Zrange'][0] * 1e3}\n"
|
|
299
299
|
f"first pixel offset (s) [3] := 0\n"
|
|
@@ -19,10 +19,12 @@ from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D
|
|
|
19
19
|
from kwave.kspaceFirstOrder2D import kspaceFirstOrder2D
|
|
20
20
|
from kwave.options.simulation_options import SimulationOptions
|
|
21
21
|
from kwave.options.simulation_execution_options import SimulationExecutionOptions
|
|
22
|
+
from AOT_biomaps.Settings import Params
|
|
22
23
|
|
|
23
24
|
from tempfile import gettempdir
|
|
24
25
|
from math import ceil
|
|
25
26
|
from abc import ABC, abstractmethod
|
|
27
|
+
import logging
|
|
26
28
|
|
|
27
29
|
|
|
28
30
|
|
|
@@ -75,7 +77,7 @@ class AcousticField(ABC):
|
|
|
75
77
|
print(f"Initialization error: {e}")
|
|
76
78
|
raise
|
|
77
79
|
if params != None:
|
|
78
|
-
if type(params) !=
|
|
80
|
+
if type(params) != Params:
|
|
79
81
|
raise TypeError("params must be an instance of the Params class")
|
|
80
82
|
|
|
81
83
|
self.params = {
|
|
@@ -225,6 +227,7 @@ class AcousticField(ABC):
|
|
|
225
227
|
Generate the acoustic field based on the specified simulation type and parameters.
|
|
226
228
|
"""
|
|
227
229
|
try:
|
|
230
|
+
logging.getLogger('root').setLevel(logging.ERROR)
|
|
228
231
|
if self.params['typeSim'] == TypeSim.FIELD2.value:
|
|
229
232
|
raise NotImplementedError("FIELD2 simulation is not implemented yet.")
|
|
230
233
|
elif self.params['typeSim'] == TypeSim.KWAVE.value:
|
|
@@ -233,7 +236,7 @@ class AcousticField(ABC):
|
|
|
233
236
|
field = self._generate_acoustic_field_KWAVE_2D(isGpu, show_log)
|
|
234
237
|
except Exception as e:
|
|
235
238
|
raise RuntimeError(f"Failed to generate 2D acoustic field: {e}")
|
|
236
|
-
self.field = calculate_envelope_squared(field)
|
|
239
|
+
self.field = reshape_field(calculate_envelope_squared(field),[self.factorT, self.factorX, self.factorZ])
|
|
237
240
|
elif self.params["dim"] == Dim.D3.value:
|
|
238
241
|
field = self._generate_acoustic_field_KWAVE_3D(isGpu, show_log)
|
|
239
242
|
self.field = reshape_field(calculate_envelope_squared(field),[self.factorT, self.factorX, self.factorZ])
|
|
@@ -265,7 +268,7 @@ class AcousticField(ABC):
|
|
|
265
268
|
print(f"Error in save_field method: {e}")
|
|
266
269
|
raise
|
|
267
270
|
|
|
268
|
-
def load_field(self, folderPath, formatSave=FormatSave.HDR_IMG):
|
|
271
|
+
def load_field(self, folderPath, formatSave=FormatSave.HDR_IMG, nameBlock=None):
|
|
269
272
|
"""
|
|
270
273
|
Load the acoustic field from a file in the specified format.
|
|
271
274
|
|
|
@@ -286,7 +289,7 @@ class AcousticField(ABC):
|
|
|
286
289
|
raise NotImplementedError("3D KWAVE field loading is not implemented yet.")
|
|
287
290
|
elif formatSave.value == FormatSave.H5.value:
|
|
288
291
|
if self.params["dim"] == Dim.D2.value:
|
|
289
|
-
self._load_field_h5(folderPath)
|
|
292
|
+
self._load_field_h5(folderPath,nameBlock)
|
|
290
293
|
elif self.params["dim"] == Dim.D3.value:
|
|
291
294
|
raise NotImplementedError("H5 KWAVE field loading is not implemented yet.")
|
|
292
295
|
elif formatSave.value == FormatSave.NPY.value:
|
|
@@ -531,6 +534,8 @@ class AcousticField(ABC):
|
|
|
531
534
|
total_size_z = next_power_of_2(Nz)
|
|
532
535
|
pml_x_size = (total_size_x - Nx) // 2
|
|
533
536
|
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
|
+
pml_z_size = max(pml_z_size, 50) # Ensure a minimum PML size of 50 grid points to avoid parasitic reflections
|
|
534
539
|
|
|
535
540
|
# --- 6. Simulation options ---
|
|
536
541
|
simulation_options = SimulationOptions(
|
|
@@ -650,7 +655,7 @@ class AcousticField(ABC):
|
|
|
650
655
|
"""
|
|
651
656
|
pass
|
|
652
657
|
|
|
653
|
-
def _load_field_h5(self, filePath):
|
|
658
|
+
def _load_field_h5(self, filePath,nameBlock):
|
|
654
659
|
"""
|
|
655
660
|
Load the 2D acoustic field from an H5 file.
|
|
656
661
|
|
|
@@ -661,8 +666,10 @@ class AcousticField(ABC):
|
|
|
661
666
|
- field (numpy.ndarray): The loaded acoustic field.
|
|
662
667
|
"""
|
|
663
668
|
try:
|
|
664
|
-
|
|
665
|
-
|
|
669
|
+
if nameBlock is None:
|
|
670
|
+
nameBlock = 'data'
|
|
671
|
+
with h5py.File(os.path.join(filePath, self.getName_field()+".h5"), 'r') as f:
|
|
672
|
+
self.field = f[nameBlock][:]
|
|
666
673
|
except Exception as e:
|
|
667
674
|
print(f"Error in _load_field_h5 method: {e}")
|
|
668
675
|
raise
|
|
@@ -7,6 +7,8 @@ import psutil
|
|
|
7
7
|
import numpy as np
|
|
8
8
|
import matplotlib.pyplot as plt
|
|
9
9
|
from tqdm import trange
|
|
10
|
+
import h5py
|
|
11
|
+
from scipy.io import loadmat
|
|
10
12
|
|
|
11
13
|
class Tomography(Experiment):
|
|
12
14
|
def __init__(self, **kwargs):
|
|
@@ -47,7 +49,7 @@ class Tomography(Experiment):
|
|
|
47
49
|
return False, f"OpticImage phantom shape {self.OpticImage.phantom.shape} does not match AcousticFields shape {self.AcousticFields[0].field.shape[1:]}."
|
|
48
50
|
return True, "Experiment is correctly initialized."
|
|
49
51
|
|
|
50
|
-
def generateAcousticFields(self, fieldDataPath=None, show_log=True):
|
|
52
|
+
def generateAcousticFields(self, fieldDataPath=None, show_log=True, nameBlock=None):
|
|
51
53
|
"""
|
|
52
54
|
Generate the acoustic fields for simulation.
|
|
53
55
|
Args:
|
|
@@ -57,7 +59,7 @@ class Tomography(Experiment):
|
|
|
57
59
|
systemMatrix: A numpy array of the generated fields.
|
|
58
60
|
"""
|
|
59
61
|
if self.TypeAcoustic.value == WaveType.StructuredWave.value:
|
|
60
|
-
self.AcousticFields = self._generateAcousticFields_STRUCT_CPU(fieldDataPath, show_log)
|
|
62
|
+
self.AcousticFields = self._generateAcousticFields_STRUCT_CPU(fieldDataPath, show_log, nameBlock)
|
|
61
63
|
else:
|
|
62
64
|
raise ValueError("Unsupported wave type.")
|
|
63
65
|
|
|
@@ -389,7 +391,7 @@ class Tomography(Experiment):
|
|
|
389
391
|
return True
|
|
390
392
|
|
|
391
393
|
# PRIVATE METHODS
|
|
392
|
-
def _generateAcousticFields_STRUCT_CPU(self, fieldDataPath=None, show_log=False):
|
|
394
|
+
def _generateAcousticFields_STRUCT_CPU(self, fieldDataPath=None, show_log=False, nameBlock=None):
|
|
393
395
|
if self.patterns is None:
|
|
394
396
|
raise ValueError("patterns is not initialized. Please load or generate the active list first.")
|
|
395
397
|
listAcousticFields = []
|
|
@@ -415,7 +417,7 @@ class Tomography(Experiment):
|
|
|
415
417
|
if pathField is not None and os.path.exists(pathField):
|
|
416
418
|
progress_bar.set_postfix_str(f"Loading field - {AcousticField.getName_field()} -- Memory used: {memory.percent}%")
|
|
417
419
|
try:
|
|
418
|
-
AcousticField.load_field(fieldDataPath, self.FormatSave)
|
|
420
|
+
AcousticField.load_field(fieldDataPath, self.FormatSave,nameBlock)
|
|
419
421
|
except:
|
|
420
422
|
progress_bar.set_postfix_str(f"Error loading field -> Generating field - {AcousticField.getName_field()} -- Memory used: {memory.percent}% ---- processing on {config.get_process().upper()} ----")
|
|
421
423
|
AcousticField.generate_field(show_log=show_log)
|
|
@@ -433,3 +435,71 @@ class Tomography(Experiment):
|
|
|
433
435
|
listAcousticFields.append(AcousticField)
|
|
434
436
|
progress_bar.set_postfix_str("")
|
|
435
437
|
return listAcousticFields
|
|
438
|
+
|
|
439
|
+
def load_experimentalAO(self, pathAO, withTumor = True, h5name='AOsignal'):
|
|
440
|
+
"""
|
|
441
|
+
Load experimental AO signals from specified file paths.
|
|
442
|
+
Args:
|
|
443
|
+
path_withTumor: Path to the AO signal with tumor.
|
|
444
|
+
path_withoutTumor: Path to the AO signal without tumor.
|
|
445
|
+
"""
|
|
446
|
+
if not os.path.exists(pathAO):
|
|
447
|
+
raise FileNotFoundError(f"File {pathAO} not found.")
|
|
448
|
+
|
|
449
|
+
if pathAO.endswith('.npy'):
|
|
450
|
+
ao_signal = np.load(pathAO)
|
|
451
|
+
elif pathAO.endswith('.h5'):
|
|
452
|
+
with h5py.File(pathAO, 'r') as f:
|
|
453
|
+
if h5name not in f:
|
|
454
|
+
raise KeyError(f"Dataset '{h5name}' not found in the HDF5 file.")
|
|
455
|
+
ao_signal = f[h5name][:]
|
|
456
|
+
elif pathAO.endswith('.mat'):
|
|
457
|
+
mat_data = loadmat(pathAO)
|
|
458
|
+
if h5name not in mat_data:
|
|
459
|
+
raise KeyError(f"Dataset '{h5name}' not found in the .mat file.")
|
|
460
|
+
ao_signal = mat_data[h5name]
|
|
461
|
+
elif pathAO.endswith('.hdr'):
|
|
462
|
+
ao_signal = self._loadAOSignal(pathAO)
|
|
463
|
+
else:
|
|
464
|
+
raise ValueError("Unsupported file format. Supported formats are: .npy, .h5, .mat, .hdr")
|
|
465
|
+
|
|
466
|
+
if withTumor:
|
|
467
|
+
self.AOsignal_withTumor = ao_signal
|
|
468
|
+
else:
|
|
469
|
+
self.AOsignal_withoutTumor = ao_signal
|
|
470
|
+
|
|
471
|
+
def check_experimentalAO(self, activeListPath, withTumor=True):
|
|
472
|
+
"""
|
|
473
|
+
Check if the experimental AO signals are correctly initialized.
|
|
474
|
+
"""
|
|
475
|
+
if withTumor:
|
|
476
|
+
if self.AOsignal_withTumor is None:
|
|
477
|
+
raise ValueError("Experimental AOsignal with tumor is not initialized. Please load the experimental AO signal with tumor first.")
|
|
478
|
+
else:
|
|
479
|
+
if self.AOsignal_withoutTumor is None:
|
|
480
|
+
raise ValueError("Experimental AOsignal without tumor is not initialized. Please load the experimental AO signal without tumor first.")
|
|
481
|
+
if self.AcousticFields is not None:
|
|
482
|
+
# get min time shape between all AO signals
|
|
483
|
+
print()
|
|
484
|
+
|
|
485
|
+
if self.AcousticFields[0].field.shape[0] > self.AOsignal_withTumor.shape[0]:
|
|
486
|
+
self.cutAcousticFields(max_t=self.AOsignal_withTumor.shape[0]/float(self.params.acoustic['f_saving']))
|
|
487
|
+
else:
|
|
488
|
+
for i in range(len(self.AcousticFields)):
|
|
489
|
+
min_time_shape = min(self.AcousticFields[i].field.shape[0])
|
|
490
|
+
if withTumor:
|
|
491
|
+
self.AOsignal_withTumor = self.AOsignal_withTumor[:min_time_shape, :]
|
|
492
|
+
else:
|
|
493
|
+
self.AOsignal_withoutTumor = self.AOsignal_withoutTumor[:min_time_shape, :]
|
|
494
|
+
|
|
495
|
+
for field in self.AcousticFields:
|
|
496
|
+
if activeListPath is not None:
|
|
497
|
+
with open(activeListPath, 'r') as file:
|
|
498
|
+
lines = file.readlines()
|
|
499
|
+
expected_name = lines[self.AcousticFields.index(field)].strip()
|
|
500
|
+
nameField = field.getName_field()
|
|
501
|
+
if nameField.startswith("field_"):
|
|
502
|
+
nameField = nameField[len("field_"):]
|
|
503
|
+
if nameField != expected_name:
|
|
504
|
+
raise ValueError(f"Field name {nameField} does not match the expected name {expected_name} from the active list.")
|
|
505
|
+
print("Experimental AO signals are correctly initialized.")
|
|
@@ -56,6 +56,7 @@ class Experiment(ABC):
|
|
|
56
56
|
pass
|
|
57
57
|
|
|
58
58
|
def cutAcousticFields(self, max_t, min_t=0):
|
|
59
|
+
|
|
59
60
|
max_t = float(max_t)
|
|
60
61
|
min_t = float(min_t)
|
|
61
62
|
|
|
@@ -113,7 +114,6 @@ class Experiment(ABC):
|
|
|
113
114
|
noiseSignals[:, i] = noisy_signal
|
|
114
115
|
return noiseSignals
|
|
115
116
|
|
|
116
|
-
|
|
117
117
|
def reduceDims(self, mode='avg'):
|
|
118
118
|
"""
|
|
119
119
|
Réduit les dimensions T, X, Z d'un numpy array (T, X, Z) par 2 en utilisant une convolution.
|
|
@@ -154,6 +154,16 @@ class Experiment(ABC):
|
|
|
154
154
|
for param in ['dx', 'dy', 'dz']:
|
|
155
155
|
convert_and_update(self.params.general, param, lambda x: x * 2)
|
|
156
156
|
|
|
157
|
+
def normalizeAOsignals(self, withTumor=True):
|
|
158
|
+
if withTumor and self.AOsignal_withTumor is None:
|
|
159
|
+
raise ValueError("AO signal with tumor is not generated. Please generate it first.")
|
|
160
|
+
if not withTumor and self.AOsignal_withoutTumor is None:
|
|
161
|
+
raise ValueError("AO signal without tumor is not generated. Please generate it first.")
|
|
162
|
+
if withTumor:
|
|
163
|
+
self.AOsignal_withTumor = self.AOsignal_withTumor - np.min(self.AOsignal_withTumor)/(np.max(self.AOsignal_withTumor)-np.min(self.AOsignal_withTumor))
|
|
164
|
+
else:
|
|
165
|
+
self.AOsignal_withoutTumor = self.AOsignal_withoutTumor - np.min(self.AOsignal_withoutTumor)/(np.max(self.AOsignal_withoutTumor)-np.min(self.AOsignal_withoutTumor))
|
|
166
|
+
|
|
157
167
|
def saveAcousticFields(self, save_directory):
|
|
158
168
|
progress_bar = trange(len(self.AcousticFields), desc="Saving Acoustic Fields")
|
|
159
169
|
for i in progress_bar:
|
|
@@ -230,67 +240,97 @@ class Experiment(ABC):
|
|
|
230
240
|
return ani
|
|
231
241
|
|
|
232
242
|
def generateAOsignal(self, withTumor=True, AOsignalDataPath=None):
|
|
233
|
-
if self.AcousticFields is None:
|
|
234
|
-
raise ValueError("AcousticFields is not initialized. Please generate the system matrix first.")
|
|
235
|
-
|
|
236
|
-
if self.OpticImage is None:
|
|
237
|
-
raise ValueError("OpticImage is not initialized. Please generate the phantom first.")
|
|
238
243
|
|
|
239
244
|
if AOsignalDataPath is not None:
|
|
240
245
|
if not os.path.exists(AOsignalDataPath):
|
|
241
246
|
raise FileNotFoundError(f"AO file {AOsignalDataPath} not found.")
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
247
|
+
if withTumor:
|
|
248
|
+
self.AOsignal_withTumor = self._loadAOSignal(AOsignalDataPath)
|
|
249
|
+
if self.AOsignal_withTumor.shape[0] != self.AcousticFields[0].field.shape[0]:
|
|
250
|
+
print(f"AO signal shape {self.AOsignal_withTumor.shape} does not match the expected shape {self.AcousticFields[0].field.shape}. Resizing Acoustic fields...")
|
|
251
|
+
self.cutAcousticFields(max_t=self.AOsignal_withTumor.shape[0] / float(self.params.acoustic['f_saving']), min_t=0)
|
|
245
252
|
else:
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
253
|
+
self.AOsignal_withoutTumor = self._loadAOSignal(AOsignalDataPath)
|
|
254
|
+
if self.AOsignal_withoutTumor.shape[0] != self.AcousticFields[0].field.shape[0]:
|
|
255
|
+
print(f"AO signal shape {self.AOsignal_withoutTumor.shape} does not match the expected shape {self.AcousticFields[0].field.shape}. Resizing Acoustic fields...")
|
|
256
|
+
self.cutAcousticFields(max_t=self.AOsignal_withoutTumor.shape[0] / float(self.params.acoustic['f_saving']), min_t=0)
|
|
257
|
+
else:
|
|
258
|
+
if self.AcousticFields is None:
|
|
259
|
+
raise ValueError("AcousticFields is not initialized. Please generate the system matrix first.")
|
|
260
|
+
|
|
261
|
+
if self.OpticImage is None:
|
|
262
|
+
raise ValueError("OpticImage is not initialized. Please generate the phantom first.")
|
|
263
|
+
|
|
264
|
+
if not all(field.field.shape == self.AcousticFields[0].field.shape for field in self.AcousticFields):
|
|
265
|
+
minShape = min([field.field.shape[0] for field in self.AcousticFields])
|
|
266
|
+
self.cutAcousticFields(max_t=minShape * self.params['fs_aq'])
|
|
267
|
+
else:
|
|
268
|
+
shape_field = self.AcousticFields[0].field.shape
|
|
260
269
|
|
|
261
|
-
|
|
262
|
-
for t in range(self.AcousticFields[i].field.shape[0]):
|
|
263
|
-
if withTumor:
|
|
264
|
-
interaction = self.OpticImage.phantom * self.AcousticFields[i].field[t, :, :]
|
|
265
|
-
else:
|
|
266
|
-
interaction = self.OpticImage.laser.intensity * self.AcousticFields[i].field[t, :, :]
|
|
267
|
-
AOsignal[t, i] = np.sum(interaction)
|
|
270
|
+
AOsignal = np.zeros((shape_field[0], len(self.AcousticFields)), dtype=np.float32)
|
|
268
271
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
272
|
+
if withTumor:
|
|
273
|
+
description = "Generating AO Signal with Tumor"
|
|
274
|
+
else:
|
|
275
|
+
description = "Generating AO Signal without Tumor"
|
|
276
|
+
|
|
277
|
+
for i in trange(len(self.AcousticFields), desc=description):
|
|
278
|
+
for t in range(self.AcousticFields[i].field.shape[0]):
|
|
279
|
+
if withTumor:
|
|
280
|
+
interaction = self.OpticImage.phantom * self.AcousticFields[i].field[t, :, :]
|
|
281
|
+
else:
|
|
282
|
+
interaction = self.OpticImage.laser.intensity * self.AcousticFields[i].field[t, :, :]
|
|
283
|
+
AOsignal[t, i] = np.sum(interaction)
|
|
284
|
+
|
|
285
|
+
if withTumor:
|
|
286
|
+
self.AOsignal_withTumor = AOsignal
|
|
287
|
+
else:
|
|
288
|
+
self.AOsignal_withoutTumor = AOsignal
|
|
273
289
|
|
|
274
290
|
@staticmethod
|
|
275
|
-
def _loadAOSignal(
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
for
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
291
|
+
def _loadAOSignal(AOsignalPath):
|
|
292
|
+
if AOsignalPath.endswith(".cdh"):
|
|
293
|
+
with open(AOsignalPath, "r") as file:
|
|
294
|
+
cdh_content = file.readlines()
|
|
295
|
+
|
|
296
|
+
cdf_path = AOsignalPath.replace(".cdh", ".cdf")
|
|
297
|
+
|
|
298
|
+
# Extraire les paramètres depuis le fichier .cdh
|
|
299
|
+
n_scans = int([line.split(":")[1].strip() for line in cdh_content if "Number of events" in line][0])
|
|
300
|
+
n_acquisitions_per_event = int([line.split(":")[1].strip() for line in cdh_content if "Number of acquisitions per event" in line][0])
|
|
301
|
+
num_elements = int([line.split(":")[1].strip() for line in cdh_content if "Number of US transducers" in line][0])
|
|
302
|
+
|
|
303
|
+
# Initialisation des structures
|
|
304
|
+
AO_signal = np.zeros((n_acquisitions_per_event, n_scans), dtype=np.float32)
|
|
305
|
+
active_lists = []
|
|
306
|
+
angles = []
|
|
307
|
+
|
|
308
|
+
# Lecture du fichier binaire
|
|
309
|
+
with open(cdf_path, "rb") as file:
|
|
310
|
+
for j in trange(n_scans, desc="Lecture des événements"):
|
|
311
|
+
# Lire l'activeList : 48 caractères hex = 24 bytes
|
|
312
|
+
active_list_bytes = file.read(24)
|
|
313
|
+
active_list_hex = active_list_bytes.hex()
|
|
314
|
+
active_lists.append(active_list_hex)
|
|
315
|
+
|
|
316
|
+
# Lire l'angle (1 byte signé)
|
|
317
|
+
angle_byte = file.read(1)
|
|
318
|
+
angle = np.frombuffer(angle_byte, dtype=np.int8)[0]
|
|
319
|
+
angles.append(angle)
|
|
320
|
+
|
|
321
|
+
# Lire le signal AO (float32)
|
|
322
|
+
data = np.frombuffer(file.read(n_acquisitions_per_event * 4), dtype=np.float32)
|
|
323
|
+
if len(data) != n_acquisitions_per_event:
|
|
324
|
+
raise ValueError(f"Erreur à l'événement {j} : attendu {n_acquisitions_per_event}, obtenu {len(data)}")
|
|
325
|
+
AO_signal[:, j] = data
|
|
326
|
+
|
|
327
|
+
return AO_signal
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
elif AOsignalPath.endswith(".npy"):
|
|
331
|
+
return np.load(AOsignalPath) # Supposé déjà au bon format
|
|
332
|
+
else:
|
|
333
|
+
raise ValueError("Format de fichier non supporté. Utilisez .cdh/.cdf ou .npy.")
|
|
294
334
|
|
|
295
335
|
def saveAOsignals_Castor(self, save_directory, withTumor=True):
|
|
296
336
|
if withTumor:
|
|
@@ -318,10 +358,10 @@ class Experiment(ABC):
|
|
|
318
358
|
header_content = (
|
|
319
359
|
f"Data filename: {'AOSignals_withTumor.cdf' if withTumor else 'AOSignals_withoutTumor.cdf'}\n"
|
|
320
360
|
f"Number of events: {nScan}\n"
|
|
321
|
-
f"Number of acquisitions per event: {AO_signal.shape[
|
|
361
|
+
f"Number of acquisitions per event: {AO_signal.shape[0]}\n"
|
|
322
362
|
f"Start time (s): 0\n"
|
|
323
363
|
f"Duration (s): 1\n"
|
|
324
|
-
f"Acquisition frequency (Hz): {
|
|
364
|
+
f"Acquisition frequency (Hz): {self.params.acoustic['f_saving']}\n"
|
|
325
365
|
f"Data mode: histogram\n"
|
|
326
366
|
f"Data type: AOT\n"
|
|
327
367
|
f"Number of US transducers: {self.params.acoustic['num_elements']}"
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from AOT_biomaps.AOT_Recon.ReconEnums import PotentialType
|
|
2
|
-
from AOT_biomaps.AOT_Recon.ReconTools import _build_adjacency_sparse
|
|
2
|
+
from AOT_biomaps.AOT_Recon.ReconTools import _build_adjacency_sparse, calculate_memory_requirement, check_gpu_memory
|
|
3
3
|
from AOT_biomaps.Config import config
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
4
6
|
import numpy as np
|
|
5
7
|
import torch
|
|
6
8
|
from tqdm import trange
|
|
@@ -11,9 +13,42 @@ if config.get_process() == 'gpu':
|
|
|
11
13
|
except ImportError:
|
|
12
14
|
raise ImportError("torch_scatter and torch_sparse are required for GPU processing. Please install them using 'pip install torch-scatter torch-sparse' with correct link (follow instructions https://github.com/LucasDuclos/AcoustoOpticTomography/edit/main/README.md).")
|
|
13
15
|
|
|
14
|
-
def
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
def DEPIERRO(
|
|
17
|
+
SMatrix,
|
|
18
|
+
y,
|
|
19
|
+
numIterations,
|
|
20
|
+
beta,
|
|
21
|
+
sigma,
|
|
22
|
+
isSavingEachIteration,
|
|
23
|
+
withTumor,
|
|
24
|
+
max_saves,
|
|
25
|
+
show_logs):
|
|
26
|
+
"""
|
|
27
|
+
This method implements the DEPIERRO algorithm using either CPU or single-GPU PyTorch acceleration.
|
|
28
|
+
Multi-GPU and Multi-CPU modes are not implemented for this algorithm.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
tumor_str = "WITH" if withTumor else "WITHOUT"
|
|
32
|
+
# Auto-select device and method
|
|
33
|
+
if device is None:
|
|
34
|
+
if torch.cuda.is_available() and check_gpu_memory(config.select_best_gpu(), calculate_memory_requirement(SMatrix, y), show_logs=show_logs):
|
|
35
|
+
device = torch.device(f"cuda:{config.select_best_gpu()}")
|
|
36
|
+
use_gpu = True
|
|
37
|
+
else:
|
|
38
|
+
device = torch.device("cpu")
|
|
39
|
+
use_gpu = False
|
|
40
|
+
else:
|
|
41
|
+
use_gpu = device.type == "cuda"
|
|
42
|
+
# Dispatch to the appropriate implementation
|
|
43
|
+
if use_gpu:
|
|
44
|
+
return _DEPIERRO_GPU(SMatrix, y, numIterations, beta, sigma, isSavingEachIteration, tumor_str, device, max_saves, show_logs)
|
|
45
|
+
else:
|
|
46
|
+
return _DEPIERRO_CPU(SMatrix, y, numIterations, beta, sigma, isSavingEachIteration, tumor_str, device, max_saves, show_logs)
|
|
47
|
+
except Exception as e:
|
|
48
|
+
print(f"Error in MLEM: {type(e).__name__}: {e}")
|
|
49
|
+
return None, None
|
|
50
|
+
|
|
51
|
+
def _DEPIERRO_GPU(SMatrix, y, numIterations, beta, sigma, isSavingEachIteration, tumor_str, device, max_saves, show_logs=True):
|
|
17
52
|
# Conversion des données en tenseurs PyTorch (float64)
|
|
18
53
|
A_matrix_torch = torch.tensor(SMatrix, dtype=torch.float64, device=device)
|
|
19
54
|
y_torch = torch.tensor(y, dtype=torch.float64, device=device)
|
|
@@ -33,7 +68,7 @@ def _DEPIERRO_GPU(SMatrix, y, numIterations, beta, sigma, isSavingEachIteration,
|
|
|
33
68
|
# Construction de la matrice d'adjacence
|
|
34
69
|
adj_index, adj_values = _build_adjacency_sparse(Z, X, device=device, dtype=torch.float64)
|
|
35
70
|
# Description pour la barre de progression
|
|
36
|
-
description = f"AOT-BioMaps -- Bayesian Reconstruction Tomography: DE PIERRO (Sparse QUADRATIC β:{beta:.4f}, σ:{sigma:.4f}) ---- {
|
|
71
|
+
description = f"AOT-BioMaps -- Bayesian Reconstruction Tomography: DE PIERRO (Sparse QUADRATIC β:{beta:.4f}, σ:{sigma:.4f}) ---- {tumor_str} TUMOR ---- processing on single GPU no.{torch.cuda.current_device()}"
|
|
37
72
|
# Configuration pour la sauvegarde des itérations
|
|
38
73
|
saved_indices = [0]
|
|
39
74
|
|
|
@@ -47,7 +82,8 @@ def _DEPIERRO_GPU(SMatrix, y, numIterations, beta, sigma, isSavingEachIteration,
|
|
|
47
82
|
save_indices.append(numIterations - 1)
|
|
48
83
|
|
|
49
84
|
# Boucle principale MAP-EM
|
|
50
|
-
|
|
85
|
+
iterator = trange(numIterations, desc=description) if show_logs else range(numIterations)
|
|
86
|
+
for it in iterator:
|
|
51
87
|
theta_p = matrix_theta_torch[-1]
|
|
52
88
|
theta_p_flat = theta_p.reshape(-1)
|
|
53
89
|
# Étape 1 : Projection avant
|
|
@@ -92,10 +128,8 @@ def _DEPIERRO_GPU(SMatrix, y, numIterations, beta, sigma, isSavingEachIteration,
|
|
|
92
128
|
else:
|
|
93
129
|
return matrix_theta_torch[-1].cpu().numpy(), None
|
|
94
130
|
|
|
95
|
-
def _DEPIERRO_CPU(SMatrix, y,
|
|
131
|
+
def _DEPIERRO_CPU(SMatrix, y, numIterations, beta, sigma, isSavingEachIteration, tumor_str, device, max_saves, show_logs=True):
|
|
96
132
|
try:
|
|
97
|
-
if Omega != PotentialType.QUADRATIC:
|
|
98
|
-
raise ValueError("Depierro95 optimizer only supports QUADRATIC potential function.")
|
|
99
133
|
if beta is None or sigma is None:
|
|
100
134
|
raise ValueError("Depierro95 optimizer requires beta and sigma parameters.")
|
|
101
135
|
|
|
@@ -122,9 +156,10 @@ def _DEPIERRO_CPU(SMatrix, y, Omega, numIterations, beta, sigma, isSavingEachIte
|
|
|
122
156
|
if save_indices[-1] != numIterations - 1:
|
|
123
157
|
save_indices.append(numIterations - 1)
|
|
124
158
|
|
|
125
|
-
description = f"AOT-BioMaps -- Bayesian Reconstruction Tomography: DE PIERRO (Sparse QUADRATIC β:{beta:.4f}, σ:{sigma:.4f}) ---- {
|
|
159
|
+
description = f"AOT-BioMaps -- Bayesian Reconstruction Tomography: DE PIERRO (Sparse QUADRATIC β:{beta:.4f}, σ:{sigma:.4f}) ---- {tumor_str} TUMOR ---- processing on single CPU ----"
|
|
126
160
|
|
|
127
|
-
|
|
161
|
+
iterator = trange(numIterations, desc=description) if show_logs else range(numIterations)
|
|
162
|
+
for it in iterator:
|
|
128
163
|
theta_p = matrix_theta[-1]
|
|
129
164
|
theta_p_flat = theta_p.reshape(-1)
|
|
130
165
|
q_flat = np.dot(A_flat, theta_p_flat)
|
|
@@ -143,9 +178,9 @@ def _DEPIERRO_CPU(SMatrix, y, Omega, numIterations, beta, sigma, isSavingEachIte
|
|
|
143
178
|
theta_p_plus_1_flat = np.clip(theta_p_plus_1_flat, a_min=0, a_max=None)
|
|
144
179
|
theta_next = theta_p_plus_1_flat.reshape(Z, X)
|
|
145
180
|
matrix_theta[-1] = theta_next
|
|
146
|
-
if isSavingEachIteration and
|
|
181
|
+
if isSavingEachIteration and it in save_indices:
|
|
147
182
|
I_reconMatrix.append(theta_next.copy())
|
|
148
|
-
saved_indices.append(
|
|
183
|
+
saved_indices.append(it)
|
|
149
184
|
|
|
150
185
|
if isSavingEachIteration:
|
|
151
186
|
return I_reconMatrix, saved_indices
|
|
@@ -2,6 +2,7 @@ from AOT_biomaps.Config import config
|
|
|
2
2
|
import torch
|
|
3
3
|
import numpy as np
|
|
4
4
|
from tqdm import trange
|
|
5
|
+
from AOT_biomaps.AOT_Recon.ReconTools import calculate_memory_requirement, check_gpu_memory
|
|
5
6
|
|
|
6
7
|
def LS(
|
|
7
8
|
SMatrix,
|
|
@@ -12,6 +13,7 @@ def LS(
|
|
|
12
13
|
withTumor=True,
|
|
13
14
|
device=None,
|
|
14
15
|
max_saves=5000,
|
|
16
|
+
show_logs=True
|
|
15
17
|
):
|
|
16
18
|
"""
|
|
17
19
|
Least Squares reconstruction using Projected Gradient Descent (PGD) with non-negativity constraint.
|
|
@@ -20,15 +22,15 @@ def LS(
|
|
|
20
22
|
tumor_str = "WITH" if withTumor else "WITHOUT"
|
|
21
23
|
# Force GPU usage for now
|
|
22
24
|
if device is None:
|
|
23
|
-
if
|
|
25
|
+
if torch.cuda.is_available() and check_gpu_memory(config.select_best_gpu(), calculate_memory_requirement(SMatrix, y), show_logs=show_logs):
|
|
24
26
|
raise RuntimeError("CUDA is required for this implementation.")
|
|
25
27
|
device = torch.device(f"cuda:{config.select_best_gpu()}")
|
|
26
28
|
else:
|
|
27
29
|
if device.type != "cuda":
|
|
28
30
|
raise RuntimeError("Only GPU implementation is available for now.")
|
|
29
|
-
return _LS_GPU_stable(SMatrix, y, numIterations, alpha, isSavingEachIteration, tumor_str, max_saves)
|
|
31
|
+
return _LS_GPU_stable(SMatrix, y, numIterations, alpha, isSavingEachIteration, tumor_str, max_saves, show_logs=show_logs)
|
|
30
32
|
|
|
31
|
-
def _LS_GPU_stable(SMatrix, y, numIterations, alpha, isSavingEachIteration, tumor_str, max_saves=5000):
|
|
33
|
+
def _LS_GPU_stable(SMatrix, y, numIterations, alpha, isSavingEachIteration, tumor_str, max_saves=5000, show_logs=True):
|
|
32
34
|
"""
|
|
33
35
|
Stable GPU implementation of LS using projected gradient descent with diagonal preconditioner.
|
|
34
36
|
"""
|
|
@@ -65,13 +67,14 @@ def _LS_GPU_stable(SMatrix, y, numIterations, alpha, isSavingEachIteration, tumo
|
|
|
65
67
|
AT_r = torch.empty(ZX, device=device)
|
|
66
68
|
description = f"AOT-BioMaps -- Stable LS Reconstruction ---- {tumor_str} TUMOR ---- GPU {torch.cuda.current_device()}"
|
|
67
69
|
|
|
68
|
-
|
|
70
|
+
iterator = trange(numIterations, desc=description) if show_logs else range(numIterations)
|
|
71
|
+
for it in iterator:
|
|
69
72
|
# Calcul du résidu (inplace)
|
|
70
73
|
torch.matmul(A_flat, lambda_k, out=r_k)
|
|
71
74
|
r_k = y_flat - r_k
|
|
72
|
-
if isSavingEachIteration and
|
|
75
|
+
if isSavingEachIteration and it in save_indices:
|
|
73
76
|
lambda_history.append(lambda_k.clone().reshape(Z, X) * (norm_y / norm_A))
|
|
74
|
-
saved_indices.append(
|
|
77
|
+
saved_indices.append(it)
|
|
75
78
|
|
|
76
79
|
# Gradient préconditionné (inplace)
|
|
77
80
|
torch.matmul(A_flat.T, r_k, out=AT_r)
|