fullwave25 1.4.2.dev0__py3-none-any.whl → 1.4.2.dev2__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 +189 -93
- 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 +497 -0
- fullwave/ultra/backend_2d.py +569 -0
- fullwave/ultra/binary.py +158 -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 +215 -0
- fullwave/ultra/writer_3d.py +199 -0
- fullwave/utils/__init__.py +2 -1
- fullwave/utils/gpu_memory.py +109 -0
- fullwave/utils/memory_tempfile.py +21 -0
- {fullwave25-1.4.2.dev0.dist-info → fullwave25-1.4.2.dev2.dist-info}/METADATA +58 -3
- {fullwave25-1.4.2.dev0.dist-info → fullwave25-1.4.2.dev2.dist-info}/RECORD +30 -15
- {fullwave25-1.4.2.dev0.dist-info → fullwave25-1.4.2.dev2.dist-info}/WHEEL +1 -1
- {fullwave25-1.4.2.dev0.dist-info → fullwave25-1.4.2.dev2.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-dev2"
|
|
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)
|