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.
Files changed (50) hide show
  1. AOT_biomaps/AOT_Acoustic/AcousticEnums.py +64 -0
  2. AOT_biomaps/AOT_Acoustic/AcousticTools.py +221 -0
  3. AOT_biomaps/AOT_Acoustic/FocusedWave.py +244 -0
  4. AOT_biomaps/AOT_Acoustic/IrregularWave.py +66 -0
  5. AOT_biomaps/AOT_Acoustic/PlaneWave.py +43 -0
  6. AOT_biomaps/AOT_Acoustic/StructuredWave.py +392 -0
  7. AOT_biomaps/AOT_Acoustic/__init__.py +15 -0
  8. AOT_biomaps/AOT_Acoustic/_mainAcoustic.py +978 -0
  9. AOT_biomaps/AOT_Experiment/Focus.py +55 -0
  10. AOT_biomaps/AOT_Experiment/Tomography.py +505 -0
  11. AOT_biomaps/AOT_Experiment/__init__.py +9 -0
  12. AOT_biomaps/AOT_Experiment/_mainExperiment.py +532 -0
  13. AOT_biomaps/AOT_Optic/Absorber.py +24 -0
  14. AOT_biomaps/AOT_Optic/Laser.py +70 -0
  15. AOT_biomaps/AOT_Optic/OpticEnums.py +17 -0
  16. AOT_biomaps/AOT_Optic/__init__.py +10 -0
  17. AOT_biomaps/AOT_Optic/_mainOptic.py +204 -0
  18. AOT_biomaps/AOT_Recon/AOT_Optimizers/DEPIERRO.py +191 -0
  19. AOT_biomaps/AOT_Recon/AOT_Optimizers/LS.py +106 -0
  20. AOT_biomaps/AOT_Recon/AOT_Optimizers/MAPEM.py +456 -0
  21. AOT_biomaps/AOT_Recon/AOT_Optimizers/MLEM.py +333 -0
  22. AOT_biomaps/AOT_Recon/AOT_Optimizers/PDHG.py +221 -0
  23. AOT_biomaps/AOT_Recon/AOT_Optimizers/__init__.py +5 -0
  24. AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Huber.py +90 -0
  25. AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/Quadratic.py +86 -0
  26. AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/RelativeDifferences.py +59 -0
  27. AOT_biomaps/AOT_Recon/AOT_PotentialFunctions/__init__.py +3 -0
  28. AOT_biomaps/AOT_Recon/AlgebraicRecon.py +1023 -0
  29. AOT_biomaps/AOT_Recon/AnalyticRecon.py +154 -0
  30. AOT_biomaps/AOT_Recon/BayesianRecon.py +230 -0
  31. AOT_biomaps/AOT_Recon/DeepLearningRecon.py +35 -0
  32. AOT_biomaps/AOT_Recon/PrimalDualRecon.py +210 -0
  33. AOT_biomaps/AOT_Recon/ReconEnums.py +375 -0
  34. AOT_biomaps/AOT_Recon/ReconTools.py +273 -0
  35. AOT_biomaps/AOT_Recon/__init__.py +11 -0
  36. AOT_biomaps/AOT_Recon/_mainRecon.py +288 -0
  37. AOT_biomaps/Config.py +95 -0
  38. AOT_biomaps/Settings.py +45 -13
  39. AOT_biomaps/__init__.py +271 -18
  40. aot_biomaps-2.9.233.dist-info/METADATA +22 -0
  41. aot_biomaps-2.9.233.dist-info/RECORD +43 -0
  42. {AOT_biomaps-2.1.3.dist-info → aot_biomaps-2.9.233.dist-info}/WHEEL +1 -1
  43. AOT_biomaps/AOT_Acoustic.py +0 -1881
  44. AOT_biomaps/AOT_Experiment.py +0 -541
  45. AOT_biomaps/AOT_Optic.py +0 -219
  46. AOT_biomaps/AOT_Reconstruction.py +0 -1416
  47. AOT_biomaps/config.py +0 -54
  48. AOT_biomaps-2.1.3.dist-info/METADATA +0 -20
  49. AOT_biomaps-2.1.3.dist-info/RECORD +0 -11
  50. {AOT_biomaps-2.1.3.dist-info → aot_biomaps-2.9.233.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,978 @@
1
+ import AOT_biomaps.Settings
2
+ from AOT_biomaps.Config import config
3
+ from AOT_biomaps.AOT_Acoustic.AcousticTools import calculate_envelope_squared, loadmat
4
+ from .AcousticTools import next_power_of_2, reshape_field
5
+ from .AcousticEnums import TypeSim, Dim, FormatSave, WaveType
6
+
7
+ from IPython.display import HTML
8
+ import h5py
9
+ import os
10
+ import numpy as np
11
+ import matplotlib.pyplot as plt
12
+ import matplotlib.animation as animation
13
+ from kwave.kgrid import kWaveGrid
14
+ from kwave.kmedium import kWaveMedium
15
+ from kwave.utils.signals import tone_burst
16
+ from kwave.ksource import kSource
17
+ from kwave.ksensor import kSensor
18
+ from kwave.kspaceFirstOrder3D import kspaceFirstOrder3D
19
+ from kwave.kspaceFirstOrder2D import kspaceFirstOrder2D
20
+ from kwave.options.simulation_options import SimulationOptions
21
+ from kwave.options.simulation_execution_options import SimulationExecutionOptions
22
+ from AOT_biomaps.Settings import Params
23
+
24
+ from tempfile import gettempdir
25
+ from math import ceil
26
+ from abc import ABC, abstractmethod
27
+ import logging
28
+
29
+
30
+
31
+ ####### ABSTRACT CLASS #######
32
+
33
+ class AcousticField(ABC):
34
+ """
35
+ Abstract class to generate and manipulate acoustic fields for ultrasound imaging.
36
+ Provides methods to initialize parameters, generate fields, save and load data, and calculate envelopes.
37
+
38
+ Principal parameters:
39
+ - field: Acoustic field data.
40
+ - burst: Burst signal used for generating the field for each piezo elements.
41
+ - delayedSignal: Delayed burst signal for each piezo element.
42
+ - 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.
43
+ """
44
+
45
+ def __init__(self, params):
46
+ """
47
+ Initialize global properties of the AcousticField object.
48
+
49
+ Parameters:
50
+ - typeSim (TypeSim): Type of simulation to be performed. Options include KWAVE, FIELD2, and HYDRO. Default is TypeSim.KWAVE.
51
+ - dim (Dim): Dimension of the acoustic field. Can be 2D or 3D. Default is Dim.D2.
52
+ - c0 (float): Speed of sound in the medium, specified in meters per second (m/s). Default is 1540 m/s.
53
+ - f_US (float): Frequency of the ultrasound signal, specified in Hertz (Hz). Default is 6 MHz.
54
+ - f_AQ (float): Frequency of data acquisition, specified in Hertz (Hz). Default is 180 MHz.
55
+ - f_saving (float): Frequency at which the acoustic field data is saved, specified in Hertz (Hz). Default is 10 MHz.
56
+ - num_cycles (int): Number of cycles in the burst signal. Default is 4 cycles.
57
+ - num_elements (int): Number of elements in the transducer array. Default is 192 elements.
58
+ - element_width (float): Width of each transducer element, specified in meters (m). Default is 0.2 mm.
59
+ - element_height (float): Height of each transducer element, specified in meters (m). Default is 6 mm.
60
+ - Xrange (list of float): Range of X coordinates for the acoustic field, specified in meters (m). Default is from -20 mm to 20 mm.
61
+ - 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.
62
+ - Zrange (list of float): Range of Z coordinates for the acoustic field, specified in meters (m). Default is from 0 m to 37 mm.
63
+ """
64
+ required_keys = [
65
+ 'c0', 'f_US', 'f_AQ', 'f_saving', 'num_cycles', 'num_elements',
66
+ 'element_width', 'element_height', 'Xrange', 'Zrange', 'dim',
67
+ 'typeSim', 'dx', 'dz'
68
+ ]
69
+
70
+ # Verify required keys
71
+ try:
72
+ if params != None:
73
+ for key in required_keys:
74
+ if key not in params.acoustic and key not in params.general:
75
+ raise ValueError(f"{key} must be provided in the parameters.")
76
+ except ValueError as e:
77
+ print(f"Initialization error: {e}")
78
+ raise
79
+ if params != None:
80
+ if type(params) != Params:
81
+ raise TypeError("params must be an instance of the Params class")
82
+
83
+ self.params = {
84
+ 'c0': params.acoustic['c0'],
85
+ 'voltage': params.acoustic['voltage'],
86
+ 'sensitivity': params.acoustic['sensitivity'],
87
+ 'Foc': params.acoustic['Foc'],
88
+ 'N_piezoFocal': params.acoustic['N_piezoFocal'],
89
+ 'f_US': int(float(params.acoustic['f_US'])),
90
+ 'f_AQ': params.acoustic['f_AQ'],
91
+ 'f_saving': int(float(params.acoustic['f_saving'])),
92
+ 'num_cycles': params.acoustic['num_cycles'],
93
+ 'num_elements': params.acoustic['num_elements'],
94
+ 'element_width': params.acoustic['element_width'],
95
+ 'element_height': params.acoustic['element_height'],
96
+ 'Xrange': params.general['Xrange'],
97
+ 'Yrange': params.general['Yrange'],
98
+ 'Zrange': params.general['Zrange'],
99
+ 'dim': params.acoustic['dim'],
100
+ 'typeSim': params.acoustic['typeSim'],
101
+ 'dx': params.general['dx'],
102
+ 'dy': params.general['dy'] if params.general['Yrange'] is not None else None,
103
+ 'dz': params.general['dz'],
104
+ 'Nx': int(np.round((params.general['Xrange'][1] - params.general['Xrange'][0])/params.general['dx'])),
105
+ 'Ny': int(np.round((params.general['Yrange'][1] - params.general['Yrange'][0])/params.general['dy'])) if params.general['Yrange'] is not None else 1,
106
+ 'Nz': int(np.round((params.general['Zrange'][1] - params.general['Zrange'][0])/params.general['dz'])),
107
+ 'probeWidth': params.acoustic['num_elements'] * params.acoustic['element_width'],
108
+ 'IsAbsorbingMedium': params.acoustic['isAbsorbingMedium'],
109
+ }
110
+ self.kgrid = kWaveGrid([self.params["Nx"], self.params["Nz"]], [self.params["dx"], self.params["dz"]])
111
+ if params.acoustic['f_AQ'] == "AUTO":
112
+
113
+ self.kgrid.makeTime(self.params['c0'])
114
+
115
+ self.params['f_AQ'] = int(1/self.kgrid.dt)
116
+ else:
117
+ Nt = ceil((self.params['Zrange'][1] - self.params['Zrange'][0])*float(params.acoustic['f_AQ']) / self.params['c0'])
118
+
119
+ self.kgrid.setTime(Nt,1/float(params.acoustic['f_AQ']))
120
+ self.params['f_AQ'] = int(float(params.acoustic['f_AQ']))
121
+
122
+ self._generate_burst_signal()
123
+ if self.params["dim"] == Dim.D3 and self.params["Yrange"] is None:
124
+ raise ValueError("Yrange must be provided for 3D fields.")
125
+ if self.params['typeSim'] == TypeSim.KWAVE.value:
126
+ if self.params ['IsAbsorbingMedium'] == True:
127
+ self.medium = kWaveMedium(
128
+ sound_speed=self.params['c0'],
129
+ density=params.acoustic['Absorption']['density'],
130
+ alpha_coeff=params.acoustic['Absorption']['alpha_coeff'], # dB/(MHz·cm)
131
+ alpha_power=params.acoustic['Absorption']['alpha_power'], # 0.5
132
+ absorbing=True
133
+ )
134
+ else:
135
+ self.medium = kWaveMedium(sound_speed=self.params['c0'])
136
+ elif self.params['typeSim'] == TypeSim.FIELD2.value:
137
+ self.medium = None
138
+ else:
139
+ self.medium = None
140
+
141
+ self.waveType = None
142
+ self.field = None
143
+
144
+ def __str__(self):
145
+ """
146
+ Returns a string representation of the AcousticField object, including its parameters and attributes.
147
+ The string is formatted in a table-like structure for better readability.
148
+ """
149
+ try:
150
+ # Get all attributes of the instance
151
+ attrs = {**self.params, **{k: v for k, v in vars(self).items() if k not in self.params}}
152
+
153
+ # Base attributes of AcousticField
154
+ base_attrs_keys = ['c0', 'f_US', 'f_AQ', 'f_saving', 'num_cycles', 'num_elements',
155
+ 'element_width', 'element_height',
156
+ 'Xrange', 'Yrange', 'Zrange', 'dim', 'typeSim', 'Nx', 'Ny', 'Nz',
157
+ 'dx', 'dy', 'dz', 'probeWidth']
158
+ base_attrs = {key: value for key, value in attrs.items() if key in base_attrs_keys}
159
+
160
+ # Attributes specific to the derived class, excluding 'params'
161
+ derived_attrs = {key: value for key, value in attrs.items() if key not in base_attrs_keys and key != 'params'}
162
+
163
+ # Create lines for base and derived attributes
164
+ base_attr_lines = [f" {key}: {value}" for key, value in base_attrs.items()]
165
+
166
+ derived_attr_lines = []
167
+ for key, value in derived_attrs.items():
168
+ if key in {'burst', 'delayedSignal'}:
169
+ continue
170
+ elif key == 'pattern':
171
+ # Inspect the pattern object
172
+ try:
173
+ pattern_attrs = vars(value)
174
+ pattern_str = ", ".join([f"{k}={v}" for k, v in pattern_attrs.items()])
175
+ derived_attr_lines.append(f" pattern: {{{pattern_str}}}")
176
+ except Exception as e:
177
+ derived_attr_lines.append(f" pattern: <unreadable: {e}>")
178
+ else:
179
+ try:
180
+ derived_attr_lines.append(f" {key}: {value}")
181
+ except Exception as e:
182
+ derived_attr_lines.append(f" {key}: <unprintable: {e}>")
183
+
184
+ # Add shapes for burst and delayedSignal
185
+ if 'burst' in derived_attrs:
186
+ derived_attr_lines.append(f" burst: shape={self.burst.shape}")
187
+ if 'delayedSignal' in derived_attrs:
188
+ derived_attr_lines.append(f" delayedSignal: shape={self.delayedSignal.shape}")
189
+
190
+ # Define borders and titles
191
+ border = "+" + "-" * 40 + "+"
192
+ title = f"|Type : {self.__class__.__name__} wave |"
193
+ base_title = "| AcousticField Attributes |"
194
+ derived_title = f"| {self.__class__.__name__} Specific Attributes |" if derived_attrs else ""
195
+
196
+ # Convert attributes to strings
197
+ base_attr_str = "\n".join(base_attr_lines)
198
+ derived_attr_str = "\n".join(derived_attr_lines)
199
+
200
+ # Assemble the final result
201
+ result = f"{border}\n{title}\n{border}\n{base_title}\n{border}\n{base_attr_str}\n"
202
+ if derived_attrs:
203
+ result += f"\n{border}\n{derived_title}\n{border}\n{derived_attr_str}\n"
204
+ result += border
205
+
206
+ return result
207
+ except Exception as e:
208
+ print(f"Error in __str__ method: {e}")
209
+ raise
210
+
211
+ def __del__(self):
212
+ """
213
+ Destructor for the AcousticField class. Cleans up the field and envelope attributes.
214
+ """
215
+ try:
216
+ self.field = None
217
+ self.burst = None
218
+ self.delayedSignal = None
219
+ except Exception as e:
220
+ print(f"Error in __del__ method: {e}")
221
+ raise
222
+
223
+ ## TOOLS METHODS ##
224
+
225
+ def generate_field(self, isGpu=config.get_process() == 'gpu',show_log = True):
226
+ """
227
+ Generate the acoustic field based on the specified simulation type and parameters.
228
+ """
229
+ try:
230
+ logging.getLogger('root').setLevel(logging.ERROR)
231
+ if self.params['typeSim'] == TypeSim.FIELD2.value:
232
+ raise NotImplementedError("FIELD2 simulation is not implemented yet.")
233
+ elif self.params['typeSim'] == TypeSim.KWAVE.value:
234
+ if self.params["dim"] == Dim.D2.value:
235
+ try:
236
+ field = self._generate_acoustic_field_KWAVE_2D(isGpu, show_log)
237
+ except Exception as e:
238
+ raise RuntimeError(f"Failed to generate 2D acoustic field: {e}")
239
+ self.field = reshape_field(calculate_envelope_squared(field),[self.factorT, self.factorX, self.factorZ])
240
+ elif self.params["dim"] == Dim.D3.value:
241
+ field = self._generate_acoustic_field_KWAVE_3D(isGpu, show_log)
242
+ self.field = reshape_field(calculate_envelope_squared(field),[self.factorT, self.factorX, self.factorZ])
243
+ elif self.params['typeSim'] == TypeSim.HYDRO.value:
244
+ raise ValueError("Cannot generate field for Hydrophone simulation, load exciting acquisitions.")
245
+ else:
246
+ raise ValueError("Invalid simulation type. Supported types are: FIELD2, KWAVE, HYDRO.")
247
+ except Exception as e:
248
+ print(f"Error in generate_field method: {e}")
249
+ raise
250
+
251
+ def save_field(self, filePath, formatSave=FormatSave.HDR_IMG):
252
+ """
253
+ Save the acoustic field to a file in the specified format.
254
+
255
+ Parameters:
256
+ - filePath (str): The path where the file will be saved.
257
+ """
258
+ try:
259
+ if formatSave.value == FormatSave.HDR_IMG.value:
260
+ self._save2D_HDR_IMG(filePath)
261
+ elif formatSave.value == FormatSave.H5.value:
262
+ self._save2D_H5(filePath)
263
+ elif formatSave.value == FormatSave.NPY.value:
264
+ self._save2D_NPY(filePath)
265
+ else:
266
+ raise ValueError("Unsupported format. Supported formats are: HDR_IMG, H5, NPY.")
267
+ except Exception as e:
268
+ print(f"Error in save_field method: {e}")
269
+ raise
270
+
271
+ def load_field(self, folderPath, formatSave=FormatSave.HDR_IMG, nameBlock=None):
272
+ """
273
+ Load the acoustic field from a file in the specified format.
274
+
275
+ Parameters:
276
+ - filePath (str): The folder path from which to load the file.
277
+ """
278
+ try:
279
+ if str(type(formatSave)) != str(AOT_biomaps.AOT_Acoustic.FormatSave):
280
+ raise ValueError(f"Unsupported file format: {formatSave}. Supported formats are: HDR_IMG, H5, NPY.")
281
+
282
+ if self.params['typeSim'] == TypeSim.FIELD2.value:
283
+ raise NotImplementedError("FIELD2 simulation is not implemented yet.")
284
+ elif self.params['typeSim'] == TypeSim.KWAVE.value:
285
+ if formatSave.value == FormatSave.HDR_IMG.value:
286
+ if self.params["dim"] == Dim.D2.value:
287
+ self._load_fieldKWAVE_XZ(os.path.join(folderPath,self.getName_field()+formatSave.value))
288
+ elif self.params["dim"] == Dim.D3.value:
289
+ raise NotImplementedError("3D KWAVE field loading is not implemented yet.")
290
+ elif formatSave.value == FormatSave.H5.value:
291
+ if self.params["dim"] == Dim.D2.value:
292
+ self._load_field_h5(folderPath,nameBlock)
293
+ elif self.params["dim"] == Dim.D3.value:
294
+ raise NotImplementedError("H5 KWAVE field loading is not implemented yet.")
295
+ elif formatSave.value == FormatSave.NPY.value:
296
+ if self.params["dim"] == Dim.D2.value:
297
+ self.field = np.load(os.path.join(folderPath,self.getName_field()+formatSave.value))
298
+ elif self.params["dim"] == Dim.D3.value:
299
+ raise NotImplementedError("3D NPY KWAVE field loading is not implemented yet.")
300
+ elif self.params['typeSim'] == TypeSim.HYDRO.value:
301
+ print("Loading Hydrophone field...")
302
+ if formatSave.value == FormatSave.HDR_IMG.value:
303
+ raise ValueError("HDR_IMG format is not supported for Hydrophone acquisition.")
304
+ if formatSave.value == FormatSave.H5.value:
305
+ if self.params["dim"] == Dim.D2.value:
306
+ 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'))
307
+ elif self.params["dim"] == Dim.D3.value:
308
+ self._load_fieldHYDRO_XYZ(os.path.join(folderPath, self.getName_field() + '.h5'), os.path.join(folderPath, "PARAMS_" +self.getName_field() + '.mat'))
309
+ elif formatSave.value == FormatSave.NPY.value:
310
+ if self.params["dim"] == Dim.D2.value:
311
+ self.field = np.load(folderPath)
312
+ elif self.params["dim"] == Dim.D3.value:
313
+ raise NotImplementedError("3D NPY Hydrophone field loading is not implemented yet.")
314
+ else:
315
+ raise ValueError("Invalid simulation type. Supported types are: FIELD2, KWAVE, HYDRO.")
316
+
317
+ except Exception as e:
318
+ print(f"Error in load_field method: {e}")
319
+ raise
320
+
321
+ @abstractmethod
322
+ def getName_field(self):
323
+ pass
324
+
325
+ ## DISPLAY METHODS ##
326
+
327
+ def plot_burst_signal(self):
328
+ """
329
+ Plot the burst signal used for generating the acoustic field.
330
+ """
331
+ try:
332
+ time2plot = np.arange(0, len(self.burst)) / self.params['f_AQ'] * 1000000 # Convert to microseconds
333
+ plt.figure(figsize=(8, 8))
334
+ plt.plot(time2plot, self.burst)
335
+ plt.title('Excitation burst signal')
336
+ plt.xlabel('Time (µs)')
337
+ plt.ylabel('Amplitude')
338
+ plt.grid()
339
+ plt.show()
340
+ except Exception as e:
341
+ print(f"Error in plot_burst_signal method: {e}")
342
+ raise
343
+
344
+ def animated_plot_AcousticField(self, desired_duration_ms = 5000, save_dir=None):
345
+ """
346
+ Plot synchronized animations of A_matrix slices for selected angles.
347
+
348
+ Args:
349
+ step (int): Time step between frames (default is every 10 frames).
350
+ save_dir (str): Directory to save the animation gif; if None, animation will not be saved.
351
+
352
+ Returns:
353
+ ani: Matplotlib FuncAnimation object.
354
+ """
355
+ try:
356
+
357
+ maxF = np.max(self.field[:,20:,:])
358
+ minF = np.min(self.field[:,20:,:])
359
+ # Set the maximum embedded animation size to 100 MB
360
+ plt.rcParams['animation.embed_limit'] = 100
361
+
362
+ if save_dir is not None:
363
+ os.makedirs(save_dir, exist_ok=True)
364
+
365
+ # Create a figure and axis
366
+ fig, ax = plt.subplots()
367
+
368
+ # Set main title
369
+ if self.waveType.value == WaveType.FocusedWave.value:
370
+ fig.suptitle("[System Matrix Animation] Focused Wave", fontsize=12, y=0.98)
371
+ elif self.waveType.value == WaveType.PlaneWave.value:
372
+ fig.suptitle(f"[System Matrix Animation] Plane Wave | Angles {self.angle}°", fontsize=12, y=0.98)
373
+ elif self.waveType.value == WaveType.StructuredWave.value:
374
+ fig.suptitle(f"[System Matrix Animation] Structured Wave | Pattern structure: {self.pattern.activeList} | Angles {self.angle}°", fontsize=12, y=0.98)
375
+ else:
376
+
377
+ raise ValueError("Invalid wave type. Supported types are: FocusedWave, PlaneWave, StructuredWave.")
378
+
379
+ # Initial plot
380
+ im = ax.imshow(
381
+ self.field[0, :, :],
382
+ extent=(self.params['Xrange'][0] * 1000, self.params['Xrange'][-1] * 1000, self.params['Zrange'][-1] * 1000, self.params['Zrange'][0] * 1000),
383
+ vmin = 1.2*minF,
384
+ vmax=0.8*maxF,
385
+ aspect='equal',
386
+ cmap='jet',
387
+ animated=True
388
+ )
389
+ ax.set_title(f"t = 0 ms", fontsize=10)
390
+ ax.set_xlabel("x (mm)", fontsize=8)
391
+ ax.set_ylabel("z (mm)", fontsize=8)
392
+
393
+
394
+ # Unified update function for all subplots
395
+ def update(frame):
396
+ im.set_data(self.field[frame, :, :])
397
+ ax.set_title(f"t = {frame / self.params['f_AQ'] * 1000:.2f} ms", fontsize=10)
398
+ return [im] # Return a list of artists that were modified
399
+
400
+ interval = desired_duration_ms / self.field.shape[0]
401
+
402
+ # Create animation
403
+ ani = animation.FuncAnimation(
404
+ fig, update,
405
+ frames=range(0, self.field.shape[0]),
406
+ interval=interval, blit=True
407
+ )
408
+
409
+ # Save animation if needed
410
+ if save_dir is not None:
411
+ if self.waveType == WaveType.FocusedWave:
412
+ save_filename = f"Focused_Wave_.gif"
413
+ elif self.waveType == WaveType.PlaneWave:
414
+ save_filename = f"Plane_Wave_{self._format_angle()}.gif"
415
+ else:
416
+ save_filename = f"Structured_Wave_PatternStructure_{self.pattern.activeList}_{self._format_angle()}.gif"
417
+ save_path = os.path.join(save_dir, save_filename)
418
+ ani.save(save_path, writer='pillow', fps=20)
419
+ print(f"Saved: {save_path}")
420
+
421
+ plt.close(fig)
422
+
423
+ return HTML(ani.to_jshtml())
424
+ except Exception as e:
425
+ print(f"Error creating animation: {e}")
426
+ return None
427
+
428
+ def show(self, use_dB=False, reference=1e6,Vmax=None):
429
+ """
430
+ Display the maximum intensity projection of the acoustic field envelope.
431
+
432
+ Parameters:
433
+ - use_dB (bool): If True, display in dB relative to the reference pressure.
434
+ - reference (float): Reference pressure in Pa for dB calculation (default: 1 MPa).
435
+ """
436
+ try:
437
+ if self.field is None:
438
+ raise ValueError("Field data is not available. Please generate or load the field first.")
439
+ if self.field.min() < 0:
440
+ raise ValueError("Calculation of the envelope has not been performed. Please generate the envelope first.")
441
+
442
+ # Convertir l'enveloppe au carré en amplitude (Pa) en prenant la racine carrée
443
+ envelope_amplitude = np.sqrt(self.field)
444
+
445
+ if use_dB:
446
+ # Convertir en dB re reference (Pa)
447
+ envelope_dB = 20 * np.log10(envelope_amplitude / reference)
448
+ data_to_show = envelope_dB
449
+ unit_label = f'dB re {reference / 1e6} MPa'
450
+ if Vmax is not None:
451
+ vmax = Vmax
452
+ else:
453
+ vmax = 0
454
+
455
+ else:
456
+ # Convertir en MPa
457
+ envelope_amplitude_mpa = envelope_amplitude / 1e6
458
+ data_to_show = envelope_amplitude_mpa
459
+ unit_label = 'MPa'
460
+ if Vmax is not None:
461
+ vmax = Vmax
462
+ else:
463
+ vmax = 0.85*np.max(envelope_amplitude_mpa)
464
+
465
+ plt.figure(figsize=(10, 6))
466
+ plt.imshow(data_to_show.max(axis=0),
467
+ extent=(self.params['Xrange'][0] * 1000, self.params['Xrange'][1] * 1000,
468
+ self.params['Zrange'][1] * 1000, self.params['Zrange'][0] * 1000),
469
+ aspect='equal', cmap='jet', vmin=0, vmax=vmax)
470
+ plt.colorbar(label=f'Envelope Amplitude ({unit_label})')
471
+ plt.title('Maximum Intensity Projection of Acoustic Field Envelope')
472
+ plt.xlabel('X (mm)')
473
+ plt.ylabel('Z (mm)')
474
+ plt.show()
475
+ except Exception as e:
476
+ print(f"Error in show method: {e}")
477
+ raise
478
+
479
+
480
+ ## PRIVATE METHODS ##
481
+
482
+ def _generate_burst_signal(self):
483
+ if self.params['typeSim'] == TypeSim.FIELD2.value:
484
+ raise NotImplementedError("FIELD2 simulation is not implemented yet.")
485
+ elif self.params['typeSim'] == TypeSim.KWAVE.value:
486
+ self._generate_burst_signalKWAVE()
487
+ elif self.params['typeSim'] == TypeSim.HYDRO.value:
488
+ raise ValueError("Cannot generate burst signal for Hydrophone simulation.")
489
+
490
+ def _generate_burst_signalKWAVE(self):
491
+ """
492
+ Private method to generate a burst signal based on the specified parameters.
493
+ """
494
+ try:
495
+ self.burst = tone_burst(1/self.kgrid.dt, self.params['f_US'], self.params['num_cycles']).squeeze()
496
+ except Exception as e:
497
+ print(f"Error in __generate_burst_signal method: {e}")
498
+ raise
499
+
500
+ def _generate_acoustic_field_KWAVE_2D(self, isGPU=True if config.get_process() == 'gpu' else False, show_log=True):
501
+ """
502
+ Base function to generate a 2D acoustic field using k-Wave.
503
+ Handles common setup, simulation, and post-processing.
504
+ """
505
+ try:
506
+ # --- 1. Grid setup ---
507
+ dx = self.params['dx']
508
+ if dx >= self.params['element_width']*2:
509
+ dx = self.params['element_width'] / 2
510
+ Nx = int(round((self.params['Xrange'][1] - self.params['Xrange'][0]) / dx))
511
+ Nz = int(round((self.params['Zrange'][1] - self.params['Zrange'][0]) / dx))
512
+ else:
513
+ Nx = self.params['Nx']
514
+ Nz = self.params['Nz']
515
+
516
+ # --- 2. Time and space factors ---
517
+ self.factorT = int(np.ceil(self.params['f_AQ'] / self.params['f_saving']))
518
+ self.factorX = int(np.ceil(Nx / self.params['Nx']))
519
+ self.factorZ = int(np.ceil(Nz / self.params['Nz']))
520
+
521
+ # --- 3. Grid and source initialization ---
522
+ kgrid = kWaveGrid([Nx, Nz], [dx, dx])
523
+ kgrid.setTime(self.kgrid.Nt, 1 / self.params['f_AQ'])
524
+
525
+ source = kSource()
526
+ source.p_mask = np.zeros((Nx, Nz))
527
+
528
+ # --- 4. Sensor setup ---
529
+ sensor = kSensor()
530
+ sensor.mask = np.ones((Nx, Nz))
531
+
532
+ # --- 5. PML setup ---
533
+ total_size_x = next_power_of_2(Nx)
534
+ total_size_z = next_power_of_2(Nz)
535
+ pml_x_size = (total_size_x - Nx) // 2
536
+ pml_z_size = (total_size_z - Nz) // 2
537
+ pml_x_size = max(pml_x_size, 50) # Ensure a minimum PML size of 50 grid points to avoid parasitic reflections
538
+ pml_z_size = max(pml_z_size, 50) # Ensure a minimum PML size of 50 grid points to avoid parasitic reflections
539
+
540
+ # --- 6. Simulation options ---
541
+ simulation_options = SimulationOptions(
542
+ pml_inside=False,
543
+ pml_size=[pml_x_size, pml_z_size],
544
+ use_sg=False,
545
+ save_to_disk=True,
546
+ input_filename=os.path.join(gettempdir(), "KwaveIN.h5"),
547
+ output_filename=os.path.join(gettempdir(), "KwaveOUT.h5")
548
+ )
549
+
550
+ execution_options = SimulationExecutionOptions(
551
+ is_gpu_simulation=config.get_process() == 'gpu' and isGPU,
552
+ device_num=config.bestGPU,
553
+ show_sim_log=show_log
554
+ )
555
+
556
+ # --- 7. Call specialized function to set up source.p_mask and source.p ---
557
+ self._SetUpSource(source, Nx, dx, self.factorT)
558
+
559
+ # --- 8. Run simulation ---
560
+ sensor_data = kspaceFirstOrder2D(
561
+ kgrid=kgrid,
562
+ medium=self.medium,
563
+ source=source,
564
+ sensor=sensor,
565
+ simulation_options=simulation_options,
566
+ execution_options=execution_options,
567
+ )
568
+
569
+ # --- 9. Post-process results ---
570
+ data = sensor_data['p'].reshape(kgrid.Nt, Nz, Nx)
571
+ return data
572
+
573
+ except Exception as e:
574
+ print(f"Error generating 2D acoustic field: {e}")
575
+ return None
576
+
577
+ def _generate_acoustic_field_KWAVE_3D(self, isGPU=True, show_log=True):
578
+ """
579
+ Generate a 3D acoustic field using k-Wave.
580
+ """
581
+ try:
582
+ # --- 1. Grid setup (common) ---
583
+ dx = self.params['dx']
584
+ if dx >= self.params['element_width']:
585
+ dx = self.params['element_width'] / 2
586
+ Nx = int(round((self.params['Xrange'][1] - self.params['Xrange'][0]) / dx))
587
+ Nz = int(round((self.params['Zrange'][1] - self.params['Zrange'][0]) / dx))
588
+ else:
589
+ Nx = self.params['Nx']
590
+ Nz = self.params['Nz']
591
+
592
+ # --- 2. Time and space factors (common) ---
593
+ factorT = int(np.ceil(self.params['f_AQ'] / self.params['f_saving']))
594
+ factorX = int(np.ceil(Nx / self.params['Nx']))
595
+ factorZ = int(np.ceil(Nz / self.params['Nz']))
596
+
597
+ kgrid = kWaveGrid([Nx, Nz], [dx, dx])
598
+ kgrid.setTime(self.kgrid.Nt, 1 / self.params['f_AQ'])
599
+
600
+ source = kSource()
601
+ source.p_mask = np.zeros((self.params['Nx'], self.params['Ny'], self.params['Nz']))
602
+
603
+ # Appel à la méthode spécialisée
604
+ self._SetUpSource(source, self.params['Nx'], self.params['dx'], factorT) # factorT=1 pour simplifier
605
+
606
+ sensor = kSensor()
607
+ sensor.mask = np.ones((self.params['Nx'], self.params['Ny'], self.params['Nz']))
608
+
609
+ simulation_options = SimulationOptions(
610
+ pml_inside=False,
611
+ pml_auto=True,
612
+ use_sg=False,
613
+ save_to_disk=True,
614
+ input_filename=os.path.join(gettempdir(), "KwaveIN.h5"),
615
+ output_filename=os.path.join(gettempdir(), "KwaveOUT.h5")
616
+ )
617
+
618
+ execution_options = SimulationExecutionOptions(
619
+ is_gpu_simulation=config.get_process() == 'gpu' and isGPU,
620
+ device_num=config.bestGPU,
621
+ show_sim_log=show_log
622
+ )
623
+
624
+ sensor_data = kspaceFirstOrder3D(
625
+ kgrid=kgrid,
626
+ medium=self.medium,
627
+ source=source,
628
+ sensor=sensor,
629
+ simulation_options=simulation_options,
630
+ execution_options=execution_options,
631
+ )
632
+
633
+ data = sensor_data['p'].reshape(kgrid.Nt, Nz, Nx)
634
+ if factorT != 1 or factorX != 1 or factorZ != 1:
635
+ return reshape_field(data, [factorT, factorX, factorZ])
636
+ else:
637
+ return data
638
+
639
+ except Exception as e:
640
+ print(f"Error generating 3D acoustic field: {e}")
641
+ return None
642
+
643
+ @abstractmethod
644
+ def _SetUpSource(self, source, Nx, dx, factorT):
645
+ """
646
+ Abstract method: each subclass must implement its own source setup.
647
+ """
648
+ pass
649
+
650
+ @abstractmethod
651
+ def _save2D_HDR_IMG(self, filePath):
652
+ """
653
+ Save the 2D acoustic field as an HDR_IMG file.
654
+ Must be implemented in subclasses.
655
+ """
656
+ pass
657
+
658
+ def _load_field_h5(self, filePath,nameBlock):
659
+ """
660
+ Load the 2D acoustic field from an H5 file.
661
+
662
+ Parameters:
663
+ - filePath (str): The path to the H5 file.
664
+
665
+ Returns:
666
+ - field (numpy.ndarray): The loaded acoustic field.
667
+ """
668
+ try:
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][:]
673
+ except Exception as e:
674
+ print(f"Error in _load_field_h5 method: {e}")
675
+ raise
676
+
677
+ def _save2D_H5(self, filePath):
678
+ """
679
+ Save the 2D acoustic field as an H5 file.
680
+
681
+ Parameters:
682
+ - filePath (str): The path where the file will be saved.
683
+ """
684
+ try:
685
+ with h5py.File(filePath+self.getName_field()+"h5", 'w') as f:
686
+ for key, value in self.__dict__.items():
687
+ if key != 'field':
688
+ f.create_dataset(key, data=value)
689
+ f.create_dataset('data', data=self.field, compression='gzip')
690
+ except Exception as e:
691
+ print(f"Error in _save2D_H5 method: {e}")
692
+ raise
693
+
694
+ def _save2D_NPY(self, filePath):
695
+ """
696
+ Save the 2D acoustic field as a NPY file.
697
+
698
+ Parameters:
699
+ - filePath (str): The path where the file will be saved.
700
+ """
701
+ try:
702
+ np.save(filePath+self.getName_field()+"npy", self.field)
703
+ except Exception as e:
704
+ print(f"Error in _save2D_NPY method: {e}")
705
+ raise
706
+
707
+ def _load_fieldKWAVE_XZ(self, hdr_path):
708
+ """
709
+ Read an Interfile (.hdr) and its binary file (.img) to reconstruct an acoustic field.
710
+
711
+ Parameters:
712
+ - hdr_path (str): The path to the .hdr file.
713
+
714
+ Returns:
715
+ - field (numpy.ndarray): The reconstructed acoustic field with dimensions reordered to (X, Z, time).
716
+ - header (dict): A dictionary containing the metadata from the .hdr file.
717
+ """
718
+ try:
719
+ header = {}
720
+ # Read the .hdr file
721
+ with open(hdr_path, 'r') as f:
722
+ for line in f:
723
+ if ':=' in line:
724
+ key, value = line.split(':=', 1)
725
+ key = key.strip().lower().replace('!', '')
726
+ value = value.strip()
727
+ header[key] = value
728
+
729
+ # Get the associated .img file name
730
+ data_file = header.get('name of data file') or header.get('name of date file')
731
+ if data_file is None:
732
+ raise ValueError(f"Cannot find the data file associated with the header file {hdr_path}")
733
+ img_path = os.path.join(os.path.dirname(hdr_path), os.path.basename(data_file))
734
+
735
+ # Determine the field size from metadata
736
+ shape = [int(header[f'matrix size [{i}]']) for i in range(1, 3) if f'matrix size [{i}]' in header]
737
+ if not shape:
738
+ raise ValueError("Cannot determine the shape of the acoustic field from metadata.")
739
+
740
+ # Data type
741
+ data_type = header.get('number format', 'short float').lower()
742
+ dtype_map = {
743
+ 'short float': np.float32,
744
+ 'float': np.float32,
745
+ 'int16': np.int16,
746
+ 'int32': np.int32,
747
+ 'uint16': np.uint16,
748
+ 'uint8': np.uint8
749
+ }
750
+ dtype = dtype_map.get(data_type)
751
+ if dtype is None:
752
+ raise ValueError(f"Unsupported data type: {data_type}")
753
+
754
+ # Byte order (endianness)
755
+ byte_order = header.get('imagedata byte order', 'LITTLEENDIAN').lower()
756
+ endianess = '<' if 'little' in byte_order else '>'
757
+
758
+ # Verify the actual size of the .img file
759
+ fileSize = os.path.getsize(img_path)
760
+ timeDim = int(fileSize / (np.dtype(dtype).itemsize * np.prod(shape)))
761
+ shape = shape + [timeDim]
762
+
763
+ # Read binary data
764
+ with open(img_path, 'rb') as f:
765
+ data = np.fromfile(f, dtype=endianess + np.dtype(dtype).char)
766
+
767
+ # Reshape data to (time, Z, X)
768
+ field = data.reshape(shape[::-1]) # NumPy interprets in C order (opposite of MATLAB)
769
+
770
+ # Apply scaling factors if available
771
+ rescale_slope = float(header.get('data rescale slope', 1))
772
+ rescale_offset = float(header.get('data rescale offset', 0))
773
+ field = field * rescale_slope + rescale_offset
774
+
775
+ self.field = field
776
+ except Exception as e:
777
+ print(f"Error in _load_fieldKWAVE_XZ method: {e}")
778
+ raise
779
+
780
+ def _load_fieldHYDRO_XZ(self, file_path_h5, param_path_mat):
781
+ """
782
+ Load the 2D acoustic field for Hydrophone simulation from H5 and MAT files.
783
+
784
+ Parameters:
785
+ - file_path_h5 (str): The path to the H5 file.
786
+ - param_path_mat (str): The path to the MAT file.
787
+
788
+ Returns:
789
+ - envelope_transposed (numpy.ndarray): The transposed envelope of the acoustic field.
790
+ """
791
+ try:
792
+ # Load parameters from the .mat file
793
+ param = loadmat(param_path_mat)
794
+
795
+ # Load the ranges for x and z
796
+ x_test = param['x'].flatten()
797
+ z_test = param['z'].flatten()
798
+
799
+ x_range = np.arange(-23, 21.2, 0.2)
800
+ z_range = np.arange(0, 37.2, 0.2)
801
+ X, Z = np.meshgrid(x_range, z_range)
802
+
803
+ # Load the data from the .h5 file
804
+ with h5py.File(file_path_h5, 'r') as file:
805
+ data = file['data'][:]
806
+
807
+ # Initialize a matrix to store the acoustic data
808
+ acoustic_field = np.zeros((len(z_range), len(x_range), data.shape[1]))
809
+
810
+ # Fill the grid with acoustic data
811
+ index = 0
812
+ for i in range(len(z_range)):
813
+ if i % 2 == 0:
814
+ # Traverse left to right
815
+ for j in range(len(x_range)):
816
+ acoustic_field[i, j, :] = data[index]
817
+ index += 1
818
+ else:
819
+ # Traverse right to left
820
+ for j in range(len(x_range) - 1, -1, -1):
821
+ acoustic_field[i, j, :] = data[index]
822
+ index += 1
823
+
824
+ # Calculate the analytic envelope
825
+ envelope = np.abs(CPU_hilbert(acoustic_field, axis=2))
826
+ # Reorganize the array to have the shape (Times, Z, X)
827
+ envelope_transposed = np.transpose(envelope, (2, 0, 1)).T
828
+
829
+ self.field = envelope_transposed
830
+ self.params['Xrange'] = x_range
831
+ self.params['Zrange'] = z_range
832
+
833
+ except Exception as e:
834
+ print(f"Error in _load_fieldHYDRO_XZ method: {e}")
835
+ raise
836
+
837
+ def _load_fieldHYDRO_YZ(self, file_path_h5, param_path_mat):
838
+ """
839
+ Load the 2D acoustic field for Hydrophone simulation from H5 and MAT files.
840
+
841
+ Parameters:
842
+ - file_path_h5 (str): The path to the H5 file.
843
+ - param_path_mat (str): The path to the MAT file.
844
+
845
+ Returns:
846
+ - envelope_transposed (numpy.ndarray): The transposed envelope of the acoustic field.
847
+ - y_range (numpy.ndarray): The range of y values.
848
+ - z_range (numpy.ndarray): The range of z values.
849
+ """
850
+ try:
851
+ # Load parameters from the .mat file
852
+ param = loadmat(param_path_mat)
853
+
854
+ # Extract the ranges for y and z
855
+ y_range = param['y'].flatten()
856
+ z_range = param['z'].flatten()
857
+
858
+ # Load the data from the .h5 file
859
+ with h5py.File(file_path_h5, 'r') as file:
860
+ data = file['data'][:]
861
+
862
+ # Calculate the number of scans
863
+ Ny = len(y_range)
864
+ Nz = len(z_range)
865
+
866
+ # Create the scan positions
867
+ positions_y = []
868
+ positions_z = []
869
+
870
+ for i in range(Nz):
871
+ if i % 2 == 0:
872
+ # Traverse top to bottom for even rows
873
+ positions_y.extend(y_range)
874
+ else:
875
+ # Traverse bottom to top for odd rows
876
+ positions_y.extend(y_range[::-1])
877
+ positions_z.extend([z_range[i]] * Ny)
878
+
879
+ Positions = np.column_stack((positions_y, positions_z))
880
+
881
+ # Initialize a matrix to store the reorganized data
882
+ reorganized_data = np.zeros((Ny, Nz, data.shape[1]))
883
+
884
+ # Reorganize the data according to the scan positions
885
+ for index, (j, k) in enumerate(Positions):
886
+ y_idx = np.where(y_range == j)[0][0]
887
+ z_idx = np.where(z_range == k)[0][0]
888
+ reorganized_data[y_idx, z_idx, :] = data[index, :]
889
+
890
+ # Calculate the analytic envelope
891
+ envelope = np.abs(CPU_hilbert(reorganized_data, axis=2))
892
+ # Reorganize the array to have the shape (Times, Z, Y)
893
+ envelope_transposed = np.transpose(envelope, (2, 0, 1))
894
+ return envelope_transposed, y_range, z_range
895
+ except Exception as e:
896
+ print(f"Error in _load_fieldHYDRO_YZ method: {e}")
897
+ raise
898
+
899
+ def _load_fieldHYDRO_XYZ(self, file_path_h5, param_path_mat):
900
+ """
901
+ Load the 3D acoustic field for Hydrophone simulation from H5 and MAT files.
902
+
903
+ Parameters:
904
+ - file_path_h5 (str): The path to the H5 file.
905
+ - param_path_mat (str): The path to the MAT file.
906
+
907
+ Returns:
908
+ - EnveloppeField (numpy.ndarray): The envelope of the acoustic field.
909
+ - x_range (numpy.ndarray): The range of x values.
910
+ - y_range (numpy.ndarray): The range of y values.
911
+ - z_range (numpy.ndarray): The range of z values.
912
+ """
913
+ try:
914
+ # Load parameters from the .mat file
915
+ param = loadmat(param_path_mat)
916
+
917
+ # Extract the ranges for x, y, and z
918
+ x_range = param['x'].flatten()
919
+ y_range = param['y'].flatten()
920
+ z_range = param['z'].flatten()
921
+
922
+ # Create a meshgrid for x, y, and z
923
+ X, Y, Z = np.meshgrid(x_range, y_range, z_range, indexing='ij')
924
+
925
+ # Load the data from the .h5 file
926
+ with h5py.File(file_path_h5, 'r') as file:
927
+ data = file['data'][:]
928
+
929
+ # Calculate the number of scans
930
+ Nx = len(x_range)
931
+ Ny = len(y_range)
932
+ Nz = len(z_range)
933
+ Nscans = Nx * Ny * Nz
934
+
935
+ # Create the scan positions
936
+ if Ny % 2 == 0:
937
+ X = np.tile(np.concatenate([x_range[:, np.newaxis], x_range[::-1, np.newaxis]]), (Ny // 2, 1))
938
+ Y = np.repeat(y_range, Nx)
939
+ else:
940
+ X = np.concatenate([x_range[:, np.newaxis], np.tile(np.concatenate([x_range[::-1, np.newaxis], x_range[:, np.newaxis]]), ((Ny - 1) // 2, 1))])
941
+ Y = np.repeat(y_range, Nx)
942
+
943
+ XY = np.column_stack((X.flatten(), Y))
944
+
945
+ if Nz % 2 == 0:
946
+ XYZ = np.tile(np.concatenate([XY, np.flipud(XY)]), (Nz // 2, 1))
947
+ Z = np.repeat(z_range, Nx * Ny)
948
+ else:
949
+ XYZ = np.concatenate([XY, np.tile(np.concatenate([np.flipud(XY), XY]), ((Nz - 1) // 2, 1))])
950
+ Z = np.repeat(z_range, Nx * Ny)
951
+
952
+ Positions = np.column_stack((XYZ, Z))
953
+
954
+ # Initialize a matrix to store the reorganized data
955
+ reorganized_data = np.zeros((Nx, Ny, Nz, data.shape[1]))
956
+
957
+ # Reorganize the data according to the scan positions
958
+ for index, (i, j, k) in enumerate(Positions):
959
+ x_idx = np.where(x_range == i)[0][0]
960
+ y_idx = np.where(y_range == j)[0][0]
961
+ z_idx = np.where(z_range == k)[0][0]
962
+ reorganized_data[x_idx, y_idx, z_idx, :] = data[index, :]
963
+
964
+ EnveloppeField = np.zeros_like(reorganized_data)
965
+
966
+ for y in range(reorganized_data.shape[1]):
967
+ for z in range(reorganized_data.shape[2]):
968
+ EnveloppeField[:, y, z, :] = np.abs(CPU_hilbert(reorganized_data[:, y, z, :], axis=1))
969
+ self.field = np.transpose(EnveloppeField, (3, 2, 1, 0))
970
+ self.params['Xrange'] = [x_range[0], x_range[-1]]
971
+ self.params['Yrange'] = [y_range[0], y_range[-1]]
972
+ self.params['Zrange'] = [z_range[0], z_range[-1]]
973
+ self.params['Nx'] = Nx
974
+ self.params['Ny'] = Ny
975
+ self.params['Nz'] = Nz
976
+ except Exception as e:
977
+ print(f"Error in _load_fieldHYDRO_XYZ method: {e}")
978
+ raise