FastSIMUS 0.0.1__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.
@@ -0,0 +1,160 @@
1
+ """Transducer parameter definitions for FastSIMUS."""
2
+
3
+ from enum import StrEnum
4
+ from math import inf
5
+ from typing import Self
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, NonNegativeFloat, model_validator
8
+
9
+
10
+ class BaffleType(StrEnum):
11
+ """Baffle type enumeration for transducer acoustic boundary conditions.
12
+
13
+ The baffle property affects the obliquity factor in the directivity of elements.
14
+
15
+ Attributes:
16
+ SOFT: Soft baffle (pressure-release boundary), obliquity factor = cos(theta)
17
+ RIGID: Rigid baffle (pressure-doubling boundary), obliquity factor = 1
18
+ """
19
+
20
+ SOFT = "soft"
21
+ RIGID = "rigid"
22
+
23
+
24
+ class TransducerParams(BaseModel):
25
+ """Transducer parameters for ultrasound simulation.
26
+
27
+ This class encapsulates all physical and geometric parameters needed for
28
+ ultrasound transducer simulation, following SIMUS/MUST conventions.
29
+
30
+ Examples:
31
+ >>> # Linear array with element width
32
+ >>> params = TransducerParams(
33
+ ... freq_center=2.5e6,
34
+ ... pitch=300e-6,
35
+ ... n_elements=128,
36
+ ... width=250e-6
37
+ ... )
38
+ >>> params.element_width
39
+ 0.00025
40
+ >>> params.kerf_width
41
+ 5e-05
42
+ >>>
43
+ >>> # Convex array with kerf
44
+ >>> params = TransducerParams(
45
+ ... freq_center=3.5e6,
46
+ ... pitch=400e-6,
47
+ ... n_elements=64,
48
+ ... kerf=50e-6,
49
+ ... radius=50e-3
50
+ ... )
51
+ >>> params.element_width
52
+ 0.00035
53
+ """
54
+
55
+ model_config = ConfigDict(use_attribute_docstrings=True, frozen=True)
56
+
57
+ # === Required Fields ===
58
+ freq_center: float = Field(..., gt=0)
59
+ """Center frequency in Hz. Must be positive."""
60
+
61
+ pitch: float = Field(..., gt=0)
62
+ """Element pitch (center-to-center spacing) in meters. Must be positive."""
63
+
64
+ n_elements: int = Field(..., gt=0)
65
+ """Number of transducer elements. Must be positive integer."""
66
+
67
+ # === Mutually Exclusive: Width or Kerf ===
68
+ width: float | None = Field(None, gt=0)
69
+ """Element width in meters. Mutually exclusive with kerf. Must be positive if provided."""
70
+
71
+ kerf: float | None = Field(None, ge=0)
72
+ """Kerf width (gap between elements) in meters. Mutually exclusive with width. Must be non-negative if provided."""
73
+
74
+ # === Optional Geometric Fields ===
75
+ height: float = Field(default=inf, gt=0)
76
+ """Element height in meters. Defaults to infinity for 2D simulation."""
77
+
78
+ elev_focus: float = Field(default=inf, gt=0)
79
+ """Elevation focus distance in meters. Defaults to infinity (unfocused)."""
80
+
81
+ radius: float = Field(default=inf, gt=0)
82
+ """Curvature radius in meters for convex arrays. Defaults to infinity (linear array)."""
83
+
84
+ # === Optional Acoustic Fields ===
85
+ bandwidth: float = Field(default=0.75, gt=0, le=2.0)
86
+ """Fractional bandwidth (0.75 = 75%). Must be in (0, 2.0]. Defaults to 0.75."""
87
+
88
+ baffle: BaffleType | NonNegativeFloat = Field(default=BaffleType.SOFT)
89
+ """Baffle type or acoustic impedance ratio.
90
+
91
+ Input: "soft", "rigid" (case-insensitive), or impedance ratio (float >= 0).
92
+ After validation, becomes BaffleType.SOFT, BaffleType.RIGID, or float.
93
+ Defaults to "soft".
94
+ """
95
+
96
+ # === Computed properties ===
97
+ @property
98
+ def element_width(self) -> float:
99
+ """Element width in meters.
100
+
101
+ Computed from width (if provided) or pitch - kerf (if kerf provided).
102
+
103
+ Returns:
104
+ Element width in meters.
105
+
106
+ Raises:
107
+ ValueError: If neither width nor kerf is provided.
108
+ """
109
+ if self.width is not None:
110
+ return self.width
111
+ if self.kerf is not None:
112
+ return self.pitch - self.kerf
113
+ msg = "Either width or kerf must be provided"
114
+ raise ValueError(msg)
115
+
116
+ @property
117
+ def kerf_width(self) -> float:
118
+ """Kerf width in meters.
119
+
120
+ Computed from kerf (if provided) or pitch - width (if width provided).
121
+
122
+ Returns:
123
+ Kerf width in meters.
124
+
125
+ Raises:
126
+ ValueError: If neither width nor kerf is provided.
127
+ """
128
+ if self.kerf is not None:
129
+ return self.kerf
130
+ if self.width is not None:
131
+ return self.pitch - self.width
132
+ msg = "Either width or kerf must be provided"
133
+ raise ValueError(msg)
134
+
135
+ @model_validator(mode="after")
136
+ def validate_width_kerf(self) -> Self:
137
+ """Validate that exactly one of width or kerf is provided and physically valid.
138
+
139
+ Returns:
140
+ Self for method chaining.
141
+
142
+ Raises:
143
+ ValueError: If validation fails.
144
+ """
145
+ if self.width is None and self.kerf is None:
146
+ msg = "Either width or kerf must be provided"
147
+ raise ValueError(msg)
148
+ if self.width is not None and self.kerf is not None:
149
+ msg = "Cannot specify both width and kerf. Provide only one."
150
+ raise ValueError(msg)
151
+
152
+ # Validate physical constraints
153
+ if self.width is not None and self.width > self.pitch:
154
+ msg = f"Element width ({self.width}) cannot exceed pitch ({self.pitch})"
155
+ raise ValueError(msg)
156
+ if self.kerf is not None and self.kerf >= self.pitch:
157
+ msg = f"Kerf ({self.kerf}) must be less than pitch ({self.pitch})"
158
+ raise ValueError(msg)
159
+
160
+ return self
@@ -0,0 +1,102 @@
1
+ """Preset transducer configurations for common ultrasound probes.
2
+
3
+ These presets are based on Verasonics transducers and match the values
4
+ from the MUST toolbox (getparam.m).
5
+ """
6
+
7
+ from math import inf
8
+
9
+ from fast_simus import TransducerParams
10
+
11
+
12
+ def P4_2v() -> TransducerParams:
13
+ """Create P4-2v phased array transducer preset.
14
+
15
+ Verasonics P4-2v phased array transducer for cardiac imaging:
16
+ - 2.72 MHz center frequency
17
+ - 64 elements
18
+ - 300 µm (0.3 mm) pitch
19
+
20
+ Returns:
21
+ TransducerParams instance configured for P4-2v transducer.
22
+ """
23
+ return TransducerParams(
24
+ freq_center=2.72e6, # 2.72 MHz
25
+ pitch=300e-6, # 300 µm
26
+ n_elements=64,
27
+ kerf=50e-6, # 50 µm
28
+ bandwidth=0.74, # 74%
29
+ radius=inf,
30
+ height=14e-3, # 14 mm
31
+ elev_focus=60e-3, # 60 mm
32
+ )
33
+
34
+
35
+ def L11_5v() -> TransducerParams:
36
+ """Create L11-5v linear array transducer preset.
37
+
38
+ Verasonics L11-5v high-frequency linear array for vascular imaging:
39
+ - 7.6 MHz center frequency
40
+ - 128 elements
41
+ - 300 µm (0.3 mm) pitch
42
+
43
+ Returns:
44
+ TransducerParams instance configured for L11-5v transducer.
45
+ """
46
+ return TransducerParams(
47
+ freq_center=7.6e6, # 7.6 MHz
48
+ pitch=300e-6, # 300 µm
49
+ n_elements=128,
50
+ kerf=30e-6, # 30 µm
51
+ bandwidth=0.77, # 77%
52
+ radius=inf,
53
+ height=5e-3, # 5 mm
54
+ elev_focus=18e-3, # 18 mm
55
+ )
56
+
57
+
58
+ def L12_3v() -> TransducerParams:
59
+ """Create L12-3v linear array transducer preset.
60
+
61
+ Verasonics L12-3v high-frequency linear array:
62
+ - 7.54 MHz center frequency
63
+ - 192 elements
64
+ - 200 µm (0.2 mm) pitch
65
+
66
+ Returns:
67
+ TransducerParams instance configured for L12-3v transducer.
68
+ """
69
+ return TransducerParams(
70
+ freq_center=7.54e6, # 7.54 MHz
71
+ pitch=200e-6, # 200 µm
72
+ n_elements=192,
73
+ kerf=30e-6, # 30 µm
74
+ bandwidth=0.93, # 93%
75
+ radius=inf,
76
+ height=5e-3, # 5 mm
77
+ elev_focus=20e-3, # 20 mm
78
+ )
79
+
80
+
81
+ def C5_2v() -> TransducerParams:
82
+ """Create C5-2v convex array transducer preset.
83
+
84
+ Verasonics C5-2v convex array for abdominal imaging:
85
+ - 3.57 MHz center frequency
86
+ - 128 elements
87
+ - 508 µm (0.508 mm) pitch
88
+ - 49.57 mm radius of curvature
89
+
90
+ Returns:
91
+ TransducerParams instance configured for C5-2v transducer.
92
+ """
93
+ return TransducerParams(
94
+ freq_center=3.57e6, # 3.57 MHz
95
+ pitch=508e-6, # 508 µm
96
+ n_elements=128,
97
+ kerf=48e-6, # 48 µm
98
+ bandwidth=0.79, # 79%
99
+ radius=49.57e-3, # 49.57 mm
100
+ height=13.5e-3, # 13.5 mm
101
+ elev_focus=60e-3, # 60 mm
102
+ )
fast_simus/tx_delay.py ADDED
@@ -0,0 +1,276 @@
1
+ """Transmit delay calculations for ultrasound transducer arrays.
2
+
3
+ This module provides functions to compute transmit time delays for different
4
+ beam patterns (focused/diverging, plane wave, circular wave) with linear and
5
+ convex arrays.
6
+
7
+ All functions are Array API compliant and work with NumPy, JAX, CuPy backends.
8
+
9
+ Coordinate System Convention:
10
+ - x-axis: Lateral (perpendicular to beam direction), in meters
11
+ - z-axis: Axial (along beam direction, positive = into tissue), in meters
12
+ - Origin: Center of transducer face
13
+
14
+ Sign Convention:
15
+ - Positive z: Points into the imaging medium (tissue)
16
+ - Delays are relative to minimum (all non-negative)
17
+ - For focused beams: positive z0 = focus in front, negative z0 = diverging
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ from math import cos, fmod, inf, pi, sin, sqrt, tan
23
+
24
+ from array_api_compat import array_namespace
25
+ from beartype import beartype as typechecker
26
+ from jaxtyping import Float, jaxtyped
27
+
28
+ from fast_simus.utils._array_api import Array
29
+
30
+
31
+ @jaxtyped(typechecker=typechecker)
32
+ def focused(
33
+ element_positions: Float[Array, "n_elements dim"],
34
+ focus: Float[Array, " dim"],
35
+ *,
36
+ speed_of_sound: float,
37
+ radius: float = inf,
38
+ apex_offset: float = 0.0,
39
+ ) -> Float[Array, " n_elements"]:
40
+ """Compute transmit time delays for focused or diverging spherical waves.
41
+
42
+ Spherical waves propagate like a collapsing sphere focusing onto a point
43
+ (positive z), or an expanding sphere diverging from a virtual source
44
+ (negative z).
45
+
46
+ Args:
47
+ element_positions:
48
+ Element positions in meters. Shape (n_elements, dim).
49
+ For 2D: (x, z) where x is lateral, z is axial.
50
+ For 3D: (x, y, z) where z is axial (depth).
51
+ focus:
52
+ Focal position in meters. Shape (dim,).
53
+ Coordinates match element_positions dimensions.
54
+ - Last coordinate (z): Axial position
55
+ - Positive z: Focused wave (focus in front of transducer)
56
+ - Negative z: Diverging wave (virtual source behind transducer)
57
+ - z=0: Treated as focusing just in front (negative delays)
58
+ speed_of_sound:
59
+ Speed of sound in m/s.
60
+ radius:
61
+ Curvature radius in meters. Use inf for linear arrays.
62
+ Convex arrays require dim=2 (runtime check).
63
+ apex_offset:
64
+ Distance from array center to arc apex in meters. Zero for linear arrays.
65
+ Defaults to 0.0.
66
+
67
+ Returns:
68
+ Transmit time delays in seconds. Shape (n_elements,).
69
+ Delays are relative to minimum (all non-negative).
70
+
71
+ Notes:
72
+ Implementation matches PyMUST reference with identical sign conventions.
73
+
74
+ For linear arrays, sign is based on z-coordinate of focus.
75
+ For convex arrays, sign is based on whether focus is inside or outside
76
+ the arc (comparing squared distance to squared radius).
77
+
78
+ Reference:
79
+ Perrot et al. 2021, "So you think you can DAS?"
80
+ https://www.biomecardio.com/publis/ultrasonics21.pdf
81
+ (Equation 5, extended to support virtual sources)
82
+ """
83
+ xp = array_namespace(element_positions, focus)
84
+
85
+ # Compute distance from each element to focus
86
+ # element_positions: (n_elements, dim), focus: (dim,)
87
+ # Broadcasting: (n_elements, dim) - (dim,) -> (n_elements, dim)
88
+ diff = element_positions - focus
89
+ distances = xp.sqrt(xp.sum(diff**2, axis=-1)) # (n_elements,)
90
+
91
+ if radius == inf:
92
+ # Linear array: sign based on axial position (last coordinate)
93
+ z_focus = focus[-1]
94
+ sgn = xp.sign(z_focus)
95
+ sgn = xp.where(sgn == 0, -1, sgn)
96
+ delays = -distances * sgn / speed_of_sound
97
+ else:
98
+ # Convex array: requires 2D coordinates
99
+ if focus.shape[0] != 2:
100
+ msg = f"Convex arrays require 2D coordinates, got dim={focus.shape[0]}"
101
+ raise ValueError(msg)
102
+
103
+ # Sign based on inside/outside arc
104
+ x_focus = focus[0]
105
+ z_focus = focus[-1]
106
+ sgn = xp.sign(x_focus**2 + (z_focus + apex_offset) ** 2 - radius**2)
107
+ sgn = xp.where(sgn == 0, -1, sgn)
108
+ delays = -distances * sgn / speed_of_sound
109
+
110
+ # Make all delays non-negative by subtracting minimum
111
+ delays = delays - xp.min(delays)
112
+
113
+ return delays
114
+
115
+
116
+ @jaxtyped(typechecker=typechecker)
117
+ def plane_wave(
118
+ element_positions: Float[Array, "n_elements xz=2"],
119
+ tilt_rad: float,
120
+ *,
121
+ speed_of_sound: float,
122
+ radius: float = inf,
123
+ apex_offset: float = 0.0,
124
+ ) -> Float[Array, " n_elements"]:
125
+ """Compute transmit time delays for plane wave transmission.
126
+
127
+ Plane waves have a flat wavefront propagating in a specified direction,
128
+ defined by the tilt angle from the array normal (z-axis).
129
+
130
+ Args:
131
+ element_positions:
132
+ Element (x, z) positions in meters. Shape (n_elements, 2).
133
+ - element_positions[:, 0]: Lateral positions (x)
134
+ - element_positions[:, 1]: Axial positions (z)
135
+ tilt_rad:
136
+ Tilt angle in radians.
137
+ - tilt_rad=0: Straight ahead (perpendicular to array)
138
+ - Positive tilt_rad: Beam steers right (positive x direction)
139
+ - Negative tilt_rad: Beam steers left (negative x direction)
140
+ Must satisfy |tilt_rad| < π/2.
141
+ speed_of_sound:
142
+ Speed of sound in m/s.
143
+ radius:
144
+ Curvature radius in meters. Use inf for linear arrays.
145
+ apex_offset:
146
+ Distance from array center to arc apex in meters. Zero for linear arrays.
147
+ Defaults to 0.0.
148
+
149
+ Returns:
150
+ Transmit time delays in seconds. Shape (n_elements,).
151
+ Delays are relative to minimum (all non-negative).
152
+
153
+ Raises:
154
+ ValueError: If |tilt_rad| >= π/2 (non-physical angle).
155
+
156
+ Notes:
157
+ For linear arrays, delays follow simple sinusoidal pattern based on
158
+ lateral element position.
159
+
160
+ For convex arrays, the computation accounts for the curved geometry
161
+ by computing distance to the plane perpendicular to the tilt direction.
162
+ """
163
+ # Validate tilt angle
164
+ if abs(tilt_rad) >= pi / 2:
165
+ msg = "Tilt angles must satisfy |tilt_rad| < π/2"
166
+ raise ValueError(msg)
167
+
168
+ xp = array_namespace(element_positions)
169
+
170
+ # Extract element positions
171
+ x_elem = element_positions[:, 0] # Shape (n_elements,)
172
+ z_elem = element_positions[:, 1] # Shape (n_elements,)
173
+
174
+ if radius == inf:
175
+ # Linear array: delay proportional to lateral position
176
+ delays = x_elem * sin(tilt_rad) / speed_of_sound
177
+ else:
178
+ # Convex array: geometric calculation
179
+ xn = radius * sin(tilt_rad)
180
+ zn = radius * cos(tilt_rad) - apex_offset
181
+ numerator = xp.abs(z_elem + xn / (zn + apex_offset) * x_elem - xn**2 / (zn + apex_offset) - zn)
182
+ denominator = sqrt(1 + xn**2 / (zn + apex_offset) ** 2)
183
+ d = numerator / denominator
184
+ delays = -d / speed_of_sound
185
+
186
+ # Make all delays non-negative
187
+ delays = delays - xp.min(delays)
188
+
189
+ return delays
190
+
191
+
192
+ @jaxtyped(typechecker=typechecker)
193
+ def diverging_wave(
194
+ element_positions: Float[Array, "n_elements xz=2"],
195
+ tilt_rad: float,
196
+ width_rad: float,
197
+ *,
198
+ aperture_length: float,
199
+ speed_of_sound: float,
200
+ ) -> Float[Array, " n_elements"]:
201
+ """Compute transmit time delays for diverging circular wave transmission.
202
+
203
+ Circular waves originate from a virtual point source positioned such that
204
+ the wavefront spans a specified angular width across the array.
205
+ Only supported for linear arrays.
206
+
207
+ Args:
208
+ element_positions:
209
+ Element (x, z) positions in meters. Shape (n_elements, 2).
210
+ - element_positions[:, 0]: Lateral positions (x)
211
+ - element_positions[:, 1]: Axial positions (z)
212
+ tilt_rad:
213
+ Tilt angle of the wave center in radians.
214
+ width_rad:
215
+ Angular width of the wave in radians.
216
+ Must satisfy 0 < width_rad < π.
217
+ aperture_length:
218
+ Array aperture length in meters (typically (n_elements - 1) * pitch).
219
+ speed_of_sound:
220
+ Speed of sound in m/s.
221
+
222
+ Returns:
223
+ Transmit time delays in seconds. Shape (n_elements,).
224
+ Delays are relative to minimum (all non-negative).
225
+
226
+ Raises:
227
+ ValueError: If width_rad is outside (0, π).
228
+
229
+ Notes:
230
+ The virtual source position is computed from the tilt and width angles
231
+ using geometric constraints. This creates a diverging spherical wave
232
+ that appears to originate from behind the transducer.
233
+
234
+ Only supported for linear arrays (not convex arrays).
235
+ """
236
+ if width_rad <= 0 or width_rad >= pi:
237
+ msg = "Width angles must satisfy 0 < width_rad < π"
238
+ raise ValueError(msg)
239
+
240
+ x0, z0 = _angles_to_virtual_source(aperture_length, tilt_rad, width_rad)
241
+
242
+ xp = array_namespace(element_positions)
243
+ focus = xp.asarray([x0, z0])
244
+
245
+ # Delegate to focused() for delay computation
246
+ return focused(element_positions, focus, speed_of_sound=speed_of_sound)
247
+
248
+
249
+ def _angles_to_virtual_source(
250
+ aperture_length: float,
251
+ tilt_rad: float,
252
+ width_rad: float,
253
+ ) -> tuple[float, float]:
254
+ """Convert tilt and width angles to virtual source position.
255
+
256
+ Args:
257
+ aperture_length: Array aperture length in meters.
258
+ tilt_rad: Tilt angle in radians.
259
+ width_rad: Angular width in radians.
260
+
261
+ Returns:
262
+ Tuple of (x0_m, z0_m) virtual source position in meters.
263
+ """
264
+ # Normalize tilt to [-π/2, π/2]
265
+ tilt_norm = fmod(-tilt_rad + pi / 2, 2 * pi) - pi / 2
266
+ sign_correction = 1.0
267
+
268
+ if abs(tilt_norm) > pi / 2:
269
+ tilt_norm = pi - tilt_norm
270
+ sign_correction = -1.0
271
+
272
+ denominator = tan(tilt_norm - width_rad / 2) - tan(tilt_norm + width_rad / 2)
273
+ z0 = sign_correction * aperture_length / denominator
274
+ x0 = sign_correction * z0 * tan(width_rad / 2 - tilt_norm) + aperture_length / 2
275
+
276
+ return x0, z0
@@ -0,0 +1,5 @@
1
+ """FastSIMUS utilities."""
2
+
3
+ from fast_simus.utils.geometry import element_positions
4
+
5
+ __all__ = ["element_positions"]