fullwave25 1.4.2.dev0__py3-none-any.whl → 1.4.2.dev1__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/medium.py +1 -3
- fullwave/solver/binary_manager.py +27 -30
- fullwave/solver/binary_markers.py +87 -0
- fullwave/solver/input_file_writer.py +26 -7
- fullwave/solver/launcher.py +1 -2
- fullwave/solver/solver.py +153 -12
- fullwave/solver/source_type.py +16 -12
- fullwave/source.py +9 -2
- fullwave/transducer.py +24 -19
- fullwave/ultra/__init__.py +15 -0
- fullwave/ultra/backend.py +415 -0
- fullwave/ultra/backend_2d.py +375 -0
- fullwave/ultra/binary.py +152 -0
- fullwave/ultra/dat_io.py +60 -0
- fullwave/ultra/dat_io_3d.py +38 -0
- fullwave/ultra/errors.py +22 -0
- fullwave/ultra/factory.py +21 -0
- fullwave/ultra/medium_to_aexp.py +50 -0
- fullwave/ultra/stencil.py +162 -0
- fullwave/ultra/time_axis.py +61 -0
- fullwave/ultra/writer_2d.py +195 -0
- fullwave/ultra/writer_3d.py +199 -0
- fullwave/utils/__init__.py +2 -1
- fullwave/utils/memory_tempfile.py +21 -0
- {fullwave25-1.4.2.dev0.dist-info → fullwave25-1.4.2.dev1.dist-info}/METADATA +52 -3
- {fullwave25-1.4.2.dev0.dist-info → fullwave25-1.4.2.dev1.dist-info}/RECORD +29 -15
- {fullwave25-1.4.2.dev0.dist-info → fullwave25-1.4.2.dev1.dist-info}/WHEEL +1 -1
- {fullwave25-1.4.2.dev0.dist-info → fullwave25-1.4.2.dev1.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.2-
|
|
69
|
+
__version__ = "1.4.2-dev1"
|
|
70
70
|
|
|
71
71
|
VERSION = __version__ # for convenience
|
|
72
72
|
logger.info("Fullwave version: %s", __version__)
|
fullwave/medium.py
CHANGED
|
@@ -4,7 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import logging
|
|
6
6
|
from collections import OrderedDict
|
|
7
|
-
from concurrent.futures import ThreadPoolExecutor
|
|
7
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
8
8
|
from dataclasses import dataclass
|
|
9
9
|
from pathlib import Path
|
|
10
10
|
from typing import TYPE_CHECKING
|
|
@@ -93,8 +93,6 @@ def _upload_arrays_multi_gpu(
|
|
|
93
93
|
dict mapping name -> CuPy array on *target_device*.
|
|
94
94
|
|
|
95
95
|
"""
|
|
96
|
-
from concurrent.futures import as_completed # noqa: PLC0415
|
|
97
|
-
|
|
98
96
|
import cupy as cp # noqa: PLC0415
|
|
99
97
|
|
|
100
98
|
n_gpus = cp.cuda.runtime.getDeviceCount()
|
|
@@ -21,6 +21,8 @@ import urllib.error
|
|
|
21
21
|
import urllib.request
|
|
22
22
|
from pathlib import Path
|
|
23
23
|
|
|
24
|
+
from tqdm import tqdm
|
|
25
|
+
|
|
24
26
|
logger = logging.getLogger("__main__." + __name__)
|
|
25
27
|
|
|
26
28
|
GITHUB_REPO = "pinton-lab/fullwave25"
|
|
@@ -28,7 +30,7 @@ CACHE_DIR = Path.home() / ".cache" / "fullwave25" / "bins"
|
|
|
28
30
|
|
|
29
31
|
# Pinned release tag for the solver binaries.
|
|
30
32
|
# Update this only when new binaries are uploaded to a GitHub release.
|
|
31
|
-
BINARY_RELEASE_TAG = "fullwave_bin_v1.
|
|
33
|
+
BINARY_RELEASE_TAG = "fullwave_bin_v1.7"
|
|
32
34
|
|
|
33
35
|
|
|
34
36
|
def _download_url(filename: str, tag: str) -> str:
|
|
@@ -36,39 +38,34 @@ def _download_url(filename: str, tag: str) -> str:
|
|
|
36
38
|
|
|
37
39
|
|
|
38
40
|
def _download_binary(url: str, dest: Path) -> None:
|
|
39
|
-
"""Download *url* to *dest*, showing a tqdm progress bar
|
|
41
|
+
"""Download *url* to *dest*, showing a tqdm progress bar."""
|
|
40
42
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
41
43
|
tmp = dest.with_suffix(".download_tmp")
|
|
42
44
|
try:
|
|
45
|
+
|
|
46
|
+
class _Hook:
|
|
47
|
+
def __init__(self) -> None:
|
|
48
|
+
self._bar: tqdm | None = None
|
|
49
|
+
|
|
50
|
+
def __call__(self, block: int, block_size: int, total: int) -> None: # noqa: ARG002
|
|
51
|
+
if self._bar is None:
|
|
52
|
+
self._bar = tqdm(
|
|
53
|
+
total=total if total > 0 else None,
|
|
54
|
+
unit="B",
|
|
55
|
+
unit_scale=True,
|
|
56
|
+
desc=dest.name,
|
|
57
|
+
)
|
|
58
|
+
self._bar.update(block_size)
|
|
59
|
+
|
|
60
|
+
def close(self) -> None:
|
|
61
|
+
if self._bar is not None:
|
|
62
|
+
self._bar.close()
|
|
63
|
+
|
|
64
|
+
hook = _Hook()
|
|
43
65
|
try:
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def __init__(self) -> None:
|
|
48
|
-
self._bar: tqdm | None = None
|
|
49
|
-
|
|
50
|
-
def __call__(self, block: int, block_size: int, total: int) -> None: # noqa: ARG002
|
|
51
|
-
if self._bar is None:
|
|
52
|
-
self._bar = tqdm(
|
|
53
|
-
total=total if total > 0 else None,
|
|
54
|
-
unit="B",
|
|
55
|
-
unit_scale=True,
|
|
56
|
-
desc=dest.name,
|
|
57
|
-
)
|
|
58
|
-
self._bar.update(block_size)
|
|
59
|
-
|
|
60
|
-
def close(self) -> None:
|
|
61
|
-
if self._bar is not None:
|
|
62
|
-
self._bar.close()
|
|
63
|
-
|
|
64
|
-
hook = _Hook()
|
|
65
|
-
try:
|
|
66
|
-
urllib.request.urlretrieve(url, tmp, reporthook=hook) # noqa: S310
|
|
67
|
-
finally:
|
|
68
|
-
hook.close()
|
|
69
|
-
|
|
70
|
-
except ImportError:
|
|
71
|
-
urllib.request.urlretrieve(url, tmp) # noqa: S310
|
|
66
|
+
urllib.request.urlretrieve(url, tmp, reporthook=hook) # noqa: S310
|
|
67
|
+
finally:
|
|
68
|
+
hook.close()
|
|
72
69
|
|
|
73
70
|
tmp.rename(dest)
|
|
74
71
|
dest.chmod(dest.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Find out whether a solver binary reads an input file, by the file name in its bytes."""
|
|
2
|
+
|
|
3
|
+
import mmap
|
|
4
|
+
from functools import lru_cache
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import NamedTuple
|
|
7
|
+
|
|
8
|
+
HOLD_SOURCE_MARKER = b"hold_source.dat"
|
|
9
|
+
DEFAULT_FIX = "rebuild the binary, or set FULLWAVE25_BINARY_TAG to the release that has it"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class HoldSourceUnsupportedError(RuntimeError):
|
|
13
|
+
"""Raised when the binary cannot hold the hard source after the drive ends."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class _FileStamp(NamedTuple):
|
|
17
|
+
"""A file with the size and modification time that tell whether it changed."""
|
|
18
|
+
|
|
19
|
+
path: Path
|
|
20
|
+
size: int
|
|
21
|
+
mtime_ns: int
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@lru_cache(maxsize=64)
|
|
25
|
+
def _find(stamp: _FileStamp, marker: bytes) -> bool:
|
|
26
|
+
if stamp.size == 0:
|
|
27
|
+
return False
|
|
28
|
+
with stamp.path.open("rb") as file, mmap.mmap(file.fileno(), 0, access=mmap.ACCESS_READ) as mm:
|
|
29
|
+
return mm.find(marker) != -1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def binary_has_marker(binary: str | Path, *, marker: bytes) -> bool:
|
|
33
|
+
"""Return whether the bytes of ``binary`` contain ``marker``.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
binary : str | Path
|
|
38
|
+
The solver binary.
|
|
39
|
+
marker : bytes
|
|
40
|
+
The name of the input file the binary reads.
|
|
41
|
+
|
|
42
|
+
Returns
|
|
43
|
+
-------
|
|
44
|
+
bool
|
|
45
|
+
True when the binary contains the name. The answer is cached until the file changes.
|
|
46
|
+
|
|
47
|
+
"""
|
|
48
|
+
path = Path(binary).resolve()
|
|
49
|
+
stat = path.stat()
|
|
50
|
+
return _find(_FileStamp(path=path, size=stat.st_size, mtime_ns=stat.st_mtime_ns), marker)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def check_binary_holds_source(
|
|
54
|
+
binary: str | Path,
|
|
55
|
+
*,
|
|
56
|
+
marker: bytes = HOLD_SOURCE_MARKER,
|
|
57
|
+
error_type: type[Exception] = HoldSourceUnsupportedError,
|
|
58
|
+
fix: str = DEFAULT_FIX,
|
|
59
|
+
) -> None:
|
|
60
|
+
"""Refuse a binary that would release the hard source cells when the drive ends.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
binary : str | Path
|
|
65
|
+
The solver binary.
|
|
66
|
+
marker : bytes, optional
|
|
67
|
+
The name of the input file that tells the binary to hold the source.
|
|
68
|
+
error_type : type[Exception], optional
|
|
69
|
+
The exception to raise.
|
|
70
|
+
fix : str, optional
|
|
71
|
+
What to do to get a binary that reads the file, worded for the message.
|
|
72
|
+
|
|
73
|
+
Raises
|
|
74
|
+
------
|
|
75
|
+
Exception
|
|
76
|
+
``error_type``, when the binary does not contain ``marker``.
|
|
77
|
+
|
|
78
|
+
"""
|
|
79
|
+
if binary_has_marker(binary, marker=marker):
|
|
80
|
+
return
|
|
81
|
+
message = (
|
|
82
|
+
f"{Path(binary).name} does not read {marker.decode()}, so it releases the hard source "
|
|
83
|
+
"cells when the drive signal ends and source_type='hard' would not hold them. "
|
|
84
|
+
f"Use a binary that reads it: {fix}. "
|
|
85
|
+
"source_type='hard_released_after' needs no such binary."
|
|
86
|
+
)
|
|
87
|
+
raise error_type(message)
|
|
@@ -5,6 +5,7 @@ import logging
|
|
|
5
5
|
import os
|
|
6
6
|
import time
|
|
7
7
|
from collections.abc import Sequence
|
|
8
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
10
11
|
import numpy as np
|
|
@@ -64,6 +65,7 @@ class InputFileWriter:
|
|
|
64
65
|
exponential_attenuation_pml_interior_offset_px: int = 0,
|
|
65
66
|
use_gpu: bool = False,
|
|
66
67
|
write_maps_the_solver_does_not_read: bool = False,
|
|
68
|
+
hold_hard_source: bool = False,
|
|
67
69
|
) -> None:
|
|
68
70
|
"""Initialize the InputGeneratorBase instance.
|
|
69
71
|
|
|
@@ -124,6 +126,11 @@ class InputFileWriter:
|
|
|
124
126
|
which an older solver binary needs. The record the skipped maps
|
|
125
127
|
replace is impedance_constraint.dat, and this writer always writes it
|
|
126
128
|
on the relaxation path.
|
|
129
|
+
hold_hard_source : bool, optional
|
|
130
|
+
Whether the hard source cells stay at zero pressure after the drive ends.
|
|
131
|
+
True writes the flag file ``hold_source.dat``, which the solver reads to keep
|
|
132
|
+
the cells at zero for the rest of the run; the signals are written as given.
|
|
133
|
+
False, the default, lets the solver release the cells when the signal ends.
|
|
127
134
|
|
|
128
135
|
"""
|
|
129
136
|
logger.debug("Initializing InputFileWriter instance.")
|
|
@@ -139,6 +146,7 @@ class InputFileWriter:
|
|
|
139
146
|
self.use_isotropic_relaxation = use_isotropic_relaxation
|
|
140
147
|
self.write_maps_the_solver_does_not_read = write_maps_the_solver_does_not_read
|
|
141
148
|
self.use_gpu = use_gpu
|
|
149
|
+
self.hold_hard_source = hold_hard_source
|
|
142
150
|
|
|
143
151
|
if validate_input:
|
|
144
152
|
check_functions.check_path_exists(self.path_fullwave_simulation_bin)
|
|
@@ -347,10 +355,21 @@ class InputFileWriter:
|
|
|
347
355
|
|
|
348
356
|
"""
|
|
349
357
|
if members is None:
|
|
350
|
-
|
|
351
|
-
|
|
358
|
+
signal = getattr(self.source, name, None)
|
|
359
|
+
elif getattr(self.source, name, None) is None:
|
|
352
360
|
return None
|
|
353
|
-
|
|
361
|
+
else:
|
|
362
|
+
signal = np.concatenate([getattr(member, name) for member in members], axis=0)
|
|
363
|
+
return signal
|
|
364
|
+
|
|
365
|
+
@property
|
|
366
|
+
def _holds_the_source(self) -> bool:
|
|
367
|
+
"""Whether the solver keeps the hard source cells at zero after the drive ends."""
|
|
368
|
+
return (
|
|
369
|
+
self.hold_hard_source
|
|
370
|
+
and self.source.n_sources > 0
|
|
371
|
+
and self.source.icmat.shape[1] < self.grid.nt
|
|
372
|
+
)
|
|
354
373
|
|
|
355
374
|
# --- constructor utils ---
|
|
356
375
|
|
|
@@ -909,8 +928,6 @@ class InputFileWriter:
|
|
|
909
928
|
(c_min_val, c_max_val) of the sound speed.
|
|
910
929
|
|
|
911
930
|
"""
|
|
912
|
-
from concurrent.futures import ThreadPoolExecutor, as_completed # noqa: PLC0415
|
|
913
|
-
|
|
914
931
|
import cupy as cp # noqa: PLC0415
|
|
915
932
|
|
|
916
933
|
sound_speed_np = self.medium.sound_speed
|
|
@@ -1259,6 +1276,7 @@ class InputFileWriter:
|
|
|
1259
1276
|
"ncoordsout",
|
|
1260
1277
|
"ncoordszero",
|
|
1261
1278
|
"nTic",
|
|
1279
|
+
"hold_source",
|
|
1262
1280
|
"modT",
|
|
1263
1281
|
"modX",
|
|
1264
1282
|
"modY",
|
|
@@ -1357,7 +1375,6 @@ class InputFileWriter:
|
|
|
1357
1375
|
self._queue_v_abs_write(np.float32, save_path, var)
|
|
1358
1376
|
|
|
1359
1377
|
def _save_coords_params(self, simulation_dir: Path) -> None:
|
|
1360
|
-
nt_ic = self.source.icmat.shape[1]
|
|
1361
1378
|
var_list = [
|
|
1362
1379
|
("nX", self.grid.nx),
|
|
1363
1380
|
("nY", self.grid.ny),
|
|
@@ -1365,9 +1382,11 @@ class InputFileWriter:
|
|
|
1365
1382
|
("ncoords", self.source.n_sources),
|
|
1366
1383
|
("ncoordsout", self.sensor.n_sensors),
|
|
1367
1384
|
("ncoordszero", self.medium.n_air),
|
|
1368
|
-
("nTic",
|
|
1385
|
+
("nTic", self.source.icmat.shape[1]),
|
|
1369
1386
|
("modT", self.sensor.sampling_modulus_time),
|
|
1370
1387
|
]
|
|
1388
|
+
if self._holds_the_source:
|
|
1389
|
+
var_list.append(("hold_source", 1))
|
|
1371
1390
|
n_sources_add = getattr(self.source, "n_sources_add", 0)
|
|
1372
1391
|
if n_sources_add > 0:
|
|
1373
1392
|
var_list.append(("ncoords_add", n_sources_add))
|
fullwave/solver/launcher.py
CHANGED
|
@@ -13,6 +13,7 @@ from time import time
|
|
|
13
13
|
import numpy as np
|
|
14
14
|
from numpy.typing import NDArray
|
|
15
15
|
|
|
16
|
+
from .cuda_utils import find_consecutive_gpus_without_peer_access
|
|
16
17
|
from .utils import load_dat_data
|
|
17
18
|
|
|
18
19
|
logger = logging.getLogger("__main__." + __name__)
|
|
@@ -211,8 +212,6 @@ class Launcher:
|
|
|
211
212
|
)
|
|
212
213
|
return
|
|
213
214
|
try:
|
|
214
|
-
from .cuda_utils import find_consecutive_gpus_without_peer_access # noqa: PLC0415
|
|
215
|
-
|
|
216
215
|
pairs = find_consecutive_gpus_without_peer_access(device_ids)
|
|
217
216
|
except (AttributeError, NameError, OSError, RuntimeError, ctypes.ArgumentError) as error:
|
|
218
217
|
logger.debug("Peer access check skipped: %s", error)
|
fullwave/solver/solver.py
CHANGED
|
@@ -7,6 +7,7 @@ import os
|
|
|
7
7
|
import shutil
|
|
8
8
|
import subprocess
|
|
9
9
|
import time
|
|
10
|
+
import warnings
|
|
10
11
|
from collections.abc import Iterator, Sequence
|
|
11
12
|
from pathlib import Path
|
|
12
13
|
|
|
@@ -18,18 +19,21 @@ from fullwave.solver.input_file_writer import InputFileWriter
|
|
|
18
19
|
from fullwave.solver.launcher import Launcher
|
|
19
20
|
from fullwave.solver.pml_builder import PMLBuilder, PMLBuilderExponentialAttenuation
|
|
20
21
|
from fullwave.solver.source_type import (
|
|
21
|
-
|
|
22
|
+
HARD_RELEASED_AFTER,
|
|
22
23
|
SOURCE_TYPES,
|
|
23
24
|
as_additive_source,
|
|
25
|
+
is_hard_held,
|
|
24
26
|
is_soft,
|
|
25
27
|
)
|
|
28
|
+
from fullwave.ultra import backend, binary, factory
|
|
26
29
|
from fullwave.utils import (
|
|
27
|
-
MemoryTempfile,
|
|
28
30
|
check_functions,
|
|
31
|
+
run_directory_in_memory,
|
|
29
32
|
)
|
|
30
33
|
from fullwave.utils.signal_filter import apply_filter
|
|
31
34
|
|
|
32
35
|
from .binary_manager import ensure_binary
|
|
36
|
+
from .binary_markers import check_binary_holds_source
|
|
33
37
|
from .cuda_utils import get_cuda_architecture, retrieve_cuda_version
|
|
34
38
|
from .utils import load_dat_data
|
|
35
39
|
|
|
@@ -40,6 +44,9 @@ logger = logging.getLogger("__main__." + __name__)
|
|
|
40
44
|
# that, so a larger run only costs memory.
|
|
41
45
|
DEFAULT_MAX_MEMBERS_PER_RUN = 32
|
|
42
46
|
|
|
47
|
+
# The smallest points per wavelength (ppw) the relaxation attenuation model is validated for.
|
|
48
|
+
MIN_VALIDATED_PPW_RELAXATION = 8
|
|
49
|
+
|
|
43
50
|
|
|
44
51
|
def _split_evenly(members: list, largest: int) -> list[list]:
|
|
45
52
|
"""Split the transmits into the fewest runs of at most ``largest``, of nearly equal size.
|
|
@@ -476,6 +483,9 @@ class Solver:
|
|
|
476
483
|
generates the required input files, and runs the simulation executable.
|
|
477
484
|
"""
|
|
478
485
|
|
|
486
|
+
#: The experimental fullwave2-ultra backend; set only by ``use_ultra=True``.
|
|
487
|
+
ultra = None
|
|
488
|
+
|
|
479
489
|
def __init__( # noqa: PLR0912
|
|
480
490
|
self,
|
|
481
491
|
work_dir: Path,
|
|
@@ -500,8 +510,9 @@ class Solver:
|
|
|
500
510
|
verify_gpu: bool = True,
|
|
501
511
|
use_gpu_pml: bool = False,
|
|
502
512
|
pml_alpha_entrance: float | None = None,
|
|
503
|
-
source_type: str =
|
|
513
|
+
source_type: str = HARD_RELEASED_AFTER,
|
|
504
514
|
write_maps_the_solver_does_not_read: bool = False,
|
|
515
|
+
use_ultra: bool = False,
|
|
505
516
|
) -> None:
|
|
506
517
|
"""Initialize a Solver instance for the fullwave simulation.
|
|
507
518
|
|
|
@@ -627,6 +638,21 @@ class Solver:
|
|
|
627
638
|
relaxation path. Set it to True for a solver binary that predates
|
|
628
639
|
that record. Such a binary stops with an error on kappax.dat rather
|
|
629
640
|
than returning a wrong answer.
|
|
641
|
+
source_type : str, optional
|
|
642
|
+
How the source drives the field. "hard_released_after", the default,
|
|
643
|
+
assigns the source pressure while the drive signal plays, then lets the
|
|
644
|
+
source cells evolve freely. "hard" assigns it for the whole run, so the
|
|
645
|
+
cells stay at zero pressure after the signal ends. "soft" adds the signal
|
|
646
|
+
to the field. ``use_ultra`` follows the same three values.
|
|
647
|
+
use_ultra : bool, optional
|
|
648
|
+
Experimental. Run a user-supplied fullwave2-ultra binary (not shipped; pass it
|
|
649
|
+
as ``path_fullwave_simulation_bin`` or set ``FW2U_BATCH_BIN``): ``bench_2d_batch``
|
|
650
|
+
for a 2D grid, ``bench_3d_opt`` for a 3D grid.
|
|
651
|
+
``exponential_attenuation_pml_thickness_px`` sets the sponge width in cells.
|
|
652
|
+
A source with ``p0_additive`` and ``source_type="soft"`` are added to the pressure
|
|
653
|
+
through the binary's additive channel, which needs a binary that reads
|
|
654
|
+
``ncoords_add.dat`` (and ``icmat_add.dat`` on a 3D grid). Unsupported options raise
|
|
655
|
+
``UltraUnsupportedError``.
|
|
630
656
|
|
|
631
657
|
Raises
|
|
632
658
|
------
|
|
@@ -637,6 +663,34 @@ class Solver:
|
|
|
637
663
|
and transducer) are defined simultaneously.
|
|
638
664
|
|
|
639
665
|
"""
|
|
666
|
+
self.ultra = None
|
|
667
|
+
if use_ultra:
|
|
668
|
+
self._init_ultra(
|
|
669
|
+
work_dir=work_dir,
|
|
670
|
+
grid=grid,
|
|
671
|
+
medium=medium,
|
|
672
|
+
source=source,
|
|
673
|
+
sensor=sensor,
|
|
674
|
+
transducer=transducer,
|
|
675
|
+
path_fullwave_simulation_bin=path_fullwave_simulation_bin,
|
|
676
|
+
run_on_memory=run_on_memory,
|
|
677
|
+
cuda_device_id=cuda_device_id,
|
|
678
|
+
verify_gpu=verify_gpu,
|
|
679
|
+
nbdy=exponential_attenuation_pml_thickness_px,
|
|
680
|
+
options={
|
|
681
|
+
"use_pml": use_pml,
|
|
682
|
+
"m_spatial_order": m_spatial_order,
|
|
683
|
+
"pml_layer_thickness_px": pml_layer_thickness_px,
|
|
684
|
+
"n_transition_layer": n_transition_layer,
|
|
685
|
+
"use_gpu": use_gpu,
|
|
686
|
+
"use_gpu_pml": use_gpu_pml,
|
|
687
|
+
"pml_alpha_entrance": pml_alpha_entrance,
|
|
688
|
+
"source_type": source_type,
|
|
689
|
+
"save_gpu_memory": save_gpu_memory,
|
|
690
|
+
"write_maps_the_solver_does_not_read": write_maps_the_solver_does_not_read,
|
|
691
|
+
},
|
|
692
|
+
)
|
|
693
|
+
return
|
|
640
694
|
if exponential_attenuation_pml_thickness_px is not None and not use_exponential_attenuation:
|
|
641
695
|
error_msg = (
|
|
642
696
|
"exponential_attenuation_pml_thickness_px belongs to the exponential "
|
|
@@ -646,6 +700,14 @@ class Solver:
|
|
|
646
700
|
)
|
|
647
701
|
raise ValueError(error_msg)
|
|
648
702
|
|
|
703
|
+
if not use_exponential_attenuation and grid.ppw < MIN_VALIDATED_PPW_RELAXATION:
|
|
704
|
+
warnings.warn(
|
|
705
|
+
f"ppw={grid.ppw} is below {MIN_VALIDATED_PPW_RELAXATION}. "
|
|
706
|
+
f"The relaxation model is validated for ppw >= {MIN_VALIDATED_PPW_RELAXATION}.",
|
|
707
|
+
UserWarning,
|
|
708
|
+
stacklevel=2,
|
|
709
|
+
)
|
|
710
|
+
|
|
649
711
|
# type hints
|
|
650
712
|
self.source: fullwave.Source
|
|
651
713
|
self.sensor: fullwave.Sensor
|
|
@@ -680,14 +742,7 @@ class Solver:
|
|
|
680
742
|
"\n"
|
|
681
743
|
)
|
|
682
744
|
logger.info(message)
|
|
683
|
-
self.
|
|
684
|
-
preferred_paths=["/run/user/{uid}"],
|
|
685
|
-
remove_paths=["/dev/shm", "/run/shm"], # noqa: S108
|
|
686
|
-
additional_paths=["/var/run"],
|
|
687
|
-
filesystem_types=["tmpfs"],
|
|
688
|
-
fallback=True,
|
|
689
|
-
)
|
|
690
|
-
self.tempdir = self.tempfile.TemporaryDirectory()
|
|
745
|
+
self.tempdir = run_directory_in_memory()
|
|
691
746
|
self.work_dir = Path(self.tempdir.name)
|
|
692
747
|
else:
|
|
693
748
|
self.work_dir = work_dir
|
|
@@ -799,6 +854,7 @@ class Solver:
|
|
|
799
854
|
use_exponential_attenuation=use_exponential_attenuation,
|
|
800
855
|
)
|
|
801
856
|
logger.info("source_type=%s, the source is driven by addition", source_type)
|
|
857
|
+
self._check_binary_holds_source(path_fullwave_simulation_bin)
|
|
802
858
|
|
|
803
859
|
if sensor is not None:
|
|
804
860
|
self.sensor = sensor
|
|
@@ -849,6 +905,49 @@ class Solver:
|
|
|
849
905
|
pml_alpha_entrance=pml_alpha_entrance,
|
|
850
906
|
)
|
|
851
907
|
|
|
908
|
+
def _init_ultra(
|
|
909
|
+
self,
|
|
910
|
+
*,
|
|
911
|
+
work_dir: Path,
|
|
912
|
+
grid: fullwave.Grid,
|
|
913
|
+
medium: fullwave.Medium,
|
|
914
|
+
source: fullwave.Source | None,
|
|
915
|
+
sensor: fullwave.Sensor | None,
|
|
916
|
+
transducer: fullwave.Transducer | None,
|
|
917
|
+
path_fullwave_simulation_bin: Path | None,
|
|
918
|
+
run_on_memory: bool | None,
|
|
919
|
+
cuda_device_id: str | int | list | None,
|
|
920
|
+
verify_gpu: bool,
|
|
921
|
+
nbdy: int | None,
|
|
922
|
+
options: dict,
|
|
923
|
+
) -> None:
|
|
924
|
+
"""Set up the experimental fullwave2-ultra backend (see ``fullwave.ultra``)."""
|
|
925
|
+
backend.warn_experimental()
|
|
926
|
+
backend.warn_ppw_above_speed_optimum(ppw=grid.ppw)
|
|
927
|
+
backend.check_solver_options(**options)
|
|
928
|
+
binary_name = binary.BINARY_NAME_3D if grid.is_3d else binary.BINARY_NAME_2D
|
|
929
|
+
ultra_binary = binary.resolve_binary(path_fullwave_simulation_bin, name=binary_name)
|
|
930
|
+
self._check_input(grid, medium, source, sensor, transducer, ultra_binary)
|
|
931
|
+
if source is None:
|
|
932
|
+
source = transducer.source
|
|
933
|
+
if sensor is None:
|
|
934
|
+
sensor = transducer.sensor
|
|
935
|
+
self.grid, self.medium, self.source, self.sensor = grid, medium, source, sensor
|
|
936
|
+
self.is_3d = grid.is_3d
|
|
937
|
+
self.ultra = factory.make_backend(
|
|
938
|
+
work_dir=Path(work_dir),
|
|
939
|
+
grid=grid,
|
|
940
|
+
medium=medium,
|
|
941
|
+
source=source,
|
|
942
|
+
sensor=sensor,
|
|
943
|
+
binary=ultra_binary,
|
|
944
|
+
run_on_memory=run_on_memory,
|
|
945
|
+
nbdy=nbdy,
|
|
946
|
+
cuda_device_id=cuda_device_id,
|
|
947
|
+
verify_gpu=verify_gpu,
|
|
948
|
+
source_type=options["source_type"],
|
|
949
|
+
)
|
|
950
|
+
|
|
852
951
|
def _warn_if_the_additive_signal_was_not_scaled(self) -> None:
|
|
853
952
|
"""Say so when an additive signal was built by hand and may carry no scale.
|
|
854
953
|
|
|
@@ -1170,6 +1269,16 @@ class Solver:
|
|
|
1170
1269
|
or if either filter option is set but ``load_results=False``.
|
|
1171
1270
|
|
|
1172
1271
|
"""
|
|
1272
|
+
if self.ultra is not None:
|
|
1273
|
+
return self.ultra.run(
|
|
1274
|
+
simulation_dir_name,
|
|
1275
|
+
is_static_map=is_static_map,
|
|
1276
|
+
record_whole_domain=record_whole_domain,
|
|
1277
|
+
load_results=load_results,
|
|
1278
|
+
generate_input_only=generate_input_only,
|
|
1279
|
+
highpass_cutoff_mhz=highpass_cutoff_mhz,
|
|
1280
|
+
bandpass_cutoff_mhz=bandpass_cutoff_mhz,
|
|
1281
|
+
)
|
|
1173
1282
|
return self._run_one(
|
|
1174
1283
|
simulation_dir_name,
|
|
1175
1284
|
members=None,
|
|
@@ -1288,6 +1397,7 @@ class Solver:
|
|
|
1288
1397
|
exponential_attenuation_pml_interior_offset_px=interior_offset,
|
|
1289
1398
|
use_gpu=self.use_gpu_pml,
|
|
1290
1399
|
write_maps_the_solver_does_not_read=self.write_maps_the_solver_does_not_read,
|
|
1400
|
+
hold_hard_source=is_hard_held(self.source_type),
|
|
1291
1401
|
)
|
|
1292
1402
|
simulation_dir = input_file_writer.run(
|
|
1293
1403
|
simulation_dir_name,
|
|
@@ -1529,6 +1639,18 @@ class Solver:
|
|
|
1529
1639
|
transmit in one simulation.
|
|
1530
1640
|
|
|
1531
1641
|
"""
|
|
1642
|
+
if self.ultra is not None:
|
|
1643
|
+
return self.ultra.run_batch(
|
|
1644
|
+
sources,
|
|
1645
|
+
simulation_dir_name,
|
|
1646
|
+
max_members_per_run=max_members_per_run,
|
|
1647
|
+
is_static_map=is_static_map,
|
|
1648
|
+
record_whole_domain=record_whole_domain,
|
|
1649
|
+
load_results=load_results,
|
|
1650
|
+
generate_input_only=generate_input_only,
|
|
1651
|
+
highpass_cutoff_mhz=highpass_cutoff_mhz,
|
|
1652
|
+
bandpass_cutoff_mhz=bandpass_cutoff_mhz,
|
|
1653
|
+
)
|
|
1532
1654
|
members = self._batch_members(sources)
|
|
1533
1655
|
# The sensor and the sizing below need the extended grid and medium.
|
|
1534
1656
|
self.pml_builder.run(use_pml=self.use_pml)
|
|
@@ -1702,7 +1824,9 @@ class Solver:
|
|
|
1702
1824
|
use_isotropic_relaxation=self.use_isotropic_relaxation,
|
|
1703
1825
|
batch=True,
|
|
1704
1826
|
)
|
|
1705
|
-
|
|
1827
|
+
binary = ensure_binary(local_path)
|
|
1828
|
+
self._check_binary_holds_source(binary)
|
|
1829
|
+
return binary
|
|
1706
1830
|
name = Path(self.path_fullwave_simulation_bin).name
|
|
1707
1831
|
if "batch" not in name:
|
|
1708
1832
|
error_msg = (
|
|
@@ -1714,6 +1838,23 @@ class Solver:
|
|
|
1714
1838
|
raise ValueError(error_msg)
|
|
1715
1839
|
return Path(self.path_fullwave_simulation_bin)
|
|
1716
1840
|
|
|
1841
|
+
def _check_binary_holds_source(self, binary: Path) -> None:
|
|
1842
|
+
"""Refuse a binary that cannot hold the hard source, when the run needs it held.
|
|
1843
|
+
|
|
1844
|
+
Parameters
|
|
1845
|
+
----------
|
|
1846
|
+
binary : Path
|
|
1847
|
+
The binary that runs the simulation.
|
|
1848
|
+
|
|
1849
|
+
"""
|
|
1850
|
+
holds_source = (
|
|
1851
|
+
is_hard_held(self.source_type)
|
|
1852
|
+
and self.source.n_sources > 0
|
|
1853
|
+
and self.source.icmat.shape[1] < self.grid.nt
|
|
1854
|
+
)
|
|
1855
|
+
if holds_source:
|
|
1856
|
+
check_binary_holds_source(binary)
|
|
1857
|
+
|
|
1717
1858
|
@contextlib.contextmanager
|
|
1718
1859
|
def _binary_in_use(self, binary: Path) -> Iterator[None]:
|
|
1719
1860
|
"""Run the simulations of the block on ``binary``, then restore the Solver's binary.
|
fullwave/solver/source_type.py
CHANGED
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Source types: hard, hard released after the drive, and soft.
|
|
2
|
+
|
|
3
|
+
``"hard"`` assigns the source pressure for the whole run, so the source cells stay at
|
|
4
|
+
zero pressure after the drive signal ends. ``"hard_released_after"`` assigns the
|
|
5
|
+
pressure while the drive signal plays, then lets the cells evolve freely.
|
|
6
|
+
``"soft"`` adds the signal to the field, as described below.
|
|
7
|
+
|
|
8
|
+
A soft source drives the source by addition rather than by assignment.
|
|
2
9
|
|
|
3
10
|
A hard source sets the pressure of one grid row on every step, while the
|
|
4
11
|
staggered derivative spans `2 * m_spatial_order` pressure points. One hard source row
|
|
@@ -19,14 +26,15 @@ from typing import TYPE_CHECKING
|
|
|
19
26
|
import numpy as np
|
|
20
27
|
from numpy.typing import NDArray
|
|
21
28
|
|
|
22
|
-
from fullwave.source import Source
|
|
29
|
+
from fullwave.source import Source, additive_signal_scale
|
|
23
30
|
|
|
24
31
|
if TYPE_CHECKING:
|
|
25
32
|
import fullwave
|
|
26
33
|
|
|
27
34
|
HARD = "hard"
|
|
35
|
+
HARD_RELEASED_AFTER = "hard_released_after"
|
|
28
36
|
SOFT = "soft"
|
|
29
|
-
SOURCE_TYPES = (HARD, SOFT)
|
|
37
|
+
SOURCE_TYPES = (HARD, HARD_RELEASED_AFTER, SOFT)
|
|
30
38
|
|
|
31
39
|
|
|
32
40
|
def _is_on_the_device(array: object) -> bool:
|
|
@@ -62,6 +70,11 @@ def is_soft(source_type: str) -> bool:
|
|
|
62
70
|
return source_type == SOFT
|
|
63
71
|
|
|
64
72
|
|
|
73
|
+
def is_hard_held(source_type: str) -> bool:
|
|
74
|
+
"""Return whether the source cells stay at zero pressure after the drive ends."""
|
|
75
|
+
return source_type == HARD
|
|
76
|
+
|
|
77
|
+
|
|
65
78
|
def source_row(coords: NDArray[np.int64], grid_shape: tuple[int, ...]) -> int:
|
|
66
79
|
"""Return the row the source occupies, and check that it fills one whole row.
|
|
67
80
|
|
|
@@ -101,15 +114,6 @@ def source_row(coords: NDArray[np.int64], grid_shape: tuple[int, ...]) -> int:
|
|
|
101
114
|
return int(rows[0])
|
|
102
115
|
|
|
103
116
|
|
|
104
|
-
def additive_signal_scale(
|
|
105
|
-
sound_speed: NDArray[np.float64] | float,
|
|
106
|
-
dt: float,
|
|
107
|
-
dx: float,
|
|
108
|
-
) -> NDArray[np.float64] | float:
|
|
109
|
-
"""Return the factor that makes an additive source radiate its nominal pressure."""
|
|
110
|
-
return 2.0 * sound_speed * dt / dx
|
|
111
|
-
|
|
112
|
-
|
|
113
117
|
def node_sound_speeds(
|
|
114
118
|
sound_speed: NDArray[np.float64] | float,
|
|
115
119
|
coords: NDArray[np.int64],
|
fullwave/source.py
CHANGED
|
@@ -18,6 +18,15 @@ if TYPE_CHECKING:
|
|
|
18
18
|
logger = logging.getLogger("__main__." + __name__)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
+
def additive_signal_scale(
|
|
22
|
+
sound_speed: NDArray[np.float64] | float,
|
|
23
|
+
dt: float,
|
|
24
|
+
dx: float,
|
|
25
|
+
) -> NDArray[np.float64] | float:
|
|
26
|
+
"""Return the factor that makes an additive source radiate its nominal pressure."""
|
|
27
|
+
return 2.0 * sound_speed * dt / dx
|
|
28
|
+
|
|
29
|
+
|
|
21
30
|
@dataclass
|
|
22
31
|
class Source:
|
|
23
32
|
"""Source class for Fullwave."""
|
|
@@ -272,8 +281,6 @@ class Source:
|
|
|
272
281
|
A source with no assigned positions and a scaled additive signal.
|
|
273
282
|
|
|
274
283
|
"""
|
|
275
|
-
from fullwave.solver.source_type import additive_signal_scale # noqa: PLC0415
|
|
276
|
-
|
|
277
284
|
speed = float(grid.c0) if sound_speed is None else sound_speed
|
|
278
285
|
scale = additive_signal_scale(speed, float(grid.dt), float(grid.dx))
|
|
279
286
|
scale = np.atleast_1d(np.asarray(scale, dtype=float))
|