fullwave25 1.2.5.dev0__tar.gz → 1.2.6.dev1__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.5.dev0 → fullwave25-1.2.6.dev1}/.bumpversion.toml +1 -1
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/PKG-INFO +1 -1
- fullwave25-1.2.6.dev1/debug_solver_bin/fullwave2_2d_exponential_attenuation_multi_gpu_cuda124 +0 -0
- fullwave25-1.2.6.dev1/debug_solver_bin/fullwave2_3d_exponential_attenuation_multi_gpu_cuda124 +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/__init__.py +1 -1
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/sensor.py +70 -6
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/binary_manager.py +1 -1
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/input_file_writer.py +46 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/pml_builder.py +77 -17
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/solver.py +27 -17
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/source.py +147 -12
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/pyproject.toml +1 -1
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/uv.lock +1 -1
- fullwave25-1.2.5.dev0/debug_solver_bin/fullwave2_2d_2_relax_multi_gpu_cuda124 +0 -0
- fullwave25-1.2.5.dev0/debug_solver_bin/fullwave2_2d_exponential_attenuation_multi_gpu +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/.gitignore +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/.pre-commit-config.yaml +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/.python-version +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/20260120_0844.log +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/CITATION.cff +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/CLAUDE.md +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/CONTRIBUTING.md +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/LICENSE +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/Makefile +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/README.md +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/beamformer.m +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/experiments/__init__.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/experiments/exp_scatterer_diameter.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/beamformer/__init__.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/beamformer/beamformer.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/constants/__init__.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/constants/material_properties.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/grid.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/__init__.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/domain.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/medium_builder.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/__init__.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/data/.keep +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/data/abdominal_wall/i2365f_etfw1.mat +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/domain_abdominal_wall.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/domain_background.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/domain_scatterer.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/domain_simple.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/domain_water_gel.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/__init__.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda118 +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda124 +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda129 +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda130 +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda118 +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda124 +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda129 +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda130 +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/bins/database/relaxation_params_database_num_relax=2_20260113_0957.mat +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/cuda_utils.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/launcher.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/utils.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/transducer.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/__init__.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/check_functions.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/coordinates.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/memory_tempfile.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/numerical.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/plot_utils.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/pulse.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/relaxation_parameters.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/scatterer.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/signal_process.py +0 -0
- {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/ruff.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fullwave25
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.6.dev1
|
|
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.
|
|
63
|
+
__version__ = "1.2.6-dev1"
|
|
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()),
|
|
@@ -169,6 +175,19 @@ class InputFileWriter:
|
|
|
169
175
|
)
|
|
170
176
|
if incoords_add is not None:
|
|
171
177
|
self._queue_coords_write(simulation_dir / "icc_add.dat", incoords_add)
|
|
178
|
+
for _vel_suffix, _vel_attr in (("u", "u0"), ("v", "v0"), ("w", "w0")):
|
|
179
|
+
_signal_vel = getattr(self.source, _vel_attr, None)
|
|
180
|
+
_incoords_vel = getattr(self.source, f"incoords_{_vel_suffix}", None)
|
|
181
|
+
if _signal_vel is not None:
|
|
182
|
+
self._queue_ic_write(
|
|
183
|
+
simulation_dir / f"icmat_{_vel_suffix}.dat",
|
|
184
|
+
np.transpose(_signal_vel),
|
|
185
|
+
)
|
|
186
|
+
if _incoords_vel is not None:
|
|
187
|
+
self._queue_coords_write(
|
|
188
|
+
simulation_dir / f"icc_{_vel_suffix}.dat",
|
|
189
|
+
_incoords_vel,
|
|
190
|
+
)
|
|
172
191
|
self._queue_coords_write(simulation_dir / "icc.dat", self.source.incoords)
|
|
173
192
|
self._copy_simulation_bin_file(simulation_dir)
|
|
174
193
|
|
|
@@ -815,6 +834,7 @@ class InputFileWriter:
|
|
|
815
834
|
self._save_step_params(simulation_dir)
|
|
816
835
|
self._save_coords_params(simulation_dir)
|
|
817
836
|
self._save_d_params(simulation_dir, dim)
|
|
837
|
+
self._save_sparse_grid_params(simulation_dir)
|
|
818
838
|
|
|
819
839
|
if self.use_isotropic_relaxation:
|
|
820
840
|
rename_dict = {
|
|
@@ -881,6 +901,7 @@ class InputFileWriter:
|
|
|
881
901
|
self._save_step_params(simulation_dir)
|
|
882
902
|
self._save_coords_params(simulation_dir)
|
|
883
903
|
self._save_d_params(simulation_dir, dim)
|
|
904
|
+
self._save_sparse_grid_params(simulation_dir)
|
|
884
905
|
|
|
885
906
|
def _build_symbolic_links_for_dat_files(self, src_dir: Path, dst_dir: Path) -> None:
|
|
886
907
|
var_name_list = [
|
|
@@ -903,6 +924,9 @@ class InputFileWriter:
|
|
|
903
924
|
"ncoordszero",
|
|
904
925
|
"nTic",
|
|
905
926
|
"modT",
|
|
927
|
+
"modX",
|
|
928
|
+
"modY",
|
|
929
|
+
"pml_thickness",
|
|
906
930
|
"d",
|
|
907
931
|
"dmap",
|
|
908
932
|
"ndmap",
|
|
@@ -935,6 +959,8 @@ class InputFileWriter:
|
|
|
935
959
|
"bpmly2",
|
|
936
960
|
],
|
|
937
961
|
)
|
|
962
|
+
if self.is_3d:
|
|
963
|
+
var_name_list.append("modZ")
|
|
938
964
|
if self.is_3d and not self.use_isotropic_relaxation:
|
|
939
965
|
var_name_list.extend(
|
|
940
966
|
[
|
|
@@ -1047,6 +1073,10 @@ class InputFileWriter:
|
|
|
1047
1073
|
n_sources_add = getattr(self.source, "n_sources_add", 0)
|
|
1048
1074
|
if n_sources_add > 0:
|
|
1049
1075
|
var_list.append(("ncoords_add", n_sources_add))
|
|
1076
|
+
for _vel_suffix in ("u", "v", "w"):
|
|
1077
|
+
_n_vel = getattr(self.source, f"n_sources_{_vel_suffix}", 0)
|
|
1078
|
+
if _n_vel > 0:
|
|
1079
|
+
var_list.append((f"ncoords_{_vel_suffix}", _n_vel))
|
|
1050
1080
|
if self.is_3d:
|
|
1051
1081
|
var_list.extend(
|
|
1052
1082
|
[
|
|
@@ -1057,6 +1087,22 @@ class InputFileWriter:
|
|
|
1057
1087
|
save_path = simulation_dir / f"{var_name}.dat"
|
|
1058
1088
|
self._queue_v_abs_write(np.int32, save_path, var)
|
|
1059
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
|
+
|
|
1060
1106
|
def _save_d_params(
|
|
1061
1107
|
self,
|
|
1062
1108
|
simulation_dir: Path,
|
|
@@ -322,24 +322,55 @@ class PMLBuilder:
|
|
|
322
322
|
if getattr(self.source_org, "incoords_add", None) is not None
|
|
323
323
|
else None
|
|
324
324
|
)
|
|
325
|
+
incoords_u_ext = (
|
|
326
|
+
self.source_org.incoords_u + self.num_boundary_points
|
|
327
|
+
if getattr(self.source_org, "incoords_u", None) is not None
|
|
328
|
+
else None
|
|
329
|
+
)
|
|
330
|
+
incoords_v_ext = (
|
|
331
|
+
self.source_org.incoords_v + self.num_boundary_points
|
|
332
|
+
if getattr(self.source_org, "incoords_v", None) is not None
|
|
333
|
+
else None
|
|
334
|
+
)
|
|
335
|
+
incoords_w_ext = (
|
|
336
|
+
self.source_org.incoords_w + self.num_boundary_points
|
|
337
|
+
if getattr(self.source_org, "incoords_w", None) is not None
|
|
338
|
+
else None
|
|
339
|
+
)
|
|
325
340
|
self.extended_source = fullwave.Source(
|
|
326
341
|
p0=self.source_org.p0,
|
|
327
342
|
coords=self.source_org.incoords + self.num_boundary_points,
|
|
328
343
|
grid_shape=extended_grid_shape,
|
|
329
344
|
p0_additive=self.source_org.p0_additive,
|
|
330
345
|
coords_additive=incoords_add_ext,
|
|
346
|
+
u0=getattr(self.source_org, "u0", None),
|
|
347
|
+
coords_u=incoords_u_ext,
|
|
348
|
+
v0=getattr(self.source_org, "v0", None),
|
|
349
|
+
coords_v=incoords_v_ext,
|
|
350
|
+
w0=getattr(self.source_org, "w0", None),
|
|
351
|
+
coords_w=incoords_w_ext,
|
|
331
352
|
)
|
|
332
353
|
logger.debug("building extended source for pml...done")
|
|
333
354
|
|
|
334
355
|
logger.debug("building extended sensor for pml...")
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
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
|
+
)
|
|
343
374
|
logger.debug("building extended sensor for pml...done")
|
|
344
375
|
if self.is_3d:
|
|
345
376
|
self.pml_mask_x, self.pml_mask_y, self.pml_mask_z = self._localize_pml_region()
|
|
@@ -1434,7 +1465,7 @@ class PMLBuilder:
|
|
|
1434
1465
|
class PMLBuilderExponentialAttenuation(PMLBuilder):
|
|
1435
1466
|
"""A class to set up PML for exponential attenuation media."""
|
|
1436
1467
|
|
|
1437
|
-
def __init__(
|
|
1468
|
+
def __init__( # noqa: PLR0915
|
|
1438
1469
|
self,
|
|
1439
1470
|
grid: fullwave.Grid,
|
|
1440
1471
|
medium: fullwave.Medium,
|
|
@@ -1594,21 +1625,50 @@ class PMLBuilderExponentialAttenuation(PMLBuilder):
|
|
|
1594
1625
|
if getattr(self.source_org, "incoords_add", None) is not None
|
|
1595
1626
|
else None
|
|
1596
1627
|
)
|
|
1628
|
+
incoords_u_ext = (
|
|
1629
|
+
self.source_org.incoords_u + self.num_boundary_points
|
|
1630
|
+
if getattr(self.source_org, "incoords_u", None) is not None
|
|
1631
|
+
else None
|
|
1632
|
+
)
|
|
1633
|
+
incoords_v_ext = (
|
|
1634
|
+
self.source_org.incoords_v + self.num_boundary_points
|
|
1635
|
+
if getattr(self.source_org, "incoords_v", None) is not None
|
|
1636
|
+
else None
|
|
1637
|
+
)
|
|
1638
|
+
incoords_w_ext = (
|
|
1639
|
+
self.source_org.incoords_w + self.num_boundary_points
|
|
1640
|
+
if getattr(self.source_org, "incoords_w", None) is not None
|
|
1641
|
+
else None
|
|
1642
|
+
)
|
|
1597
1643
|
self.extended_source = fullwave.Source(
|
|
1598
1644
|
p0=self.source_org.p0,
|
|
1599
1645
|
coords=self.source_org.incoords + self.num_boundary_points,
|
|
1600
1646
|
grid_shape=extended_grid_shape,
|
|
1601
1647
|
p0_additive=self.source_org.p0_additive,
|
|
1602
1648
|
coords_additive=incoords_add_ext,
|
|
1649
|
+
u0=getattr(self.source_org, "u0", None),
|
|
1650
|
+
coords_u=incoords_u_ext,
|
|
1651
|
+
v0=getattr(self.source_org, "v0", None),
|
|
1652
|
+
coords_v=incoords_v_ext,
|
|
1653
|
+
w0=getattr(self.source_org, "w0", None),
|
|
1654
|
+
coords_w=incoords_w_ext,
|
|
1603
1655
|
)
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
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
|
+
)
|
|
1612
1672
|
logger.debug("Extended source and sensor for PML built successfully.")
|
|
1613
1673
|
|
|
1614
1674
|
logger.debug("Localizing PML region...")
|
|
@@ -642,6 +642,8 @@ class Solver:
|
|
|
642
642
|
def _reshape_sensor_data(
|
|
643
643
|
raw_sensor_output: NDArray[np.float64],
|
|
644
644
|
sensor: fullwave.Sensor,
|
|
645
|
+
*,
|
|
646
|
+
n_t: int | None = None,
|
|
645
647
|
) -> NDArray[np.float64]:
|
|
646
648
|
"""Reshape the raw sensor output data.
|
|
647
649
|
|
|
@@ -651,12 +653,21 @@ class Solver:
|
|
|
651
653
|
The raw sensor output data from the simulation. [nt*ncoordsout, 1]
|
|
652
654
|
sensor: fullwave.Sensor
|
|
653
655
|
The sensor object used in the simulation.
|
|
656
|
+
n_t: int | None
|
|
657
|
+
Number of time steps in the extended grid. Required for sparse-grid
|
|
658
|
+
sensors because n_sensors is not known at Python time.
|
|
654
659
|
|
|
655
660
|
Returns
|
|
656
661
|
-------
|
|
657
662
|
NDArray[np.float64]: The reshaped sensor output data. [ncoordsout, nt]
|
|
658
663
|
|
|
659
664
|
"""
|
|
665
|
+
if sensor.is_sparse_grid:
|
|
666
|
+
if n_t is None:
|
|
667
|
+
msg = "n_t is required to reshape sparse-grid sensor output"
|
|
668
|
+
raise ValueError(msg)
|
|
669
|
+
n_t_recorded = -(-n_t // sensor.sampling_modulus_time) # ceiling division
|
|
670
|
+
return raw_sensor_output.reshape(n_t_recorded, -1).T
|
|
660
671
|
return raw_sensor_output.reshape(-1, sensor.n_sensors).T
|
|
661
672
|
|
|
662
673
|
def run(
|
|
@@ -783,30 +794,27 @@ class Solver:
|
|
|
783
794
|
)
|
|
784
795
|
logger.warning(warning_msg)
|
|
785
796
|
|
|
786
|
-
sensor_mask: NDArray[np.bool_]
|
|
787
797
|
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
|
|
798
|
+
mod_x = 1
|
|
799
|
+
mod_y = 1
|
|
800
|
+
mod_z = 1 if self.is_3d else None
|
|
801
|
+
|
|
803
802
|
sensor = fullwave.Sensor(
|
|
804
|
-
|
|
803
|
+
mod_x=mod_x,
|
|
804
|
+
mod_y=mod_y,
|
|
805
|
+
mod_z=mod_z,
|
|
805
806
|
sampling_modulus_time=sampling_modulus_time_whole_domain,
|
|
806
807
|
)
|
|
807
808
|
else:
|
|
808
809
|
sensor = self.pml_builder.extended_sensor
|
|
809
810
|
|
|
811
|
+
# pml_thickness = PML + transition layers on each side, excluding ghost cells.
|
|
812
|
+
# Used by the binary to locate the interior domain when building a sparse sensor grid.
|
|
813
|
+
if record_whole_domain:
|
|
814
|
+
pml_thickness = 0
|
|
815
|
+
else:
|
|
816
|
+
pml_thickness = self.pml_builder.num_boundary_points - self.pml_builder.m_spatial_order
|
|
817
|
+
|
|
810
818
|
start_input_file_writer_time = time.time()
|
|
811
819
|
input_file_writer = InputFileWriter(
|
|
812
820
|
work_dir=self.work_dir,
|
|
@@ -818,6 +826,7 @@ class Solver:
|
|
|
818
826
|
use_exponential_attenuation=self.use_exponential_attenuation,
|
|
819
827
|
use_isotropic_relaxation=self.use_isotropic_relaxation,
|
|
820
828
|
release_after_write=release_after_write,
|
|
829
|
+
pml_thickness=pml_thickness,
|
|
821
830
|
)
|
|
822
831
|
simulation_dir = input_file_writer.run(
|
|
823
832
|
simulation_dir_name,
|
|
@@ -850,6 +859,7 @@ class Solver:
|
|
|
850
859
|
result = self._reshape_sensor_data(
|
|
851
860
|
sim_result,
|
|
852
861
|
sensor=sensor,
|
|
862
|
+
n_t=self.pml_builder.extended_grid.nt,
|
|
853
863
|
)
|
|
854
864
|
end_loading_time = time.time()
|
|
855
865
|
message = (
|
|
@@ -23,6 +23,12 @@ class Source:
|
|
|
23
23
|
grid_shape: tuple[int, ...]
|
|
24
24
|
p0_additive: NDArray[np.float64] | None = None
|
|
25
25
|
incoords_add: NDArray[np.int64] | None = None
|
|
26
|
+
u0: NDArray[np.float64] | None = None
|
|
27
|
+
incoords_u: NDArray[np.int64] | None = None
|
|
28
|
+
v0: NDArray[np.float64] | None = None
|
|
29
|
+
incoords_v: NDArray[np.int64] | None = None
|
|
30
|
+
w0: NDArray[np.float64] | None = None
|
|
31
|
+
incoords_w: NDArray[np.int64] | None = None
|
|
26
32
|
|
|
27
33
|
def __init__( # noqa: C901 PLR0912 PLR0915
|
|
28
34
|
self,
|
|
@@ -37,6 +43,16 @@ class Source:
|
|
|
37
43
|
coords_additive: NDArray[np.int64] | None = None,
|
|
38
44
|
# ---
|
|
39
45
|
grid_shape: tuple[int, ...] | None = None,
|
|
46
|
+
# --- velocity source components ---
|
|
47
|
+
u0: NDArray[np.float64] | None = None,
|
|
48
|
+
coords_u: NDArray[np.int64] | None = None,
|
|
49
|
+
mask_u: NDArray[np.bool] | None = None,
|
|
50
|
+
v0: NDArray[np.float64] | None = None,
|
|
51
|
+
coords_v: NDArray[np.int64] | None = None,
|
|
52
|
+
mask_v: NDArray[np.bool] | None = None,
|
|
53
|
+
w0: NDArray[np.float64] | None = None,
|
|
54
|
+
coords_w: NDArray[np.int64] | None = None,
|
|
55
|
+
mask_w: NDArray[np.bool] | None = None,
|
|
40
56
|
) -> None:
|
|
41
57
|
"""Source class for Fullwave.
|
|
42
58
|
|
|
@@ -44,8 +60,9 @@ class Source:
|
|
|
44
60
|
----------
|
|
45
61
|
p0 : NDArray[np.float64] | None
|
|
46
62
|
Time-varying pressure at each source position; shape [n_sources, nt].
|
|
47
|
-
If None, p0_additive
|
|
48
|
-
|
|
63
|
+
If None, either p0_additive or a velocity component (u0/v0/w0) must be provided.
|
|
64
|
+
When omitted with a velocity-only source, the pressure node list is empty
|
|
65
|
+
and icmat is written as a zero-row matrix.
|
|
49
66
|
mask : NDArray[np.bool] | None
|
|
50
67
|
binary matrix specifying the positions of the time varying pressure source distribution
|
|
51
68
|
shape: [nx, ny] for 2D, [nx, ny, nz] for 3D.
|
|
@@ -54,7 +71,7 @@ class Source:
|
|
|
54
71
|
Coordinate array of source positions (hard source); shape [n_sources, ndim].
|
|
55
72
|
Must be provided together with grid_shape.
|
|
56
73
|
grid_shape : tuple[int, ...] | None
|
|
57
|
-
Shape of the computational grid. Required when using coords input.
|
|
74
|
+
Shape of the computational grid. Required when using coords or velocity-only input.
|
|
58
75
|
p0_additive : NDArray[np.float64] | None
|
|
59
76
|
Optional additive (soft) source term; shape [n_sources_add, nt].
|
|
60
77
|
When provided, positions come from coords_additive or mask_additive,
|
|
@@ -66,15 +83,36 @@ class Source:
|
|
|
66
83
|
Coordinates for the additive source; shape [n_sources_add, ndim].
|
|
67
84
|
If None and p0_additive is provided, defaults to primary incoords.
|
|
68
85
|
Mutually exclusive with mask_additive.
|
|
86
|
+
u0 : NDArray[np.float64] | None
|
|
87
|
+
Time-varying velocity in the depth (x) direction; shape [n_sources_u, nt].
|
|
88
|
+
coords_u : NDArray[np.int64] | None
|
|
89
|
+
Coordinates for the u-velocity source; shape [n_sources_u, ndim].
|
|
90
|
+
Mutually exclusive with mask_u.
|
|
91
|
+
mask_u : NDArray[np.bool] | None
|
|
92
|
+
Boolean mask for u-velocity source positions. Mutually exclusive with coords_u.
|
|
93
|
+
v0 : NDArray[np.float64] | None
|
|
94
|
+
Time-varying velocity in the lateral (y) direction; shape [n_sources_v, nt].
|
|
95
|
+
coords_v : NDArray[np.int64] | None
|
|
96
|
+
Coordinates for the v-velocity source; shape [n_sources_v, ndim].
|
|
97
|
+
Mutually exclusive with mask_v.
|
|
98
|
+
mask_v : NDArray[np.bool] | None
|
|
99
|
+
Boolean mask for v-velocity source positions. Mutually exclusive with coords_v.
|
|
100
|
+
w0 : NDArray[np.float64] | None
|
|
101
|
+
Time-varying velocity in the elevational (z) direction; shape [n_sources_w, nt].
|
|
102
|
+
coords_w : NDArray[np.int64] | None
|
|
103
|
+
Coordinates for the w-velocity source; shape [n_sources_w, ndim].
|
|
104
|
+
Mutually exclusive with mask_w.
|
|
105
|
+
mask_w : NDArray[np.bool] | None
|
|
106
|
+
Boolean mask for w-velocity source positions. Mutually exclusive with coords_w.
|
|
69
107
|
|
|
70
108
|
Raises
|
|
71
109
|
------
|
|
72
110
|
ValueError
|
|
73
|
-
If grid_shape is missing when using coords or
|
|
111
|
+
If grid_shape is missing when using coords, additive-only, or velocity-only.
|
|
74
112
|
If both mask and coords, or both coords_additive and mask_additive, are provided.
|
|
75
|
-
If primary source positions cannot be resolved (need coords+grid_shape,
|
|
76
|
-
|
|
77
|
-
If
|
|
113
|
+
If primary source positions cannot be resolved (need coords+grid_shape, mask,
|
|
114
|
+
additive-only args, or at least one velocity component with grid_shape).
|
|
115
|
+
If all of p0, p0_additive, u0, v0, and w0 are None.
|
|
78
116
|
If p0 / p0_additive row counts do not match their coordinate arrays.
|
|
79
117
|
|
|
80
118
|
"""
|
|
@@ -110,17 +148,28 @@ class Source:
|
|
|
110
148
|
mask_add = np.atleast_2d(mask_additive)
|
|
111
149
|
self.grid_shape = mask_add.shape
|
|
112
150
|
self.incoords = map_to_coords(mask_add)
|
|
151
|
+
elif u0 is not None or v0 is not None or w0 is not None:
|
|
152
|
+
# Velocity-only: no hard pressure nodes; pressure arrays will be empty
|
|
153
|
+
if grid_shape is None:
|
|
154
|
+
error_msg = "grid_shape is required for velocity-only source"
|
|
155
|
+
raise ValueError(error_msg)
|
|
156
|
+
self.grid_shape = tuple(grid_shape)
|
|
157
|
+
self.incoords = np.empty((0, len(self.grid_shape)), dtype=np.int64)
|
|
113
158
|
else:
|
|
114
159
|
error_msg = (
|
|
115
160
|
"Provide (coords + grid_shape), or mask, or"
|
|
116
|
-
" (p0_additive + (coords_additive or mask_additive))"
|
|
161
|
+
" (p0_additive + (coords_additive or mask_additive)), or"
|
|
162
|
+
" a velocity component (u0/v0/w0) with grid_shape"
|
|
117
163
|
)
|
|
118
164
|
raise ValueError(
|
|
119
165
|
error_msg,
|
|
120
166
|
)
|
|
121
167
|
|
|
122
|
-
if p0 is None and p0_additive is None:
|
|
123
|
-
error_msg =
|
|
168
|
+
if p0 is None and p0_additive is None and u0 is None and v0 is None and w0 is None:
|
|
169
|
+
error_msg = (
|
|
170
|
+
"At least one of p0, p0_additive, or a velocity component"
|
|
171
|
+
" (u0/v0/w0) must be provided"
|
|
172
|
+
)
|
|
124
173
|
raise ValueError(error_msg)
|
|
125
174
|
|
|
126
175
|
# --- Resolve additive coords (for later use) ---
|
|
@@ -148,7 +197,7 @@ class Source:
|
|
|
148
197
|
)
|
|
149
198
|
raise ValueError(error_msg)
|
|
150
199
|
self.p0_additive = np.atleast_2d(p0_additive) if p0_additive is not None else None
|
|
151
|
-
|
|
200
|
+
elif p0_additive is not None:
|
|
152
201
|
# Additive-only: icmat written as zeros
|
|
153
202
|
p0_add = np.atleast_2d(p0_additive)
|
|
154
203
|
n_add = _coords_add.shape[0] if _coords_add is not None else self.incoords.shape[0]
|
|
@@ -166,6 +215,12 @@ class Source:
|
|
|
166
215
|
dtype=np.float64,
|
|
167
216
|
)
|
|
168
217
|
self.p0_additive = p0_add
|
|
218
|
+
else:
|
|
219
|
+
# Velocity-only: no pressure nodes; derive nt from the first velocity signal
|
|
220
|
+
_first_vel = next(s for s in (u0, v0, w0) if s is not None)
|
|
221
|
+
nt_vel = np.atleast_2d(_first_vel).shape[1]
|
|
222
|
+
self.p0 = np.zeros((0, nt_vel), dtype=np.float64)
|
|
223
|
+
self.p0_additive = None
|
|
169
224
|
|
|
170
225
|
# --- Set incoords_add for writer/binary ---
|
|
171
226
|
if self.p0_additive is not None:
|
|
@@ -173,6 +228,11 @@ class Source:
|
|
|
173
228
|
else:
|
|
174
229
|
self.incoords_add = None
|
|
175
230
|
|
|
231
|
+
# --- Resolve velocity source components ---
|
|
232
|
+
self.incoords_u, self.u0 = self._resolve_velocity_component("u", u0, coords_u, mask_u)
|
|
233
|
+
self.incoords_v, self.v0 = self._resolve_velocity_component("v", v0, coords_v, mask_v)
|
|
234
|
+
self.incoords_w, self.w0 = self._resolve_velocity_component("w", w0, coords_w, mask_w)
|
|
235
|
+
|
|
176
236
|
self.is_3d = len(self.grid_shape) == 3
|
|
177
237
|
super().__init__()
|
|
178
238
|
self.__post_init__()
|
|
@@ -202,11 +262,64 @@ class Source:
|
|
|
202
262
|
)
|
|
203
263
|
raise ValueError(error_msg)
|
|
204
264
|
|
|
265
|
+
@staticmethod
|
|
266
|
+
def _resolve_velocity_component(
|
|
267
|
+
name: str,
|
|
268
|
+
signal: NDArray | None,
|
|
269
|
+
coords: NDArray | None,
|
|
270
|
+
mask: NDArray | None,
|
|
271
|
+
) -> tuple[NDArray | None, NDArray | None]:
|
|
272
|
+
"""Resolve a velocity source component to (incoords, signal_arr).
|
|
273
|
+
|
|
274
|
+
Returns
|
|
275
|
+
-------
|
|
276
|
+
tuple[NDArray | None, NDArray | None]
|
|
277
|
+
(incoords, signal_arr) if the component is provided, else (None, None).
|
|
278
|
+
|
|
279
|
+
Raises
|
|
280
|
+
------
|
|
281
|
+
ValueError
|
|
282
|
+
If coords and mask are both provided.
|
|
283
|
+
If signal is provided but neither coords nor mask are given.
|
|
284
|
+
If signal is None but coords/mask are provided.
|
|
285
|
+
If signal row count does not match the number of coordinate points.
|
|
286
|
+
|
|
287
|
+
"""
|
|
288
|
+
if signal is None:
|
|
289
|
+
if coords is not None or mask is not None:
|
|
290
|
+
error_msg = f"coords_{name}/mask_{name} provided without {name}0 signal"
|
|
291
|
+
raise ValueError(error_msg)
|
|
292
|
+
return None, None
|
|
293
|
+
if coords is not None and mask is not None:
|
|
294
|
+
error_msg = f"coords_{name} and mask_{name} are mutually exclusive"
|
|
295
|
+
raise ValueError(error_msg)
|
|
296
|
+
if coords is None and mask is None:
|
|
297
|
+
error_msg = f"{name}0 signal provided but neither coords_{name} nor mask_{name} given"
|
|
298
|
+
raise ValueError(error_msg)
|
|
299
|
+
if coords is not None:
|
|
300
|
+
incoords = np.atleast_2d(coords).astype(np.int64, copy=False)
|
|
301
|
+
else:
|
|
302
|
+
incoords = map_to_coords(np.atleast_2d(mask))
|
|
303
|
+
signal_arr = np.atleast_2d(signal)
|
|
304
|
+
if signal_arr.shape[0] != incoords.shape[0]:
|
|
305
|
+
error_msg = (
|
|
306
|
+
f"{name}0 has {signal_arr.shape[0]} rows but "
|
|
307
|
+
f"coords_{name}/mask_{name} has {incoords.shape[0]} (must match)"
|
|
308
|
+
)
|
|
309
|
+
raise ValueError(error_msg)
|
|
310
|
+
return incoords, signal_arr
|
|
311
|
+
|
|
205
312
|
def validate(self, grid_shape: NDArray[np.int64] | tuple) -> None:
|
|
206
313
|
"""Check if the source coordinates are consistent with the grid shape."""
|
|
207
314
|
grid_shape = tuple(grid_shape) if isinstance(grid_shape, np.ndarray) else grid_shape
|
|
208
315
|
assert self.grid_shape == grid_shape, f"{self.grid_shape} != {grid_shape}"
|
|
209
|
-
assert
|
|
316
|
+
assert (
|
|
317
|
+
self.n_sources > 0
|
|
318
|
+
or self.n_sources_add > 0
|
|
319
|
+
or self.n_sources_u > 0
|
|
320
|
+
or self.n_sources_v > 0
|
|
321
|
+
or self.n_sources_w > 0
|
|
322
|
+
), "No active source found."
|
|
210
323
|
logger.debug("Source validated against grid shape.")
|
|
211
324
|
|
|
212
325
|
@property
|
|
@@ -246,6 +359,21 @@ class Source:
|
|
|
246
359
|
return 0
|
|
247
360
|
return self.incoords_add.shape[0]
|
|
248
361
|
|
|
362
|
+
@property
|
|
363
|
+
def n_sources_u(self) -> int:
|
|
364
|
+
"""Return the number of velocity-u (depth) source positions."""
|
|
365
|
+
return self.incoords_u.shape[0] if self.incoords_u is not None else 0
|
|
366
|
+
|
|
367
|
+
@property
|
|
368
|
+
def n_sources_v(self) -> int:
|
|
369
|
+
"""Return the number of velocity-v (lateral) source positions."""
|
|
370
|
+
return self.incoords_v.shape[0] if self.incoords_v is not None else 0
|
|
371
|
+
|
|
372
|
+
@property
|
|
373
|
+
def n_sources_w(self) -> int:
|
|
374
|
+
"""Return the number of velocity-w (elevational) source positions."""
|
|
375
|
+
return self.incoords_w.shape[0] if self.incoords_w is not None else 0
|
|
376
|
+
|
|
249
377
|
def plot(
|
|
250
378
|
self,
|
|
251
379
|
export_path: Path | str | None = Path("./temp/temp.png"),
|
|
@@ -312,6 +440,13 @@ class Source:
|
|
|
312
440
|
lines.append(f" p0_additive shape: {self.p0_additive.shape}")
|
|
313
441
|
if self.incoords_add is not None:
|
|
314
442
|
lines.append(f" incoords_add: {self.incoords_add.shape[0]} points")
|
|
443
|
+
for comp, sig, n in (
|
|
444
|
+
("u", self.u0, self.n_sources_u),
|
|
445
|
+
("v", self.v0, self.n_sources_v),
|
|
446
|
+
("w", self.w0, self.n_sources_w),
|
|
447
|
+
):
|
|
448
|
+
if sig is not None:
|
|
449
|
+
lines.append(f" {comp}0 shape: {sig.shape} ({n} points)")
|
|
315
450
|
return "\n".join(lines) + "\n"
|
|
316
451
|
|
|
317
452
|
def __repr__(self) -> str:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "fullwave25"
|
|
3
|
-
version = "1.2.
|
|
3
|
+
version = "1.2.6-dev1" # 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
|
|
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.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/domain_background.py
RENAMED
|
File without changes
|
{fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/domain_scatterer.py
RENAMED
|
File without changes
|
{fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/domain_simple.py
RENAMED
|
File without changes
|
{fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev1}/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
|