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 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-dev0"
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.6"
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 when available."""
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
- from tqdm import tqdm # noqa: PLC0415
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()
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
- return getattr(self.source, name, None)
351
- if getattr(self.source, name, None) is None:
358
+ signal = getattr(self.source, name, None)
359
+ elif getattr(self.source, name, None) is None:
352
360
  return None
353
- return np.concatenate([getattr(member, name) for member in members], axis=0)
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", nt_ic),
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))
@@ -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)