fullwave25 1.3.0.dev0__py3-none-any.whl → 1.3.2.dev0__py3-none-any.whl
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.
- fullwave/__init__.py +14 -1
- fullwave/medium.py +29 -6
- fullwave/solver/binary_manager.py +1 -1
- fullwave/solver/input_file_writer.py +34 -17
- fullwave/solver/pml_builder.py +94 -37
- fullwave/solver/solver.py +183 -35
- fullwave/transmit.py +20 -4
- fullwave/utils/relaxation_parameters.py +12 -3
- {fullwave25-1.3.0.dev0.dist-info → fullwave25-1.3.2.dev0.dist-info}/METADATA +2 -1
- {fullwave25-1.3.0.dev0.dist-info → fullwave25-1.3.2.dev0.dist-info}/RECORD +12 -12
- {fullwave25-1.3.0.dev0.dist-info → fullwave25-1.3.2.dev0.dist-info}/WHEEL +1 -1
- {fullwave25-1.3.0.dev0.dist-info → fullwave25-1.3.2.dev0.dist-info}/licenses/LICENSE +0 -0
fullwave/__init__.py
CHANGED
|
@@ -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.
|
|
69
|
+
__version__ = "1.3.2-dev0"
|
|
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())
|
fullwave/medium.py
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
408
|
-
|
|
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.
|
|
31
|
+
BINARY_RELEASE_TAG = "fullwave_bin_v1.4.2"
|
|
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 =
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
fullwave/solver/pml_builder.py
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
867
|
-
|
|
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
|
-
|
|
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
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
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
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
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
|
-
|
|
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
|
fullwave/solver/solver.py
CHANGED
|
@@ -1197,11 +1197,21 @@ class Solver:
|
|
|
1197
1197
|
|
|
1198
1198
|
n_source_timesteps = source.icmat.shape[1]
|
|
1199
1199
|
|
|
1200
|
-
|
|
1200
|
+
gib = 1024.0**3
|
|
1201
1201
|
|
|
1202
1202
|
base_depth = depth // n_gpus
|
|
1203
1203
|
remainder = depth % n_gpus
|
|
1204
1204
|
|
|
1205
|
+
box, hole = (None, None)
|
|
1206
|
+
if not self.use_exponential_attenuation:
|
|
1207
|
+
box, hole = self._relaxation_regions(medium)
|
|
1208
|
+
# Every GPU holds every source matrix and every zero pressure coordinate.
|
|
1209
|
+
source_points = sum(
|
|
1210
|
+
getattr(source, name, 0) or 0
|
|
1211
|
+
for name in ("n_sources", "n_sources_add", "n_sources_u", "n_sources_v", "n_sources_w")
|
|
1212
|
+
)
|
|
1213
|
+
body_start = 0
|
|
1214
|
+
|
|
1205
1215
|
for rank, dev_id in enumerate(device_ids):
|
|
1206
1216
|
depth_this = base_depth + (1 if rank < remainder else 0)
|
|
1207
1217
|
|
|
@@ -1213,10 +1223,12 @@ class Solver:
|
|
|
1213
1223
|
n_halo_sides = 2
|
|
1214
1224
|
local_depth = depth_this + n_halo_sides * halo_depth
|
|
1215
1225
|
slab = local_depth * lateral
|
|
1226
|
+
low = max(body_start - (halo_depth if rank > 0 else 0), 0)
|
|
1227
|
+
high = min(body_start + depth_this + (halo_depth if rank < n_gpus - 1 else 0), depth)
|
|
1228
|
+
body_start += depth_this
|
|
1216
1229
|
|
|
1217
1230
|
n_sources = max(source.n_sources // n_gpus, 0)
|
|
1218
1231
|
n_sensors = max(sensor.n_sensors // n_gpus, 0)
|
|
1219
|
-
n_air_local = max(medium.n_air // n_gpus, 0)
|
|
1220
1232
|
|
|
1221
1233
|
if self.use_exponential_attenuation:
|
|
1222
1234
|
total = self._mem_exponential(
|
|
@@ -1231,29 +1243,32 @@ class Solver:
|
|
|
1231
1243
|
is_3d=self.is_3d,
|
|
1232
1244
|
)
|
|
1233
1245
|
else:
|
|
1246
|
+
band, interior = self._band_and_interior(low, high, grid.shape, box, hole)
|
|
1234
1247
|
total = self._mem_relaxation(
|
|
1235
1248
|
slab,
|
|
1236
1249
|
n_deriv_levels,
|
|
1237
|
-
|
|
1250
|
+
source_points,
|
|
1238
1251
|
n_source_timesteps,
|
|
1239
1252
|
save_gpu_memory=self.save_gpu_memory,
|
|
1240
|
-
n_air=
|
|
1253
|
+
n_air=medium.n_air,
|
|
1241
1254
|
n_sensors=n_sensors,
|
|
1242
1255
|
n_relax=self.n_relax_mechanisms,
|
|
1243
1256
|
float_bytes=float_bytes,
|
|
1244
1257
|
int_bytes=int_bytes,
|
|
1245
1258
|
is_3d=self.is_3d,
|
|
1259
|
+
band=band,
|
|
1260
|
+
interior=interior,
|
|
1246
1261
|
)
|
|
1247
1262
|
|
|
1248
1263
|
mode = "exponential" if self.use_exponential_attenuation else "relaxation"
|
|
1249
1264
|
saving = ", save_gpu_memory=True" if self.save_gpu_memory else ""
|
|
1250
1265
|
logger.info(
|
|
1251
1266
|
"GPU memory estimate [GPU %s] (%s mode%s): "
|
|
1252
|
-
"%.2f
|
|
1267
|
+
"%.2f GiB (depth=%d +%d halo, lateral=%d)",
|
|
1253
1268
|
dev_id.strip(),
|
|
1254
1269
|
mode,
|
|
1255
1270
|
saving,
|
|
1256
|
-
total /
|
|
1271
|
+
total / gib,
|
|
1257
1272
|
depth_this,
|
|
1258
1273
|
n_halo_sides * halo_depth,
|
|
1259
1274
|
lateral,
|
|
@@ -1337,8 +1352,20 @@ class Solver:
|
|
|
1337
1352
|
float_bytes: int,
|
|
1338
1353
|
int_bytes: int,
|
|
1339
1354
|
is_3d: bool,
|
|
1355
|
+
band: int | None = None,
|
|
1356
|
+
interior: int | None = None,
|
|
1340
1357
|
) -> int:
|
|
1341
|
-
"""Return estimated GPU bytes for relaxation (power-law) solver.
|
|
1358
|
+
"""Return estimated GPU bytes for the relaxation (power-law) solver.
|
|
1359
|
+
|
|
1360
|
+
The count follows the allocation of the region trimmed kernel, the same
|
|
1361
|
+
count as ``nrelax_device_bytes`` in the solver's ``gpu_memory_nrelax.cuh``.
|
|
1362
|
+
Over the whole slab the card holds the pressure and one velocity for each
|
|
1363
|
+
axis at two time levels, ``kappa_x2``, ``rho``, ``K``, the two finite
|
|
1364
|
+
amplitude maps and ``dcmap``, and mechanism 1 of the pressure memory, its
|
|
1365
|
+
``a``, ``b`` and one memory variable for each axis. The mechanisms above
|
|
1366
|
+
the first hold the same over the interior box alone, and the momentum
|
|
1367
|
+
absorbing layer holds its ``a``, ``b`` and one memory variable for each
|
|
1368
|
+
axis over the band around the hole alone.
|
|
1342
1369
|
|
|
1343
1370
|
Parameters
|
|
1344
1371
|
----------
|
|
@@ -1347,7 +1374,7 @@ class Solver:
|
|
|
1347
1374
|
n_deriv_levels : int
|
|
1348
1375
|
Number of derivative-map levels.
|
|
1349
1376
|
n_sources : int
|
|
1350
|
-
|
|
1377
|
+
Source points this GPU holds, summed over every source kind.
|
|
1351
1378
|
n_source_timesteps : int
|
|
1352
1379
|
Number of source time steps.
|
|
1353
1380
|
save_gpu_memory : bool
|
|
@@ -1362,8 +1389,13 @@ class Solver:
|
|
|
1362
1389
|
Bytes per float (4).
|
|
1363
1390
|
int_bytes : int
|
|
1364
1391
|
Bytes per int (4).
|
|
1365
|
-
is_3d: bool
|
|
1366
|
-
Whether the simulation is 3D
|
|
1392
|
+
is_3d : bool
|
|
1393
|
+
Whether the simulation is 3D.
|
|
1394
|
+
band : int | None
|
|
1395
|
+
Grid points of the momentum layer band on this GPU. None takes the slab.
|
|
1396
|
+
interior : int | None
|
|
1397
|
+
Grid points of this GPU's share of the interior box of the mechanisms
|
|
1398
|
+
above the first. None takes the slab.
|
|
1367
1399
|
|
|
1368
1400
|
Returns
|
|
1369
1401
|
-------
|
|
@@ -1373,36 +1405,152 @@ class Solver:
|
|
|
1373
1405
|
"""
|
|
1374
1406
|
fb = float_bytes
|
|
1375
1407
|
ib = int_bytes
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
mem +=
|
|
1383
|
-
|
|
1384
|
-
mem +=
|
|
1385
|
-
|
|
1386
|
-
mem +=
|
|
1387
|
-
|
|
1388
|
-
mem += 2 * (2 * n_relax) * slab * fb
|
|
1389
|
-
# (dmap + dcmap)
|
|
1390
|
-
mem += 9 * 2 * n_deriv_levels * fb + slab * ib
|
|
1391
|
-
# source (icmat + coords)
|
|
1408
|
+
axes = 3 if is_3d else 2
|
|
1409
|
+
band = slab if band is None else band
|
|
1410
|
+
interior = slab if interior is None else interior
|
|
1411
|
+
per_mechanism = 2 + axes
|
|
1412
|
+
|
|
1413
|
+
mem = 2 * (axes + 1) * slab * fb
|
|
1414
|
+
mem += 5 * slab * fb
|
|
1415
|
+
mem += slab * ib
|
|
1416
|
+
mem += per_mechanism * slab * fb
|
|
1417
|
+
mem += per_mechanism * max(n_relax - 1, 0) * interior * fb
|
|
1418
|
+
mem += per_mechanism * band * fb
|
|
1419
|
+
mem += 9 * 2 * n_deriv_levels * fb
|
|
1392
1420
|
if n_sources > 0:
|
|
1393
|
-
|
|
1394
|
-
mem +=
|
|
1395
|
-
|
|
1396
|
-
# air
|
|
1421
|
+
samples = n_sources if save_gpu_memory else n_sources * n_source_timesteps
|
|
1422
|
+
mem += samples * fb + axes * n_sources * ib
|
|
1397
1423
|
if n_air > 0:
|
|
1398
|
-
mem +=
|
|
1399
|
-
# sensor
|
|
1424
|
+
mem += axes * n_air * ib
|
|
1400
1425
|
if n_sensors > 0:
|
|
1401
|
-
mem += n_sensors * fb
|
|
1402
|
-
mem += (ndim + 1) * n_sensors * ib
|
|
1403
|
-
mem += n_sensors * ib
|
|
1426
|
+
mem += n_sensors * (fb + (axes + 2) * ib)
|
|
1404
1427
|
return mem
|
|
1405
1428
|
|
|
1429
|
+
@staticmethod
|
|
1430
|
+
def _center_line_span(values: NDArray, axis: int, *, zeros: bool) -> tuple[int, int]:
|
|
1431
|
+
"""Return the first and one past the last index of a map's center line along one axis.
|
|
1432
|
+
|
|
1433
|
+
Parameters
|
|
1434
|
+
----------
|
|
1435
|
+
values : NDArray
|
|
1436
|
+
The map, on the host or on the GPU.
|
|
1437
|
+
axis : int
|
|
1438
|
+
The axis the line runs along. The other axes sit at their centers.
|
|
1439
|
+
zeros : bool
|
|
1440
|
+
True spans the zero cells, False the nonzero cells.
|
|
1441
|
+
|
|
1442
|
+
Returns
|
|
1443
|
+
-------
|
|
1444
|
+
tuple[int, int]
|
|
1445
|
+
The span, empty as ``(0, 0)`` when no cell matches.
|
|
1446
|
+
|
|
1447
|
+
"""
|
|
1448
|
+
index = tuple(
|
|
1449
|
+
slice(None) if a == axis else size // 2 for a, size in enumerate(values.shape)
|
|
1450
|
+
)
|
|
1451
|
+
line = values[index]
|
|
1452
|
+
line = line.get() if hasattr(line, "get") else np.asarray(line)
|
|
1453
|
+
found = np.flatnonzero(line == 0 if zeros else line != 0)
|
|
1454
|
+
if found.size == 0:
|
|
1455
|
+
return (0, 0)
|
|
1456
|
+
return (int(found[0]), int(found[-1]) + 1)
|
|
1457
|
+
|
|
1458
|
+
def _relaxation_regions(
|
|
1459
|
+
self,
|
|
1460
|
+
medium: fullwave.MediumRelaxationMaps,
|
|
1461
|
+
) -> tuple[list[tuple[int, int]] | None, list[tuple[int, int]] | None]:
|
|
1462
|
+
"""Return the interior box and the momentum layer hole the kernel will measure.
|
|
1463
|
+
|
|
1464
|
+
The kernel stores every mechanism above the first over the box where its
|
|
1465
|
+
``a`` is nonzero, and the momentum absorbing layer over the band around the
|
|
1466
|
+
box where the first momentum ``a`` is zero. Both are read here along the
|
|
1467
|
+
center lines of each axis, which is how the kernel finds the hole, and a
|
|
1468
|
+
clean box shape is assumed for both.
|
|
1469
|
+
|
|
1470
|
+
Parameters
|
|
1471
|
+
----------
|
|
1472
|
+
medium : fullwave.MediumRelaxationMaps
|
|
1473
|
+
The extended medium after the absorbing layer is built.
|
|
1474
|
+
|
|
1475
|
+
Returns
|
|
1476
|
+
-------
|
|
1477
|
+
tuple
|
|
1478
|
+
The box and the hole as one ``(start, end)`` span for each axis. None
|
|
1479
|
+
stands for a map the medium does not carry, which counts as the whole
|
|
1480
|
+
slab.
|
|
1481
|
+
|
|
1482
|
+
"""
|
|
1483
|
+
maps = medium.relaxation_param_dict_for_fw2
|
|
1484
|
+
box_maps = [maps.get(f"a_pml_u{nu}") for nu in range(2, self.n_relax_mechanisms + 1)]
|
|
1485
|
+
box_maps = [values for values in box_maps if values is not None]
|
|
1486
|
+
box = None
|
|
1487
|
+
if self.n_relax_mechanisms > 1 and len(box_maps) == self.n_relax_mechanisms - 1:
|
|
1488
|
+
box = []
|
|
1489
|
+
for axis in range(box_maps[0].ndim):
|
|
1490
|
+
spans = [self._center_line_span(values, axis, zeros=False) for values in box_maps]
|
|
1491
|
+
spans = [span for span in spans if span[1] > span[0]]
|
|
1492
|
+
box.append(
|
|
1493
|
+
(min(s for s, _ in spans), max(e for _, e in spans)) if spans else (0, 0)
|
|
1494
|
+
)
|
|
1495
|
+
hole = None
|
|
1496
|
+
if maps.get("a_pml_x1") is not None:
|
|
1497
|
+
values = maps["a_pml_x1"]
|
|
1498
|
+
hole = [self._center_line_span(values, axis, zeros=True) for axis in range(values.ndim)]
|
|
1499
|
+
if any(end <= start for start, end in hole):
|
|
1500
|
+
hole = [(0, 0)] * values.ndim
|
|
1501
|
+
return box, hole
|
|
1502
|
+
|
|
1503
|
+
@staticmethod
|
|
1504
|
+
def _band_and_interior(
|
|
1505
|
+
low: int,
|
|
1506
|
+
high: int,
|
|
1507
|
+
shape: tuple[int, ...],
|
|
1508
|
+
box: list[tuple[int, int]] | None,
|
|
1509
|
+
hole: list[tuple[int, int]] | None,
|
|
1510
|
+
) -> tuple[int, int]:
|
|
1511
|
+
"""Return one GPU's band and interior box cells, as the kernel counts them.
|
|
1512
|
+
|
|
1513
|
+
Parameters
|
|
1514
|
+
----------
|
|
1515
|
+
low : int
|
|
1516
|
+
The first grid row the GPU holds, its halo included.
|
|
1517
|
+
high : int
|
|
1518
|
+
One past the last grid row the GPU holds, its halo included.
|
|
1519
|
+
shape : tuple[int, ...]
|
|
1520
|
+
The extended grid shape.
|
|
1521
|
+
box : list[tuple[int, int]] | None
|
|
1522
|
+
The interior box, one span for each axis, or None for the whole slab.
|
|
1523
|
+
hole : list[tuple[int, int]] | None
|
|
1524
|
+
The momentum layer hole, one span for each axis, or None for the whole slab.
|
|
1525
|
+
|
|
1526
|
+
Returns
|
|
1527
|
+
-------
|
|
1528
|
+
tuple[int, int]
|
|
1529
|
+
The band cells and the interior cells of this GPU.
|
|
1530
|
+
|
|
1531
|
+
"""
|
|
1532
|
+
rows = high - low
|
|
1533
|
+
lateral = int(np.prod(shape[1:]))
|
|
1534
|
+
if box is None:
|
|
1535
|
+
interior = rows * lateral
|
|
1536
|
+
else:
|
|
1537
|
+
box_rows = max(0, min(high, box[0][1]) - max(low, box[0][0]))
|
|
1538
|
+
interior = box_rows * int(np.prod([end - start for start, end in box[1:]]))
|
|
1539
|
+
if hole is None:
|
|
1540
|
+
return rows * lateral, interior
|
|
1541
|
+
hole_low = min(max(hole[0][0] - low, 0), rows)
|
|
1542
|
+
hole_high = max(min(max(hole[0][1] - low, 0), rows), hole_low)
|
|
1543
|
+
hole_rows = hole_high - hole_low
|
|
1544
|
+
band = (rows - hole_rows) * lateral
|
|
1545
|
+
hole_columns = hole[1][1] - hole[1][0]
|
|
1546
|
+
strip_columns = shape[1] - hole_columns
|
|
1547
|
+
if len(shape) == 3:
|
|
1548
|
+
strip_depths = shape[2] - (hole[2][1] - hole[2][0])
|
|
1549
|
+
band += hole_rows * (strip_columns * shape[2] + hole_columns * strip_depths)
|
|
1550
|
+
else:
|
|
1551
|
+
band += hole_rows * strip_columns
|
|
1552
|
+
return band, interior
|
|
1553
|
+
|
|
1406
1554
|
def print_info(self) -> None:
|
|
1407
1555
|
"""Print the Solver instance information."""
|
|
1408
1556
|
print(str(self))
|
fullwave/transmit.py
CHANGED
|
@@ -272,12 +272,28 @@ def signal_of(
|
|
|
272
272
|
layers = layer_of_each_pixel(coords, identifiers)
|
|
273
273
|
center_hertz = grid.f0 if pulse.center_hertz is None else pulse.center_hertz
|
|
274
274
|
|
|
275
|
-
|
|
276
|
-
|
|
275
|
+
# Pixels whose element weight, element delay, pixel delay and layer carry the
|
|
276
|
+
# same bits get the same row, so each row is built once, from one of its
|
|
277
|
+
# pixels, with the arguments that pixel gives.
|
|
278
|
+
element_index = identifiers - 1
|
|
279
|
+
columns = [
|
|
280
|
+
np.asarray(weights, dtype=np.float64)[element_index].view(np.uint64),
|
|
281
|
+
np.asarray(delays_s, dtype=np.float64)[element_index].view(np.uint64),
|
|
282
|
+
np.asarray(layers, dtype=np.uint64),
|
|
283
|
+
]
|
|
284
|
+
if pixel_delays_s is not None:
|
|
285
|
+
columns.append(np.ascontiguousarray(pixel_delays_s, dtype=np.float64).view(np.uint64))
|
|
286
|
+
_, first_pixel, row_of_pixel = np.unique(
|
|
287
|
+
np.stack(columns, axis=1), axis=0, return_index=True, return_inverse=True
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
rows = np.zeros((len(first_pixel), grid.nt))
|
|
291
|
+
for row, index in enumerate(first_pixel):
|
|
292
|
+
element = identifiers[index]
|
|
277
293
|
weight = float(weights[element - 1])
|
|
278
294
|
if weight == 0.0:
|
|
279
295
|
continue
|
|
280
|
-
|
|
296
|
+
rows[row] = pulse_utils.gaussian_modulated_sinusoidal_signal(
|
|
281
297
|
nt=grid.nt,
|
|
282
298
|
f0=center_hertz,
|
|
283
299
|
duration=grid.duration,
|
|
@@ -290,4 +306,4 @@ def signal_of(
|
|
|
290
306
|
delay_sec=float(delays_s[element - 1])
|
|
291
307
|
+ (0.0 if pixel_delays_s is None else float(pixel_delays_s[index])),
|
|
292
308
|
)
|
|
293
|
-
return
|
|
309
|
+
return rows[row_of_pixel.reshape(-1)]
|
|
@@ -9,6 +9,7 @@ import time
|
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
|
|
11
11
|
import numba as nb
|
|
12
|
+
import numexpr as ne
|
|
12
13
|
import numpy as np
|
|
13
14
|
from numpy.typing import NDArray
|
|
14
15
|
from scipy.io import loadmat
|
|
@@ -128,10 +129,17 @@ def _first_order_attenuation(
|
|
|
128
129
|
exceeds one and the recursion grows without bound, so that is refused.
|
|
129
130
|
"""
|
|
130
131
|
scaled = dict(relaxation_param_dict)
|
|
132
|
+
on_the_host = xp is np
|
|
131
133
|
for direction in ("x1", "x2"):
|
|
132
134
|
stretching = f"kappa_{direction}"
|
|
133
|
-
|
|
134
|
-
if
|
|
135
|
+
kappa = relaxation_param_dict[stretching]
|
|
136
|
+
if on_the_host:
|
|
137
|
+
moved = ne.evaluate("1.0 + ratio * (kappa - 1.0)")
|
|
138
|
+
collapsed = bool(ne.evaluate("moved <= 0.0").any())
|
|
139
|
+
else:
|
|
140
|
+
moved = 1.0 + ratio * (kappa - 1.0)
|
|
141
|
+
collapsed = bool(xp.any(moved <= 0.0))
|
|
142
|
+
if collapsed:
|
|
135
143
|
error_msg = (
|
|
136
144
|
f"scaling the attenuation by this factor carries {stretching} to "
|
|
137
145
|
f"{float(xp.min(moved)):.6g}, at or below zero, which makes the memory "
|
|
@@ -143,7 +151,8 @@ def _first_order_attenuation(
|
|
|
143
151
|
scaled[stretching] = moved
|
|
144
152
|
for i_relax in range(n_relaxation_mechanisms):
|
|
145
153
|
strength = f"d_{direction}_nu{i_relax + 1}"
|
|
146
|
-
|
|
154
|
+
values = relaxation_param_dict[strength]
|
|
155
|
+
scaled[strength] = ne.evaluate("ratio * values") if on_the_host else ratio * values
|
|
147
156
|
return scaled
|
|
148
157
|
|
|
149
158
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fullwave25
|
|
3
|
-
Version: 1.3.
|
|
3
|
+
Version: 1.3.2-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
|
|
@@ -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
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
fullwave/__init__.py,sha256=
|
|
1
|
+
fullwave/__init__.py,sha256=p-kSxLN7kRdorciAXD-u5Ffx4l8d6XHDpBuHRoRblPk,2382
|
|
2
2
|
fullwave/grid.py,sha256=YbxAAgdYKR8AXwobtTZzHm9C4KR4D5--51zgbs0aQGY,7435
|
|
3
|
-
fullwave/medium.py,sha256=
|
|
3
|
+
fullwave/medium.py,sha256=Tz8Cwda6zLF8UFxjWFkRcAdko_KmF4l1zfmjWaqxu-M,68626
|
|
4
4
|
fullwave/sensor.py,sha256=McHyaSKxM11_Kfb_2hfnOtV9Z1UMBCLkeovncLT8_bA,9308
|
|
5
5
|
fullwave/source.py,sha256=2mJ9djoc_PZAdGbkpJBTPqiF58rxe4YcyUIyxAtjAWA,21143
|
|
6
6
|
fullwave/transducer.py,sha256=NL_GxX4ViYfICeY-mrV1MrkWao_sEhgZWCQ4Lvhpuao,75263
|
|
7
|
-
fullwave/transmit.py,sha256=
|
|
7
|
+
fullwave/transmit.py,sha256=BAD_G-J7Q1pMppXw1yjLrQDUdkhPq-3Q4eNq4jaf7Ss,9693
|
|
8
8
|
fullwave/beamformer/__init__.py,sha256=tNPq_5lfLXfAdueyfQtdTxLTVEV6nEDkeSGAeTf7Q5s,88
|
|
9
9
|
fullwave/beamformer/beamformer.py,sha256=0jE_B5UlxDDBsUcQSHa5d1q3uSTSCyqgNiUJrU10juI,5601
|
|
10
10
|
fullwave/constants/__init__.py,sha256=WOmkhLsSe86D71gwKsa4Tz8QPeOdayLGgvBCh7UEOPk,104
|
|
@@ -21,13 +21,13 @@ fullwave/medium_builder/presets/domain_water_gel.py,sha256=nFiL0CkmDeJWo7aR2Khn0
|
|
|
21
21
|
fullwave/medium_builder/presets/data/.keep,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
22
22
|
fullwave/medium_builder/presets/data/abdominal_wall/i2365f_etfw1.mat,sha256=N3JFFxc9HW1S-NaTPcPlFgXWGIzlrr9X40JbDapTo4g,67348
|
|
23
23
|
fullwave/solver/__init__.py,sha256=E0Jg5akBpicqTjku1puw9rAIoKiw7TwtYNDPiv8G5IY,30
|
|
24
|
-
fullwave/solver/binary_manager.py,sha256=
|
|
24
|
+
fullwave/solver/binary_manager.py,sha256=VhI6VgkdfIwAeX4RkM5S0oHm_RWoo3Ph4BsNlz1vETM,5041
|
|
25
25
|
fullwave/solver/cuda_utils.py,sha256=n7p3TJYbLAXmIzSMS5Q_8m4OWg5U9VG5_3hAxa0uTYI,11407
|
|
26
|
-
fullwave/solver/input_file_writer.py,sha256=
|
|
26
|
+
fullwave/solver/input_file_writer.py,sha256=JamQHuA3-YrdLpdMFT6Ymw7noiCrtMr-DNo6LFxCvmM,57993
|
|
27
27
|
fullwave/solver/launcher.py,sha256=zh-3HY4iKGmjreALu9WczDpWX3d74nOcIns9xVvnUIg,10531
|
|
28
|
-
fullwave/solver/pml_builder.py,sha256=
|
|
28
|
+
fullwave/solver/pml_builder.py,sha256=Pfy3AklZT-p6TX61A5ntFmpt2mJb5TDaV-gdtpjIwNo,87341
|
|
29
29
|
fullwave/solver/shipped_database.py,sha256=lPqlgY-ShDyRqtH65m5RyNIMVlqswECVDNh5XCk1Yzg,3194
|
|
30
|
-
fullwave/solver/solver.py,sha256=
|
|
30
|
+
fullwave/solver/solver.py,sha256=oQOrL-9YSi5C9vP6rOyfZW8soTHnWBLusi1rMIwI978,65346
|
|
31
31
|
fullwave/solver/source_type.py,sha256=li4-4tpu4bd1i9DqyDXZKOhkzW4IV6OcxB97C13c5MU,11180
|
|
32
32
|
fullwave/solver/utils.py,sha256=0Q9PKl-4hohWXGzuldqo2XubI-LEHMjs0kYgb4oTLC0,5561
|
|
33
33
|
fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda118,sha256=bIzyHFzr_bsIBSlCcRA3M9LlTSRrBVn6ASJHyozvcwU,1245432
|
|
@@ -49,11 +49,11 @@ fullwave/utils/memory_tempfile.py,sha256=BzMRNvINzLYdtV8NcSNjrTr2F9Qow8vYl1o7RUX
|
|
|
49
49
|
fullwave/utils/numerical.py,sha256=khdi2KE3y_Ky9cEnmGXvVHjeYuKfb-8gQTV1a8nx9uA,3260
|
|
50
50
|
fullwave/utils/plot_utils.py,sha256=SoByd2fEr3VdvMM9D2kNq8VgaAQ-fh82bDGRFdeq2R8,39327
|
|
51
51
|
fullwave/utils/pulse.py,sha256=aqnCdBxivACouIGlot1aE2k4V9nctUxNCEPzdBpsupg,2811
|
|
52
|
-
fullwave/utils/relaxation_parameters.py,sha256=
|
|
52
|
+
fullwave/utils/relaxation_parameters.py,sha256=wHUNMtJGAJwgCyUYd24_qtQZflMnkQTYhxSJwjE3vA4,46151
|
|
53
53
|
fullwave/utils/scatterer.py,sha256=CLqD085f9pqX4cF1YiT2IkzT5P0o1xp45Do7lf5BLnM,24049
|
|
54
54
|
fullwave/utils/signal_filter.py,sha256=_Tf42aBygGtIUrcSPybYqvZoAW2o_4dd-hHTEvZ8--E,5219
|
|
55
55
|
fullwave/utils/signal_process.py,sha256=MP38r37JSPHN4sT-lIReXS5BT2GT_tkRxD0Y1wQ-lNs,5153
|
|
56
|
-
fullwave25-1.3.
|
|
57
|
-
fullwave25-1.3.
|
|
58
|
-
fullwave25-1.3.
|
|
59
|
-
fullwave25-1.3.
|
|
56
|
+
fullwave25-1.3.2.dev0.dist-info/METADATA,sha256=wYeq9o1hmkqstMoW0B6zQrbOUQnQAIDBVbhkqGwHJKM,21747
|
|
57
|
+
fullwave25-1.3.2.dev0.dist-info/WHEEL,sha256=6zicbvgMWfHSD_sSydPAPL1UNY8-QHIQA4J8fOxyAc8,87
|
|
58
|
+
fullwave25-1.3.2.dev0.dist-info/licenses/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
|
|
59
|
+
fullwave25-1.3.2.dev0.dist-info/RECORD,,
|
|
File without changes
|