fullwave25 1.2.5.dev0__tar.gz → 1.2.6.dev0__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 (68) hide show
  1. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/.bumpversion.toml +1 -1
  2. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/PKG-INFO +1 -1
  3. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/__init__.py +1 -1
  4. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/solver/binary_manager.py +1 -1
  5. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/solver/input_file_writer.py +17 -0
  6. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/solver/pml_builder.py +43 -1
  7. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/source.py +147 -12
  8. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/pyproject.toml +1 -1
  9. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/uv.lock +1 -1
  10. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/.gitignore +0 -0
  11. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/.pre-commit-config.yaml +0 -0
  12. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/.python-version +0 -0
  13. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/20260120_0844.log +0 -0
  14. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/CITATION.cff +0 -0
  15. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/CLAUDE.md +0 -0
  16. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/CONTRIBUTING.md +0 -0
  17. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/LICENSE +0 -0
  18. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/Makefile +0 -0
  19. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/README.md +0 -0
  20. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/beamformer.m +0 -0
  21. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/debug_solver_bin/fullwave2_2d_2_relax_multi_gpu_cuda124 +0 -0
  22. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/debug_solver_bin/fullwave2_2d_exponential_attenuation_multi_gpu +0 -0
  23. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/experiments/__init__.py +0 -0
  24. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/experiments/exp_scatterer_diameter.py +0 -0
  25. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/beamformer/__init__.py +0 -0
  26. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/beamformer/beamformer.py +0 -0
  27. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/constants/__init__.py +0 -0
  28. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/constants/material_properties.py +0 -0
  29. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/grid.py +0 -0
  30. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/medium.py +0 -0
  31. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/medium_builder/__init__.py +0 -0
  32. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/medium_builder/domain.py +0 -0
  33. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/medium_builder/medium_builder.py +0 -0
  34. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/medium_builder/presets/__init__.py +0 -0
  35. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/medium_builder/presets/data/.keep +0 -0
  36. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/medium_builder/presets/data/abdominal_wall/i2365f_etfw1.mat +0 -0
  37. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/medium_builder/presets/domain_abdominal_wall.py +0 -0
  38. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/medium_builder/presets/domain_background.py +0 -0
  39. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/medium_builder/presets/domain_scatterer.py +0 -0
  40. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/medium_builder/presets/domain_simple.py +0 -0
  41. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/medium_builder/presets/domain_water_gel.py +0 -0
  42. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/sensor.py +0 -0
  43. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/solver/__init__.py +0 -0
  44. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda118 +0 -0
  45. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda124 +0 -0
  46. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda129 +0 -0
  47. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda130 +0 -0
  48. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda118 +0 -0
  49. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda124 +0 -0
  50. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda129 +0 -0
  51. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda130 +0 -0
  52. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/solver/bins/database/relaxation_params_database_num_relax=2_20260113_0957.mat +0 -0
  53. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/solver/cuda_utils.py +0 -0
  54. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/solver/launcher.py +0 -0
  55. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/solver/solver.py +0 -0
  56. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/solver/utils.py +0 -0
  57. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/transducer.py +0 -0
  58. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/utils/__init__.py +0 -0
  59. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/utils/check_functions.py +0 -0
  60. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/utils/coordinates.py +0 -0
  61. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/utils/memory_tempfile.py +0 -0
  62. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/utils/numerical.py +0 -0
  63. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/utils/plot_utils.py +0 -0
  64. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/utils/pulse.py +0 -0
  65. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/utils/relaxation_parameters.py +0 -0
  66. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/utils/scatterer.py +0 -0
  67. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/fullwave/utils/signal_process.py +0 -0
  68. {fullwave25-1.2.5.dev0 → fullwave25-1.2.6.dev0}/ruff.toml +0 -0
@@ -1,5 +1,5 @@
1
1
  [tool.bumpversion]
2
- current_version = "1.2.5-dev0"
2
+ current_version = "1.2.6-dev0"
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.5.dev0
3
+ Version: 1.2.6.dev0
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.5-dev0"
63
+ __version__ = "1.2.6-dev0"
64
64
 
65
65
  VERSION = __version__ # for convenience
66
66
  logger.info("Fullwave version: %s", __version__)
@@ -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.1"
31
+ BINARY_RELEASE_TAG = "fullwave_bin_v1.2"
32
32
 
33
33
 
34
34
  def _download_url(filename: str, tag: str) -> str:
@@ -169,6 +169,19 @@ class InputFileWriter:
169
169
  )
170
170
  if incoords_add is not None:
171
171
  self._queue_coords_write(simulation_dir / "icc_add.dat", incoords_add)
172
+ for _vel_suffix, _vel_attr in (("u", "u0"), ("v", "v0"), ("w", "w0")):
173
+ _signal_vel = getattr(self.source, _vel_attr, None)
174
+ _incoords_vel = getattr(self.source, f"incoords_{_vel_suffix}", None)
175
+ if _signal_vel is not None:
176
+ self._queue_ic_write(
177
+ simulation_dir / f"icmat_{_vel_suffix}.dat",
178
+ np.transpose(_signal_vel),
179
+ )
180
+ if _incoords_vel is not None:
181
+ self._queue_coords_write(
182
+ simulation_dir / f"icc_{_vel_suffix}.dat",
183
+ _incoords_vel,
184
+ )
172
185
  self._queue_coords_write(simulation_dir / "icc.dat", self.source.incoords)
173
186
  self._copy_simulation_bin_file(simulation_dir)
174
187
 
@@ -1047,6 +1060,10 @@ class InputFileWriter:
1047
1060
  n_sources_add = getattr(self.source, "n_sources_add", 0)
1048
1061
  if n_sources_add > 0:
1049
1062
  var_list.append(("ncoords_add", n_sources_add))
1063
+ for _vel_suffix in ("u", "v", "w"):
1064
+ _n_vel = getattr(self.source, f"n_sources_{_vel_suffix}", 0)
1065
+ if _n_vel > 0:
1066
+ var_list.append((f"ncoords_{_vel_suffix}", _n_vel))
1050
1067
  if self.is_3d:
1051
1068
  var_list.extend(
1052
1069
  [
@@ -322,12 +322,33 @@ 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
 
@@ -1434,7 +1455,7 @@ class PMLBuilder:
1434
1455
  class PMLBuilderExponentialAttenuation(PMLBuilder):
1435
1456
  """A class to set up PML for exponential attenuation media."""
1436
1457
 
1437
- def __init__(
1458
+ def __init__( # noqa: PLR0915
1438
1459
  self,
1439
1460
  grid: fullwave.Grid,
1440
1461
  medium: fullwave.Medium,
@@ -1594,12 +1615,33 @@ class PMLBuilderExponentialAttenuation(PMLBuilder):
1594
1615
  if getattr(self.source_org, "incoords_add", None) is not None
1595
1616
  else None
1596
1617
  )
1618
+ incoords_u_ext = (
1619
+ self.source_org.incoords_u + self.num_boundary_points
1620
+ if getattr(self.source_org, "incoords_u", None) is not None
1621
+ else None
1622
+ )
1623
+ incoords_v_ext = (
1624
+ self.source_org.incoords_v + self.num_boundary_points
1625
+ if getattr(self.source_org, "incoords_v", None) is not None
1626
+ else None
1627
+ )
1628
+ incoords_w_ext = (
1629
+ self.source_org.incoords_w + self.num_boundary_points
1630
+ if getattr(self.source_org, "incoords_w", None) is not None
1631
+ else None
1632
+ )
1597
1633
  self.extended_source = fullwave.Source(
1598
1634
  p0=self.source_org.p0,
1599
1635
  coords=self.source_org.incoords + self.num_boundary_points,
1600
1636
  grid_shape=extended_grid_shape,
1601
1637
  p0_additive=self.source_org.p0_additive,
1602
1638
  coords_additive=incoords_add_ext,
1639
+ u0=getattr(self.source_org, "u0", None),
1640
+ coords_u=incoords_u_ext,
1641
+ v0=getattr(self.source_org, "v0", None),
1642
+ coords_v=incoords_v_ext,
1643
+ w0=getattr(self.source_org, "w0", None),
1644
+ coords_w=incoords_w_ext,
1603
1645
  )
1604
1646
  extended_sensor_grid_shape = tuple(
1605
1647
  s + 2 * self.num_boundary_points for s in self.sensor_org.grid_shape
@@ -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 must be provided (additive-only / soft initial condition):
48
- icmat is written as zeros and only the additive term drives the source.
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 additive-only.
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
- or mask, or additive-only args).
77
- If both p0 and p0_additive are None.
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 = "At least one of p0 or p0_additive must be provided"
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
- else:
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 self.n_sources > 0 or self.n_sources_add > 0, "No active source found."
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.5-dev0" # Update via bump-my-version, not manually
3
+ version = "1.2.6-dev0" # 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.5.dev0"
738
+ version = "1.2.6.dev0"
739
739
  source = { editable = "." }
740
740
  dependencies = [
741
741
  { name = "joblib" },
File without changes