Pulse shapes¶
An acoustic pulse in jbubble is a callable p_ac(t) returning the driving pressure in Pascals at time t. Every pulse is an Equinox module (a JAX PyTree), so pulses are JIT-compilable and differentiable.
ToneBurst¶
The standard ultrasound pulse: a carrier waveform gated by a finite-duration envelope.
from jbubble.pulse import ToneBurst
from jbubble.pulse.shapes import Sine
pulse = ToneBurst(
freq=1e6, # centre frequency [Hz]
pressure=100e3, # peak pressure amplitude [Pa]
shape=Sine(), # carrier waveform
cycle_num=5, # number of complete cycles
# Optional:
# phase=0.0 # initial carrier phase [rad]
# envelope=SoftRectangularEnvelope()
)
Carrier shapes¶
The shape argument selects the carrier waveform within each cycle. All shapes are normalised to peak amplitude 1.
| Shape class | Description |
|---|---|
Sine |
Standard sine wave (default) |
Square |
Fourier-series square wave (10 terms) |
Sawtooth |
Rising sawtooth |
InvertedSawtooth |
Falling sawtooth |
Triangle |
Triangular wave |
Quadratic |
Parabolic carrier |
NegativeQuadratic |
Inverted parabolic carrier |
Rectangular(duty) |
General duty-cycle rectangular waveform |
TimeDomainSquare(sharpness) |
tanh-smoothed square, avoids Gibbs ringing |
TimeDomainSawtooth |
arctan-based smooth sawtooth |
TimeDomainTriangle |
arcsin-based smooth triangle |
from jbubble.pulse.shapes import Square, Rectangular
import jax.numpy as jnp
pulse = ToneBurst(freq=1e6, pressure=200e3, shape=Square())
# Monopolar rectangular pulse (99% duty)
pulse = ToneBurst(
freq=1e6, pressure=200e3,
shape=Rectangular(duty=0.01, high_level=0.0, low_level=-1.0,
phase_offset=1.98 * jnp.pi),
)
Envelopes¶
The envelope gates the carrier over the active window. The default for ToneBurst is SoftRectangularEnvelope.
| Envelope class | Description |
|---|---|
RectangularEnvelope |
Hard on/off step. Avoid for fitting — non-differentiable edges. |
SoftRectangularEnvelope |
Sigmoid on and off transitions. \(C^\infty\), near-rectangular. Preferred. |
HannEnvelope |
Cosine-squared window. Smooth, tapered edges. |
TukeyEnvelope(alpha) |
Hann-tapered at both ends, flat in the middle. alpha controls taper fraction. |
from jbubble.pulse import ToneBurst, HannEnvelope, TukeyEnvelope
from jbubble.pulse.shapes import Sine
# Hann-windowed tone burst
pulse = ToneBurst(freq=1e6, pressure=100e3, shape=Sine(), cycle_num=5,
envelope=HannEnvelope())
# 10% taper on each side, flat in the middle
pulse = ToneBurst(freq=1e6, pressure=100e3, shape=Sine(), cycle_num=20,
envelope=TukeyEnvelope(alpha=0.1))
ChirpPulse¶
A frequency-swept pulse. Useful for broadband excitation and some therapeutic protocols.
from jbubble.pulse import ChirpPulse
pulse = ChirpPulse(
freq_start=0.5e6, # start frequency [Hz]
freq_end=2.0e6, # end frequency [Hz]
pressure=100e3, # peak amplitude [Pa]
duration=20e-6, # pulse duration [s]
# mode="linear" or "exponential"
)
SampledPulse¶
Wraps a discrete pressure waveform, interpolating at query times. Useful when the driving waveform is measured experimentally.
import jax.numpy as jnp
from jbubble.pulse import SampledPulse
ts = jnp.linspace(0, 10e-6, 1000) # time axis [s]
ps = measured_waveform # pressure values [Pa], shape (1000,)
pulse = SampledPulse(ts=ts, ps=ps)
The interpolation is performed with jnp.interp (linear, clamped to boundary values outside the range), so it is differentiable through the sampled pressures.
NeuralPulse¶
A pulse parameterised by a small neural network. Useful for learned or optimised driving waveforms.
import equinox as eqx
from jbubble.pulse import NeuralPulse
net = eqx.nn.MLP(1, 1, width_size=32, depth=3, key=jax.random.key(0))
pulse = NeuralPulse(net=net)
The network receives the scalar time t and returns the driving pressure. Because it is an Equinox module, the network weights are differentiable and can be optimised via fit_parameters.
Composing pulses¶
Superposition¶
from jbubble.pulse import ToneBurst, Summed
from jbubble.pulse.shapes import Sine
# Dual-frequency driving
p1 = ToneBurst(freq=1e6, pressure=80e3, shape=Sine(), cycle_num=10)
p2 = ToneBurst(freq=2e6, pressure=40e3, shape=Sine(), cycle_num=20)
pulse = Summed(p1, p2)
Amplitude scaling¶
Time offset¶
Applying an envelope to any pulse¶
Every Pulse has a .windowed(envelope) method:
from jbubble.pulse import SoftRectangularEnvelope
windowed = sampled_pulse.windowed(SoftRectangularEnvelope())
Evaluating a pulse¶
All pulses share the same interface:
import jax.numpy as jnp
t = jnp.linspace(0, 10e-6, 1000)
p = jax.vmap(pulse)(t) # shape (1000,) [Pa]
Or simply call at a scalar time: