fullwave25 1.4.1.dev2__py3-none-any.whl → 1.4.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 +1 -1
- fullwave/solver/binary_manager.py +1 -1
- fullwave/solver/input_file_writer.py +45 -3
- fullwave/solver/launcher.py +21 -0
- fullwave/solver/solver.py +714 -41
- {fullwave25-1.4.1.dev2.dist-info → fullwave25-1.4.2.dev0.dist-info}/METADATA +21 -1
- {fullwave25-1.4.1.dev2.dist-info → fullwave25-1.4.2.dev0.dist-info}/RECORD +9 -9
- {fullwave25-1.4.1.dev2.dist-info → fullwave25-1.4.2.dev0.dist-info}/WHEEL +0 -0
- {fullwave25-1.4.1.dev2.dist-info → fullwave25-1.4.2.dev0.dist-info}/licenses/LICENSE +0 -0
fullwave/__init__.py
CHANGED
|
@@ -66,7 +66,7 @@ try:
|
|
|
66
66
|
__version__ = version("fullwave")
|
|
67
67
|
except PackageNotFoundError:
|
|
68
68
|
# Update via bump-my-version, not manually
|
|
69
|
-
__version__ = "1.4.
|
|
69
|
+
__version__ = "1.4.2-dev0"
|
|
70
70
|
|
|
71
71
|
VERSION = __version__ # for convenience
|
|
72
72
|
logger.info("Fullwave version: %s", __version__)
|
|
@@ -28,7 +28,7 @@ CACHE_DIR = Path.home() / ".cache" / "fullwave25" / "bins"
|
|
|
28
28
|
|
|
29
29
|
# Pinned release tag for the solver binaries.
|
|
30
30
|
# Update this only when new binaries are uploaded to a GitHub release.
|
|
31
|
-
BINARY_RELEASE_TAG = "fullwave_bin_v1.
|
|
31
|
+
BINARY_RELEASE_TAG = "fullwave_bin_v1.6"
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def _download_url(filename: str, tag: str) -> str:
|
|
@@ -4,6 +4,7 @@ import concurrent.futures
|
|
|
4
4
|
import logging
|
|
5
5
|
import os
|
|
6
6
|
import time
|
|
7
|
+
from collections.abc import Sequence
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
|
|
9
10
|
import numpy as np
|
|
@@ -202,6 +203,7 @@ class InputFileWriter:
|
|
|
202
203
|
*,
|
|
203
204
|
is_static_map: bool = False,
|
|
204
205
|
recalculate_pml: bool = True,
|
|
206
|
+
members: Sequence[fullwave.Source] | None = None,
|
|
205
207
|
) -> Path:
|
|
206
208
|
r"""Run the input data generation and return the simulation directory path.
|
|
207
209
|
|
|
@@ -230,6 +232,11 @@ class InputFileWriter:
|
|
|
230
232
|
set False if you are using the same PML parameters for each transmit event
|
|
231
233
|
such as plane wave
|
|
232
234
|
AND this is the second or later transmit event.
|
|
235
|
+
members : Sequence[fullwave.Source] | None
|
|
236
|
+
The transmits of a batch run, in order. Each member gives its own signals and
|
|
237
|
+
shares the coordinates of ``source``. The signals of all members are written one
|
|
238
|
+
after another, and nsims.dat holds the member count. None writes a single
|
|
239
|
+
transmit from ``source``.
|
|
233
240
|
|
|
234
241
|
Returns
|
|
235
242
|
-------
|
|
@@ -243,11 +250,19 @@ class InputFileWriter:
|
|
|
243
250
|
|
|
244
251
|
self._init_pending_writes()
|
|
245
252
|
|
|
253
|
+
member_count_file = simulation_dir / "nsims.dat"
|
|
254
|
+
if members is None:
|
|
255
|
+
# A file left by an earlier batch run in this directory would turn this run into
|
|
256
|
+
# a batch.
|
|
257
|
+
member_count_file.unlink(missing_ok=True)
|
|
258
|
+
else:
|
|
259
|
+
self._queue_v_abs_write(np.int32, member_count_file, len(members))
|
|
260
|
+
|
|
246
261
|
self._queue_ic_write(
|
|
247
262
|
simulation_dir / "icmat.dat",
|
|
248
|
-
np.transpose(self.
|
|
263
|
+
np.transpose(self._signals("icmat", members)),
|
|
249
264
|
)
|
|
250
|
-
p0_additive =
|
|
265
|
+
p0_additive = self._signals("p0_additive", members)
|
|
251
266
|
incoords_add = getattr(self.source, "incoords_add", None)
|
|
252
267
|
if p0_additive is not None:
|
|
253
268
|
self._queue_ic_write(
|
|
@@ -257,7 +272,7 @@ class InputFileWriter:
|
|
|
257
272
|
if incoords_add is not None:
|
|
258
273
|
self._queue_coords_write(simulation_dir / "icc_add.dat", incoords_add)
|
|
259
274
|
for _vel_suffix, _vel_attr in (("u", "u0"), ("v", "v0"), ("w", "w0")):
|
|
260
|
-
_signal_vel =
|
|
275
|
+
_signal_vel = self._signals(_vel_attr, members)
|
|
261
276
|
_incoords_vel = getattr(self.source, f"incoords_{_vel_suffix}", None)
|
|
262
277
|
if _signal_vel is not None:
|
|
263
278
|
self._queue_ic_write(
|
|
@@ -310,6 +325,33 @@ class InputFileWriter:
|
|
|
310
325
|
logger.info(message)
|
|
311
326
|
return simulation_dir
|
|
312
327
|
|
|
328
|
+
def _signals(
|
|
329
|
+
self,
|
|
330
|
+
name: str,
|
|
331
|
+
members: Sequence[fullwave.Source] | None,
|
|
332
|
+
) -> np.ndarray | None:
|
|
333
|
+
"""Return one source signal matrix, with the rows of every member stacked in order.
|
|
334
|
+
|
|
335
|
+
Parameters
|
|
336
|
+
----------
|
|
337
|
+
name : str
|
|
338
|
+
The signal attribute of fullwave.Source: icmat, p0_additive, u0, v0 or w0.
|
|
339
|
+
members : Sequence[fullwave.Source] | None
|
|
340
|
+
The transmits of a batch run, or None for the single transmit of ``source``.
|
|
341
|
+
|
|
342
|
+
Returns
|
|
343
|
+
-------
|
|
344
|
+
np.ndarray | None
|
|
345
|
+
[n_points, n_t] for a single transmit, [n_members * n_points, n_t] for a batch,
|
|
346
|
+
or None when the source has no signal of this kind.
|
|
347
|
+
|
|
348
|
+
"""
|
|
349
|
+
if members is None:
|
|
350
|
+
return getattr(self.source, name, None)
|
|
351
|
+
if getattr(self.source, name, None) is None:
|
|
352
|
+
return None
|
|
353
|
+
return np.concatenate([getattr(member, name) for member in members], axis=0)
|
|
354
|
+
|
|
313
355
|
# --- constructor utils ---
|
|
314
356
|
|
|
315
357
|
def _set_d_mat(self) -> None:
|
fullwave/solver/launcher.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""Module for launching Fullwave simulation."""
|
|
2
2
|
|
|
3
|
+
import copy
|
|
3
4
|
import ctypes
|
|
4
5
|
import logging
|
|
5
6
|
import os
|
|
@@ -85,6 +86,26 @@ class Launcher:
|
|
|
85
86
|
self.save_gpu_memory = save_gpu_memory
|
|
86
87
|
logger.debug("Launcher instance created.")
|
|
87
88
|
|
|
89
|
+
def with_binary(self, path_fullwave_simulation_bin: Path) -> "Launcher":
|
|
90
|
+
"""Return a copy of this launcher that runs another binary.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
path_fullwave_simulation_bin : Path
|
|
95
|
+
The binary of the copy.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
Launcher
|
|
100
|
+
The copy, with the same devices and memory settings.
|
|
101
|
+
|
|
102
|
+
"""
|
|
103
|
+
error_msg = f"Fullwave simulation binary not found at {path_fullwave_simulation_bin}"
|
|
104
|
+
assert path_fullwave_simulation_bin.exists(), error_msg
|
|
105
|
+
launcher = copy.copy(self)
|
|
106
|
+
launcher._path_fullwave_simulation_bin = path_fullwave_simulation_bin
|
|
107
|
+
return launcher
|
|
108
|
+
|
|
88
109
|
@staticmethod
|
|
89
110
|
def _parse_cuda_device_id(cuda_device_id: str | int | list | None) -> str:
|
|
90
111
|
"""Parse the CUDA device ID input into a string format.
|
fullwave/solver/solver.py
CHANGED
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
"""solver module."""
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
import gc
|
|
4
5
|
import logging
|
|
6
|
+
import os
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
5
9
|
import time
|
|
10
|
+
from collections.abc import Iterator, Sequence
|
|
6
11
|
from pathlib import Path
|
|
7
12
|
|
|
8
13
|
import numpy as np
|
|
@@ -26,9 +31,84 @@ from fullwave.utils.signal_filter import apply_filter
|
|
|
26
31
|
|
|
27
32
|
from .binary_manager import ensure_binary
|
|
28
33
|
from .cuda_utils import get_cuda_architecture, retrieve_cuda_version
|
|
34
|
+
from .utils import load_dat_data
|
|
29
35
|
|
|
30
36
|
logger = logging.getLogger("__main__." + __name__)
|
|
31
37
|
|
|
38
|
+
# The default largest number of transmits in one batch simulation. In a 2D L7-4 abdomen test
|
|
39
|
+
# (2489 x 1136 cells) the time per transmit stopped falling at 16 to 32 transmits and rose above
|
|
40
|
+
# that, so a larger run only costs memory.
|
|
41
|
+
DEFAULT_MAX_MEMBERS_PER_RUN = 32
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _split_evenly(members: list, largest: int) -> list[list]:
|
|
45
|
+
"""Split the transmits into the fewest runs of at most ``largest``, of nearly equal size.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
members : list
|
|
50
|
+
The transmits, in order.
|
|
51
|
+
largest : int
|
|
52
|
+
The largest number of transmits in one run.
|
|
53
|
+
|
|
54
|
+
Returns
|
|
55
|
+
-------
|
|
56
|
+
list[list]
|
|
57
|
+
The runs, in order. Their sizes differ by at most one, the larger ones first.
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
n_runs = -(-len(members) // largest)
|
|
61
|
+
base, extra = divmod(len(members), n_runs)
|
|
62
|
+
runs, first = [], 0
|
|
63
|
+
for index in range(n_runs):
|
|
64
|
+
size = base + (1 if index < extra else 0)
|
|
65
|
+
runs.append(members[first : first + size])
|
|
66
|
+
first += size
|
|
67
|
+
return runs
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _free_gpu_memory_bytes(device_ids: Sequence[str]) -> dict[str, int] | None:
|
|
71
|
+
"""Return the free memory of each GPU in bytes, as nvidia-smi reports it.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
device_ids : Sequence[str]
|
|
76
|
+
The GPU indices, as nvidia-smi counts them.
|
|
77
|
+
|
|
78
|
+
Returns
|
|
79
|
+
-------
|
|
80
|
+
dict[str, int] | None
|
|
81
|
+
The free bytes of each GPU, or None when nvidia-smi cannot be run or does not
|
|
82
|
+
report every GPU.
|
|
83
|
+
|
|
84
|
+
"""
|
|
85
|
+
wanted = [device.strip() for device in device_ids]
|
|
86
|
+
nvidia_smi_path = shutil.which("nvidia-smi")
|
|
87
|
+
if nvidia_smi_path is None:
|
|
88
|
+
return None
|
|
89
|
+
try:
|
|
90
|
+
found = subprocess.run( # noqa: S603
|
|
91
|
+
[nvidia_smi_path, "--query-gpu=index,memory.free", "--format=csv,noheader,nounits"],
|
|
92
|
+
capture_output=True,
|
|
93
|
+
text=True,
|
|
94
|
+
check=True,
|
|
95
|
+
timeout=30,
|
|
96
|
+
).stdout
|
|
97
|
+
except (OSError, subprocess.SubprocessError):
|
|
98
|
+
return None
|
|
99
|
+
free = {}
|
|
100
|
+
for line in found.splitlines():
|
|
101
|
+
index, _, mebibytes = line.partition(",")
|
|
102
|
+
if index.strip() in wanted:
|
|
103
|
+
try:
|
|
104
|
+
free[index.strip()] = int(float(mebibytes)) * 1024**2
|
|
105
|
+
except ValueError:
|
|
106
|
+
return None
|
|
107
|
+
if set(free) != set(wanted):
|
|
108
|
+
return None
|
|
109
|
+
return free
|
|
110
|
+
|
|
111
|
+
|
|
32
112
|
COMPATIBLE_CUDA_ARCHITECTURES = [
|
|
33
113
|
"sm_50", # Maxwell: GTX 750, GTX 750 Ti
|
|
34
114
|
"sm_52", # Maxwell: GTX 980, GTX 970
|
|
@@ -230,6 +310,7 @@ def _relaxation_binary_path(
|
|
|
230
310
|
dimension: str,
|
|
231
311
|
cuda_version_option: str,
|
|
232
312
|
isotropic_str: str = "",
|
|
313
|
+
batch_str: str = "",
|
|
233
314
|
) -> Path:
|
|
234
315
|
"""Return where the relaxation solver binary sits.
|
|
235
316
|
|
|
@@ -244,6 +325,9 @@ def _relaxation_binary_path(
|
|
|
244
325
|
The CUDA tag the binary name ends with, such as "cuda124".
|
|
245
326
|
isotropic_str : str, optional
|
|
246
327
|
The anisotropy tag the name carries, empty for the isotropic solver.
|
|
328
|
+
batch_str : str, optional
|
|
329
|
+
"_batch" for the binary that runs several transmits in one simulation,
|
|
330
|
+
empty otherwise.
|
|
247
331
|
|
|
248
332
|
Returns
|
|
249
333
|
-------
|
|
@@ -252,7 +336,40 @@ def _relaxation_binary_path(
|
|
|
252
336
|
|
|
253
337
|
"""
|
|
254
338
|
root = Path(__file__).parent / "bins" / "gpu" / dimension
|
|
255
|
-
|
|
339
|
+
model = f"n_relax{isotropic_str}_multi_gpu{batch_str}"
|
|
340
|
+
return root / f"fullwave2_{dimension}_{model}_{cuda_version_option}"
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _batch_suffix(*, batch: bool, is_3d: bool, use_gpu: bool) -> str:
|
|
344
|
+
"""Return the tag that the name of a batch binary carries, or "" for other binaries.
|
|
345
|
+
|
|
346
|
+
Parameters
|
|
347
|
+
----------
|
|
348
|
+
batch : bool
|
|
349
|
+
Whether the binary runs several transmits in one simulation.
|
|
350
|
+
is_3d : bool
|
|
351
|
+
Whether the simulation is 3D.
|
|
352
|
+
use_gpu : bool
|
|
353
|
+
Whether the simulation runs on GPUs.
|
|
354
|
+
|
|
355
|
+
Returns
|
|
356
|
+
-------
|
|
357
|
+
str
|
|
358
|
+
"_batch" or "".
|
|
359
|
+
|
|
360
|
+
Raises
|
|
361
|
+
------
|
|
362
|
+
NotImplementedError
|
|
363
|
+
For a batch binary of a 3D or CPU simulation, which does not exist.
|
|
364
|
+
|
|
365
|
+
"""
|
|
366
|
+
if not batch:
|
|
367
|
+
return ""
|
|
368
|
+
if is_3d or not use_gpu:
|
|
369
|
+
error_msg = "The batch binaries support 2D GPU simulations only."
|
|
370
|
+
logger.error(error_msg)
|
|
371
|
+
raise NotImplementedError(error_msg)
|
|
372
|
+
return "_batch"
|
|
256
373
|
|
|
257
374
|
|
|
258
375
|
def _retrieve_fullwave_simulation_path(
|
|
@@ -261,6 +378,7 @@ def _retrieve_fullwave_simulation_path(
|
|
|
261
378
|
is_3d: bool = False,
|
|
262
379
|
use_exponential_attenuation: bool = False,
|
|
263
380
|
use_isotropic_relaxation: bool = True,
|
|
381
|
+
batch: bool = False,
|
|
264
382
|
) -> Path:
|
|
265
383
|
arch_option = _make_cuda_arch_option(use_gpu=use_gpu)
|
|
266
384
|
cuda_version_option, cuda_version = _make_cuda_version_option(
|
|
@@ -276,6 +394,7 @@ def _retrieve_fullwave_simulation_path(
|
|
|
276
394
|
|
|
277
395
|
# isotropic_str = "_isotropic" if use_isotropic_relaxation else ""
|
|
278
396
|
isotropic_str = ""
|
|
397
|
+
batch_str = _batch_suffix(batch=batch, is_3d=is_3d, use_gpu=use_gpu)
|
|
279
398
|
|
|
280
399
|
if use_gpu and not _check_compatible_set(
|
|
281
400
|
cuda_version=cuda_version,
|
|
@@ -301,7 +420,7 @@ def _retrieve_fullwave_simulation_path(
|
|
|
301
420
|
/ "exponential_attenuation"
|
|
302
421
|
/ "gpu"
|
|
303
422
|
/ "2d"
|
|
304
|
-
/ f"
|
|
423
|
+
/ f"fullwave2_2d_exponential_attenuation_multi_gpu{batch_str}_{cuda_version_option}"
|
|
305
424
|
)
|
|
306
425
|
else:
|
|
307
426
|
error_msg = (
|
|
@@ -333,6 +452,7 @@ def _retrieve_fullwave_simulation_path(
|
|
|
333
452
|
dimension="2d",
|
|
334
453
|
cuda_version_option=cuda_version_option,
|
|
335
454
|
isotropic_str=isotropic_str,
|
|
455
|
+
batch_str=batch_str,
|
|
336
456
|
)
|
|
337
457
|
else:
|
|
338
458
|
path_fullwave_simulation_bin = (
|
|
@@ -582,6 +702,8 @@ class Solver:
|
|
|
582
702
|
|
|
583
703
|
self.n_relax_mechanisms = medium.n_relaxation_mechanisms
|
|
584
704
|
|
|
705
|
+
# run_batch picks the batch binary itself only when the user did not pick one.
|
|
706
|
+
self._uses_default_binary = path_fullwave_simulation_bin is None
|
|
585
707
|
if path_fullwave_simulation_bin is None:
|
|
586
708
|
local_path = _retrieve_fullwave_simulation_path(
|
|
587
709
|
use_gpu=use_gpu,
|
|
@@ -657,6 +779,7 @@ class Solver:
|
|
|
657
779
|
error_msg = f"source_type {source_type!r} is not one of {SOURCE_TYPES}"
|
|
658
780
|
logger.error(error_msg)
|
|
659
781
|
raise ValueError(error_msg)
|
|
782
|
+
self.source_type = source_type
|
|
660
783
|
if is_soft(source_type) and self.source.p0_additive is not None:
|
|
661
784
|
error_msg = (
|
|
662
785
|
"this source already carries an additive signal. source_type='soft' "
|
|
@@ -1046,6 +1169,43 @@ class Solver:
|
|
|
1046
1169
|
Also raised if both ``highpass_cutoff_mhz`` and ``bandpass_cutoff_mhz`` are given,
|
|
1047
1170
|
or if either filter option is set but ``load_results=False``.
|
|
1048
1171
|
|
|
1172
|
+
"""
|
|
1173
|
+
return self._run_one(
|
|
1174
|
+
simulation_dir_name,
|
|
1175
|
+
members=None,
|
|
1176
|
+
is_static_map=is_static_map,
|
|
1177
|
+
recalculate_pml=recalculate_pml,
|
|
1178
|
+
record_whole_domain=record_whole_domain,
|
|
1179
|
+
sampling_modulus_time_whole_domain=sampling_modulus_time_whole_domain,
|
|
1180
|
+
load_results=load_results,
|
|
1181
|
+
generate_input_only=generate_input_only,
|
|
1182
|
+
release_after_write=release_after_write,
|
|
1183
|
+
highpass_cutoff_mhz=highpass_cutoff_mhz,
|
|
1184
|
+
bandpass_cutoff_mhz=bandpass_cutoff_mhz,
|
|
1185
|
+
gpu_memory_estimate=gpu_memory_estimate,
|
|
1186
|
+
)
|
|
1187
|
+
|
|
1188
|
+
def _run_one(
|
|
1189
|
+
self,
|
|
1190
|
+
simulation_dir_name: str | Path,
|
|
1191
|
+
*,
|
|
1192
|
+
members: list[fullwave.Source] | None,
|
|
1193
|
+
is_static_map: bool,
|
|
1194
|
+
recalculate_pml: bool,
|
|
1195
|
+
record_whole_domain: bool,
|
|
1196
|
+
sampling_modulus_time_whole_domain: int,
|
|
1197
|
+
load_results: bool,
|
|
1198
|
+
generate_input_only: bool,
|
|
1199
|
+
release_after_write: bool,
|
|
1200
|
+
highpass_cutoff_mhz: float | None,
|
|
1201
|
+
bandpass_cutoff_mhz: tuple[float, float] | None,
|
|
1202
|
+
gpu_memory_estimate: bool,
|
|
1203
|
+
) -> NDArray[np.float64] | Path | list[Path]:
|
|
1204
|
+
"""Write the inputs of one simulation, run it, and return its output.
|
|
1205
|
+
|
|
1206
|
+
``members`` None runs the single transmit of ``self.source``, as ``run`` documents.
|
|
1207
|
+
A list runs those transmits in one simulation and returns
|
|
1208
|
+
[n_members, n_sensors, n_t] (or the output paths, in member order).
|
|
1049
1209
|
"""
|
|
1050
1210
|
# self._save_data_for_beamforming()
|
|
1051
1211
|
|
|
@@ -1099,20 +1259,10 @@ class Solver:
|
|
|
1099
1259
|
)
|
|
1100
1260
|
logger.warning(warning_msg)
|
|
1101
1261
|
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
mod_z = 1 if self.is_3d else 0
|
|
1107
|
-
|
|
1108
|
-
sensor = fullwave.Sensor(
|
|
1109
|
-
mod_x=mod_x,
|
|
1110
|
-
mod_y=mod_y,
|
|
1111
|
-
mod_z=mod_z,
|
|
1112
|
-
sampling_modulus_time=sampling_modulus_time_whole_domain,
|
|
1113
|
-
)
|
|
1114
|
-
else:
|
|
1115
|
-
sensor = self.pml_builder.extended_sensor
|
|
1262
|
+
sensor = self._output_sensor(
|
|
1263
|
+
record_whole_domain=record_whole_domain,
|
|
1264
|
+
sampling_modulus_time_whole_domain=sampling_modulus_time_whole_domain,
|
|
1265
|
+
)
|
|
1116
1266
|
|
|
1117
1267
|
# pml_thickness = PML + transition layers on each side, excluding ghost cells.
|
|
1118
1268
|
# Used by the binary to locate the interior domain when building a sparse sensor grid.
|
|
@@ -1143,6 +1293,7 @@ class Solver:
|
|
|
1143
1293
|
simulation_dir_name,
|
|
1144
1294
|
is_static_map=is_static_map,
|
|
1145
1295
|
recalculate_pml=recalculate_pml,
|
|
1296
|
+
members=members,
|
|
1146
1297
|
)
|
|
1147
1298
|
end_input_file_writer_time = time.time()
|
|
1148
1299
|
message = (
|
|
@@ -1152,7 +1303,7 @@ class Solver:
|
|
|
1152
1303
|
logger.debug(message)
|
|
1153
1304
|
|
|
1154
1305
|
if gpu_memory_estimate:
|
|
1155
|
-
self._estimate_gpu_memory(sensor)
|
|
1306
|
+
self._estimate_gpu_memory(sensor, n_members=1 if members is None else len(members))
|
|
1156
1307
|
|
|
1157
1308
|
if generate_input_only:
|
|
1158
1309
|
logger.info(
|
|
@@ -1164,6 +1315,16 @@ class Solver:
|
|
|
1164
1315
|
|
|
1165
1316
|
self._release_gpu_memory_pools()
|
|
1166
1317
|
|
|
1318
|
+
if members is not None:
|
|
1319
|
+
return self._run_members(
|
|
1320
|
+
simulation_dir,
|
|
1321
|
+
len(members),
|
|
1322
|
+
sensor,
|
|
1323
|
+
load_results=load_results,
|
|
1324
|
+
highpass_cutoff_mhz=highpass_cutoff_mhz,
|
|
1325
|
+
bandpass_cutoff_mhz=bandpass_cutoff_mhz,
|
|
1326
|
+
)
|
|
1327
|
+
|
|
1167
1328
|
sim_result = self.fullwave_launcher.run(
|
|
1168
1329
|
simulation_dir,
|
|
1169
1330
|
load_results=load_results,
|
|
@@ -1195,9 +1356,475 @@ class Solver:
|
|
|
1195
1356
|
# which is a list of file names
|
|
1196
1357
|
return sim_result
|
|
1197
1358
|
|
|
1359
|
+
def _output_sensor(
|
|
1360
|
+
self,
|
|
1361
|
+
*,
|
|
1362
|
+
record_whole_domain: bool,
|
|
1363
|
+
sampling_modulus_time_whole_domain: int,
|
|
1364
|
+
) -> fullwave.Sensor:
|
|
1365
|
+
"""Return the sensor written to the input files.
|
|
1366
|
+
|
|
1367
|
+
Parameters
|
|
1368
|
+
----------
|
|
1369
|
+
record_whole_domain : bool
|
|
1370
|
+
True records the whole domain on a sparse grid instead of the sensor.
|
|
1371
|
+
sampling_modulus_time_whole_domain : int
|
|
1372
|
+
The time sampling modulus of the whole-domain recording.
|
|
1373
|
+
|
|
1374
|
+
Returns
|
|
1375
|
+
-------
|
|
1376
|
+
fullwave.Sensor
|
|
1377
|
+
The whole-domain sensor, or the sensor on the extended grid.
|
|
1378
|
+
|
|
1379
|
+
"""
|
|
1380
|
+
if record_whole_domain:
|
|
1381
|
+
# Sensor reads mod_z as a count and treats 0 as two dimensions.
|
|
1382
|
+
return fullwave.Sensor(
|
|
1383
|
+
mod_x=1,
|
|
1384
|
+
mod_y=1,
|
|
1385
|
+
mod_z=1 if self.is_3d else 0,
|
|
1386
|
+
sampling_modulus_time=sampling_modulus_time_whole_domain,
|
|
1387
|
+
)
|
|
1388
|
+
return self.pml_builder.extended_sensor
|
|
1389
|
+
|
|
1390
|
+
def _run_members(
|
|
1391
|
+
self,
|
|
1392
|
+
simulation_dir: Path,
|
|
1393
|
+
n_members: int,
|
|
1394
|
+
sensor: fullwave.Sensor,
|
|
1395
|
+
*,
|
|
1396
|
+
load_results: bool,
|
|
1397
|
+
highpass_cutoff_mhz: float | None,
|
|
1398
|
+
bandpass_cutoff_mhz: tuple[float, float] | None,
|
|
1399
|
+
) -> NDArray[np.float64] | list[Path]:
|
|
1400
|
+
"""Run one batch simulation and return the output of each member.
|
|
1401
|
+
|
|
1402
|
+
Parameters
|
|
1403
|
+
----------
|
|
1404
|
+
simulation_dir : Path
|
|
1405
|
+
The simulation directory with the input files.
|
|
1406
|
+
n_members : int
|
|
1407
|
+
The number of transmits in this simulation.
|
|
1408
|
+
sensor : fullwave.Sensor
|
|
1409
|
+
The sensor written to the input files.
|
|
1410
|
+
load_results : bool
|
|
1411
|
+
True loads the outputs, False returns their paths.
|
|
1412
|
+
highpass_cutoff_mhz : float | None
|
|
1413
|
+
See ``run``.
|
|
1414
|
+
bandpass_cutoff_mhz : tuple[float, float] | None
|
|
1415
|
+
See ``run``.
|
|
1416
|
+
|
|
1417
|
+
Returns
|
|
1418
|
+
-------
|
|
1419
|
+
NDArray[np.float64] | list[Path]
|
|
1420
|
+
[n_members, n_sensors, n_t], or the output file of each member in order.
|
|
1421
|
+
|
|
1422
|
+
"""
|
|
1423
|
+
first_output = self.fullwave_launcher.run(simulation_dir, load_results=False)
|
|
1424
|
+
paths = [first_output] + [
|
|
1425
|
+
first_output.parent / f"genout_s{member}.dat" for member in range(1, n_members)
|
|
1426
|
+
]
|
|
1427
|
+
if not load_results:
|
|
1428
|
+
return paths
|
|
1429
|
+
outputs = []
|
|
1430
|
+
for path in paths:
|
|
1431
|
+
result = self._reshape_sensor_data(
|
|
1432
|
+
load_dat_data(path),
|
|
1433
|
+
sensor=sensor,
|
|
1434
|
+
n_t=self.pml_builder.extended_grid.nt,
|
|
1435
|
+
)
|
|
1436
|
+
outputs.append(
|
|
1437
|
+
self._apply_output_filter(
|
|
1438
|
+
result,
|
|
1439
|
+
self.grid.dt,
|
|
1440
|
+
highpass_cutoff_mhz,
|
|
1441
|
+
bandpass_cutoff_mhz,
|
|
1442
|
+
),
|
|
1443
|
+
)
|
|
1444
|
+
return np.stack(outputs)
|
|
1445
|
+
|
|
1446
|
+
def run_batch(
|
|
1447
|
+
self,
|
|
1448
|
+
sources: Sequence[fullwave.Source],
|
|
1449
|
+
simulation_dir_name: str | Path = "txrx_batch",
|
|
1450
|
+
*,
|
|
1451
|
+
max_members_per_run: int | None = None,
|
|
1452
|
+
is_static_map: bool = False,
|
|
1453
|
+
record_whole_domain: bool = False,
|
|
1454
|
+
sampling_modulus_time_whole_domain: int = 1,
|
|
1455
|
+
load_results: bool = True,
|
|
1456
|
+
generate_input_only: bool = False,
|
|
1457
|
+
release_after_write: bool = False,
|
|
1458
|
+
highpass_cutoff_mhz: float | None = None,
|
|
1459
|
+
bandpass_cutoff_mhz: tuple[float, float] | None = None,
|
|
1460
|
+
gpu_memory_estimate: bool = True,
|
|
1461
|
+
) -> NDArray[np.float64] | list[Path]:
|
|
1462
|
+
r"""Run several transmits on the same medium as one batch and return every output.
|
|
1463
|
+
|
|
1464
|
+
A batch runs a transmit sequence, such as the angles of a plane wave sequence,
|
|
1465
|
+
the lines of a focused sequence or the elements of a synthetic aperture sequence,
|
|
1466
|
+
in fewer and faster simulations than one ``run`` per transmit.
|
|
1467
|
+
The transmits of a batch share the medium, the sensor and the source coordinates,
|
|
1468
|
+
and differ only in their signals.
|
|
1469
|
+
A Transducer gives such transmits: set the transmit
|
|
1470
|
+
(``plane_wave``, ``focus``, ``synthetic_aperture`` ...)
|
|
1471
|
+
and take ``transducer.source`` for each one.
|
|
1472
|
+
|
|
1473
|
+
A simulation of several transmits runs on a 2D batch solver binary.
|
|
1474
|
+
Without ``path_fullwave_simulation_bin``, run_batch downloads it like the
|
|
1475
|
+
other binaries. A binary passed as ``path_fullwave_simulation_bin`` must be
|
|
1476
|
+
a batch binary, whose file name contains "batch".
|
|
1477
|
+
|
|
1478
|
+
Parameters
|
|
1479
|
+
----------
|
|
1480
|
+
sources : Sequence[fullwave.Source]
|
|
1481
|
+
The transmits, in order. Each one must have the same source coordinates,
|
|
1482
|
+
the same signal kinds and the same signal length as the Solver's source.
|
|
1483
|
+
The Solver's ``source_type`` applies to every transmit.
|
|
1484
|
+
simulation_dir_name : str | Path
|
|
1485
|
+
The directory name of the simulation under the work directory.
|
|
1486
|
+
When the batch runs as several simulations,
|
|
1487
|
+
simulation k uses ``<simulation_dir_name>_<k>``.
|
|
1488
|
+
max_members_per_run : int | None
|
|
1489
|
+
The largest number of transmits in one simulation.
|
|
1490
|
+
None, the default, takes the smaller of DEFAULT_MAX_MEMBERS_PER_RUN (32) and the
|
|
1491
|
+
number that the GPU memory estimate fits into the free memory of every GPU.
|
|
1492
|
+
The transmits are split into simulations of nearly equal size,
|
|
1493
|
+
for example 128 transmits into 4 of 32, and 7 into 3, 2 and 2.
|
|
1494
|
+
is_static_map : bool
|
|
1495
|
+
See ``run``.
|
|
1496
|
+
The medium files are written by the first simulation and shared by the others.
|
|
1497
|
+
record_whole_domain : bool
|
|
1498
|
+
See ``run``.
|
|
1499
|
+
sampling_modulus_time_whole_domain : int
|
|
1500
|
+
See ``run``.
|
|
1501
|
+
load_results : bool
|
|
1502
|
+
True returns the outputs. False returns the output file of every transmit.
|
|
1503
|
+
generate_input_only : bool
|
|
1504
|
+
True writes the input files and returns the simulation directories.
|
|
1505
|
+
release_after_write : bool
|
|
1506
|
+
See ``run``.
|
|
1507
|
+
highpass_cutoff_mhz : float | None
|
|
1508
|
+
See ``run``.
|
|
1509
|
+
bandpass_cutoff_mhz : tuple[float, float] | None
|
|
1510
|
+
See ``run``.
|
|
1511
|
+
gpu_memory_estimate : bool
|
|
1512
|
+
See ``run``.
|
|
1513
|
+
|
|
1514
|
+
Returns
|
|
1515
|
+
-------
|
|
1516
|
+
NDArray[np.float64] | list[Path]
|
|
1517
|
+
[n_transmits, n_sensors, n_t] in transmit order when load_results is True.
|
|
1518
|
+
Otherwise the output file of each transmit in order, or the simulation
|
|
1519
|
+
directories when generate_input_only is True.
|
|
1520
|
+
|
|
1521
|
+
Raises
|
|
1522
|
+
------
|
|
1523
|
+
ValueError
|
|
1524
|
+
If ``sources`` is empty, if a transmit does not match the Solver's source,
|
|
1525
|
+
if ``max_members_per_run`` is smaller than 1, or if a simulation of more than
|
|
1526
|
+
one transmit would run on a binary that is not a batch binary.
|
|
1527
|
+
NotImplementedError
|
|
1528
|
+
For a 3D grid, or for velocity output (OUTPUT_VELOCITY) of more than one
|
|
1529
|
+
transmit in one simulation.
|
|
1530
|
+
|
|
1531
|
+
"""
|
|
1532
|
+
members = self._batch_members(sources)
|
|
1533
|
+
# The sensor and the sizing below need the extended grid and medium.
|
|
1534
|
+
self.pml_builder.run(use_pml=self.use_pml)
|
|
1535
|
+
sensor = self._output_sensor(
|
|
1536
|
+
record_whole_domain=record_whole_domain,
|
|
1537
|
+
sampling_modulus_time_whole_domain=sampling_modulus_time_whole_domain,
|
|
1538
|
+
)
|
|
1539
|
+
per_run = self._members_per_run(len(members), max_members_per_run, sensor)
|
|
1540
|
+
binary = self._batch_binary() if per_run > 1 else self.path_fullwave_simulation_bin
|
|
1541
|
+
runs = _split_evenly(members, per_run)
|
|
1542
|
+
logger.info(
|
|
1543
|
+
"Running %d transmits as %d simulation(s) of up to %d transmits.",
|
|
1544
|
+
len(members),
|
|
1545
|
+
len(runs),
|
|
1546
|
+
per_run,
|
|
1547
|
+
)
|
|
1548
|
+
|
|
1549
|
+
outputs = []
|
|
1550
|
+
with self._binary_in_use(binary):
|
|
1551
|
+
for index, run_members in enumerate(runs):
|
|
1552
|
+
name = simulation_dir_name if len(runs) == 1 else f"{simulation_dir_name}_{index}"
|
|
1553
|
+
outputs.append(
|
|
1554
|
+
self._run_one(
|
|
1555
|
+
name,
|
|
1556
|
+
members=run_members,
|
|
1557
|
+
is_static_map=is_static_map,
|
|
1558
|
+
recalculate_pml=index == 0 or not is_static_map,
|
|
1559
|
+
record_whole_domain=record_whole_domain,
|
|
1560
|
+
sampling_modulus_time_whole_domain=sampling_modulus_time_whole_domain,
|
|
1561
|
+
load_results=load_results,
|
|
1562
|
+
generate_input_only=generate_input_only,
|
|
1563
|
+
release_after_write=release_after_write,
|
|
1564
|
+
highpass_cutoff_mhz=highpass_cutoff_mhz,
|
|
1565
|
+
bandpass_cutoff_mhz=bandpass_cutoff_mhz,
|
|
1566
|
+
gpu_memory_estimate=gpu_memory_estimate,
|
|
1567
|
+
),
|
|
1568
|
+
)
|
|
1569
|
+
if generate_input_only:
|
|
1570
|
+
return outputs
|
|
1571
|
+
if not load_results:
|
|
1572
|
+
return [path for run_paths in outputs for path in run_paths]
|
|
1573
|
+
return np.concatenate(outputs, axis=0)
|
|
1574
|
+
|
|
1575
|
+
def _batch_members(self, sources: Sequence[fullwave.Source]) -> list[fullwave.Source]:
|
|
1576
|
+
"""Check the transmits of a batch against the Solver's source and return them.
|
|
1577
|
+
|
|
1578
|
+
The Solver's source_type is applied to every transmit, as it is to the Solver's
|
|
1579
|
+
source.
|
|
1580
|
+
|
|
1581
|
+
Parameters
|
|
1582
|
+
----------
|
|
1583
|
+
sources : Sequence[fullwave.Source]
|
|
1584
|
+
The transmits, in order.
|
|
1585
|
+
|
|
1586
|
+
Returns
|
|
1587
|
+
-------
|
|
1588
|
+
list[fullwave.Source]
|
|
1589
|
+
The transmits, converted to the Solver's source type.
|
|
1590
|
+
|
|
1591
|
+
Raises
|
|
1592
|
+
------
|
|
1593
|
+
ValueError
|
|
1594
|
+
If there is no transmit, or a transmit does not match the Solver's source.
|
|
1595
|
+
NotImplementedError
|
|
1596
|
+
For a 3D grid.
|
|
1597
|
+
|
|
1598
|
+
"""
|
|
1599
|
+
if self.is_3d:
|
|
1600
|
+
error_msg = "run_batch supports 2D simulations only."
|
|
1601
|
+
logger.error(error_msg)
|
|
1602
|
+
raise NotImplementedError(error_msg)
|
|
1603
|
+
if len(sources) == 0:
|
|
1604
|
+
error_msg = "run_batch needs at least one source."
|
|
1605
|
+
logger.error(error_msg)
|
|
1606
|
+
raise ValueError(error_msg)
|
|
1607
|
+
members = []
|
|
1608
|
+
for index, source in enumerate(sources):
|
|
1609
|
+
member = source
|
|
1610
|
+
if is_soft(self.source_type):
|
|
1611
|
+
member = as_additive_source(
|
|
1612
|
+
source,
|
|
1613
|
+
self.grid,
|
|
1614
|
+
self.medium,
|
|
1615
|
+
use_exponential_attenuation=self.use_exponential_attenuation,
|
|
1616
|
+
)
|
|
1617
|
+
mismatch = self._batch_member_mismatch(member)
|
|
1618
|
+
if mismatch is not None:
|
|
1619
|
+
error_msg = (
|
|
1620
|
+
f"source {index} of the batch does not match the Solver's source: "
|
|
1621
|
+
f"{mismatch}. The transmits of a batch share the source coordinates and "
|
|
1622
|
+
"differ only in their signals."
|
|
1623
|
+
)
|
|
1624
|
+
logger.error(error_msg)
|
|
1625
|
+
raise ValueError(error_msg)
|
|
1626
|
+
members.append(member)
|
|
1627
|
+
return members
|
|
1628
|
+
|
|
1629
|
+
def _batch_member_mismatch(self, member: fullwave.Source) -> str | None:
|
|
1630
|
+
"""Return how a transmit differs from the Solver's source, or None if it matches.
|
|
1631
|
+
|
|
1632
|
+
Parameters
|
|
1633
|
+
----------
|
|
1634
|
+
member : fullwave.Source
|
|
1635
|
+
One transmit of a batch.
|
|
1636
|
+
|
|
1637
|
+
Returns
|
|
1638
|
+
-------
|
|
1639
|
+
str | None
|
|
1640
|
+
A description of the first difference, or None.
|
|
1641
|
+
|
|
1642
|
+
"""
|
|
1643
|
+
if tuple(member.grid_shape) != tuple(self.source.grid_shape):
|
|
1644
|
+
return f"grid shape {member.grid_shape} != {self.source.grid_shape}"
|
|
1645
|
+
for kind, signal, coords in (
|
|
1646
|
+
("pressure", "icmat", "incoords"),
|
|
1647
|
+
("additive pressure", "p0_additive", "incoords_add"),
|
|
1648
|
+
("u velocity", "u0", "incoords_u"),
|
|
1649
|
+
("v velocity", "v0", "incoords_v"),
|
|
1650
|
+
("w velocity", "w0", "incoords_w"),
|
|
1651
|
+
):
|
|
1652
|
+
expected = getattr(self.source, signal, None)
|
|
1653
|
+
found = getattr(member, signal, None)
|
|
1654
|
+
if (expected is None) != (found is None):
|
|
1655
|
+
return f"the {kind} signal is present in only one of them"
|
|
1656
|
+
if expected is None:
|
|
1657
|
+
continue
|
|
1658
|
+
if np.shape(found) != np.shape(expected):
|
|
1659
|
+
return f"the {kind} signal shape {np.shape(found)} != {np.shape(expected)}"
|
|
1660
|
+
expected_coords = getattr(self.source, coords, None)
|
|
1661
|
+
found_coords = getattr(member, coords, None)
|
|
1662
|
+
if (expected_coords is None) != (found_coords is None) or (
|
|
1663
|
+
expected_coords is not None
|
|
1664
|
+
and not np.array_equal(np.asarray(found_coords), np.asarray(expected_coords))
|
|
1665
|
+
):
|
|
1666
|
+
return f"the {kind} coordinates differ"
|
|
1667
|
+
return None
|
|
1668
|
+
|
|
1669
|
+
def _batch_binary(self) -> Path:
|
|
1670
|
+
"""Return the binary for a simulation of several transmits.
|
|
1671
|
+
|
|
1672
|
+
Without a binary from the user, this is the batch binary of the same model and
|
|
1673
|
+
CUDA version as the Solver's binary, downloaded on first use.
|
|
1674
|
+
A binary without batch support reads only the first transmit's signals,
|
|
1675
|
+
so its output would look valid and be wrong for every other transmit.
|
|
1676
|
+
|
|
1677
|
+
Returns
|
|
1678
|
+
-------
|
|
1679
|
+
Path
|
|
1680
|
+
The batch binary.
|
|
1681
|
+
|
|
1682
|
+
Raises
|
|
1683
|
+
------
|
|
1684
|
+
ValueError
|
|
1685
|
+
If the user's binary file name does not contain "batch".
|
|
1686
|
+
NotImplementedError
|
|
1687
|
+
If velocity output is requested (OUTPUT_VELOCITY=1).
|
|
1688
|
+
|
|
1689
|
+
"""
|
|
1690
|
+
if os.environ.get("OUTPUT_VELOCITY") == "1":
|
|
1691
|
+
error_msg = (
|
|
1692
|
+
"velocity output (OUTPUT_VELOCITY=1) is not supported for several "
|
|
1693
|
+
"transmits in one simulation. Set max_members_per_run=1."
|
|
1694
|
+
)
|
|
1695
|
+
logger.error(error_msg)
|
|
1696
|
+
raise NotImplementedError(error_msg)
|
|
1697
|
+
if self._uses_default_binary:
|
|
1698
|
+
local_path = _retrieve_fullwave_simulation_path(
|
|
1699
|
+
use_gpu=self.use_gpu,
|
|
1700
|
+
is_3d=self.is_3d,
|
|
1701
|
+
use_exponential_attenuation=self.use_exponential_attenuation,
|
|
1702
|
+
use_isotropic_relaxation=self.use_isotropic_relaxation,
|
|
1703
|
+
batch=True,
|
|
1704
|
+
)
|
|
1705
|
+
return ensure_binary(local_path)
|
|
1706
|
+
name = Path(self.path_fullwave_simulation_bin).name
|
|
1707
|
+
if "batch" not in name:
|
|
1708
|
+
error_msg = (
|
|
1709
|
+
f"the binary {name} cannot run several transmits in one simulation. "
|
|
1710
|
+
"Pass a batch binary (its file name contains 'batch') as "
|
|
1711
|
+
"path_fullwave_simulation_bin, or set max_members_per_run=1."
|
|
1712
|
+
)
|
|
1713
|
+
logger.error(error_msg)
|
|
1714
|
+
raise ValueError(error_msg)
|
|
1715
|
+
return Path(self.path_fullwave_simulation_bin)
|
|
1716
|
+
|
|
1717
|
+
@contextlib.contextmanager
|
|
1718
|
+
def _binary_in_use(self, binary: Path) -> Iterator[None]:
|
|
1719
|
+
"""Run the simulations of the block on ``binary``, then restore the Solver's binary.
|
|
1720
|
+
|
|
1721
|
+
Parameters
|
|
1722
|
+
----------
|
|
1723
|
+
binary : Path
|
|
1724
|
+
The binary for the simulations of the block.
|
|
1725
|
+
|
|
1726
|
+
Yields
|
|
1727
|
+
------
|
|
1728
|
+
None
|
|
1729
|
+
Control to the block.
|
|
1730
|
+
|
|
1731
|
+
"""
|
|
1732
|
+
previous = self.path_fullwave_simulation_bin, self.fullwave_launcher
|
|
1733
|
+
if binary != self.path_fullwave_simulation_bin:
|
|
1734
|
+
self.path_fullwave_simulation_bin = binary
|
|
1735
|
+
self.fullwave_launcher = self.fullwave_launcher.with_binary(binary)
|
|
1736
|
+
try:
|
|
1737
|
+
yield
|
|
1738
|
+
finally:
|
|
1739
|
+
self.path_fullwave_simulation_bin, self.fullwave_launcher = previous
|
|
1740
|
+
|
|
1741
|
+
def _members_per_run(
|
|
1742
|
+
self,
|
|
1743
|
+
n_members: int,
|
|
1744
|
+
max_members_per_run: int | None,
|
|
1745
|
+
sensor: fullwave.Sensor,
|
|
1746
|
+
) -> int:
|
|
1747
|
+
"""Return the number of transmits in one simulation of a batch.
|
|
1748
|
+
|
|
1749
|
+
Parameters
|
|
1750
|
+
----------
|
|
1751
|
+
n_members : int
|
|
1752
|
+
The number of transmits in the batch.
|
|
1753
|
+
max_members_per_run : int | None
|
|
1754
|
+
The user's limit, or None to size from the GPU memory estimate.
|
|
1755
|
+
sensor : fullwave.Sensor
|
|
1756
|
+
The sensor written to the input files.
|
|
1757
|
+
|
|
1758
|
+
Returns
|
|
1759
|
+
-------
|
|
1760
|
+
int
|
|
1761
|
+
Between 1 and n_members.
|
|
1762
|
+
|
|
1763
|
+
Raises
|
|
1764
|
+
------
|
|
1765
|
+
ValueError
|
|
1766
|
+
If max_members_per_run is smaller than 1.
|
|
1767
|
+
|
|
1768
|
+
"""
|
|
1769
|
+
if max_members_per_run is not None:
|
|
1770
|
+
if max_members_per_run < 1:
|
|
1771
|
+
error_msg = f"max_members_per_run must be 1 or more, not {max_members_per_run}"
|
|
1772
|
+
logger.error(error_msg)
|
|
1773
|
+
raise ValueError(error_msg)
|
|
1774
|
+
return min(n_members, max_members_per_run)
|
|
1775
|
+
fit = self._members_that_fit(sensor)
|
|
1776
|
+
if fit is None:
|
|
1777
|
+
logger.warning(
|
|
1778
|
+
"The free GPU memory could not be read, so up to %d transmits run in one "
|
|
1779
|
+
"simulation. Set max_members_per_run to limit it.",
|
|
1780
|
+
DEFAULT_MAX_MEMBERS_PER_RUN,
|
|
1781
|
+
)
|
|
1782
|
+
return min(n_members, DEFAULT_MAX_MEMBERS_PER_RUN)
|
|
1783
|
+
if fit < 1:
|
|
1784
|
+
logger.warning(
|
|
1785
|
+
"The GPU memory estimate of one transmit exceeds the free GPU memory. "
|
|
1786
|
+
"The transmits run one per simulation.",
|
|
1787
|
+
)
|
|
1788
|
+
return 1
|
|
1789
|
+
logger.info("The free GPU memory fits about %d transmits per simulation.", fit)
|
|
1790
|
+
return min(n_members, fit, DEFAULT_MAX_MEMBERS_PER_RUN)
|
|
1791
|
+
|
|
1792
|
+
def _members_that_fit(self, sensor: fullwave.Sensor) -> int | None:
|
|
1793
|
+
"""Return how many transmits the GPU memory estimate fits into the free memory.
|
|
1794
|
+
|
|
1795
|
+
The estimate of one GPU grows by the same amount for each transmit. The count
|
|
1796
|
+
keeps 10 % of the free memory and 512 MiB free for the CUDA context and for
|
|
1797
|
+
what the estimate does not count, and takes the smallest count over the GPUs.
|
|
1798
|
+
|
|
1799
|
+
Parameters
|
|
1800
|
+
----------
|
|
1801
|
+
sensor : fullwave.Sensor
|
|
1802
|
+
The sensor written to the input files.
|
|
1803
|
+
|
|
1804
|
+
Returns
|
|
1805
|
+
-------
|
|
1806
|
+
int | None
|
|
1807
|
+
The count, or None when the free memory cannot be read.
|
|
1808
|
+
|
|
1809
|
+
"""
|
|
1810
|
+
free = _free_gpu_memory_bytes(self.fullwave_launcher.cuda_device_id.split(","))
|
|
1811
|
+
if free is None:
|
|
1812
|
+
return None
|
|
1813
|
+
one = self._gpu_memory_bytes(sensor, n_members=1)
|
|
1814
|
+
two = self._gpu_memory_bytes(sensor, n_members=2)
|
|
1815
|
+
reserve = 512 * 1024**2
|
|
1816
|
+
counts = []
|
|
1817
|
+
for (device, one_bytes, *_), (_, two_bytes, *_) in zip(one, two, strict=True):
|
|
1818
|
+
per_member = max(two_bytes - one_bytes, 1)
|
|
1819
|
+
shared = one_bytes - per_member
|
|
1820
|
+
usable = 0.9 * free.get(device, 0) - reserve - shared
|
|
1821
|
+
counts.append(int(usable // per_member))
|
|
1822
|
+
return min(counts)
|
|
1823
|
+
|
|
1198
1824
|
def _estimate_gpu_memory(
|
|
1199
1825
|
self,
|
|
1200
1826
|
sensor: fullwave.Sensor,
|
|
1827
|
+
n_members: int = 1,
|
|
1201
1828
|
) -> None:
|
|
1202
1829
|
"""Estimate and log GPU memory usage per device.
|
|
1203
1830
|
|
|
@@ -1209,10 +1836,54 @@ class Solver:
|
|
|
1209
1836
|
sensor : fullwave.Sensor
|
|
1210
1837
|
The sensor that will actually be written to the input files (may
|
|
1211
1838
|
differ from ``self.sensor`` when ``record_whole_domain=True``).
|
|
1839
|
+
n_members : int
|
|
1840
|
+
The number of transmits in the simulation (a batch has more than one).
|
|
1212
1841
|
|
|
1213
1842
|
"""
|
|
1214
1843
|
# show that this is an experimental feature
|
|
1215
1844
|
logger.info("Estimating GPU memory usage... (experimental feature, may be inaccurate)")
|
|
1845
|
+
gib = 1024.0**3
|
|
1846
|
+
mode = "exponential" if self.use_exponential_attenuation else "relaxation"
|
|
1847
|
+
saving = ", save_gpu_memory=True" if self.save_gpu_memory else ""
|
|
1848
|
+
if n_members > 1:
|
|
1849
|
+
saving += f", batch of {n_members} transmits"
|
|
1850
|
+
for dev_id, total, depth_this, halo, lateral in self._gpu_memory_bytes(
|
|
1851
|
+
sensor,
|
|
1852
|
+
n_members=n_members,
|
|
1853
|
+
):
|
|
1854
|
+
logger.info(
|
|
1855
|
+
"GPU memory estimate [GPU %s] (%s mode%s): "
|
|
1856
|
+
"%.2f GiB (depth=%d +%d halo, lateral=%d)",
|
|
1857
|
+
dev_id,
|
|
1858
|
+
mode,
|
|
1859
|
+
saving,
|
|
1860
|
+
total / gib,
|
|
1861
|
+
depth_this,
|
|
1862
|
+
halo,
|
|
1863
|
+
lateral,
|
|
1864
|
+
)
|
|
1865
|
+
|
|
1866
|
+
def _gpu_memory_bytes(
|
|
1867
|
+
self,
|
|
1868
|
+
sensor: fullwave.Sensor,
|
|
1869
|
+
n_members: int = 1,
|
|
1870
|
+
) -> list[tuple[str, int, int, int, int]]:
|
|
1871
|
+
"""Return the estimated GPU memory of each device.
|
|
1872
|
+
|
|
1873
|
+
Parameters
|
|
1874
|
+
----------
|
|
1875
|
+
sensor : fullwave.Sensor
|
|
1876
|
+
The sensor written to the input files.
|
|
1877
|
+
n_members : int
|
|
1878
|
+
The number of transmits in the simulation.
|
|
1879
|
+
|
|
1880
|
+
Returns
|
|
1881
|
+
-------
|
|
1882
|
+
list[tuple[str, int, int, int, int]]
|
|
1883
|
+
For each device: its id, the estimated bytes, its depth, its halo depth
|
|
1884
|
+
and the lateral size.
|
|
1885
|
+
|
|
1886
|
+
"""
|
|
1216
1887
|
device_ids = self.fullwave_launcher.cuda_device_id.split(",")
|
|
1217
1888
|
n_gpus = len(device_ids)
|
|
1218
1889
|
|
|
@@ -1233,8 +1904,6 @@ class Solver:
|
|
|
1233
1904
|
|
|
1234
1905
|
n_source_timesteps = source.icmat.shape[1]
|
|
1235
1906
|
|
|
1236
|
-
gib = 1024.0**3
|
|
1237
|
-
|
|
1238
1907
|
base_depth = depth // n_gpus
|
|
1239
1908
|
remainder = depth % n_gpus
|
|
1240
1909
|
|
|
@@ -1247,6 +1916,7 @@ class Solver:
|
|
|
1247
1916
|
for name in ("n_sources", "n_sources_add", "n_sources_u", "n_sources_v", "n_sources_w")
|
|
1248
1917
|
)
|
|
1249
1918
|
body_start = 0
|
|
1919
|
+
estimates = []
|
|
1250
1920
|
|
|
1251
1921
|
for rank, dev_id in enumerate(device_ids):
|
|
1252
1922
|
depth_this = base_depth + (1 if rank < remainder else 0)
|
|
@@ -1277,6 +1947,7 @@ class Solver:
|
|
|
1277
1947
|
float_bytes=float_bytes,
|
|
1278
1948
|
int_bytes=int_bytes,
|
|
1279
1949
|
is_3d=self.is_3d,
|
|
1950
|
+
n_members=n_members,
|
|
1280
1951
|
)
|
|
1281
1952
|
else:
|
|
1282
1953
|
band, interior = self._band_and_interior(low, high, grid.shape, box, hole)
|
|
@@ -1294,21 +1965,13 @@ class Solver:
|
|
|
1294
1965
|
is_3d=self.is_3d,
|
|
1295
1966
|
band=band,
|
|
1296
1967
|
interior=interior,
|
|
1968
|
+
n_members=n_members,
|
|
1297
1969
|
)
|
|
1298
1970
|
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
logger.info(
|
|
1302
|
-
"GPU memory estimate [GPU %s] (%s mode%s): "
|
|
1303
|
-
"%.2f GiB (depth=%d +%d halo, lateral=%d)",
|
|
1304
|
-
dev_id.strip(),
|
|
1305
|
-
mode,
|
|
1306
|
-
saving,
|
|
1307
|
-
total / gib,
|
|
1308
|
-
depth_this,
|
|
1309
|
-
n_halo_sides * halo_depth,
|
|
1310
|
-
lateral,
|
|
1971
|
+
estimates.append(
|
|
1972
|
+
(dev_id.strip(), total, depth_this, n_halo_sides * halo_depth, lateral),
|
|
1311
1973
|
)
|
|
1974
|
+
return estimates
|
|
1312
1975
|
|
|
1313
1976
|
@staticmethod
|
|
1314
1977
|
def _mem_exponential(
|
|
@@ -1322,6 +1985,7 @@ class Solver:
|
|
|
1322
1985
|
float_bytes: int,
|
|
1323
1986
|
int_bytes: int,
|
|
1324
1987
|
is_3d: bool,
|
|
1988
|
+
n_members: int = 1,
|
|
1325
1989
|
) -> int:
|
|
1326
1990
|
"""Return estimated GPU bytes for exponential-attenuation solver.
|
|
1327
1991
|
|
|
@@ -1345,6 +2009,9 @@ class Solver:
|
|
|
1345
2009
|
Bytes per int (4).
|
|
1346
2010
|
is_3d: bool,
|
|
1347
2011
|
Whether the simulation is 3D (affects sensor memory).
|
|
2012
|
+
n_members : int
|
|
2013
|
+
The number of transmits in the simulation. Each one holds its own wave fields,
|
|
2014
|
+
source signals and recorded frame.
|
|
1348
2015
|
|
|
1349
2016
|
Returns
|
|
1350
2017
|
-------
|
|
@@ -1358,18 +2025,19 @@ class Solver:
|
|
|
1358
2025
|
n_fields = 4 if is_3d else 3 # p, u, [v], w
|
|
1359
2026
|
|
|
1360
2027
|
# wave fields: n_fields pairs x 2 time levels
|
|
1361
|
-
mem = n_fields * 2 * slab * fb
|
|
2028
|
+
mem = n_fields * 2 * slab * fb * n_members
|
|
1362
2029
|
# material: rho + K + beta + a_exp
|
|
1363
2030
|
mem += 4 * slab * fb
|
|
1364
2031
|
# derivative maps (dmap + dcmap)
|
|
1365
2032
|
mem += 9 * 2 * n_deriv_levels * fb + slab * ib
|
|
1366
2033
|
# source (icmat + coords)
|
|
1367
2034
|
if n_sources > 0:
|
|
1368
|
-
|
|
2035
|
+
samples = n_sources if save_gpu_memory else n_sources * n_source_timesteps
|
|
2036
|
+
mem += samples * fb * n_members
|
|
1369
2037
|
mem += ndim * n_sources * ib
|
|
1370
2038
|
# sensor (genoutframe + coordsout_local + p_idx_array)
|
|
1371
2039
|
if n_sensors > 0:
|
|
1372
|
-
mem += n_sensors * fb
|
|
2040
|
+
mem += n_sensors * fb * n_members
|
|
1373
2041
|
mem += (ndim + 1) * n_sensors * ib
|
|
1374
2042
|
mem += n_sensors * ib
|
|
1375
2043
|
return mem
|
|
@@ -1390,6 +2058,7 @@ class Solver:
|
|
|
1390
2058
|
is_3d: bool,
|
|
1391
2059
|
band: int | None = None,
|
|
1392
2060
|
interior: int | None = None,
|
|
2061
|
+
n_members: int = 1,
|
|
1393
2062
|
) -> int:
|
|
1394
2063
|
"""Return estimated GPU bytes for the relaxation (power-law) solver.
|
|
1395
2064
|
|
|
@@ -1432,6 +2101,10 @@ class Solver:
|
|
|
1432
2101
|
interior : int | None
|
|
1433
2102
|
Grid points of this GPU's share of the interior box of the mechanisms
|
|
1434
2103
|
above the first. None takes the slab.
|
|
2104
|
+
n_members : int
|
|
2105
|
+
The number of transmits in the simulation. Each one holds its own wave fields,
|
|
2106
|
+
memory variables, source signals and recorded frame; the medium and the
|
|
2107
|
+
coefficients are held once.
|
|
1435
2108
|
|
|
1436
2109
|
Returns
|
|
1437
2110
|
-------
|
|
@@ -1444,22 +2117,22 @@ class Solver:
|
|
|
1444
2117
|
axes = 3 if is_3d else 2
|
|
1445
2118
|
band = slab if band is None else band
|
|
1446
2119
|
interior = slab if interior is None else interior
|
|
1447
|
-
|
|
2120
|
+
# Each mechanism holds a and b (shared) and one memory variable per axis (per member).
|
|
2121
|
+
per_mechanism = 2 + axes * n_members
|
|
2122
|
+
mechanism_cells = slab + max(n_relax - 1, 0) * interior + band
|
|
1448
2123
|
|
|
1449
|
-
mem = 2 * (axes + 1) * slab * fb
|
|
2124
|
+
mem = 2 * (axes + 1) * slab * fb * n_members
|
|
1450
2125
|
mem += 5 * slab * fb
|
|
1451
2126
|
mem += slab * ib
|
|
1452
|
-
mem += per_mechanism *
|
|
1453
|
-
mem += per_mechanism * max(n_relax - 1, 0) * interior * fb
|
|
1454
|
-
mem += per_mechanism * band * fb
|
|
2127
|
+
mem += per_mechanism * mechanism_cells * fb
|
|
1455
2128
|
mem += 9 * 2 * n_deriv_levels * fb
|
|
1456
2129
|
if n_sources > 0:
|
|
1457
2130
|
samples = n_sources if save_gpu_memory else n_sources * n_source_timesteps
|
|
1458
|
-
mem += samples * fb + axes * n_sources * ib
|
|
2131
|
+
mem += samples * fb * n_members + axes * n_sources * ib
|
|
1459
2132
|
if n_air > 0:
|
|
1460
2133
|
mem += axes * n_air * ib
|
|
1461
2134
|
if n_sensors > 0:
|
|
1462
|
-
mem += n_sensors * (fb + (axes + 2) * ib)
|
|
2135
|
+
mem += n_sensors * (fb * n_members + (axes + 2) * ib)
|
|
1463
2136
|
return mem
|
|
1464
2137
|
|
|
1465
2138
|
@staticmethod
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fullwave25
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.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
|
|
@@ -274,6 +274,24 @@ Every transducer is additive by default, which is what the calibrated setups use
|
|
|
274
274
|
|
|
275
275
|
`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.
|
|
276
276
|
|
|
277
|
+
### Transmit batches
|
|
278
|
+
|
|
279
|
+
A transmit sequence on one medium, such as plane wave angles, focused lines or synthetic aperture elements, can run as a batch: several transmits in one simulation, which is faster than one `run` per transmit. The transmits of a batch share the medium, the sensor and the source positions, and differ only in their signals. The transmits of one transducer satisfy this.
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
sources = []
|
|
283
|
+
for angle in (-10, -5, 0, 5, 10):
|
|
284
|
+
transducer.plane_wave(angle_deg=angle)
|
|
285
|
+
sources.append(transducer.source)
|
|
286
|
+
|
|
287
|
+
solver = fullwave.Solver(
|
|
288
|
+
work_dir="./outputs/my_batch", grid=grid, medium=medium, transducer=transducer,
|
|
289
|
+
)
|
|
290
|
+
recorded = solver.run_batch(sources) # [n_transmits, n_sensors, n_t]
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
`run_batch` runs all transmits of the list. It puts at most `max_members_per_run` transmits in one simulation: by default 32, or fewer when the GPU memory estimate does not fit that many into the free GPU memory. A longer sequence runs as several simulations of nearly equal size, so 128 transmits run as 4 simulations of 32. In a 2D L7-4 test the time per transmit stopped falling at 16 to 32 transmits. [transmit_batch_basics.py](examples/transmit_batch/transmit_batch_basics.py) shows plane waves and focused lines as batches, and [plane_wave_compounding_batch.py](examples/linear_transducer/plane_wave_compounding_batch.py) makes a compounded B-mode from one batch.
|
|
294
|
+
|
|
277
295
|
### Which example to read
|
|
278
296
|
|
|
279
297
|
| I want to | example |
|
|
@@ -281,6 +299,8 @@ Every transducer is additive by default, which is what the calibrated setups use
|
|
|
281
299
|
| send one plane wave | [simple_plane_wave.py](examples/simple_plane_wave/simple_plane_wave.py) |
|
|
282
300
|
| do the same in 3D | [simple_plane_wave_3d.py](examples/wave_3d/simple_plane_wave_3d.py) |
|
|
283
301
|
| compound plane waves into a B-mode | [plane_wave_compounding.py](examples/linear_transducer/plane_wave_compounding.py) |
|
|
302
|
+
| run a transmit sequence as batches | [transmit_batch_basics.py](examples/transmit_batch/transmit_batch_basics.py) |
|
|
303
|
+
| compound plane waves in one batch | [plane_wave_compounding_batch.py](examples/linear_transducer/plane_wave_compounding_batch.py) |
|
|
284
304
|
| focus and watch the beam | [linear_transducer_focused_animation.py](examples/linear_transducer/linear_transducer_focused_animation.py) |
|
|
285
305
|
| run a full synthetic aperture | [full_synthetic_aperture.py](examples/linear_transducer/full_synthetic_aperture.py) |
|
|
286
306
|
| do the same on a curved array | [convex_transducer_fsa.py](examples/convex_transducer/convex_transducer_fsa.py) |
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
fullwave/__init__.py,sha256=
|
|
1
|
+
fullwave/__init__.py,sha256=Bt3xst0fa4fvHB1gr5t-vCZmRbPExvNPG46I4oeTaQ4,2382
|
|
2
2
|
fullwave/grid.py,sha256=YbxAAgdYKR8AXwobtTZzHm9C4KR4D5--51zgbs0aQGY,7435
|
|
3
3
|
fullwave/medium.py,sha256=Tz8Cwda6zLF8UFxjWFkRcAdko_KmF4l1zfmjWaqxu-M,68626
|
|
4
4
|
fullwave/sensor.py,sha256=McHyaSKxM11_Kfb_2hfnOtV9Z1UMBCLkeovncLT8_bA,9308
|
|
@@ -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=t1UjYdcp823HhiDQzy7wtsbi3xGoaAU8LPcDkcHalrs,5039
|
|
25
25
|
fullwave/solver/cuda_utils.py,sha256=OxlRtUV4bJT8s8GL0G54toyEOyah25mJKAL_JtnAbdI,12695
|
|
26
|
-
fullwave/solver/input_file_writer.py,sha256=
|
|
27
|
-
fullwave/solver/launcher.py,sha256=
|
|
26
|
+
fullwave/solver/input_file_writer.py,sha256=H98kb9gE664iFLTtQxgFl10PI8w-Psk1G8Tw-uFiwYQ,59779
|
|
27
|
+
fullwave/solver/launcher.py,sha256=yp7l5-ufDbZ0ofeFt-tk-WW14vfqdOYxX6bdl9YwmNE,13302
|
|
28
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=cUBXnOuwrst-MUEyWkWGjtuYmf9ok-fsMmoyDMcU0yY,93093
|
|
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
|
|
@@ -53,7 +53,7 @@ fullwave/utils/relaxation_parameters.py,sha256=wHUNMtJGAJwgCyUYd24_qtQZflMnkQTYh
|
|
|
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.4.
|
|
57
|
-
fullwave25-1.4.
|
|
58
|
-
fullwave25-1.4.
|
|
59
|
-
fullwave25-1.4.
|
|
56
|
+
fullwave25-1.4.2.dev0.dist-info/METADATA,sha256=CvBRMKzvkXTU9FxECjFTkgAQfPiQXiZwhLpHtfrL1Cs,24055
|
|
57
|
+
fullwave25-1.4.2.dev0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
58
|
+
fullwave25-1.4.2.dev0.dist-info/licenses/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
|
|
59
|
+
fullwave25-1.4.2.dev0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|