fullwave25 1.4.1.dev2__py3-none-any.whl → 1.4.2.dev0__py3-none-any.whl

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