fullwave25 1.3.0.dev0__py3-none-any.whl → 1.3.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
@@ -1,10 +1,13 @@
1
1
  """fullwave module."""
2
2
 
3
3
  import logging
4
+ import os
4
5
  import platform
5
6
  import time
6
7
  from importlib.metadata import PackageNotFoundError, version
7
8
 
9
+ import numexpr
10
+
8
11
  from .grid import Grid
9
12
  from .medium import Medium, MediumExponentialAttenuation, MediumRelaxationMaps
10
13
  from .sensor import Sensor
@@ -63,7 +66,17 @@ try:
63
66
  __version__ = version("fullwave")
64
67
  except PackageNotFoundError:
65
68
  # Update via bump-my-version, not manually
66
- __version__ = "1.3.0-dev0"
69
+ __version__ = "1.3.2-dev0"
67
70
 
68
71
  VERSION = __version__ # for convenience
69
72
  logger.info("Fullwave version: %s", __version__)
73
+
74
+ # numexpr keeps 16 threads unless told otherwise, and the medium, the absorbing
75
+ # layer and the coefficients run through it on grids of hundreds of millions of
76
+ # cells. A thread count set in the environment is left alone.
77
+ if not any(
78
+ os.environ.get(name)
79
+ for name in ("NUMEXPR_MAX_THREADS", "NUMEXPR_NUM_THREADS", "OMP_NUM_THREADS")
80
+ ):
81
+ numexpr.set_num_threads(min(numexpr.detect_number_of_cores(), numexpr.MAX_THREADS))
82
+ logger.info("numexpr threads: %d", numexpr.get_num_threads())
fullwave/medium.py CHANGED
@@ -19,6 +19,7 @@ from fullwave.solver.utils import initialize_relaxation_param_dict
19
19
  from fullwave.utils import check_functions, plot_utils
20
20
  from fullwave.utils.coordinates import coords_to_map, map_to_coords
21
21
  from fullwave.utils.relaxation_parameters import (
22
+ _array_module,
22
23
  band_scaled_sound_speed,
23
24
  generate_relaxation_params,
24
25
  )
@@ -200,7 +201,8 @@ def _make_the_lossless_voxels_lossless(
200
201
  """
201
202
  if not relaxation_param_dict:
202
203
  return
203
- coefficient = np.asarray(alpha_coeff)
204
+ xp = np if np.ndim(alpha_coeff) == 0 else _array_module(alpha_coeff)
205
+ coefficient = xp.asarray(alpha_coeff)
204
206
  if coefficient.ndim == 0:
205
207
  if float(coefficient) != 0.0:
206
208
  return
@@ -273,6 +275,7 @@ class MediumRelaxationMaps:
273
275
  n_jobs: int = -1,
274
276
  dtype: type = np.float64,
275
277
  use_gpu: bool = False,
278
+ compute_coefficients: bool = True,
276
279
  ) -> None:
277
280
  """Medium class for Fullwave.
278
281
 
@@ -330,6 +333,10 @@ class MediumRelaxationMaps:
330
333
  use_gpu : bool, optional
331
334
  If True, use CuPy for GPU-accelerated computation (default is False).
332
335
  Requires CuPy to be installed. Falls back to CPU if CuPy is unavailable.
336
+ compute_coefficients : bool, optional
337
+ If True (the default), compute ``relaxation_param_dict_for_fw2``
338
+ here. False leaves it empty for a caller that writes every entry
339
+ itself, as the PML builder does.
333
340
 
334
341
  """
335
342
  check_functions.check_compatible_value(
@@ -354,7 +361,6 @@ class MediumRelaxationMaps:
354
361
  try:
355
362
  self.relaxation_param_dict = initialize_relaxation_param_dict(
356
363
  n_relaxation_mechanisms=n_relaxation_mechanisms,
357
- value=xp.zeros_like(xp.asarray(sound_speed), dtype=self.dtype),
358
364
  )
359
365
  self.grid = grid
360
366
  self.is_3d = grid.is_3d
@@ -379,7 +385,6 @@ class MediumRelaxationMaps:
379
385
  xp = np
380
386
  self.relaxation_param_dict = initialize_relaxation_param_dict(
381
387
  n_relaxation_mechanisms=n_relaxation_mechanisms,
382
- value=np.zeros_like(np.asarray(sound_speed), dtype=self.dtype),
383
388
  )
384
389
  self.grid = grid
385
390
  self.is_3d = grid.is_3d
@@ -404,12 +409,23 @@ class MediumRelaxationMaps:
404
409
  relaxation_param_updates=relaxation_param_dict,
405
410
  )
406
411
  self.use_isotropic_relaxation = use_isotropic_relaxation
407
- self.relaxation_param_dict_for_fw2 = self._calc_relaxation_param_dict_for_fw2(
408
- use_isotropic_relaxation=self.use_isotropic_relaxation,
412
+ self.relaxation_param_dict_for_fw2 = (
413
+ self._calc_relaxation_param_dict_for_fw2(
414
+ use_isotropic_relaxation=self.use_isotropic_relaxation,
415
+ )
416
+ if compute_coefficients
417
+ else {}
409
418
  )
410
419
  self.check_fields()
411
420
  logger.debug("MediumRelaxationMaps instance created.")
412
421
 
422
+ def build_coefficients(self) -> None:
423
+ """Compute ``relaxation_param_dict_for_fw2`` if the constructor was told to skip it."""
424
+ if not self.relaxation_param_dict_for_fw2:
425
+ self.relaxation_param_dict_for_fw2 = self._calc_relaxation_param_dict_for_fw2(
426
+ use_isotropic_relaxation=self.use_isotropic_relaxation,
427
+ )
428
+
413
429
  def _update_relaxation_param_dict(
414
430
  self,
415
431
  relaxation_param_updates: dict[str, NDArray[np.float64]],
@@ -1627,12 +1643,18 @@ class Medium:
1627
1643
  parameters = self._relaxation_parameters_of(sound_speed, alpha_coeff, alpha_power)
1628
1644
  return parameters, sound_speed
1629
1645
 
1630
- def build(self) -> MediumRelaxationMaps:
1646
+ def build(self, *, compute_coefficients: bool = True) -> MediumRelaxationMaps:
1631
1647
  """Retrieve the relaxation parameters from alpha and power maps.
1632
1648
 
1633
1649
  it uses the relaxation parameters look up table
1634
1650
  to generate the relaxation parameters.
1635
1651
 
1652
+ Parameters
1653
+ ----------
1654
+ compute_coefficients : bool, optional
1655
+ Passed to `MediumRelaxationMaps`. False skips the solver
1656
+ coefficients, which a caller that extends the maps first does not use.
1657
+
1636
1658
  Returns
1637
1659
  -------
1638
1660
  MediumRelaxationMaps: An instance of MediumRelaxationMaps
@@ -1670,6 +1692,7 @@ class Medium:
1670
1692
  n_jobs=self.n_jobs,
1671
1693
  dtype=self.dtype,
1672
1694
  use_gpu=self.use_gpu,
1695
+ compute_coefficients=compute_coefficients,
1673
1696
  )
1674
1697
 
1675
1698
  def _db_mhz_cm_to_a_exp(
@@ -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.4.1"
31
+ BINARY_RELEASE_TAG = "fullwave_bin_v1.4.2"
32
32
 
33
33
 
34
34
  def _download_url(filename: str, tag: str) -> str:
@@ -56,7 +56,7 @@ class InputFileWriter:
56
56
  path_fullwave_simulation_bin: Path = Path(__file__).parent / "bins" / "fullwave_solver_gpu",
57
57
  validate_input: bool = True,
58
58
  use_exponential_attenuation: bool = False,
59
- use_isotropic_relaxation: bool = False,
59
+ use_isotropic_relaxation: bool = True,
60
60
  release_after_write: bool = False,
61
61
  pml_thickness: int = 0,
62
62
  exponential_attenuation_pml_thickness_px: int = 0,
@@ -94,6 +94,7 @@ class InputFileWriter:
94
94
  to reduce memory usage while retaining accuracy.
95
95
  For 2D it will reduce the memory usage by approximately 15%.
96
96
  For 3D it will reduce the memory usage by approximately 25%.
97
+ Only True is supported, and False raises NotImplementedError.
97
98
  This option omits the anisotropic relaxation mechanisms to model the attenuation.
98
99
  We usually recommend using isotropic relaxation mechanisms
99
100
  unless the anisotropic attenuation is required for the simulation.
@@ -126,6 +127,12 @@ class InputFileWriter:
126
127
  """
127
128
  logger.debug("Initializing InputFileWriter instance.")
128
129
 
130
+ if use_isotropic_relaxation is False:
131
+ error_msg = (
132
+ "Currently, only isotropic relaxation is supported. "
133
+ "Please set use_isotropic_relaxation to True for the simulation."
134
+ )
135
+ raise NotImplementedError(error_msg)
129
136
  self._work_dir = Path(work_dir)
130
137
  self.path_fullwave_simulation_bin = path_fullwave_simulation_bin
131
138
  self.use_isotropic_relaxation = use_isotropic_relaxation
@@ -1228,16 +1235,8 @@ class InputFileWriter:
1228
1235
  mechanisms = range(1, self.medium.n_relaxation_mechanisms + 1)
1229
1236
  for nu in mechanisms:
1230
1237
  var_name_list.extend([f"apmlu{nu}", f"bpmlu{nu}", f"apmlx{nu}", f"bpmlx{nu}"])
1231
- if not self.use_isotropic_relaxation:
1232
- var_name_list.extend(["kappay", "kappaw"])
1233
- for nu in mechanisms:
1234
- var_name_list.extend([f"apmlw{nu}", f"apmly{nu}", f"bpmlw{nu}", f"bpmly{nu}"])
1235
1238
  if self.is_3d:
1236
- var_name_list.append("modZ")
1237
- if self.is_3d and not self.use_isotropic_relaxation:
1238
- var_name_list.extend(["nZ", "dZ", "kappaz", "kappav"])
1239
- for nu in mechanisms:
1240
- var_name_list.extend([f"apmlz{nu}", f"apmlv{nu}", f"bpmlz{nu}", f"bpmlv{nu}"])
1239
+ var_name_list.extend(["modZ", "nZ", "dZ"])
1241
1240
  for var_name in var_name_list:
1242
1241
  src_data = src_dir / f"{var_name}.dat"
1243
1242
  dst_data = dst_dir / f"{var_name}.dat"
@@ -1409,6 +1408,29 @@ class InputFileWriter:
1409
1408
  dst.unlink()
1410
1409
  dst.symlink_to(self.path_fullwave_simulation_bin.resolve())
1411
1410
 
1411
+ @staticmethod
1412
+ def _write_in_slabs(
1413
+ variable_mat: np.ndarray,
1414
+ dtype: DTypeLike,
1415
+ save_path: str | Path,
1416
+ slab_bytes: int = 256 * 1024**2,
1417
+ ) -> None:
1418
+ """Write an array in C order, cast one slab of its first axis at a time.
1419
+
1420
+ The bytes are those of one whole cast buffer, and the largest buffer
1421
+ held is one slab, so many maps can be written at once.
1422
+ """
1423
+ dtype = np.dtype(dtype)
1424
+ if variable_mat.ndim == 0:
1425
+ np.ascontiguousarray(variable_mat, dtype=dtype).tofile(save_path)
1426
+ return
1427
+ row_bytes = max(1, dtype.itemsize * (variable_mat.size // max(variable_mat.shape[0], 1)))
1428
+ rows_per_slab = max(1, slab_bytes // row_bytes)
1429
+ with Path(save_path).open("wb") as handle:
1430
+ for start in range(0, variable_mat.shape[0], rows_per_slab):
1431
+ slab = variable_mat[start : start + rows_per_slab]
1432
+ np.ascontiguousarray(slab, dtype=dtype).tofile(handle)
1433
+
1412
1434
  @staticmethod
1413
1435
  def _write_ic(
1414
1436
  fname: str | Path,
@@ -1418,11 +1440,7 @@ class InputFileWriter:
1418
1440
  logger.debug("Writing initial condition matrix to %s", fname, stacklevel=2)
1419
1441
 
1420
1442
  t0 = time.perf_counter()
1421
- out = np.ascontiguousarray(
1422
- icmat.T,
1423
- dtype=np.float32,
1424
- ) # does transpose+cast into one new buffer
1425
- out.tofile(fname)
1443
+ InputFileWriter._write_in_slabs(icmat.T, np.float32, fname)
1426
1444
  t1 = time.perf_counter()
1427
1445
 
1428
1446
  logger.debug("Initial condition matrix written in %.2e seconds", t1 - t0, stacklevel=2)
@@ -1486,8 +1504,7 @@ class InputFileWriter:
1486
1504
  if variable_mat.dtype == dtype and variable_mat.flags.c_contiguous:
1487
1505
  variable_mat.tofile(save_path) # writes in C order
1488
1506
  else:
1489
- out = np.ascontiguousarray(variable_mat, dtype=dtype) # at most one new buffer
1490
- out.tofile(save_path)
1507
+ InputFileWriter._write_in_slabs(variable_mat, dtype, save_path)
1491
1508
 
1492
1509
  t1 = time.perf_counter()
1493
1510
  logger.debug("Matrix written in %.2e seconds", t1 - t0, stacklevel=2)
@@ -5,6 +5,7 @@ from __future__ import annotations
5
5
  import concurrent.futures
6
6
  import gc
7
7
  import logging
8
+ import os
8
9
  from collections import OrderedDict
9
10
  from dataclasses import dataclass, field
10
11
  from functools import cached_property
@@ -312,12 +313,20 @@ class PMLBuilder:
312
313
  logger.debug("building extended medium for pml...")
313
314
  medium_as_given = self.medium_org
314
315
  if not isinstance(self.medium_org, fullwave.MediumRelaxationMaps):
315
- self.medium_org = self.medium_org.build()
316
+ logger.info(
317
+ "Building the relaxation maps of %d cells...", int(self.medium_org.sound_speed.size)
318
+ )
319
+ self.medium_org = self.medium_org.build(compute_coefficients=False)
316
320
  base_attrs = ["sound_speed", "density", "beta"]
317
321
  original_alpha_coeff = getattr(self.medium_org, "alpha_coeff", None)
318
322
  if original_alpha_coeff is not None and np.ndim(original_alpha_coeff) != 0:
319
323
  base_attrs.append("alpha_coeff")
320
324
  relax_attrs = list(self.medium_org.relaxation_param_dict.keys())
325
+ logger.info(
326
+ "Extending %d maps by %d cells on each side...",
327
+ len(base_attrs) + len(relax_attrs),
328
+ self.num_boundary_points,
329
+ )
321
330
 
322
331
  if self.xp is not np:
323
332
  # Pass CuPy arrays directly — multi-GPU extension uses D2D copy (NVLink)
@@ -366,7 +375,11 @@ class PMLBuilder:
366
375
  n_jobs=self.medium_org.n_jobs,
367
376
  dtype=getattr(self.medium_org, "dtype", np.float64),
368
377
  use_gpu=False,
378
+ compute_coefficients=False,
369
379
  )
380
+ # The extended maps carry everything the absorbing layer reads, so the
381
+ # interior maps are released here rather than held through the run.
382
+ self.medium_org = medium_as_given
370
383
  logger.debug("building extended medium for pml...done")
371
384
 
372
385
  logger.debug("building extended source for pml...")
@@ -861,10 +874,16 @@ class PMLBuilder:
861
874
 
862
875
  eps = xp.finfo(xp.float64).eps
863
876
 
864
- rate = d_x / kappa_x + alpha_x
865
877
  two_over_dt = 2.0 / dt
866
- b = (two_over_dt - rate) / (two_over_dt + rate)
867
- a = -(d_x / (kappa_x**2 + eps)) / (rate + two_over_dt)
878
+ if xp is np:
879
+ rate = ne.evaluate("d_x / kappa_x + alpha_x")
880
+ b = ne.evaluate("(two_over_dt - rate) / (two_over_dt + rate)")
881
+ a = ne.evaluate("-(d_x / (kappa_x ** 2 + eps)) / (rate + two_over_dt)")
882
+ else:
883
+ rate = d_x / kappa_x + alpha_x
884
+ b = (two_over_dt - rate) / (two_over_dt + rate)
885
+ a = -(d_x / (kappa_x**2 + eps)) / (rate + two_over_dt)
886
+ del rate
868
887
 
869
888
  if output_dtype is not None and output_dtype != xp.float64:
870
889
  a = a.astype(output_dtype, copy=False)
@@ -886,6 +905,10 @@ class PMLBuilder:
886
905
  """
887
906
  logger.debug("Running PML builder...")
888
907
  if use_pml:
908
+ logger.info(
909
+ "Building the absorbing layer on %d cells...",
910
+ int(self.extended_medium.sound_speed.size),
911
+ )
889
912
  extended_medium: fullwave.MediumRelaxationMaps = self.extended_medium.build()
890
913
  if self.is_3d:
891
914
  return self._apply_pml_3d(
@@ -901,30 +924,54 @@ class PMLBuilder:
901
924
  )
902
925
 
903
926
  extended_medium: fullwave.MediumRelaxationMaps = self.extended_medium.build()
927
+ extended_medium.build_coefficients()
904
928
  return extended_medium
905
929
 
906
930
  def _medium_copy(self, relaxation_param_dict: dict, key: str) -> NDArray[np.float64]:
907
- """Return a fresh float64 copy of one medium map."""
908
- return self.xp.array(relaxation_param_dict[key], dtype=self.xp.float64, copy=True)
931
+ """Return a fresh float64 copy of one medium map.
932
+
933
+ On the host the copy is split along the first axis across threads,
934
+ because one thread copying a map of the whole padded grid is the slowest
935
+ step of the absorbing layer.
936
+ """
937
+ source = relaxation_param_dict[key]
938
+ if self.xp is not np or np.ndim(source) == 0:
939
+ return self.xp.array(source, dtype=self.xp.float64, copy=True)
940
+ copied = np.empty(np.shape(source), dtype=np.float64)
941
+ n_rows = copied.shape[0]
942
+ n_slabs = max(1, min(n_rows, os.cpu_count() or 1, ne.MAX_THREADS))
943
+ edges = np.linspace(0, n_rows, n_slabs + 1).astype(int)
944
+
945
+ def copy_slab(start: int, stop: int) -> None:
946
+ np.copyto(copied[start:stop], source[start:stop], casting="unsafe")
947
+
948
+ with concurrent.futures.ThreadPoolExecutor(max_workers=n_slabs) as pool:
949
+ list(pool.map(copy_slab, edges[:-1], edges[1:]))
950
+ return copied
909
951
 
910
952
  def _ramp_on_every_axis(
911
- self, field: NDArray[np.float64], **kwargs: object
953
+ self, field: NDArray[np.float64], *, in_place: bool = False, **kwargs: object
912
954
  ) -> NDArray[np.float64]:
913
- """Apply one ramp along each grid axis in turn, in two dimensions or three."""
955
+ """Apply one ramp along each grid axis in turn, in two dimensions or three.
956
+
957
+ The ramp writes only the layers at both ends of each axis. With
958
+ `in_place` the field is written itself, otherwise one copy is written.
959
+ """
914
960
  is_3d = field.ndim == self.three_dimensions
915
- out = field
961
+ out = field if in_place else self.xp.array(field, copy=True)
916
962
  for axis_index in range(field.ndim):
917
963
  out = self._apply_transition_and_pml(
918
- out, array_shape=field.shape, axis=axis_index, is_3d=is_3d, **kwargs
964
+ out, array_shape=field.shape, axis=axis_index, is_3d=is_3d, in_place=True, **kwargs
919
965
  )
920
966
  return out
921
967
 
922
968
  def _empty_damping_in_transition_layer(
923
- self, damping: NDArray[np.float64]
969
+ self, damping: NDArray[np.float64], *, in_place: bool = False
924
970
  ) -> NDArray[np.float64]:
925
971
  """Return the damping strength taken to zero across the transition layer."""
926
972
  return self._ramp_on_every_axis(
927
973
  damping,
974
+ in_place=in_place,
928
975
  value_target=0.0,
929
976
  transition_type="cosine",
930
977
  transit_within_transition_layer=True,
@@ -935,10 +982,13 @@ class PMLBuilder:
935
982
  damping: NDArray[np.float64],
936
983
  n_polynomial: float,
937
984
  d_target_pml: float,
985
+ *,
986
+ in_place: bool = False,
938
987
  ) -> NDArray[np.float64]:
939
988
  """Return the damping strength raised to the PML target across the PML layer."""
940
989
  return self._ramp_on_every_axis(
941
990
  damping,
991
+ in_place=in_place,
942
992
  value_target=d_target_pml,
943
993
  n_polynomial=n_polynomial,
944
994
  transition_type="polynomial",
@@ -949,27 +999,36 @@ class PMLBuilder:
949
999
  self,
950
1000
  relaxation_frequency: NDArray[np.float64],
951
1001
  entrance: float,
1002
+ *,
1003
+ in_place: bool = False,
952
1004
  ) -> NDArray[np.float64]:
953
1005
  """Return the relaxation frequency replaced by the CFS ramp inside the PML layer.
954
1006
 
955
1007
  Replaced rather than ramped into, because ramping a relaxation frequency
956
1008
  through the evaluation frequency is what confines the damping to the
957
- outer edge.
1009
+ outer edge. The PML layer is the slab of `edge` cells at both ends of
1010
+ each axis, so only those slabs are copied from the ramp.
958
1011
  """
959
1012
  xp = self.xp
960
1013
  edge = self.m_spatial_order + self.n_pml_layer
961
1014
  ramp = self._ramp_on_every_axis(
962
1015
  xp.full_like(relaxation_frequency, entrance),
1016
+ in_place=True,
963
1017
  value_target=0.0,
964
1018
  transition_type="linear",
965
1019
  transit_within_pml_layer=True,
966
1020
  )
967
- inside_pml_layer = xp.zeros(relaxation_frequency.shape, dtype=bool)
968
- for axis_index in range(relaxation_frequency.ndim):
969
- view = xp.moveaxis(inside_pml_layer, axis_index, 0)
970
- view[:edge] = True
971
- view[view.shape[0] - edge :] = True
972
- return xp.where(inside_pml_layer, xp.asarray(ramp), xp.asarray(relaxation_frequency))
1021
+ spliced = (
1022
+ xp.asarray(relaxation_frequency)
1023
+ if in_place
1024
+ else xp.array(relaxation_frequency, copy=True)
1025
+ )
1026
+ for axis_index in range(spliced.ndim):
1027
+ target = xp.moveaxis(spliced, axis_index, 0)
1028
+ source = xp.moveaxis(ramp, axis_index, 0)
1029
+ target[:edge] = source[:edge]
1030
+ target[target.shape[0] - edge :] = source[source.shape[0] - edge :]
1031
+ return spliced
973
1032
 
974
1033
  def _build_decoupled(
975
1034
  self,
@@ -1002,16 +1061,16 @@ class PMLBuilder:
1002
1061
  relaxation_frequency = self._medium_copy(
1003
1062
  relaxation_param_dict, rename_dict[f"alpha_{letter}_nu{nu}"]
1004
1063
  )
1005
- emptied = self._empty_damping_in_transition_layer(damping)
1064
+ emptied = self._empty_damping_in_transition_layer(damping, in_place=True)
1006
1065
  if nu > 1:
1007
1066
  out_dict[f"d_{letter}_nu{nu}"] = emptied
1008
1067
  out_dict[f"alpha_{letter}_nu{nu}"] = relaxation_frequency
1009
1068
  continue
1010
1069
  out_dict[f"d_{letter}_nu{nu}"] = self._raise_damping_in_pml_layer(
1011
- emptied, n_polynomial, d_target_pml
1070
+ emptied, n_polynomial, d_target_pml, in_place=True
1012
1071
  )
1013
1072
  out_dict[f"alpha_{letter}_nu{nu}"] = self._splice_entrance_frequency_into_pml_layer(
1014
- relaxation_frequency, entrance
1073
+ relaxation_frequency, entrance, in_place=True
1015
1074
  )
1016
1075
  return out_dict
1017
1076
 
@@ -1124,9 +1183,11 @@ class PMLBuilder:
1124
1183
  found = np.asarray(relaxation_param_dict[key], dtype=np.float64)
1125
1184
  if found.ndim == 0 or np.allclose(found, 1.0):
1126
1185
  continue
1186
+ found = self.xp.array(found, copy=True)
1127
1187
  for axis in range(3 if is_3d else 2):
1128
1188
  found = self._apply_transition_and_pml(
1129
1189
  found,
1190
+ in_place=True,
1130
1191
  value_target=1.0,
1131
1192
  array_shape=found.shape,
1132
1193
  axis=axis,
@@ -1213,13 +1274,9 @@ class PMLBuilder:
1213
1274
  # Return keys + values so parent can update dict safely
1214
1275
  return (f"a_pml_{axis}{nu}", a, f"b_pml_{axis}{nu}", b)
1215
1276
 
1216
- if self.xp is not np:
1217
- # GPU path: run sequentially
1218
- results = [_worker(nu, axis) for nu, axis in tasks]
1219
- else:
1220
- with concurrent.futures.ThreadPoolExecutor() as executor:
1221
- futures = [executor.submit(_worker, nu, axis) for nu, axis in tasks]
1222
- results = [f.result() for f in futures]
1277
+ # One task at a time. numexpr threads each expression, and a pool of
1278
+ # tasks held every task's temporaries of the whole grid at once.
1279
+ results = [_worker(nu, axis) for nu, axis in tasks]
1223
1280
 
1224
1281
  for a_key, a_val, b_key, b_val in results:
1225
1282
  out_dict[a_key] = a_val
@@ -1312,13 +1369,9 @@ class PMLBuilder:
1312
1369
  # Return keys + values so parent can update dict safely
1313
1370
  return (f"a_pml_{axis}{nu}", a, f"b_pml_{axis}{nu}", b)
1314
1371
 
1315
- if self.xp is not np:
1316
- # GPU path: run sequentially
1317
- results = [_worker(nu, axis) for nu, axis in tasks]
1318
- else:
1319
- with concurrent.futures.ThreadPoolExecutor() as executor:
1320
- futures = [executor.submit(_worker, nu, axis) for nu, axis in tasks]
1321
- results = [f.result() for f in futures]
1372
+ # One task at a time. numexpr threads each expression, and a pool of
1373
+ # tasks held every task's temporaries of the whole grid at once.
1374
+ results = [_worker(nu, axis) for nu, axis in tasks]
1322
1375
 
1323
1376
  for a_key, a_val, b_key, b_val in results:
1324
1377
  out_dict[a_key] = a_val
@@ -1347,6 +1400,7 @@ class PMLBuilder:
1347
1400
  transit_within_pml_layer: bool = False,
1348
1401
  disable_the_transition_and_pml: bool = False,
1349
1402
  is_3d: bool = False,
1403
+ in_place: bool = False,
1350
1404
  ) -> NDArray[np.float64]:
1351
1405
  if transit_within_transition_layer and transit_within_pml_layer:
1352
1406
  error_msg = (
@@ -1428,8 +1482,11 @@ class PMLBuilder:
1428
1482
  if use_gpu:
1429
1483
  input_array = xp.asarray(input_array)
1430
1484
 
1431
- # Move axis to 0 for uniform processing
1432
- working_array = xp.moveaxis(input_array, axis, 0).copy()
1485
+ # Move axis to 0 for uniform processing. Only the layers at both ends
1486
+ # are written, so an array the caller owns needs no copy.
1487
+ working_array = xp.moveaxis(input_array, axis, 0)
1488
+ if not in_place:
1489
+ working_array = working_array.copy()
1433
1490
 
1434
1491
  # Apply boundary conditions
1435
1492
  working_array[: m_offset + layer_thickness] = value_target
fullwave/solver/solver.py CHANGED
@@ -1197,11 +1197,21 @@ class Solver:
1197
1197
 
1198
1198
  n_source_timesteps = source.icmat.shape[1]
1199
1199
 
1200
- gb = 1024.0**3
1200
+ gib = 1024.0**3
1201
1201
 
1202
1202
  base_depth = depth // n_gpus
1203
1203
  remainder = depth % n_gpus
1204
1204
 
1205
+ box, hole = (None, None)
1206
+ if not self.use_exponential_attenuation:
1207
+ box, hole = self._relaxation_regions(medium)
1208
+ # Every GPU holds every source matrix and every zero pressure coordinate.
1209
+ source_points = sum(
1210
+ getattr(source, name, 0) or 0
1211
+ for name in ("n_sources", "n_sources_add", "n_sources_u", "n_sources_v", "n_sources_w")
1212
+ )
1213
+ body_start = 0
1214
+
1205
1215
  for rank, dev_id in enumerate(device_ids):
1206
1216
  depth_this = base_depth + (1 if rank < remainder else 0)
1207
1217
 
@@ -1213,10 +1223,12 @@ class Solver:
1213
1223
  n_halo_sides = 2
1214
1224
  local_depth = depth_this + n_halo_sides * halo_depth
1215
1225
  slab = local_depth * lateral
1226
+ low = max(body_start - (halo_depth if rank > 0 else 0), 0)
1227
+ high = min(body_start + depth_this + (halo_depth if rank < n_gpus - 1 else 0), depth)
1228
+ body_start += depth_this
1216
1229
 
1217
1230
  n_sources = max(source.n_sources // n_gpus, 0)
1218
1231
  n_sensors = max(sensor.n_sensors // n_gpus, 0)
1219
- n_air_local = max(medium.n_air // n_gpus, 0)
1220
1232
 
1221
1233
  if self.use_exponential_attenuation:
1222
1234
  total = self._mem_exponential(
@@ -1231,29 +1243,32 @@ class Solver:
1231
1243
  is_3d=self.is_3d,
1232
1244
  )
1233
1245
  else:
1246
+ band, interior = self._band_and_interior(low, high, grid.shape, box, hole)
1234
1247
  total = self._mem_relaxation(
1235
1248
  slab,
1236
1249
  n_deriv_levels,
1237
- n_sources,
1250
+ source_points,
1238
1251
  n_source_timesteps,
1239
1252
  save_gpu_memory=self.save_gpu_memory,
1240
- n_air=n_air_local,
1253
+ n_air=medium.n_air,
1241
1254
  n_sensors=n_sensors,
1242
1255
  n_relax=self.n_relax_mechanisms,
1243
1256
  float_bytes=float_bytes,
1244
1257
  int_bytes=int_bytes,
1245
1258
  is_3d=self.is_3d,
1259
+ band=band,
1260
+ interior=interior,
1246
1261
  )
1247
1262
 
1248
1263
  mode = "exponential" if self.use_exponential_attenuation else "relaxation"
1249
1264
  saving = ", save_gpu_memory=True" if self.save_gpu_memory else ""
1250
1265
  logger.info(
1251
1266
  "GPU memory estimate [GPU %s] (%s mode%s): "
1252
- "%.2f GB (depth=%d +%d halo, lateral=%d)",
1267
+ "%.2f GiB (depth=%d +%d halo, lateral=%d)",
1253
1268
  dev_id.strip(),
1254
1269
  mode,
1255
1270
  saving,
1256
- total / gb,
1271
+ total / gib,
1257
1272
  depth_this,
1258
1273
  n_halo_sides * halo_depth,
1259
1274
  lateral,
@@ -1337,8 +1352,20 @@ class Solver:
1337
1352
  float_bytes: int,
1338
1353
  int_bytes: int,
1339
1354
  is_3d: bool,
1355
+ band: int | None = None,
1356
+ interior: int | None = None,
1340
1357
  ) -> int:
1341
- """Return estimated GPU bytes for relaxation (power-law) solver.
1358
+ """Return estimated GPU bytes for the relaxation (power-law) solver.
1359
+
1360
+ The count follows the allocation of the region trimmed kernel, the same
1361
+ count as ``nrelax_device_bytes`` in the solver's ``gpu_memory_nrelax.cuh``.
1362
+ Over the whole slab the card holds the pressure and one velocity for each
1363
+ axis at two time levels, ``kappa_x2``, ``rho``, ``K``, the two finite
1364
+ amplitude maps and ``dcmap``, and mechanism 1 of the pressure memory, its
1365
+ ``a``, ``b`` and one memory variable for each axis. The mechanisms above
1366
+ the first hold the same over the interior box alone, and the momentum
1367
+ absorbing layer holds its ``a``, ``b`` and one memory variable for each
1368
+ axis over the band around the hole alone.
1342
1369
 
1343
1370
  Parameters
1344
1371
  ----------
@@ -1347,7 +1374,7 @@ class Solver:
1347
1374
  n_deriv_levels : int
1348
1375
  Number of derivative-map levels.
1349
1376
  n_sources : int
1350
- Approximate source count on this GPU.
1377
+ Source points this GPU holds, summed over every source kind.
1351
1378
  n_source_timesteps : int
1352
1379
  Number of source time steps.
1353
1380
  save_gpu_memory : bool
@@ -1362,8 +1389,13 @@ class Solver:
1362
1389
  Bytes per float (4).
1363
1390
  int_bytes : int
1364
1391
  Bytes per int (4).
1365
- is_3d: bool,
1366
- Whether the simulation is 3D (affects sensor memory).
1392
+ is_3d : bool
1393
+ Whether the simulation is 3D.
1394
+ band : int | None
1395
+ Grid points of the momentum layer band on this GPU. None takes the slab.
1396
+ interior : int | None
1397
+ Grid points of this GPU's share of the interior box of the mechanisms
1398
+ above the first. None takes the slab.
1367
1399
 
1368
1400
  Returns
1369
1401
  -------
@@ -1373,36 +1405,152 @@ class Solver:
1373
1405
  """
1374
1406
  fb = float_bytes
1375
1407
  ib = int_bytes
1376
- ndim = 3 if is_3d else 2
1377
- n_fields = 4 if is_3d else 3 # p, u, [v], w
1378
-
1379
- # wave fields: n_fields pairs x 2 time levels
1380
- mem = n_fields * 2 * slab * fb
1381
- # relaxation psi:
1382
- mem += 2 * (ndim * n_relax * 2 * slab * fb)
1383
- # material: rho + K + beta
1384
- mem += 3 * slab * fb
1385
- # kappa: 2 arrays (kappa_x1, kappa_x2)
1386
- mem += 2 * slab * fb
1387
- # PML: pml_x1 + pml_x2, each has 2 * n_relax arrays
1388
- mem += 2 * (2 * n_relax) * slab * fb
1389
- # (dmap + dcmap)
1390
- mem += 9 * 2 * n_deriv_levels * fb + slab * ib
1391
- # source (icmat + coords)
1408
+ axes = 3 if is_3d else 2
1409
+ band = slab if band is None else band
1410
+ interior = slab if interior is None else interior
1411
+ per_mechanism = 2 + axes
1412
+
1413
+ mem = 2 * (axes + 1) * slab * fb
1414
+ mem += 5 * slab * fb
1415
+ mem += slab * ib
1416
+ mem += per_mechanism * slab * fb
1417
+ mem += per_mechanism * max(n_relax - 1, 0) * interior * fb
1418
+ mem += per_mechanism * band * fb
1419
+ mem += 9 * 2 * n_deriv_levels * fb
1392
1420
  if n_sources > 0:
1393
- mem += n_sources * fb if save_gpu_memory else n_sources * n_source_timesteps * fb
1394
- mem += ndim * n_sources * ib
1395
-
1396
- # air
1421
+ samples = n_sources if save_gpu_memory else n_sources * n_source_timesteps
1422
+ mem += samples * fb + axes * n_sources * ib
1397
1423
  if n_air > 0:
1398
- mem += ndim * n_air * ib
1399
- # sensor
1424
+ mem += axes * n_air * ib
1400
1425
  if n_sensors > 0:
1401
- mem += n_sensors * fb
1402
- mem += (ndim + 1) * n_sensors * ib
1403
- mem += n_sensors * ib
1426
+ mem += n_sensors * (fb + (axes + 2) * ib)
1404
1427
  return mem
1405
1428
 
1429
+ @staticmethod
1430
+ def _center_line_span(values: NDArray, axis: int, *, zeros: bool) -> tuple[int, int]:
1431
+ """Return the first and one past the last index of a map's center line along one axis.
1432
+
1433
+ Parameters
1434
+ ----------
1435
+ values : NDArray
1436
+ The map, on the host or on the GPU.
1437
+ axis : int
1438
+ The axis the line runs along. The other axes sit at their centers.
1439
+ zeros : bool
1440
+ True spans the zero cells, False the nonzero cells.
1441
+
1442
+ Returns
1443
+ -------
1444
+ tuple[int, int]
1445
+ The span, empty as ``(0, 0)`` when no cell matches.
1446
+
1447
+ """
1448
+ index = tuple(
1449
+ slice(None) if a == axis else size // 2 for a, size in enumerate(values.shape)
1450
+ )
1451
+ line = values[index]
1452
+ line = line.get() if hasattr(line, "get") else np.asarray(line)
1453
+ found = np.flatnonzero(line == 0 if zeros else line != 0)
1454
+ if found.size == 0:
1455
+ return (0, 0)
1456
+ return (int(found[0]), int(found[-1]) + 1)
1457
+
1458
+ def _relaxation_regions(
1459
+ self,
1460
+ medium: fullwave.MediumRelaxationMaps,
1461
+ ) -> tuple[list[tuple[int, int]] | None, list[tuple[int, int]] | None]:
1462
+ """Return the interior box and the momentum layer hole the kernel will measure.
1463
+
1464
+ The kernel stores every mechanism above the first over the box where its
1465
+ ``a`` is nonzero, and the momentum absorbing layer over the band around the
1466
+ box where the first momentum ``a`` is zero. Both are read here along the
1467
+ center lines of each axis, which is how the kernel finds the hole, and a
1468
+ clean box shape is assumed for both.
1469
+
1470
+ Parameters
1471
+ ----------
1472
+ medium : fullwave.MediumRelaxationMaps
1473
+ The extended medium after the absorbing layer is built.
1474
+
1475
+ Returns
1476
+ -------
1477
+ tuple
1478
+ The box and the hole as one ``(start, end)`` span for each axis. None
1479
+ stands for a map the medium does not carry, which counts as the whole
1480
+ slab.
1481
+
1482
+ """
1483
+ maps = medium.relaxation_param_dict_for_fw2
1484
+ box_maps = [maps.get(f"a_pml_u{nu}") for nu in range(2, self.n_relax_mechanisms + 1)]
1485
+ box_maps = [values for values in box_maps if values is not None]
1486
+ box = None
1487
+ if self.n_relax_mechanisms > 1 and len(box_maps) == self.n_relax_mechanisms - 1:
1488
+ box = []
1489
+ for axis in range(box_maps[0].ndim):
1490
+ spans = [self._center_line_span(values, axis, zeros=False) for values in box_maps]
1491
+ spans = [span for span in spans if span[1] > span[0]]
1492
+ box.append(
1493
+ (min(s for s, _ in spans), max(e for _, e in spans)) if spans else (0, 0)
1494
+ )
1495
+ hole = None
1496
+ if maps.get("a_pml_x1") is not None:
1497
+ values = maps["a_pml_x1"]
1498
+ hole = [self._center_line_span(values, axis, zeros=True) for axis in range(values.ndim)]
1499
+ if any(end <= start for start, end in hole):
1500
+ hole = [(0, 0)] * values.ndim
1501
+ return box, hole
1502
+
1503
+ @staticmethod
1504
+ def _band_and_interior(
1505
+ low: int,
1506
+ high: int,
1507
+ shape: tuple[int, ...],
1508
+ box: list[tuple[int, int]] | None,
1509
+ hole: list[tuple[int, int]] | None,
1510
+ ) -> tuple[int, int]:
1511
+ """Return one GPU's band and interior box cells, as the kernel counts them.
1512
+
1513
+ Parameters
1514
+ ----------
1515
+ low : int
1516
+ The first grid row the GPU holds, its halo included.
1517
+ high : int
1518
+ One past the last grid row the GPU holds, its halo included.
1519
+ shape : tuple[int, ...]
1520
+ The extended grid shape.
1521
+ box : list[tuple[int, int]] | None
1522
+ The interior box, one span for each axis, or None for the whole slab.
1523
+ hole : list[tuple[int, int]] | None
1524
+ The momentum layer hole, one span for each axis, or None for the whole slab.
1525
+
1526
+ Returns
1527
+ -------
1528
+ tuple[int, int]
1529
+ The band cells and the interior cells of this GPU.
1530
+
1531
+ """
1532
+ rows = high - low
1533
+ lateral = int(np.prod(shape[1:]))
1534
+ if box is None:
1535
+ interior = rows * lateral
1536
+ else:
1537
+ box_rows = max(0, min(high, box[0][1]) - max(low, box[0][0]))
1538
+ interior = box_rows * int(np.prod([end - start for start, end in box[1:]]))
1539
+ if hole is None:
1540
+ return rows * lateral, interior
1541
+ hole_low = min(max(hole[0][0] - low, 0), rows)
1542
+ hole_high = max(min(max(hole[0][1] - low, 0), rows), hole_low)
1543
+ hole_rows = hole_high - hole_low
1544
+ band = (rows - hole_rows) * lateral
1545
+ hole_columns = hole[1][1] - hole[1][0]
1546
+ strip_columns = shape[1] - hole_columns
1547
+ if len(shape) == 3:
1548
+ strip_depths = shape[2] - (hole[2][1] - hole[2][0])
1549
+ band += hole_rows * (strip_columns * shape[2] + hole_columns * strip_depths)
1550
+ else:
1551
+ band += hole_rows * strip_columns
1552
+ return band, interior
1553
+
1406
1554
  def print_info(self) -> None:
1407
1555
  """Print the Solver instance information."""
1408
1556
  print(str(self))
fullwave/transmit.py CHANGED
@@ -272,12 +272,28 @@ def signal_of(
272
272
  layers = layer_of_each_pixel(coords, identifiers)
273
273
  center_hertz = grid.f0 if pulse.center_hertz is None else pulse.center_hertz
274
274
 
275
- signal = np.zeros((len(identifiers), grid.nt))
276
- for index, element in enumerate(identifiers):
275
+ # Pixels whose element weight, element delay, pixel delay and layer carry the
276
+ # same bits get the same row, so each row is built once, from one of its
277
+ # pixels, with the arguments that pixel gives.
278
+ element_index = identifiers - 1
279
+ columns = [
280
+ np.asarray(weights, dtype=np.float64)[element_index].view(np.uint64),
281
+ np.asarray(delays_s, dtype=np.float64)[element_index].view(np.uint64),
282
+ np.asarray(layers, dtype=np.uint64),
283
+ ]
284
+ if pixel_delays_s is not None:
285
+ columns.append(np.ascontiguousarray(pixel_delays_s, dtype=np.float64).view(np.uint64))
286
+ _, first_pixel, row_of_pixel = np.unique(
287
+ np.stack(columns, axis=1), axis=0, return_index=True, return_inverse=True
288
+ )
289
+
290
+ rows = np.zeros((len(first_pixel), grid.nt))
291
+ for row, index in enumerate(first_pixel):
292
+ element = identifiers[index]
277
293
  weight = float(weights[element - 1])
278
294
  if weight == 0.0:
279
295
  continue
280
- signal[index] = pulse_utils.gaussian_modulated_sinusoidal_signal(
296
+ rows[row] = pulse_utils.gaussian_modulated_sinusoidal_signal(
281
297
  nt=grid.nt,
282
298
  f0=center_hertz,
283
299
  duration=grid.duration,
@@ -290,4 +306,4 @@ def signal_of(
290
306
  delay_sec=float(delays_s[element - 1])
291
307
  + (0.0 if pixel_delays_s is None else float(pixel_delays_s[index])),
292
308
  )
293
- return signal
309
+ return rows[row_of_pixel.reshape(-1)]
@@ -9,6 +9,7 @@ import time
9
9
  from pathlib import Path
10
10
 
11
11
  import numba as nb
12
+ import numexpr as ne
12
13
  import numpy as np
13
14
  from numpy.typing import NDArray
14
15
  from scipy.io import loadmat
@@ -128,10 +129,17 @@ def _first_order_attenuation(
128
129
  exceeds one and the recursion grows without bound, so that is refused.
129
130
  """
130
131
  scaled = dict(relaxation_param_dict)
132
+ on_the_host = xp is np
131
133
  for direction in ("x1", "x2"):
132
134
  stretching = f"kappa_{direction}"
133
- moved = 1.0 + ratio * (relaxation_param_dict[stretching] - 1.0)
134
- if bool(xp.any(moved <= 0.0)):
135
+ kappa = relaxation_param_dict[stretching]
136
+ if on_the_host:
137
+ moved = ne.evaluate("1.0 + ratio * (kappa - 1.0)")
138
+ collapsed = bool(ne.evaluate("moved <= 0.0").any())
139
+ else:
140
+ moved = 1.0 + ratio * (kappa - 1.0)
141
+ collapsed = bool(xp.any(moved <= 0.0))
142
+ if collapsed:
135
143
  error_msg = (
136
144
  f"scaling the attenuation by this factor carries {stretching} to "
137
145
  f"{float(xp.min(moved)):.6g}, at or below zero, which makes the memory "
@@ -143,7 +151,8 @@ def _first_order_attenuation(
143
151
  scaled[stretching] = moved
144
152
  for i_relax in range(n_relaxation_mechanisms):
145
153
  strength = f"d_{direction}_nu{i_relax + 1}"
146
- scaled[strength] = ratio * relaxation_param_dict[strength]
154
+ values = relaxation_param_dict[strength]
155
+ scaled[strength] = ne.evaluate("ratio * values") if on_the_host else ratio * values
147
156
  return scaled
148
157
 
149
158
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fullwave25
3
- Version: 1.3.0.dev0
3
+ Version: 1.3.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
@@ -13,6 +13,7 @@ Classifier: Programming Language :: Python :: 3.11
13
13
  Classifier: Programming Language :: Python :: 3.12
14
14
  Classifier: Topic :: Scientific/Engineering :: Physics
15
15
  Requires-Python: >=3.10
16
+ Requires-Dist: cupy-cuda12x>=13.6.0
16
17
  Requires-Dist: matplotlib>=3.10.7
17
18
  Requires-Dist: numba>=0.63.1
18
19
  Requires-Dist: numexpr>=2.14.1
@@ -1,10 +1,10 @@
1
- fullwave/__init__.py,sha256=W_rJYZITUpH7crE6e87RdQCJQBvsDXDEm4g8k6mNm8I,1861
1
+ fullwave/__init__.py,sha256=p-kSxLN7kRdorciAXD-u5Ffx4l8d6XHDpBuHRoRblPk,2382
2
2
  fullwave/grid.py,sha256=YbxAAgdYKR8AXwobtTZzHm9C4KR4D5--51zgbs0aQGY,7435
3
- fullwave/medium.py,sha256=vlGFkk-GZCuJGLfYRPi7Hc8VamI_uC4NQdxSCOFAhAA,67621
3
+ fullwave/medium.py,sha256=Tz8Cwda6zLF8UFxjWFkRcAdko_KmF4l1zfmjWaqxu-M,68626
4
4
  fullwave/sensor.py,sha256=McHyaSKxM11_Kfb_2hfnOtV9Z1UMBCLkeovncLT8_bA,9308
5
5
  fullwave/source.py,sha256=2mJ9djoc_PZAdGbkpJBTPqiF58rxe4YcyUIyxAtjAWA,21143
6
6
  fullwave/transducer.py,sha256=NL_GxX4ViYfICeY-mrV1MrkWao_sEhgZWCQ4Lvhpuao,75263
7
- fullwave/transmit.py,sha256=6TK0ow86PM4ivgiU9DmD9pjy6WLqOu6JoHt7CrYi07I,8907
7
+ fullwave/transmit.py,sha256=BAD_G-J7Q1pMppXw1yjLrQDUdkhPq-3Q4eNq4jaf7Ss,9693
8
8
  fullwave/beamformer/__init__.py,sha256=tNPq_5lfLXfAdueyfQtdTxLTVEV6nEDkeSGAeTf7Q5s,88
9
9
  fullwave/beamformer/beamformer.py,sha256=0jE_B5UlxDDBsUcQSHa5d1q3uSTSCyqgNiUJrU10juI,5601
10
10
  fullwave/constants/__init__.py,sha256=WOmkhLsSe86D71gwKsa4Tz8QPeOdayLGgvBCh7UEOPk,104
@@ -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=olqi2JGqLybQu8iThIo8D-KYtZaErk3clFpkT2LAKwg,5041
24
+ fullwave/solver/binary_manager.py,sha256=VhI6VgkdfIwAeX4RkM5S0oHm_RWoo3Ph4BsNlz1vETM,5041
25
25
  fullwave/solver/cuda_utils.py,sha256=n7p3TJYbLAXmIzSMS5Q_8m4OWg5U9VG5_3hAxa0uTYI,11407
26
- fullwave/solver/input_file_writer.py,sha256=Z-Ha-1ceuAcT0CJM5iElsUVBKedwDRgWiLIaA-0Sm4M,57249
26
+ fullwave/solver/input_file_writer.py,sha256=JamQHuA3-YrdLpdMFT6Ymw7noiCrtMr-DNo6LFxCvmM,57993
27
27
  fullwave/solver/launcher.py,sha256=zh-3HY4iKGmjreALu9WczDpWX3d74nOcIns9xVvnUIg,10531
28
- fullwave/solver/pml_builder.py,sha256=ClLInmYQSbhkno2-003dn-15V3cDy9x-F4NCMiBWlCA,84751
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=val60dmnrcOmqMdBBVZw7QEHT8wDMp4Q1aFJMbkPc5c,58774
30
+ fullwave/solver/solver.py,sha256=oQOrL-9YSi5C9vP6rOyfZW8soTHnWBLusi1rMIwI978,65346
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
@@ -49,11 +49,11 @@ fullwave/utils/memory_tempfile.py,sha256=BzMRNvINzLYdtV8NcSNjrTr2F9Qow8vYl1o7RUX
49
49
  fullwave/utils/numerical.py,sha256=khdi2KE3y_Ky9cEnmGXvVHjeYuKfb-8gQTV1a8nx9uA,3260
50
50
  fullwave/utils/plot_utils.py,sha256=SoByd2fEr3VdvMM9D2kNq8VgaAQ-fh82bDGRFdeq2R8,39327
51
51
  fullwave/utils/pulse.py,sha256=aqnCdBxivACouIGlot1aE2k4V9nctUxNCEPzdBpsupg,2811
52
- fullwave/utils/relaxation_parameters.py,sha256=uPN31h7YzgsxUBhHbEoxLapRplbpNITKJQPgbGoUtk4,45800
52
+ fullwave/utils/relaxation_parameters.py,sha256=wHUNMtJGAJwgCyUYd24_qtQZflMnkQTYhxSJwjE3vA4,46151
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.3.0.dev0.dist-info/METADATA,sha256=mgwujcZWq0A_pFpxRittcPsWh6GVO_xuKdTsQbqyXU0,21711
57
- fullwave25-1.3.0.dev0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
58
- fullwave25-1.3.0.dev0.dist-info/licenses/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
59
- fullwave25-1.3.0.dev0.dist-info/RECORD,,
56
+ fullwave25-1.3.2.dev0.dist-info/METADATA,sha256=wYeq9o1hmkqstMoW0B6zQrbOUQnQAIDBVbhkqGwHJKM,21747
57
+ fullwave25-1.3.2.dev0.dist-info/WHEEL,sha256=6zicbvgMWfHSD_sSydPAPL1UNY8-QHIQA4J8fOxyAc8,87
58
+ fullwave25-1.3.2.dev0.dist-info/licenses/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
59
+ fullwave25-1.3.2.dev0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.0
2
+ Generator: hatchling 1.32.1
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any