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.
- fast_simus/__init__.py +33 -0
- fast_simus/_pfield_math.py +261 -0
- fast_simus/_pfield_strategies.py +203 -0
- fast_simus/_simus_strategies.py +210 -0
- fast_simus/backends/__init__.py +1 -0
- fast_simus/backends/mlx.py +101 -0
- fast_simus/kernels/__init__.py +9 -0
- fast_simus/kernels/cuda_simus.py +321 -0
- fast_simus/kernels/metal_pfield.py +219 -0
- fast_simus/kernels/metal_simus.py +377 -0
- fast_simus/kernels/pfield.metal +97 -0
- fast_simus/kernels/simus_fused.cu +332 -0
- fast_simus/kernels/simus_rx_simd.metal +128 -0
- fast_simus/kernels/simus_tx_tiled.metal +175 -0
- fast_simus/medium_params.py +22 -0
- fast_simus/pfield.py +475 -0
- fast_simus/py.typed +0 -0
- fast_simus/simus.py +567 -0
- fast_simus/spectrum.py +107 -0
- fast_simus/transducer_params.py +160 -0
- fast_simus/transducer_presets.py +102 -0
- fast_simus/tx_delay.py +276 -0
- fast_simus/utils/__init__.py +5 -0
- fast_simus/utils/_array_api.py +294 -0
- fast_simus/utils/geometry.py +88 -0
- fastsimus-0.0.1.dist-info/METADATA +594 -0
- fastsimus-0.0.1.dist-info/RECORD +28 -0
- fastsimus-0.0.1.dist-info/WHEEL +4 -0
|
@@ -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
|