fullwave25 1.2.6.dev0__tar.gz → 1.2.6.dev2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/.bumpversion.toml +1 -1
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/PKG-INFO +1 -1
- fullwave25-1.2.6.dev2/debug_solver_bin/fullwave2_2d_exponential_attenuation_multi_gpu_cuda124 +0 -0
- fullwave25-1.2.6.dev2/debug_solver_bin/fullwave2_3d_exponential_attenuation_multi_gpu_cuda124 +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/__init__.py +1 -1
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/sensor.py +70 -6
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/solver/binary_manager.py +1 -1
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/solver/input_file_writer.py +29 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/solver/pml_builder.py +34 -16
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/solver/solver.py +131 -18
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/utils/__init__.py +2 -1
- fullwave25-1.2.6.dev2/fullwave/utils/signal_filter.py +161 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/pyproject.toml +1 -1
- fullwave25-1.2.6.dev2/signal_filter_example.png +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/uv.lock +1 -1
- fullwave25-1.2.6.dev0/debug_solver_bin/fullwave2_2d_2_relax_multi_gpu_cuda124 +0 -0
- fullwave25-1.2.6.dev0/debug_solver_bin/fullwave2_2d_exponential_attenuation_multi_gpu +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/.gitignore +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/.pre-commit-config.yaml +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/.python-version +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/20260120_0844.log +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/CITATION.cff +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/CLAUDE.md +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/CONTRIBUTING.md +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/LICENSE +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/Makefile +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/README.md +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/beamformer.m +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/experiments/__init__.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/experiments/exp_scatterer_diameter.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/beamformer/__init__.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/beamformer/beamformer.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/constants/__init__.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/constants/material_properties.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/grid.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/medium.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/__init__.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/domain.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/medium_builder.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/__init__.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/data/.keep +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/data/abdominal_wall/i2365f_etfw1.mat +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_abdominal_wall.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_background.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_scatterer.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_simple.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_water_gel.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/solver/__init__.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda118 +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda124 +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda129 +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda130 +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda118 +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda124 +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda129 +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda130 +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/solver/bins/database/relaxation_params_database_num_relax=2_20260113_0957.mat +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/solver/cuda_utils.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/solver/launcher.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/solver/utils.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/source.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/transducer.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/utils/check_functions.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/utils/coordinates.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/utils/memory_tempfile.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/utils/numerical.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/utils/plot_utils.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/utils/pulse.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/utils/relaxation_parameters.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/utils/scatterer.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/utils/signal_process.py +0 -0
- {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/ruff.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fullwave25
|
|
3
|
-
Version: 1.2.6.
|
|
3
|
+
Version: 1.2.6.dev2
|
|
4
4
|
Summary: Fullwave 2.5: Ultrasound wave propagation simulation with heterogeneous power law attenuation modelling capabilities
|
|
5
5
|
Author: Masashi Sode, Gianmarco Pinton
|
|
6
6
|
Maintainer: Masashi Sode
|
|
Binary file
|
|
Binary file
|
|
@@ -60,7 +60,7 @@ try:
|
|
|
60
60
|
__version__ = version("fullwave")
|
|
61
61
|
except PackageNotFoundError:
|
|
62
62
|
# Update via bump-my-version, not manually
|
|
63
|
-
__version__ = "1.2.6-
|
|
63
|
+
__version__ = "1.2.6-dev2"
|
|
64
64
|
|
|
65
65
|
VERSION = __version__ # for convenience
|
|
66
66
|
logger.info("Fullwave version: %s", __version__)
|
|
@@ -21,13 +21,16 @@ class Sensor:
|
|
|
21
21
|
outcoords: NDArray[np.int64]
|
|
22
22
|
sampling_modulus_time: int = 1
|
|
23
23
|
|
|
24
|
-
def __init__(
|
|
24
|
+
def __init__( # noqa: PLR0915
|
|
25
25
|
self,
|
|
26
26
|
mask: NDArray[np.bool] | None = None,
|
|
27
27
|
sampling_modulus_time: int = 1,
|
|
28
28
|
*,
|
|
29
29
|
coords: NDArray[np.int64] | None = None,
|
|
30
30
|
grid_shape: tuple[int, ...] | None = None,
|
|
31
|
+
mod_x: int | None = None,
|
|
32
|
+
mod_y: int | None = None,
|
|
33
|
+
mod_z: int = 0,
|
|
31
34
|
) -> None:
|
|
32
35
|
"""Sensor class for Fullwave.
|
|
33
36
|
|
|
@@ -36,7 +39,7 @@ class Sensor:
|
|
|
36
39
|
mask : NDArray[np.bool] | None
|
|
37
40
|
Binary matrix where the pressure is recorded at each time-step
|
|
38
41
|
shape: [nx, ny] for 2D, [nx, ny, nz] for 3D.
|
|
39
|
-
Mutually exclusive with coords/grid_shape.
|
|
42
|
+
Mutually exclusive with coords/grid_shape and mod_x/mod_y.
|
|
40
43
|
sampling_modulus_time: int
|
|
41
44
|
Sampling modulus in time. Default is 1 (record at every time step).
|
|
42
45
|
Changing this value to n will record the pressure every n time steps.
|
|
@@ -46,39 +49,95 @@ class Sensor:
|
|
|
46
49
|
Must be provided together with grid_shape.
|
|
47
50
|
grid_shape : tuple[int, ...] | None
|
|
48
51
|
Shape of the computational grid. Required when using coords input.
|
|
52
|
+
mod_x : int | None
|
|
53
|
+
Spatial decimation stride in x (depth) for sparse-grid sensor output.
|
|
54
|
+
When provided together with mod_y, activates sparse-grid mode.
|
|
55
|
+
In this mode the solver binary generates sensor positions automatically
|
|
56
|
+
as [::mod_x, ::mod_y] (and [::mod_z] for 3D) across the domain,
|
|
57
|
+
ignoring any explicit coords/mask. Mutually exclusive with mask/coords.
|
|
58
|
+
mod_y : int | None
|
|
59
|
+
Spatial decimation stride in y (lateral). Must be provided with mod_x.
|
|
60
|
+
mod_z : int
|
|
61
|
+
Spatial decimation stride in z (elevational). Only relevant in 3D simulations.
|
|
62
|
+
Providing mod_z > 0 together with mod_x and mod_y creates a 3D sparse sensor.
|
|
63
|
+
Default 0 (2D sensor, or no z-subsampling when used in a 3D run).
|
|
49
64
|
|
|
50
65
|
Raises
|
|
51
66
|
------
|
|
52
67
|
ValueError
|
|
53
68
|
If grid_shape is not provided when using coords input.
|
|
54
69
|
If both mask and coords are provided (mutually exclusive).
|
|
55
|
-
If
|
|
70
|
+
If only one of mod_x / mod_y is provided.
|
|
71
|
+
If mod_x/mod_y are mixed with mask or coords.
|
|
72
|
+
If none of the three input modes is supplied.
|
|
56
73
|
|
|
57
74
|
"""
|
|
58
|
-
if
|
|
75
|
+
if mod_x is not None or mod_y is not None:
|
|
76
|
+
# --- sparse-grid mode ---
|
|
77
|
+
if mod_x is None or mod_y is None:
|
|
78
|
+
msg = "Both mod_x and mod_y must be provided for sparse-grid sensor mode"
|
|
79
|
+
raise ValueError(msg)
|
|
80
|
+
if mask is not None or coords is not None:
|
|
81
|
+
msg = "mod_x/mod_y are mutually exclusive with mask and coords"
|
|
82
|
+
raise ValueError(msg)
|
|
83
|
+
if mod_x <= 0 or mod_y <= 0:
|
|
84
|
+
msg = "mod_x and mod_y must be positive integers"
|
|
85
|
+
raise ValueError(msg)
|
|
86
|
+
if mod_z < 0:
|
|
87
|
+
msg = "mod_z must be a non-negative integer"
|
|
88
|
+
raise ValueError(msg)
|
|
89
|
+
self.is_sparse_grid = True
|
|
90
|
+
self.mod_x = mod_x
|
|
91
|
+
self.mod_y = mod_y
|
|
92
|
+
self.mod_z = mod_z
|
|
93
|
+
self.is_3d = mod_z > 0
|
|
94
|
+
ndim = 3 if self.is_3d else 2
|
|
95
|
+
# Empty placeholder — the binary computes positions from mod values.
|
|
96
|
+
self.outcoords = np.empty((0, ndim), dtype=np.int64)
|
|
97
|
+
self.grid_shape = None
|
|
98
|
+
elif coords is not None:
|
|
59
99
|
if grid_shape is None:
|
|
60
100
|
msg = "grid_shape is required when using coords input"
|
|
61
101
|
raise ValueError(msg)
|
|
62
102
|
if mask is not None:
|
|
63
103
|
msg = "mask and coords are mutually exclusive"
|
|
64
104
|
raise ValueError(msg)
|
|
105
|
+
self.is_sparse_grid = False
|
|
106
|
+
self.mod_x = 0
|
|
107
|
+
self.mod_y = 0
|
|
108
|
+
self.mod_z = 0
|
|
65
109
|
self.outcoords = np.atleast_2d(coords).astype(np.int64, copy=False)
|
|
66
110
|
self.grid_shape = tuple(grid_shape)
|
|
111
|
+
self.is_3d = len(self.grid_shape) == 3
|
|
67
112
|
elif mask is not None:
|
|
113
|
+
self.is_sparse_grid = False
|
|
114
|
+
self.mod_x = 0
|
|
115
|
+
self.mod_y = 0
|
|
116
|
+
self.mod_z = 0
|
|
68
117
|
mask = np.atleast_2d(mask)
|
|
69
118
|
self.grid_shape = mask.shape
|
|
70
119
|
self.outcoords = map_to_coords(mask)
|
|
120
|
+
self.is_3d = len(self.grid_shape) == 3
|
|
71
121
|
else:
|
|
72
|
-
msg = "Either mask
|
|
122
|
+
msg = "Either mask, coords (with grid_shape), or mod_x with mod_y must be provided"
|
|
73
123
|
raise ValueError(msg)
|
|
74
124
|
|
|
75
125
|
self.sampling_modulus_time = sampling_modulus_time
|
|
76
|
-
self.is_3d = len(self.grid_shape) == 3
|
|
77
126
|
super().__init__()
|
|
78
127
|
logger.debug("Sensor instance created.")
|
|
79
128
|
|
|
80
129
|
def validate(self, grid_shape: NDArray[np.int64] | tuple) -> None:
|
|
81
130
|
"""Check if the sensor coordinates are consistent with the grid shape."""
|
|
131
|
+
if self.is_sparse_grid:
|
|
132
|
+
grid_shape = tuple(grid_shape) if isinstance(grid_shape, np.ndarray) else grid_shape
|
|
133
|
+
if len(grid_shape) == 3 and self.mod_z == 0:
|
|
134
|
+
msg = (
|
|
135
|
+
"Sparse-grid sensor used in a 3D simulation but mod_z was not provided. "
|
|
136
|
+
"Pass mod_z > 0 to Sensor (e.g. Sensor(mod_x=4, mod_y=4, mod_z=4))."
|
|
137
|
+
)
|
|
138
|
+
raise ValueError(msg)
|
|
139
|
+
logger.debug("Sparse-grid sensor validated.")
|
|
140
|
+
return
|
|
82
141
|
grid_shape = tuple(grid_shape) if isinstance(grid_shape, np.ndarray) else grid_shape
|
|
83
142
|
assert self.grid_shape == grid_shape, f"{self.grid_shape} != {grid_shape}"
|
|
84
143
|
assert self.n_sensors > 0, "No active sensor found."
|
|
@@ -173,6 +232,11 @@ class Sensor:
|
|
|
173
232
|
Formatted string containing source information.
|
|
174
233
|
|
|
175
234
|
"""
|
|
235
|
+
if self.is_sparse_grid:
|
|
236
|
+
mod_str = f"mod_x={self.mod_x}, mod_y={self.mod_y}"
|
|
237
|
+
if self.is_3d:
|
|
238
|
+
mod_str += f", mod_z={self.mod_z}"
|
|
239
|
+
return f"Sensor (sparse-grid): \n Strides: {mod_str}\n Is 3D: {self.is_3d}\n"
|
|
176
240
|
return (
|
|
177
241
|
f"Sensor: \n"
|
|
178
242
|
f" Number of sensors: {self.n_sensors}\n"
|
|
@@ -28,7 +28,7 @@ CACHE_DIR = Path.home() / ".cache" / "fullwave25" / "bins"
|
|
|
28
28
|
|
|
29
29
|
# Pinned release tag for the solver binaries.
|
|
30
30
|
# Update this only when new binaries are uploaded to a GitHub release.
|
|
31
|
-
BINARY_RELEASE_TAG = "fullwave_bin_v1.
|
|
31
|
+
BINARY_RELEASE_TAG = "fullwave_bin_v1.3"
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def _download_url(filename: str, tag: str) -> str:
|
|
@@ -36,6 +36,7 @@ class InputFileWriter:
|
|
|
36
36
|
use_exponential_attenuation: bool = False,
|
|
37
37
|
use_isotropic_relaxation: bool = False,
|
|
38
38
|
release_after_write: bool = False,
|
|
39
|
+
pml_thickness: int = 0,
|
|
39
40
|
) -> None:
|
|
40
41
|
"""Initialize the InputGeneratorBase instance.
|
|
41
42
|
|
|
@@ -74,6 +75,10 @@ class InputFileWriter:
|
|
|
74
75
|
Whether to release the variable from memory after writing to file.
|
|
75
76
|
This can help reduce memory usage when generating input files for large simulations.
|
|
76
77
|
default is False.
|
|
78
|
+
pml_thickness : int, optional
|
|
79
|
+
PML boundary thickness in grid points (n_pml_layer + n_transition_layer).
|
|
80
|
+
Required by the solver binary when using sparse grid (mod_x/mod_y != 0) to determine
|
|
81
|
+
the interior domain boundaries. Also written when mod_x == 0 for future use.
|
|
77
82
|
|
|
78
83
|
"""
|
|
79
84
|
logger.debug("Initializing InputFileWriter instance.")
|
|
@@ -99,6 +104,7 @@ class InputFileWriter:
|
|
|
99
104
|
self.is_3d = self.grid.is_3d
|
|
100
105
|
self.use_exponential_attenuation = use_exponential_attenuation
|
|
101
106
|
self.release_after_write = release_after_write
|
|
107
|
+
self.pml_thickness = pml_thickness
|
|
102
108
|
|
|
103
109
|
self._dim = int(
|
|
104
110
|
np.rint(self.medium.sound_speed.max()) - np.rint(self.medium.sound_speed.min()),
|
|
@@ -828,6 +834,7 @@ class InputFileWriter:
|
|
|
828
834
|
self._save_step_params(simulation_dir)
|
|
829
835
|
self._save_coords_params(simulation_dir)
|
|
830
836
|
self._save_d_params(simulation_dir, dim)
|
|
837
|
+
self._save_sparse_grid_params(simulation_dir)
|
|
831
838
|
|
|
832
839
|
if self.use_isotropic_relaxation:
|
|
833
840
|
rename_dict = {
|
|
@@ -894,6 +901,7 @@ class InputFileWriter:
|
|
|
894
901
|
self._save_step_params(simulation_dir)
|
|
895
902
|
self._save_coords_params(simulation_dir)
|
|
896
903
|
self._save_d_params(simulation_dir, dim)
|
|
904
|
+
self._save_sparse_grid_params(simulation_dir)
|
|
897
905
|
|
|
898
906
|
def _build_symbolic_links_for_dat_files(self, src_dir: Path, dst_dir: Path) -> None:
|
|
899
907
|
var_name_list = [
|
|
@@ -916,6 +924,9 @@ class InputFileWriter:
|
|
|
916
924
|
"ncoordszero",
|
|
917
925
|
"nTic",
|
|
918
926
|
"modT",
|
|
927
|
+
"modX",
|
|
928
|
+
"modY",
|
|
929
|
+
"pml_thickness",
|
|
919
930
|
"d",
|
|
920
931
|
"dmap",
|
|
921
932
|
"ndmap",
|
|
@@ -948,6 +959,8 @@ class InputFileWriter:
|
|
|
948
959
|
"bpmly2",
|
|
949
960
|
],
|
|
950
961
|
)
|
|
962
|
+
if self.is_3d:
|
|
963
|
+
var_name_list.append("modZ")
|
|
951
964
|
if self.is_3d and not self.use_isotropic_relaxation:
|
|
952
965
|
var_name_list.extend(
|
|
953
966
|
[
|
|
@@ -1074,6 +1087,22 @@ class InputFileWriter:
|
|
|
1074
1087
|
save_path = simulation_dir / f"{var_name}.dat"
|
|
1075
1088
|
self._queue_v_abs_write(np.int32, save_path, var)
|
|
1076
1089
|
|
|
1090
|
+
def _save_sparse_grid_params(self, simulation_dir: Path) -> None:
|
|
1091
|
+
"""Write sparse-grid parameters when the sensor is in sparse-grid mode.
|
|
1092
|
+
|
|
1093
|
+
modX / modY / modZ and pml_thickness are only written when the sensor
|
|
1094
|
+
was constructed with mod_x/mod_y (i.e. sensor.is_sparse_grid is True).
|
|
1095
|
+
In standard coordinate/mask mode these files are not produced, preserving
|
|
1096
|
+
backward compatibility with older binaries that do not expect them.
|
|
1097
|
+
"""
|
|
1098
|
+
if not self.sensor.is_sparse_grid:
|
|
1099
|
+
return
|
|
1100
|
+
self._queue_v_abs_write(np.int32, simulation_dir / "modX.dat", self.sensor.mod_x)
|
|
1101
|
+
self._queue_v_abs_write(np.int32, simulation_dir / "modY.dat", self.sensor.mod_y)
|
|
1102
|
+
if self.is_3d:
|
|
1103
|
+
self._queue_v_abs_write(np.int32, simulation_dir / "modZ.dat", self.sensor.mod_z)
|
|
1104
|
+
self._queue_v_abs_write(np.int32, simulation_dir / "pml_thickness.dat", self.pml_thickness)
|
|
1105
|
+
|
|
1077
1106
|
def _save_d_params(
|
|
1078
1107
|
self,
|
|
1079
1108
|
simulation_dir: Path,
|
|
@@ -353,14 +353,24 @@ class PMLBuilder:
|
|
|
353
353
|
logger.debug("building extended source for pml...done")
|
|
354
354
|
|
|
355
355
|
logger.debug("building extended sensor for pml...")
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
356
|
+
if self.sensor_org.is_sparse_grid:
|
|
357
|
+
# Sparse-grid sensor: no explicit coordinates to shift.
|
|
358
|
+
# Pass mod values through; the binary generates positions at run time.
|
|
359
|
+
self.extended_sensor = fullwave.Sensor(
|
|
360
|
+
mod_x=self.sensor_org.mod_x,
|
|
361
|
+
mod_y=self.sensor_org.mod_y,
|
|
362
|
+
mod_z=self.sensor_org.mod_z,
|
|
363
|
+
sampling_modulus_time=self.sensor_org.sampling_modulus_time,
|
|
364
|
+
)
|
|
365
|
+
else:
|
|
366
|
+
extended_sensor_grid_shape = tuple(
|
|
367
|
+
s + 2 * self.num_boundary_points for s in self.sensor_org.grid_shape
|
|
368
|
+
)
|
|
369
|
+
self.extended_sensor = fullwave.Sensor(
|
|
370
|
+
coords=self.sensor_org.outcoords + self.num_boundary_points,
|
|
371
|
+
grid_shape=extended_sensor_grid_shape,
|
|
372
|
+
sampling_modulus_time=self.sensor_org.sampling_modulus_time,
|
|
373
|
+
)
|
|
364
374
|
logger.debug("building extended sensor for pml...done")
|
|
365
375
|
if self.is_3d:
|
|
366
376
|
self.pml_mask_x, self.pml_mask_y, self.pml_mask_z = self._localize_pml_region()
|
|
@@ -1643,14 +1653,22 @@ class PMLBuilderExponentialAttenuation(PMLBuilder):
|
|
|
1643
1653
|
w0=getattr(self.source_org, "w0", None),
|
|
1644
1654
|
coords_w=incoords_w_ext,
|
|
1645
1655
|
)
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1656
|
+
if self.sensor_org.is_sparse_grid:
|
|
1657
|
+
self.extended_sensor = fullwave.Sensor(
|
|
1658
|
+
mod_x=self.sensor_org.mod_x,
|
|
1659
|
+
mod_y=self.sensor_org.mod_y,
|
|
1660
|
+
mod_z=self.sensor_org.mod_z,
|
|
1661
|
+
sampling_modulus_time=self.sensor_org.sampling_modulus_time,
|
|
1662
|
+
)
|
|
1663
|
+
else:
|
|
1664
|
+
extended_sensor_grid_shape = tuple(
|
|
1665
|
+
s + 2 * self.num_boundary_points for s in self.sensor_org.grid_shape
|
|
1666
|
+
)
|
|
1667
|
+
self.extended_sensor = fullwave.Sensor(
|
|
1668
|
+
coords=self.sensor_org.outcoords + self.num_boundary_points,
|
|
1669
|
+
grid_shape=extended_sensor_grid_shape,
|
|
1670
|
+
sampling_modulus_time=self.sensor_org.sampling_modulus_time,
|
|
1671
|
+
)
|
|
1654
1672
|
logger.debug("Extended source and sensor for PML built successfully.")
|
|
1655
1673
|
|
|
1656
1674
|
logger.debug("Localizing PML region...")
|
|
@@ -15,6 +15,7 @@ from fullwave.utils import (
|
|
|
15
15
|
MemoryTempfile,
|
|
16
16
|
check_functions,
|
|
17
17
|
)
|
|
18
|
+
from fullwave.utils.signal_filter import apply_filter
|
|
18
19
|
|
|
19
20
|
from .binary_manager import ensure_binary
|
|
20
21
|
from .cuda_utils import get_cuda_architecture, retrieve_cuda_version
|
|
@@ -638,10 +639,86 @@ class Solver:
|
|
|
638
639
|
error_msg = f"{path_fullwave_simulation_bin} does not exist"
|
|
639
640
|
assert path_fullwave_simulation_bin.exists(), error_msg
|
|
640
641
|
|
|
642
|
+
@staticmethod
|
|
643
|
+
def _validate_filter_params(
|
|
644
|
+
highpass_cutoff_mhz: float | None,
|
|
645
|
+
bandpass_cutoff_mhz: tuple[float, float] | None,
|
|
646
|
+
*,
|
|
647
|
+
load_results: bool,
|
|
648
|
+
) -> None:
|
|
649
|
+
"""Validate high-pass / band-pass filter arguments passed to run().
|
|
650
|
+
|
|
651
|
+
Raises
|
|
652
|
+
------
|
|
653
|
+
ValueError
|
|
654
|
+
If both filter options are set simultaneously, or if a filter is
|
|
655
|
+
requested without ``load_results=True``.
|
|
656
|
+
|
|
657
|
+
"""
|
|
658
|
+
if highpass_cutoff_mhz is not None and bandpass_cutoff_mhz is not None:
|
|
659
|
+
error_msg = (
|
|
660
|
+
"highpass_cutoff_mhz and bandpass_cutoff_mhz cannot both be specified. "
|
|
661
|
+
"Use highpass_cutoff_mhz for a simple high-pass filter or "
|
|
662
|
+
"bandpass_cutoff_mhz for a band-pass filter."
|
|
663
|
+
)
|
|
664
|
+
logger.error(error_msg)
|
|
665
|
+
raise ValueError(error_msg)
|
|
666
|
+
if (highpass_cutoff_mhz is not None or bandpass_cutoff_mhz is not None) and (
|
|
667
|
+
not load_results
|
|
668
|
+
):
|
|
669
|
+
error_msg = (
|
|
670
|
+
"Filtering requires load_results=True. "
|
|
671
|
+
"Set load_results=True or disable the filter options."
|
|
672
|
+
)
|
|
673
|
+
logger.error(error_msg)
|
|
674
|
+
raise ValueError(error_msg)
|
|
675
|
+
|
|
676
|
+
@staticmethod
|
|
677
|
+
def _apply_output_filter(
|
|
678
|
+
result: NDArray[np.float64],
|
|
679
|
+
dt: float,
|
|
680
|
+
highpass_cutoff_mhz: float | None,
|
|
681
|
+
bandpass_cutoff_mhz: tuple[float, float] | None,
|
|
682
|
+
) -> NDArray[np.float64]:
|
|
683
|
+
"""Apply the optional frequency filter to the reshaped sensor output.
|
|
684
|
+
|
|
685
|
+
Parameters
|
|
686
|
+
----------
|
|
687
|
+
result : NDArray[np.float64]
|
|
688
|
+
Sensor data shaped ``[n_sensors, n_t]``.
|
|
689
|
+
dt : float
|
|
690
|
+
Grid time step in seconds.
|
|
691
|
+
highpass_cutoff_mhz : float | None
|
|
692
|
+
High-pass edge in MHz, or ``None``.
|
|
693
|
+
bandpass_cutoff_mhz : tuple[float, float] | None
|
|
694
|
+
``(f_low_mhz, f_high_mhz)`` band-pass edges, or ``None``.
|
|
695
|
+
|
|
696
|
+
Returns
|
|
697
|
+
-------
|
|
698
|
+
NDArray[np.float64]
|
|
699
|
+
Filtered (or unchanged) sensor data.
|
|
700
|
+
|
|
701
|
+
"""
|
|
702
|
+
if highpass_cutoff_mhz is not None:
|
|
703
|
+
logger.info("Applying high-pass filter at %.4g MHz...", highpass_cutoff_mhz)
|
|
704
|
+
return apply_filter(result, dt, f_low_hz=highpass_cutoff_mhz * 1e6)
|
|
705
|
+
if bandpass_cutoff_mhz is not None:
|
|
706
|
+
f_low_hz = bandpass_cutoff_mhz[0] * 1e6
|
|
707
|
+
f_high_hz = bandpass_cutoff_mhz[1] * 1e6
|
|
708
|
+
logger.info(
|
|
709
|
+
"Applying band-pass filter %.4g-%.4g MHz...",
|
|
710
|
+
bandpass_cutoff_mhz[0],
|
|
711
|
+
bandpass_cutoff_mhz[1],
|
|
712
|
+
)
|
|
713
|
+
return apply_filter(result, dt, f_low_hz=f_low_hz, f_high_hz=f_high_hz)
|
|
714
|
+
return result
|
|
715
|
+
|
|
641
716
|
@staticmethod
|
|
642
717
|
def _reshape_sensor_data(
|
|
643
718
|
raw_sensor_output: NDArray[np.float64],
|
|
644
719
|
sensor: fullwave.Sensor,
|
|
720
|
+
*,
|
|
721
|
+
n_t: int | None = None,
|
|
645
722
|
) -> NDArray[np.float64]:
|
|
646
723
|
"""Reshape the raw sensor output data.
|
|
647
724
|
|
|
@@ -651,12 +728,21 @@ class Solver:
|
|
|
651
728
|
The raw sensor output data from the simulation. [nt*ncoordsout, 1]
|
|
652
729
|
sensor: fullwave.Sensor
|
|
653
730
|
The sensor object used in the simulation.
|
|
731
|
+
n_t: int | None
|
|
732
|
+
Number of time steps in the extended grid. Required for sparse-grid
|
|
733
|
+
sensors because n_sensors is not known at Python time.
|
|
654
734
|
|
|
655
735
|
Returns
|
|
656
736
|
-------
|
|
657
737
|
NDArray[np.float64]: The reshaped sensor output data. [ncoordsout, nt]
|
|
658
738
|
|
|
659
739
|
"""
|
|
740
|
+
if sensor.is_sparse_grid:
|
|
741
|
+
if n_t is None:
|
|
742
|
+
msg = "n_t is required to reshape sparse-grid sensor output"
|
|
743
|
+
raise ValueError(msg)
|
|
744
|
+
n_t_recorded = -(-n_t // sensor.sampling_modulus_time) # ceiling division
|
|
745
|
+
return raw_sensor_output.reshape(n_t_recorded, -1).T
|
|
660
746
|
return raw_sensor_output.reshape(-1, sensor.n_sensors).T
|
|
661
747
|
|
|
662
748
|
def run(
|
|
@@ -670,6 +756,8 @@ class Solver:
|
|
|
670
756
|
load_results: bool = True,
|
|
671
757
|
generate_input_only: bool = False,
|
|
672
758
|
release_after_write: bool = False,
|
|
759
|
+
highpass_cutoff_mhz: float | None = None,
|
|
760
|
+
bandpass_cutoff_mhz: tuple[float, float] | None = None,
|
|
673
761
|
) -> NDArray[np.float64] | Path:
|
|
674
762
|
r"""Run the fullwave simulation and return the result as a NumPy array.
|
|
675
763
|
|
|
@@ -729,6 +817,18 @@ class Solver:
|
|
|
729
817
|
If True, the memory used by the input files will be released after writing them to disk.
|
|
730
818
|
This is useful when run_on_memory is True to free up memory space for the simulation
|
|
731
819
|
or when the input files are large. Default is False.
|
|
820
|
+
highpass_cutoff_mhz : float | None
|
|
821
|
+
Apply a high-pass filter to the sensor recordings after the simulation.
|
|
822
|
+
Removes low-frequency PML drift by attenuating frequencies below this value (in MHz).
|
|
823
|
+
Uses a cosine (Hann) taper to avoid Gibbs ringing.
|
|
824
|
+
Cannot be combined with ``bandpass_cutoff_mhz``.
|
|
825
|
+
Requires ``load_results=True``. Default is ``None`` (no filtering).
|
|
826
|
+
bandpass_cutoff_mhz : tuple[float, float] | None
|
|
827
|
+
Apply a band-pass filter ``(f_low_mhz, f_high_mhz)`` to the sensor recordings
|
|
828
|
+
after the simulation. Retains only frequencies inside the specified band.
|
|
829
|
+
Uses cosine (Hann) tapers on both edges.
|
|
830
|
+
Cannot be combined with ``highpass_cutoff_mhz``.
|
|
831
|
+
Requires ``load_results=True``. Default is ``None`` (no filtering).
|
|
732
832
|
|
|
733
833
|
Returns
|
|
734
834
|
-------
|
|
@@ -746,6 +846,8 @@ class Solver:
|
|
|
746
846
|
Static map simulations require input files to be stored on a disk.
|
|
747
847
|
run_on_memory, on the other hand, removes the input files
|
|
748
848
|
after the simulation is complete.
|
|
849
|
+
Also raised if both ``highpass_cutoff_mhz`` and ``bandpass_cutoff_mhz`` are given,
|
|
850
|
+
or if either filter option is set but ``load_results=False``.
|
|
749
851
|
|
|
750
852
|
"""
|
|
751
853
|
# self._save_data_for_beamforming()
|
|
@@ -767,6 +869,12 @@ class Solver:
|
|
|
767
869
|
logger.error(error_msg)
|
|
768
870
|
raise ValueError(error_msg)
|
|
769
871
|
|
|
872
|
+
self._validate_filter_params(
|
|
873
|
+
highpass_cutoff_mhz,
|
|
874
|
+
bandpass_cutoff_mhz,
|
|
875
|
+
load_results=load_results,
|
|
876
|
+
)
|
|
877
|
+
|
|
770
878
|
start_time = time.time()
|
|
771
879
|
extended_medium = self.pml_builder.run(use_pml=self.use_pml)
|
|
772
880
|
end_pml_builder_time = time.time()
|
|
@@ -783,30 +891,27 @@ class Solver:
|
|
|
783
891
|
)
|
|
784
892
|
logger.warning(warning_msg)
|
|
785
893
|
|
|
786
|
-
sensor_mask: NDArray[np.bool_]
|
|
787
894
|
if record_whole_domain:
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
self.pml_builder.extended_grid.ny,
|
|
793
|
-
self.pml_builder.extended_grid.nz,
|
|
794
|
-
),
|
|
795
|
-
dtype=bool,
|
|
796
|
-
)
|
|
797
|
-
else:
|
|
798
|
-
sensor_mask = np.zeros(
|
|
799
|
-
(self.pml_builder.extended_grid.nx, self.pml_builder.extended_grid.ny),
|
|
800
|
-
dtype=bool,
|
|
801
|
-
)
|
|
802
|
-
sensor_mask[:, :] = True
|
|
895
|
+
mod_x = 1
|
|
896
|
+
mod_y = 1
|
|
897
|
+
mod_z = 1 if self.is_3d else None
|
|
898
|
+
|
|
803
899
|
sensor = fullwave.Sensor(
|
|
804
|
-
|
|
900
|
+
mod_x=mod_x,
|
|
901
|
+
mod_y=mod_y,
|
|
902
|
+
mod_z=mod_z,
|
|
805
903
|
sampling_modulus_time=sampling_modulus_time_whole_domain,
|
|
806
904
|
)
|
|
807
905
|
else:
|
|
808
906
|
sensor = self.pml_builder.extended_sensor
|
|
809
907
|
|
|
908
|
+
# pml_thickness = PML + transition layers on each side, excluding ghost cells.
|
|
909
|
+
# Used by the binary to locate the interior domain when building a sparse sensor grid.
|
|
910
|
+
if record_whole_domain:
|
|
911
|
+
pml_thickness = 0
|
|
912
|
+
else:
|
|
913
|
+
pml_thickness = self.pml_builder.num_boundary_points - self.pml_builder.m_spatial_order
|
|
914
|
+
|
|
810
915
|
start_input_file_writer_time = time.time()
|
|
811
916
|
input_file_writer = InputFileWriter(
|
|
812
917
|
work_dir=self.work_dir,
|
|
@@ -818,6 +923,7 @@ class Solver:
|
|
|
818
923
|
use_exponential_attenuation=self.use_exponential_attenuation,
|
|
819
924
|
use_isotropic_relaxation=self.use_isotropic_relaxation,
|
|
820
925
|
release_after_write=release_after_write,
|
|
926
|
+
pml_thickness=pml_thickness,
|
|
821
927
|
)
|
|
822
928
|
simulation_dir = input_file_writer.run(
|
|
823
929
|
simulation_dir_name,
|
|
@@ -850,6 +956,7 @@ class Solver:
|
|
|
850
956
|
result = self._reshape_sensor_data(
|
|
851
957
|
sim_result,
|
|
852
958
|
sensor=sensor,
|
|
959
|
+
n_t=self.pml_builder.extended_grid.nt,
|
|
853
960
|
)
|
|
854
961
|
end_loading_time = time.time()
|
|
855
962
|
message = (
|
|
@@ -857,7 +964,13 @@ class Solver:
|
|
|
857
964
|
f"{end_loading_time - start_loading_time:.2e} seconds."
|
|
858
965
|
)
|
|
859
966
|
logger.info(message)
|
|
860
|
-
|
|
967
|
+
|
|
968
|
+
return self._apply_output_filter(
|
|
969
|
+
result,
|
|
970
|
+
self.grid.dt,
|
|
971
|
+
highpass_cutoff_mhz,
|
|
972
|
+
bandpass_cutoff_mhz,
|
|
973
|
+
)
|
|
861
974
|
# if load_results is False, return the raw result
|
|
862
975
|
# which is a list of file names
|
|
863
976
|
return sim_result
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""misc utils for fullwave package."""
|
|
2
2
|
|
|
3
|
-
from . import pulse, relaxation_parameters, signal_process
|
|
3
|
+
from . import pulse, relaxation_parameters, signal_filter, signal_process
|
|
4
4
|
from .memory_tempfile import MemoryTempfile
|
|
5
5
|
from .scatterer import (
|
|
6
6
|
generate_resolution_based_scatterer,
|
|
@@ -14,5 +14,6 @@ __all__ = [
|
|
|
14
14
|
"generate_scatterer",
|
|
15
15
|
"pulse",
|
|
16
16
|
"relaxation_parameters",
|
|
17
|
+
"signal_filter",
|
|
17
18
|
"signal_process",
|
|
18
19
|
]
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""FFT-based frequency-domain filtering for sensor data.
|
|
2
|
+
|
|
3
|
+
GPU backend: CuPy when available; falls back silently to NumPy.
|
|
4
|
+
No new hard dependencies — CuPy is already listed under the ``examples`` optional extra.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from numpy.typing import NDArray
|
|
16
|
+
|
|
17
|
+
logger = logging.getLogger("__main__." + __name__)
|
|
18
|
+
|
|
19
|
+
# Module-level cache to avoid repeated import overhead
|
|
20
|
+
_CUPY_AVAILABLE: bool | None = None
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _check_cupy() -> bool:
|
|
24
|
+
"""Return True if CuPy is importable; result is cached after the first call."""
|
|
25
|
+
global _CUPY_AVAILABLE # noqa: PLW0603
|
|
26
|
+
if _CUPY_AVAILABLE is None:
|
|
27
|
+
try:
|
|
28
|
+
import cupy # noqa: F401
|
|
29
|
+
|
|
30
|
+
_CUPY_AVAILABLE = True
|
|
31
|
+
except ImportError:
|
|
32
|
+
_CUPY_AVAILABLE = False
|
|
33
|
+
return _CUPY_AVAILABLE
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _build_frequency_mask(
|
|
37
|
+
n_fft: int,
|
|
38
|
+
dt: float,
|
|
39
|
+
f_low_hz: float | None = None,
|
|
40
|
+
f_high_hz: float | None = None,
|
|
41
|
+
taper_ratio: float = 0.1,
|
|
42
|
+
) -> NDArray[np.float64]:
|
|
43
|
+
"""Build a frequency-domain gain mask with cosine (Hann) tapers.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
n_fft : int
|
|
48
|
+
FFT length (number of time samples before zero-padding, i.e. ``n_t``).
|
|
49
|
+
dt : float
|
|
50
|
+
Simulation time step in seconds.
|
|
51
|
+
f_low_hz : float | None
|
|
52
|
+
High-pass cut-off frequency in Hz. Frequencies below this value are
|
|
53
|
+
attenuated. The mask transitions smoothly from 0 to 1 in a window of
|
|
54
|
+
width ``f_low_hz * taper_ratio`` centred at ``f_low_hz``.
|
|
55
|
+
f_high_hz : float | None
|
|
56
|
+
Low-pass cut-off frequency in Hz. Frequencies above this value are
|
|
57
|
+
attenuated. The mask transitions smoothly from 1 to 0 in a window of
|
|
58
|
+
width ``f_high_hz * taper_ratio`` centred at ``f_high_hz``.
|
|
59
|
+
taper_ratio : float
|
|
60
|
+
Fractional width of each cosine taper relative to its centre frequency.
|
|
61
|
+
Default is 0.1 (10 %).
|
|
62
|
+
|
|
63
|
+
Returns
|
|
64
|
+
-------
|
|
65
|
+
NDArray[np.float64]
|
|
66
|
+
Frequency-domain gain mask of shape ``[n_fft // 2 + 1]``.
|
|
67
|
+
|
|
68
|
+
"""
|
|
69
|
+
freqs = np.fft.rfftfreq(n_fft, d=dt)
|
|
70
|
+
mask = np.ones(len(freqs), dtype=np.float64)
|
|
71
|
+
|
|
72
|
+
if f_low_hz is not None:
|
|
73
|
+
half_width = f_low_hz * taper_ratio / 2.0
|
|
74
|
+
f_start = f_low_hz - half_width
|
|
75
|
+
f_end = f_low_hz + half_width
|
|
76
|
+
width = f_end - f_start # == f_low_hz * taper_ratio
|
|
77
|
+
|
|
78
|
+
in_taper = (freqs >= f_start) & (freqs <= f_end)
|
|
79
|
+
below_taper = freqs < f_start
|
|
80
|
+
|
|
81
|
+
mask[below_taper] = 0.0
|
|
82
|
+
mask[in_taper] = 0.5 * (1.0 - np.cos(np.pi * (freqs[in_taper] - f_start) / width))
|
|
83
|
+
|
|
84
|
+
if f_high_hz is not None:
|
|
85
|
+
half_width = f_high_hz * taper_ratio / 2.0
|
|
86
|
+
f_start = f_high_hz - half_width
|
|
87
|
+
f_end = f_high_hz + half_width
|
|
88
|
+
width = f_end - f_start # == f_high_hz * taper_ratio
|
|
89
|
+
|
|
90
|
+
in_taper = (freqs >= f_start) & (freqs <= f_end)
|
|
91
|
+
above_taper = freqs > f_end
|
|
92
|
+
|
|
93
|
+
lp_taper = np.ones(len(freqs), dtype=np.float64)
|
|
94
|
+
lp_taper[in_taper] = 0.5 * (1.0 + np.cos(np.pi * (freqs[in_taper] - f_start) / width))
|
|
95
|
+
lp_taper[above_taper] = 0.0
|
|
96
|
+
mask *= lp_taper
|
|
97
|
+
|
|
98
|
+
return mask
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def apply_filter(
|
|
102
|
+
data: NDArray[np.float64],
|
|
103
|
+
dt: float,
|
|
104
|
+
f_low_hz: float | None = None,
|
|
105
|
+
f_high_hz: float | None = None,
|
|
106
|
+
taper_ratio: float = 0.1,
|
|
107
|
+
*,
|
|
108
|
+
use_gpu: bool = True,
|
|
109
|
+
) -> NDArray[np.float64]:
|
|
110
|
+
"""Apply a frequency-domain filter to sensor data.
|
|
111
|
+
|
|
112
|
+
The filter is built as a cosine-tapered gain mask (see :func:`_build_frequency_mask`).
|
|
113
|
+
When CuPy is available and ``use_gpu=True``, the FFT operations run on the GPU
|
|
114
|
+
for maximum throughput; otherwise NumPy is used transparently.
|
|
115
|
+
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
data : NDArray[np.float64]
|
|
119
|
+
Sensor time traces, shape ``[n_sensors, n_t]``.
|
|
120
|
+
dt : float
|
|
121
|
+
Simulation time step in seconds.
|
|
122
|
+
f_low_hz : float | None
|
|
123
|
+
High-pass edge frequency in Hz. Pass ``None`` to skip high-passing.
|
|
124
|
+
f_high_hz : float | None
|
|
125
|
+
Low-pass edge frequency in Hz. Pass ``None`` to skip low-passing.
|
|
126
|
+
taper_ratio : float
|
|
127
|
+
Fractional taper width relative to each cut-off frequency. Default 0.1.
|
|
128
|
+
use_gpu : bool
|
|
129
|
+
If ``True`` (default), attempt to use CuPy for GPU-accelerated FFTs.
|
|
130
|
+
Falls back to NumPy silently if CuPy is unavailable.
|
|
131
|
+
|
|
132
|
+
Returns
|
|
133
|
+
-------
|
|
134
|
+
NDArray[np.float64]
|
|
135
|
+
Filtered data, same shape as ``data``.
|
|
136
|
+
|
|
137
|
+
"""
|
|
138
|
+
n_t = data.shape[1]
|
|
139
|
+
mask = _build_frequency_mask(
|
|
140
|
+
n_t,
|
|
141
|
+
dt,
|
|
142
|
+
f_low_hz=f_low_hz,
|
|
143
|
+
f_high_hz=f_high_hz,
|
|
144
|
+
taper_ratio=taper_ratio,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if use_gpu and _check_cupy():
|
|
148
|
+
import cupy as cp
|
|
149
|
+
|
|
150
|
+
logger.debug("apply_filter: using CuPy GPU backend")
|
|
151
|
+
data_gpu = cp.asarray(data, dtype=cp.float64)
|
|
152
|
+
mask_gpu = cp.asarray(mask, dtype=cp.float64)
|
|
153
|
+
spec = cp.fft.rfft(data_gpu, axis=1)
|
|
154
|
+
spec *= mask_gpu[cp.newaxis, :]
|
|
155
|
+
filtered = cp.fft.irfft(spec, n=n_t, axis=1)
|
|
156
|
+
return cp.asnumpy(filtered)
|
|
157
|
+
|
|
158
|
+
logger.debug("apply_filter: using NumPy CPU backend")
|
|
159
|
+
spec = np.fft.rfft(data, axis=1)
|
|
160
|
+
spec *= mask[np.newaxis, :]
|
|
161
|
+
return np.fft.irfft(spec, n=n_t, axis=1)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fullwave25"
|
|
3
|
-
version = "1.2.6-
|
|
3
|
+
version = "1.2.6-dev2" # Update via bump-my-version, not manually
|
|
4
4
|
description = "Fullwave 2.5: Ultrasound wave propagation simulation with heterogeneous power law attenuation modelling capabilities"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_background.py
RENAMED
|
File without changes
|
{fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_scatterer.py
RENAMED
|
File without changes
|
{fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_simple.py
RENAMED
|
File without changes
|
{fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev2}/fullwave/medium_builder/presets/domain_water_gel.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|