fullwave25 1.2.3.dev1__tar.gz → 1.2.3rc1__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 (67) hide show
  1. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/.bumpversion.toml +1 -1
  2. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/PKG-INFO +1 -1
  3. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/__init__.py +1 -1
  4. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/grid.py +3 -3
  5. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/solver/binary_manager.py +1 -1
  6. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/solver/input_file_writer.py +12 -0
  7. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/solver/launcher.py +20 -1
  8. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/solver/pml_builder.py +14 -0
  9. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/solver/solver.py +14 -0
  10. fullwave25-1.2.3rc1/fullwave/source.py +326 -0
  11. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/pyproject.toml +1 -1
  12. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/uv.lock +1 -1
  13. fullwave25-1.2.3.dev1/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda118 +0 -0
  14. fullwave25-1.2.3.dev1/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda124 +0 -0
  15. fullwave25-1.2.3.dev1/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda129 +0 -0
  16. fullwave25-1.2.3.dev1/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda130 +0 -0
  17. fullwave25-1.2.3.dev1/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda118 +0 -0
  18. fullwave25-1.2.3.dev1/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda124 +0 -0
  19. fullwave25-1.2.3.dev1/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda129 +0 -0
  20. fullwave25-1.2.3.dev1/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda130 +0 -0
  21. fullwave25-1.2.3.dev1/fullwave/source.py +0 -206
  22. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/.gitignore +0 -0
  23. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/.pre-commit-config.yaml +0 -0
  24. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/.python-version +0 -0
  25. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/20260120_0844.log +0 -0
  26. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/CITATION.cff +0 -0
  27. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/CLAUDE.md +0 -0
  28. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/CONTRIBUTING.md +0 -0
  29. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/LICENSE +0 -0
  30. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/Makefile +0 -0
  31. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/README.md +0 -0
  32. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/beamformer.m +0 -0
  33. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/experiments/__init__.py +0 -0
  34. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/experiments/exp_scatterer_diameter.py +0 -0
  35. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/beamformer/__init__.py +0 -0
  36. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/beamformer/beamformer.py +0 -0
  37. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/constants/__init__.py +0 -0
  38. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/constants/material_properties.py +0 -0
  39. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/medium.py +0 -0
  40. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/medium_builder/__init__.py +0 -0
  41. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/medium_builder/domain.py +0 -0
  42. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/medium_builder/medium_builder.py +0 -0
  43. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/medium_builder/presets/__init__.py +0 -0
  44. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/medium_builder/presets/data/.keep +0 -0
  45. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/medium_builder/presets/data/abdominal_wall/i2365f_etfw1.mat +0 -0
  46. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/medium_builder/presets/domain_abdominal_wall.py +0 -0
  47. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/medium_builder/presets/domain_background.py +0 -0
  48. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/medium_builder/presets/domain_scatterer.py +0 -0
  49. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/medium_builder/presets/domain_simple.py +0 -0
  50. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/medium_builder/presets/domain_water_gel.py +0 -0
  51. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/sensor.py +0 -0
  52. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/solver/__init__.py +0 -0
  53. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/solver/bins/database/relaxation_params_database_num_relax=2_20260113_0957.mat +0 -0
  54. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/solver/cuda_utils.py +0 -0
  55. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/solver/utils.py +0 -0
  56. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/transducer.py +0 -0
  57. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/utils/__init__.py +0 -0
  58. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/utils/check_functions.py +0 -0
  59. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/utils/coordinates.py +0 -0
  60. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/utils/memory_tempfile.py +0 -0
  61. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/utils/numerical.py +0 -0
  62. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/utils/plot_utils.py +0 -0
  63. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/utils/pulse.py +0 -0
  64. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/utils/relaxation_parameters.py +0 -0
  65. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/utils/scatterer.py +0 -0
  66. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/fullwave/utils/signal_process.py +0 -0
  67. {fullwave25-1.2.3.dev1 → fullwave25-1.2.3rc1}/ruff.toml +0 -0
@@ -1,5 +1,5 @@
1
1
  [tool.bumpversion]
2
- current_version = "1.2.3-dev1"
2
+ current_version = "1.2.3-rc1"
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.3.dev1
3
+ Version: 1.2.3rc1
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.3-dev1"
63
+ __version__ = "1.2.3-rc1"
64
64
 
65
65
  VERSION = __version__ # for convenience
66
66
  logger.info("Fullwave version: %s", __version__)
@@ -183,9 +183,9 @@ class Grid:
183
183
  else " Grid spacing (grid.dx, grid.dy): "
184
184
  )
185
185
  grid_spacing_str = (
186
- f"({self.dx * 1e3:.2e} m, {self.dy * 1e3:.2e} m, {self.dz * 1e3:.2e} m)"
186
+ f"({self.dx * 1e3:.2e} mm, {self.dy * 1e3:.2e} mm, {self.dz * 1e3:.2e} mm)"
187
187
  if self.is_3d
188
- else f"({self.dx * 1e3:.2e} m, {self.dy * 1e3:.2e} m)"
188
+ else f"({self.dx * 1e3:.2e} mm, {self.dy * 1e3:.2e} mm)"
189
189
  )
190
190
 
191
191
  num_grid_points_str_desc = (
@@ -203,7 +203,7 @@ class Grid:
203
203
  f" Speed of sound (grid.c0): {self.c0} m/s\n"
204
204
  f" Points per wavelength (PPW) (grid.ppw): {self.ppw}\n"
205
205
  f" Courant-Friedrichs-Lewy (CFL) number (grid.cfl): {self.cfl}\n"
206
- f" Wavelength (grid.wavelength): {self.wavelength * 1e3:.2e} m\n"
206
+ f" Wavelength (grid.wavelength): {self.wavelength * 1e3:.2e} mm\n"
207
207
  + grid_spacing_str_desc
208
208
  + grid_spacing_str
209
209
  + "\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 = "v1.2.2"
31
+ BINARY_RELEASE_TAG = "fullwave_bin_v1.1"
32
32
 
33
33
 
34
34
  def _download_url(filename: str, tag: str) -> str:
@@ -160,6 +160,15 @@ class InputFileWriter:
160
160
  simulation_dir / "icmat.dat",
161
161
  np.transpose(self.source.icmat),
162
162
  )
163
+ p0_additive = getattr(self.source, "p0_additive", None)
164
+ incoords_add = getattr(self.source, "incoords_add", None)
165
+ if p0_additive is not None:
166
+ self._queue_ic_write(
167
+ simulation_dir / "icmat_add.dat",
168
+ np.transpose(p0_additive),
169
+ )
170
+ if incoords_add is not None:
171
+ self._queue_coords_write(simulation_dir / "icc_add.dat", incoords_add)
163
172
  self._queue_coords_write(simulation_dir / "icc.dat", self.source.incoords)
164
173
  self._copy_simulation_bin_file(simulation_dir)
165
174
 
@@ -1035,6 +1044,9 @@ class InputFileWriter:
1035
1044
  ("nTic", nt_ic),
1036
1045
  ("modT", self.sensor.sampling_modulus_time),
1037
1046
  ]
1047
+ n_sources_add = getattr(self.source, "n_sources_add", 0)
1048
+ if n_sources_add > 0:
1049
+ var_list.append(("ncoords_add", n_sources_add))
1038
1050
  if self.is_3d:
1039
1051
  var_list.extend(
1040
1052
  [
@@ -30,6 +30,7 @@ class Launcher:
30
30
  is_3d: bool = False,
31
31
  use_gpu: bool = True,
32
32
  cuda_device_id: str | int | list | None = None,
33
+ save_gpu_memory: bool = False,
33
34
  ) -> None:
34
35
  """Initialize a FullwaveLauncher instance.
35
36
 
@@ -50,6 +51,17 @@ class Launcher:
50
51
  for multiple GPUs, provide a list of device IDs.
51
52
  example 1: [0, 1] for using GPU 0 and GPU 1. or "0,1" as a string.
52
53
  example 2: 2 for using GPU 2 or "2" as a string.
54
+ save_gpu_memory : bool, optional
55
+ Whether to save GPU memory by using ICMAT_MEMORY_SAVING flag in the simulation.
56
+ The simulation does not load initial conditions into GPU memory and
57
+ it loads the slice of the wavefield needed for the current time step
58
+ from CPU memory at each time step.
59
+ Defaults to False. If True, it may significantly reduce GPU memory usage,
60
+ but it may also increase the simulation time
61
+ due to the overhead of data transfer between CPU and GPU
62
+ depending on the hardware and the simulation settings.
63
+ useful in 3D simulations with large grid sizes
64
+ where GPU memory is a limiting factor.
53
65
 
54
66
  """
55
67
  self._path_fullwave_simulation_bin = path_fullwave_simulation_bin
@@ -58,6 +70,7 @@ class Launcher:
58
70
  self.is_3d = is_3d
59
71
  self.use_gpu = use_gpu
60
72
  self.cuda_device_id = self._configure_cuda_device_id(cuda_device_id)
73
+ self.save_gpu_memory = save_gpu_memory
61
74
  logger.debug("Launcher instance created.")
62
75
 
63
76
  @staticmethod
@@ -203,7 +216,12 @@ class Launcher:
203
216
  logger.info("Running simulation...")
204
217
  with (simulation_dir / "fw2_execution.log").open("w", encoding="utf-8") as file:
205
218
  time_start = time()
206
- os.environ["CUDA_VISIBLE_DEVICES"] = self.cuda_device_id
219
+ env = os.environ.copy()
220
+ env["CUDA_VISIBLE_DEVICES"] = self.cuda_device_id
221
+ if self.save_gpu_memory:
222
+ env["ICMAT_MEMORY_SAVING"] = "1"
223
+ else:
224
+ env["ICMAT_MEMORY_SAVING"] = "0"
207
225
  subprocess.run( # noqa: S603
208
226
  command,
209
227
  check=True,
@@ -211,6 +229,7 @@ class Launcher:
211
229
  stdout=file,
212
230
  stderr=file,
213
231
  text=True,
232
+ env=env,
214
233
  # check=False,
215
234
  )
216
235
  time_passed = time() - time_start
@@ -317,10 +317,17 @@ class PMLBuilder:
317
317
  extended_grid_shape = tuple(
318
318
  s + 2 * self.num_boundary_points for s in self.source_org.grid_shape
319
319
  )
320
+ incoords_add_ext = (
321
+ self.source_org.incoords_add + self.num_boundary_points
322
+ if getattr(self.source_org, "incoords_add", None) is not None
323
+ else None
324
+ )
320
325
  self.extended_source = fullwave.Source(
321
326
  p0=self.source_org.p0,
322
327
  coords=self.source_org.incoords + self.num_boundary_points,
323
328
  grid_shape=extended_grid_shape,
329
+ p0_additive=self.source_org.p0_additive,
330
+ coords_additive=incoords_add_ext,
324
331
  )
325
332
  logger.debug("building extended source for pml...done")
326
333
 
@@ -1582,10 +1589,17 @@ class PMLBuilderExponentialAttenuation(PMLBuilder):
1582
1589
  extended_grid_shape = tuple(
1583
1590
  s + 2 * self.num_boundary_points for s in self.source_org.grid_shape
1584
1591
  )
1592
+ incoords_add_ext = (
1593
+ self.source_org.incoords_add + self.num_boundary_points
1594
+ if getattr(self.source_org, "incoords_add", None) is not None
1595
+ else None
1596
+ )
1585
1597
  self.extended_source = fullwave.Source(
1586
1598
  p0=self.source_org.p0,
1587
1599
  coords=self.source_org.incoords + self.num_boundary_points,
1588
1600
  grid_shape=extended_grid_shape,
1601
+ p0_additive=self.source_org.p0_additive,
1602
+ coords_additive=incoords_add_ext,
1589
1603
  )
1590
1604
  extended_sensor_grid_shape = tuple(
1591
1605
  s + 2 * self.num_boundary_points for s in self.sensor_org.grid_shape
@@ -327,6 +327,7 @@ class Solver:
327
327
  use_exponential_attenuation: bool = False,
328
328
  use_isotropic_relaxation: bool = True,
329
329
  cuda_device_id: str | int | list | None = None,
330
+ save_gpu_memory: bool = False,
330
331
  ) -> None:
331
332
  """Initialize a Solver instance for the fullwave simulation.
332
333
 
@@ -402,6 +403,17 @@ class Solver:
402
403
  for multiple GPUs, provide a list of device IDs.
403
404
  example 1: [0, 1] for using GPU 0 and GPU 1. or "0,1" as a string.
404
405
  example 2: 2 for using GPU 2 or "2" as a string.
406
+ save_gpu_memory : bool, optional
407
+ Whether to save GPU memory by using ICMAT_MEMORY_SAVING flag in the simulation.
408
+ The simulation does not load initial conditions into GPU memory and
409
+ it loads the slice of the wavefield needed for the current time step
410
+ from CPU memory at each time step.
411
+ Defaults to False. If True, it may significantly reduce GPU memory usage,
412
+ but it may also increase the simulation time
413
+ due to the overhead of data transfer between CPU and GPU
414
+ depending on the hardware and the simulation settings.
415
+ useful in 3D simulations with large grid sizes
416
+ where GPU memory is a limiting factor.
405
417
 
406
418
  Raises
407
419
  ------
@@ -418,6 +430,7 @@ class Solver:
418
430
  self.medium: fullwave.Medium
419
431
  self.grid: fullwave.Grid
420
432
  self.input_file_writer: InputFileWriter
433
+ self.save_gpu_memory = save_gpu_memory
421
434
 
422
435
  self.run_on_memory = run_on_memory
423
436
  if run_on_memory:
@@ -540,6 +553,7 @@ class Solver:
540
553
  is_3d=self.is_3d,
541
554
  use_gpu=self.use_gpu,
542
555
  cuda_device_id=self.cuda_device_id,
556
+ save_gpu_memory=self.save_gpu_memory,
543
557
  )
544
558
 
545
559
  if use_exponential_attenuation:
@@ -0,0 +1,326 @@
1
+ """Source class for Fullwave."""
2
+
3
+ import logging
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+ from matplotlib import pyplot as plt
9
+ from numpy.typing import NDArray
10
+
11
+ from fullwave.utils import plot_utils
12
+ from fullwave.utils.coordinates import coords_to_map, map_to_coords
13
+
14
+ logger = logging.getLogger("__main__." + __name__)
15
+
16
+
17
+ @dataclass
18
+ class Source:
19
+ """Source class for Fullwave."""
20
+
21
+ p0: NDArray[np.float64]
22
+ incoords: NDArray[np.int64]
23
+ grid_shape: tuple[int, ...]
24
+ p0_additive: NDArray[np.float64] | None = None
25
+ incoords_add: NDArray[np.int64] | None = None
26
+
27
+ def __init__( # noqa: C901 PLR0912 PLR0915
28
+ self,
29
+ p0: NDArray[np.float64] | None = None,
30
+ mask: NDArray[np.bool] | None = None,
31
+ *,
32
+ # ---
33
+ p0_additive: NDArray[np.float64] | None = None,
34
+ mask_additive: NDArray[np.bool] | None = None,
35
+ # ---
36
+ coords: NDArray[np.int64] | None = None,
37
+ coords_additive: NDArray[np.int64] | None = None,
38
+ # ---
39
+ grid_shape: tuple[int, ...] | None = None,
40
+ ) -> None:
41
+ """Source class for Fullwave.
42
+
43
+ Parameters
44
+ ----------
45
+ p0 : NDArray[np.float64] | None
46
+ 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.
49
+ mask : NDArray[np.bool] | None
50
+ binary matrix specifying the positions of the time varying pressure source distribution
51
+ shape: [nx, ny] for 2D, [nx, ny, nz] for 3D.
52
+ Mutually exclusive with coords/grid_shape.
53
+ coords : NDArray[np.int64] | None
54
+ Coordinate array of source positions (hard source); shape [n_sources, ndim].
55
+ Must be provided together with grid_shape.
56
+ grid_shape : tuple[int, ...] | None
57
+ Shape of the computational grid. Required when using coords input.
58
+ p0_additive : NDArray[np.float64] | None
59
+ Optional additive (soft) source term; shape [n_sources_add, nt].
60
+ When provided, positions come from coords_additive or mask_additive,
61
+ or default to primary coords.
62
+ mask_additive : NDArray[np.bool] | None
63
+ Mask for additive source positions; shape matches grid_shape.
64
+ Mutually exclusive with coords_additive.
65
+ coords_additive : NDArray[np.int64] | None
66
+ Coordinates for the additive source; shape [n_sources_add, ndim].
67
+ If None and p0_additive is provided, defaults to primary incoords.
68
+ Mutually exclusive with mask_additive.
69
+
70
+ Raises
71
+ ------
72
+ ValueError
73
+ If grid_shape is missing when using coords or additive-only.
74
+ 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.
78
+ If p0 / p0_additive row counts do not match their coordinate arrays.
79
+
80
+ """
81
+ # --- Resolve primary source positions and grid_shape ---
82
+ if coords is not None:
83
+ if grid_shape is None:
84
+ error_msg = "grid_shape is required when using coords"
85
+ raise ValueError(error_msg)
86
+ if mask is not None:
87
+ error_msg = "mask and coords are mutually exclusive"
88
+ raise ValueError(error_msg)
89
+ self.incoords = np.atleast_2d(coords).astype(np.int64, copy=False)
90
+ self.grid_shape = tuple(grid_shape)
91
+ elif mask is not None:
92
+ mask = np.atleast_2d(mask)
93
+ self.grid_shape = mask.shape
94
+ self.incoords = map_to_coords(mask)
95
+ elif p0_additive is not None and (coords_additive is not None or mask_additive is not None):
96
+ # Additive-only: no primary mask/coords; use additive positions as primary
97
+ if coords_additive is not None:
98
+ if grid_shape is None:
99
+ error_msg = "grid_shape is required for additive-only with coords_additive"
100
+ raise ValueError(
101
+ error_msg,
102
+ )
103
+ self.incoords = np.atleast_2d(coords_additive).astype(
104
+ np.int64,
105
+ copy=False,
106
+ )
107
+ self.grid_shape = tuple(grid_shape)
108
+ else:
109
+ assert mask_additive is not None
110
+ mask_add = np.atleast_2d(mask_additive)
111
+ self.grid_shape = mask_add.shape
112
+ self.incoords = map_to_coords(mask_add)
113
+ else:
114
+ error_msg = (
115
+ "Provide (coords + grid_shape), or mask, or"
116
+ " (p0_additive + (coords_additive or mask_additive))"
117
+ )
118
+ raise ValueError(
119
+ error_msg,
120
+ )
121
+
122
+ if p0 is None and p0_additive is None:
123
+ error_msg = "At least one of p0 or p0_additive must be provided"
124
+ raise ValueError(error_msg)
125
+
126
+ # --- Resolve additive coords (for later use) ---
127
+ if coords_additive is not None and mask_additive is not None:
128
+ error_msg = "coords_additive and mask_additive are mutually exclusive"
129
+ raise ValueError(error_msg)
130
+ if coords_additive is not None:
131
+ _coords_add = np.atleast_2d(coords_additive).astype(np.int64, copy=False)
132
+ elif mask_additive is not None:
133
+ mask_add = np.atleast_2d(mask_additive)
134
+ if mask_add.shape != self.grid_shape:
135
+ error_msg = "mask_additive shape must match grid_shape"
136
+ raise ValueError(error_msg)
137
+ _coords_add = map_to_coords(mask_add)
138
+ else:
139
+ _coords_add = None
140
+
141
+ # --- Set p0 and p0_additive ---
142
+ if p0 is not None:
143
+ self.p0 = np.atleast_2d(p0)
144
+ if self.p0.shape[0] != self.incoords.shape[0]:
145
+ error_msg = (
146
+ f"p0 has {self.p0.shape[0]} rows but "
147
+ f"incoords has {self.incoords.shape[0]} (must match)"
148
+ )
149
+ raise ValueError(error_msg)
150
+ self.p0_additive = np.atleast_2d(p0_additive) if p0_additive is not None else None
151
+ else:
152
+ # Additive-only: icmat written as zeros
153
+ p0_add = np.atleast_2d(p0_additive)
154
+ n_add = _coords_add.shape[0] if _coords_add is not None else self.incoords.shape[0]
155
+ if p0_add.shape[0] != n_add:
156
+ error_msg = (
157
+ f"p0_additive shape {p0_add.shape} must have {n_add} rows "
158
+ f"(coords_additive/mask_additive or primary coords)"
159
+ )
160
+ raise ValueError(error_msg)
161
+ if _coords_add is None:
162
+ self.p0 = np.zeros_like(p0_add, dtype=np.float64)
163
+ else:
164
+ self.p0 = np.zeros(
165
+ (self.incoords.shape[0], p0_add.shape[1]),
166
+ dtype=np.float64,
167
+ )
168
+ self.p0_additive = p0_add
169
+
170
+ # --- Set incoords_add for writer/binary ---
171
+ if self.p0_additive is not None:
172
+ self.incoords_add = _coords_add if _coords_add is not None else self.incoords
173
+ else:
174
+ self.incoords_add = None
175
+
176
+ self.is_3d = len(self.grid_shape) == 3
177
+ super().__init__()
178
+ self.__post_init__()
179
+ logger.debug("Source instance created.")
180
+
181
+ def __post_init__(self) -> None:
182
+ """Post-initialization processing for Source.
183
+
184
+ Raises
185
+ ------
186
+ ValueError
187
+ If the number of sources in the input signal
188
+ does not match the number of source coordinates.
189
+ If p0_additive length does not match incoords_add.
190
+
191
+ """
192
+ self.p0 = np.atleast_2d(self.p0)
193
+ if self.p0.shape[0] != self.incoords.shape[0]:
194
+ error_msg = "Input signal has the wrong number of elements"
195
+ raise ValueError(error_msg)
196
+ if self.p0_additive is not None and self.incoords_add is not None:
197
+ self.p0_additive = np.atleast_2d(self.p0_additive)
198
+ if self.p0_additive.shape[0] != self.incoords_add.shape[0]:
199
+ error_msg = (
200
+ f"p0_additive shape {self.p0_additive.shape} must have "
201
+ f"{self.incoords_add.shape[0]} rows (incoords_add)"
202
+ )
203
+ raise ValueError(error_msg)
204
+
205
+ def validate(self, grid_shape: NDArray[np.int64] | tuple) -> None:
206
+ """Check if the source coordinates are consistent with the grid shape."""
207
+ grid_shape = tuple(grid_shape) if isinstance(grid_shape, np.ndarray) else grid_shape
208
+ 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."
210
+ logger.debug("Source validated against grid shape.")
211
+
212
+ @property
213
+ def icmat(self) -> NDArray[np.float64]:
214
+ """Returns icmat for the compatibility with the fullwave code."""
215
+ return self.p0
216
+
217
+ @property
218
+ def mask(self) -> NDArray[np.int64]:
219
+ """Returns the source mask.
220
+
221
+ it calculates the source mask from the source coordinates to reduce the memory usage.
222
+ """
223
+ return coords_to_map(
224
+ self.incoords,
225
+ grid_shape=self.grid_shape,
226
+ is_3d=self.is_3d,
227
+ )
228
+
229
+ @property
230
+ def ncoords(self) -> int:
231
+ """Return the number of sources.
232
+
233
+ ailiased to n_sensors for the compatibility with matlab version
234
+ """
235
+ return self.n_sources
236
+
237
+ @property
238
+ def n_sources(self) -> int:
239
+ """Return the number of sources (hard / primary)."""
240
+ return self.incoords.shape[0]
241
+
242
+ @property
243
+ def n_sources_add(self) -> int:
244
+ """Return the number of additive (soft) source positions."""
245
+ if self.incoords_add is None:
246
+ return 0
247
+ return self.incoords_add.shape[0]
248
+
249
+ def plot(
250
+ self,
251
+ export_path: Path | str | None = Path("./temp/temp.png"),
252
+ *,
253
+ show: bool = False,
254
+ dpi: int = 3000,
255
+ ) -> None:
256
+ """Plot the transducer mask, optionally exporting and displaying the figure.
257
+
258
+ Raises
259
+ ------
260
+ ValueError
261
+ If 3D plotting is requested but not supported.
262
+
263
+ """
264
+ if self.is_3d:
265
+ error_msg = "3D plotting is not supported yet."
266
+ raise ValueError(error_msg)
267
+ plt.close("all")
268
+ fig, _ = plt.subplots()
269
+ fig = plot_utils.plot_array(
270
+ self.mask,
271
+ xlim=[-10, self.mask.shape[1] + 10],
272
+ ylim=[-10, self.mask.shape[0] + 10],
273
+ reverse_y_axis=True,
274
+ save=False,
275
+ clear_all=False,
276
+ fig=fig,
277
+ colorbar=True,
278
+ )
279
+
280
+ if export_path is not None:
281
+ plt.savefig(export_path, dpi=dpi)
282
+ if show:
283
+ plt.show()
284
+
285
+ plt.close("all")
286
+
287
+ def print_info(self) -> None:
288
+ """Print source information to the logger."""
289
+ print(str(self))
290
+
291
+ def summary(self) -> None:
292
+ """Alias for print_info."""
293
+ self.print_info()
294
+
295
+ def __str__(self) -> str:
296
+ """Show source information.
297
+
298
+ Returns
299
+ -------
300
+ str
301
+ Formatted string containing source information.
302
+
303
+ """
304
+ lines = [
305
+ "Source:",
306
+ f" Number of sources: {self.n_sources}",
307
+ f" Grid shape: {self.grid_shape}",
308
+ f" Is 3D: {self.is_3d}",
309
+ f" p0 shape: {self.p0.shape}",
310
+ ]
311
+ if self.p0_additive is not None:
312
+ lines.append(f" p0_additive shape: {self.p0_additive.shape}")
313
+ if self.incoords_add is not None:
314
+ lines.append(f" incoords_add: {self.incoords_add.shape[0]} points")
315
+ return "\n".join(lines) + "\n"
316
+
317
+ def __repr__(self) -> str:
318
+ """Show source information.
319
+
320
+ Returns
321
+ -------
322
+ str
323
+ Formatted string containing source information.
324
+
325
+ """
326
+ return self.__str__()
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "fullwave25"
3
- version = "1.2.3-dev1" # Update via bump-my-version, not manually
3
+ version = "1.2.3-rc1" # 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.3.dev1"
738
+ version = "1.2.3rc1"
739
739
  source = { editable = "." }
740
740
  dependencies = [
741
741
  { name = "joblib" },
@@ -1,206 +0,0 @@
1
- """Source class for Fullwave."""
2
-
3
- import logging
4
- from dataclasses import dataclass
5
- from pathlib import Path
6
-
7
- import numpy as np
8
- from matplotlib import pyplot as plt
9
- from numpy.typing import NDArray
10
-
11
- from fullwave.utils import plot_utils
12
- from fullwave.utils.coordinates import coords_to_map, map_to_coords
13
-
14
- logger = logging.getLogger("__main__." + __name__)
15
-
16
-
17
- @dataclass
18
- class Source:
19
- """Source class for Fullwave."""
20
-
21
- p0: NDArray[np.float64]
22
- incoords: NDArray[np.int64]
23
- grid_shape: tuple[int, ...]
24
-
25
- def __init__(
26
- self,
27
- p0: NDArray[np.float64],
28
- mask: NDArray[np.bool] | None = None,
29
- *,
30
- coords: NDArray[np.int64] | None = None,
31
- grid_shape: tuple[int, ...] | None = None,
32
- ) -> None:
33
- """Source class for Fullwave.
34
-
35
- Parameters
36
- ----------
37
- p0 : NDArray[np.float64]
38
- time varying pressure at each of the source positions given by source.p_mask
39
- shape: [n_sources, nt]
40
- mask : NDArray[np.bool] | None
41
- binary matrix specifying the positions of the time varying pressure source distribution
42
- shape: [nx, ny] for 2D, [nx, ny, nz] for 3D.
43
- Mutually exclusive with coords/grid_shape.
44
- coords : NDArray[np.int64] | None
45
- Coordinate array of source positions, shape [n_sources, ndim].
46
- Must be provided together with grid_shape.
47
- grid_shape : tuple[int, ...] | None
48
- Shape of the computational grid. Required when using coords input.
49
-
50
- Raises
51
- ------
52
- ValueError
53
- If grid_shape is not provided when using coords input.
54
- If both mask and coords are provided (mutually exclusive).
55
- If neither mask nor coords (with grid_shape) are provided.
56
-
57
- """
58
- self.p0 = np.atleast_2d(p0)
59
-
60
- if coords is not None:
61
- if grid_shape is None:
62
- msg = "grid_shape is required when using coords input"
63
- raise ValueError(msg)
64
- if mask is not None:
65
- msg = "mask and coords are mutually exclusive"
66
- raise ValueError(msg)
67
- self.incoords = np.atleast_2d(coords).astype(np.int64, copy=False)
68
- self.grid_shape = tuple(grid_shape)
69
- elif mask is not None:
70
- mask = np.atleast_2d(mask)
71
- self.grid_shape = mask.shape
72
- self.incoords = map_to_coords(mask)
73
- else:
74
- msg = "Either mask or coords (with grid_shape) must be provided"
75
- raise ValueError(msg)
76
-
77
- self.is_3d = len(self.grid_shape) == 3
78
- super().__init__()
79
- self.__post_init__()
80
- logger.debug("Source instance created.")
81
-
82
- def __post_init__(self) -> None:
83
- """Post-initialization processing for Source.
84
-
85
- Raises
86
- ------
87
- ValueError
88
- If the number of sources in the input signal
89
- does not match the number of source coordinates.
90
-
91
- """
92
- self.p0 = np.atleast_2d(self.p0)
93
- if self.p0.shape[0] != self.incoords.shape[0]:
94
- error_msg = "Input signal has the wrong number of elements"
95
- raise ValueError(error_msg)
96
-
97
- def validate(self, grid_shape: NDArray[np.int64] | tuple) -> None:
98
- """Check if the source coordinates are consistent with the grid shape."""
99
- grid_shape = tuple(grid_shape) if isinstance(grid_shape, np.ndarray) else grid_shape
100
- assert self.grid_shape == grid_shape, f"{self.grid_shape} != {grid_shape}"
101
- assert self.n_sources > 0, "No active source found."
102
- logger.debug("Source validated against grid shape.")
103
-
104
- @property
105
- def icmat(self) -> NDArray[np.float64]:
106
- """Returns icmat for the compatibility with the fullwave code."""
107
- return self.p0
108
-
109
- @property
110
- def mask(self) -> NDArray[np.int64]:
111
- """Returns the source mask.
112
-
113
- it calculates the source mask from the source coordinates to reduce the memory usage.
114
- """
115
- return coords_to_map(
116
- self.incoords,
117
- grid_shape=self.grid_shape,
118
- is_3d=self.is_3d,
119
- )
120
-
121
- @property
122
- def ncoords(self) -> int:
123
- """Return the number of sources.
124
-
125
- ailiased to n_sensors for the compatibility with matlab version
126
- """
127
- return self.n_sources
128
-
129
- @property
130
- def n_sources(self) -> int:
131
- """Return the number of sources."""
132
- return self.incoords.shape[0]
133
-
134
- def plot(
135
- self,
136
- export_path: Path | str | None = Path("./temp/temp.png"),
137
- *,
138
- show: bool = False,
139
- dpi: int = 3000,
140
- ) -> None:
141
- """Plot the transducer mask, optionally exporting and displaying the figure.
142
-
143
- Raises
144
- ------
145
- ValueError
146
- If 3D plotting is requested but not supported.
147
-
148
- """
149
- if self.is_3d:
150
- error_msg = "3D plotting is not supported yet."
151
- raise ValueError(error_msg)
152
- plt.close("all")
153
- fig, _ = plt.subplots()
154
- fig = plot_utils.plot_array(
155
- self.mask,
156
- xlim=[-10, self.mask.shape[1] + 10],
157
- ylim=[-10, self.mask.shape[0] + 10],
158
- reverse_y_axis=True,
159
- save=False,
160
- clear_all=False,
161
- fig=fig,
162
- colorbar=True,
163
- )
164
-
165
- if export_path is not None:
166
- plt.savefig(export_path, dpi=dpi)
167
- if show:
168
- plt.show()
169
-
170
- plt.close("all")
171
-
172
- def print_info(self) -> None:
173
- """Print source information to the logger."""
174
- print(str(self))
175
-
176
- def summary(self) -> None:
177
- """Alias for print_info."""
178
- self.print_info()
179
-
180
- def __str__(self) -> str:
181
- """Show source information.
182
-
183
- Returns
184
- -------
185
- str
186
- Formatted string containing source information.
187
-
188
- """
189
- return (
190
- f"Source: \n"
191
- f" Number of sources: {self.n_sources}\n"
192
- f" Grid shape: {self.grid_shape}\n"
193
- f" Is 3D: {self.is_3d}\n"
194
- f" p0 shape: {self.p0.shape}\n"
195
- )
196
-
197
- def __repr__(self) -> str:
198
- """Show source information.
199
-
200
- Returns
201
- -------
202
- str
203
- Formatted string containing source information.
204
-
205
- """
206
- return self.__str__()
File without changes
File without changes
File without changes
File without changes
File without changes