AOT-biomaps 2.9.188__tar.gz → 2.9.354__tar.gz
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.
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/AcousticTools.py +0 -2
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/StructuredWave.py +2 -2
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/_mainAcoustic.py +11 -6
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Experiment/ExperimentTools.py +69 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Experiment/Tomography.py +318 -12
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Experiment/_mainExperiment.py +95 -55
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_Optimizers/DEPIERRO.py +48 -13
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_Optimizers/LS.py +496 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_Optimizers/MAPEM.py +118 -38
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_Optimizers/MLEM.py +551 -0
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_Optimizers/PDHG.py +652 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/RelativeDifferences.py +10 -14
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/SparseSMatrix_CSR.py +274 -0
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/SparseSMatrix_SELL.py +360 -0
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_SparseSMatrix/__init__.py +2 -0
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AOT_biomaps_kernels.cubin +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AlgebraicRecon.py +243 -113
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/AnalyticRecon.py +419 -0
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/BayesianRecon.py +230 -0
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/PrimalDualRecon.py +263 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/ReconEnums.py +27 -2
- aot_biomaps-2.9.354/AOT_biomaps/AOT_Recon/ReconTools.py +692 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/__init__.py +1 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/_mainRecon.py +66 -55
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/__init__.py +4 -11
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps.egg-info/PKG-INFO +2 -1
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps.egg-info/SOURCES.txt +6 -1
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps.egg-info/requires.txt +1 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/PKG-INFO +2 -1
- aot_biomaps-2.9.354/setup.py +378 -0
- aot_biomaps-2.9.188/AOT_biomaps/AOT_Recon/AOT_Optimizers/LS.py +0 -103
- aot_biomaps-2.9.188/AOT_biomaps/AOT_Recon/AOT_Optimizers/MLEM.py +0 -262
- aot_biomaps-2.9.188/AOT_biomaps/AOT_Recon/AOT_Optimizers/PDHG.py +0 -221
- aot_biomaps-2.9.188/AOT_biomaps/AOT_Recon/AnalyticRecon.py +0 -154
- aot_biomaps-2.9.188/AOT_biomaps/AOT_Recon/BayesianRecon.py +0 -295
- aot_biomaps-2.9.188/AOT_biomaps/AOT_Recon/PrimalDualRecon.py +0 -200
- aot_biomaps-2.9.188/AOT_biomaps/AOT_Recon/ReconTools.py +0 -272
- aot_biomaps-2.9.188/setup.py +0 -202
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/AcousticEnums.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/FocusedWave.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/IrregularWave.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/PlaneWave.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Acoustic/__init__.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Experiment/Focus.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Experiment/__init__.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Optic/Absorber.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Optic/Laser.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Optic/OpticEnums.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Optic/__init__.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Optic/_mainOptic.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_Optimizers/__init__.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Huber.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Quadratic.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/__init__.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/AOT_Recon/DeepLearningRecon.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/Config.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps/Settings.py +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps.egg-info/dependency_links.txt +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/AOT_biomaps.egg-info/top_level.txt +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/README.md +0 -0
- {aot_biomaps-2.9.188 → aot_biomaps-2.9.354}/setup.cfg +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:
|
|
@@ -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:
|
|
@@ -652,7 +655,7 @@ class AcousticField(ABC):
|
|
|
652
655
|
"""
|
|
653
656
|
pass
|
|
654
657
|
|
|
655
|
-
def _load_field_h5(self, filePath):
|
|
658
|
+
def _load_field_h5(self, filePath,nameBlock):
|
|
656
659
|
"""
|
|
657
660
|
Load the 2D acoustic field from an H5 file.
|
|
658
661
|
|
|
@@ -663,8 +666,10 @@ class AcousticField(ABC):
|
|
|
663
666
|
- field (numpy.ndarray): The loaded acoustic field.
|
|
664
667
|
"""
|
|
665
668
|
try:
|
|
666
|
-
|
|
667
|
-
|
|
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][:]
|
|
668
673
|
except Exception as e:
|
|
669
674
|
print(f"Error in _load_field_h5 method: {e}")
|
|
670
675
|
raise
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
def calc_mat_os(xm, fx, bool_active_list, signal_type):
|
|
4
|
+
"""
|
|
5
|
+
xm : vecteur des positions réelles des éléments (en m)
|
|
6
|
+
fx : fréquence spatiale (en m^-1)
|
|
7
|
+
signal_type : 'cos' ou 'sin'
|
|
8
|
+
"""
|
|
9
|
+
num_els = len(xm)
|
|
10
|
+
num_cols = bool_active_list.shape[1]
|
|
11
|
+
|
|
12
|
+
if signal_type == 'cos':
|
|
13
|
+
mask = (np.cos(2 * np.pi * fx * xm) > 0).astype(float)
|
|
14
|
+
elif signal_type == 'sin':
|
|
15
|
+
mask = (np.sin(2 * np.pi * fx * xm) > 0).astype(float)
|
|
16
|
+
else:
|
|
17
|
+
mask = np.ones(num_els) # Sécurité
|
|
18
|
+
|
|
19
|
+
return np.tile(mask[:, np.newaxis], (1, num_cols))
|
|
20
|
+
|
|
21
|
+
def convert_to_hex_list(matrix):
|
|
22
|
+
"""
|
|
23
|
+
Convertit une matrice binaire en liste de strings hexa (paquets de 4 bits).
|
|
24
|
+
Chaque colonne devient une chaîne de caractères.
|
|
25
|
+
"""
|
|
26
|
+
n_els, n_scans = matrix.shape
|
|
27
|
+
|
|
28
|
+
# 1. Padding pour s'assurer que n_els est multiple de 4
|
|
29
|
+
remainder = n_els % 4
|
|
30
|
+
if remainder != 0:
|
|
31
|
+
padding = np.zeros((4 - remainder, n_scans))
|
|
32
|
+
matrix = np.vstack([matrix, padding])
|
|
33
|
+
|
|
34
|
+
# 2. Reshape pour isoler des blocs de 4 bits (nibbles)
|
|
35
|
+
# Shape résultante : (Nombre de blocs, 4 bits, Nombre de scans)
|
|
36
|
+
blocks = matrix.reshape(-1, 4, n_scans)
|
|
37
|
+
|
|
38
|
+
# 3. Calcul de la valeur décimale de chaque bloc (0 à 15)
|
|
39
|
+
# On considère le premier élément comme le bit de poids faible (LSB)
|
|
40
|
+
weights = np.array([1, 2, 4, 8]).reshape(1, 4, 1)
|
|
41
|
+
dec_values = np.sum(blocks * weights, axis=1).astype(int)
|
|
42
|
+
|
|
43
|
+
# 4. Conversion en caractères Hexadécimaux
|
|
44
|
+
# On définit la table de conversion pour la rapidité
|
|
45
|
+
hex_table = np.array(list("0123456789abcdef"))
|
|
46
|
+
hex_matrix = hex_table[dec_values]
|
|
47
|
+
|
|
48
|
+
# 5. Assemblage des chaînes (de l'élément N vers 0 pour l'ordre Shift Register standard)
|
|
49
|
+
return ["".join(hex_matrix[::-1, col]) for col in range(n_scans)]
|
|
50
|
+
|
|
51
|
+
def hex_to_binary_profile(hex_string, n_piezos=192):
|
|
52
|
+
hex_string = hex_string.strip().replace(" ", "").replace("\n", "")
|
|
53
|
+
if set(hex_string.lower()) == {'f'}:
|
|
54
|
+
return np.ones(n_piezos, dtype=int)
|
|
55
|
+
|
|
56
|
+
try:
|
|
57
|
+
n_char = len(hex_string)
|
|
58
|
+
n_bits = n_char * 4
|
|
59
|
+
binary_str = bin(int(hex_string, 16))[2:].zfill(n_bits)
|
|
60
|
+
if len(binary_str) < n_piezos:
|
|
61
|
+
# Tronquer/padder en fonction de la taille réelle de la sonde
|
|
62
|
+
binary_str = binary_str.ljust(n_piezos, '0')
|
|
63
|
+
elif len(binary_str) > n_piezos:
|
|
64
|
+
binary_str = binary_str[:n_piezos]
|
|
65
|
+
return np.array([int(b) for b in binary_str])
|
|
66
|
+
except ValueError:
|
|
67
|
+
return np.zeros(n_piezos, dtype=int)
|
|
68
|
+
|
|
69
|
+
|
|
@@ -2,16 +2,24 @@ from ._mainExperiment import Experiment
|
|
|
2
2
|
from AOT_biomaps.AOT_Acoustic.AcousticEnums import WaveType
|
|
3
3
|
from AOT_biomaps.AOT_Acoustic.StructuredWave import StructuredWave
|
|
4
4
|
from AOT_biomaps.Config import config
|
|
5
|
+
from AOT_biomaps.AOT_Experiment.ExperimentTools import calc_mat_os, convert_to_hex_list, hex_to_binary_profile
|
|
5
6
|
import os
|
|
6
7
|
import psutil
|
|
7
8
|
import numpy as np
|
|
8
9
|
import matplotlib.pyplot as plt
|
|
9
10
|
from tqdm import trange
|
|
11
|
+
import h5py
|
|
12
|
+
from scipy.io import loadmat, savemat
|
|
13
|
+
|
|
10
14
|
|
|
11
15
|
class Tomography(Experiment):
|
|
12
16
|
def __init__(self, **kwargs):
|
|
13
17
|
super().__init__(**kwargs)
|
|
14
18
|
self.patterns = None
|
|
19
|
+
self.theta = []
|
|
20
|
+
self.decimations = []
|
|
21
|
+
self.ActiveList = []
|
|
22
|
+
self.DelayLaw = []
|
|
15
23
|
|
|
16
24
|
# PUBLIC METHODS
|
|
17
25
|
def check(self):
|
|
@@ -47,7 +55,7 @@ class Tomography(Experiment):
|
|
|
47
55
|
return False, f"OpticImage phantom shape {self.OpticImage.phantom.shape} does not match AcousticFields shape {self.AcousticFields[0].field.shape[1:]}."
|
|
48
56
|
return True, "Experiment is correctly initialized."
|
|
49
57
|
|
|
50
|
-
def generateAcousticFields(self, fieldDataPath=None, show_log=True):
|
|
58
|
+
def generateAcousticFields(self, fieldDataPath=None, show_log=True, nameBlock=None):
|
|
51
59
|
"""
|
|
52
60
|
Generate the acoustic fields for simulation.
|
|
53
61
|
Args:
|
|
@@ -57,7 +65,30 @@ class Tomography(Experiment):
|
|
|
57
65
|
systemMatrix: A numpy array of the generated fields.
|
|
58
66
|
"""
|
|
59
67
|
if self.TypeAcoustic.value == WaveType.StructuredWave.value:
|
|
60
|
-
self.AcousticFields = self._generateAcousticFields_STRUCT_CPU(fieldDataPath, show_log)
|
|
68
|
+
self.AcousticFields = self._generateAcousticFields_STRUCT_CPU(fieldDataPath, show_log, nameBlock)
|
|
69
|
+
for i in range(len(self.AcousticFields)):
|
|
70
|
+
profile = hex_to_binary_profile(self.AcousticFields[i].getName_field()[6:-4], self.params.acoustic['num_elements'])
|
|
71
|
+
self.ActiveList.append(profile)
|
|
72
|
+
angle = self.AcousticFields[i].angle
|
|
73
|
+
self.theta.append(angle)
|
|
74
|
+
Delay = 1000 * (1/self.params.acoustic['c0']) * np.sin(np.deg2rad(angle)) * np.arange(1, self.params.acoustic['num_elements'] + 1) * self.params.acoustic['element_width']
|
|
75
|
+
self.DelayLaw.append(Delay - np.min(Delay))
|
|
76
|
+
|
|
77
|
+
if set(self.AcousticFields[i].getName_field()[6:-4].lower().replace(" ", "")) == {'f'}:
|
|
78
|
+
fs_key = 0.0 # fs_key est en mm^-1 (0.0 mm^-1)
|
|
79
|
+
else:
|
|
80
|
+
ft_prof = np.fft.fft(profile)
|
|
81
|
+
idx_max = np.argmax(np.abs(ft_prof[1:len(profile)//2])) + 1
|
|
82
|
+
freqs = np.fft.fftfreq(len(profile), d=self.params.general['dx'])
|
|
83
|
+
|
|
84
|
+
# freqs est en m^-1 car delta_x est en mètres.
|
|
85
|
+
fs_m_inv = abs(freqs[idx_max])
|
|
86
|
+
|
|
87
|
+
fs_key = fs_m_inv # Fréquence spatiale en mm^-1
|
|
88
|
+
|
|
89
|
+
# fs = n * dfx => n = fs / dfx with dfx = 1/(N*delta_x)
|
|
90
|
+
self.decimations.append(int(fs_key / (1/(len(profile)*self.params.general['dx']))))
|
|
91
|
+
|
|
61
92
|
else:
|
|
62
93
|
raise ValueError("Unsupported wave type.")
|
|
63
94
|
|
|
@@ -271,7 +302,7 @@ class Tomography(Experiment):
|
|
|
271
302
|
line = f"({coords}, {angles})\n"
|
|
272
303
|
file.write(line)
|
|
273
304
|
|
|
274
|
-
def generateActiveList(self, N):
|
|
305
|
+
def generateActiveList(self, N = None, decimations = None, angles = None):
|
|
275
306
|
"""
|
|
276
307
|
Génère une liste de patterns d'activation équilibrés et réguliers.
|
|
277
308
|
Args:
|
|
@@ -279,13 +310,139 @@ class Tomography(Experiment):
|
|
|
279
310
|
Returns:
|
|
280
311
|
list: Liste de strings au format "hex_angle".
|
|
281
312
|
"""
|
|
282
|
-
if
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
313
|
+
if decimations is not None and angles is not None:
|
|
314
|
+
self.patterns = self._genereate_patterns_from_decimations(decimations, angles)
|
|
315
|
+
elif N is not None and N > 1:
|
|
316
|
+
self.patterns = self._generate_patterns(N)
|
|
317
|
+
if not self._check_patterns(self.patterns):
|
|
318
|
+
raise ValueError("Generated patterns failed validation.")
|
|
319
|
+
else:
|
|
320
|
+
raise ValueError("Either N (>=2) or both decimations and angles must be provided for pattern generation.")
|
|
321
|
+
|
|
322
|
+
def saveAOsignals_matlab(self, filePath):
|
|
323
|
+
|
|
324
|
+
savemat(filePath, {'data': self.AOsignal_withTumor, 'thetas': self.theta, 'decimations': self.decimations, 'ActiveList' : self.ActiveList, 'DelayLaw': self.DelayLaw})
|
|
325
|
+
|
|
326
|
+
def selectAngles(self, angles):
|
|
327
|
+
|
|
328
|
+
if self.AOsignal_withTumor is None and self.AOsignal_withoutTumor is None:
|
|
329
|
+
raise ValueError("AO signals are not initialized. Please load or generate the AO signals first.")
|
|
330
|
+
if self.AcousticFields is None or len(self.AcousticFields) == 0:
|
|
331
|
+
raise ValueError("AcousticFields is not initialized. Please generate the system matrix first.")
|
|
332
|
+
newAcousticFields = []
|
|
333
|
+
index = []
|
|
334
|
+
for i,field in enumerate(self.AcousticFields):
|
|
335
|
+
if field.angle in angles:
|
|
336
|
+
newAcousticFields.append(field)
|
|
337
|
+
index.append(i)
|
|
338
|
+
if self.AOsignal_withTumor is not None:
|
|
339
|
+
self.AOsignal_withTumor = self.AOsignal_withTumor[:, index]
|
|
340
|
+
if self.AOsignal_withoutTumor is not None:
|
|
341
|
+
self.AOsignal_withoutTumor = self.AOsignal_withoutTumor[:, index]
|
|
342
|
+
self.AcousticFields = newAcousticFields
|
|
343
|
+
|
|
344
|
+
def selectPatterns(self, pattern_names):
|
|
345
|
+
if self.AOsignal_withTumor is None and self.AOsignal_withoutTumor is None:
|
|
346
|
+
raise ValueError("AO signals are not initialized. Please load or generate the AO signals first.")
|
|
347
|
+
if self.AcousticFields is None or len(self.AcousticFields) == 0:
|
|
348
|
+
raise ValueError("AcousticFields is not initialized. Please generate the system matrix first.")
|
|
349
|
+
newAcousticFields = []
|
|
350
|
+
index = []
|
|
351
|
+
for i,field in enumerate(self.AcousticFields):
|
|
352
|
+
if field.pattern.activeList in pattern_names:
|
|
353
|
+
newAcousticFields.append(field)
|
|
354
|
+
index.append(i)
|
|
355
|
+
if self.AOsignal_withTumor is not None:
|
|
356
|
+
self.AOsignal_withTumor = self.AOsignal_withTumor[:, index]
|
|
357
|
+
if self.AOsignal_withoutTumor is not None:
|
|
358
|
+
self.AOsignal_withoutTumor = self.AOsignal_withoutTumor[:, index]
|
|
359
|
+
self.AcousticFields = newAcousticFields
|
|
360
|
+
|
|
361
|
+
def selectRandom(self,N):
|
|
362
|
+
if self.AOsignal_withTumor is None and self.AOsignal_withoutTumor is None:
|
|
363
|
+
raise ValueError("AO signals are not initialized. Please load or generate the AO signals first.")
|
|
364
|
+
if self.AcousticFields is None or len(self.AcousticFields) == 0:
|
|
365
|
+
raise ValueError("AcousticFields is not initialized. Please generate the system matrix first.")
|
|
366
|
+
if N > len(self.AcousticFields):
|
|
367
|
+
raise ValueError("N is larger than the number of available AcousticFields.")
|
|
368
|
+
indices = np.random.choice(len(self.AcousticFields), size=N, replace=False)
|
|
369
|
+
newAcousticFields = [self.AcousticFields[i] for i in indices]
|
|
370
|
+
if self.AOsignal_withTumor is not None:
|
|
371
|
+
self.AOsignal_withTumor = self.AOsignal_withTumor[:, indices]
|
|
372
|
+
if self.AOsignal_withoutTumor is not None:
|
|
373
|
+
self.AOsignal_withoutTumor = self.AOsignal_withoutTumor[:, indices]
|
|
374
|
+
self.AcousticFields = newAcousticFields
|
|
375
|
+
|
|
376
|
+
def _genereate_patterns_from_decimations(self, decimations, angles):
|
|
377
|
+
if isinstance(decimations, list):
|
|
378
|
+
decimations = np.array(decimations)
|
|
379
|
+
if isinstance(angles, list):
|
|
380
|
+
angles = np.array(angles)
|
|
381
|
+
|
|
382
|
+
angles = np.sort(angles)
|
|
383
|
+
decimations = np.sort(decimations)
|
|
384
|
+
|
|
385
|
+
num_elements = self.params.acoustic['num_elements']
|
|
386
|
+
Width = self.params.acoustic['element_width'] # en m
|
|
387
|
+
kerf = self.params.acoustic.get('kerf', 0.00000) # en m
|
|
388
|
+
Nactuators = self.params.acoustic['num_elements']
|
|
389
|
+
|
|
390
|
+
# --- Calcul du nombre de Scans ---
|
|
391
|
+
if 0 in decimations:
|
|
392
|
+
Nscans = 4 * len(angles) * (len(decimations) - 1) + len(angles)
|
|
393
|
+
else:
|
|
394
|
+
Nscans = 4 * len(angles) * len(decimations)
|
|
395
|
+
|
|
396
|
+
ActiveLIST = np.ones((num_elements, Nscans))
|
|
397
|
+
|
|
398
|
+
# --- Calcul des positions centrées des éléments (en m) ---
|
|
399
|
+
Xc = (Width + (Nactuators - 1) * (kerf + Width)) / 2
|
|
400
|
+
Xm = np.array([Width * (i - 1) + Width / 2 - Xc for i in range(1, Nactuators + 1)])
|
|
401
|
+
|
|
402
|
+
# --- Gestion de l'onde plane (tous les piezo ON) au début ---
|
|
403
|
+
if 0 in decimations:
|
|
404
|
+
I_plane = np.arange(len(angles))
|
|
405
|
+
ActiveLIST[:, I_plane] = 1 # Tous les piezo ON pour les len(angles) premières colonnes
|
|
287
406
|
|
|
288
|
-
|
|
407
|
+
# --- Traitement des décimations non nulles ---
|
|
408
|
+
active_decimations = decimations[decimations != 0]
|
|
409
|
+
dFx = 1 / (Nactuators * Width) # fx de base (en m^-1)
|
|
410
|
+
|
|
411
|
+
for i_dec in range(len(active_decimations)):
|
|
412
|
+
# Décalage des indices pour placer les motifs modulés après l'onde plane
|
|
413
|
+
I = np.arange(len(angles)) + len(angles) + (i_dec * 4 * len(angles))
|
|
414
|
+
|
|
415
|
+
Icos = I
|
|
416
|
+
Incos = I + 1* len(angles)
|
|
417
|
+
Isin = I + 3 * len(angles)
|
|
418
|
+
Insin = I + 2 * len(angles)
|
|
419
|
+
|
|
420
|
+
fx = dFx * active_decimations[i_dec]
|
|
421
|
+
|
|
422
|
+
# Appliquer les motifs modulés
|
|
423
|
+
ActiveLIST[:, Icos] = calc_mat_os(Xm, fx, ActiveLIST[:, Icos[:1]], 'cos')
|
|
424
|
+
ActiveLIST[:, Incos] = 1 - ActiveLIST[:, Icos]
|
|
425
|
+
ActiveLIST[:, Isin] = calc_mat_os(Xm, fx, ActiveLIST[:, Isin[:1]], 'sin')
|
|
426
|
+
ActiveLIST[:, Insin] = 1 - ActiveLIST[:, Isin]
|
|
427
|
+
|
|
428
|
+
# --- Conversion au format attendu ---
|
|
429
|
+
hexa_list = convert_to_hex_list(ActiveLIST)
|
|
430
|
+
|
|
431
|
+
def format_angle(a):
|
|
432
|
+
return f"{'1' if a < 0 else '0'}{abs(a):02d}"
|
|
433
|
+
|
|
434
|
+
patterns = []
|
|
435
|
+
print(f"Generating {Nscans} patterns from decimations and angles...")
|
|
436
|
+
for i in range(Nscans):
|
|
437
|
+
angle_val = angles[i % len(angles)]
|
|
438
|
+
hex_pattern = hexa_list[i]
|
|
439
|
+
pair = f"{hex_pattern}_{format_angle(angle_val)}"
|
|
440
|
+
patterns.append({"fileName": pair})
|
|
441
|
+
|
|
442
|
+
return patterns
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
def _generate_patterns(self, N,angles = None):
|
|
289
446
|
def format_angle(a):
|
|
290
447
|
return f"{'1' if a < 0 else '0'}{abs(a):02d}"
|
|
291
448
|
|
|
@@ -296,7 +453,13 @@ class Tomography(Experiment):
|
|
|
296
453
|
return hex_string
|
|
297
454
|
|
|
298
455
|
num_elements = self.params.acoustic['num_elements']
|
|
299
|
-
|
|
456
|
+
if angles is None:
|
|
457
|
+
angle_choices = list(range(-20, 21))
|
|
458
|
+
else:
|
|
459
|
+
# convert np.array to list if necessary
|
|
460
|
+
if isinstance(angles, np.ndarray):
|
|
461
|
+
angles = angles.tolist()
|
|
462
|
+
angle_choices = angles
|
|
300
463
|
|
|
301
464
|
# 1. Trouver TOUS les diviseurs PAIRS de num_elements (y compris num_elements)
|
|
302
465
|
divs = [d for d in range(2, num_elements + 1) if num_elements % d == 0 and d % 2 == 0]
|
|
@@ -388,8 +551,82 @@ class Tomography(Experiment):
|
|
|
388
551
|
|
|
389
552
|
return True
|
|
390
553
|
|
|
554
|
+
def applyApodisation(self, alpha=0.3, divergence_deg=0.5):
|
|
555
|
+
"""
|
|
556
|
+
Applique une apodisation dynamique sur les champs acoustiques stockés dans l'objet.
|
|
557
|
+
L'apodisation suit l'angle d'émission et la divergence naturelle du faisceau pour
|
|
558
|
+
supprimer les lobes de diffraction (artefacts de bord) sans toucher au signal utile.
|
|
559
|
+
Args:
|
|
560
|
+
probe_width (float): Largeur physique active de la sonde (ex: 40e-3 pour 40mm).
|
|
561
|
+
alpha (float): Paramètre de Tukey (0.0=rectangle, 1.0=hann). 0.3 est un bon compromis.
|
|
562
|
+
divergence_deg (float): Angle d'ouverture du masque pour suivre l'élargissement du faisceau.
|
|
563
|
+
0.0 = Droit, 0.5 = Légère ouverture (conseillé).
|
|
564
|
+
"""
|
|
565
|
+
print(f"Applying apodization (Alpha={alpha}, Div={divergence_deg}°) on {len(self.AcousticFields)} fields...")
|
|
566
|
+
|
|
567
|
+
probe_width = self.params.acoustic['num_elements'] * self.params.acoustic['element_width']
|
|
568
|
+
|
|
569
|
+
for i in trange(len(self.AcousticFields), desc="Apodisation"):
|
|
570
|
+
# 1. Récupération des données et de l'angle
|
|
571
|
+
field = self.AcousticFields[i].field # Peut être (Z, X) ou (Time, Z, X)
|
|
572
|
+
angle = self.AcousticFields[i].angle # L'angle de l'onde plane
|
|
573
|
+
|
|
574
|
+
# 2. Récupération ou construction des axes physiques
|
|
575
|
+
nz, nx = field.shape[-2:]
|
|
576
|
+
|
|
577
|
+
if hasattr(self, 'x_axis') and self.x_axis is not None:
|
|
578
|
+
x_axis = self.x_axis
|
|
579
|
+
else:
|
|
580
|
+
# Génération par défaut centrée sur 0 (ex: -20mm à +20mm)
|
|
581
|
+
x_axis = np.linspace(-probe_width/2, probe_width/2, nx)
|
|
582
|
+
|
|
583
|
+
if hasattr(self, 'z_axis') and self.z_axis is not None:
|
|
584
|
+
z_axis = self.z_axis
|
|
585
|
+
else:
|
|
586
|
+
# Génération par défaut (ex: 0 à 40mm, basé sur un pitch standard ou arbitraire)
|
|
587
|
+
estimated_depth = 40e-3 # Valeur arbitraire si inconnue
|
|
588
|
+
z_axis = np.linspace(0, estimated_depth, nz)
|
|
589
|
+
|
|
590
|
+
# 3. Préparation des grilles pour le masque
|
|
591
|
+
Z, X = np.meshgrid(z_axis, x_axis, indexing='ij')
|
|
592
|
+
|
|
593
|
+
# 4. Calcul de la géométrie orientée (Steering)
|
|
594
|
+
angle_rad = np.deg2rad(angle)
|
|
595
|
+
X_aligned = X - Z * np.tan(angle_rad)
|
|
596
|
+
|
|
597
|
+
# 5. Calcul de la largeur dynamique du masque (Divergence)
|
|
598
|
+
div_rad = np.deg2rad(divergence_deg)
|
|
599
|
+
current_half_width = (probe_width / 2.0) + Z * np.tan(div_rad)
|
|
600
|
+
|
|
601
|
+
# 6. Normalisation et création du masque Tukey
|
|
602
|
+
X_norm = np.divide(X_aligned, current_half_width, out=np.zeros_like(X_aligned), where=current_half_width!=0)
|
|
603
|
+
|
|
604
|
+
mask = np.zeros_like(X_norm)
|
|
605
|
+
plateau_threshold = 1.0 * (1 - alpha)
|
|
606
|
+
|
|
607
|
+
# Zone centrale (plateau = 1)
|
|
608
|
+
mask[np.abs(X_norm) <= plateau_threshold] = 1.0
|
|
609
|
+
|
|
610
|
+
# Zone de transition (cosinus)
|
|
611
|
+
transition_indices = (np.abs(X_norm) > plateau_threshold) & (np.abs(X_norm) <= 1.0)
|
|
612
|
+
if np.any(transition_indices):
|
|
613
|
+
x_trans = np.abs(X_norm[transition_indices]) - plateau_threshold
|
|
614
|
+
width_trans = 1.0 * alpha
|
|
615
|
+
mask[transition_indices] = 0.5 * (1 + np.cos(np.pi * x_trans / width_trans))
|
|
616
|
+
|
|
617
|
+
# 7. Application du masque (Gestion 2D vs 3D)
|
|
618
|
+
if field.ndim == 3:
|
|
619
|
+
field_apodized = field * mask[np.newaxis, :, :]
|
|
620
|
+
else:
|
|
621
|
+
field_apodized = field * mask
|
|
622
|
+
|
|
623
|
+
# 8. Mise à jour de l'objet
|
|
624
|
+
self.AcousticFields[i].field = field_apodized
|
|
625
|
+
|
|
626
|
+
print("Apodisation done.")
|
|
627
|
+
|
|
391
628
|
# PRIVATE METHODS
|
|
392
|
-
def _generateAcousticFields_STRUCT_CPU(self, fieldDataPath=None, show_log=False):
|
|
629
|
+
def _generateAcousticFields_STRUCT_CPU(self, fieldDataPath=None, show_log=False, nameBlock=None):
|
|
393
630
|
if self.patterns is None:
|
|
394
631
|
raise ValueError("patterns is not initialized. Please load or generate the active list first.")
|
|
395
632
|
listAcousticFields = []
|
|
@@ -415,7 +652,7 @@ class Tomography(Experiment):
|
|
|
415
652
|
if pathField is not None and os.path.exists(pathField):
|
|
416
653
|
progress_bar.set_postfix_str(f"Loading field - {AcousticField.getName_field()} -- Memory used: {memory.percent}%")
|
|
417
654
|
try:
|
|
418
|
-
AcousticField.load_field(fieldDataPath, self.FormatSave)
|
|
655
|
+
AcousticField.load_field(fieldDataPath, self.FormatSave,nameBlock)
|
|
419
656
|
except:
|
|
420
657
|
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
658
|
AcousticField.generate_field(show_log=show_log)
|
|
@@ -432,4 +669,73 @@ class Tomography(Experiment):
|
|
|
432
669
|
AcousticField.save_field(fieldDataPath)
|
|
433
670
|
listAcousticFields.append(AcousticField)
|
|
434
671
|
progress_bar.set_postfix_str("")
|
|
672
|
+
|
|
435
673
|
return listAcousticFields
|
|
674
|
+
|
|
675
|
+
def load_experimentalAO(self, pathAO, withTumor = True, h5name='AOsignal'):
|
|
676
|
+
"""
|
|
677
|
+
Load experimental AO signals from specified file paths.
|
|
678
|
+
Args:
|
|
679
|
+
path_withTumor: Path to the AO signal with tumor.
|
|
680
|
+
path_withoutTumor: Path to the AO signal without tumor.
|
|
681
|
+
"""
|
|
682
|
+
if not os.path.exists(pathAO):
|
|
683
|
+
raise FileNotFoundError(f"File {pathAO} not found.")
|
|
684
|
+
|
|
685
|
+
if pathAO.endswith('.npy'):
|
|
686
|
+
ao_signal = np.load(pathAO)
|
|
687
|
+
elif pathAO.endswith('.h5'):
|
|
688
|
+
with h5py.File(pathAO, 'r') as f:
|
|
689
|
+
if h5name not in f:
|
|
690
|
+
raise KeyError(f"Dataset '{h5name}' not found in the HDF5 file.")
|
|
691
|
+
ao_signal = f[h5name][:]
|
|
692
|
+
elif pathAO.endswith('.mat'):
|
|
693
|
+
mat_data = loadmat(pathAO)
|
|
694
|
+
if h5name not in mat_data:
|
|
695
|
+
raise KeyError(f"Dataset '{h5name}' not found in the .mat file.")
|
|
696
|
+
ao_signal = mat_data[h5name]
|
|
697
|
+
elif pathAO.endswith('.hdr'):
|
|
698
|
+
ao_signal = self._loadAOSignal(pathAO)
|
|
699
|
+
else:
|
|
700
|
+
raise ValueError("Unsupported file format. Supported formats are: .npy, .h5, .mat, .hdr")
|
|
701
|
+
|
|
702
|
+
if withTumor:
|
|
703
|
+
self.AOsignal_withTumor = ao_signal
|
|
704
|
+
else:
|
|
705
|
+
self.AOsignal_withoutTumor = ao_signal
|
|
706
|
+
|
|
707
|
+
def check_experimentalAO(self, activeListPath, withTumor=True):
|
|
708
|
+
"""
|
|
709
|
+
Check if the experimental AO signals are correctly initialized.
|
|
710
|
+
"""
|
|
711
|
+
if withTumor:
|
|
712
|
+
if self.AOsignal_withTumor is None:
|
|
713
|
+
raise ValueError("Experimental AOsignal with tumor is not initialized. Please load the experimental AO signal with tumor first.")
|
|
714
|
+
else:
|
|
715
|
+
if self.AOsignal_withoutTumor is None:
|
|
716
|
+
raise ValueError("Experimental AOsignal without tumor is not initialized. Please load the experimental AO signal without tumor first.")
|
|
717
|
+
if self.AcousticFields is not None:
|
|
718
|
+
# get min time shape between all AO signals
|
|
719
|
+
print()
|
|
720
|
+
|
|
721
|
+
if self.AcousticFields[0].field.shape[0] > self.AOsignal_withTumor.shape[0]:
|
|
722
|
+
self.cutAcousticFields(max_t=self.AOsignal_withTumor.shape[0]/float(self.params.acoustic['f_saving']))
|
|
723
|
+
else:
|
|
724
|
+
for i in range(len(self.AcousticFields)):
|
|
725
|
+
min_time_shape = min(self.AcousticFields[i].field.shape[0])
|
|
726
|
+
if withTumor:
|
|
727
|
+
self.AOsignal_withTumor = self.AOsignal_withTumor[:min_time_shape, :]
|
|
728
|
+
else:
|
|
729
|
+
self.AOsignal_withoutTumor = self.AOsignal_withoutTumor[:min_time_shape, :]
|
|
730
|
+
|
|
731
|
+
for field in self.AcousticFields:
|
|
732
|
+
if activeListPath is not None:
|
|
733
|
+
with open(activeListPath, 'r') as file:
|
|
734
|
+
lines = file.readlines()
|
|
735
|
+
expected_name = lines[self.AcousticFields.index(field)].strip()
|
|
736
|
+
nameField = field.getName_field()
|
|
737
|
+
if nameField.startswith("field_"):
|
|
738
|
+
nameField = nameField[len("field_"):]
|
|
739
|
+
if nameField != expected_name:
|
|
740
|
+
raise ValueError(f"Field name {nameField} does not match the expected name {expected_name} from the active list.")
|
|
741
|
+
print("Experimental AO signals are correctly initialized.")
|