fullwave25 1.2.6.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.
Files changed (70) hide show
  1. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/.bumpversion.toml +1 -1
  2. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/PKG-INFO +1 -1
  3. fullwave25-1.2.6.dev1/debug_solver_bin/fullwave2_2d_exponential_attenuation_multi_gpu_cuda124 +0 -0
  4. fullwave25-1.2.6.dev1/debug_solver_bin/fullwave2_3d_exponential_attenuation_multi_gpu_cuda124 +0 -0
  5. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/__init__.py +1 -1
  6. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/sensor.py +70 -6
  7. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/binary_manager.py +1 -1
  8. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/input_file_writer.py +29 -0
  9. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/pml_builder.py +34 -16
  10. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/solver.py +27 -17
  11. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/pyproject.toml +1 -1
  12. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/uv.lock +1 -1
  13. fullwave25-1.2.6.dev0/debug_solver_bin/fullwave2_2d_2_relax_multi_gpu_cuda124 +0 -0
  14. fullwave25-1.2.6.dev0/debug_solver_bin/fullwave2_2d_exponential_attenuation_multi_gpu +0 -0
  15. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/.gitignore +0 -0
  16. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/.pre-commit-config.yaml +0 -0
  17. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/.python-version +0 -0
  18. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/20260120_0844.log +0 -0
  19. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/CITATION.cff +0 -0
  20. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/CLAUDE.md +0 -0
  21. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/CONTRIBUTING.md +0 -0
  22. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/LICENSE +0 -0
  23. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/Makefile +0 -0
  24. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/README.md +0 -0
  25. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/beamformer.m +0 -0
  26. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/experiments/__init__.py +0 -0
  27. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/experiments/exp_scatterer_diameter.py +0 -0
  28. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/beamformer/__init__.py +0 -0
  29. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/beamformer/beamformer.py +0 -0
  30. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/constants/__init__.py +0 -0
  31. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/constants/material_properties.py +0 -0
  32. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/grid.py +0 -0
  33. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium.py +0 -0
  34. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/__init__.py +0 -0
  35. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/domain.py +0 -0
  36. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/medium_builder.py +0 -0
  37. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/__init__.py +0 -0
  38. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/data/.keep +0 -0
  39. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/data/abdominal_wall/i2365f_etfw1.mat +0 -0
  40. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/domain_abdominal_wall.py +0 -0
  41. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/domain_background.py +0 -0
  42. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/domain_scatterer.py +0 -0
  43. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/domain_simple.py +0 -0
  44. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/medium_builder/presets/domain_water_gel.py +0 -0
  45. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/__init__.py +0 -0
  46. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda118 +0 -0
  47. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda124 +0 -0
  48. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda129 +0 -0
  49. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda130 +0 -0
  50. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda118 +0 -0
  51. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda124 +0 -0
  52. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda129 +0 -0
  53. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda130 +0 -0
  54. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/bins/database/relaxation_params_database_num_relax=2_20260113_0957.mat +0 -0
  55. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/cuda_utils.py +0 -0
  56. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/launcher.py +0 -0
  57. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/solver/utils.py +0 -0
  58. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/source.py +0 -0
  59. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/transducer.py +0 -0
  60. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/__init__.py +0 -0
  61. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/check_functions.py +0 -0
  62. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/coordinates.py +0 -0
  63. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/memory_tempfile.py +0 -0
  64. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/numerical.py +0 -0
  65. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/plot_utils.py +0 -0
  66. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/pulse.py +0 -0
  67. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/relaxation_parameters.py +0 -0
  68. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/scatterer.py +0 -0
  69. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/fullwave/utils/signal_process.py +0 -0
  70. {fullwave25-1.2.6.dev0 → fullwave25-1.2.6.dev1}/ruff.toml +0 -0
@@ -1,5 +1,5 @@
1
1
  [tool.bumpversion]
2
- current_version = "1.2.6-dev0"
2
+ current_version = "1.2.6-dev1"
3
3
  parse = """(?x)
4
4
  (?P<major>0|[1-9]\\d*)\\.
5
5
  (?P<minor>0|[1-9]\\d*)\\.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fullwave25
3
- Version: 1.2.6.dev0
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
@@ -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-dev0"
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 neither mask nor coords (with grid_shape) is provided.
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 coords is not None:
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 or coords (with grid_shape) must be provided"
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.2"
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
- extended_sensor_grid_shape = tuple(
357
- s + 2 * self.num_boundary_points for s in self.sensor_org.grid_shape
358
- )
359
- self.extended_sensor = fullwave.Sensor(
360
- coords=self.sensor_org.outcoords + self.num_boundary_points,
361
- grid_shape=extended_sensor_grid_shape,
362
- sampling_modulus_time=self.sensor_org.sampling_modulus_time,
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
- extended_sensor_grid_shape = tuple(
1647
- s + 2 * self.num_boundary_points for s in self.sensor_org.grid_shape
1648
- )
1649
- self.extended_sensor = fullwave.Sensor(
1650
- coords=self.sensor_org.outcoords + self.num_boundary_points,
1651
- grid_shape=extended_sensor_grid_shape,
1652
- sampling_modulus_time=self.sensor_org.sampling_modulus_time,
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...")
@@ -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
- if self.is_3d:
789
- sensor_mask = np.zeros(
790
- (
791
- self.pml_builder.extended_grid.nx,
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
- mask=sensor_mask,
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 = (
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fullwave25"
3
- version = "1.2.6-dev0" # Update via bump-my-version, not manually
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"
@@ -735,7 +735,7 @@ wheels = [
735
735
 
736
736
  [[package]]
737
737
  name = "fullwave25"
738
- version = "1.2.6.dev0"
738
+ version = "1.2.6.dev1"
739
739
  source = { editable = "." }
740
740
  dependencies = [
741
741
  { name = "joblib" },
File without changes