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.
- AOT_biomaps/AOT_Acoustic/AcousticEnums.py +64 -0
- AOT_biomaps/AOT_Acoustic/AcousticTools.py +221 -0
- AOT_biomaps/AOT_Acoustic/FocusedWave.py +244 -0
- AOT_biomaps/AOT_Acoustic/IrregularWave.py +66 -0
- AOT_biomaps/AOT_Acoustic/PlaneWave.py +43 -0
- AOT_biomaps/AOT_Acoustic/StructuredWave.py +392 -0
- AOT_biomaps/AOT_Acoustic/__init__.py +15 -0
- AOT_biomaps/AOT_Acoustic/_mainAcoustic.py +978 -0
- AOT_biomaps/AOT_Experiment/Focus.py +55 -0
- AOT_biomaps/AOT_Experiment/Tomography.py +505 -0
- AOT_biomaps/AOT_Experiment/__init__.py +9 -0
- AOT_biomaps/AOT_Experiment/_mainExperiment.py +532 -0
- AOT_biomaps/AOT_Optic/Absorber.py +24 -0
- AOT_biomaps/AOT_Optic/Laser.py +70 -0
- AOT_biomaps/AOT_Optic/OpticEnums.py +17 -0
- AOT_biomaps/AOT_Optic/__init__.py +10 -0
- AOT_biomaps/AOT_Optic/_mainOptic.py +204 -0
- AOT_biomaps/AOT_Recon/AOT_Optimizers/DEPIERRO.py +191 -0
- AOT_biomaps/AOT_Recon/AOT_Optimizers/LS.py +106 -0
- AOT_biomaps/AOT_Recon/AOT_Optimizers/MAPEM.py +456 -0
- AOT_biomaps/AOT_Recon/AOT_Optimizers/MLEM.py +333 -0
- AOT_biomaps/AOT_Recon/AOT_Optimizers/PDHG.py +221 -0
- AOT_biomaps/AOT_Recon/AOT_Optimizers/__init__.py +5 -0
- AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Huber.py +90 -0
- AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Quadratic.py +86 -0
- AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/RelativeDifferences.py +59 -0
- AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/__init__.py +3 -0
- AOT_biomaps/AOT_Recon/AlgebraicRecon.py +1023 -0
- AOT_biomaps/AOT_Recon/AnalyticRecon.py +154 -0
- AOT_biomaps/AOT_Recon/BayesianRecon.py +230 -0
- AOT_biomaps/AOT_Recon/DeepLearningRecon.py +35 -0
- AOT_biomaps/AOT_Recon/PrimalDualRecon.py +210 -0
- AOT_biomaps/AOT_Recon/ReconEnums.py +375 -0
- AOT_biomaps/AOT_Recon/ReconTools.py +273 -0
- AOT_biomaps/AOT_Recon/__init__.py +11 -0
- AOT_biomaps/AOT_Recon/_mainRecon.py +288 -0
- AOT_biomaps/Config.py +95 -0
- AOT_biomaps/Settings.py +45 -13
- AOT_biomaps/__init__.py +271 -18
- aot_biomaps-2.9.233.dist-info/METADATA +22 -0
- aot_biomaps-2.9.233.dist-info/RECORD +43 -0
- {AOT_biomaps-2.1.3.dist-info → aot_biomaps-2.9.233.dist-info}/WHEEL +1 -1
- AOT_biomaps/AOT_Acoustic.py +0 -1881
- AOT_biomaps/AOT_Experiment.py +0 -541
- AOT_biomaps/AOT_Optic.py +0 -219
- AOT_biomaps/AOT_Reconstruction.py +0 -1416
- AOT_biomaps/config.py +0 -54
- AOT_biomaps-2.1.3.dist-info/METADATA +0 -20
- AOT_biomaps-2.1.3.dist-info/RECORD +0 -11
- {AOT_biomaps-2.1.3.dist-info → aot_biomaps-2.9.233.dist-info}/top_level.txt +0 -0
AOT_biomaps/AOT_Acoustic.py
DELETED
|
@@ -1,1881 +0,0 @@
|
|
|
1
|
-
import scipy.io
|
|
2
|
-
import numpy as np
|
|
3
|
-
import h5py
|
|
4
|
-
from scipy.signal import hilbert
|
|
5
|
-
from math import ceil, floor
|
|
6
|
-
import os
|
|
7
|
-
from kwave.kgrid import kWaveGrid
|
|
8
|
-
from kwave.kmedium import kWaveMedium
|
|
9
|
-
from kwave.ksource import kSource
|
|
10
|
-
from kwave.ksensor import kSensor
|
|
11
|
-
from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D
|
|
12
|
-
from kwave.kspaceFirstOrder2D import kspaceFirstOrder2D
|
|
13
|
-
from kwave.utils.signals import tone_burst
|
|
14
|
-
from kwave.options.simulation_options import SimulationOptions
|
|
15
|
-
from kwave.options.simulation_execution_options import SimulationExecutionOptions
|
|
16
|
-
import matplotlib.pyplot as plt
|
|
17
|
-
import matplotlib.animation as animation
|
|
18
|
-
import matplotlib as mpl
|
|
19
|
-
from tempfile import gettempdir
|
|
20
|
-
from .config import config
|
|
21
|
-
import AOT_biomaps.Settings
|
|
22
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
23
|
-
import time
|
|
24
|
-
|
|
25
|
-
if config.get_process() == 'gpu':
|
|
26
|
-
import cupy as cp
|
|
27
|
-
from cupyx.scipy.signal import hilbert as cp_hilbert
|
|
28
|
-
import pynvml
|
|
29
|
-
from scipy.signal import hilbert as np_hilbert
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
from abc import ABC, abstractmethod
|
|
33
|
-
from enum import Enum
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
class TypeSim(Enum):
|
|
37
|
-
"""
|
|
38
|
-
Enum for the type of simulation to be performed.
|
|
39
|
-
|
|
40
|
-
Selection of simulation types:
|
|
41
|
-
- KWAVE: k-Wave simulation.
|
|
42
|
-
- FIELD2: Field II simulation.
|
|
43
|
-
- HYDRO: Hydrophone acquisition.
|
|
44
|
-
"""
|
|
45
|
-
KWAVE = 'k-wave'
|
|
46
|
-
"""k-Wave simulation."""
|
|
47
|
-
|
|
48
|
-
FIELD2 = 'Field2'
|
|
49
|
-
"""Field II simulation."""
|
|
50
|
-
|
|
51
|
-
HYDRO = 'Hydrophone'
|
|
52
|
-
"""Hydrophone acquisition."""
|
|
53
|
-
|
|
54
|
-
class Dim(Enum):
|
|
55
|
-
"""
|
|
56
|
-
Enum for the dimension of the acoustic field.
|
|
57
|
-
|
|
58
|
-
Selection of dimensions:
|
|
59
|
-
- D2: 2D field.
|
|
60
|
-
- D3: 3D field.
|
|
61
|
-
"""
|
|
62
|
-
D2 = '2D'
|
|
63
|
-
"""2D field."""
|
|
64
|
-
D3 = '3D'
|
|
65
|
-
"""3D field."""
|
|
66
|
-
|
|
67
|
-
class FormatSave(Enum):
|
|
68
|
-
"""
|
|
69
|
-
Enum for different file formats to save the acoustic field.
|
|
70
|
-
|
|
71
|
-
Selection of file formats:
|
|
72
|
-
- HDR_IMG: Interfile format (.hdr and .img).
|
|
73
|
-
- H5: HDF5 format (.h5).
|
|
74
|
-
- NPY: NumPy format (.npy).
|
|
75
|
-
"""
|
|
76
|
-
HDR_IMG = '.hdr'
|
|
77
|
-
"""Interfile format (.hdr and .img)."""
|
|
78
|
-
H5 = '.h5'
|
|
79
|
-
"""HDF5 format (.h5)."""
|
|
80
|
-
NPY = '.npy'
|
|
81
|
-
"""NumPy format (.npy)."""
|
|
82
|
-
|
|
83
|
-
class WaveType(Enum):
|
|
84
|
-
"""
|
|
85
|
-
Enum for different types of acoustic waves.
|
|
86
|
-
|
|
87
|
-
Selection of wave types:
|
|
88
|
-
- FocusedWave: A wave type where the energy is focused at a specific point.
|
|
89
|
-
- StructuredWave: A wave type characterized by a specific pattern or structure.
|
|
90
|
-
- PlaneWave: A wave type where the wavefronts are parallel and travel in a single direction.
|
|
91
|
-
"""
|
|
92
|
-
FocusedWave = 'focus'
|
|
93
|
-
"""A wave type where the energy is focused at a specific point."""
|
|
94
|
-
StructuredWave = 'structured'
|
|
95
|
-
"""A wave type characterized by a specific pattern or structure."""
|
|
96
|
-
PlaneWave = 'plane'
|
|
97
|
-
"""A wave type where the wavefronts are parallel and travel in a single direction."""
|
|
98
|
-
|
|
99
|
-
####### ABSTRACT CLASS #######
|
|
100
|
-
|
|
101
|
-
class AcousticField(ABC):
|
|
102
|
-
"""
|
|
103
|
-
Abstract class to generate and manipulate acoustic fields for ultrasound imaging.
|
|
104
|
-
Provides methods to initialize parameters, generate fields, save and load data, and calculate envelopes.
|
|
105
|
-
|
|
106
|
-
Principal parameters:
|
|
107
|
-
- field: Acoustic field data.
|
|
108
|
-
- enveloppe: Analytic envelope squared of the acoustic field.
|
|
109
|
-
- burst: Burst signal used for generating the field for each piezo elements.
|
|
110
|
-
- delayedSignal: Delayed burst signal for each piezo element.
|
|
111
|
-
- medium: Medium properties for k-Wave simulation. Because field2 and Hydrophone simulation are not implemented yet, this attribute is set to None for these types of simulation.
|
|
112
|
-
"""
|
|
113
|
-
|
|
114
|
-
def __init__(self, params):
|
|
115
|
-
"""
|
|
116
|
-
Initialize global properties of the AcousticField object.
|
|
117
|
-
|
|
118
|
-
Parameters:
|
|
119
|
-
- typeSim (TypeSim): Type of simulation to be performed. Options include KWAVE, FIELD2, and HYDRO. Default is TypeSim.KWAVE.
|
|
120
|
-
- dim (Dim): Dimension of the acoustic field. Can be 2D or 3D. Default is Dim.D2.
|
|
121
|
-
- c0 (float): Speed of sound in the medium, specified in meters per second (m/s). Default is 1540 m/s.
|
|
122
|
-
- f_US (float): Frequency of the ultrasound signal, specified in Hertz (Hz). Default is 6 MHz.
|
|
123
|
-
- f_AQ (float): Frequency of data acquisition, specified in Hertz (Hz). Default is 180 MHz.
|
|
124
|
-
- f_saving (float): Frequency at which the acoustic field data is saved, specified in Hertz (Hz). Default is 10 MHz.
|
|
125
|
-
- num_cycles (int): Number of cycles in the burst signal. Default is 4 cycles.
|
|
126
|
-
- num_elements (int): Number of elements in the transducer array. Default is 192 elements.
|
|
127
|
-
- element_width (float): Width of each transducer element, specified in meters (m). Default is 0.2 mm.
|
|
128
|
-
- element_height (float): Height of each transducer element, specified in meters (m). Default is 6 mm.
|
|
129
|
-
- Xrange (list of float): Range of X coordinates for the acoustic field, specified in meters (m). Default is from -20 mm to 20 mm.
|
|
130
|
-
- Yrange (list of float, optional): Range of Y coordinates for the acoustic field, specified in meters (m). Default is None, indicating no specific Y range.
|
|
131
|
-
- Zrange (list of float): Range of Z coordinates for the acoustic field, specified in meters (m). Default is from 0 m to 37 mm.
|
|
132
|
-
"""
|
|
133
|
-
required_keys = [
|
|
134
|
-
'c0', 'f_US', 'f_AQ', 'f_saving', 'num_cycles', 'num_elements',
|
|
135
|
-
'element_width', 'element_height', 'Xrange', 'Zrange', 'dim',
|
|
136
|
-
'typeSim', 'dx', 'dz'
|
|
137
|
-
]
|
|
138
|
-
|
|
139
|
-
# Verify required keys
|
|
140
|
-
try:
|
|
141
|
-
if params != None:
|
|
142
|
-
for key in required_keys:
|
|
143
|
-
if key not in params.acoustic and key not in params.general:
|
|
144
|
-
raise ValueError(f"{key} must be provided in the parameters.")
|
|
145
|
-
except ValueError as e:
|
|
146
|
-
print(f"Initialization error: {e}")
|
|
147
|
-
raise
|
|
148
|
-
if params != None:
|
|
149
|
-
if type(params) != AOT_biomaps.Settings.Params:
|
|
150
|
-
raise TypeError("params must be an instance of the Params class")
|
|
151
|
-
|
|
152
|
-
self.params = {
|
|
153
|
-
'c0': params.acoustic['c0'],
|
|
154
|
-
'f_US': int(float(params.acoustic['f_US'])),
|
|
155
|
-
'f_AQ': params.acoustic['f_AQ'],
|
|
156
|
-
'f_saving': int(float(params.acoustic['f_saving'])),
|
|
157
|
-
'num_cycles': params.acoustic['num_cycles'],
|
|
158
|
-
'num_elements': params.acoustic['num_elements'],
|
|
159
|
-
'element_width': params.acoustic['element_width'],
|
|
160
|
-
'element_height': params.acoustic['element_height'],
|
|
161
|
-
'Xrange': params.general['Xrange'],
|
|
162
|
-
'Yrange': params.general['Yrange'],
|
|
163
|
-
'Zrange': params.general['Zrange'],
|
|
164
|
-
'dim': params.acoustic['dim'],
|
|
165
|
-
'typeSim': params.acoustic['typeSim'],
|
|
166
|
-
'dx': params.general['dx'],
|
|
167
|
-
'dy': params.general['dy'] if params.general['Yrange'] is not None else None,
|
|
168
|
-
'dz': params.general['dz'],
|
|
169
|
-
'Nx': int(np.round((params.general['Xrange'][1] - params.general['Xrange'][0])/params.general['dx'])),
|
|
170
|
-
'Ny': int(np.round((params.general['Yrange'][1] - params.general['Yrange'][0])/params.general['dy'])) if params.general['Yrange'] is not None else 1,
|
|
171
|
-
'Nz': int(np.round((params.general['Zrange'][1] - params.general['Zrange'][0])/params.general['dz'])),
|
|
172
|
-
'probeWidth': params.acoustic['num_elements'] * params.acoustic['element_width'],
|
|
173
|
-
'IsAbsorbingMedium': params.acoustic['isAbsorbingMedium'],
|
|
174
|
-
}
|
|
175
|
-
self.kgrid = kWaveGrid([self.params["Nx"], self.params["Nz"]], [self.params["dx"], self.params["dz"]])
|
|
176
|
-
if params.acoustic['f_AQ'] == "AUTO":
|
|
177
|
-
|
|
178
|
-
self.kgrid.makeTime(self.params['c0'])
|
|
179
|
-
|
|
180
|
-
self.params['f_AQ'] = int(1/self.kgrid.dt)
|
|
181
|
-
else:
|
|
182
|
-
Nt = ceil((self.params['Zrange'][1] - self.params['Zrange'][0])*float(params.acoustic['f_AQ']) / self.params['c0'])
|
|
183
|
-
|
|
184
|
-
self.kgrid.setTime(Nt,1/float(params.acoustic['f_AQ']))
|
|
185
|
-
self.params['f_AQ'] = int(float(params.acoustic['f_AQ']))
|
|
186
|
-
|
|
187
|
-
self._generate_burst_signal()
|
|
188
|
-
if self.params["dim"] == Dim.D3 and self.params["Yrange"] is None:
|
|
189
|
-
raise ValueError("Yrange must be provided for 3D fields.")
|
|
190
|
-
if self.params['typeSim'] == TypeSim.KWAVE.value:
|
|
191
|
-
if self.params ['IsAbsorbingMedium'] == True:
|
|
192
|
-
self.medium = kWaveMedium(
|
|
193
|
-
sound_speed=self.params['c0'],
|
|
194
|
-
density=params.acoustic['Absorption']['density'],
|
|
195
|
-
alpha_coeff=params.acoustic['Absorption']['alpha_coeff'], # dB/(MHz·cm)
|
|
196
|
-
alpha_power=params.acoustic['Absorption']['alpha_power'], # 0.5
|
|
197
|
-
absorbing=True
|
|
198
|
-
)
|
|
199
|
-
else:
|
|
200
|
-
self.medium = kWaveMedium(sound_speed=self.params['c0'])
|
|
201
|
-
elif self.params['typeSim'] == TypeSim.FIELD2.value:
|
|
202
|
-
self.medium = None
|
|
203
|
-
else:
|
|
204
|
-
self.medium = None
|
|
205
|
-
|
|
206
|
-
self.waveType = None
|
|
207
|
-
self.field = None
|
|
208
|
-
self.enveloppe = None
|
|
209
|
-
|
|
210
|
-
def __str__(self):
|
|
211
|
-
"""
|
|
212
|
-
Returns a string representation of the AcousticField object, including its parameters and attributes.
|
|
213
|
-
The string is formatted in a table-like structure for better readability.
|
|
214
|
-
"""
|
|
215
|
-
try:
|
|
216
|
-
# Get all attributes of the instance
|
|
217
|
-
attrs = {**self.params, **{k: v for k, v in vars(self).items() if k not in self.params}}
|
|
218
|
-
|
|
219
|
-
# Base attributes of AcousticField
|
|
220
|
-
base_attrs_keys = ['c0', 'f_US', 'f_AQ', 'f_saving', 'num_cycles', 'num_elements',
|
|
221
|
-
'element_width', 'element_height',
|
|
222
|
-
'Xrange', 'Yrange', 'Zrange', 'dim', 'typeSim', 'Nx', 'Ny', 'Nz',
|
|
223
|
-
'dx', 'dy', 'dz', 'probeWidth']
|
|
224
|
-
base_attrs = {key: value for key, value in attrs.items() if key in base_attrs_keys}
|
|
225
|
-
|
|
226
|
-
# Attributes specific to the derived class, excluding 'params'
|
|
227
|
-
derived_attrs = {key: value for key, value in attrs.items() if key not in base_attrs_keys and key != 'params'}
|
|
228
|
-
|
|
229
|
-
# Create lines for base and derived attributes
|
|
230
|
-
base_attr_lines = [f" {key}: {value}" for key, value in base_attrs.items()]
|
|
231
|
-
|
|
232
|
-
derived_attr_lines = []
|
|
233
|
-
for key, value in derived_attrs.items():
|
|
234
|
-
if key in {'burst', 'delayedSignal'}:
|
|
235
|
-
continue
|
|
236
|
-
elif key == 'pattern':
|
|
237
|
-
# Inspect the pattern object
|
|
238
|
-
try:
|
|
239
|
-
pattern_attrs = vars(value)
|
|
240
|
-
pattern_str = ", ".join([f"{k}={v}" for k, v in pattern_attrs.items()])
|
|
241
|
-
derived_attr_lines.append(f" pattern: {{{pattern_str}}}")
|
|
242
|
-
except Exception as e:
|
|
243
|
-
derived_attr_lines.append(f" pattern: <unreadable: {e}>")
|
|
244
|
-
else:
|
|
245
|
-
try:
|
|
246
|
-
derived_attr_lines.append(f" {key}: {value}")
|
|
247
|
-
except Exception as e:
|
|
248
|
-
derived_attr_lines.append(f" {key}: <unprintable: {e}>")
|
|
249
|
-
|
|
250
|
-
# Add shapes for burst and delayedSignal
|
|
251
|
-
if 'burst' in derived_attrs:
|
|
252
|
-
derived_attr_lines.append(f" burst: shape={self.burst.shape}")
|
|
253
|
-
if 'delayedSignal' in derived_attrs:
|
|
254
|
-
derived_attr_lines.append(f" delayedSignal: shape={self.delayedSignal.shape}")
|
|
255
|
-
|
|
256
|
-
# Define borders and titles
|
|
257
|
-
border = "+" + "-" * 40 + "+"
|
|
258
|
-
title = f"|Type : {self.__class__.__name__} wave |"
|
|
259
|
-
base_title = "| AcousticField Attributes |"
|
|
260
|
-
derived_title = f"| {self.__class__.__name__} Specific Attributes |" if derived_attrs else ""
|
|
261
|
-
|
|
262
|
-
# Convert attributes to strings
|
|
263
|
-
base_attr_str = "\n".join(base_attr_lines)
|
|
264
|
-
derived_attr_str = "\n".join(derived_attr_lines)
|
|
265
|
-
|
|
266
|
-
# Assemble the final result
|
|
267
|
-
result = f"{border}\n{title}\n{border}\n{base_title}\n{border}\n{base_attr_str}\n"
|
|
268
|
-
if derived_attrs:
|
|
269
|
-
result += f"\n{border}\n{derived_title}\n{border}\n{derived_attr_str}\n"
|
|
270
|
-
result += border
|
|
271
|
-
|
|
272
|
-
return result
|
|
273
|
-
except Exception as e:
|
|
274
|
-
print(f"Error in __str__ method: {e}")
|
|
275
|
-
raise
|
|
276
|
-
|
|
277
|
-
def __del__(self):
|
|
278
|
-
"""
|
|
279
|
-
Destructor for the AcousticField class. Cleans up the field and envelope attributes.
|
|
280
|
-
"""
|
|
281
|
-
try:
|
|
282
|
-
self.field = None
|
|
283
|
-
self.enveloppe = None
|
|
284
|
-
self.burst = None
|
|
285
|
-
self.delayedSignal = None
|
|
286
|
-
if config.get_process() == 'gpu':
|
|
287
|
-
cp.cuda.Device(config.bestGPU).synchronize()
|
|
288
|
-
except Exception as e:
|
|
289
|
-
print(f"Error in __del__ method: {e}")
|
|
290
|
-
raise
|
|
291
|
-
|
|
292
|
-
## TOOLS METHODS ##
|
|
293
|
-
|
|
294
|
-
def generate_field(self, isGpu=config.get_process() == 'gpu'):
|
|
295
|
-
"""
|
|
296
|
-
Generate the acoustic field based on the specified simulation type and parameters.
|
|
297
|
-
"""
|
|
298
|
-
try:
|
|
299
|
-
if self.params['typeSim'] == TypeSim.FIELD2.value:
|
|
300
|
-
raise NotImplementedError("FIELD2 simulation is not implemented yet.")
|
|
301
|
-
elif self.params['typeSim'] == TypeSim.KWAVE.value:
|
|
302
|
-
if self.params["dim"] == Dim.D2.value:
|
|
303
|
-
self.field = self._generate_2Dacoustic_field_KWAVE(isGpu)
|
|
304
|
-
elif self.params["dim"] == Dim.D3.value:
|
|
305
|
-
self.field = self._generate_3Dacoustic_field_KWAVE(isGpu)
|
|
306
|
-
elif self.params['typeSim'] == TypeSim.HYDRO.value:
|
|
307
|
-
raise ValueError("Cannot generate field for Hydrophone simulation, load exciting acquisitions.")
|
|
308
|
-
else:
|
|
309
|
-
raise ValueError("Invalid simulation type. Supported types are: FIELD2, KWAVE, HYDRO.")
|
|
310
|
-
except Exception as e:
|
|
311
|
-
print(f"Error in generate_field method: {e}")
|
|
312
|
-
raise
|
|
313
|
-
|
|
314
|
-
def calculate_envelope_squared(self, isGPU=True if config.get_process() == 'gpu' else False):
|
|
315
|
-
"""
|
|
316
|
-
Calculate the analytic envelope of the acoustic field using either CPU or GPU.
|
|
317
|
-
|
|
318
|
-
Parameters:
|
|
319
|
-
- use_gpu (bool): If True, use GPU for computation. Otherwise, use CPU.
|
|
320
|
-
|
|
321
|
-
Returns:
|
|
322
|
-
- envelope (numpy.ndarray or cupy.ndarray): The squared analytic envelope of the acoustic field.
|
|
323
|
-
"""
|
|
324
|
-
try:
|
|
325
|
-
if self.field is None:
|
|
326
|
-
raise ValueError("Acoustic field is not generated. Please generate the field first.")
|
|
327
|
-
|
|
328
|
-
if isGPU:
|
|
329
|
-
|
|
330
|
-
pynvml.nvmlInit()
|
|
331
|
-
handle = pynvml.nvmlDeviceGetHandleByIndex(0) # Assuming you want to check the first GPU
|
|
332
|
-
info = pynvml.nvmlDeviceGetMemoryInfo(handle)
|
|
333
|
-
total_memory = info.total / (1024 ** 2) # Convert to MB
|
|
334
|
-
used_memory = info.used / (1024 ** 2) # Convert to MB
|
|
335
|
-
free_memory = int(total_memory - used_memory)
|
|
336
|
-
|
|
337
|
-
if free_memory < self.field.nbytes / (1024 ** 2):
|
|
338
|
-
print(f"GPU memory insufficient {int(self.field.nbytes / (1024 ** 2))} MB, Free GPU memory: {free_memory} MB, falling back to CPU.")
|
|
339
|
-
isGPU = False
|
|
340
|
-
acoustic_field = np.asarray(self.field)
|
|
341
|
-
else:
|
|
342
|
-
acoustic_field = cp.asarray(self.field)
|
|
343
|
-
else:
|
|
344
|
-
acoustic_field = np.asarray(self.field)
|
|
345
|
-
|
|
346
|
-
if len(acoustic_field.shape) not in [3, 4]:
|
|
347
|
-
raise ValueError("Input acoustic field must be a 3D or 4D array.")
|
|
348
|
-
|
|
349
|
-
def process_slice(slice_index,isGPU):
|
|
350
|
-
"""Calculate the envelope for a given slice of the acoustic field."""
|
|
351
|
-
if isGPU:
|
|
352
|
-
hilbert = cp_hilbert
|
|
353
|
-
else:
|
|
354
|
-
hilbert = np_hilbert
|
|
355
|
-
|
|
356
|
-
if len(acoustic_field.shape) == 3:
|
|
357
|
-
return np.abs(hilbert(acoustic_field[slice_index], axis=0))**2
|
|
358
|
-
elif len(acoustic_field.shape) == 4:
|
|
359
|
-
envelope_slice = np.zeros_like(acoustic_field[slice_index])
|
|
360
|
-
for y in range(acoustic_field.shape[2]):
|
|
361
|
-
for z in range(acoustic_field.shape[1]):
|
|
362
|
-
envelope_slice[:, z, y, :] = np.abs(hilbert(acoustic_field[slice_index][:, z, y, :], axis=0))**2
|
|
363
|
-
return envelope_slice
|
|
364
|
-
|
|
365
|
-
# Determine the number of slices to process in parallel
|
|
366
|
-
num_slices = acoustic_field.shape[0]
|
|
367
|
-
slice_indices = [(i,) for i in range(num_slices)]
|
|
368
|
-
|
|
369
|
-
if isGPU:
|
|
370
|
-
# Use GPU directly without multithreading
|
|
371
|
-
envelopes = [process_slice(slice_index,isGPU) for slice_index in slice_indices]
|
|
372
|
-
else:
|
|
373
|
-
# Use ThreadPoolExecutor to parallelize the computation on CPU
|
|
374
|
-
with ThreadPoolExecutor() as executor:
|
|
375
|
-
envelopes = list(executor.map(lambda index: process_slice(index,isGPU), slice_indices))
|
|
376
|
-
|
|
377
|
-
# Combine the results into a single array
|
|
378
|
-
if isGPU:
|
|
379
|
-
self.enveloppe = cp.stack(envelopes, axis=0).get()
|
|
380
|
-
else:
|
|
381
|
-
self.enveloppe = np.stack(envelopes, axis=0)
|
|
382
|
-
|
|
383
|
-
except Exception as e:
|
|
384
|
-
print(f"Error in calculate_envelope_squared method: {e}")
|
|
385
|
-
raise
|
|
386
|
-
|
|
387
|
-
def save_field(self, filePath, formatSave=FormatSave.HDR_IMG):
|
|
388
|
-
"""
|
|
389
|
-
Save the acoustic field to a file in the specified format.
|
|
390
|
-
|
|
391
|
-
Parameters:
|
|
392
|
-
- filePath (str): The path where the file will be saved.
|
|
393
|
-
"""
|
|
394
|
-
try:
|
|
395
|
-
if formatSave.value == FormatSave.HDR_IMG.value:
|
|
396
|
-
self._save2D_HDR_IMG(filePath)
|
|
397
|
-
elif formatSave.value == FormatSave.H5.value:
|
|
398
|
-
self._save2D_H5(filePath)
|
|
399
|
-
elif formatSave.value == FormatSave.NPY.value:
|
|
400
|
-
self._save2D_NPY(filePath)
|
|
401
|
-
else:
|
|
402
|
-
raise ValueError("Unsupported format. Supported formats are: HDR_IMG, H5, NPY.")
|
|
403
|
-
except Exception as e:
|
|
404
|
-
print(f"Error in save_field method: {e}")
|
|
405
|
-
raise
|
|
406
|
-
|
|
407
|
-
def load_field(self, folderPath, formatSave=FormatSave.HDR_IMG):
|
|
408
|
-
"""
|
|
409
|
-
Load the acoustic field from a file in the specified format.
|
|
410
|
-
|
|
411
|
-
Parameters:
|
|
412
|
-
- filePath (str): The folder path from which to load the file.
|
|
413
|
-
"""
|
|
414
|
-
try:
|
|
415
|
-
if str(type(formatSave)) != str(AOT_biomaps.AOT_Acoustic.FormatSave):
|
|
416
|
-
raise ValueError(f"Unsupported file format: {formatSave}. Supported formats are: HDR_IMG, H5, NPY.")
|
|
417
|
-
|
|
418
|
-
if self.params['typeSim'] == TypeSim.FIELD2.value:
|
|
419
|
-
raise NotImplementedError("FIELD2 simulation is not implemented yet.")
|
|
420
|
-
elif self.params['typeSim'] == TypeSim.KWAVE.value:
|
|
421
|
-
if formatSave.value == FormatSave.HDR_IMG.value:
|
|
422
|
-
if self.params["dim"] == Dim.D2.value:
|
|
423
|
-
self._load_fieldKWAVE_XZ(os.path.join(folderPath,self.getName_field()+formatSave.value))
|
|
424
|
-
self.calculate_envelope_squared()
|
|
425
|
-
elif self.params["dim"] == Dim.D3.value:
|
|
426
|
-
raise NotImplementedError("3D KWAVE field loading is not implemented yet.")
|
|
427
|
-
elif formatSave.value == FormatSave.H5.value:
|
|
428
|
-
if self.params["dim"] == Dim.D2.value:
|
|
429
|
-
self._load_field_h5(folderPath)
|
|
430
|
-
elif self.params["dim"] == Dim.D3.value:
|
|
431
|
-
raise NotImplementedError("H5 KWAVE field loading is not implemented yet.")
|
|
432
|
-
elif formatSave.value == FormatSave.NPY.value:
|
|
433
|
-
if self.params["dim"] == Dim.D2.value:
|
|
434
|
-
self.field = np.load(os.path.join(folderPath,self.getName_field()+formatSave.value))
|
|
435
|
-
elif self.params["dim"] == Dim.D3.value:
|
|
436
|
-
raise NotImplementedError("3D NPY KWAVE field loading is not implemented yet.")
|
|
437
|
-
elif self.params['typeSim'] == TypeSim.HYDRO.value:
|
|
438
|
-
print("Loading Hydrophone field...")
|
|
439
|
-
if formatSave.value == FormatSave.HDR_IMG.value:
|
|
440
|
-
raise ValueError("HDR_IMG format is not supported for Hydrophone acquisition.")
|
|
441
|
-
if formatSave.value == FormatSave.H5.value:
|
|
442
|
-
if self.params["dim"] == Dim.D2.value:
|
|
443
|
-
self.field, self.params['Xrange'], self.params['Zrange'] = self._load_fieldHYDRO_XZ(os.path.join(folderPath, self.getName_field() + '.h5'), os.path.join(folderPath, "PARAMS_" +self.getName_field() + '.mat'))
|
|
444
|
-
elif self.params["dim"] == Dim.D3.value:
|
|
445
|
-
self._load_fieldHYDRO_XYZ(os.path.join(folderPath, self.getName_field() + '.h5'), os.path.join(folderPath, "PARAMS_" +self.getName_field() + '.mat'))
|
|
446
|
-
elif formatSave.value == FormatSave.NPY.value:
|
|
447
|
-
if self.params["dim"] == Dim.D2.value:
|
|
448
|
-
self.field = np.load(folderPath)
|
|
449
|
-
elif self.params["dim"] == Dim.D3.value:
|
|
450
|
-
raise NotImplementedError("3D NPY Hydrophone field loading is not implemented yet.")
|
|
451
|
-
else:
|
|
452
|
-
raise ValueError("Invalid simulation type. Supported types are: FIELD2, KWAVE, HYDRO.")
|
|
453
|
-
|
|
454
|
-
except Exception as e:
|
|
455
|
-
print(f"Error in load_field method: {e}")
|
|
456
|
-
raise
|
|
457
|
-
|
|
458
|
-
def iSFieldInFolder(self, folderPath,formatSave=FormatSave.HDR_IMG):
|
|
459
|
-
"""
|
|
460
|
-
Check if the field is already in the specified folder.
|
|
461
|
-
|
|
462
|
-
Parameters:
|
|
463
|
-
- filePath (str): The path to check.
|
|
464
|
-
|
|
465
|
-
Returns:
|
|
466
|
-
- bool: True if the field is in the folder, False otherwise.
|
|
467
|
-
"""
|
|
468
|
-
try:
|
|
469
|
-
if type(folderPath) != str:
|
|
470
|
-
raise TypeError("folderPath must be a string")
|
|
471
|
-
if not os.path.exists(folderPath):
|
|
472
|
-
raise FileNotFoundError(f"The specified folder does not exist: {folderPath}")
|
|
473
|
-
if type(formatSave) != FormatSave:
|
|
474
|
-
raise TypeError("formatSave must be an instance of FormatSave Enum")
|
|
475
|
-
if os.path.exists(os.path.join(folderPath, self.getName_field())+formatSave.value):
|
|
476
|
-
return True
|
|
477
|
-
else:
|
|
478
|
-
return False
|
|
479
|
-
except Exception as e:
|
|
480
|
-
print(f"Error in iSFieldInFolder method: {e}")
|
|
481
|
-
raise
|
|
482
|
-
|
|
483
|
-
@abstractmethod
|
|
484
|
-
def getName_field(self):
|
|
485
|
-
pass
|
|
486
|
-
|
|
487
|
-
## DISPLAY METHODS ##
|
|
488
|
-
|
|
489
|
-
def plot_burst_signal(self):
|
|
490
|
-
"""
|
|
491
|
-
Plot the burst signal used for generating the acoustic field.
|
|
492
|
-
"""
|
|
493
|
-
try:
|
|
494
|
-
time2plot = np.arange(0, len(self.burst)) / self.params['f_AQ'] * 1000000 # Convert to microseconds
|
|
495
|
-
plt.figure(figsize=(8, 8))
|
|
496
|
-
plt.plot(time2plot, self.burst)
|
|
497
|
-
plt.title('Excitation burst signal')
|
|
498
|
-
plt.xlabel('Time (µs)')
|
|
499
|
-
plt.ylabel('Amplitude')
|
|
500
|
-
plt.grid()
|
|
501
|
-
plt.show()
|
|
502
|
-
except Exception as e:
|
|
503
|
-
print(f"Error in plot_burst_signal method: {e}")
|
|
504
|
-
raise
|
|
505
|
-
|
|
506
|
-
def animated_plot_AcousticField(self, desired_duration_ms = 5000, save_dir=None):
|
|
507
|
-
"""
|
|
508
|
-
Plot synchronized animations of A_matrix slices for selected angles.
|
|
509
|
-
|
|
510
|
-
Args:
|
|
511
|
-
step (int): Time step between frames (default is every 10 frames).
|
|
512
|
-
save_dir (str): Directory to save the animation gif; if None, animation will not be saved.
|
|
513
|
-
|
|
514
|
-
Returns:
|
|
515
|
-
ani: Matplotlib FuncAnimation object.
|
|
516
|
-
"""
|
|
517
|
-
try:
|
|
518
|
-
|
|
519
|
-
maxF = np.max(self.enveloppe[:,20:,:])
|
|
520
|
-
minF = np.min(self.enveloppe[:,20:,:])
|
|
521
|
-
# Set the maximum embedded animation size to 100 MB
|
|
522
|
-
plt.rcParams['animation.embed_limit'] = 100
|
|
523
|
-
|
|
524
|
-
if save_dir is not None:
|
|
525
|
-
os.makedirs(save_dir, exist_ok=True)
|
|
526
|
-
|
|
527
|
-
# Create a figure and axis
|
|
528
|
-
fig, ax = plt.subplots()
|
|
529
|
-
|
|
530
|
-
# Set main title
|
|
531
|
-
if self.waveType.value == WaveType.FocusedWave.value:
|
|
532
|
-
fig.suptitle("[System Matrix Animation] Focused Wave", fontsize=12, y=0.98)
|
|
533
|
-
elif self.waveType.value == WaveType.PlaneWave.value:
|
|
534
|
-
fig.suptitle(f"[System Matrix Animation] Plane Wave | Angles {self.angle}°", fontsize=12, y=0.98)
|
|
535
|
-
elif self.waveType.value == WaveType.StructuredWave.value:
|
|
536
|
-
fig.suptitle(f"[System Matrix Animation] Structured Wave | Pattern structure: {self.pattern.activeList} | Angles {self.angle}°", fontsize=12, y=0.98)
|
|
537
|
-
else:
|
|
538
|
-
|
|
539
|
-
raise ValueError("Invalid wave type. Supported types are: FocusedWave, PlaneWave, StructuredWave.")
|
|
540
|
-
|
|
541
|
-
# Initial plot
|
|
542
|
-
im = ax.imshow(
|
|
543
|
-
self.enveloppe[0, :, :],
|
|
544
|
-
extent=(self.params['Xrange'][0] * 1000, self.params['Xrange'][-1] * 1000, self.params['Zrange'][-1] * 1000, self.params['Zrange'][0] * 1000),
|
|
545
|
-
vmin = 1.2*minF,
|
|
546
|
-
vmax=0.8*maxF,
|
|
547
|
-
aspect='equal',
|
|
548
|
-
cmap='jet',
|
|
549
|
-
animated=True
|
|
550
|
-
)
|
|
551
|
-
ax.set_title(f"t = 0 ms", fontsize=10)
|
|
552
|
-
ax.set_xlabel("x (mm)", fontsize=8)
|
|
553
|
-
ax.set_ylabel("z (mm)", fontsize=8)
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
# Unified update function for all subplots
|
|
557
|
-
def update(frame):
|
|
558
|
-
im.set_data(self.enveloppe[frame, :, :])
|
|
559
|
-
ax.set_title(f"t = {frame / self.params['f_AQ'] * 1000:.2f} ms", fontsize=10)
|
|
560
|
-
return [im] # Return a list of artists that were modified
|
|
561
|
-
|
|
562
|
-
interval = desired_duration_ms / self.AcousticFields.shape[0]
|
|
563
|
-
|
|
564
|
-
# Create animation
|
|
565
|
-
ani = animation.FuncAnimation(
|
|
566
|
-
fig, update,
|
|
567
|
-
frames=range(0, self.enveloppe.shape[0]),
|
|
568
|
-
interval=interval, blit=True
|
|
569
|
-
)
|
|
570
|
-
|
|
571
|
-
# Save animation if needed
|
|
572
|
-
if save_dir is not None:
|
|
573
|
-
if self.waveType == WaveType.FocusedWave:
|
|
574
|
-
save_filename = f"Focused_Wave_.gif"
|
|
575
|
-
elif self.waveType == WaveType.PlaneWave:
|
|
576
|
-
save_filename = f"Plane_Wave_{self._format_angle()}.gif"
|
|
577
|
-
else:
|
|
578
|
-
save_filename = f"Structured_Wave_PatternStructure_{self.pattern.activeList}_{self._format_angle()}.gif"
|
|
579
|
-
save_path = os.path.join(save_dir, save_filename)
|
|
580
|
-
ani.save(save_path, writer='pillow', fps=20)
|
|
581
|
-
print(f"Saved: {save_path}")
|
|
582
|
-
|
|
583
|
-
plt.close(fig)
|
|
584
|
-
|
|
585
|
-
return ani
|
|
586
|
-
except Exception as e:
|
|
587
|
-
print(f"Error creating animation: {e}")
|
|
588
|
-
return None
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
## PRIVATE METHODS ##
|
|
592
|
-
|
|
593
|
-
def _generate_burst_signal(self):
|
|
594
|
-
if self.params['typeSim'] == TypeSim.FIELD2.value:
|
|
595
|
-
raise NotImplementedError("FIELD2 simulation is not implemented yet.")
|
|
596
|
-
elif self.params['typeSim'] == TypeSim.KWAVE.value:
|
|
597
|
-
self._generate_burst_signalKWAVE()
|
|
598
|
-
elif self.params['typeSim'] == TypeSim.HYDRO.value:
|
|
599
|
-
raise ValueError("Cannot generate burst signal for Hydrophone simulation.")
|
|
600
|
-
|
|
601
|
-
def _generate_burst_signalKWAVE(self):
|
|
602
|
-
"""
|
|
603
|
-
Private method to generate a burst signal based on the specified parameters.
|
|
604
|
-
"""
|
|
605
|
-
try:
|
|
606
|
-
self.burst = tone_burst(1/self.kgrid.dt, self.params['f_US'], self.params['num_cycles']).squeeze()
|
|
607
|
-
except Exception as e:
|
|
608
|
-
print(f"Error in __generate_burst_signal method: {e}")
|
|
609
|
-
raise
|
|
610
|
-
|
|
611
|
-
@abstractmethod
|
|
612
|
-
def _generate_2Dacoustic_field_KWAVE(self, isGpu):
|
|
613
|
-
"""
|
|
614
|
-
Generate a 2D acoustic field using k-Wave simulation.
|
|
615
|
-
Must be implemented in subclasses.
|
|
616
|
-
"""
|
|
617
|
-
pass
|
|
618
|
-
|
|
619
|
-
@abstractmethod
|
|
620
|
-
def _generate_3Dacoustic_field_KWAVE(self, isGpu):
|
|
621
|
-
"""
|
|
622
|
-
Generate a 3D acoustic field using k-Wave simulation.
|
|
623
|
-
Must be implemented in subclasses.
|
|
624
|
-
"""
|
|
625
|
-
pass
|
|
626
|
-
|
|
627
|
-
@abstractmethod
|
|
628
|
-
def _save2D_HDR_IMG(self, filePath):
|
|
629
|
-
"""
|
|
630
|
-
Save the 2D acoustic field as an HDR_IMG file.
|
|
631
|
-
Must be implemented in subclasses.
|
|
632
|
-
"""
|
|
633
|
-
pass
|
|
634
|
-
|
|
635
|
-
def _load_field_h5(self, filePath):
|
|
636
|
-
"""
|
|
637
|
-
Load the 2D acoustic field from an H5 file.
|
|
638
|
-
|
|
639
|
-
Parameters:
|
|
640
|
-
- filePath (str): The path to the H5 file.
|
|
641
|
-
|
|
642
|
-
Returns:
|
|
643
|
-
- field (numpy.ndarray): The loaded acoustic field.
|
|
644
|
-
"""
|
|
645
|
-
try:
|
|
646
|
-
with h5py.File(filePath+self.getName_field()+".h5", 'r') as f:
|
|
647
|
-
self.enveloppe = f['data'][:]
|
|
648
|
-
except Exception as e:
|
|
649
|
-
print(f"Error in _load_field_h5 method: {e}")
|
|
650
|
-
raise
|
|
651
|
-
|
|
652
|
-
def _save2D_H5(self, filePath):
|
|
653
|
-
"""
|
|
654
|
-
Save the 2D acoustic field as an H5 file.
|
|
655
|
-
|
|
656
|
-
Parameters:
|
|
657
|
-
- filePath (str): The path where the file will be saved.
|
|
658
|
-
"""
|
|
659
|
-
try:
|
|
660
|
-
with h5py.File(filePath+self.getName_field()+"h5", 'w') as f:
|
|
661
|
-
for key, value in self.__dict__.items():
|
|
662
|
-
if key != 'enveloppe':
|
|
663
|
-
f.create_dataset(key, data=value)
|
|
664
|
-
f.create_dataset('data', data=self.enveloppe, compression='gzip')
|
|
665
|
-
except Exception as e:
|
|
666
|
-
print(f"Error in _save2D_H5 method: {e}")
|
|
667
|
-
raise
|
|
668
|
-
|
|
669
|
-
def _save2D_NPY(self, filePath):
|
|
670
|
-
"""
|
|
671
|
-
Save the 2D acoustic field as a NPY file.
|
|
672
|
-
|
|
673
|
-
Parameters:
|
|
674
|
-
- filePath (str): The path where the file will be saved.
|
|
675
|
-
"""
|
|
676
|
-
try:
|
|
677
|
-
np.save(filePath+self.getName_field()+"npy", self.enveloppe)
|
|
678
|
-
except Exception as e:
|
|
679
|
-
print(f"Error in _save2D_NPY method: {e}")
|
|
680
|
-
raise
|
|
681
|
-
|
|
682
|
-
def _load_fieldKWAVE_XZ(self, hdr_path):
|
|
683
|
-
"""
|
|
684
|
-
Read an Interfile (.hdr) and its binary file (.img) to reconstruct an acoustic field.
|
|
685
|
-
|
|
686
|
-
Parameters:
|
|
687
|
-
- hdr_path (str): The path to the .hdr file.
|
|
688
|
-
|
|
689
|
-
Returns:
|
|
690
|
-
- field (numpy.ndarray): The reconstructed acoustic field with dimensions reordered to (X, Z, time).
|
|
691
|
-
- header (dict): A dictionary containing the metadata from the .hdr file.
|
|
692
|
-
"""
|
|
693
|
-
try:
|
|
694
|
-
header = {}
|
|
695
|
-
# Read the .hdr file
|
|
696
|
-
with open(hdr_path, 'r') as f:
|
|
697
|
-
for line in f:
|
|
698
|
-
if ':=' in line:
|
|
699
|
-
key, value = line.split(':=', 1)
|
|
700
|
-
key = key.strip().lower().replace('!', '')
|
|
701
|
-
value = value.strip()
|
|
702
|
-
header[key] = value
|
|
703
|
-
|
|
704
|
-
# Get the associated .img file name
|
|
705
|
-
data_file = header.get('name of data file') or header.get('name of date file')
|
|
706
|
-
if data_file is None:
|
|
707
|
-
raise ValueError(f"Cannot find the data file associated with the header file {hdr_path}")
|
|
708
|
-
img_path = os.path.join(os.path.dirname(hdr_path), os.path.basename(data_file))
|
|
709
|
-
|
|
710
|
-
# Determine the field size from metadata
|
|
711
|
-
shape = [int(header[f'matrix size [{i}]']) for i in range(1, 3) if f'matrix size [{i}]' in header]
|
|
712
|
-
if not shape:
|
|
713
|
-
raise ValueError("Cannot determine the shape of the acoustic field from metadata.")
|
|
714
|
-
|
|
715
|
-
# Data type
|
|
716
|
-
data_type = header.get('number format', 'short float').lower()
|
|
717
|
-
dtype_map = {
|
|
718
|
-
'short float': np.float32,
|
|
719
|
-
'float': np.float32,
|
|
720
|
-
'int16': np.int16,
|
|
721
|
-
'int32': np.int32,
|
|
722
|
-
'uint16': np.uint16,
|
|
723
|
-
'uint8': np.uint8
|
|
724
|
-
}
|
|
725
|
-
dtype = dtype_map.get(data_type)
|
|
726
|
-
if dtype is None:
|
|
727
|
-
raise ValueError(f"Unsupported data type: {data_type}")
|
|
728
|
-
|
|
729
|
-
# Byte order (endianness)
|
|
730
|
-
byte_order = header.get('imagedata byte order', 'LITTLEENDIAN').lower()
|
|
731
|
-
endianess = '<' if 'little' in byte_order else '>'
|
|
732
|
-
|
|
733
|
-
# Verify the actual size of the .img file
|
|
734
|
-
fileSize = os.path.getsize(img_path)
|
|
735
|
-
timeDim = int(fileSize / (np.dtype(dtype).itemsize * np.prod(shape)))
|
|
736
|
-
shape = shape + [timeDim]
|
|
737
|
-
|
|
738
|
-
# Read binary data
|
|
739
|
-
with open(img_path, 'rb') as f:
|
|
740
|
-
data = np.fromfile(f, dtype=endianess + np.dtype(dtype).char)
|
|
741
|
-
|
|
742
|
-
# Reshape data to (time, Z, X)
|
|
743
|
-
field = data.reshape(shape[::-1]) # NumPy interprets in C order (opposite of MATLAB)
|
|
744
|
-
|
|
745
|
-
# Apply scaling factors if available
|
|
746
|
-
rescale_slope = float(header.get('data rescale slope', 1))
|
|
747
|
-
rescale_offset = float(header.get('data rescale offset', 0))
|
|
748
|
-
field = field * rescale_slope + rescale_offset
|
|
749
|
-
|
|
750
|
-
self.field = field
|
|
751
|
-
except Exception as e:
|
|
752
|
-
print(f"Error in _load_fieldKWAVE_XZ method: {e}")
|
|
753
|
-
raise
|
|
754
|
-
|
|
755
|
-
def _load_fieldHYDRO_XZ(self, file_path_h5, param_path_mat):
|
|
756
|
-
"""
|
|
757
|
-
Load the 2D acoustic field for Hydrophone simulation from H5 and MAT files.
|
|
758
|
-
|
|
759
|
-
Parameters:
|
|
760
|
-
- file_path_h5 (str): The path to the H5 file.
|
|
761
|
-
- param_path_mat (str): The path to the MAT file.
|
|
762
|
-
|
|
763
|
-
Returns:
|
|
764
|
-
- envelope_transposed (numpy.ndarray): The transposed envelope of the acoustic field.
|
|
765
|
-
"""
|
|
766
|
-
try:
|
|
767
|
-
# Load parameters from the .mat file
|
|
768
|
-
param = scipy.io.loadmat(param_path_mat)
|
|
769
|
-
|
|
770
|
-
# Load the ranges for x and z
|
|
771
|
-
x_test = param['x'].flatten()
|
|
772
|
-
z_test = param['z'].flatten()
|
|
773
|
-
|
|
774
|
-
x_range = np.arange(-23, 21.2, 0.2)
|
|
775
|
-
z_range = np.arange(0, 37.2, 0.2)
|
|
776
|
-
X, Z = np.meshgrid(x_range, z_range)
|
|
777
|
-
|
|
778
|
-
# Load the data from the .h5 file
|
|
779
|
-
with h5py.File(file_path_h5, 'r') as file:
|
|
780
|
-
data = file['data'][:]
|
|
781
|
-
|
|
782
|
-
# Initialize a matrix to store the acoustic data
|
|
783
|
-
acoustic_field = np.zeros((len(z_range), len(x_range), data.shape[1]))
|
|
784
|
-
|
|
785
|
-
# Fill the grid with acoustic data
|
|
786
|
-
index = 0
|
|
787
|
-
for i in range(len(z_range)):
|
|
788
|
-
if i % 2 == 0:
|
|
789
|
-
# Traverse left to right
|
|
790
|
-
for j in range(len(x_range)):
|
|
791
|
-
acoustic_field[i, j, :] = data[index]
|
|
792
|
-
index += 1
|
|
793
|
-
else:
|
|
794
|
-
# Traverse right to left
|
|
795
|
-
for j in range(len(x_range) - 1, -1, -1):
|
|
796
|
-
acoustic_field[i, j, :] = data[index]
|
|
797
|
-
index += 1
|
|
798
|
-
|
|
799
|
-
self.field =np.transpose(acoustic_field, (2, 0, 1)).T
|
|
800
|
-
# Calculate the analytic envelope
|
|
801
|
-
envelope = np.abs(hilbert(acoustic_field, axis=2))
|
|
802
|
-
# Reorganize the array to have the shape (Times, Z, X)
|
|
803
|
-
envelope_transposed = np.transpose(envelope, (2, 0, 1)).T
|
|
804
|
-
|
|
805
|
-
self.enveloppe = envelope_transposed
|
|
806
|
-
self.params['Xrange'] = x_range
|
|
807
|
-
self.params['Zrange'] = z_range
|
|
808
|
-
|
|
809
|
-
except Exception as e:
|
|
810
|
-
print(f"Error in _load_fieldHYDRO_XZ method: {e}")
|
|
811
|
-
raise
|
|
812
|
-
|
|
813
|
-
def _load_fieldHYDRO_YZ(self, file_path_h5, param_path_mat):
|
|
814
|
-
"""
|
|
815
|
-
Load the 2D acoustic field for Hydrophone simulation from H5 and MAT files.
|
|
816
|
-
|
|
817
|
-
Parameters:
|
|
818
|
-
- file_path_h5 (str): The path to the H5 file.
|
|
819
|
-
- param_path_mat (str): The path to the MAT file.
|
|
820
|
-
|
|
821
|
-
Returns:
|
|
822
|
-
- envelope_transposed (numpy.ndarray): The transposed envelope of the acoustic field.
|
|
823
|
-
- y_range (numpy.ndarray): The range of y values.
|
|
824
|
-
- z_range (numpy.ndarray): The range of z values.
|
|
825
|
-
"""
|
|
826
|
-
try:
|
|
827
|
-
# Load parameters from the .mat file
|
|
828
|
-
param = scipy.io.loadmat(param_path_mat)
|
|
829
|
-
|
|
830
|
-
# Extract the ranges for y and z
|
|
831
|
-
y_range = param['y'].flatten()
|
|
832
|
-
z_range = param['z'].flatten()
|
|
833
|
-
|
|
834
|
-
# Load the data from the .h5 file
|
|
835
|
-
with h5py.File(file_path_h5, 'r') as file:
|
|
836
|
-
data = file['data'][:]
|
|
837
|
-
|
|
838
|
-
# Calculate the number of scans
|
|
839
|
-
Ny = len(y_range)
|
|
840
|
-
Nz = len(z_range)
|
|
841
|
-
|
|
842
|
-
# Create the scan positions
|
|
843
|
-
positions_y = []
|
|
844
|
-
positions_z = []
|
|
845
|
-
|
|
846
|
-
for i in range(Nz):
|
|
847
|
-
if i % 2 == 0:
|
|
848
|
-
# Traverse top to bottom for even rows
|
|
849
|
-
positions_y.extend(y_range)
|
|
850
|
-
else:
|
|
851
|
-
# Traverse bottom to top for odd rows
|
|
852
|
-
positions_y.extend(y_range[::-1])
|
|
853
|
-
positions_z.extend([z_range[i]] * Ny)
|
|
854
|
-
|
|
855
|
-
Positions = np.column_stack((positions_y, positions_z))
|
|
856
|
-
|
|
857
|
-
# Initialize a matrix to store the reorganized data
|
|
858
|
-
reorganized_data = np.zeros((Ny, Nz, data.shape[1]))
|
|
859
|
-
|
|
860
|
-
# Reorganize the data according to the scan positions
|
|
861
|
-
for index, (j, k) in enumerate(Positions):
|
|
862
|
-
y_idx = np.where(y_range == j)[0][0]
|
|
863
|
-
z_idx = np.where(z_range == k)[0][0]
|
|
864
|
-
reorganized_data[y_idx, z_idx, :] = data[index, :]
|
|
865
|
-
|
|
866
|
-
# Calculate the analytic envelope
|
|
867
|
-
envelope = np.abs(hilbert(reorganized_data, axis=2))
|
|
868
|
-
# Reorganize the array to have the shape (Times, Z, Y)
|
|
869
|
-
envelope_transposed = np.transpose(envelope, (2, 0, 1))
|
|
870
|
-
return envelope_transposed, y_range, z_range
|
|
871
|
-
except Exception as e:
|
|
872
|
-
print(f"Error in _load_fieldHYDRO_YZ method: {e}")
|
|
873
|
-
raise
|
|
874
|
-
|
|
875
|
-
def _load_fieldHYDRO_XYZ(self, file_path_h5, param_path_mat):
|
|
876
|
-
"""
|
|
877
|
-
Load the 3D acoustic field for Hydrophone simulation from H5 and MAT files.
|
|
878
|
-
|
|
879
|
-
Parameters:
|
|
880
|
-
- file_path_h5 (str): The path to the H5 file.
|
|
881
|
-
- param_path_mat (str): The path to the MAT file.
|
|
882
|
-
|
|
883
|
-
Returns:
|
|
884
|
-
- EnveloppeField (numpy.ndarray): The envelope of the acoustic field.
|
|
885
|
-
- x_range (numpy.ndarray): The range of x values.
|
|
886
|
-
- y_range (numpy.ndarray): The range of y values.
|
|
887
|
-
- z_range (numpy.ndarray): The range of z values.
|
|
888
|
-
"""
|
|
889
|
-
try:
|
|
890
|
-
# Load parameters from the .mat file
|
|
891
|
-
param = scipy.io.loadmat(param_path_mat)
|
|
892
|
-
|
|
893
|
-
# Extract the ranges for x, y, and z
|
|
894
|
-
x_range = param['x'].flatten()
|
|
895
|
-
y_range = param['y'].flatten()
|
|
896
|
-
z_range = param['z'].flatten()
|
|
897
|
-
|
|
898
|
-
# Create a meshgrid for x, y, and z
|
|
899
|
-
X, Y, Z = np.meshgrid(x_range, y_range, z_range, indexing='ij')
|
|
900
|
-
|
|
901
|
-
# Load the data from the .h5 file
|
|
902
|
-
with h5py.File(file_path_h5, 'r') as file:
|
|
903
|
-
data = file['data'][:]
|
|
904
|
-
|
|
905
|
-
# Calculate the number of scans
|
|
906
|
-
Nx = len(x_range)
|
|
907
|
-
Ny = len(y_range)
|
|
908
|
-
Nz = len(z_range)
|
|
909
|
-
Nscans = Nx * Ny * Nz
|
|
910
|
-
|
|
911
|
-
# Create the scan positions
|
|
912
|
-
if Ny % 2 == 0:
|
|
913
|
-
X = np.tile(np.concatenate([x_range[:, np.newaxis], x_range[::-1, np.newaxis]]), (Ny // 2, 1))
|
|
914
|
-
Y = np.repeat(y_range, Nx)
|
|
915
|
-
else:
|
|
916
|
-
X = np.concatenate([x_range[:, np.newaxis], np.tile(np.concatenate([x_range[::-1, np.newaxis], x_range[:, np.newaxis]]), ((Ny - 1) // 2, 1))])
|
|
917
|
-
Y = np.repeat(y_range, Nx)
|
|
918
|
-
|
|
919
|
-
XY = np.column_stack((X.flatten(), Y))
|
|
920
|
-
|
|
921
|
-
if Nz % 2 == 0:
|
|
922
|
-
XYZ = np.tile(np.concatenate([XY, np.flipud(XY)]), (Nz // 2, 1))
|
|
923
|
-
Z = np.repeat(z_range, Nx * Ny)
|
|
924
|
-
else:
|
|
925
|
-
XYZ = np.concatenate([XY, np.tile(np.concatenate([np.flipud(XY), XY]), ((Nz - 1) // 2, 1))])
|
|
926
|
-
Z = np.repeat(z_range, Nx * Ny)
|
|
927
|
-
|
|
928
|
-
Positions = np.column_stack((XYZ, Z))
|
|
929
|
-
|
|
930
|
-
# Initialize a matrix to store the reorganized data
|
|
931
|
-
reorganized_data = np.zeros((Nx, Ny, Nz, data.shape[1]))
|
|
932
|
-
|
|
933
|
-
# Reorganize the data according to the scan positions
|
|
934
|
-
for index, (i, j, k) in enumerate(Positions):
|
|
935
|
-
x_idx = np.where(x_range == i)[0][0]
|
|
936
|
-
y_idx = np.where(y_range == j)[0][0]
|
|
937
|
-
z_idx = np.where(z_range == k)[0][0]
|
|
938
|
-
reorganized_data[x_idx, y_idx, z_idx, :] = data[index, :]
|
|
939
|
-
|
|
940
|
-
self.field = np.transpose(reorganized_data, (3, 2, 1, 0))
|
|
941
|
-
EnveloppeField = np.zeros_like(reorganized_data)
|
|
942
|
-
|
|
943
|
-
for y in range(reorganized_data.shape[1]):
|
|
944
|
-
for z in range(reorganized_data.shape[2]):
|
|
945
|
-
EnveloppeField[:, y, z, :] = np.abs(hilbert(reorganized_data[:, y, z, :], axis=1))
|
|
946
|
-
self.enveloppe = np.transpose(EnveloppeField, (3, 2, 1, 0))
|
|
947
|
-
self.params['Xrange'] = [x_range[0], x_range[-1]]
|
|
948
|
-
self.params['Yrange'] = [y_range[0], y_range[-1]]
|
|
949
|
-
self.params['Zrange'] = [z_range[0], z_range[-1]]
|
|
950
|
-
self.params['Nx'] = Nx
|
|
951
|
-
self.params['Ny'] = Ny
|
|
952
|
-
self.params['Nz'] = Nz
|
|
953
|
-
except Exception as e:
|
|
954
|
-
print(f"Error in _load_fieldHYDRO_XYZ method: {e}")
|
|
955
|
-
raise
|
|
956
|
-
|
|
957
|
-
####### SUBCLASS #######
|
|
958
|
-
|
|
959
|
-
class StructuredWave(AcousticField):
|
|
960
|
-
|
|
961
|
-
class PatternParams:
|
|
962
|
-
def __init__(self, space_0, space_1, move_head_0_2tail, move_tail_1_2head):
|
|
963
|
-
"""
|
|
964
|
-
Initialize the PatternParams object with given parameters.
|
|
965
|
-
|
|
966
|
-
Args:
|
|
967
|
-
space_0 (int): Number of zeros in the pattern.
|
|
968
|
-
space_1 (int): Number of ones in the pattern.
|
|
969
|
-
move_head_0_2tail (int): Number of zeros to move from head to tail.
|
|
970
|
-
move_tail_1_2head (int): Number of ones to move from tail to head.
|
|
971
|
-
"""
|
|
972
|
-
self.space_0 = space_0
|
|
973
|
-
self.space_1 = space_1
|
|
974
|
-
self.move_head_0_2tail = move_head_0_2tail
|
|
975
|
-
self.move_tail_1_2head = move_tail_1_2head
|
|
976
|
-
self.activeList = None
|
|
977
|
-
self.len_hex = None
|
|
978
|
-
|
|
979
|
-
def __str__(self):
|
|
980
|
-
"""Return a string representation of the PatternParams object."""
|
|
981
|
-
pass
|
|
982
|
-
|
|
983
|
-
def generate_pattern(self):
|
|
984
|
-
"""
|
|
985
|
-
Generate a binary pattern and return it as a hex string.
|
|
986
|
-
|
|
987
|
-
Returns:
|
|
988
|
-
str: Hexadecimal representation of the binary pattern.
|
|
989
|
-
"""
|
|
990
|
-
try:
|
|
991
|
-
total_bits = self.len_hex * 4
|
|
992
|
-
unit = "0" * self.space_0 + "1" * self.space_1
|
|
993
|
-
repeat_time = (total_bits + len(unit) - 1) // len(unit)
|
|
994
|
-
pattern = (unit * repeat_time)[:total_bits]
|
|
995
|
-
|
|
996
|
-
# Move 0s from head to tail
|
|
997
|
-
if self.move_head_0_2tail > 0:
|
|
998
|
-
head_zeros = '0' * self.move_head_0_2tail
|
|
999
|
-
pattern = pattern[self.move_head_0_2tail:] + head_zeros
|
|
1000
|
-
|
|
1001
|
-
# Move 1s from tail to head
|
|
1002
|
-
if self.move_tail_1_2head > 0:
|
|
1003
|
-
tail_ones = '1' * self.move_tail_1_2head
|
|
1004
|
-
pattern = tail_ones + pattern[:-self.move_tail_1_2head]
|
|
1005
|
-
|
|
1006
|
-
# Convert to hex
|
|
1007
|
-
hex_output = hex(int(pattern, 2))[2:]
|
|
1008
|
-
hex_output = hex_output.zfill(self.len_hex)
|
|
1009
|
-
|
|
1010
|
-
return hex_output
|
|
1011
|
-
except Exception as e:
|
|
1012
|
-
print(f"Error generating pattern: {e}")
|
|
1013
|
-
return None
|
|
1014
|
-
|
|
1015
|
-
def generate_paths(self, base_path):
|
|
1016
|
-
"""Generate the list of system matrix .hdr file paths for this wave."""
|
|
1017
|
-
#pattern_str = self.pattern_params.to_string()
|
|
1018
|
-
pattern_str = self.generate_pattern()
|
|
1019
|
-
paths = []
|
|
1020
|
-
for angle in self.angles:
|
|
1021
|
-
angle_str = self.format_angle(angle)
|
|
1022
|
-
paths.append(f"{base_path}/field_{pattern_str}_{angle_str}.hdr")
|
|
1023
|
-
return paths
|
|
1024
|
-
|
|
1025
|
-
def to_string(self):
|
|
1026
|
-
"""
|
|
1027
|
-
Format the pattern parameters into a string like '0_48_0_0'.
|
|
1028
|
-
|
|
1029
|
-
Returns:
|
|
1030
|
-
str: Formatted string of pattern parameters.
|
|
1031
|
-
"""
|
|
1032
|
-
return f"{self.space_0}_{self.space_1}_{self.move_head_0_2tail}_{self.move_tail_1_2head}"
|
|
1033
|
-
|
|
1034
|
-
def describe(self):
|
|
1035
|
-
"""
|
|
1036
|
-
Return a readable description of the pattern parameters.
|
|
1037
|
-
|
|
1038
|
-
Returns:
|
|
1039
|
-
str: Description of the pattern parameters.
|
|
1040
|
-
"""
|
|
1041
|
-
return f"Pattern structure: {self.to_string()}"
|
|
1042
|
-
|
|
1043
|
-
def __init__(self, angle_deg, space_0, space_1, move_head_0_2tail, move_tail_1_2head, **kwargs):
|
|
1044
|
-
"""
|
|
1045
|
-
Initialize the StructuredWave object.
|
|
1046
|
-
|
|
1047
|
-
Args:
|
|
1048
|
-
angle_deg (float): Angle in degrees.
|
|
1049
|
-
space_0 (int): Number of zeros in the pattern.
|
|
1050
|
-
space_1 (int): Number of ones in the pattern.
|
|
1051
|
-
move_head_0_2tail (int): Number of zeros to move from head to tail.
|
|
1052
|
-
move_tail_1_2head (int): Number of ones to move from tail to head.
|
|
1053
|
-
**kwargs: Additional keyword arguments.
|
|
1054
|
-
"""
|
|
1055
|
-
try:
|
|
1056
|
-
super().__init__(**kwargs)
|
|
1057
|
-
self.waveType = WaveType.StructuredWave
|
|
1058
|
-
self.kgrid.setTime(int(self.kgrid.Nt*1.5),self.kgrid.dt) # Extend the time grid to allow for delays
|
|
1059
|
-
self.pattern = self.PatternParams(space_0, space_1, move_head_0_2tail, move_tail_1_2head)
|
|
1060
|
-
self.pattern.len_hex = self.params['num_elements'] // 4
|
|
1061
|
-
self.pattern.activeList = self.pattern.generate_pattern()
|
|
1062
|
-
self.angle = angle_deg
|
|
1063
|
-
self.f_s = self._getDecimationFrequency()
|
|
1064
|
-
|
|
1065
|
-
if self.angle < -20 or self.angle > 20:
|
|
1066
|
-
raise ValueError("Angle must be between -20 and 20 degrees.")
|
|
1067
|
-
|
|
1068
|
-
if len(self.pattern.activeList) != self.params["num_elements"] // 4:
|
|
1069
|
-
raise ValueError(f"Active list string must be {self.params['num_elements'] // 4} characters long.")
|
|
1070
|
-
self.delayedSignal = self._apply_delay()
|
|
1071
|
-
except Exception as e:
|
|
1072
|
-
print(f"Error initializing StructuredWave: {e}")
|
|
1073
|
-
|
|
1074
|
-
def getName_field(self):
|
|
1075
|
-
"""
|
|
1076
|
-
Generate the list of system matrix .hdr file paths for this wave.
|
|
1077
|
-
|
|
1078
|
-
Returns:
|
|
1079
|
-
str: File path for the system matrix .hdr file.
|
|
1080
|
-
"""
|
|
1081
|
-
try:
|
|
1082
|
-
pattern_str = self.pattern.activeList
|
|
1083
|
-
angle_str = self._format_angle()
|
|
1084
|
-
return f"field_{pattern_str}_{angle_str}"
|
|
1085
|
-
except Exception as e:
|
|
1086
|
-
print(f"Error generating file path: {e}")
|
|
1087
|
-
return None
|
|
1088
|
-
|
|
1089
|
-
def _getDecimationFrequency(self):
|
|
1090
|
-
"""
|
|
1091
|
-
Calculate the decimation frequency based on the pattern parameters.
|
|
1092
|
-
|
|
1093
|
-
Returns:
|
|
1094
|
-
int: Decimation frequency.
|
|
1095
|
-
"""
|
|
1096
|
-
try:
|
|
1097
|
-
return 1/(self.pattern.space_0 + self.pattern.space_1)/self.params['element_width']
|
|
1098
|
-
except Exception as e:
|
|
1099
|
-
print(f"Error calculating decimation frequency: {e}")
|
|
1100
|
-
return None
|
|
1101
|
-
|
|
1102
|
-
@staticmethod
|
|
1103
|
-
def getPattern(pathFile):
|
|
1104
|
-
"""
|
|
1105
|
-
Get the pattern from a file path.
|
|
1106
|
-
|
|
1107
|
-
Args:
|
|
1108
|
-
pathFile (str): Path to the file containing the pattern.
|
|
1109
|
-
|
|
1110
|
-
Returns:
|
|
1111
|
-
str: The pattern string.
|
|
1112
|
-
"""
|
|
1113
|
-
try:
|
|
1114
|
-
# Pattern between first _ and last _
|
|
1115
|
-
pattern = os.path.basename(pathFile).split('_')[1:-1]
|
|
1116
|
-
pattern_str = ''.join(pattern)
|
|
1117
|
-
return pattern_str
|
|
1118
|
-
except Exception as e:
|
|
1119
|
-
print(f"Error reading pattern from file: {e}")
|
|
1120
|
-
return None
|
|
1121
|
-
|
|
1122
|
-
@staticmethod
|
|
1123
|
-
def getAngle(pathFile):
|
|
1124
|
-
"""
|
|
1125
|
-
Get the angle from a file path.
|
|
1126
|
-
|
|
1127
|
-
Args:
|
|
1128
|
-
pathFile (str): Path to the file containing the angle.
|
|
1129
|
-
|
|
1130
|
-
Returns:
|
|
1131
|
-
int: The angle in degrees.
|
|
1132
|
-
"""
|
|
1133
|
-
try:
|
|
1134
|
-
# Angle between last _ and .
|
|
1135
|
-
angle_str = os.path.basename(pathFile).split('_')[-1].replace('.', '')
|
|
1136
|
-
if angle_str.startswith('0'):
|
|
1137
|
-
angle_str = angle_str[1:]
|
|
1138
|
-
elif angle_str.startswith('1'):
|
|
1139
|
-
angle_str = '-' + angle_str[1:]
|
|
1140
|
-
else:
|
|
1141
|
-
raise ValueError("Invalid angle format in file name.")
|
|
1142
|
-
return int(angle_str)
|
|
1143
|
-
except Exception as e:
|
|
1144
|
-
print(f"Error reading angle from file: {e}")
|
|
1145
|
-
return None
|
|
1146
|
-
|
|
1147
|
-
## PRIVATE METHODS ##
|
|
1148
|
-
|
|
1149
|
-
def _format_angle(self):
|
|
1150
|
-
"""
|
|
1151
|
-
Format an angle into a 3-digit code like '120' for -20°, '020' for +20°.
|
|
1152
|
-
|
|
1153
|
-
Args:
|
|
1154
|
-
angle (float): Angle in degrees.
|
|
1155
|
-
|
|
1156
|
-
Returns:
|
|
1157
|
-
str: Formatted angle string.
|
|
1158
|
-
"""
|
|
1159
|
-
return f"{'1' if self.angle < 0 else '0'}{abs(self.angle):02d}"
|
|
1160
|
-
|
|
1161
|
-
def _apply_delay(self):
|
|
1162
|
-
"""
|
|
1163
|
-
Apply a temporal delay to the signal for each transducer element.
|
|
1164
|
-
|
|
1165
|
-
Returns:
|
|
1166
|
-
ndarray: Array of delayed signals.
|
|
1167
|
-
"""
|
|
1168
|
-
try:
|
|
1169
|
-
is_positive = self.angle >= 0
|
|
1170
|
-
|
|
1171
|
-
# Calculate the total number of grid points for all elements
|
|
1172
|
-
total_grid_points = self.params['num_elements'] * int(round(self.params['element_width'] / self.params['dx']))
|
|
1173
|
-
|
|
1174
|
-
# Initialize delays array with size total_grid_points
|
|
1175
|
-
delays = np.zeros(total_grid_points)
|
|
1176
|
-
|
|
1177
|
-
# Calculate the physical positions of the elements starting from Xrange[0]
|
|
1178
|
-
element_positions = np.linspace(0, total_grid_points * self.params['dx'], total_grid_points)
|
|
1179
|
-
|
|
1180
|
-
# Calculate delays based on physical positions
|
|
1181
|
-
for i in range(total_grid_points):
|
|
1182
|
-
delays[i] = (element_positions[i] * np.tan(np.deg2rad(abs(self.angle)))) / self.params['c0'] # Delay in seconds
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
delay_samples = np.round(delays / self.kgrid.dt).astype(int)
|
|
1186
|
-
max_delay = np.max(np.abs(delay_samples))
|
|
1187
|
-
|
|
1188
|
-
delayed_signals = np.zeros((total_grid_points, len(self.burst) + max_delay))
|
|
1189
|
-
for i in range(total_grid_points):
|
|
1190
|
-
shift = delay_samples[i]
|
|
1191
|
-
|
|
1192
|
-
if is_positive:
|
|
1193
|
-
delayed_signals[i, shift:shift + len(self.burst)] = self.burst # Right shift
|
|
1194
|
-
else:
|
|
1195
|
-
delayed_signals[i, max_delay - shift:max_delay - shift + len(self.burst)] = self.burst # Left shift
|
|
1196
|
-
|
|
1197
|
-
return delayed_signals
|
|
1198
|
-
except Exception as e:
|
|
1199
|
-
print(f"Error applying delay: {e}")
|
|
1200
|
-
return None
|
|
1201
|
-
|
|
1202
|
-
def plot_delay(self):
|
|
1203
|
-
"""
|
|
1204
|
-
Plot the time of the maximum of each delayed signal to visualize the wavefront.
|
|
1205
|
-
"""
|
|
1206
|
-
try:
|
|
1207
|
-
# Find the index of the maximum for each delayed signal
|
|
1208
|
-
max_indices = np.argmax(self.delayedSignal, axis=1)
|
|
1209
|
-
element_indices = np.linspace(0, self.params['num_elements'] - 1, self.delayedSignal.shape[0])
|
|
1210
|
-
# Convert indices to time
|
|
1211
|
-
max_times = max_indices / self.params['f_AQ']
|
|
1212
|
-
|
|
1213
|
-
# Plot the times of the maxima
|
|
1214
|
-
plt.figure(figsize=(10, 6))
|
|
1215
|
-
plt.plot(element_indices, max_times, 'o-')
|
|
1216
|
-
plt.title('Time of Maximum for Each Delayed Signal')
|
|
1217
|
-
plt.xlabel('Transducer Element Index')
|
|
1218
|
-
plt.ylabel('Time of Maximum (s)')
|
|
1219
|
-
plt.grid(True)
|
|
1220
|
-
plt.show()
|
|
1221
|
-
except Exception as e:
|
|
1222
|
-
print(f"Error plotting max times: {e}")
|
|
1223
|
-
|
|
1224
|
-
def _save2D_HDR_IMG(self, pathFolder):
|
|
1225
|
-
"""
|
|
1226
|
-
Save the acoustic field to .img and .hdr files.
|
|
1227
|
-
|
|
1228
|
-
Args:
|
|
1229
|
-
pathFolder (str): Path to the folder where files will be saved.
|
|
1230
|
-
"""
|
|
1231
|
-
try:
|
|
1232
|
-
t_ex = 1 / self.params['f_US']
|
|
1233
|
-
angle_sign = '1' if self.angle < 0 else '0'
|
|
1234
|
-
formatted_angle = f"{angle_sign}{abs(self.angle):02d}"
|
|
1235
|
-
|
|
1236
|
-
# Define file names (img and hdr)
|
|
1237
|
-
file_name = f"field_{self.pattern.activeList}_{formatted_angle}"
|
|
1238
|
-
|
|
1239
|
-
img_path = os.path.join(pathFolder, file_name + ".img")
|
|
1240
|
-
hdr_path = os.path.join(pathFolder, file_name + ".hdr")
|
|
1241
|
-
|
|
1242
|
-
# Save the acoustic field to the .img file
|
|
1243
|
-
with open(img_path, "wb") as f_img:
|
|
1244
|
-
self.field.astype('float32').tofile(f_img) # Save in float32 format (equivalent to "single" in MATLAB)
|
|
1245
|
-
|
|
1246
|
-
# Generate headerFieldGlob
|
|
1247
|
-
headerFieldGlob = (
|
|
1248
|
-
f"!INTERFILE :=\n"
|
|
1249
|
-
f"modality : AOT\n"
|
|
1250
|
-
f"voxels number transaxial: {self.field.shape[2]}\n"
|
|
1251
|
-
f"voxels number transaxial 2: {self.field.shape[1]}\n"
|
|
1252
|
-
f"voxels number axial: {1}\n"
|
|
1253
|
-
f"field of view transaxial: {(self.params['Xrange'][1] - self.params['Xrange'][0]) * 1000}\n"
|
|
1254
|
-
f"field of view transaxial 2: {(self.params['Zrange'][1] - self.params['Zrange'][0]) * 1000}\n"
|
|
1255
|
-
f"field of view axial: {1}\n"
|
|
1256
|
-
)
|
|
1257
|
-
|
|
1258
|
-
# Generate header
|
|
1259
|
-
header = (
|
|
1260
|
-
f"!INTERFILE :=\n"
|
|
1261
|
-
f"!imaging modality := AOT\n\n"
|
|
1262
|
-
f"!GENERAL DATA :=\n"
|
|
1263
|
-
f"!data offset in bytes := 0\n"
|
|
1264
|
-
f"!name of data file := system_matrix/{file_name}.img\n\n"
|
|
1265
|
-
f"!GENERAL IMAGE DATA\n"
|
|
1266
|
-
f"!total number of images := {self.field.shape[0]}\n"
|
|
1267
|
-
f"imagedata byte order := LITTLEENDIAN\n"
|
|
1268
|
-
f"!number of frame groups := 1\n\n"
|
|
1269
|
-
f"!STATIC STUDY (General) :=\n"
|
|
1270
|
-
f"number of dimensions := 3\n"
|
|
1271
|
-
f"!matrix size [1] := {self.field.shape[2]}\n"
|
|
1272
|
-
f"!matrix size [2] := {self.field.shape[1]}\n"
|
|
1273
|
-
f"!matrix size [3] := {self.field.shape[0]}\n"
|
|
1274
|
-
f"!number format := short float\n"
|
|
1275
|
-
f"!number of bytes per pixel := 4\n"
|
|
1276
|
-
f"scaling factor (mm/pixel) [1] := {self.params['dx'] * 1000}\n"
|
|
1277
|
-
f"scaling factor (mm/pixel) [2] := {self.params['dx'] * 1000}\n"
|
|
1278
|
-
f"scaling factor (s/pixel) [3] := {1 / self.params['f_AQ']}\n"
|
|
1279
|
-
f"first pixel offset (mm) [1] := {self.params['Xrange'][0] * 1e3}\n"
|
|
1280
|
-
f"first pixel offset (mm) [2] := {self.params['Zrange'][0] * 1e3}\n"
|
|
1281
|
-
f"first pixel offset (s) [3] := 0\n"
|
|
1282
|
-
f"data rescale offset := 0\n"
|
|
1283
|
-
f"data rescale slope := 1\n"
|
|
1284
|
-
f"quantification units := 1\n\n"
|
|
1285
|
-
f"!SPECIFIC PARAMETERS :=\n"
|
|
1286
|
-
f"angle (degree) := {self.angle}\n"
|
|
1287
|
-
f"activation list := {''.join(f'{int(self.pattern.activeList[i:i+2], 16):08b}' for i in range(0, len(self.pattern.activeList), 2))}\n"
|
|
1288
|
-
f"number of US transducers := {self.params['num_elements']}\n"
|
|
1289
|
-
f"delay (s) := 0\n"
|
|
1290
|
-
f"us frequency (Hz) := {self.params['f_US']}\n"
|
|
1291
|
-
f"excitation duration (s) := {t_ex}\n"
|
|
1292
|
-
f"!END OF INTERFILE :=\n"
|
|
1293
|
-
)
|
|
1294
|
-
# Save the .hdr file
|
|
1295
|
-
with open(hdr_path, "w") as f_hdr:
|
|
1296
|
-
f_hdr.write(header)
|
|
1297
|
-
|
|
1298
|
-
with open(os.path.join(pathFolder, "field.hdr"), "w") as f_hdr2:
|
|
1299
|
-
f_hdr2.write(headerFieldGlob)
|
|
1300
|
-
except Exception as e:
|
|
1301
|
-
print(f"Error saving HDR/IMG files: {e}")
|
|
1302
|
-
|
|
1303
|
-
def _generate_2Dacoustic_field_KWAVE(self, isGPU=True if config.get_process() == 'gpu' else False):
|
|
1304
|
-
"""
|
|
1305
|
-
Generate a 2D acoustic field using k-Wave.
|
|
1306
|
-
|
|
1307
|
-
Args:
|
|
1308
|
-
isGpu (bool): Flag indicating whether to use GPU for simulation.
|
|
1309
|
-
|
|
1310
|
-
Returns:
|
|
1311
|
-
ndarray: Simulated acoustic field data.
|
|
1312
|
-
"""
|
|
1313
|
-
try:
|
|
1314
|
-
active_list = np.array([int(char) for char in ''.join(f"{int(self.pattern.activeList[i:i+2], 16):08b}" for i in range(0, len(self.pattern.activeList), 2))])
|
|
1315
|
-
|
|
1316
|
-
# Probe mask: aligned in the XZ plane
|
|
1317
|
-
source = kSource()
|
|
1318
|
-
source.p_mask = np.zeros((self.params['Nx'], self.params['Nz'])) # Create an empty grid for the source mask
|
|
1319
|
-
|
|
1320
|
-
# Calculate the number of grid points per element
|
|
1321
|
-
element_width_meters = self.params['element_width'] # Element width in meters
|
|
1322
|
-
dx = self.params['dx'] # Spatial resolution in meters
|
|
1323
|
-
element_width_grid_points = int(round(element_width_meters / dx)) # Element width in grid points
|
|
1324
|
-
|
|
1325
|
-
# Calculate the spacing between elements
|
|
1326
|
-
total_elements_width = self.params['num_elements'] * element_width_grid_points
|
|
1327
|
-
remaining_space = self.params['Nx'] - total_elements_width
|
|
1328
|
-
spacing = remaining_space // (self.params['num_elements'] + 1) # Spacing between elements
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
center_index = np.argmin(np.abs(np.linspace(self.params['Xrange'][0], self.params['Xrange'][1], self.params['Nx'])))
|
|
1332
|
-
|
|
1333
|
-
activeListGrid = np.zeros(total_elements_width, dtype=int)
|
|
1334
|
-
|
|
1335
|
-
# Place active transducers in the mask and count active elements
|
|
1336
|
-
active_indices = []
|
|
1337
|
-
current_position = center_index - (total_elements_width + (self.params['num_elements'] - 1) * spacing) // 2
|
|
1338
|
-
for i in range(self.params['num_elements']):
|
|
1339
|
-
if active_list[i] == 1: # Check if the element is active
|
|
1340
|
-
x_pos = current_position
|
|
1341
|
-
source.p_mask[x_pos:x_pos + element_width_grid_points, 0] = 1 # Position in the XZ plane
|
|
1342
|
-
active_indices.append(i) # Record the index of the active element
|
|
1343
|
-
start_idx = i * element_width_grid_points
|
|
1344
|
-
end_idx = start_idx + element_width_grid_points
|
|
1345
|
-
activeListGrid[start_idx:end_idx] = 1
|
|
1346
|
-
current_position += element_width_grid_points + spacing # Move to the next element position
|
|
1347
|
-
source.p_mask = source.p_mask.astype(int)
|
|
1348
|
-
|
|
1349
|
-
# Ensure source.p matches the number of active elements
|
|
1350
|
-
source.p = self.delayedSignal[activeListGrid == 1, :]
|
|
1351
|
-
|
|
1352
|
-
# Define sensors to observe acoustic fields
|
|
1353
|
-
sensor = kSensor()
|
|
1354
|
-
sensor.mask = np.ones((self.params['Nx'], self.params['Nz'])) # Sensor covering the entire domain
|
|
1355
|
-
|
|
1356
|
-
# Simulation options
|
|
1357
|
-
simulation_options = SimulationOptions(
|
|
1358
|
-
pml_inside=False, # Prevent PML from being added inside the grid
|
|
1359
|
-
pml_x_size=20, # PML size on the X-axis
|
|
1360
|
-
pml_z_size=20, # PML size on the Z-axis
|
|
1361
|
-
use_sg=False,
|
|
1362
|
-
save_to_disk=True,
|
|
1363
|
-
input_filename=os.path.join(gettempdir(), "KwaveIN.h5"),
|
|
1364
|
-
output_filename=os.path.join(gettempdir(), "KwaveOUT.h5")
|
|
1365
|
-
)
|
|
1366
|
-
|
|
1367
|
-
execution_options = SimulationExecutionOptions(
|
|
1368
|
-
is_gpu_simulation=config.get_process() == 'gpu' and isGPU,
|
|
1369
|
-
device_num=config.bestGPU
|
|
1370
|
-
)
|
|
1371
|
-
# Run the simulation
|
|
1372
|
-
print("Starting simulation...")
|
|
1373
|
-
sensor_data = kspaceFirstOrder2D(
|
|
1374
|
-
# kgrid=self.kgrid,
|
|
1375
|
-
kgrid=self.kgrid,
|
|
1376
|
-
medium=self.medium,
|
|
1377
|
-
source=source,
|
|
1378
|
-
sensor=sensor,
|
|
1379
|
-
simulation_options=simulation_options,
|
|
1380
|
-
execution_options=execution_options,
|
|
1381
|
-
)
|
|
1382
|
-
print("Simulation completed successfully.")
|
|
1383
|
-
|
|
1384
|
-
return sensor_data['p'].reshape(self.kgrid.Nt, self.params['Nz'], self.params['Nx'])
|
|
1385
|
-
except Exception as e:
|
|
1386
|
-
print(f"Error generating 2D acoustic field: {e}")
|
|
1387
|
-
return None
|
|
1388
|
-
|
|
1389
|
-
def _generate_3Dacoustic_field_KWAVE(self):
|
|
1390
|
-
"""
|
|
1391
|
-
Generate a 3D acoustic field using k-Wave.
|
|
1392
|
-
|
|
1393
|
-
Returns:
|
|
1394
|
-
ndarray: Simulated acoustic field data.
|
|
1395
|
-
"""
|
|
1396
|
-
try:
|
|
1397
|
-
active_list = np.array([int(char) for char in ''.join(f"{int(self.pattern.activeList[i:i+2], 16):08b}" for i in range(0, len(self.pattern.activeList), 2))])
|
|
1398
|
-
|
|
1399
|
-
# Initialize grid and medium
|
|
1400
|
-
kgrid = kWaveGrid([self.params['Nx'], self.params['Ny'], self.params['Nz']], [self.params['dx'], self.params['dy'], self.params['dz']])
|
|
1401
|
-
kgrid.setTime(Nt=self.params['Nt'], dt=1 / self.params['f_AQ'])
|
|
1402
|
-
|
|
1403
|
-
# Probe mask: aligned in the XZ plane
|
|
1404
|
-
source = kSource()
|
|
1405
|
-
source.p_mask = np.zeros((self.params['Nx'], self.params['Ny'], self.params['Nz'])) # Create an empty grid for the source mask
|
|
1406
|
-
|
|
1407
|
-
# Place active transducers in the mask
|
|
1408
|
-
for i in range(self.params['num_elements']):
|
|
1409
|
-
if active_list[i] == 1: # Check if the element is active
|
|
1410
|
-
x_pos = i + self.params['Nx'] // 2 - self.params['num_elements'] // 2 # Position of elements on the X-axis
|
|
1411
|
-
source.p_mask[x_pos, self.params['Ny'] // 2, 0] = 1 # Position in the XZ plane
|
|
1412
|
-
|
|
1413
|
-
source.p_mask = source.p_mask.astype(int)
|
|
1414
|
-
source.p = self.delayedSignal[active_list == 1, :]
|
|
1415
|
-
|
|
1416
|
-
# Define sensors to observe acoustic fields
|
|
1417
|
-
sensor = kSensor()
|
|
1418
|
-
sensor.mask = np.ones((self.params['Nx'], self.params['Ny'], self.params['Nz'])) # Sensor covering the entire domain
|
|
1419
|
-
|
|
1420
|
-
# Simulation options
|
|
1421
|
-
simulation_options = SimulationOptions(
|
|
1422
|
-
pml_inside=False, # Prevent PML from being added inside the grid
|
|
1423
|
-
pml_auto=True,
|
|
1424
|
-
use_sg=False,
|
|
1425
|
-
save_to_disk=True,
|
|
1426
|
-
input_filename=os.path.join(gettempdir(), "KwaveIN.h5"),
|
|
1427
|
-
output_filename=os.path.join(gettempdir(), "KwaveOUT.h5")
|
|
1428
|
-
)
|
|
1429
|
-
|
|
1430
|
-
execution_options = SimulationExecutionOptions(
|
|
1431
|
-
is_gpu_simulation=config.get_process() == 'gpu',
|
|
1432
|
-
device_num=config.bestGPU() if config.get_process() == 'gpu' else None
|
|
1433
|
-
)
|
|
1434
|
-
|
|
1435
|
-
# Run the simulation
|
|
1436
|
-
print("Starting simulation...")
|
|
1437
|
-
sensor_data = kspaceFirstOrder3D(
|
|
1438
|
-
kgrid=kgrid,
|
|
1439
|
-
medium=self.medium,
|
|
1440
|
-
source=source,
|
|
1441
|
-
sensor=sensor,
|
|
1442
|
-
simulation_options=simulation_options,
|
|
1443
|
-
execution_options=execution_options,
|
|
1444
|
-
)
|
|
1445
|
-
print("Simulation completed successfully.")
|
|
1446
|
-
|
|
1447
|
-
return sensor_data['p'].reshape(kgrid.Nt, (self.params['Nz'], self.params['Ny'], self.params['Nx']))
|
|
1448
|
-
except Exception as e:
|
|
1449
|
-
print(f"Error generating 3D acoustic field: {e}")
|
|
1450
|
-
return None
|
|
1451
|
-
|
|
1452
|
-
class PlaneWave(StructuredWave):
|
|
1453
|
-
def __init__(self, angle, space_0 = 0, space_1 = 192, move_head_0_2tail = 0, move_tail_1_2head = 0, **kwargs):
|
|
1454
|
-
"""
|
|
1455
|
-
Initialize the PlaneWave object.
|
|
1456
|
-
|
|
1457
|
-
Args:
|
|
1458
|
-
angle_deg (float): Angle in degrees.
|
|
1459
|
-
**kwargs: Additional keyword arguments.
|
|
1460
|
-
"""
|
|
1461
|
-
try:
|
|
1462
|
-
super().__init__(angle, space_0, space_1, move_head_0_2tail, move_tail_1_2head, **kwargs)
|
|
1463
|
-
self.waveType = WaveType.PlaneWave
|
|
1464
|
-
except Exception as e:
|
|
1465
|
-
print(f"Error initializing PlaneWave: {e}")
|
|
1466
|
-
raise
|
|
1467
|
-
|
|
1468
|
-
def _check_angle(self):
|
|
1469
|
-
"""
|
|
1470
|
-
Check if the angle is within the valid range.
|
|
1471
|
-
|
|
1472
|
-
Raises:
|
|
1473
|
-
ValueError: If the angle is not between -20 and 20 degrees.
|
|
1474
|
-
"""
|
|
1475
|
-
if self.angle < -20 or self.angle > 20:
|
|
1476
|
-
raise ValueError("Angle must be between -20 and 20 degrees.")
|
|
1477
|
-
|
|
1478
|
-
def getName_field(self):
|
|
1479
|
-
"""
|
|
1480
|
-
Generate the list of system matrix .hdr file paths for this wave.
|
|
1481
|
-
|
|
1482
|
-
Returns:
|
|
1483
|
-
str: File path for the system matrix .hdr file.
|
|
1484
|
-
"""
|
|
1485
|
-
try:
|
|
1486
|
-
angle_str = self._format_angle()
|
|
1487
|
-
return f"field_{self.pattern.activeList}_{angle_str}"
|
|
1488
|
-
except Exception as e:
|
|
1489
|
-
print(f"Error generating file path: {e}")
|
|
1490
|
-
return None
|
|
1491
|
-
|
|
1492
|
-
class FocusedWave(AcousticField):
|
|
1493
|
-
|
|
1494
|
-
def __init__(self, focal_point, **kwargs):
|
|
1495
|
-
"""
|
|
1496
|
-
Initialize the FocusedWave object.
|
|
1497
|
-
|
|
1498
|
-
Parameters:
|
|
1499
|
-
- focal_point (tuple): The focal point coordinates (x, z) in meters.
|
|
1500
|
-
- **kwargs: Additional keyword arguments for AcousticField initialization.
|
|
1501
|
-
"""
|
|
1502
|
-
super().__init__(**kwargs)
|
|
1503
|
-
self.waveType = WaveType.FocusedWave
|
|
1504
|
-
self.kgrid.setTime(int(self.kgrid.Nt*2),self.kgrid.dt) # Extend the time grid to allow for delays
|
|
1505
|
-
self.focal_point = (focal_point[0] / 1000, focal_point[1] / 1000)
|
|
1506
|
-
self.delayedSignal = self._apply_delay()
|
|
1507
|
-
|
|
1508
|
-
def getName_field(self):
|
|
1509
|
-
"""
|
|
1510
|
-
Generate the name for the field file based on the focal point.
|
|
1511
|
-
|
|
1512
|
-
Returns:
|
|
1513
|
-
str: File name for the system matrix file.
|
|
1514
|
-
"""
|
|
1515
|
-
try:
|
|
1516
|
-
x_focal, z_focal = self.focal_point
|
|
1517
|
-
return f"field_focused_X{x_focal*1000:.2f}_Z{z_focal*1000:.2f}"
|
|
1518
|
-
except Exception as e:
|
|
1519
|
-
print(f"Error generating file name: {e}")
|
|
1520
|
-
return None
|
|
1521
|
-
|
|
1522
|
-
def _apply_delay(self):
|
|
1523
|
-
"""
|
|
1524
|
-
Apply a temporal delay to the signal for each transducer element to focus the wave at the desired focal point.
|
|
1525
|
-
|
|
1526
|
-
Returns:
|
|
1527
|
-
ndarray: Array of delayed signals.
|
|
1528
|
-
"""
|
|
1529
|
-
try:
|
|
1530
|
-
x_focal, z_focal = self.focal_point
|
|
1531
|
-
|
|
1532
|
-
# Calculate the total number of grid points for all elements
|
|
1533
|
-
total_grid_points = self.params['num_elements'] * int(round(self.params['element_width'] / self.params['dx']))
|
|
1534
|
-
|
|
1535
|
-
# Initialize delays array with size total_grid_points
|
|
1536
|
-
delays = np.zeros(total_grid_points)
|
|
1537
|
-
|
|
1538
|
-
# Calculate the physical positions of the elements starting from Xrange[0]
|
|
1539
|
-
element_positions = np.linspace(self.params['Xrange'][0], self.params['Xrange'][1], total_grid_points)
|
|
1540
|
-
|
|
1541
|
-
# Calculate delays based on physical positions
|
|
1542
|
-
for i in range(total_grid_points):
|
|
1543
|
-
distance = np.sqrt((x_focal - element_positions[i])**2 + (z_focal)**2)
|
|
1544
|
-
delays[i] = distance / self.params['c0'] # Delay in seconds
|
|
1545
|
-
|
|
1546
|
-
delay_samples = np.round(delays / self.kgrid.dt).astype(int)
|
|
1547
|
-
max_delay = np.max(np.abs(delay_samples))
|
|
1548
|
-
delayed_signals = np.zeros((total_grid_points, len(self.burst) + max_delay))
|
|
1549
|
-
for i in range(total_grid_points):
|
|
1550
|
-
shift = delay_samples[i]
|
|
1551
|
-
delayed_signals[i, shift:shift + len(self.burst)] = self.burst # Apply delay
|
|
1552
|
-
|
|
1553
|
-
return delayed_signals
|
|
1554
|
-
except Exception as e:
|
|
1555
|
-
print(f"Error applying delay: {e}")
|
|
1556
|
-
return None
|
|
1557
|
-
|
|
1558
|
-
def plot_delay(self):
|
|
1559
|
-
"""
|
|
1560
|
-
Plot the time of the maximum of each delayed signal to visualize the wavefront.
|
|
1561
|
-
"""
|
|
1562
|
-
try:
|
|
1563
|
-
# Find the index of the maximum for each delayed signal
|
|
1564
|
-
max_indices = np.argmax(self.delayedSignal, axis=1)
|
|
1565
|
-
element_indices = np.linspace(0, self.params['num_elements'] - 1, self.delayedSignal.shape[0])
|
|
1566
|
-
# Convert indices to time
|
|
1567
|
-
max_times = max_indices / self.params['f_AQ']
|
|
1568
|
-
|
|
1569
|
-
# Plot the times of the maxima
|
|
1570
|
-
plt.figure(figsize=(10, 6))
|
|
1571
|
-
plt.plot(element_indices, max_times, 'o-')
|
|
1572
|
-
plt.title('Time of Maximum for Each Delayed Signal')
|
|
1573
|
-
plt.xlabel('Transducer Element Index')
|
|
1574
|
-
plt.ylabel('Time of Maximum (s)')
|
|
1575
|
-
plt.grid(True)
|
|
1576
|
-
plt.show()
|
|
1577
|
-
except Exception as e:
|
|
1578
|
-
print(f"Error plotting max times: {e}")
|
|
1579
|
-
|
|
1580
|
-
def _generate_2Dacoustic_field_KWAVE(self, isGpu=True if config.get_process() == 'gpu' else False):
|
|
1581
|
-
"""
|
|
1582
|
-
Generate a 2D acoustic field using k-Wave simulation for a focused wave.
|
|
1583
|
-
|
|
1584
|
-
Parameters:
|
|
1585
|
-
- isGpu (bool): Flag indicating whether to use GPU for simulation.
|
|
1586
|
-
|
|
1587
|
-
Returns:
|
|
1588
|
-
ndarray: Simulated acoustic field data.
|
|
1589
|
-
"""
|
|
1590
|
-
try:
|
|
1591
|
-
# Create a source mask for the transducer
|
|
1592
|
-
source = kSource()
|
|
1593
|
-
source.p_mask = np.zeros((self.params['Nx'], self.params['Nz']))
|
|
1594
|
-
source.p = np.zeros((self.params['num_elements'], self.delayedSignal.shape[1])) # Initialize source pressure
|
|
1595
|
-
# Calculate the center of the transducer
|
|
1596
|
-
center_index = self.params['Nx'] // 2
|
|
1597
|
-
|
|
1598
|
-
coeff = self.delayedSignal.shape[0] // self.params['num_elements']
|
|
1599
|
-
|
|
1600
|
-
if not coeff.is_integer():
|
|
1601
|
-
raise ValueError("The number of elements must be a divisor of the delayed signal length.")
|
|
1602
|
-
|
|
1603
|
-
# Set the active elements in the source mask
|
|
1604
|
-
element_width_grid_points = int(round(self.params['element_width'] / self.params['dx']))
|
|
1605
|
-
for i in range(self.params['num_elements']):
|
|
1606
|
-
source.p[i] = self.delayedSignal[i*coeff]
|
|
1607
|
-
x_pos = center_index - (self.params['num_elements'] // 2) * element_width_grid_points + i * element_width_grid_points
|
|
1608
|
-
source.p_mask[x_pos, 0] = 1
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
# Define sensors to observe acoustic fields
|
|
1612
|
-
sensor = kSensor()
|
|
1613
|
-
sensor.mask = np.ones((self.params['Nx'], self.params['Nz']))
|
|
1614
|
-
|
|
1615
|
-
# Simulation options
|
|
1616
|
-
simulation_options = SimulationOptions(
|
|
1617
|
-
pml_inside=False,
|
|
1618
|
-
pml_x_size=20,
|
|
1619
|
-
pml_z_size=20,
|
|
1620
|
-
use_sg=False,
|
|
1621
|
-
save_to_disk=True,
|
|
1622
|
-
input_filename=os.path.join(gettempdir(), "KwaveIN.h5"),
|
|
1623
|
-
output_filename=os.path.join(gettempdir(), "KwaveOUT.h5")
|
|
1624
|
-
)
|
|
1625
|
-
|
|
1626
|
-
execution_options = SimulationExecutionOptions(
|
|
1627
|
-
is_gpu_simulation=config.get_process() == 'gpu' and isGpu,
|
|
1628
|
-
device_num=config.bestGPU
|
|
1629
|
-
)
|
|
1630
|
-
|
|
1631
|
-
# Run the simulation
|
|
1632
|
-
print("Starting simulation...")
|
|
1633
|
-
sensor_data = kspaceFirstOrder2D(
|
|
1634
|
-
kgrid=self.kgrid,
|
|
1635
|
-
medium=self.medium,
|
|
1636
|
-
source=source,
|
|
1637
|
-
sensor=sensor,
|
|
1638
|
-
simulation_options=simulation_options,
|
|
1639
|
-
execution_options=execution_options,
|
|
1640
|
-
)
|
|
1641
|
-
print("Simulation completed successfully.")
|
|
1642
|
-
|
|
1643
|
-
return sensor_data['p'].reshape(self.kgrid.Nt, self.params['Nz'], self.params['Nx'])
|
|
1644
|
-
except Exception as e:
|
|
1645
|
-
print(f"Error generating 2D acoustic field: {e}")
|
|
1646
|
-
return None
|
|
1647
|
-
|
|
1648
|
-
def _generate_3Dacoustic_field_KWAVE(self, isGpu=True if config.get_process() == 'gpu' else False):
|
|
1649
|
-
"""
|
|
1650
|
-
Generate a 3D acoustic field using k-Wave simulation for a focused wave.
|
|
1651
|
-
|
|
1652
|
-
Parameters:
|
|
1653
|
-
- isGpu (bool): Flag indicating whether to use GPU for simulation.
|
|
1654
|
-
|
|
1655
|
-
Returns:
|
|
1656
|
-
ndarray: Simulated acoustic field data.
|
|
1657
|
-
"""
|
|
1658
|
-
try:
|
|
1659
|
-
# Create a source mask for the transducer
|
|
1660
|
-
source = kSource()
|
|
1661
|
-
source.p_mask = np.zeros((self.params['Nx'], self.params['Ny'], self.params['Nz']))
|
|
1662
|
-
|
|
1663
|
-
# Calculate the center of the transducer
|
|
1664
|
-
center_index_x = self.params['Nx'] // 2
|
|
1665
|
-
center_index_y = self.params['Ny'] // 2
|
|
1666
|
-
|
|
1667
|
-
# Set the active elements in the source mask
|
|
1668
|
-
element_width_grid_points = int(round(self.params['element_width'] / self.params['dx']))
|
|
1669
|
-
for i in range(self.params['num_elements']):
|
|
1670
|
-
x_pos = center_index_x - (self.params['num_elements'] // 2) * element_width_grid_points + i * element_width_grid_points
|
|
1671
|
-
source.p_mask[x_pos, center_index_y, 0] = 1
|
|
1672
|
-
|
|
1673
|
-
# Apply delays to the burst signal using the _apply_delay method
|
|
1674
|
-
delayed_signals = self._apply_delay()
|
|
1675
|
-
|
|
1676
|
-
source.p = delayed_signals.T
|
|
1677
|
-
|
|
1678
|
-
# Define sensors to observe acoustic fields
|
|
1679
|
-
sensor = kSensor()
|
|
1680
|
-
sensor.mask = np.ones((self.params['Nx'], self.params['Ny'], self.params['Nz']))
|
|
1681
|
-
|
|
1682
|
-
# Simulation options
|
|
1683
|
-
simulation_options = SimulationOptions(
|
|
1684
|
-
pml_inside=False,
|
|
1685
|
-
pml_auto=True,
|
|
1686
|
-
use_sg=False,
|
|
1687
|
-
save_to_disk=True,
|
|
1688
|
-
input_filename=os.path.join(gettempdir(), "KwaveIN.h5"),
|
|
1689
|
-
output_filename=os.path.join(gettempdir(), "KwaveOUT.h5")
|
|
1690
|
-
)
|
|
1691
|
-
|
|
1692
|
-
execution_options = SimulationExecutionOptions(
|
|
1693
|
-
is_gpu_simulation=config.get_process() == 'gpu' and isGpu,
|
|
1694
|
-
device_num=config.bestGPU
|
|
1695
|
-
)
|
|
1696
|
-
|
|
1697
|
-
# Run the simulation
|
|
1698
|
-
print("Starting simulation...")
|
|
1699
|
-
sensor_data = kspaceFirstOrder3D(
|
|
1700
|
-
kgrid=self.kgrid,
|
|
1701
|
-
medium=self.medium,
|
|
1702
|
-
source=source,
|
|
1703
|
-
sensor=sensor,
|
|
1704
|
-
simulation_options=simulation_options,
|
|
1705
|
-
execution_options=execution_options,
|
|
1706
|
-
)
|
|
1707
|
-
print("Simulation completed successfully.")
|
|
1708
|
-
|
|
1709
|
-
return sensor_data['p'].reshape(self.kgrid.Nt, self.params['Nz'], self.params['Ny'], self.params['Nx'])
|
|
1710
|
-
except Exception as e:
|
|
1711
|
-
print(f"Error generating 3D acoustic field: {e}")
|
|
1712
|
-
return None
|
|
1713
|
-
|
|
1714
|
-
def _save2D_HDR_IMG(self, filePath):
|
|
1715
|
-
"""
|
|
1716
|
-
Save the acoustic field to .img and .hdr files.
|
|
1717
|
-
|
|
1718
|
-
Parameters:
|
|
1719
|
-
- filePath (str): Path to the folder where files will be saved.
|
|
1720
|
-
"""
|
|
1721
|
-
try:
|
|
1722
|
-
t_ex = 1 / self.params['f_US']
|
|
1723
|
-
x_focal, z_focal = self.focal_point
|
|
1724
|
-
|
|
1725
|
-
# Define file names (img and hdr)
|
|
1726
|
-
file_name = f"field_focused_{x_focal:.2f}_{z_focal:.2f}"
|
|
1727
|
-
|
|
1728
|
-
img_path = os.path.join(filePath, file_name + ".img")
|
|
1729
|
-
hdr_path = os.path.join(filePath, file_name + ".hdr")
|
|
1730
|
-
|
|
1731
|
-
# Save the acoustic field to the .img file
|
|
1732
|
-
with open(img_path, "wb") as f_img:
|
|
1733
|
-
self.field.astype('float32').tofile(f_img)
|
|
1734
|
-
|
|
1735
|
-
# Generate headerFieldGlob
|
|
1736
|
-
headerFieldGlob = (
|
|
1737
|
-
f"!INTERFILE :=\n"
|
|
1738
|
-
f"modality : AOT\n"
|
|
1739
|
-
f"voxels number transaxial: {self.field.shape[2]}\n"
|
|
1740
|
-
f"voxels number transaxial 2: {self.field.shape[1]}\n"
|
|
1741
|
-
f"voxels number axial: {1}\n"
|
|
1742
|
-
f"field of view transaxial: {(self.params['Xrange'][1] - self.params['Xrange'][0]) * 1000}\n"
|
|
1743
|
-
f"field of view transaxial 2: {(self.params['Zrange'][1] - self.params['Zrange'][0]) * 1000}\n"
|
|
1744
|
-
f"field of view axial: {1}\n"
|
|
1745
|
-
)
|
|
1746
|
-
|
|
1747
|
-
# Generate header
|
|
1748
|
-
header = (
|
|
1749
|
-
f"!INTERFILE :=\n"
|
|
1750
|
-
f"!imaging modality := AOT\n\n"
|
|
1751
|
-
f"!GENERAL DATA :=\n"
|
|
1752
|
-
f"!data offset in bytes := 0\n"
|
|
1753
|
-
f"!name of data file := system_matrix/{file_name}.img\n\n"
|
|
1754
|
-
f"!GENERAL IMAGE DATA\n"
|
|
1755
|
-
f"!total number of images := {self.field.shape[0]}\n"
|
|
1756
|
-
f"imagedata byte order := LITTLEENDIAN\n"
|
|
1757
|
-
f"!number of frame groups := 1\n\n"
|
|
1758
|
-
f"!STATIC STUDY (General) :=\n"
|
|
1759
|
-
f"number of dimensions := 3\n"
|
|
1760
|
-
f"!matrix size [1] := {self.field.shape[2]}\n"
|
|
1761
|
-
f"!matrix size [2] := {self.field.shape[1]}\n"
|
|
1762
|
-
f"!matrix size [3] := {self.field.shape[0]}\n"
|
|
1763
|
-
f"!number format := short float\n"
|
|
1764
|
-
f"!number of bytes per pixel := 4\n"
|
|
1765
|
-
f"scaling factor (mm/pixel) [1] := {self.params['dx'] * 1000}\n"
|
|
1766
|
-
f"scaling factor (mm/pixel) [2] := {self.params['dx'] * 1000}\n"
|
|
1767
|
-
f"scaling factor (s/pixel) [3] := {1 / self.params['f_AQ']}\n"
|
|
1768
|
-
f"first pixel offset (mm) [1] := {self.params['Xrange'][0] * 1e3}\n"
|
|
1769
|
-
f"first pixel offset (mm) [2] := {self.params['Zrange'][0] * 1e3}\n"
|
|
1770
|
-
f"first pixel offset (s) [3] := 0\n"
|
|
1771
|
-
f"data rescale offset := 0\n"
|
|
1772
|
-
f"data rescale slope := 1\n"
|
|
1773
|
-
f"quantification units := 1\n\n"
|
|
1774
|
-
f"!SPECIFIC PARAMETERS :=\n"
|
|
1775
|
-
f"focal point (x, z) := {x_focal}, {z_focal}\n"
|
|
1776
|
-
f"number of US transducers := {self.params['num_elements']}\n"
|
|
1777
|
-
f"delay (s) := 0\n"
|
|
1778
|
-
f"us frequency (Hz) := {self.params['f_US']}\n"
|
|
1779
|
-
f"excitation duration (s) := {t_ex}\n"
|
|
1780
|
-
f"!END OF INTERFILE :=\n"
|
|
1781
|
-
)
|
|
1782
|
-
|
|
1783
|
-
# Save the .hdr file
|
|
1784
|
-
with open(hdr_path, "w") as f_hdr:
|
|
1785
|
-
f_hdr.write(header)
|
|
1786
|
-
|
|
1787
|
-
with open(os.path.join(filePath, "field.hdr"), "w") as f_hdr2:
|
|
1788
|
-
f_hdr2.write(headerFieldGlob)
|
|
1789
|
-
except Exception as e:
|
|
1790
|
-
print(f"Error saving HDR/IMG files: {e}")
|
|
1791
|
-
|
|
1792
|
-
class HydroWave(AcousticField):
|
|
1793
|
-
|
|
1794
|
-
def __init__(self, waveType, dim=Dim.D3,**kwargs):
|
|
1795
|
-
super().__init__(**kwargs)
|
|
1796
|
-
if type(dim) != Dim:
|
|
1797
|
-
raise TypeError("dim must be an instance of the Dim Enum")
|
|
1798
|
-
if type(waveType) != WaveType:
|
|
1799
|
-
raise TypeError("waveType must be an instance of the WaveType Enum")
|
|
1800
|
-
self.waveType = waveType
|
|
1801
|
-
self.params = {
|
|
1802
|
-
'typeSim': TypeSim.HYDRO.value,
|
|
1803
|
-
'dim': dim.value,
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
def getName_field(self):
|
|
1808
|
-
raise NotImplementedError("getName_field method not implemented for HydroWave.")
|
|
1809
|
-
pass
|
|
1810
|
-
|
|
1811
|
-
def _generate_2Dacoustic_field_KWAVE(self):
|
|
1812
|
-
raise NotImplementedError("2D acoustic field generation not implemented for HydroWave.")
|
|
1813
|
-
|
|
1814
|
-
def _generate_3Dacoustic_field_KWAVE(self):
|
|
1815
|
-
raise NotImplementedError("3D acoustic field generation not implemented for HydroWave.")
|
|
1816
|
-
|
|
1817
|
-
def _save2D_HDR_IMG(sel):
|
|
1818
|
-
raise NotImplementedError("HDR/IMG saving not implemented for HydroWave.")
|
|
1819
|
-
|
|
1820
|
-
class IrregularWave(AcousticField):
|
|
1821
|
-
"""
|
|
1822
|
-
Class for irregular wave types, inheriting from AcousticField.
|
|
1823
|
-
This class is a placeholder for future implementation of irregular wave types.
|
|
1824
|
-
"""
|
|
1825
|
-
|
|
1826
|
-
def __init__(self, **kwargs):
|
|
1827
|
-
super().__init__(**kwargs)
|
|
1828
|
-
self.waveType = WaveType.IrregularWave
|
|
1829
|
-
self.params = {
|
|
1830
|
-
'typeSim': TypeSim.IRREGULAR.value,
|
|
1831
|
-
}
|
|
1832
|
-
|
|
1833
|
-
def getName_field(self):
|
|
1834
|
-
raise NotImplementedError("getName_field method not implemented for IrregularWave.")
|
|
1835
|
-
|
|
1836
|
-
def _generate_diverse_structurations(self,num_elements, num_sequences, num_frequencies):
|
|
1837
|
-
"""
|
|
1838
|
-
Génère num_sequences structurations irrégulières ON/OFF pour une sonde de num_elements éléments.
|
|
1839
|
-
Chaque structuration contient exactement num_frequencies fréquences spatiales distinctes.
|
|
1840
|
-
|
|
1841
|
-
:param num_elements: Nombre total d'éléments piézoélectriques de la sonde.
|
|
1842
|
-
:param num_sequences: Nombre total de structurations générées.
|
|
1843
|
-
:param num_frequencies: Nombre de fréquences spatiales distinctes par structuration.
|
|
1844
|
-
:return: Matrice de structuration de taille (num_sequences, num_elements)
|
|
1845
|
-
"""
|
|
1846
|
-
|
|
1847
|
-
# Définition des fréquences spatiales disponibles
|
|
1848
|
-
max_freq = num_elements // 2 # Nyquist limit
|
|
1849
|
-
available_frequencies = np.arange(1, max_freq + 1) # Fréquences possibles
|
|
1850
|
-
|
|
1851
|
-
# Matrice des structurations
|
|
1852
|
-
structurations = np.zeros((num_sequences, num_elements), dtype=int)
|
|
1853
|
-
|
|
1854
|
-
# Sélectionner des fréquences uniques pour chaque structuration
|
|
1855
|
-
chosen_frequencies = []
|
|
1856
|
-
for _ in range(num_sequences):
|
|
1857
|
-
freqs = np.random.choice(available_frequencies, size=num_frequencies, replace=False)
|
|
1858
|
-
chosen_frequencies.append(freqs)
|
|
1859
|
-
|
|
1860
|
-
# Construire la structuration correspondante
|
|
1861
|
-
structuration = np.zeros(num_elements)
|
|
1862
|
-
for f in freqs:
|
|
1863
|
-
structuration += np.cos(2 * np.pi * f * np.arange(num_elements) / num_elements) # Ajouter la fréquence
|
|
1864
|
-
|
|
1865
|
-
structuration = np.where(structuration >= 0, 1, 0) # Binarisation ON/OFF
|
|
1866
|
-
structurations[_] = structuration
|
|
1867
|
-
|
|
1868
|
-
return structurations, chosen_frequencies
|
|
1869
|
-
|
|
1870
|
-
def getName_field(self):
|
|
1871
|
-
raise NotImplementedError("getName_field method not implemented for IrregularWave.")
|
|
1872
|
-
|
|
1873
|
-
def _generate_2Dacoustic_field_KWAVE(self, isGpu):
|
|
1874
|
-
raise NotImplementedError("2D acoustic field generation not implemented for IrregularWave.")
|
|
1875
|
-
|
|
1876
|
-
def _generate_3Dacoustic_field_KWAVE(self, isGpu):
|
|
1877
|
-
raise NotImplementedError("3D acoustic field generation not implemented for IrregularWave.")
|
|
1878
|
-
|
|
1879
|
-
def _save2D_HDR_IMG(self, filePath):
|
|
1880
|
-
raise NotImplementedError("HDR/IMG saving not implemented for IrregularWave.")
|
|
1881
|
-
|