fullwave25 1.4.2.dev1__py3-none-any.whl → 1.4.2.dev3__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/beamformer/beamformer.py +12 -3
- fullwave/solver/solver.py +48 -93
- fullwave/transducer.py +50 -2
- fullwave/ultra/backend.py +109 -27
- fullwave/ultra/backend_2d.py +203 -9
- fullwave/ultra/binary.py +8 -2
- fullwave/ultra/writer_2d.py +22 -2
- fullwave/utils/gpu_memory.py +109 -0
- fullwave/utils/relaxation_parameters.py +51 -0
- {fullwave25-1.4.2.dev1.dist-info → fullwave25-1.4.2.dev3.dist-info}/METADATA +8 -2
- {fullwave25-1.4.2.dev1.dist-info → fullwave25-1.4.2.dev3.dist-info}/RECORD +14 -13
- {fullwave25-1.4.2.dev1.dist-info → fullwave25-1.4.2.dev3.dist-info}/WHEEL +0 -0
- {fullwave25-1.4.2.dev1.dist-info → fullwave25-1.4.2.dev3.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-dev3"
|
|
70
70
|
|
|
71
71
|
VERSION = __version__ # for convenience
|
|
72
72
|
logger.info("Fullwave version: %s", __version__)
|
|
@@ -14,6 +14,13 @@ class Beamformer:
|
|
|
14
14
|
For faster implementations, consider using libraries such as
|
|
15
15
|
mach beamformer: https://github.com/Forest-Neurotech/mach
|
|
16
16
|
|
|
17
|
+
Conventions: ``axial_position_m`` is measured from the top of the grid
|
|
18
|
+
(row 0), not from the array. ``transducer_coordinates`` must be the
|
|
19
|
+
receiver positions in grid points (for example from
|
|
20
|
+
``Transducer.element_id_to_receiver_center``). The time origin is the
|
|
21
|
+
mean receiver row, so the array depth is subtracted from the two-way
|
|
22
|
+
travel time.
|
|
23
|
+
|
|
17
24
|
References
|
|
18
25
|
----------
|
|
19
26
|
- Fullwave2 BMME890 implementation
|
|
@@ -46,11 +53,11 @@ class Beamformer:
|
|
|
46
53
|
lateral_position_m : np.ndarray
|
|
47
54
|
Lateral positions (m) where the beamformed image will be computed.
|
|
48
55
|
axial_position_m : np.ndarray
|
|
49
|
-
Axial positions (m) where the
|
|
56
|
+
Axial positions (m) from the top of the grid where the image is computed.
|
|
50
57
|
num_elements : int
|
|
51
58
|
Number of transducer elements.
|
|
52
59
|
transducer_coordinates : np.ndarray
|
|
53
|
-
|
|
60
|
+
Receiver coordinates [n_elements, 2] in grid points (row, column).
|
|
54
61
|
f_number : float, optional
|
|
55
62
|
F-number for aperture calculation, by default 1.0.
|
|
56
63
|
|
|
@@ -83,6 +90,8 @@ class Beamformer:
|
|
|
83
90
|
if signals.ndim == 2:
|
|
84
91
|
signals = signals[:, :, np.newaxis]
|
|
85
92
|
n_transmit = signals.shape[2]
|
|
93
|
+
# idt_0 is the pulse arrival at the receivers, so time is measured from the array.
|
|
94
|
+
z_array = np.mean(self.transducer_coordinates[:, 0]) * self.dx
|
|
86
95
|
|
|
87
96
|
hilbert_signals = hilbert(signals, axis=1)
|
|
88
97
|
beamformed_image = np.zeros(
|
|
@@ -133,7 +142,7 @@ class Beamformer:
|
|
|
133
142
|
dd = np.round(dd / (self.dt / self.dx * self.c0)).astype(int)
|
|
134
143
|
dd = dd - dd.min()
|
|
135
144
|
|
|
136
|
-
idt = idt_0 + int(2 * axial / self.c0 / self.dt)
|
|
145
|
+
idt = idt_0 + int(2 * (axial - z_array) / self.c0 / self.dt)
|
|
137
146
|
|
|
138
147
|
# idp: 1D indices into signals flattened array
|
|
139
148
|
# we convert 2D indices to 1D indices
|
fullwave/solver/solver.py
CHANGED
|
@@ -1,11 +1,8 @@
|
|
|
1
1
|
"""solver module."""
|
|
2
2
|
|
|
3
3
|
import contextlib
|
|
4
|
-
import gc
|
|
5
4
|
import logging
|
|
6
5
|
import os
|
|
7
|
-
import shutil
|
|
8
|
-
import subprocess
|
|
9
6
|
import time
|
|
10
7
|
import warnings
|
|
11
8
|
from collections.abc import Iterator, Sequence
|
|
@@ -30,6 +27,11 @@ from fullwave.utils import (
|
|
|
30
27
|
check_functions,
|
|
31
28
|
run_directory_in_memory,
|
|
32
29
|
)
|
|
30
|
+
from fullwave.utils.gpu_memory import (
|
|
31
|
+
free_gpu_memory_bytes,
|
|
32
|
+
members_that_fit,
|
|
33
|
+
release_gpu_memory_pools,
|
|
34
|
+
)
|
|
33
35
|
from fullwave.utils.signal_filter import apply_filter
|
|
34
36
|
|
|
35
37
|
from .binary_manager import ensure_binary
|
|
@@ -74,48 +76,6 @@ def _split_evenly(members: list, largest: int) -> list[list]:
|
|
|
74
76
|
return runs
|
|
75
77
|
|
|
76
78
|
|
|
77
|
-
def _free_gpu_memory_bytes(device_ids: Sequence[str]) -> dict[str, int] | None:
|
|
78
|
-
"""Return the free memory of each GPU in bytes, as nvidia-smi reports it.
|
|
79
|
-
|
|
80
|
-
Parameters
|
|
81
|
-
----------
|
|
82
|
-
device_ids : Sequence[str]
|
|
83
|
-
The GPU indices, as nvidia-smi counts them.
|
|
84
|
-
|
|
85
|
-
Returns
|
|
86
|
-
-------
|
|
87
|
-
dict[str, int] | None
|
|
88
|
-
The free bytes of each GPU, or None when nvidia-smi cannot be run or does not
|
|
89
|
-
report every GPU.
|
|
90
|
-
|
|
91
|
-
"""
|
|
92
|
-
wanted = [device.strip() for device in device_ids]
|
|
93
|
-
nvidia_smi_path = shutil.which("nvidia-smi")
|
|
94
|
-
if nvidia_smi_path is None:
|
|
95
|
-
return None
|
|
96
|
-
try:
|
|
97
|
-
found = subprocess.run( # noqa: S603
|
|
98
|
-
[nvidia_smi_path, "--query-gpu=index,memory.free", "--format=csv,noheader,nounits"],
|
|
99
|
-
capture_output=True,
|
|
100
|
-
text=True,
|
|
101
|
-
check=True,
|
|
102
|
-
timeout=30,
|
|
103
|
-
).stdout
|
|
104
|
-
except (OSError, subprocess.SubprocessError):
|
|
105
|
-
return None
|
|
106
|
-
free = {}
|
|
107
|
-
for line in found.splitlines():
|
|
108
|
-
index, _, mebibytes = line.partition(",")
|
|
109
|
-
if index.strip() in wanted:
|
|
110
|
-
try:
|
|
111
|
-
free[index.strip()] = int(float(mebibytes)) * 1024**2
|
|
112
|
-
except ValueError:
|
|
113
|
-
return None
|
|
114
|
-
if set(free) != set(wanted):
|
|
115
|
-
return None
|
|
116
|
-
return free
|
|
117
|
-
|
|
118
|
-
|
|
119
79
|
COMPATIBLE_CUDA_ARCHITECTURES = [
|
|
120
80
|
"sm_50", # Maxwell: GTX 750, GTX 750 Ti
|
|
121
81
|
"sm_52", # Maxwell: GTX 980, GTX 970
|
|
@@ -513,6 +473,7 @@ class Solver:
|
|
|
513
473
|
source_type: str = HARD_RELEASED_AFTER,
|
|
514
474
|
write_maps_the_solver_does_not_read: bool = False,
|
|
515
475
|
use_ultra: bool = False,
|
|
476
|
+
ultra_sponge_thickness_px: int | None = None,
|
|
516
477
|
) -> None:
|
|
517
478
|
"""Initialize a Solver instance for the fullwave simulation.
|
|
518
479
|
|
|
@@ -648,11 +609,17 @@ class Solver:
|
|
|
648
609
|
Experimental. Run a user-supplied fullwave2-ultra binary (not shipped; pass it
|
|
649
610
|
as ``path_fullwave_simulation_bin`` or set ``FW2U_BATCH_BIN``): ``bench_2d_batch``
|
|
650
611
|
for a 2D grid, ``bench_3d_opt`` for a 3D grid.
|
|
651
|
-
|
|
652
|
-
A source with ``p0_additive`` and
|
|
653
|
-
|
|
654
|
-
``ncoords_add.dat`` (and ``icmat_add.dat``
|
|
655
|
-
|
|
612
|
+
The backend runs the exponential attenuation model (frequency-independent:
|
|
613
|
+
the same attenuation at every frequency). A source with ``p0_additive`` and
|
|
614
|
+
``source_type="soft"`` are added to the pressure through the binary's additive
|
|
615
|
+
channel, which needs a binary that reads ``ncoords_add.dat`` (and ``icmat_add.dat``
|
|
616
|
+
on a 3D grid). Options that only tune the native solver are ignored with a
|
|
617
|
+
warning; the README lists them.
|
|
618
|
+
Other unsupported options raise ``UltraUnsupportedError``.
|
|
619
|
+
ultra_sponge_thickness_px : int, optional
|
|
620
|
+
Width of the absorbing sponge of the ``use_ultra`` backend, in cells.
|
|
621
|
+
None, the default, gives 6 wavelengths in 2D and 4 wavelengths in 3D.
|
|
622
|
+
Used only with ``use_ultra=True``.
|
|
656
623
|
|
|
657
624
|
Raises
|
|
658
625
|
------
|
|
@@ -676,21 +643,32 @@ class Solver:
|
|
|
676
643
|
run_on_memory=run_on_memory,
|
|
677
644
|
cuda_device_id=cuda_device_id,
|
|
678
645
|
verify_gpu=verify_gpu,
|
|
679
|
-
nbdy=
|
|
646
|
+
nbdy=ultra_sponge_thickness_px,
|
|
680
647
|
options={
|
|
681
648
|
"use_pml": use_pml,
|
|
682
649
|
"m_spatial_order": m_spatial_order,
|
|
683
|
-
"pml_layer_thickness_px": pml_layer_thickness_px,
|
|
684
|
-
"n_transition_layer": n_transition_layer,
|
|
685
650
|
"use_gpu": use_gpu,
|
|
686
|
-
"use_gpu_pml": use_gpu_pml,
|
|
687
|
-
"pml_alpha_entrance": pml_alpha_entrance,
|
|
688
651
|
"source_type": source_type,
|
|
689
|
-
"
|
|
690
|
-
"
|
|
652
|
+
"use_exponential_attenuation": use_exponential_attenuation,
|
|
653
|
+
"native_only_options": {
|
|
654
|
+
"pml_layer_thickness_px": pml_layer_thickness_px,
|
|
655
|
+
"n_transition_layer": n_transition_layer,
|
|
656
|
+
"pml_alpha_entrance": pml_alpha_entrance,
|
|
657
|
+
"exponential_attenuation_pml_thickness_px": (
|
|
658
|
+
exponential_attenuation_pml_thickness_px
|
|
659
|
+
),
|
|
660
|
+
"use_gpu_pml": use_gpu_pml,
|
|
661
|
+
"save_gpu_memory": save_gpu_memory,
|
|
662
|
+
"write_maps_the_solver_does_not_read": (
|
|
663
|
+
write_maps_the_solver_does_not_read
|
|
664
|
+
),
|
|
665
|
+
"use_isotropic_relaxation": use_isotropic_relaxation,
|
|
666
|
+
},
|
|
691
667
|
},
|
|
692
668
|
)
|
|
693
669
|
return
|
|
670
|
+
if ultra_sponge_thickness_px is not None:
|
|
671
|
+
backend.warn_sponge_without_ultra()
|
|
694
672
|
if exponential_attenuation_pml_thickness_px is not None and not use_exponential_attenuation:
|
|
695
673
|
error_msg = (
|
|
696
674
|
"exponential_attenuation_pml_thickness_px belongs to the exponential "
|
|
@@ -968,31 +946,6 @@ class Solver:
|
|
|
968
946
|
)
|
|
969
947
|
logger.warning(message)
|
|
970
948
|
|
|
971
|
-
@staticmethod
|
|
972
|
-
def _release_gpu_memory_pools() -> None:
|
|
973
|
-
"""Release all CuPy GPU memory pool blocks back to CUDA.
|
|
974
|
-
|
|
975
|
-
Call ``gc.collect()`` first so that Python releases references to
|
|
976
|
-
CuPy arrays, then drain both the device and pinned memory pools.
|
|
977
|
-
This prevents stale allocations from causing memory pressure when
|
|
978
|
-
subsequent operations allocate large GPU arrays.
|
|
979
|
-
|
|
980
|
-
The release is best effort. It does nothing when CuPy is absent, and
|
|
981
|
-
it does nothing when CuPy is present without a usable CUDA driver,
|
|
982
|
-
which is the case on a machine with no GPU.
|
|
983
|
-
"""
|
|
984
|
-
gc.collect()
|
|
985
|
-
try:
|
|
986
|
-
import cupy as cp # noqa: PLC0415
|
|
987
|
-
except ImportError:
|
|
988
|
-
return
|
|
989
|
-
|
|
990
|
-
try:
|
|
991
|
-
cp.get_default_memory_pool().free_all_blocks()
|
|
992
|
-
cp.get_default_pinned_memory_pool().free_all_blocks()
|
|
993
|
-
except cp.cuda.runtime.CUDARuntimeError as error:
|
|
994
|
-
logger.debug("GPU memory pools were not released: %s", error)
|
|
995
|
-
|
|
996
949
|
@staticmethod
|
|
997
950
|
def _check_input(
|
|
998
951
|
grid: fullwave.Grid,
|
|
@@ -1420,10 +1373,10 @@ class Solver:
|
|
|
1420
1373
|
"Input data generation completed in %s. Skipping simulation execution.",
|
|
1421
1374
|
simulation_dir,
|
|
1422
1375
|
)
|
|
1423
|
-
|
|
1376
|
+
release_gpu_memory_pools()
|
|
1424
1377
|
return simulation_dir
|
|
1425
1378
|
|
|
1426
|
-
|
|
1379
|
+
release_gpu_memory_pools()
|
|
1427
1380
|
|
|
1428
1381
|
if members is not None:
|
|
1429
1382
|
return self._run_members(
|
|
@@ -1933,9 +1886,8 @@ class Solver:
|
|
|
1933
1886
|
def _members_that_fit(self, sensor: fullwave.Sensor) -> int | None:
|
|
1934
1887
|
"""Return how many transmits the GPU memory estimate fits into the free memory.
|
|
1935
1888
|
|
|
1936
|
-
The estimate of one GPU grows by the same amount for each transmit. The count
|
|
1937
|
-
|
|
1938
|
-
what the estimate does not count, and takes the smallest count over the GPUs.
|
|
1889
|
+
The estimate of one GPU grows by the same amount for each transmit. The count is the
|
|
1890
|
+
smallest over the GPUs.
|
|
1939
1891
|
|
|
1940
1892
|
Parameters
|
|
1941
1893
|
----------
|
|
@@ -1948,18 +1900,21 @@ class Solver:
|
|
|
1948
1900
|
The count, or None when the free memory cannot be read.
|
|
1949
1901
|
|
|
1950
1902
|
"""
|
|
1951
|
-
free =
|
|
1903
|
+
free = free_gpu_memory_bytes(self.fullwave_launcher.cuda_device_id.split(","))
|
|
1952
1904
|
if free is None:
|
|
1953
1905
|
return None
|
|
1954
1906
|
one = self._gpu_memory_bytes(sensor, n_members=1)
|
|
1955
1907
|
two = self._gpu_memory_bytes(sensor, n_members=2)
|
|
1956
|
-
reserve = 512 * 1024**2
|
|
1957
1908
|
counts = []
|
|
1958
1909
|
for (device, one_bytes, *_), (_, two_bytes, *_) in zip(one, two, strict=True):
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1910
|
+
per_member_bytes = max(two_bytes - one_bytes, 1)
|
|
1911
|
+
counts.append(
|
|
1912
|
+
members_that_fit(
|
|
1913
|
+
free_bytes=free.get(device, 0),
|
|
1914
|
+
shared_bytes=one_bytes - per_member_bytes,
|
|
1915
|
+
per_member_bytes=per_member_bytes,
|
|
1916
|
+
)
|
|
1917
|
+
)
|
|
1963
1918
|
return min(counts)
|
|
1964
1919
|
|
|
1965
1920
|
def _estimate_gpu_memory(
|
fullwave/transducer.py
CHANGED
|
@@ -352,6 +352,9 @@ class TransducerGeometry:
|
|
|
352
352
|
) -> tuple[NDArray[np.int64], NDArray[np.int64], NDArray[np.int64], NDArray[np.int64]]:
|
|
353
353
|
"""Build flat coordinate arrays for source and sensor pixels.
|
|
354
354
|
|
|
355
|
+
For a flat array the sensor row is the first row in front of the element
|
|
356
|
+
(``position_px[0] + element_layer_px``), outside the source cells.
|
|
357
|
+
|
|
355
358
|
Returns
|
|
356
359
|
-------
|
|
357
360
|
source_coords : NDArray[np.int64], shape [N_src, ndim]
|
|
@@ -404,7 +407,7 @@ class TransducerGeometry:
|
|
|
404
407
|
source_ids_parts.append(np.full(len(src), element_index + 1))
|
|
405
408
|
|
|
406
409
|
# Single center pixel for sensor output
|
|
407
|
-
sx =
|
|
410
|
+
sx = self._flat_sensor_row(element_pos_x=element_pos_x)
|
|
408
411
|
sy = element_pos_y + self.element_width_px - 1
|
|
409
412
|
sz = element_pos_z + self.element_height_px // 2 - 1
|
|
410
413
|
sensor_coords_parts.append(np.array([[sx, sy, sz]]))
|
|
@@ -432,7 +435,7 @@ class TransducerGeometry:
|
|
|
432
435
|
source_coords_parts.append(src)
|
|
433
436
|
source_ids_parts.append(np.full(len(src), element_index + 1))
|
|
434
437
|
|
|
435
|
-
sx =
|
|
438
|
+
sx = self._flat_sensor_row(element_pos_x=element_pos_x)
|
|
436
439
|
if self.average_surface_signals:
|
|
437
440
|
out_ys = np.arange(element_pos_y, element_pos_y + self.element_width_px)
|
|
438
441
|
out_xs = np.full(len(out_ys), sx)
|
|
@@ -452,6 +455,39 @@ class TransducerGeometry:
|
|
|
452
455
|
sensor_ids = np.concatenate(sensor_ids_parts, axis=0).astype(np.int64)
|
|
453
456
|
return source_coords, source_ids, sensor_coords, sensor_ids
|
|
454
457
|
|
|
458
|
+
def _flat_sensor_row(self, *, element_pos_x: int) -> int:
|
|
459
|
+
"""Return the sensor row of a flat-array element.
|
|
460
|
+
|
|
461
|
+
The row lies in front of the source cells because a hard source keeps a
|
|
462
|
+
static pressure after the drive, which a receiver inside it would record.
|
|
463
|
+
|
|
464
|
+
Parameters
|
|
465
|
+
----------
|
|
466
|
+
element_pos_x : int
|
|
467
|
+
First (back) source row of the element.
|
|
468
|
+
|
|
469
|
+
Returns
|
|
470
|
+
-------
|
|
471
|
+
int
|
|
472
|
+
The sensor row, ``element_pos_x + element_layer_px``.
|
|
473
|
+
|
|
474
|
+
Raises
|
|
475
|
+
------
|
|
476
|
+
ValueError
|
|
477
|
+
If the sensor row is outside the grid.
|
|
478
|
+
|
|
479
|
+
"""
|
|
480
|
+
sensor_row = int(element_pos_x + self.element_layer_px)
|
|
481
|
+
if sensor_row >= self.stored_grid_size[0]:
|
|
482
|
+
error_msg = (
|
|
483
|
+
f"The sensor row {sensor_row} (one row in front of the element) is "
|
|
484
|
+
f"outside the grid, which has {self.stored_grid_size[0]} rows in x. "
|
|
485
|
+
"Move the transducer back or enlarge the grid."
|
|
486
|
+
)
|
|
487
|
+
logger.error(error_msg)
|
|
488
|
+
raise ValueError(error_msg)
|
|
489
|
+
return sensor_row
|
|
490
|
+
|
|
455
491
|
def _arc_element_coords(
|
|
456
492
|
self,
|
|
457
493
|
) -> tuple[NDArray[np.int64], NDArray[np.int64], NDArray[np.int64], NDArray[np.int64]]:
|
|
@@ -1728,6 +1764,18 @@ class Transducer:
|
|
|
1728
1764
|
out[eid] = np.rint(coords[sel].mean(axis=0)).astype(np.int64)
|
|
1729
1765
|
return out
|
|
1730
1766
|
|
|
1767
|
+
@property
|
|
1768
|
+
def element_id_to_receiver_center(self) -> dict[int, NDArray[np.int64]]:
|
|
1769
|
+
"""Return the mapping from element IDs to the mean receiver coordinate in grid points."""
|
|
1770
|
+
ndim = 3 if self.is_3d else 2
|
|
1771
|
+
out: dict[int, NDArray[np.int64]] = {}
|
|
1772
|
+
for eid, coords in self.element_id_to_element_surface.items():
|
|
1773
|
+
if len(coords) == 0:
|
|
1774
|
+
out[eid] = np.full(ndim, -1, dtype=np.int64)
|
|
1775
|
+
else:
|
|
1776
|
+
out[eid] = np.rint(coords.mean(axis=0)).astype(np.int64)
|
|
1777
|
+
return out
|
|
1778
|
+
|
|
1731
1779
|
@property
|
|
1732
1780
|
def sensor(self) -> fullwave.sensor.Sensor:
|
|
1733
1781
|
"""Return the Sensor object with active sensor coordinates.
|
fullwave/ultra/backend.py
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import inspect
|
|
5
6
|
import time
|
|
6
7
|
import warnings
|
|
7
8
|
from pathlib import Path
|
|
8
|
-
from typing import TYPE_CHECKING
|
|
9
|
+
from typing import TYPE_CHECKING, NamedTuple
|
|
9
10
|
|
|
10
11
|
import numpy as np
|
|
11
12
|
|
|
@@ -36,15 +37,43 @@ if TYPE_CHECKING:
|
|
|
36
37
|
CFL_LIMIT_3D = 0.30
|
|
37
38
|
SPEED_OPTIMIZED_PPW = 6
|
|
38
39
|
|
|
40
|
+
PACKAGE_DIR = Path(__file__).parents[1]
|
|
41
|
+
|
|
39
42
|
_shown_warnings: set[str] = set()
|
|
40
43
|
|
|
41
44
|
|
|
45
|
+
def _stacklevel_of_first_caller_outside_package() -> int:
|
|
46
|
+
"""Return the ``warnings.warn`` stacklevel of the first frame outside the fullwave package."""
|
|
47
|
+
frame = inspect.currentframe()
|
|
48
|
+
stacklevel = 0
|
|
49
|
+
while frame is not None and Path(frame.f_code.co_filename).is_relative_to(PACKAGE_DIR):
|
|
50
|
+
frame = frame.f_back
|
|
51
|
+
stacklevel += 1
|
|
52
|
+
return stacklevel
|
|
53
|
+
|
|
54
|
+
|
|
42
55
|
def _warn_once(*, name: str, message: str, category: type[Warning]) -> None:
|
|
43
|
-
"""Warn with ``message`` the first time ``name`` is seen in this process.
|
|
56
|
+
"""Warn with ``message`` the first time ``name`` is seen in this process.
|
|
57
|
+
|
|
58
|
+
The warning points at the line of the user's code, however deep the call is.
|
|
59
|
+
"""
|
|
44
60
|
if name in _shown_warnings:
|
|
45
61
|
return
|
|
46
62
|
_shown_warnings.add(name)
|
|
47
|
-
warnings.warn(message, category, stacklevel=
|
|
63
|
+
warnings.warn(message, category, stacklevel=_stacklevel_of_first_caller_outside_package())
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def warn_sponge_without_ultra() -> None:
|
|
67
|
+
"""Warn once per process that ``ultra_sponge_thickness_px`` needs ``use_ultra=True``."""
|
|
68
|
+
_warn_once(
|
|
69
|
+
name="sponge_without_ultra",
|
|
70
|
+
message=(
|
|
71
|
+
"ultra_sponge_thickness_px is ignored without use_ultra=True. The native "
|
|
72
|
+
"absorber is sized by pml_layer_thickness_px (relaxation model) or "
|
|
73
|
+
"exponential_attenuation_pml_thickness_px (exponential model)."
|
|
74
|
+
),
|
|
75
|
+
category=UserWarning,
|
|
76
|
+
)
|
|
48
77
|
|
|
49
78
|
|
|
50
79
|
def warn_ppw_above_speed_optimum(*, ppw: int) -> None:
|
|
@@ -72,6 +101,25 @@ def warn_experimental() -> None:
|
|
|
72
101
|
)
|
|
73
102
|
|
|
74
103
|
|
|
104
|
+
def warn_ignored(*, option: str, detail: str = "") -> None:
|
|
105
|
+
"""Warn once per process that ``use_ultra=True`` ignores ``option``."""
|
|
106
|
+
suffix = f"; {detail}" if detail else ""
|
|
107
|
+
_warn_once(
|
|
108
|
+
name=f"ignored:{option}",
|
|
109
|
+
message=f"{option} is ignored with use_ultra=True{suffix}.",
|
|
110
|
+
category=UserWarning,
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def warn_one_device_only(*, device: int) -> None:
|
|
115
|
+
"""Warn once per process that ``use_ultra=True`` runs on one GPU, the first one listed."""
|
|
116
|
+
_warn_once(
|
|
117
|
+
name="one_device_only",
|
|
118
|
+
message=f"cuda_device_id lists several GPUs; use_ultra=True runs on GPU {device} only.",
|
|
119
|
+
category=UserWarning,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
|
|
75
123
|
def refuse(message: str) -> None:
|
|
76
124
|
"""Raise ``UltraUnsupportedError`` with ``message``."""
|
|
77
125
|
raise UltraUnsupportedError(message)
|
|
@@ -122,9 +170,12 @@ def single_device(cuda_device_id: str | int | list | None) -> int:
|
|
|
122
170
|
devices = list(cuda_device_id)
|
|
123
171
|
else:
|
|
124
172
|
devices = [cuda_device_id]
|
|
125
|
-
if
|
|
126
|
-
refuse("
|
|
127
|
-
|
|
173
|
+
if not devices:
|
|
174
|
+
refuse("cuda_device_id lists no device")
|
|
175
|
+
device = int(devices[0])
|
|
176
|
+
if len(devices) > 1:
|
|
177
|
+
warn_one_device_only(device=device)
|
|
178
|
+
return device
|
|
128
179
|
|
|
129
180
|
|
|
130
181
|
def work_directory(
|
|
@@ -377,39 +428,70 @@ class UltraBackend3D:
|
|
|
377
428
|
)
|
|
378
429
|
|
|
379
430
|
|
|
431
|
+
SPONGE_HINT = "set the sponge width with ultra_sponge_thickness_px"
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
class NativeOnlyOption(NamedTuple):
|
|
435
|
+
"""An option of the native solver that ``use_ultra=True`` ignores."""
|
|
436
|
+
|
|
437
|
+
solver_default: object
|
|
438
|
+
hint: str = ""
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
NATIVE_ONLY_OPTIONS: dict[str, NativeOnlyOption] = {
|
|
442
|
+
"pml_layer_thickness_px": NativeOnlyOption(solver_default=None, hint=SPONGE_HINT),
|
|
443
|
+
"n_transition_layer": NativeOnlyOption(solver_default=None, hint=SPONGE_HINT),
|
|
444
|
+
"pml_alpha_entrance": NativeOnlyOption(solver_default=None, hint=SPONGE_HINT),
|
|
445
|
+
"exponential_attenuation_pml_thickness_px": NativeOnlyOption(
|
|
446
|
+
solver_default=None, hint=SPONGE_HINT
|
|
447
|
+
),
|
|
448
|
+
"use_gpu_pml": NativeOnlyOption(solver_default=False),
|
|
449
|
+
"save_gpu_memory": NativeOnlyOption(solver_default=False),
|
|
450
|
+
"write_maps_the_solver_does_not_read": NativeOnlyOption(solver_default=False),
|
|
451
|
+
"use_isotropic_relaxation": NativeOnlyOption(solver_default=True),
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
def warn_native_only_options(*, options: dict[str, object]) -> None:
|
|
456
|
+
"""Warn once per process for each native-only option set to a non-default value."""
|
|
457
|
+
for name, option in NATIVE_ONLY_OPTIONS.items():
|
|
458
|
+
if options[name] != option.solver_default:
|
|
459
|
+
warn_ignored(option=name, detail=option.hint)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def warn_relaxation_model_requested(*, use_exponential_attenuation: bool) -> None:
|
|
463
|
+
"""Warn once per process when the relaxation model is requested, as the native default does."""
|
|
464
|
+
if use_exponential_attenuation:
|
|
465
|
+
return
|
|
466
|
+
_warn_once(
|
|
467
|
+
name="attenuation_model",
|
|
468
|
+
message=(
|
|
469
|
+
"use_ultra=True runs the exponential attenuation model, whose attenuation is "
|
|
470
|
+
"frequency-independent (the same at every frequency), not the relaxation model that "
|
|
471
|
+
"use_exponential_attenuation=False requests, whose attenuation is "
|
|
472
|
+
"frequency-dependent."
|
|
473
|
+
),
|
|
474
|
+
category=UserWarning,
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
|
|
380
478
|
def check_solver_options(
|
|
381
479
|
*,
|
|
382
480
|
use_pml: bool,
|
|
383
481
|
m_spatial_order: int,
|
|
384
|
-
pml_layer_thickness_px: int | None,
|
|
385
|
-
n_transition_layer: int | None,
|
|
386
482
|
use_gpu: bool,
|
|
387
|
-
use_gpu_pml: bool,
|
|
388
|
-
pml_alpha_entrance: float | None,
|
|
389
483
|
source_type: str,
|
|
390
|
-
|
|
391
|
-
|
|
484
|
+
native_only_options: dict[str, object],
|
|
485
|
+
use_exponential_attenuation: bool,
|
|
392
486
|
) -> None:
|
|
393
|
-
"""Refuse the ``Solver`` options that ``use_ultra=True`` cannot honor."""
|
|
487
|
+
"""Refuse the ``Solver`` options that ``use_ultra=True`` cannot honor; warn on the rest."""
|
|
394
488
|
if not use_pml:
|
|
395
489
|
refuse("use_pml=False is not supported by use_ultra=True")
|
|
396
|
-
if (
|
|
397
|
-
pml_layer_thickness_px is not None
|
|
398
|
-
or n_transition_layer is not None
|
|
399
|
-
or use_gpu_pml
|
|
400
|
-
or pml_alpha_entrance is not None
|
|
401
|
-
):
|
|
402
|
-
refuse(
|
|
403
|
-
"PML settings do not apply to use_ultra=True; "
|
|
404
|
-
"set the sponge with exponential_attenuation_pml_thickness_px"
|
|
405
|
-
)
|
|
406
490
|
if m_spatial_order != 8:
|
|
407
491
|
refuse("use_ultra=True supports m_spatial_order=8 only")
|
|
408
492
|
if not use_gpu:
|
|
409
493
|
refuse("use_ultra=True needs the GPU")
|
|
410
494
|
if source_type not in SOURCE_TYPES:
|
|
411
495
|
refuse(f"source_type must be one of {SOURCE_TYPES}, not {source_type!r}")
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
if write_maps_the_solver_does_not_read:
|
|
415
|
-
refuse("write_maps_the_solver_does_not_read does not apply to use_ultra=True")
|
|
496
|
+
warn_native_only_options(options=native_only_options)
|
|
497
|
+
warn_relaxation_model_requested(use_exponential_attenuation=use_exponential_attenuation)
|
fullwave/ultra/backend_2d.py
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import logging
|
|
5
6
|
import shutil
|
|
6
7
|
import time
|
|
7
8
|
from pathlib import Path
|
|
@@ -11,6 +12,11 @@ import numpy as np
|
|
|
11
12
|
|
|
12
13
|
from fullwave.solver.binary_markers import check_binary_holds_source
|
|
13
14
|
from fullwave.solver.source_type import HARD_RELEASED_AFTER, is_hard_held
|
|
15
|
+
from fullwave.utils.gpu_memory import (
|
|
16
|
+
free_gpu_memory_bytes,
|
|
17
|
+
members_that_fit,
|
|
18
|
+
release_gpu_memory_pools,
|
|
19
|
+
)
|
|
14
20
|
|
|
15
21
|
from . import binary as binary_module
|
|
16
22
|
from . import dat_io
|
|
@@ -23,9 +29,10 @@ from .backend import (
|
|
|
23
29
|
to_host,
|
|
24
30
|
work_directory,
|
|
25
31
|
)
|
|
26
|
-
from .
|
|
32
|
+
from .binary import OUTPUT_TAIL_CHARS
|
|
33
|
+
from .errors import UltraBinaryError, UltraUnsupportedError
|
|
27
34
|
from .time_axis import check_time_axis, delay_additive_signal
|
|
28
|
-
from .writer_2d import write_run_directory_2d, write_sources_2d
|
|
35
|
+
from .writer_2d import extension_cells, write_run_directory_2d, write_sources_2d
|
|
29
36
|
|
|
30
37
|
if TYPE_CHECKING:
|
|
31
38
|
from collections.abc import Sequence
|
|
@@ -34,20 +41,125 @@ if TYPE_CHECKING:
|
|
|
34
41
|
|
|
35
42
|
import fullwave
|
|
36
43
|
|
|
44
|
+
logger = logging.getLogger("__main__." + __name__)
|
|
45
|
+
|
|
46
|
+
FLOAT_BYTES = 4
|
|
47
|
+
BYTES_PER_CELL_PER_MEMBER = 6 * FLOAT_BYTES # pressure and two velocities at two time levels
|
|
48
|
+
BYTES_PER_CELL_SHARED = 172 # material maps, sponge coefficients and sponge memory variables
|
|
49
|
+
BYTES_PER_AXIS_CELL = 2 * FLOAT_BYTES
|
|
50
|
+
BYTES_PER_COORDINATE = 2 * np.dtype(np.int32).itemsize
|
|
51
|
+
STENCIL_TABLE_BYTES = 9 * 2 * FLOAT_BYTES
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def device_bytes_2d(
|
|
55
|
+
*,
|
|
56
|
+
n_x: int,
|
|
57
|
+
n_y: int,
|
|
58
|
+
n_t: int,
|
|
59
|
+
mod_t: int,
|
|
60
|
+
ncoords: int,
|
|
61
|
+
n_tic: int,
|
|
62
|
+
ncoordsout: int,
|
|
63
|
+
ncoords_add: int = 0,
|
|
64
|
+
n_tic_add: int = 0,
|
|
65
|
+
ncoordszero: int = 0,
|
|
66
|
+
) -> tuple[int, int]:
|
|
67
|
+
"""Return the GPU memory of a 2D batch, in bytes, without the CUDA context.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
n_x, n_y : int
|
|
72
|
+
Cells of the extended grid, which includes the sponge and the stencil margin.
|
|
73
|
+
n_t, mod_t : int
|
|
74
|
+
Number of time steps and sensor decimation in time.
|
|
75
|
+
ncoords, n_tic : int
|
|
76
|
+
Hard source cells and source length in steps.
|
|
77
|
+
ncoordsout : int
|
|
78
|
+
Sensor points.
|
|
79
|
+
ncoords_add, n_tic_add : int
|
|
80
|
+
Additive source cells and source length in steps.
|
|
81
|
+
ncoordszero : int
|
|
82
|
+
Cells held at zero pressure.
|
|
83
|
+
|
|
84
|
+
Returns
|
|
85
|
+
-------
|
|
86
|
+
tuple[int, int]
|
|
87
|
+
The bytes shared by all members and the bytes added by each member.
|
|
88
|
+
"""
|
|
89
|
+
cells = n_x * n_y
|
|
90
|
+
frames = (n_t - 1) // mod_t
|
|
91
|
+
n_coordinates = ncoords + ncoordszero + ncoordsout + ncoords_add
|
|
92
|
+
per_member = (
|
|
93
|
+
BYTES_PER_CELL_PER_MEMBER * cells
|
|
94
|
+
+ FLOAT_BYTES * ncoords * n_tic
|
|
95
|
+
+ FLOAT_BYTES * ncoords_add * n_tic_add
|
|
96
|
+
+ FLOAT_BYTES * frames * ncoordsout
|
|
97
|
+
)
|
|
98
|
+
shared = (
|
|
99
|
+
BYTES_PER_CELL_SHARED * cells
|
|
100
|
+
+ BYTES_PER_AXIS_CELL * (n_x + n_y)
|
|
101
|
+
+ BYTES_PER_COORDINATE * n_coordinates
|
|
102
|
+
+ FLOAT_BYTES * ncoordsout
|
|
103
|
+
+ STENCIL_TABLE_BYTES
|
|
104
|
+
)
|
|
105
|
+
return shared, per_member
|
|
106
|
+
|
|
37
107
|
|
|
38
108
|
def _members_per_run(n_members: int, max_members_per_run: int | None) -> list[range]:
|
|
39
109
|
"""Split the members into the fewest runs of at most ``max_members_per_run``."""
|
|
40
110
|
if max_members_per_run is None:
|
|
41
111
|
return [range(n_members)]
|
|
42
|
-
if max_members_per_run < 1:
|
|
43
|
-
msg = f"max_members_per_run must be 1 or more, not {max_members_per_run}"
|
|
44
|
-
raise ValueError(msg)
|
|
45
112
|
n_runs = -(-n_members // max_members_per_run)
|
|
46
113
|
return [
|
|
47
114
|
range(int(part[0]), int(part[-1]) + 1) for part in np.array_split(range(n_members), n_runs)
|
|
48
115
|
]
|
|
49
116
|
|
|
50
117
|
|
|
118
|
+
def check_genout_files(
|
|
119
|
+
genout_paths: Sequence[Path], *, ncoordsout: int, rundir: Path, stdout: str
|
|
120
|
+
) -> None:
|
|
121
|
+
"""Check that every output file of a run is complete.
|
|
122
|
+
|
|
123
|
+
Parameters
|
|
124
|
+
----------
|
|
125
|
+
genout_paths : sequence of Path
|
|
126
|
+
The output file of each batch member.
|
|
127
|
+
ncoordsout : int
|
|
128
|
+
Number of sensor points per frame.
|
|
129
|
+
rundir : Path
|
|
130
|
+
The run directory, named in the error message.
|
|
131
|
+
stdout : str
|
|
132
|
+
Standard output of the binary, of which the end goes into the error message.
|
|
133
|
+
|
|
134
|
+
Raises
|
|
135
|
+
------
|
|
136
|
+
UltraBinaryError
|
|
137
|
+
If a file is missing or empty, does not hold whole ``float32`` frames, or differs in
|
|
138
|
+
size from the other files of the run.
|
|
139
|
+
"""
|
|
140
|
+
frame_bytes = FLOAT_BYTES * ncoordsout
|
|
141
|
+
sizes = {path.name: path.stat().st_size if path.is_file() else None for path in genout_paths}
|
|
142
|
+
common_size = max((size for size in sizes.values() if size), default=0)
|
|
143
|
+
incomplete = [
|
|
144
|
+
name
|
|
145
|
+
for name, size in sizes.items()
|
|
146
|
+
if not size or size % frame_bytes != 0 or size != common_size
|
|
147
|
+
]
|
|
148
|
+
if not incomplete:
|
|
149
|
+
return
|
|
150
|
+
descriptions = [
|
|
151
|
+
f"{name} (missing)" if sizes[name] is None else f"{name} ({sizes[name]} bytes)"
|
|
152
|
+
for name in incomplete
|
|
153
|
+
]
|
|
154
|
+
msg = (
|
|
155
|
+
"The fullwave2-ultra binary exited with code 0 but its output is incomplete: "
|
|
156
|
+
f"{', '.join(descriptions)}. "
|
|
157
|
+
f"Each output file must be non-empty, a multiple of {frame_bytes} bytes and of equal size. "
|
|
158
|
+
f"Run directory: {rundir}. Last output of the binary:\n{stdout[-OUTPUT_TAIL_CHARS:]}"
|
|
159
|
+
)
|
|
160
|
+
raise UltraBinaryError(msg)
|
|
161
|
+
|
|
162
|
+
|
|
51
163
|
class UltraBackend2D:
|
|
52
164
|
"""Write, run and read 2D fullwave2-ultra simulations of one medium and several transmits.
|
|
53
165
|
|
|
@@ -66,7 +178,7 @@ class UltraBackend2D:
|
|
|
66
178
|
cuda_device_id : str | int | list | None
|
|
67
179
|
One CUDA device.
|
|
68
180
|
verify_gpu : bool
|
|
69
|
-
Check that the device is visible before the
|
|
181
|
+
Check that the device is visible before the run.
|
|
70
182
|
source_type : str
|
|
71
183
|
``"hard"`` sets the pressure at the source cells for the whole run.
|
|
72
184
|
``"hard_released_after"`` sets it while the signal plays, then lets the cells evolve
|
|
@@ -228,7 +340,10 @@ class UltraBackend2D:
|
|
|
228
340
|
highpass_cutoff_mhz: float | None = None,
|
|
229
341
|
bandpass_cutoff_mhz: tuple[float, float] | None = None,
|
|
230
342
|
) -> NDArray | list[Path]:
|
|
231
|
-
"""Run the transmits
|
|
343
|
+
"""Run the transmits in runs of at most ``max_members_per_run`` (None: one run).
|
|
344
|
+
|
|
345
|
+
The free GPU memory can lower the limit, with a warning. The traces do not depend on
|
|
346
|
+
the split.
|
|
232
347
|
|
|
233
348
|
Returns
|
|
234
349
|
-------
|
|
@@ -272,6 +387,9 @@ class UltraBackend2D:
|
|
|
272
387
|
if len(sources) == 0:
|
|
273
388
|
msg = "run_batch needs at least one source."
|
|
274
389
|
raise ValueError(msg)
|
|
390
|
+
if max_members_per_run is not None and max_members_per_run < 1:
|
|
391
|
+
msg = f"max_members_per_run must be 1 or more, not {max_members_per_run}"
|
|
392
|
+
raise ValueError(msg)
|
|
275
393
|
n_t = check_time_axis(self.grid)
|
|
276
394
|
transmits = [self._check_member(source, index=i) for i, source in enumerate(sources)]
|
|
277
395
|
signals = [hard for hard, _ in transmits]
|
|
@@ -284,7 +402,19 @@ class UltraBackend2D:
|
|
|
284
402
|
msg = f"the source has {n_steps} steps, more than the {n_t} of the simulation"
|
|
285
403
|
raise ValueError(msg)
|
|
286
404
|
|
|
287
|
-
|
|
405
|
+
has_additive = self.source.n_sources_add > 0
|
|
406
|
+
if generate_input_only:
|
|
407
|
+
members_per_run_limit = max_members_per_run
|
|
408
|
+
else:
|
|
409
|
+
release_gpu_memory_pools()
|
|
410
|
+
members_per_run_limit = self._members_per_run_limit(
|
|
411
|
+
n_members=len(signals),
|
|
412
|
+
max_members_per_run=max_members_per_run,
|
|
413
|
+
n_t=n_t,
|
|
414
|
+
n_tic=signals[0].shape[1],
|
|
415
|
+
n_tic_add=additive_signals[0].shape[1] if has_additive else 0,
|
|
416
|
+
)
|
|
417
|
+
runs = _members_per_run(len(signals), members_per_run_limit)
|
|
288
418
|
outputs = []
|
|
289
419
|
for index, members in enumerate(runs):
|
|
290
420
|
name = simulation_dir_name if len(runs) == 1 else f"{simulation_dir_name}_{index}"
|
|
@@ -292,7 +422,7 @@ class UltraBackend2D:
|
|
|
292
422
|
self._run_one(
|
|
293
423
|
name,
|
|
294
424
|
[signals[m] for m in members],
|
|
295
|
-
[additive_signals[m] for m in members] if
|
|
425
|
+
[additive_signals[m] for m in members] if has_additive else None,
|
|
296
426
|
n_t=n_t,
|
|
297
427
|
load_results=load_results,
|
|
298
428
|
generate_input_only=generate_input_only,
|
|
@@ -304,6 +434,67 @@ class UltraBackend2D:
|
|
|
304
434
|
return np.concatenate(outputs, axis=0)
|
|
305
435
|
return [path for run_paths in outputs for path in run_paths]
|
|
306
436
|
|
|
437
|
+
def _members_per_run_limit(
|
|
438
|
+
self,
|
|
439
|
+
*,
|
|
440
|
+
n_members: int,
|
|
441
|
+
max_members_per_run: int | None,
|
|
442
|
+
n_t: int,
|
|
443
|
+
n_tic: int,
|
|
444
|
+
n_tic_add: int,
|
|
445
|
+
) -> int | None:
|
|
446
|
+
"""Return the most members one run may hold, or None when nothing limits it.
|
|
447
|
+
|
|
448
|
+
A run that does not fit into the free GPU memory ends with empty output files.
|
|
449
|
+
"""
|
|
450
|
+
wanted = n_members if max_members_per_run is None else min(max_members_per_run, n_members)
|
|
451
|
+
free = free_gpu_memory_bytes([str(self.device)])
|
|
452
|
+
if free is None:
|
|
453
|
+
logger.warning(
|
|
454
|
+
"The free GPU memory is unknown, so the %d transmits per run are not checked "
|
|
455
|
+
"against it.",
|
|
456
|
+
wanted,
|
|
457
|
+
)
|
|
458
|
+
return max_members_per_run
|
|
459
|
+
shared_bytes, per_member_bytes = self._member_bytes(
|
|
460
|
+
n_t=n_t, n_tic=n_tic, n_tic_add=n_tic_add
|
|
461
|
+
)
|
|
462
|
+
fit = max(
|
|
463
|
+
members_that_fit(
|
|
464
|
+
free_bytes=free[str(self.device)],
|
|
465
|
+
shared_bytes=shared_bytes,
|
|
466
|
+
per_member_bytes=per_member_bytes,
|
|
467
|
+
),
|
|
468
|
+
1,
|
|
469
|
+
)
|
|
470
|
+
if fit < wanted:
|
|
471
|
+
logger.warning(
|
|
472
|
+
"The free GPU memory fits %d transmits per run, not %d. "
|
|
473
|
+
"The %d transmits run in runs of at most %d.",
|
|
474
|
+
fit,
|
|
475
|
+
wanted,
|
|
476
|
+
n_members,
|
|
477
|
+
fit,
|
|
478
|
+
)
|
|
479
|
+
return min(wanted, fit)
|
|
480
|
+
|
|
481
|
+
def _member_bytes(self, *, n_t: int, n_tic: int, n_tic_add: int) -> tuple[int, int]:
|
|
482
|
+
"""Return the GPU bytes shared by all members and the bytes added by each member."""
|
|
483
|
+
_, margin = extension_cells(ppw=self.grid.ppw, nbdy=self.nbdy)
|
|
484
|
+
n_x, n_y = (cells + 2 * margin for cells in self.medium.sound_speed.shape)
|
|
485
|
+
return device_bytes_2d(
|
|
486
|
+
n_x=n_x,
|
|
487
|
+
n_y=n_y,
|
|
488
|
+
n_t=n_t,
|
|
489
|
+
mod_t=self.sensor.sampling_modulus_time,
|
|
490
|
+
ncoords=len(self.source.incoords),
|
|
491
|
+
n_tic=n_tic,
|
|
492
|
+
ncoordsout=len(self.sensor.outcoords),
|
|
493
|
+
ncoords_add=len(self.source.incoords_add) if n_tic_add else 0,
|
|
494
|
+
n_tic_add=n_tic_add,
|
|
495
|
+
ncoordszero=len(self.medium.air_coords),
|
|
496
|
+
)
|
|
497
|
+
|
|
307
498
|
def _run_one(
|
|
308
499
|
self,
|
|
309
500
|
simulation_dir_name: str | Path,
|
|
@@ -357,6 +548,9 @@ class UltraBackend2D:
|
|
|
357
548
|
)
|
|
358
549
|
self.last_run.update(run_seconds=time.perf_counter() - started, stdout=stdout)
|
|
359
550
|
genout_paths = [rundir / dat_io.genout_name(m) for m in range(len(signals))]
|
|
551
|
+
check_genout_files(
|
|
552
|
+
genout_paths, ncoordsout=scalars["ncoordsout"], rundir=rundir, stdout=stdout
|
|
553
|
+
)
|
|
360
554
|
if not load_results:
|
|
361
555
|
return genout_paths
|
|
362
556
|
traces = np.stack(
|
fullwave/ultra/binary.py
CHANGED
|
@@ -28,6 +28,8 @@ MARKER_AIR_WINDOW_2D = b"nzero0.dat"
|
|
|
28
28
|
MARKER_SOURCE_RELEASE_2D = b"nszero0.dat"
|
|
29
29
|
MARKER_HOLD_SOURCE_3D = b"nszero0.dat"
|
|
30
30
|
|
|
31
|
+
OUTPUT_TAIL_CHARS = 2000
|
|
32
|
+
|
|
31
33
|
|
|
32
34
|
class PreflightError(RuntimeError):
|
|
33
35
|
"""Raised when no usable CUDA device is visible."""
|
|
@@ -146,7 +148,11 @@ def run_binary(
|
|
|
146
148
|
check=False,
|
|
147
149
|
)
|
|
148
150
|
if result.returncode != 0:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
+
stderr_tail = (result.stderr or "")[-OUTPUT_TAIL_CHARS:]
|
|
152
|
+
stdout_tail = (result.stdout or "")[-OUTPUT_TAIL_CHARS:]
|
|
153
|
+
msg = (
|
|
154
|
+
f"{Path(binary).name} failed (returncode={result.returncode}):\n"
|
|
155
|
+
f"stderr:\n{stderr_tail}\nstdout:\n{stdout_tail}"
|
|
156
|
+
)
|
|
151
157
|
raise RuntimeError(msg)
|
|
152
158
|
return result.stdout or ""
|
fullwave/ultra/writer_2d.py
CHANGED
|
@@ -19,6 +19,27 @@ if TYPE_CHECKING:
|
|
|
19
19
|
SPONGE_WAVELENGTHS = 6
|
|
20
20
|
|
|
21
21
|
|
|
22
|
+
def extension_cells(*, ppw: float, nbdy: int | None, m_stencil: int = 8) -> tuple[int, int]:
|
|
23
|
+
"""Return the sponge width and the cells added on each side of the domain.
|
|
24
|
+
|
|
25
|
+
Parameters
|
|
26
|
+
----------
|
|
27
|
+
ppw : float
|
|
28
|
+
Points per wavelength.
|
|
29
|
+
nbdy : int | None
|
|
30
|
+
Sponge width in cells; None gives ``ppw * SPONGE_WAVELENGTHS``.
|
|
31
|
+
m_stencil : int
|
|
32
|
+
Stencil half-width.
|
|
33
|
+
|
|
34
|
+
Returns
|
|
35
|
+
-------
|
|
36
|
+
tuple[int, int]
|
|
37
|
+
The sponge width ``nbdy`` and the extension ``mext = nbdy + m_stencil``.
|
|
38
|
+
"""
|
|
39
|
+
nbdy = int(ppw * SPONGE_WAVELENGTHS) if nbdy is None else nbdy
|
|
40
|
+
return nbdy, nbdy + m_stencil
|
|
41
|
+
|
|
42
|
+
|
|
22
43
|
def write_run_directory_2d(
|
|
23
44
|
outdir: str | Path,
|
|
24
45
|
*,
|
|
@@ -65,9 +86,8 @@ def write_run_directory_2d(
|
|
|
65
86
|
"""
|
|
66
87
|
outdir = Path(outdir)
|
|
67
88
|
outdir.mkdir(parents=True, exist_ok=True)
|
|
68
|
-
nbdy =
|
|
89
|
+
nbdy, mext = extension_cells(ppw=ppw, nbdy=nbdy, m_stencil=m_stencil)
|
|
69
90
|
dx, dt, n_t = time_axis(c0=c0, omega0=omega0, dur=dur, ppw=ppw, cfl=cfl)
|
|
70
|
-
mext = nbdy + m_stencil
|
|
71
91
|
|
|
72
92
|
c = extend_map(maps["cmap"], mext)
|
|
73
93
|
rho = extend_map(maps["rmap"], mext)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Read the free GPU memory and release the CuPy memory pools."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import gc
|
|
6
|
+
import logging
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
from typing import TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger("__main__." + __name__)
|
|
15
|
+
|
|
16
|
+
GPU_MEMORY_FRACTION = 0.9
|
|
17
|
+
GPU_MEMORY_RESERVE_BYTES = 512 * 1024**2
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def members_that_fit(*, free_bytes: int, shared_bytes: int, per_member_bytes: int) -> int:
|
|
21
|
+
"""Return how many batch members fit into the free memory of a GPU.
|
|
22
|
+
|
|
23
|
+
Only ``GPU_MEMORY_FRACTION`` of the free memory is used, less ``GPU_MEMORY_RESERVE_BYTES``
|
|
24
|
+
for the CUDA context and for what the estimate does not count.
|
|
25
|
+
|
|
26
|
+
Parameters
|
|
27
|
+
----------
|
|
28
|
+
free_bytes : int
|
|
29
|
+
The free memory of the GPU.
|
|
30
|
+
shared_bytes : int
|
|
31
|
+
The memory used once, whatever the number of members.
|
|
32
|
+
per_member_bytes : int
|
|
33
|
+
The memory each member adds.
|
|
34
|
+
|
|
35
|
+
Returns
|
|
36
|
+
-------
|
|
37
|
+
int
|
|
38
|
+
The count, which is zero or negative when not even one member fits.
|
|
39
|
+
|
|
40
|
+
"""
|
|
41
|
+
usable_bytes = GPU_MEMORY_FRACTION * free_bytes - GPU_MEMORY_RESERVE_BYTES - shared_bytes
|
|
42
|
+
return int(usable_bytes // per_member_bytes)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def free_gpu_memory_bytes(device_ids: Sequence[str]) -> dict[str, int] | None:
|
|
46
|
+
"""Return the free memory of each GPU in bytes, as nvidia-smi reports it.
|
|
47
|
+
|
|
48
|
+
Parameters
|
|
49
|
+
----------
|
|
50
|
+
device_ids : Sequence[str]
|
|
51
|
+
The GPU indices, as nvidia-smi counts them.
|
|
52
|
+
|
|
53
|
+
Returns
|
|
54
|
+
-------
|
|
55
|
+
dict[str, int] | None
|
|
56
|
+
The free bytes of each GPU, or None when nvidia-smi cannot be run or does not
|
|
57
|
+
report every GPU.
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
wanted = [device.strip() for device in device_ids]
|
|
61
|
+
nvidia_smi_path = shutil.which("nvidia-smi")
|
|
62
|
+
if nvidia_smi_path is None:
|
|
63
|
+
return None
|
|
64
|
+
try:
|
|
65
|
+
found = subprocess.run( # noqa: S603
|
|
66
|
+
[nvidia_smi_path, "--query-gpu=index,memory.free", "--format=csv,noheader,nounits"],
|
|
67
|
+
capture_output=True,
|
|
68
|
+
text=True,
|
|
69
|
+
check=True,
|
|
70
|
+
timeout=30,
|
|
71
|
+
).stdout
|
|
72
|
+
except (OSError, subprocess.SubprocessError):
|
|
73
|
+
return None
|
|
74
|
+
free = {}
|
|
75
|
+
for line in found.splitlines():
|
|
76
|
+
index, _, mebibytes = line.partition(",")
|
|
77
|
+
if index.strip() in wanted:
|
|
78
|
+
try:
|
|
79
|
+
free[index.strip()] = int(float(mebibytes)) * 1024**2
|
|
80
|
+
except ValueError:
|
|
81
|
+
return None
|
|
82
|
+
if set(free) != set(wanted):
|
|
83
|
+
return None
|
|
84
|
+
return free
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def release_gpu_memory_pools() -> None:
|
|
88
|
+
"""Release all CuPy GPU memory pool blocks back to CUDA.
|
|
89
|
+
|
|
90
|
+
Call ``gc.collect()`` first so that Python releases references to
|
|
91
|
+
CuPy arrays, then drain both the device and pinned memory pools.
|
|
92
|
+
This prevents stale allocations from causing memory pressure when
|
|
93
|
+
subsequent operations allocate large GPU arrays.
|
|
94
|
+
|
|
95
|
+
The release is best effort. It does nothing when CuPy is absent, and
|
|
96
|
+
it does nothing when CuPy is present without a usable CUDA driver,
|
|
97
|
+
which is the case on a machine with no GPU.
|
|
98
|
+
"""
|
|
99
|
+
gc.collect()
|
|
100
|
+
try:
|
|
101
|
+
import cupy as cp # noqa: PLC0415
|
|
102
|
+
except ImportError:
|
|
103
|
+
return
|
|
104
|
+
|
|
105
|
+
try:
|
|
106
|
+
cp.get_default_memory_pool().free_all_blocks()
|
|
107
|
+
cp.get_default_pinned_memory_pool().free_all_blocks()
|
|
108
|
+
except cp.cuda.runtime.CUDARuntimeError as error:
|
|
109
|
+
logger.debug("GPU memory pools were not released: %s", error)
|
|
@@ -35,6 +35,44 @@ def _on_the_host(array: NDArray) -> NDArray:
|
|
|
35
35
|
return _array_module(array).asnumpy(array)
|
|
36
36
|
|
|
37
37
|
|
|
38
|
+
# float32 stores 1.2 as 1.2000000477 (about 6e-8 relative); table nodes are 1e-2 or more apart.
|
|
39
|
+
_FLOAT32_NOISE_RTOL = 1e-6
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _snap_to_nearby_node(
|
|
43
|
+
*,
|
|
44
|
+
values: NDArray[np.float64],
|
|
45
|
+
nodes: NDArray[np.float64],
|
|
46
|
+
xp: object,
|
|
47
|
+
) -> NDArray[np.float64]:
|
|
48
|
+
"""Replace each value that is float32 noise away from a table node by that node.
|
|
49
|
+
|
|
50
|
+
Parameters
|
|
51
|
+
----------
|
|
52
|
+
values : NDArray[np.float64]
|
|
53
|
+
Requested values.
|
|
54
|
+
nodes : NDArray[np.float64]
|
|
55
|
+
Sorted table nodes.
|
|
56
|
+
xp : object
|
|
57
|
+
Array module of ``values``, numpy or cupy.
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
-------
|
|
61
|
+
NDArray[np.float64]
|
|
62
|
+
The values, with near-node values replaced by the node.
|
|
63
|
+
|
|
64
|
+
"""
|
|
65
|
+
if nodes.size < 2:
|
|
66
|
+
return values
|
|
67
|
+
values = xp.asarray(values)
|
|
68
|
+
upper_index = xp.clip(xp.searchsorted(nodes, values, side="left"), 1, nodes.size - 1)
|
|
69
|
+
lower_node = nodes[upper_index - 1]
|
|
70
|
+
upper_node = nodes[upper_index]
|
|
71
|
+
is_near_lower = xp.abs(values - lower_node) <= _FLOAT32_NOISE_RTOL * xp.abs(lower_node)
|
|
72
|
+
is_near_upper = xp.abs(values - upper_node) <= _FLOAT32_NOISE_RTOL * xp.abs(upper_node)
|
|
73
|
+
return xp.where(is_near_lower, lower_node, xp.where(is_near_upper, upper_node, values))
|
|
74
|
+
|
|
75
|
+
|
|
38
76
|
def scale_relaxation_attenuation(
|
|
39
77
|
relaxation_param_dict: dict[str, NDArray[np.float64]],
|
|
40
78
|
factor: NDArray[np.float64] | float,
|
|
@@ -938,6 +976,8 @@ class RelaxationParametersGenerator:
|
|
|
938
976
|
|
|
939
977
|
"""
|
|
940
978
|
alpha_coeff = band_scaled_alpha_coeff(alpha_coeff, alpha_power, band_scale)
|
|
979
|
+
alpha_coeff = self._snap_to_table(alpha_coeff, table_axis=self.alpha_list)
|
|
980
|
+
alpha_power = self._snap_to_table(alpha_power, table_axis=self.power_list)
|
|
941
981
|
self._warn_invalid(alpha_coeff, alpha_power)
|
|
942
982
|
|
|
943
983
|
use_gpu = not isinstance(alpha_coeff, np.ndarray)
|
|
@@ -991,6 +1031,7 @@ class RelaxationParametersGenerator:
|
|
|
991
1031
|
|
|
992
1032
|
"""
|
|
993
1033
|
xp = np if isinstance(alpha_coeff, np.ndarray) else _array_module(alpha_coeff)
|
|
1034
|
+
alpha_coeff = self._snap_to_table(alpha_coeff, table_axis=self.alpha_list)
|
|
994
1035
|
axis = xp.asarray(np.asarray(self.alpha_list).ravel().round(10))
|
|
995
1036
|
return (
|
|
996
1037
|
alpha_coeff / axis[self._axis_index(alpha_coeff, axis, self.alpha_min, self.alpha_max)]
|
|
@@ -1113,6 +1154,15 @@ class RelaxationParametersGenerator:
|
|
|
1113
1154
|
],
|
|
1114
1155
|
)
|
|
1115
1156
|
|
|
1157
|
+
@staticmethod
|
|
1158
|
+
def _snap_to_table(
|
|
1159
|
+
values: NDArray[np.float64], *, table_axis: NDArray[np.float64]
|
|
1160
|
+
) -> NDArray[np.float64]:
|
|
1161
|
+
"""Replace each value that is float32 noise away from a node of one table axis."""
|
|
1162
|
+
xp = np if isinstance(values, np.ndarray) else _array_module(values)
|
|
1163
|
+
nodes = xp.asarray(np.asarray(table_axis).ravel().round(10))
|
|
1164
|
+
return _snap_to_nearby_node(values=values, nodes=nodes, xp=xp)
|
|
1165
|
+
|
|
1116
1166
|
@staticmethod
|
|
1117
1167
|
def _axis_index(
|
|
1118
1168
|
value: NDArray[np.float64],
|
|
@@ -1122,6 +1172,7 @@ class RelaxationParametersGenerator:
|
|
|
1122
1172
|
) -> NDArray[np.int64]:
|
|
1123
1173
|
"""Return the axis index the lookup kernel selects, which clips at both ends."""
|
|
1124
1174
|
xp = np if isinstance(value, np.ndarray) else _array_module(value)
|
|
1175
|
+
value = _snap_to_nearby_node(values=value, nodes=axis, xp=xp)
|
|
1125
1176
|
clipped = xp.clip(value, lowest, highest)
|
|
1126
1177
|
return xp.minimum(xp.searchsorted(axis, clipped, side="left"), axis.size - 1)
|
|
1127
1178
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fullwave25
|
|
3
|
-
Version: 1.4.2-
|
|
3
|
+
Version: 1.4.2-dev3
|
|
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
|
|
@@ -417,6 +417,8 @@ traces = solver.run() # (n_sensors, n_frames)
|
|
|
417
417
|
# 2D only: traces = solver.run_batch(sources) # (n_transmits, n_sensors, n_frames)
|
|
418
418
|
```
|
|
419
419
|
|
|
420
|
+
In 2D, `run_batch` treats `max_members_per_run` as an upper bound that the free GPU memory can lower, and it keeps the requested value, with a warning, when the free memory cannot be read. The native path uses an explicit `max_members_per_run` as given.
|
|
421
|
+
|
|
420
422
|
Supported now:
|
|
421
423
|
|
|
422
424
|
- 2D grids with `run` and `run_batch`, including air cells.
|
|
@@ -430,12 +432,16 @@ Supported now:
|
|
|
430
432
|
- Velocity sources, and sources that are not pressure sources.
|
|
431
433
|
- Sparse-grid sensors, `is_static_map`, and `record_whole_domain`.
|
|
432
434
|
- Media other than `fullwave.Medium` (exponential attenuation), and `alpha_power` other than 1.
|
|
433
|
-
- `use_pml=False`,
|
|
435
|
+
- `use_pml=False`, `m_spatial_order` other than 8, `use_gpu=False`, and an invalid `source_type`.
|
|
434
436
|
- In 2D, a binary that does not read the files for air cells, for the additive channel, or for releasing the source (with such a binary, pad `p0` to `grid.nt`).
|
|
435
437
|
- In 3D, air cells with a binary that does not read `icczero.dat` and `nzero0.dat`, and additive sources with a binary that does not read `ncoords_add.dat` and `icmat_add.dat`.
|
|
436
438
|
|
|
437
439
|
This list will grow as the backend does.
|
|
438
440
|
|
|
441
|
+
The backend runs the exponential attenuation model (frequency-independent: the same attenuation at every frequency). Set the width of its absorbing sponge, in cells, with `ultra_sponge_thickness_px`; the default is 6 wavelengths in 2D and 4 wavelengths in 3D. `exponential_attenuation_pml_thickness_px` no longer sets this width with `use_ultra=True`; a script that relied on it now gets the default width until it passes `ultra_sponge_thickness_px`.
|
|
442
|
+
|
|
443
|
+
Options that only tune the native solver are ignored with a warning, once per process: `pml_layer_thickness_px`, `n_transition_layer`, `pml_alpha_entrance`, `exponential_attenuation_pml_thickness_px` (use `ultra_sponge_thickness_px` instead), `use_gpu_pml`, `save_gpu_memory`, `write_maps_the_solver_does_not_read`, `use_isotropic_relaxation=False`, and a `cuda_device_id` that lists more than one device (the run uses the first one listed). `use_exponential_attenuation=False`, the native default, also warns that the backend runs the exponential attenuation model, whose attenuation is frequency-independent, not the relaxation model, whose attenuation is frequency-dependent. Pass `use_exponential_attenuation=True` to silence it.
|
|
444
|
+
|
|
439
445
|
---
|
|
440
446
|
|
|
441
447
|
## Attention
|
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
fullwave/__init__.py,sha256=
|
|
1
|
+
fullwave/__init__.py,sha256=Lu4dG1mNq4CKaKD34LmhtJNlYn8m2gRXrw8qYqnAZKg,2382
|
|
2
2
|
fullwave/grid.py,sha256=YbxAAgdYKR8AXwobtTZzHm9C4KR4D5--51zgbs0aQGY,7435
|
|
3
3
|
fullwave/medium.py,sha256=qoAh-aYLVkC_ojt1K61hDNV0j9KfIeRkh35tZU8gSKM,68574
|
|
4
4
|
fullwave/sensor.py,sha256=McHyaSKxM11_Kfb_2hfnOtV9Z1UMBCLkeovncLT8_bA,9308
|
|
5
5
|
fullwave/source.py,sha256=5IHktJubOsk94qBWlLBbu-Vxo2UczVFL4EnEyKqEIUE,21321
|
|
6
|
-
fullwave/transducer.py,sha256=
|
|
6
|
+
fullwave/transducer.py,sha256=H67VF_T2KKF68mWtXG6DrVYKaaZieGc7p-IDtWxX1SQ,77623
|
|
7
7
|
fullwave/transmit.py,sha256=BAD_G-J7Q1pMppXw1yjLrQDUdkhPq-3Q4eNq4jaf7Ss,9693
|
|
8
8
|
fullwave/beamformer/__init__.py,sha256=tNPq_5lfLXfAdueyfQtdTxLTVEV6nEDkeSGAeTf7Q5s,88
|
|
9
|
-
fullwave/beamformer/beamformer.py,sha256=
|
|
9
|
+
fullwave/beamformer/beamformer.py,sha256=7dlQVArUGpUeUP6O-9QyMpR6asbbI5JBVJ_F1fpSowU,6175
|
|
10
10
|
fullwave/constants/__init__.py,sha256=WOmkhLsSe86D71gwKsa4Tz8QPeOdayLGgvBCh7UEOPk,104
|
|
11
11
|
fullwave/constants/material_properties.py,sha256=SouMKD4ONzYl0VU8ZsC2I_85y-ZLj9scRNwAArCD7bg,2729
|
|
12
12
|
fullwave/medium_builder/__init__.py,sha256=QVz0sA4niq51O94RnXUVlHutx7FYEIa1TRcR5fDc_Ps,201
|
|
@@ -28,7 +28,7 @@ fullwave/solver/input_file_writer.py,sha256=tzi1MZF-KhNIstrhlu-2lTWGInRtbPaSSFNw
|
|
|
28
28
|
fullwave/solver/launcher.py,sha256=pNZBecyANOtGP9rrG-kfa1HDrXH7qCT8BL4NCTmsRog,13272
|
|
29
29
|
fullwave/solver/pml_builder.py,sha256=Pfy3AklZT-p6TX61A5ntFmpt2mJb5TDaV-gdtpjIwNo,87341
|
|
30
30
|
fullwave/solver/shipped_database.py,sha256=lPqlgY-ShDyRqtH65m5RyNIMVlqswECVDNh5XCk1Yzg,3194
|
|
31
|
-
fullwave/solver/solver.py,sha256
|
|
31
|
+
fullwave/solver/solver.py,sha256=rMVmBB294f3b2PoQpSJWlEifzxPo9ssFOffzLJ5NkjE,97971
|
|
32
32
|
fullwave/solver/source_type.py,sha256=iozo2wGuFLAbhRmXKETcmYV2I0UrA-AB3mnbFRp2s1o,11545
|
|
33
33
|
fullwave/solver/utils.py,sha256=0Q9PKl-4hohWXGzuldqo2XubI-LEHMjs0kYgb4oTLC0,5561
|
|
34
34
|
fullwave/solver/bins/_gpu/2d/num_relax=2/fullwave2_2d_2_relax_multi_gpu_cuda118,sha256=bIzyHFzr_bsIBSlCcRA3M9LlTSRrBVn6ASJHyozvcwU,1245432
|
|
@@ -44,9 +44,9 @@ fullwave/solver/bins/database/relaxation_params_database_num_relax=3_20260828_15
|
|
|
44
44
|
fullwave/solver/bins/database/relaxation_params_database_num_relax=4_20260828_1456.mat,sha256=YfR8RaWQn2dDYiw9TACSUx27QUpKhO7iJQA8rAPLHZA,253928
|
|
45
45
|
fullwave/solver/bins/database/relaxation_params_database_num_relax=4_20260828_1456_invalid_cells.json,sha256=-qA_p-S_3rr8Hl-fXfgvI6m2EED3duc_wW0RlKDUHoE,63983
|
|
46
46
|
fullwave/ultra/__init__.py,sha256=O6-Dw5_UmmG6ikKQ8NCXrmkBjG_CHzWJpBDTwfUhcM0,356
|
|
47
|
-
fullwave/ultra/backend.py,sha256=
|
|
48
|
-
fullwave/ultra/backend_2d.py,sha256=
|
|
49
|
-
fullwave/ultra/binary.py,sha256=
|
|
47
|
+
fullwave/ultra/backend.py,sha256=_L2B9OUNL8suime6bPp-GD89RYn4bg37jG3WZv6qnKI,18627
|
|
48
|
+
fullwave/ultra/backend_2d.py,sha256=f_cK-W9dJeyRfbkGCfCJ60wwIRILhnHjMRwlGuWdW88,21552
|
|
49
|
+
fullwave/ultra/binary.py,sha256=xouFNbwi3NMaH8kuAgk1GQeLGe8DZrwG0RDBftDav-8,4795
|
|
50
50
|
fullwave/ultra/dat_io.py,sha256=mmHfMSsBFUeNSkDC4IMKwGMB_ryA5V_FYzlAG_G6Ht8,2170
|
|
51
51
|
fullwave/ultra/dat_io_3d.py,sha256=ZVBdHe-caTHdofkHUqPlHZSWKlv793ll6juofH5W6y4,1183
|
|
52
52
|
fullwave/ultra/errors.py,sha256=Z1uY2sdnxAA0_fzFf3QmYgR0ZyBJGmsdQyrzoLhyEFY,962
|
|
@@ -54,20 +54,21 @@ fullwave/ultra/factory.py,sha256=hfEAXz2D8EHscgsy2XFK3Iiom8dGdrT2o5yDFGk5qds,548
|
|
|
54
54
|
fullwave/ultra/medium_to_aexp.py,sha256=_uOHDhH6lwQfj4sSgE_jd5Ujq5vCnXvdCdu6Fcx9QKE,1864
|
|
55
55
|
fullwave/ultra/stencil.py,sha256=XG5rPZsJbKaFDkli2GVmFdeqP72QkwTIEZc_49vGmNE,4912
|
|
56
56
|
fullwave/ultra/time_axis.py,sha256=KXsc2EaT6zo-Glh27nxxX50BfpC3oO8jkwbO6SgE3bg,1825
|
|
57
|
-
fullwave/ultra/writer_2d.py,sha256=
|
|
57
|
+
fullwave/ultra/writer_2d.py,sha256=QlVZyED8TwtbQQKZ40v0VnWm-DIgo735RCg_c9Ad4Jg,7645
|
|
58
58
|
fullwave/ultra/writer_3d.py,sha256=gW-GcWWItdE43rcwx6YWn_O9edTwkiCeIcX1IIrJAz0,6985
|
|
59
59
|
fullwave/utils/__init__.py,sha256=YKBUZnQ6E9AiT0tt4gL9HVapvcGv0lbcXXQyF-IWBXc,517
|
|
60
60
|
fullwave/utils/check_functions.py,sha256=benZqFjOQF7aaQDTBGaOq-eplJ-dC8Dz4jcqPE-9HSk,1514
|
|
61
61
|
fullwave/utils/coordinates.py,sha256=TEVQE9JXUIVFoKEyg8ZZMeGh31elbpecHnDgbljHQ14,4402
|
|
62
|
+
fullwave/utils/gpu_memory.py,sha256=yitC9ZaVws5wBXjsaCuW7qGPDZlrbelYLsSDnxMilkQ,3420
|
|
62
63
|
fullwave/utils/memory_tempfile.py,sha256=RqoFX67VCMIHNIDYWwHLCaS4dwSPK6eKsvElXX43Qjg,15083
|
|
63
64
|
fullwave/utils/numerical.py,sha256=khdi2KE3y_Ky9cEnmGXvVHjeYuKfb-8gQTV1a8nx9uA,3260
|
|
64
65
|
fullwave/utils/plot_utils.py,sha256=SoByd2fEr3VdvMM9D2kNq8VgaAQ-fh82bDGRFdeq2R8,39327
|
|
65
66
|
fullwave/utils/pulse.py,sha256=aqnCdBxivACouIGlot1aE2k4V9nctUxNCEPzdBpsupg,2811
|
|
66
|
-
fullwave/utils/relaxation_parameters.py,sha256=
|
|
67
|
+
fullwave/utils/relaxation_parameters.py,sha256=pNul3_8rrbBrjkcc0nKBJ6An9y3bI92DVxNu1Ww8cf4,48133
|
|
67
68
|
fullwave/utils/scatterer.py,sha256=CLqD085f9pqX4cF1YiT2IkzT5P0o1xp45Do7lf5BLnM,24049
|
|
68
69
|
fullwave/utils/signal_filter.py,sha256=_Tf42aBygGtIUrcSPybYqvZoAW2o_4dd-hHTEvZ8--E,5219
|
|
69
70
|
fullwave/utils/signal_process.py,sha256=MP38r37JSPHN4sT-lIReXS5BT2GT_tkRxD0Y1wQ-lNs,5153
|
|
70
|
-
fullwave25-1.4.2.
|
|
71
|
-
fullwave25-1.4.2.
|
|
72
|
-
fullwave25-1.4.2.
|
|
73
|
-
fullwave25-1.4.2.
|
|
71
|
+
fullwave25-1.4.2.dev3.dist-info/METADATA,sha256=q0L2mArY9AQfnOaJKDYCLIke8U4wNM0w5TOm-EPxMIU,28226
|
|
72
|
+
fullwave25-1.4.2.dev3.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
|
|
73
|
+
fullwave25-1.4.2.dev3.dist-info/licenses/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
|
|
74
|
+
fullwave25-1.4.2.dev3.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|