fullwave25 1.2.6.dev10__tar.gz → 1.3.0.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 (83) hide show
  1. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/.bumpversion.toml +1 -1
  2. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/.gitignore +7 -0
  3. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/PKG-INFO +37 -2
  4. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/README.md +35 -0
  5. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/__init__.py +5 -2
  6. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/medium.py +262 -52
  7. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/medium_builder/domain.py +3 -6
  8. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/medium_builder/medium_builder.py +12 -6
  9. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/medium_builder/presets/domain_abdominal_wall.py +4 -10
  10. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/medium_builder/presets/domain_background.py +3 -6
  11. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/medium_builder/presets/domain_scatterer.py +3 -6
  12. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/medium_builder/presets/domain_simple.py +3 -7
  13. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/solver/binary_manager.py +3 -3
  14. fullwave25-1.3.0.dev0/fullwave/solver/bins/database/relaxation_params_database_num_relax=3_20260828_1513.mat +0 -0
  15. fullwave25-1.3.0.dev0/fullwave/solver/bins/database/relaxation_params_database_num_relax=3_20260828_1513_invalid_cells.json +4248 -0
  16. fullwave25-1.3.0.dev0/fullwave/solver/bins/database/relaxation_params_database_num_relax=4_20260828_1456.mat +0 -0
  17. fullwave25-1.3.0.dev0/fullwave/solver/bins/database/relaxation_params_database_num_relax=4_20260828_1456_invalid_cells.json +2514 -0
  18. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/solver/input_file_writer.py +156 -53
  19. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/solver/pml_builder.py +452 -483
  20. fullwave25-1.3.0.dev0/fullwave/solver/shipped_database.py +109 -0
  21. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/solver/solver.py +188 -50
  22. fullwave25-1.3.0.dev0/fullwave/solver/source_type.py +318 -0
  23. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/solver/utils.py +3 -1
  24. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/source.py +75 -21
  25. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/transducer.py +809 -74
  26. fullwave25-1.3.0.dev0/fullwave/transmit.py +293 -0
  27. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/utils/plot_utils.py +62 -2
  28. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/utils/pulse.py +3 -14
  29. fullwave25-1.3.0.dev0/fullwave/utils/relaxation_parameters.py +1188 -0
  30. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/pyproject.toml +9 -2
  31. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/uv.lock +63 -1
  32. fullwave25-1.2.6.dev10/20260120_0844.log +0 -1158
  33. fullwave25-1.2.6.dev10/fullwave/solver/bins/database/relaxation_params_database_num_relax=2_20260113_0957.mat +0 -0
  34. fullwave25-1.2.6.dev10/fullwave/utils/relaxation_parameters.py +0 -540
  35. fullwave25-1.2.6.dev10/ruff.log +0 -1177
  36. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/.pre-commit-config.yaml +0 -0
  37. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/.python-version +0 -0
  38. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/CITATION.cff +0 -0
  39. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/CLAUDE.md +0 -0
  40. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/CONTRIBUTING.md +0 -0
  41. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/LICENSE +0 -0
  42. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/Makefile +0 -0
  43. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/beamformer.m +0 -0
  44. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/debug_solver_bin/fullwave2_2d_2_relax_multi_gpu_cuda124 +0 -0
  45. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/debug_solver_bin/fullwave2_2d_exponential_attenuation_multi_gpu +0 -0
  46. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/debug_solver_bin/fullwave2_2d_exponential_attenuation_multi_gpu_cuda124 +0 -0
  47. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/debug_solver_bin/fullwave2_2d_exponential_attenuation_multi_gpu_cuda124_ +0 -0
  48. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/debug_solver_bin/fullwave2_3d_exponential_attenuation_multi_gpu_cuda124 +0 -0
  49. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/debug_solver_bin/fullwave2_3d_exponential_attenuation_multi_gpu_cuda124_ +0 -0
  50. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/experiments/__init__.py +0 -0
  51. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/experiments/exp_scatterer_diameter.py +0 -0
  52. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/beamformer/__init__.py +0 -0
  53. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/beamformer/beamformer.py +0 -0
  54. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/constants/__init__.py +0 -0
  55. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/constants/material_properties.py +0 -0
  56. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/grid.py +0 -0
  57. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/medium_builder/__init__.py +0 -0
  58. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/medium_builder/presets/__init__.py +0 -0
  59. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/medium_builder/presets/data/.keep +0 -0
  60. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/medium_builder/presets/data/abdominal_wall/i2365f_etfw1.mat +0 -0
  61. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/medium_builder/presets/domain_water_gel.py +0 -0
  62. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/sensor.py +0 -0
  63. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/solver/__init__.py +0 -0
  64. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda118 +0 -0
  65. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda124 +0 -0
  66. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda129 +0 -0
  67. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda130 +0 -0
  68. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda118 +0 -0
  69. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda124 +0 -0
  70. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda129 +0 -0
  71. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/solver/bins/_gpu/3d/num_relax=2/fullwave2_3d_2_relax_multi_gpu_cuda130 +0 -0
  72. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/solver/cuda_utils.py +0 -0
  73. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/solver/launcher.py +0 -0
  74. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/utils/__init__.py +0 -0
  75. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/utils/check_functions.py +0 -0
  76. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/utils/coordinates.py +0 -0
  77. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/utils/memory_tempfile.py +0 -0
  78. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/utils/numerical.py +0 -0
  79. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/utils/scatterer.py +0 -0
  80. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/utils/signal_filter.py +0 -0
  81. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/fullwave/utils/signal_process.py +0 -0
  82. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/ruff.toml +0 -0
  83. {fullwave25-1.2.6.dev10 → fullwave25-1.3.0.dev0}/signal_filter_example.png +0 -0
@@ -1,5 +1,5 @@
1
1
  [tool.bumpversion]
2
- current_version = "1.2.6-dev10"
2
+ current_version = "1.3.0-dev0"
3
3
  parse = """(?x)
4
4
  (?P<major>0|[1-9]\\d*)\\.
5
5
  (?P<minor>0|[1-9]\\d*)\\.
@@ -12,3 +12,10 @@ wheels/
12
12
  temp/*
13
13
  outputs
14
14
  .env
15
+
16
+ # Working notes and scratch probes. Never published.
17
+ .claude/
18
+
19
+ # Run logs
20
+ *.log
21
+ logs/
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: fullwave25
3
- Version: 1.2.6.dev10
3
+ Version: 1.3.0.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
@@ -232,6 +232,41 @@ Here are the main steps to run the Fullwave simulation
232
232
  4. Define the sensor.
233
233
  5. Execute the simulation.
234
234
 
235
+ ## Tutorial: transducers and transmits
236
+
237
+ A named array places itself on the grid, and one call states the transmit.
238
+
239
+ ```python
240
+ grid = fullwave.Grid(domain_size=(4.5e-2, 4.5e-2), f0=5.208e6, duration=6.7e-5, c0=1540)
241
+
242
+ transducer = fullwave.Transducer.l7_4(grid)
243
+ transducer.plane_wave(angle_deg=2.5)
244
+
245
+ solver = fullwave.Solver(work_dir="./outputs/my_run", grid=grid, medium=medium, transducer=transducer)
246
+ recorded = solver.run()
247
+ ```
248
+
249
+ The arrays are `l7_4` (linear, 128 elements, 5.208 MHz), `c5_2v` (curved, 49.57 mm radius, 3.7 MHz) and `p4_1c` (phased, 64 elements). `fullwave.TransducerGeometry` builds any other.
250
+
251
+ The transmits are `plane_wave(angle_deg=...)`, `focus(focus_m=...)`, `diverging(virtual_source_m=...)` and `synthetic_aperture(element)`. Each takes `apodization` and `pulse`. Set `transducer.active_source_elements` first to use part of the aperture.
252
+
253
+ Every transducer is additive by default, which is what the calibrated setups use. Pass `source_type="clamped"` for a hard source.
254
+
255
+ `transducer.apply_transducer_stack(...)` paints a probe's backing, matching layer, lens and standoff onto the medium maps before the `Medium` is built. `fullwave.TransducerStack` holds the values.
256
+
257
+ ### Which example to read
258
+
259
+ | I want to | example |
260
+ | ---------------------------------- | ----------------------------------------------------------------------------------------------------------- |
261
+ | send one plane wave | [simple_plane_wave.py](examples/simple_plane_wave/simple_plane_wave.py) |
262
+ | do the same in 3D | [simple_plane_wave_3d.py](examples/wave_3d/simple_plane_wave_3d.py) |
263
+ | compound plane waves into a B-mode | [plane_wave_compounding.py](examples/linear_transducer/plane_wave_compounding.py) |
264
+ | focus and watch the beam | [linear_transducer_focused_animation.py](examples/linear_transducer/linear_transducer_focused_animation.py) |
265
+ | run a full synthetic aperture | [full_synthetic_aperture.py](examples/linear_transducer/full_synthetic_aperture.py) |
266
+ | do the same on a curved array | [convex_transducer_fsa.py](examples/convex_transducer/convex_transducer_fsa.py) |
267
+ | image through an abdominal wall | [linear_transducer_abdominal_wall.py](examples/linear_transducer/linear_transducer_abdominal_wall.py) |
268
+ | build a layered phantom | [medium_builder_example.py](examples/medium_builder/medium_builder_example.py) |
269
+
235
270
  ## New simulation development instruction
236
271
 
237
272
  - after the [installation](#installation)
@@ -193,6 +193,41 @@ Here are the main steps to run the Fullwave simulation
193
193
  4. Define the sensor.
194
194
  5. Execute the simulation.
195
195
 
196
+ ## Tutorial: transducers and transmits
197
+
198
+ A named array places itself on the grid, and one call states the transmit.
199
+
200
+ ```python
201
+ grid = fullwave.Grid(domain_size=(4.5e-2, 4.5e-2), f0=5.208e6, duration=6.7e-5, c0=1540)
202
+
203
+ transducer = fullwave.Transducer.l7_4(grid)
204
+ transducer.plane_wave(angle_deg=2.5)
205
+
206
+ solver = fullwave.Solver(work_dir="./outputs/my_run", grid=grid, medium=medium, transducer=transducer)
207
+ recorded = solver.run()
208
+ ```
209
+
210
+ The arrays are `l7_4` (linear, 128 elements, 5.208 MHz), `c5_2v` (curved, 49.57 mm radius, 3.7 MHz) and `p4_1c` (phased, 64 elements). `fullwave.TransducerGeometry` builds any other.
211
+
212
+ The transmits are `plane_wave(angle_deg=...)`, `focus(focus_m=...)`, `diverging(virtual_source_m=...)` and `synthetic_aperture(element)`. Each takes `apodization` and `pulse`. Set `transducer.active_source_elements` first to use part of the aperture.
213
+
214
+ Every transducer is additive by default, which is what the calibrated setups use. Pass `source_type="clamped"` for a hard source.
215
+
216
+ `transducer.apply_transducer_stack(...)` paints a probe's backing, matching layer, lens and standoff onto the medium maps before the `Medium` is built. `fullwave.TransducerStack` holds the values.
217
+
218
+ ### Which example to read
219
+
220
+ | I want to | example |
221
+ | ---------------------------------- | ----------------------------------------------------------------------------------------------------------- |
222
+ | send one plane wave | [simple_plane_wave.py](examples/simple_plane_wave/simple_plane_wave.py) |
223
+ | do the same in 3D | [simple_plane_wave_3d.py](examples/wave_3d/simple_plane_wave_3d.py) |
224
+ | compound plane waves into a B-mode | [plane_wave_compounding.py](examples/linear_transducer/plane_wave_compounding.py) |
225
+ | focus and watch the beam | [linear_transducer_focused_animation.py](examples/linear_transducer/linear_transducer_focused_animation.py) |
226
+ | run a full synthetic aperture | [full_synthetic_aperture.py](examples/linear_transducer/full_synthetic_aperture.py) |
227
+ | do the same on a curved array | [convex_transducer_fsa.py](examples/convex_transducer/convex_transducer_fsa.py) |
228
+ | image through an abdominal wall | [linear_transducer_abdominal_wall.py](examples/linear_transducer/linear_transducer_abdominal_wall.py) |
229
+ | build a layered phantom | [medium_builder_example.py](examples/medium_builder/medium_builder_example.py) |
230
+
196
231
  ## New simulation development instruction
197
232
 
198
233
  - after the [installation](#installation)
@@ -9,7 +9,8 @@ from .grid import Grid
9
9
  from .medium import Medium, MediumExponentialAttenuation, MediumRelaxationMaps
10
10
  from .sensor import Sensor
11
11
  from .source import Source
12
- from .transducer import Transducer, TransducerGeometry
12
+ from .transducer import Transducer, TransducerGeometry, TransducerStack
13
+ from .transmit import Pulse
13
14
 
14
15
  from .medium_builder import presets # isort:skip
15
16
 
@@ -35,11 +36,13 @@ __all__ = [
35
36
  "MediumBuilder",
36
37
  "MediumExponentialAttenuation",
37
38
  "MediumRelaxationMaps",
39
+ "Pulse",
38
40
  "Sensor",
39
41
  "Solver",
40
42
  "Source",
41
43
  "Transducer",
42
44
  "TransducerGeometry",
45
+ "TransducerStack",
43
46
  "presets",
44
47
  "utils",
45
48
  ]
@@ -60,7 +63,7 @@ try:
60
63
  __version__ = version("fullwave")
61
64
  except PackageNotFoundError:
62
65
  # Update via bump-my-version, not manually
63
- __version__ = "1.2.6-dev10"
66
+ __version__ = "1.3.0-dev0"
64
67
 
65
68
  VERSION = __version__ # for convenience
66
69
  logger.info("Fullwave version: %s", __version__)
@@ -14,10 +14,14 @@ import numexpr as ne
14
14
  import numpy as np
15
15
 
16
16
  from fullwave import Grid
17
+ from fullwave.solver.shipped_database import ShippedDatabase
17
18
  from fullwave.solver.utils import initialize_relaxation_param_dict
18
19
  from fullwave.utils import check_functions, plot_utils
19
20
  from fullwave.utils.coordinates import coords_to_map, map_to_coords
20
- from fullwave.utils.relaxation_parameters import generate_relaxation_params
21
+ from fullwave.utils.relaxation_parameters import (
22
+ band_scaled_sound_speed,
23
+ generate_relaxation_params,
24
+ )
21
25
 
22
26
  if TYPE_CHECKING:
23
27
  from types import ModuleType
@@ -156,6 +160,87 @@ def _upload_or_convert_arrays(
156
160
  }
157
161
 
158
162
 
163
+ def lossless_value_of(parameter_name: str) -> float:
164
+ """Return what a lossless voxel holds for one relaxation parameter.
165
+
166
+ A stretching factor of 1 and a damping of 0 give a wavenumber of exactly
167
+ omega over c.
168
+
169
+ Parameters
170
+ ----------
171
+ parameter_name : str
172
+ The name of the relaxation parameter.
173
+
174
+ Returns
175
+ -------
176
+ float
177
+ The value a lossless voxel holds.
178
+
179
+ """
180
+ return 1.0 if parameter_name.startswith("kappa") else 0.0
181
+
182
+
183
+ def _make_the_lossless_voxels_lossless(
184
+ relaxation_param_dict: dict[str, NDArray[np.float64]],
185
+ alpha_coeff: NDArray[np.float64] | float,
186
+ ) -> None:
187
+ """Write the lossless values wherever the attenuation coefficient is zero.
188
+
189
+ Parameters
190
+ ----------
191
+ relaxation_param_dict : dict[str, NDArray[np.float64]]
192
+ The parameters, changed in place.
193
+ alpha_coeff : NDArray[np.float64] | float
194
+ The attenuation coefficient of each voxel [dB/(MHz^y cm)].
195
+
196
+ Returns
197
+ -------
198
+ None
199
+
200
+ """
201
+ if not relaxation_param_dict:
202
+ return
203
+ coefficient = np.asarray(alpha_coeff)
204
+ if coefficient.ndim == 0:
205
+ if float(coefficient) != 0.0:
206
+ return
207
+ for name, values in relaxation_param_dict.items():
208
+ values[...] = lossless_value_of(name)
209
+ return
210
+ lossless = coefficient == 0
211
+ if not bool(lossless.any()):
212
+ return
213
+ for name, values in relaxation_param_dict.items():
214
+ values[lossless] = lossless_value_of(name)
215
+
216
+
217
+ def _resolve_lossless_marking(
218
+ alpha_coeff: NDArray[np.float64] | float | None,
219
+ lossless_coords: NDArray[np.int64] | None,
220
+ xp: ModuleType,
221
+ dtype: np.dtype,
222
+ ) -> tuple[NDArray[np.float64] | float | None, NDArray[np.int64] | None]:
223
+ """Return the stored `(alpha_coeff, lossless_coords)` pair.
224
+
225
+ A scalar stays a scalar and coordinates stay coordinates, so neither costs a
226
+ domain-sized array.
227
+ """
228
+ if lossless_coords is not None and np.ndim(alpha_coeff) > 0:
229
+ message = (
230
+ "lossless_coords and an array alpha_coeff both mark the lossless voxels, "
231
+ "so give one or the other"
232
+ )
233
+ raise ValueError(message)
234
+ if alpha_coeff is None:
235
+ stored_coefficient = None
236
+ elif np.ndim(alpha_coeff) == 0:
237
+ stored_coefficient = float(alpha_coeff)
238
+ else:
239
+ stored_coefficient = xp.atleast_2d(xp.asarray(alpha_coeff)).astype(dtype, copy=False)
240
+ stored_coords = None if lossless_coords is None else np.asarray(lossless_coords, dtype=np.int64)
241
+ return stored_coefficient, stored_coords
242
+
243
+
159
244
  @dataclass
160
245
  class MediumRelaxationMaps:
161
246
  """Medium class for Fullwave."""
@@ -167,6 +252,8 @@ class MediumRelaxationMaps:
167
252
  air_coords: NDArray[np.int64]
168
253
  relaxation_param_dict: dict[str, NDArray[np.float64]]
169
254
  relaxation_param_dict_for_fw2: dict[str, NDArray[np.float64]]
255
+ alpha_coeff: NDArray[np.float64] | float | None
256
+ lossless_coords: NDArray[np.int64] | None
170
257
  use_regression: bool = False
171
258
 
172
259
  def __init__(
@@ -177,9 +264,11 @@ class MediumRelaxationMaps:
177
264
  beta: NDArray[np.float64],
178
265
  relaxation_param_dict: dict[str, NDArray[np.float64]],
179
266
  *,
267
+ alpha_coeff: NDArray[np.float64] | float | None = None,
268
+ lossless_coords: NDArray[np.int64] | None = None,
180
269
  air_map: NDArray[np.int64] | None = None,
181
270
  air_coords: NDArray[np.int64] | None = None,
182
- n_relaxation_mechanisms: int = 2,
271
+ n_relaxation_mechanisms: int = ShippedDatabase.mechanisms,
183
272
  use_isotropic_relaxation: bool = True,
184
273
  n_jobs: int = -1,
185
274
  dtype: type = np.float64,
@@ -206,6 +295,16 @@ class MediumRelaxationMaps:
206
295
  key: kappa_x1, kappa_x2, d_x1_nu{i}, alpha_x1_nu{i}, d_x2_nu{i}, alpha_x2_nu{i}
207
296
  value.shape: [nx, ny] for 2D, [nx, ny, nz] for 3D for each value
208
297
  see Pinton, G. (2021) http://arxiv.org/abs/2106.11476 for more detail.
298
+ alpha_coeff : float or NDArray[np.float64], optional
299
+ Attenuation coefficient the relaxation parameters were built for
300
+ [dB/(MHz^y cm)]. Marks a voxel lossless where it is exactly 0, which
301
+ the relaxation maps alone cannot express because the lookup clips a
302
+ smaller request to its minimum. Prefer a scalar, or lossless_coords.
303
+ None leaves the zero-attenuation gate a no-op for this medium.
304
+ lossless_coords : NDArray[np.int64], optional
305
+ Coordinates of the voxels that carry no attenuation, shape
306
+ [n_lossless, ndim], as air_coords does for air. Mutually exclusive
307
+ with an array alpha_coeff.
209
308
  air_map: NDArray[np.int64], optional
210
309
  Binary matrix where the medium is air.
211
310
  shape: [nx, ny] for 2D, [nx, ny, nz] for 3D
@@ -235,8 +334,10 @@ class MediumRelaxationMaps:
235
334
  """
236
335
  check_functions.check_compatible_value(
237
336
  n_relaxation_mechanisms,
238
- [2],
239
- "Only n_relaxation_mechanisms=2 are supported currently.",
337
+ [1, 2, 3, 4, 5, 6],
338
+ "n_relaxation_mechanisms must be between 1 and 6. "
339
+ "A count other than 2 needs the n_relax solver kernel, "
340
+ "which reads the count from n_relax.dat.",
240
341
  )
241
342
  self.use_gpu = use_gpu
242
343
  self.xp: ModuleType = _get_array_module(use_gpu=use_gpu)
@@ -261,6 +362,9 @@ class MediumRelaxationMaps:
261
362
  self.sound_speed = xp.atleast_2d(xp.asarray(sound_speed)).astype(self.dtype, copy=False)
262
363
  self.density = xp.atleast_2d(xp.asarray(density)).astype(self.dtype, copy=False)
263
364
  self.beta = xp.atleast_2d(xp.asarray(beta)).astype(self.dtype, copy=False)
365
+ self.alpha_coeff, self.lossless_coords = _resolve_lossless_marking(
366
+ alpha_coeff, lossless_coords, xp, self.dtype
367
+ )
264
368
  except Exception:
265
369
  if xp is np:
266
370
  raise
@@ -347,6 +451,8 @@ class MediumRelaxationMaps:
347
451
  di, dj = d_arrays[i], d_arrays[j]
348
452
  ai, aj = a_arrays[i], a_arrays[j]
349
453
  swap = ne.evaluate("di / kappa + ai > dj / kappa + aj")
454
+ if not swap.any():
455
+ continue
350
456
  d_arrays[i] = np.where(swap, dj, di)
351
457
  d_arrays[j] = np.where(swap, di, dj)
352
458
  a_arrays[i] = np.where(swap, aj, ai)
@@ -422,7 +528,7 @@ class MediumRelaxationMaps:
422
528
  self,
423
529
  relaxation_param_dict: dict[str, NDArray[np.float64]],
424
530
  contents_shape: NDArray[np.int64] | tuple[int, ...],
425
- n_relaxation_mechanisms: int = 2,
531
+ n_relaxation_mechanisms: int = ShippedDatabase.mechanisms,
426
532
  ) -> None:
427
533
  """Check if the relaxation parameter updates have valid keys and matching shapes.
428
534
 
@@ -499,7 +605,6 @@ class MediumRelaxationMaps:
499
605
  output_dtype: np.dtype | None = None,
500
606
  ) -> tuple[NDArray[np.float64], NDArray[np.float64]]:
501
607
  xp = self.xp
502
- use_gpu = xp is not np
503
608
 
504
609
  dx = xp.asarray(dx, dtype=xp.float64)
505
610
  kappa_x = xp.asarray(kappa_x, dtype=xp.float64)
@@ -507,19 +612,18 @@ class MediumRelaxationMaps:
507
612
  dt = xp.asarray(dt, dtype=xp.float64)
508
613
 
509
614
  eps = xp.finfo(xp.float64).eps
615
+ two_over_dt = 2.0 / dt
510
616
 
511
- if use_gpu:
512
- b = xp.exp(-(dx / kappa_x + alpha_x) * dt)
513
- denom = kappa_x * (dx + kappa_x * alpha_x) + eps
514
- a = dx / denom * (b - 1)
617
+ if xp is np:
618
+ rate = ne.evaluate("dx / kappa_x + alpha_x")
619
+ total = ne.evaluate("two_over_dt + rate")
620
+ b = ne.evaluate("(two_over_dt - rate) / total")
621
+ a = ne.evaluate("-(dx / (kappa_x ** 2 + eps)) / total")
515
622
  else:
516
- eps_local = eps # noqa: F841
517
- # b = exp(-(dx/kappa_x + alpha_x) * dt)
518
- b = ne.evaluate("exp(-(dx/kappa_x + alpha_x) * dt)")
519
- # denom = kappa_x*(dx + kappa_x*alpha_x) + eps
520
- denom = ne.evaluate("kappa_x*(dx + kappa_x*alpha_x) + eps_local")
521
- # a = dx/denom*(b - 1)
522
- a = ne.evaluate("dx/denom*(b - 1)")
623
+ rate = dx / kappa_x + alpha_x
624
+ total = two_over_dt + rate
625
+ b = (two_over_dt - rate) / total
626
+ a = -(dx / (kappa_x**2 + eps)) / total
523
627
 
524
628
  if output_dtype is not None and output_dtype != xp.float64:
525
629
  a = a.astype(output_dtype, copy=False)
@@ -1091,14 +1195,14 @@ class Medium:
1091
1195
  *,
1092
1196
  air_map: NDArray[np.int64] | None = None,
1093
1197
  air_coords: NDArray[np.int64] | None = None,
1094
- path_relaxation_parameters_database: Path = Path(__file__).parent
1095
- / "solver"
1096
- / "bins"
1097
- / "database"
1098
- / "relaxation_params_database_num_relax=2_20260113_0957.mat",
1099
- n_relaxation_mechanisms: int = 2,
1198
+ path_relaxation_parameters_database: Path = ShippedDatabase.table,
1199
+ path_invalid_cells: Path | None = None,
1200
+ n_relaxation_mechanisms: int = ShippedDatabase.mechanisms,
1100
1201
  attenuation_builder: str = "lookup",
1101
1202
  use_isotropic_relaxation: bool = True,
1203
+ sound_speed_transfer: bool = True,
1204
+ band_scale: float = 1.0,
1205
+ scale_to_requested_alpha_coeff: bool = False,
1102
1206
  n_jobs: int = -1,
1103
1207
  dtype: type = np.float64,
1104
1208
  use_gpu: bool = False,
@@ -1135,6 +1239,11 @@ class Medium:
1135
1239
  Mutually exclusive with air_map.
1136
1240
  path_relaxation_parameters_database : Path, optional
1137
1241
  Path to the relaxation parameters database.
1242
+ path_invalid_cells : Path, optional
1243
+ Path to the JSON record an evaluation wrote, which names every
1244
+ invalid cell of the table and why it is invalid. A request that lands
1245
+ on one is warned about, and the lookup still serves it. Without it
1246
+ nothing is marked and the behaviour is unchanged.
1138
1247
  n_relaxation_mechanisms : int, optional
1139
1248
  Number of relaxation mechanisms, by default 4
1140
1249
  attenuation_builder : str, optional
@@ -1148,6 +1257,33 @@ class Medium:
1148
1257
  This option omits the anisotropic relaxation mechanisms to model the attenuation.
1149
1258
  We usually recommend using isotropic relaxation mechanisms
1150
1259
  unless the anisotropic attenuation is required for the simulation.
1260
+ sound_speed_transfer : bool, optional
1261
+ Correct the looked-up relaxation parameters for the medium's sound
1262
+ speed. The table was calibrated at 1540 m/s, where this is the
1263
+ identity. Elsewhere the uncorrected attenuation is wrong by
1264
+ 1540/c, reaching 9.1% at a fat sound speed of 1412 m/s. Set False
1265
+ to reproduce a result produced before this existed.
1266
+ band_scale : float, optional
1267
+ Transfer the calibration to a frequency band scaled by this factor,
1268
+ so 0.1 moves the usable band from 1-20 MHz to 0.1-2 MHz. Give it
1269
+ explicitly rather than deriving it from the transmit frequency,
1270
+ which needs no transfer while it lies inside the calibrated band.
1271
+ A value other than 1.0 also moves the sound speed the built medium
1272
+ carries, by up to 0.5%, because the transfer re-anchors the
1273
+ Kramers-Kronig phase velocity. ``sound_speed`` is then the speed the
1274
+ medium carries at the calibration reference frequency of 5 MHz, and
1275
+ ``MediumRelaxationMaps.sound_speed`` reports the base speed that
1276
+ gives it. See ``band_scaled_sound_speed``.
1277
+ scale_to_requested_alpha_coeff : bool, optional
1278
+ Give the requested attenuation coefficient rather than the
1279
+ calibrated level the lookup serves. The coefficient axis holds
1280
+ 0.0022 and then 0.01 to 1.00 in absolute steps of 0.01, so a request
1281
+ between two levels is served the one above and a request past either
1282
+ end is clipped. Setting this True scales the relaxation departure by
1283
+ the shortfall instead. It matters most under a band transfer, which
1284
+ multiplies the request by a factor and often carries it off the
1285
+ axis. False, the default, reproduces every result produced before
1286
+ this existed.
1151
1287
  n_jobs : int, optional
1152
1288
  Number of parallel jobs for relaxation parameter calculation.
1153
1289
  Default is -1, which uses all available CPUs.
@@ -1163,8 +1299,10 @@ class Medium:
1163
1299
  """
1164
1300
  check_functions.check_compatible_value(
1165
1301
  n_relaxation_mechanisms,
1166
- [2],
1167
- "Only n_relaxation_mechanisms=2 are supported currently.",
1302
+ [1, 2, 3, 4, 5, 6],
1303
+ "n_relaxation_mechanisms must be between 1 and 6. "
1304
+ "A count other than 2 needs the n_relax solver kernel, "
1305
+ "which reads the count from n_relax.dat.",
1168
1306
  )
1169
1307
  check_functions.check_instance(grid, Grid)
1170
1308
  check_functions.check_path_exists(path_relaxation_parameters_database)
@@ -1215,19 +1353,14 @@ class Medium:
1215
1353
  self.air_coords = np.empty((0, ndim), dtype=np.int64)
1216
1354
 
1217
1355
  self.path_relaxation_parameters_database = path_relaxation_parameters_database
1356
+ self.path_invalid_cells = path_invalid_cells
1218
1357
  self.n_relaxation_mechanisms = n_relaxation_mechanisms
1219
1358
  self.use_isotropic_relaxation = use_isotropic_relaxation
1220
1359
 
1221
- if self.n_relaxation_mechanisms != 2 and self.n_air > 0:
1222
- warning_msg = (
1223
- "Warning: Currently, only n_relaxation_mechanisms=2 supports air regions. "
1224
- "Setting air regions to zero for other n_relaxation_mechanisms."
1225
- )
1226
- logger.warning(warning_msg)
1227
- ndim = 3 if self.is_3d else 2
1228
- self.air_coords = np.empty((0, ndim), dtype=np.int64)
1229
-
1230
1360
  self.attenuation_builder = attenuation_builder
1361
+ self.sound_speed_transfer = sound_speed_transfer
1362
+ self.band_scale = band_scale
1363
+ self.scale_to_requested_alpha_coeff = scale_to_requested_alpha_coeff
1231
1364
  self.n_jobs = n_jobs
1232
1365
  self.check_fields()
1233
1366
  logger.debug("Medium instance created.")
@@ -1412,48 +1545,125 @@ class Medium:
1412
1545
 
1413
1546
  # ---
1414
1547
 
1415
- def build(self) -> MediumRelaxationMaps:
1416
- """Retrieve the relaxation parameters from alpha and power maps.
1548
+ def _relaxation_parameters_of(
1549
+ self,
1550
+ sound_speed: NDArray[np.float64],
1551
+ alpha_coeff: NDArray[np.float64],
1552
+ alpha_power: NDArray[np.float64],
1553
+ ) -> dict[str, NDArray[np.float64]]:
1554
+ """Look up the relaxation parameters of the given maps.
1417
1555
 
1418
- it uses the relaxation parameters look up table
1419
- to generate the relaxation parameters.
1556
+ Parameters
1557
+ ----------
1558
+ sound_speed : NDArray[np.float64]
1559
+ The band scaled sound speed the parameters are built at [m/s].
1560
+ alpha_coeff : NDArray[np.float64]
1561
+ Attenuation coefficient [dB/cm/MHz^y].
1562
+ alpha_power : NDArray[np.float64]
1563
+ Attenuation power [-].
1420
1564
 
1421
1565
  Returns
1422
1566
  -------
1423
- MediumRelaxationMaps: An instance of MediumRelaxationMaps
1424
- built from the retrieved relaxation parameters.
1567
+ dict[str, NDArray[np.float64]]
1568
+ One entry for each relaxation parameter, of the shape given.
1425
1569
 
1426
1570
  Raises
1427
1571
  ------
1428
- ValueError: If an unknown attenuation_builder is specified.
1572
+ ValueError
1573
+ If the attenuation builder is not the lookup.
1429
1574
 
1430
1575
  """
1431
- logger.debug("Building MediumRelaxationMaps from alpha and power maps.")
1432
- if self.attenuation_builder == "lookup":
1433
- relaxation_param_dict = generate_relaxation_params(
1434
- n_relaxation_mechanisms=self.n_relaxation_mechanisms,
1435
- alpha_coeff=self.alpha_coeff,
1436
- alpha_power=self.alpha_power,
1437
- path_database=self.path_relaxation_parameters_database,
1438
- )
1439
- else:
1576
+ if self.attenuation_builder != "lookup":
1440
1577
  error_msg = (
1441
1578
  f"Unknown attenuation_builder: {self.attenuation_builder}. "
1442
1579
  'Only "lookup" is supported currently.'
1443
1580
  )
1444
1581
  raise ValueError(error_msg)
1582
+ relaxation_param_dict = generate_relaxation_params(
1583
+ n_relaxation_mechanisms=self.n_relaxation_mechanisms,
1584
+ alpha_coeff=alpha_coeff,
1585
+ alpha_power=alpha_power,
1586
+ path_database=self.path_relaxation_parameters_database,
1587
+ path_invalid_cells=self.path_invalid_cells,
1588
+ band_scale=self.band_scale,
1589
+ sound_speed=sound_speed if self.sound_speed_transfer else None,
1590
+ scale_to_requested_alpha_coeff=self.scale_to_requested_alpha_coeff,
1591
+ )
1592
+ _make_the_lossless_voxels_lossless(relaxation_param_dict, alpha_coeff)
1445
1593
  if self.dtype != np.float64:
1446
1594
  relaxation_param_dict = {
1447
- k: v.astype(self.dtype, copy=False) for k, v in relaxation_param_dict.items()
1595
+ key: value.astype(self.dtype, copy=False)
1596
+ for key, value in relaxation_param_dict.items()
1448
1597
  }
1598
+ return relaxation_param_dict
1599
+
1600
+ def relaxation_parameters_at(
1601
+ self,
1602
+ coords: NDArray[np.int64],
1603
+ ) -> tuple[dict[str, NDArray[np.float64]], NDArray[np.float64]]:
1604
+ """Return the relaxation parameters and the sound speed at the given positions.
1605
+
1606
+ Parameters
1607
+ ----------
1608
+ coords : NDArray[np.int64]
1609
+ Positions, shape [n_positions, ndim].
1610
+
1611
+ Returns
1612
+ -------
1613
+ tuple[dict[str, NDArray[np.float64]], NDArray[np.float64]]
1614
+ One value for each relaxation parameter at each position, and the
1615
+ band scaled sound speed at each position [m/s].
1616
+
1617
+ """
1618
+ index = tuple(np.asarray(coords).T)
1619
+ alpha_coeff = self._to_numpy(self.alpha_coeff[index])
1620
+ alpha_power = self._to_numpy(self.alpha_power[index])
1621
+ sound_speed = band_scaled_sound_speed(
1622
+ self._to_numpy(self.sound_speed[index]),
1623
+ alpha_coeff,
1624
+ alpha_power,
1625
+ self.band_scale,
1626
+ )
1627
+ parameters = self._relaxation_parameters_of(sound_speed, alpha_coeff, alpha_power)
1628
+ return parameters, sound_speed
1629
+
1630
+ def build(self) -> MediumRelaxationMaps:
1631
+ """Retrieve the relaxation parameters from alpha and power maps.
1632
+
1633
+ it uses the relaxation parameters look up table
1634
+ to generate the relaxation parameters.
1635
+
1636
+ Returns
1637
+ -------
1638
+ MediumRelaxationMaps: An instance of MediumRelaxationMaps
1639
+ built from the retrieved relaxation parameters.
1640
+
1641
+ Raises
1642
+ ------
1643
+ ValueError: If an unknown attenuation_builder is specified.
1644
+
1645
+ """
1646
+ logger.debug("Building MediumRelaxationMaps from alpha and power maps.")
1647
+ sound_speed = band_scaled_sound_speed(
1648
+ self.sound_speed,
1649
+ self.alpha_coeff,
1650
+ self.alpha_power,
1651
+ self.band_scale,
1652
+ )
1653
+ relaxation_param_dict = self._relaxation_parameters_of(
1654
+ sound_speed,
1655
+ self.alpha_coeff,
1656
+ self.alpha_power,
1657
+ )
1449
1658
  # Convert relaxation params to GPU if needed (MediumRelaxationMaps will
1450
1659
  # call xp.asarray on them in __init__)
1451
1660
  return MediumRelaxationMaps(
1452
1661
  grid=self.grid,
1453
- sound_speed=self.sound_speed,
1662
+ sound_speed=sound_speed,
1454
1663
  density=self.density,
1455
1664
  beta=self.beta,
1456
1665
  relaxation_param_dict=relaxation_param_dict,
1666
+ alpha_coeff=self.alpha_coeff,
1457
1667
  air_coords=self.air_coords,
1458
1668
  n_relaxation_mechanisms=self.n_relaxation_mechanisms,
1459
1669
  use_isotropic_relaxation=self.use_isotropic_relaxation,
@@ -8,6 +8,7 @@ import numpy as np
8
8
  from numpy.typing import NDArray
9
9
 
10
10
  from fullwave import Grid, Medium
11
+ from fullwave.solver.shipped_database import ShippedDatabase
11
12
  from fullwave.utils import check_functions
12
13
 
13
14
 
@@ -36,12 +37,8 @@ class Domain(ABC):
36
37
  name: str,
37
38
  grid: Grid,
38
39
  *,
39
- path_relaxation_parameters_database: Path = Path(__file__).parent.parent
40
- / "solver"
41
- / "bins"
42
- / "database"
43
- / "relaxation_params_database_num_relax=2_20260113_0957.mat",
44
- n_relaxation_mechanisms: int = 2,
40
+ path_relaxation_parameters_database: Path = ShippedDatabase.table,
41
+ n_relaxation_mechanisms: int = ShippedDatabase.mechanisms,
45
42
  ) -> None:
46
43
  """Initialize the Domain class.
47
44