fullwave25 1.3.0.dev0__tar.gz → 1.3.2__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 (80) hide show
  1. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/.bumpversion.toml +1 -1
  2. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/PKG-INFO +2 -1
  3. fullwave25-1.3.2/attenuation.png +0 -0
  4. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/__init__.py +14 -1
  5. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/medium.py +29 -6
  6. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/binary_manager.py +1 -1
  7. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/input_file_writer.py +34 -17
  8. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/pml_builder.py +94 -37
  9. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/solver.py +259 -75
  10. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/transmit.py +20 -4
  11. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/utils/relaxation_parameters.py +12 -3
  12. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/pyproject.toml +3 -1
  13. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/uv.lock +157 -1
  14. fullwave25-1.3.0.dev0/CLAUDE.md +0 -286
  15. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/.gitignore +0 -0
  16. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/.pre-commit-config.yaml +0 -0
  17. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/.python-version +0 -0
  18. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/CITATION.cff +0 -0
  19. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/CONTRIBUTING.md +0 -0
  20. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/LICENSE +0 -0
  21. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/Makefile +0 -0
  22. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/README.md +0 -0
  23. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/beamformer.m +0 -0
  24. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/debug_solver_bin/fullwave2_2d_2_relax_multi_gpu_cuda124 +0 -0
  25. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/debug_solver_bin/fullwave2_2d_exponential_attenuation_multi_gpu +0 -0
  26. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/debug_solver_bin/fullwave2_2d_exponential_attenuation_multi_gpu_cuda124 +0 -0
  27. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/debug_solver_bin/fullwave2_2d_exponential_attenuation_multi_gpu_cuda124_ +0 -0
  28. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/debug_solver_bin/fullwave2_3d_exponential_attenuation_multi_gpu_cuda124 +0 -0
  29. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/debug_solver_bin/fullwave2_3d_exponential_attenuation_multi_gpu_cuda124_ +0 -0
  30. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/experiments/__init__.py +0 -0
  31. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/experiments/exp_scatterer_diameter.py +0 -0
  32. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/beamformer/__init__.py +0 -0
  33. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/beamformer/beamformer.py +0 -0
  34. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/constants/__init__.py +0 -0
  35. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/constants/material_properties.py +0 -0
  36. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/grid.py +0 -0
  37. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/medium_builder/__init__.py +0 -0
  38. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/medium_builder/domain.py +0 -0
  39. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/medium_builder/medium_builder.py +0 -0
  40. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/medium_builder/presets/__init__.py +0 -0
  41. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/medium_builder/presets/data/.keep +0 -0
  42. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/medium_builder/presets/data/abdominal_wall/i2365f_etfw1.mat +0 -0
  43. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/medium_builder/presets/domain_abdominal_wall.py +0 -0
  44. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/medium_builder/presets/domain_background.py +0 -0
  45. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/medium_builder/presets/domain_scatterer.py +0 -0
  46. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/medium_builder/presets/domain_simple.py +0 -0
  47. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/medium_builder/presets/domain_water_gel.py +0 -0
  48. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/sensor.py +0 -0
  49. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/__init__.py +0 -0
  50. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda118 +0 -0
  51. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda124 +0 -0
  52. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda129 +0 -0
  53. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda130 +0 -0
  54. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda118 +0 -0
  55. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda124 +0 -0
  56. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda129 +0 -0
  57. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda130 +0 -0
  58. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/bins/database/relaxation_params_database_num_relax=3_20260828_1513.mat +0 -0
  59. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/bins/database/relaxation_params_database_num_relax=3_20260828_1513_invalid_cells.json +0 -0
  60. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/bins/database/relaxation_params_database_num_relax=4_20260828_1456.mat +0 -0
  61. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/bins/database/relaxation_params_database_num_relax=4_20260828_1456_invalid_cells.json +0 -0
  62. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/cuda_utils.py +0 -0
  63. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/launcher.py +0 -0
  64. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/shipped_database.py +0 -0
  65. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/source_type.py +0 -0
  66. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/solver/utils.py +0 -0
  67. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/source.py +0 -0
  68. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/transducer.py +0 -0
  69. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/utils/__init__.py +0 -0
  70. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/utils/check_functions.py +0 -0
  71. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/utils/coordinates.py +0 -0
  72. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/utils/memory_tempfile.py +0 -0
  73. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/utils/numerical.py +0 -0
  74. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/utils/plot_utils.py +0 -0
  75. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/utils/pulse.py +0 -0
  76. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/utils/scatterer.py +0 -0
  77. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/utils/signal_filter.py +0 -0
  78. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/fullwave/utils/signal_process.py +0 -0
  79. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/ruff.toml +0 -0
  80. {fullwave25-1.3.0.dev0 → fullwave25-1.3.2}/signal_filter_example.png +0 -0
@@ -1,5 +1,5 @@
1
1
  [tool.bumpversion]
2
- current_version = "1.3.0-dev0"
2
+ current_version = "1.3.2"
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.5
2
2
  Name: fullwave25
3
- Version: 1.3.0.dev0
3
+ Version: 1.3.2
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
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Topic :: Scientific/Engineering :: Physics
15
15
  Requires-Python: >=3.10
16
+ Requires-Dist: cupy-cuda12x>=13.6.0
16
17
  Requires-Dist: matplotlib>=3.10.7
17
18
  Requires-Dist: numba>=0.63.1
18
19
  Requires-Dist: numexpr>=2.14.1
Binary file
@@ -1,10 +1,13 @@
1
1
  """fullwave module."""
2
2
 
3
3
  import logging
4
+ import os
4
5
  import platform
5
6
  import time
6
7
  from importlib.metadata import PackageNotFoundError, version
7
8
 
9
+ import numexpr
10
+
8
11
  from .grid import Grid
9
12
  from .medium import Medium, MediumExponentialAttenuation, MediumRelaxationMaps
10
13
  from .sensor import Sensor
@@ -63,7 +66,17 @@ try:
63
66
  __version__ = version("fullwave")
64
67
  except PackageNotFoundError:
65
68
  # Update via bump-my-version, not manually
66
- __version__ = "1.3.0-dev0"
69
+ __version__ = "1.3.2"
67
70
 
68
71
  VERSION = __version__ # for convenience
69
72
  logger.info("Fullwave version: %s", __version__)
73
+
74
+ # numexpr keeps 16 threads unless told otherwise, and the medium, the absorbing
75
+ # layer and the coefficients run through it on grids of hundreds of millions of
76
+ # cells. A thread count set in the environment is left alone.
77
+ if not any(
78
+ os.environ.get(name)
79
+ for name in ("NUMEXPR_MAX_THREADS", "NUMEXPR_NUM_THREADS", "OMP_NUM_THREADS")
80
+ ):
81
+ numexpr.set_num_threads(min(numexpr.detect_number_of_cores(), numexpr.MAX_THREADS))
82
+ logger.info("numexpr threads: %d", numexpr.get_num_threads())
@@ -19,6 +19,7 @@ from fullwave.solver.utils import initialize_relaxation_param_dict
19
19
  from fullwave.utils import check_functions, plot_utils
20
20
  from fullwave.utils.coordinates import coords_to_map, map_to_coords
21
21
  from fullwave.utils.relaxation_parameters import (
22
+ _array_module,
22
23
  band_scaled_sound_speed,
23
24
  generate_relaxation_params,
24
25
  )
@@ -200,7 +201,8 @@ def _make_the_lossless_voxels_lossless(
200
201
  """
201
202
  if not relaxation_param_dict:
202
203
  return
203
- coefficient = np.asarray(alpha_coeff)
204
+ xp = np if np.ndim(alpha_coeff) == 0 else _array_module(alpha_coeff)
205
+ coefficient = xp.asarray(alpha_coeff)
204
206
  if coefficient.ndim == 0:
205
207
  if float(coefficient) != 0.0:
206
208
  return
@@ -273,6 +275,7 @@ class MediumRelaxationMaps:
273
275
  n_jobs: int = -1,
274
276
  dtype: type = np.float64,
275
277
  use_gpu: bool = False,
278
+ compute_coefficients: bool = True,
276
279
  ) -> None:
277
280
  """Medium class for Fullwave.
278
281
 
@@ -330,6 +333,10 @@ class MediumRelaxationMaps:
330
333
  use_gpu : bool, optional
331
334
  If True, use CuPy for GPU-accelerated computation (default is False).
332
335
  Requires CuPy to be installed. Falls back to CPU if CuPy is unavailable.
336
+ compute_coefficients : bool, optional
337
+ If True (the default), compute ``relaxation_param_dict_for_fw2``
338
+ here. False leaves it empty for a caller that writes every entry
339
+ itself, as the PML builder does.
333
340
 
334
341
  """
335
342
  check_functions.check_compatible_value(
@@ -354,7 +361,6 @@ class MediumRelaxationMaps:
354
361
  try:
355
362
  self.relaxation_param_dict = initialize_relaxation_param_dict(
356
363
  n_relaxation_mechanisms=n_relaxation_mechanisms,
357
- value=xp.zeros_like(xp.asarray(sound_speed), dtype=self.dtype),
358
364
  )
359
365
  self.grid = grid
360
366
  self.is_3d = grid.is_3d
@@ -379,7 +385,6 @@ class MediumRelaxationMaps:
379
385
  xp = np
380
386
  self.relaxation_param_dict = initialize_relaxation_param_dict(
381
387
  n_relaxation_mechanisms=n_relaxation_mechanisms,
382
- value=np.zeros_like(np.asarray(sound_speed), dtype=self.dtype),
383
388
  )
384
389
  self.grid = grid
385
390
  self.is_3d = grid.is_3d
@@ -404,12 +409,23 @@ class MediumRelaxationMaps:
404
409
  relaxation_param_updates=relaxation_param_dict,
405
410
  )
406
411
  self.use_isotropic_relaxation = use_isotropic_relaxation
407
- self.relaxation_param_dict_for_fw2 = self._calc_relaxation_param_dict_for_fw2(
408
- use_isotropic_relaxation=self.use_isotropic_relaxation,
412
+ self.relaxation_param_dict_for_fw2 = (
413
+ self._calc_relaxation_param_dict_for_fw2(
414
+ use_isotropic_relaxation=self.use_isotropic_relaxation,
415
+ )
416
+ if compute_coefficients
417
+ else {}
409
418
  )
410
419
  self.check_fields()
411
420
  logger.debug("MediumRelaxationMaps instance created.")
412
421
 
422
+ def build_coefficients(self) -> None:
423
+ """Compute ``relaxation_param_dict_for_fw2`` if the constructor was told to skip it."""
424
+ if not self.relaxation_param_dict_for_fw2:
425
+ self.relaxation_param_dict_for_fw2 = self._calc_relaxation_param_dict_for_fw2(
426
+ use_isotropic_relaxation=self.use_isotropic_relaxation,
427
+ )
428
+
413
429
  def _update_relaxation_param_dict(
414
430
  self,
415
431
  relaxation_param_updates: dict[str, NDArray[np.float64]],
@@ -1627,12 +1643,18 @@ class Medium:
1627
1643
  parameters = self._relaxation_parameters_of(sound_speed, alpha_coeff, alpha_power)
1628
1644
  return parameters, sound_speed
1629
1645
 
1630
- def build(self) -> MediumRelaxationMaps:
1646
+ def build(self, *, compute_coefficients: bool = True) -> MediumRelaxationMaps:
1631
1647
  """Retrieve the relaxation parameters from alpha and power maps.
1632
1648
 
1633
1649
  it uses the relaxation parameters look up table
1634
1650
  to generate the relaxation parameters.
1635
1651
 
1652
+ Parameters
1653
+ ----------
1654
+ compute_coefficients : bool, optional
1655
+ Passed to `MediumRelaxationMaps`. False skips the solver
1656
+ coefficients, which a caller that extends the maps first does not use.
1657
+
1636
1658
  Returns
1637
1659
  -------
1638
1660
  MediumRelaxationMaps: An instance of MediumRelaxationMaps
@@ -1670,6 +1692,7 @@ class Medium:
1670
1692
  n_jobs=self.n_jobs,
1671
1693
  dtype=self.dtype,
1672
1694
  use_gpu=self.use_gpu,
1695
+ compute_coefficients=compute_coefficients,
1673
1696
  )
1674
1697
 
1675
1698
  def _db_mhz_cm_to_a_exp(
@@ -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.4.1"
31
+ BINARY_RELEASE_TAG = "fullwave_bin_v1.4.4"
32
32
 
33
33
 
34
34
  def _download_url(filename: str, tag: str) -> str:
@@ -56,7 +56,7 @@ class InputFileWriter:
56
56
  path_fullwave_simulation_bin: Path = Path(__file__).parent / "bins" / "fullwave_solver_gpu",
57
57
  validate_input: bool = True,
58
58
  use_exponential_attenuation: bool = False,
59
- use_isotropic_relaxation: bool = False,
59
+ use_isotropic_relaxation: bool = True,
60
60
  release_after_write: bool = False,
61
61
  pml_thickness: int = 0,
62
62
  exponential_attenuation_pml_thickness_px: int = 0,
@@ -94,6 +94,7 @@ class InputFileWriter:
94
94
  to reduce memory usage while retaining accuracy.
95
95
  For 2D it will reduce the memory usage by approximately 15%.
96
96
  For 3D it will reduce the memory usage by approximately 25%.
97
+ Only True is supported, and False raises NotImplementedError.
97
98
  This option omits the anisotropic relaxation mechanisms to model the attenuation.
98
99
  We usually recommend using isotropic relaxation mechanisms
99
100
  unless the anisotropic attenuation is required for the simulation.
@@ -126,6 +127,12 @@ class InputFileWriter:
126
127
  """
127
128
  logger.debug("Initializing InputFileWriter instance.")
128
129
 
130
+ if use_isotropic_relaxation is False:
131
+ error_msg = (
132
+ "Currently, only isotropic relaxation is supported. "
133
+ "Please set use_isotropic_relaxation to True for the simulation."
134
+ )
135
+ raise NotImplementedError(error_msg)
129
136
  self._work_dir = Path(work_dir)
130
137
  self.path_fullwave_simulation_bin = path_fullwave_simulation_bin
131
138
  self.use_isotropic_relaxation = use_isotropic_relaxation
@@ -1228,16 +1235,8 @@ class InputFileWriter:
1228
1235
  mechanisms = range(1, self.medium.n_relaxation_mechanisms + 1)
1229
1236
  for nu in mechanisms:
1230
1237
  var_name_list.extend([f"apmlu{nu}", f"bpmlu{nu}", f"apmlx{nu}", f"bpmlx{nu}"])
1231
- if not self.use_isotropic_relaxation:
1232
- var_name_list.extend(["kappay", "kappaw"])
1233
- for nu in mechanisms:
1234
- var_name_list.extend([f"apmlw{nu}", f"apmly{nu}", f"bpmlw{nu}", f"bpmly{nu}"])
1235
1238
  if self.is_3d:
1236
- var_name_list.append("modZ")
1237
- if self.is_3d and not self.use_isotropic_relaxation:
1238
- var_name_list.extend(["nZ", "dZ", "kappaz", "kappav"])
1239
- for nu in mechanisms:
1240
- var_name_list.extend([f"apmlz{nu}", f"apmlv{nu}", f"bpmlz{nu}", f"bpmlv{nu}"])
1239
+ var_name_list.extend(["modZ", "nZ", "dZ"])
1241
1240
  for var_name in var_name_list:
1242
1241
  src_data = src_dir / f"{var_name}.dat"
1243
1242
  dst_data = dst_dir / f"{var_name}.dat"
@@ -1409,6 +1408,29 @@ class InputFileWriter:
1409
1408
  dst.unlink()
1410
1409
  dst.symlink_to(self.path_fullwave_simulation_bin.resolve())
1411
1410
 
1411
+ @staticmethod
1412
+ def _write_in_slabs(
1413
+ variable_mat: np.ndarray,
1414
+ dtype: DTypeLike,
1415
+ save_path: str | Path,
1416
+ slab_bytes: int = 256 * 1024**2,
1417
+ ) -> None:
1418
+ """Write an array in C order, cast one slab of its first axis at a time.
1419
+
1420
+ The bytes are those of one whole cast buffer, and the largest buffer
1421
+ held is one slab, so many maps can be written at once.
1422
+ """
1423
+ dtype = np.dtype(dtype)
1424
+ if variable_mat.ndim == 0:
1425
+ np.ascontiguousarray(variable_mat, dtype=dtype).tofile(save_path)
1426
+ return
1427
+ row_bytes = max(1, dtype.itemsize * (variable_mat.size // max(variable_mat.shape[0], 1)))
1428
+ rows_per_slab = max(1, slab_bytes // row_bytes)
1429
+ with Path(save_path).open("wb") as handle:
1430
+ for start in range(0, variable_mat.shape[0], rows_per_slab):
1431
+ slab = variable_mat[start : start + rows_per_slab]
1432
+ np.ascontiguousarray(slab, dtype=dtype).tofile(handle)
1433
+
1412
1434
  @staticmethod
1413
1435
  def _write_ic(
1414
1436
  fname: str | Path,
@@ -1418,11 +1440,7 @@ class InputFileWriter:
1418
1440
  logger.debug("Writing initial condition matrix to %s", fname, stacklevel=2)
1419
1441
 
1420
1442
  t0 = time.perf_counter()
1421
- out = np.ascontiguousarray(
1422
- icmat.T,
1423
- dtype=np.float32,
1424
- ) # does transpose+cast into one new buffer
1425
- out.tofile(fname)
1443
+ InputFileWriter._write_in_slabs(icmat.T, np.float32, fname)
1426
1444
  t1 = time.perf_counter()
1427
1445
 
1428
1446
  logger.debug("Initial condition matrix written in %.2e seconds", t1 - t0, stacklevel=2)
@@ -1486,8 +1504,7 @@ class InputFileWriter:
1486
1504
  if variable_mat.dtype == dtype and variable_mat.flags.c_contiguous:
1487
1505
  variable_mat.tofile(save_path) # writes in C order
1488
1506
  else:
1489
- out = np.ascontiguousarray(variable_mat, dtype=dtype) # at most one new buffer
1490
- out.tofile(save_path)
1507
+ InputFileWriter._write_in_slabs(variable_mat, dtype, save_path)
1491
1508
 
1492
1509
  t1 = time.perf_counter()
1493
1510
  logger.debug("Matrix written in %.2e seconds", t1 - t0, stacklevel=2)
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import concurrent.futures
6
6
  import gc
7
7
  import logging
8
+ import os
8
9
  from collections import OrderedDict
9
10
  from dataclasses import dataclass, field
10
11
  from functools import cached_property
@@ -312,12 +313,20 @@ class PMLBuilder:
312
313
  logger.debug("building extended medium for pml...")
313
314
  medium_as_given = self.medium_org
314
315
  if not isinstance(self.medium_org, fullwave.MediumRelaxationMaps):
315
- self.medium_org = self.medium_org.build()
316
+ logger.info(
317
+ "Building the relaxation maps of %d cells...", int(self.medium_org.sound_speed.size)
318
+ )
319
+ self.medium_org = self.medium_org.build(compute_coefficients=False)
316
320
  base_attrs = ["sound_speed", "density", "beta"]
317
321
  original_alpha_coeff = getattr(self.medium_org, "alpha_coeff", None)
318
322
  if original_alpha_coeff is not None and np.ndim(original_alpha_coeff) != 0:
319
323
  base_attrs.append("alpha_coeff")
320
324
  relax_attrs = list(self.medium_org.relaxation_param_dict.keys())
325
+ logger.info(
326
+ "Extending %d maps by %d cells on each side...",
327
+ len(base_attrs) + len(relax_attrs),
328
+ self.num_boundary_points,
329
+ )
321
330
 
322
331
  if self.xp is not np:
323
332
  # Pass CuPy arrays directly — multi-GPU extension uses D2D copy (NVLink)
@@ -366,7 +375,11 @@ class PMLBuilder:
366
375
  n_jobs=self.medium_org.n_jobs,
367
376
  dtype=getattr(self.medium_org, "dtype", np.float64),
368
377
  use_gpu=False,
378
+ compute_coefficients=False,
369
379
  )
380
+ # The extended maps carry everything the absorbing layer reads, so the
381
+ # interior maps are released here rather than held through the run.
382
+ self.medium_org = medium_as_given
370
383
  logger.debug("building extended medium for pml...done")
371
384
 
372
385
  logger.debug("building extended source for pml...")
@@ -861,10 +874,16 @@ class PMLBuilder:
861
874
 
862
875
  eps = xp.finfo(xp.float64).eps
863
876
 
864
- rate = d_x / kappa_x + alpha_x
865
877
  two_over_dt = 2.0 / dt
866
- b = (two_over_dt - rate) / (two_over_dt + rate)
867
- a = -(d_x / (kappa_x**2 + eps)) / (rate + two_over_dt)
878
+ if xp is np:
879
+ rate = ne.evaluate("d_x / kappa_x + alpha_x")
880
+ b = ne.evaluate("(two_over_dt - rate) / (two_over_dt + rate)")
881
+ a = ne.evaluate("-(d_x / (kappa_x ** 2 + eps)) / (rate + two_over_dt)")
882
+ else:
883
+ rate = d_x / kappa_x + alpha_x
884
+ b = (two_over_dt - rate) / (two_over_dt + rate)
885
+ a = -(d_x / (kappa_x**2 + eps)) / (rate + two_over_dt)
886
+ del rate
868
887
 
869
888
  if output_dtype is not None and output_dtype != xp.float64:
870
889
  a = a.astype(output_dtype, copy=False)
@@ -886,6 +905,10 @@ class PMLBuilder:
886
905
  """
887
906
  logger.debug("Running PML builder...")
888
907
  if use_pml:
908
+ logger.info(
909
+ "Building the absorbing layer on %d cells...",
910
+ int(self.extended_medium.sound_speed.size),
911
+ )
889
912
  extended_medium: fullwave.MediumRelaxationMaps = self.extended_medium.build()
890
913
  if self.is_3d:
891
914
  return self._apply_pml_3d(
@@ -901,30 +924,54 @@ class PMLBuilder:
901
924
  )
902
925
 
903
926
  extended_medium: fullwave.MediumRelaxationMaps = self.extended_medium.build()
927
+ extended_medium.build_coefficients()
904
928
  return extended_medium
905
929
 
906
930
  def _medium_copy(self, relaxation_param_dict: dict, key: str) -> NDArray[np.float64]:
907
- """Return a fresh float64 copy of one medium map."""
908
- return self.xp.array(relaxation_param_dict[key], dtype=self.xp.float64, copy=True)
931
+ """Return a fresh float64 copy of one medium map.
932
+
933
+ On the host the copy is split along the first axis across threads,
934
+ because one thread copying a map of the whole padded grid is the slowest
935
+ step of the absorbing layer.
936
+ """
937
+ source = relaxation_param_dict[key]
938
+ if self.xp is not np or np.ndim(source) == 0:
939
+ return self.xp.array(source, dtype=self.xp.float64, copy=True)
940
+ copied = np.empty(np.shape(source), dtype=np.float64)
941
+ n_rows = copied.shape[0]
942
+ n_slabs = max(1, min(n_rows, os.cpu_count() or 1, ne.MAX_THREADS))
943
+ edges = np.linspace(0, n_rows, n_slabs + 1).astype(int)
944
+
945
+ def copy_slab(start: int, stop: int) -> None:
946
+ np.copyto(copied[start:stop], source[start:stop], casting="unsafe")
947
+
948
+ with concurrent.futures.ThreadPoolExecutor(max_workers=n_slabs) as pool:
949
+ list(pool.map(copy_slab, edges[:-1], edges[1:]))
950
+ return copied
909
951
 
910
952
  def _ramp_on_every_axis(
911
- self, field: NDArray[np.float64], **kwargs: object
953
+ self, field: NDArray[np.float64], *, in_place: bool = False, **kwargs: object
912
954
  ) -> NDArray[np.float64]:
913
- """Apply one ramp along each grid axis in turn, in two dimensions or three."""
955
+ """Apply one ramp along each grid axis in turn, in two dimensions or three.
956
+
957
+ The ramp writes only the layers at both ends of each axis. With
958
+ `in_place` the field is written itself, otherwise one copy is written.
959
+ """
914
960
  is_3d = field.ndim == self.three_dimensions
915
- out = field
961
+ out = field if in_place else self.xp.array(field, copy=True)
916
962
  for axis_index in range(field.ndim):
917
963
  out = self._apply_transition_and_pml(
918
- out, array_shape=field.shape, axis=axis_index, is_3d=is_3d, **kwargs
964
+ out, array_shape=field.shape, axis=axis_index, is_3d=is_3d, in_place=True, **kwargs
919
965
  )
920
966
  return out
921
967
 
922
968
  def _empty_damping_in_transition_layer(
923
- self, damping: NDArray[np.float64]
969
+ self, damping: NDArray[np.float64], *, in_place: bool = False
924
970
  ) -> NDArray[np.float64]:
925
971
  """Return the damping strength taken to zero across the transition layer."""
926
972
  return self._ramp_on_every_axis(
927
973
  damping,
974
+ in_place=in_place,
928
975
  value_target=0.0,
929
976
  transition_type="cosine",
930
977
  transit_within_transition_layer=True,
@@ -935,10 +982,13 @@ class PMLBuilder:
935
982
  damping: NDArray[np.float64],
936
983
  n_polynomial: float,
937
984
  d_target_pml: float,
985
+ *,
986
+ in_place: bool = False,
938
987
  ) -> NDArray[np.float64]:
939
988
  """Return the damping strength raised to the PML target across the PML layer."""
940
989
  return self._ramp_on_every_axis(
941
990
  damping,
991
+ in_place=in_place,
942
992
  value_target=d_target_pml,
943
993
  n_polynomial=n_polynomial,
944
994
  transition_type="polynomial",
@@ -949,27 +999,36 @@ class PMLBuilder:
949
999
  self,
950
1000
  relaxation_frequency: NDArray[np.float64],
951
1001
  entrance: float,
1002
+ *,
1003
+ in_place: bool = False,
952
1004
  ) -> NDArray[np.float64]:
953
1005
  """Return the relaxation frequency replaced by the CFS ramp inside the PML layer.
954
1006
 
955
1007
  Replaced rather than ramped into, because ramping a relaxation frequency
956
1008
  through the evaluation frequency is what confines the damping to the
957
- outer edge.
1009
+ outer edge. The PML layer is the slab of `edge` cells at both ends of
1010
+ each axis, so only those slabs are copied from the ramp.
958
1011
  """
959
1012
  xp = self.xp
960
1013
  edge = self.m_spatial_order + self.n_pml_layer
961
1014
  ramp = self._ramp_on_every_axis(
962
1015
  xp.full_like(relaxation_frequency, entrance),
1016
+ in_place=True,
963
1017
  value_target=0.0,
964
1018
  transition_type="linear",
965
1019
  transit_within_pml_layer=True,
966
1020
  )
967
- inside_pml_layer = xp.zeros(relaxation_frequency.shape, dtype=bool)
968
- for axis_index in range(relaxation_frequency.ndim):
969
- view = xp.moveaxis(inside_pml_layer, axis_index, 0)
970
- view[:edge] = True
971
- view[view.shape[0] - edge :] = True
972
- return xp.where(inside_pml_layer, xp.asarray(ramp), xp.asarray(relaxation_frequency))
1021
+ spliced = (
1022
+ xp.asarray(relaxation_frequency)
1023
+ if in_place
1024
+ else xp.array(relaxation_frequency, copy=True)
1025
+ )
1026
+ for axis_index in range(spliced.ndim):
1027
+ target = xp.moveaxis(spliced, axis_index, 0)
1028
+ source = xp.moveaxis(ramp, axis_index, 0)
1029
+ target[:edge] = source[:edge]
1030
+ target[target.shape[0] - edge :] = source[source.shape[0] - edge :]
1031
+ return spliced
973
1032
 
974
1033
  def _build_decoupled(
975
1034
  self,
@@ -1002,16 +1061,16 @@ class PMLBuilder:
1002
1061
  relaxation_frequency = self._medium_copy(
1003
1062
  relaxation_param_dict, rename_dict[f"alpha_{letter}_nu{nu}"]
1004
1063
  )
1005
- emptied = self._empty_damping_in_transition_layer(damping)
1064
+ emptied = self._empty_damping_in_transition_layer(damping, in_place=True)
1006
1065
  if nu > 1:
1007
1066
  out_dict[f"d_{letter}_nu{nu}"] = emptied
1008
1067
  out_dict[f"alpha_{letter}_nu{nu}"] = relaxation_frequency
1009
1068
  continue
1010
1069
  out_dict[f"d_{letter}_nu{nu}"] = self._raise_damping_in_pml_layer(
1011
- emptied, n_polynomial, d_target_pml
1070
+ emptied, n_polynomial, d_target_pml, in_place=True
1012
1071
  )
1013
1072
  out_dict[f"alpha_{letter}_nu{nu}"] = self._splice_entrance_frequency_into_pml_layer(
1014
- relaxation_frequency, entrance
1073
+ relaxation_frequency, entrance, in_place=True
1015
1074
  )
1016
1075
  return out_dict
1017
1076
 
@@ -1124,9 +1183,11 @@ class PMLBuilder:
1124
1183
  found = np.asarray(relaxation_param_dict[key], dtype=np.float64)
1125
1184
  if found.ndim == 0 or np.allclose(found, 1.0):
1126
1185
  continue
1186
+ found = self.xp.array(found, copy=True)
1127
1187
  for axis in range(3 if is_3d else 2):
1128
1188
  found = self._apply_transition_and_pml(
1129
1189
  found,
1190
+ in_place=True,
1130
1191
  value_target=1.0,
1131
1192
  array_shape=found.shape,
1132
1193
  axis=axis,
@@ -1213,13 +1274,9 @@ class PMLBuilder:
1213
1274
  # Return keys + values so parent can update dict safely
1214
1275
  return (f"a_pml_{axis}{nu}", a, f"b_pml_{axis}{nu}", b)
1215
1276
 
1216
- if self.xp is not np:
1217
- # GPU path: run sequentially
1218
- results = [_worker(nu, axis) for nu, axis in tasks]
1219
- else:
1220
- with concurrent.futures.ThreadPoolExecutor() as executor:
1221
- futures = [executor.submit(_worker, nu, axis) for nu, axis in tasks]
1222
- results = [f.result() for f in futures]
1277
+ # One task at a time. numexpr threads each expression, and a pool of
1278
+ # tasks held every task's temporaries of the whole grid at once.
1279
+ results = [_worker(nu, axis) for nu, axis in tasks]
1223
1280
 
1224
1281
  for a_key, a_val, b_key, b_val in results:
1225
1282
  out_dict[a_key] = a_val
@@ -1312,13 +1369,9 @@ class PMLBuilder:
1312
1369
  # Return keys + values so parent can update dict safely
1313
1370
  return (f"a_pml_{axis}{nu}", a, f"b_pml_{axis}{nu}", b)
1314
1371
 
1315
- if self.xp is not np:
1316
- # GPU path: run sequentially
1317
- results = [_worker(nu, axis) for nu, axis in tasks]
1318
- else:
1319
- with concurrent.futures.ThreadPoolExecutor() as executor:
1320
- futures = [executor.submit(_worker, nu, axis) for nu, axis in tasks]
1321
- results = [f.result() for f in futures]
1372
+ # One task at a time. numexpr threads each expression, and a pool of
1373
+ # tasks held every task's temporaries of the whole grid at once.
1374
+ results = [_worker(nu, axis) for nu, axis in tasks]
1322
1375
 
1323
1376
  for a_key, a_val, b_key, b_val in results:
1324
1377
  out_dict[a_key] = a_val
@@ -1347,6 +1400,7 @@ class PMLBuilder:
1347
1400
  transit_within_pml_layer: bool = False,
1348
1401
  disable_the_transition_and_pml: bool = False,
1349
1402
  is_3d: bool = False,
1403
+ in_place: bool = False,
1350
1404
  ) -> NDArray[np.float64]:
1351
1405
  if transit_within_transition_layer and transit_within_pml_layer:
1352
1406
  error_msg = (
@@ -1428,8 +1482,11 @@ class PMLBuilder:
1428
1482
  if use_gpu:
1429
1483
  input_array = xp.asarray(input_array)
1430
1484
 
1431
- # Move axis to 0 for uniform processing
1432
- working_array = xp.moveaxis(input_array, axis, 0).copy()
1485
+ # Move axis to 0 for uniform processing. Only the layers at both ends
1486
+ # are written, so an array the caller owns needs no copy.
1487
+ working_array = xp.moveaxis(input_array, axis, 0)
1488
+ if not in_place:
1489
+ working_array = working_array.copy()
1433
1490
 
1434
1491
  # Apply boundary conditions
1435
1492
  working_array[: m_offset + layer_thickness] = value_target