fullwave25 1.4.2.dev2__py3-none-any.whl → 1.4.2.dev3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
fullwave/__init__.py CHANGED
@@ -66,7 +66,7 @@ try:
66
66
  __version__ = version("fullwave")
67
67
  except PackageNotFoundError:
68
68
  # Update via bump-my-version, not manually
69
- __version__ = "1.4.2-dev2"
69
+ __version__ = "1.4.2-dev3"
70
70
 
71
71
  VERSION = __version__ # for convenience
72
72
  logger.info("Fullwave version: %s", __version__)
@@ -14,6 +14,13 @@ class Beamformer:
14
14
  For faster implementations, consider using libraries such as
15
15
  mach beamformer: https://github.com/Forest-Neurotech/mach
16
16
 
17
+ Conventions: ``axial_position_m`` is measured from the top of the grid
18
+ (row 0), not from the array. ``transducer_coordinates`` must be the
19
+ receiver positions in grid points (for example from
20
+ ``Transducer.element_id_to_receiver_center``). The time origin is the
21
+ mean receiver row, so the array depth is subtracted from the two-way
22
+ travel time.
23
+
17
24
  References
18
25
  ----------
19
26
  - Fullwave2 BMME890 implementation
@@ -46,11 +53,11 @@ class Beamformer:
46
53
  lateral_position_m : np.ndarray
47
54
  Lateral positions (m) where the beamformed image will be computed.
48
55
  axial_position_m : np.ndarray
49
- Axial positions (m) where the beamformed image will be computed.
56
+ Axial positions (m) from the top of the grid where the image is computed.
50
57
  num_elements : int
51
58
  Number of transducer elements.
52
59
  transducer_coordinates : np.ndarray
53
- Coordinates of the transducer elements (m).
60
+ Receiver coordinates [n_elements, 2] in grid points (row, column).
54
61
  f_number : float, optional
55
62
  F-number for aperture calculation, by default 1.0.
56
63
 
@@ -83,6 +90,8 @@ class Beamformer:
83
90
  if signals.ndim == 2:
84
91
  signals = signals[:, :, np.newaxis]
85
92
  n_transmit = signals.shape[2]
93
+ # idt_0 is the pulse arrival at the receivers, so time is measured from the array.
94
+ z_array = np.mean(self.transducer_coordinates[:, 0]) * self.dx
86
95
 
87
96
  hilbert_signals = hilbert(signals, axis=1)
88
97
  beamformed_image = np.zeros(
@@ -133,7 +142,7 @@ class Beamformer:
133
142
  dd = np.round(dd / (self.dt / self.dx * self.c0)).astype(int)
134
143
  dd = dd - dd.min()
135
144
 
136
- idt = idt_0 + int(2 * axial / self.c0 / self.dt)
145
+ idt = idt_0 + int(2 * (axial - z_array) / self.c0 / self.dt)
137
146
 
138
147
  # idp: 1D indices into signals flattened array
139
148
  # we convert 2D indices to 1D indices
fullwave/transducer.py CHANGED
@@ -352,6 +352,9 @@ class TransducerGeometry:
352
352
  ) -> tuple[NDArray[np.int64], NDArray[np.int64], NDArray[np.int64], NDArray[np.int64]]:
353
353
  """Build flat coordinate arrays for source and sensor pixels.
354
354
 
355
+ For a flat array the sensor row is the first row in front of the element
356
+ (``position_px[0] + element_layer_px``), outside the source cells.
357
+
355
358
  Returns
356
359
  -------
357
360
  source_coords : NDArray[np.int64], shape [N_src, ndim]
@@ -404,7 +407,7 @@ class TransducerGeometry:
404
407
  source_ids_parts.append(np.full(len(src), element_index + 1))
405
408
 
406
409
  # Single center pixel for sensor output
407
- sx = element_pos_x + self.element_layer_px - 1
410
+ sx = self._flat_sensor_row(element_pos_x=element_pos_x)
408
411
  sy = element_pos_y + self.element_width_px - 1
409
412
  sz = element_pos_z + self.element_height_px // 2 - 1
410
413
  sensor_coords_parts.append(np.array([[sx, sy, sz]]))
@@ -432,7 +435,7 @@ class TransducerGeometry:
432
435
  source_coords_parts.append(src)
433
436
  source_ids_parts.append(np.full(len(src), element_index + 1))
434
437
 
435
- sx = element_pos_x + self.element_layer_px - 1
438
+ sx = self._flat_sensor_row(element_pos_x=element_pos_x)
436
439
  if self.average_surface_signals:
437
440
  out_ys = np.arange(element_pos_y, element_pos_y + self.element_width_px)
438
441
  out_xs = np.full(len(out_ys), sx)
@@ -452,6 +455,39 @@ class TransducerGeometry:
452
455
  sensor_ids = np.concatenate(sensor_ids_parts, axis=0).astype(np.int64)
453
456
  return source_coords, source_ids, sensor_coords, sensor_ids
454
457
 
458
+ def _flat_sensor_row(self, *, element_pos_x: int) -> int:
459
+ """Return the sensor row of a flat-array element.
460
+
461
+ The row lies in front of the source cells because a hard source keeps a
462
+ static pressure after the drive, which a receiver inside it would record.
463
+
464
+ Parameters
465
+ ----------
466
+ element_pos_x : int
467
+ First (back) source row of the element.
468
+
469
+ Returns
470
+ -------
471
+ int
472
+ The sensor row, ``element_pos_x + element_layer_px``.
473
+
474
+ Raises
475
+ ------
476
+ ValueError
477
+ If the sensor row is outside the grid.
478
+
479
+ """
480
+ sensor_row = int(element_pos_x + self.element_layer_px)
481
+ if sensor_row >= self.stored_grid_size[0]:
482
+ error_msg = (
483
+ f"The sensor row {sensor_row} (one row in front of the element) is "
484
+ f"outside the grid, which has {self.stored_grid_size[0]} rows in x. "
485
+ "Move the transducer back or enlarge the grid."
486
+ )
487
+ logger.error(error_msg)
488
+ raise ValueError(error_msg)
489
+ return sensor_row
490
+
455
491
  def _arc_element_coords(
456
492
  self,
457
493
  ) -> tuple[NDArray[np.int64], NDArray[np.int64], NDArray[np.int64], NDArray[np.int64]]:
@@ -1728,6 +1764,18 @@ class Transducer:
1728
1764
  out[eid] = np.rint(coords[sel].mean(axis=0)).astype(np.int64)
1729
1765
  return out
1730
1766
 
1767
+ @property
1768
+ def element_id_to_receiver_center(self) -> dict[int, NDArray[np.int64]]:
1769
+ """Return the mapping from element IDs to the mean receiver coordinate in grid points."""
1770
+ ndim = 3 if self.is_3d else 2
1771
+ out: dict[int, NDArray[np.int64]] = {}
1772
+ for eid, coords in self.element_id_to_element_surface.items():
1773
+ if len(coords) == 0:
1774
+ out[eid] = np.full(ndim, -1, dtype=np.int64)
1775
+ else:
1776
+ out[eid] = np.rint(coords.mean(axis=0)).astype(np.int64)
1777
+ return out
1778
+
1731
1779
  @property
1732
1780
  def sensor(self) -> fullwave.sensor.Sensor:
1733
1781
  """Return the Sensor object with active sensor coordinates.
@@ -35,6 +35,44 @@ def _on_the_host(array: NDArray) -> NDArray:
35
35
  return _array_module(array).asnumpy(array)
36
36
 
37
37
 
38
+ # float32 stores 1.2 as 1.2000000477 (about 6e-8 relative); table nodes are 1e-2 or more apart.
39
+ _FLOAT32_NOISE_RTOL = 1e-6
40
+
41
+
42
+ def _snap_to_nearby_node(
43
+ *,
44
+ values: NDArray[np.float64],
45
+ nodes: NDArray[np.float64],
46
+ xp: object,
47
+ ) -> NDArray[np.float64]:
48
+ """Replace each value that is float32 noise away from a table node by that node.
49
+
50
+ Parameters
51
+ ----------
52
+ values : NDArray[np.float64]
53
+ Requested values.
54
+ nodes : NDArray[np.float64]
55
+ Sorted table nodes.
56
+ xp : object
57
+ Array module of ``values``, numpy or cupy.
58
+
59
+ Returns
60
+ -------
61
+ NDArray[np.float64]
62
+ The values, with near-node values replaced by the node.
63
+
64
+ """
65
+ if nodes.size < 2:
66
+ return values
67
+ values = xp.asarray(values)
68
+ upper_index = xp.clip(xp.searchsorted(nodes, values, side="left"), 1, nodes.size - 1)
69
+ lower_node = nodes[upper_index - 1]
70
+ upper_node = nodes[upper_index]
71
+ is_near_lower = xp.abs(values - lower_node) <= _FLOAT32_NOISE_RTOL * xp.abs(lower_node)
72
+ is_near_upper = xp.abs(values - upper_node) <= _FLOAT32_NOISE_RTOL * xp.abs(upper_node)
73
+ return xp.where(is_near_lower, lower_node, xp.where(is_near_upper, upper_node, values))
74
+
75
+
38
76
  def scale_relaxation_attenuation(
39
77
  relaxation_param_dict: dict[str, NDArray[np.float64]],
40
78
  factor: NDArray[np.float64] | float,
@@ -938,6 +976,8 @@ class RelaxationParametersGenerator:
938
976
 
939
977
  """
940
978
  alpha_coeff = band_scaled_alpha_coeff(alpha_coeff, alpha_power, band_scale)
979
+ alpha_coeff = self._snap_to_table(alpha_coeff, table_axis=self.alpha_list)
980
+ alpha_power = self._snap_to_table(alpha_power, table_axis=self.power_list)
941
981
  self._warn_invalid(alpha_coeff, alpha_power)
942
982
 
943
983
  use_gpu = not isinstance(alpha_coeff, np.ndarray)
@@ -991,6 +1031,7 @@ class RelaxationParametersGenerator:
991
1031
 
992
1032
  """
993
1033
  xp = np if isinstance(alpha_coeff, np.ndarray) else _array_module(alpha_coeff)
1034
+ alpha_coeff = self._snap_to_table(alpha_coeff, table_axis=self.alpha_list)
994
1035
  axis = xp.asarray(np.asarray(self.alpha_list).ravel().round(10))
995
1036
  return (
996
1037
  alpha_coeff / axis[self._axis_index(alpha_coeff, axis, self.alpha_min, self.alpha_max)]
@@ -1113,6 +1154,15 @@ class RelaxationParametersGenerator:
1113
1154
  ],
1114
1155
  )
1115
1156
 
1157
+ @staticmethod
1158
+ def _snap_to_table(
1159
+ values: NDArray[np.float64], *, table_axis: NDArray[np.float64]
1160
+ ) -> NDArray[np.float64]:
1161
+ """Replace each value that is float32 noise away from a node of one table axis."""
1162
+ xp = np if isinstance(values, np.ndarray) else _array_module(values)
1163
+ nodes = xp.asarray(np.asarray(table_axis).ravel().round(10))
1164
+ return _snap_to_nearby_node(values=values, nodes=nodes, xp=xp)
1165
+
1116
1166
  @staticmethod
1117
1167
  def _axis_index(
1118
1168
  value: NDArray[np.float64],
@@ -1122,6 +1172,7 @@ class RelaxationParametersGenerator:
1122
1172
  ) -> NDArray[np.int64]:
1123
1173
  """Return the axis index the lookup kernel selects, which clips at both ends."""
1124
1174
  xp = np if isinstance(value, np.ndarray) else _array_module(value)
1175
+ value = _snap_to_nearby_node(values=value, nodes=axis, xp=xp)
1125
1176
  clipped = xp.clip(value, lowest, highest)
1126
1177
  return xp.minimum(xp.searchsorted(axis, clipped, side="left"), axis.size - 1)
1127
1178
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: fullwave25
3
- Version: 1.4.2-dev2
3
+ Version: 1.4.2-dev3
4
4
  Summary: Fullwave 2.5: Ultrasound wave propagation simulation with heterogeneous power law attenuation modelling capabilities
5
5
  Author: Masashi Sode, Gianmarco Pinton
6
6
  Maintainer: Masashi Sode
@@ -1,12 +1,12 @@
1
- fullwave/__init__.py,sha256=kB7UDg9SHqBPUkMZSPknt2b4sEI6WArCltM-2ExrS1c,2382
1
+ fullwave/__init__.py,sha256=Lu4dG1mNq4CKaKD34LmhtJNlYn8m2gRXrw8qYqnAZKg,2382
2
2
  fullwave/grid.py,sha256=YbxAAgdYKR8AXwobtTZzHm9C4KR4D5--51zgbs0aQGY,7435
3
3
  fullwave/medium.py,sha256=qoAh-aYLVkC_ojt1K61hDNV0j9KfIeRkh35tZU8gSKM,68574
4
4
  fullwave/sensor.py,sha256=McHyaSKxM11_Kfb_2hfnOtV9Z1UMBCLkeovncLT8_bA,9308
5
5
  fullwave/source.py,sha256=5IHktJubOsk94qBWlLBbu-Vxo2UczVFL4EnEyKqEIUE,21321
6
- fullwave/transducer.py,sha256=WmaYwKOPSWLVnjDFZkpP1qlLs4IT27HLtB04vJhm0Ec,75778
6
+ fullwave/transducer.py,sha256=H67VF_T2KKF68mWtXG6DrVYKaaZieGc7p-IDtWxX1SQ,77623
7
7
  fullwave/transmit.py,sha256=BAD_G-J7Q1pMppXw1yjLrQDUdkhPq-3Q4eNq4jaf7Ss,9693
8
8
  fullwave/beamformer/__init__.py,sha256=tNPq_5lfLXfAdueyfQtdTxLTVEV6nEDkeSGAeTf7Q5s,88
9
- fullwave/beamformer/beamformer.py,sha256=0jE_B5UlxDDBsUcQSHa5d1q3uSTSCyqgNiUJrU10juI,5601
9
+ fullwave/beamformer/beamformer.py,sha256=7dlQVArUGpUeUP6O-9QyMpR6asbbI5JBVJ_F1fpSowU,6175
10
10
  fullwave/constants/__init__.py,sha256=WOmkhLsSe86D71gwKsa4Tz8QPeOdayLGgvBCh7UEOPk,104
11
11
  fullwave/constants/material_properties.py,sha256=SouMKD4ONzYl0VU8ZsC2I_85y-ZLj9scRNwAArCD7bg,2729
12
12
  fullwave/medium_builder/__init__.py,sha256=QVz0sA4niq51O94RnXUVlHutx7FYEIa1TRcR5fDc_Ps,201
@@ -64,11 +64,11 @@ fullwave/utils/memory_tempfile.py,sha256=RqoFX67VCMIHNIDYWwHLCaS4dwSPK6eKsvElXX4
64
64
  fullwave/utils/numerical.py,sha256=khdi2KE3y_Ky9cEnmGXvVHjeYuKfb-8gQTV1a8nx9uA,3260
65
65
  fullwave/utils/plot_utils.py,sha256=SoByd2fEr3VdvMM9D2kNq8VgaAQ-fh82bDGRFdeq2R8,39327
66
66
  fullwave/utils/pulse.py,sha256=aqnCdBxivACouIGlot1aE2k4V9nctUxNCEPzdBpsupg,2811
67
- fullwave/utils/relaxation_parameters.py,sha256=wHUNMtJGAJwgCyUYd24_qtQZflMnkQTYhxSJwjE3vA4,46151
67
+ fullwave/utils/relaxation_parameters.py,sha256=pNul3_8rrbBrjkcc0nKBJ6An9y3bI92DVxNu1Ww8cf4,48133
68
68
  fullwave/utils/scatterer.py,sha256=CLqD085f9pqX4cF1YiT2IkzT5P0o1xp45Do7lf5BLnM,24049
69
69
  fullwave/utils/signal_filter.py,sha256=_Tf42aBygGtIUrcSPybYqvZoAW2o_4dd-hHTEvZ8--E,5219
70
70
  fullwave/utils/signal_process.py,sha256=MP38r37JSPHN4sT-lIReXS5BT2GT_tkRxD0Y1wQ-lNs,5153
71
- fullwave25-1.4.2.dev2.dist-info/METADATA,sha256=D3sDh1xucymIxtFEhw1cwpI6XbBEFBceYSD5qE9a-i4,28226
72
- fullwave25-1.4.2.dev2.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
73
- fullwave25-1.4.2.dev2.dist-info/licenses/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
74
- fullwave25-1.4.2.dev2.dist-info/RECORD,,
71
+ fullwave25-1.4.2.dev3.dist-info/METADATA,sha256=q0L2mArY9AQfnOaJKDYCLIke8U4wNM0w5TOm-EPxMIU,28226
72
+ fullwave25-1.4.2.dev3.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
73
+ fullwave25-1.4.2.dev3.dist-info/licenses/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
74
+ fullwave25-1.4.2.dev3.dist-info/RECORD,,