Skip to content

bubble

The jbubble.bubble subpackage contains all bubble physics components: state representation, the Property abstraction, and gas/shell/medium/EoM models.


State

BubbleState

BubbleState(
    R: Array,
    *,
    R_dot: Array = jnp.zeros(()),
    R0: Array = jnp.zeros(()),
    P_gas0: Array = jnp.zeros(()),
)

Bases: Module

Standard bubble state.

Parameters:

Name Type Description Default
R Array

Bubble wall radius [m].

required
R_dot Array

Bubble wall velocity [m/s]. Keyword-only. Default: 0.

jnp.zeros(())
R0 Array

Equilibrium bubble radius [m]. Frozen (\(\mathrm{d}R_0/\mathrm{d}t = 0\)) in the standard case; it becomes a slow state variable for processes such as rectified diffusion. Keyword-only. Default: 0.

jnp.zeros(())
P_gas0 Array

Equilibrium gas pressure [Pa]. Frozen in the standard case. Keyword-only. Default: 0.

jnp.zeros(())
Notes

A zero R0 or P_gas0 means "unset": solve_eom fills it from the equation of motion, so BubbleState(R=1.2 * R0) starts at rest at 1.2 times the equilibrium radius. EquationOfMotion.initial_state builds the same state with R= and R_dot=. For an empty cavity, set P_gas0 to a tiny positive value, such as 1e-12, because zero means "unset".


Properties

The Property abstraction is the key extensibility mechanism in jbubble. Any physical parameter that could depend on the bubble state is represented as a Property — a callable state → scalar. Plain floats are automatically promoted via as_property.

Property

Property()

Bases: Module, ABC

Abstract base for state-dependent (or constant) bubble properties.

Any callable eqx.Module that maps a BubbleState to a scalar array inherits from this class. The concrete subclass ConstantProperty handles the common case of a fixed value; state-dependent models, such as MarmottantSurfaceTension, override __call__ directly.

ConstantProperty

ConstantProperty(val: ArrayLike)

Bases: Property

A property that returns a constant value regardless of state.

Parameters:

Name Type Description Default
val float or Array

The constant value. It can be a JAX array, including a traced value inside jax.grad or jax.jit, so gradients flow through it.

required

NeuralProperty

NeuralProperty(net: Module)

Bases: Property

A Property backed by an Equinox neural network.

The network receives a normalised 1-D input [R / R0] and must return a 1-D output of shape (1,). You are responsible for any output transform that the physics needs. For example, wrap the final layer output with jax.nn.softplus or jnp.exp to keep a surface tension positive.

Parameters:

Name Type Description Default
net Module

Any callable Equinox module with signature (x: Array[1]) -> Array[1]. eqx.nn.MLP is the natural choice.

required

Examples:

>>> import equinox as eqx
>>> import jax
>>> import jax.numpy as jnp
>>> key = jax.random.PRNGKey(0)
>>> mlp = eqx.nn.MLP(in_size=1, out_size=1, width_size=32, depth=3,
...                  final_activation=jax.nn.softplus, key=key)
>>> sigma = NeuralProperty(net=mlp)

as_property

as_property(val: ArrayLike | Property) -> Property

Convert a plain scalar or JAX array to a ConstantProperty, or pass it through.

Parameters:

Name Type Description Default
val float, jax.Array, or Property

A plain scalar, a JAX array (possibly a tracer), or an existing Property instance.

required

Returns:

Type Description
Property

val itself if it's already a Property; otherwise ConstantProperty(val=val).

Source code in jbubble/bubble/property.py
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
def as_property(val: ArrayLike | Property) -> Property:
    """Convert a plain scalar or JAX array to a `ConstantProperty`, or pass it through.

    Parameters
    ----------
    val : float, jax.Array, or Property
        A plain scalar, a JAX array (possibly a tracer), or an existing
        [`Property`][jbubble.bubble.property.Property] instance.

    Returns
    -------
    Property
        `val` itself if it's already a `Property`; otherwise
        `ConstantProperty(val=val)`.
    """
    if isinstance(val, Property):
        return val
    return ConstantProperty(val=val)

Gas models

GasModel

GasModel()

Bases: Module, ABC

Internal gas pressure model.

Computes the outward gas pressure \(p_\text{gas}\) as a function of the instantaneous state. The model reads the equilibrium configuration, R0 and P_gas0, directly from state, so gas models store only their intrinsic physical parameters, such as the polytropic exponent.

Concrete models include the polytropic law (PolytropicGas) and the van der Waals corrected gas (VanDerWaalsGas).

is_admissible

is_admissible(state: BubbleState) -> Array

Return whether the gas law is defined at state.

The default requires a positive radius, R > 0. A gas law with a smaller domain, such as VanDerWaalsGas, overrides this method. solve_eom uses it, through EquationOfMotion.is_admissible, to keep gradients finite when a rejected trial step leaves the domain.

Parameters:

Name Type Description Default
state BubbleState

State to check. Its leaves may be NaN or infinite.

required

Returns:

Type Description
Array

Boolean scalar.

Source code in jbubble/bubble/gas.py
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
def is_admissible(self, state: BubbleState) -> jax.Array:
    """Return whether the gas law is defined at `state`.

    The default requires a positive radius, `R > 0`. A gas law with a
    smaller domain, such as
    [`VanDerWaalsGas`][jbubble.bubble.gas.VanDerWaalsGas], overrides
    this method. [`solve_eom`][jbubble.solver.solve_eom] uses it,
    through
    [`EquationOfMotion.is_admissible`][jbubble.bubble.eom.EquationOfMotion.is_admissible],
    to keep gradients finite when a rejected trial step leaves the
    domain.

    Parameters
    ----------
    state : BubbleState
        State to check. Its leaves may be NaN or infinite.

    Returns
    -------
    jax.Array
        Boolean scalar.
    """
    return state.R > 0

PolytropicGas

PolytropicGas(gamma: ArrayLike | Property)

Bases: GasModel

Polytropic gas law.

\[ p_\text{gas}(R) = P_{\text{gas},0}\left(\frac{R_0}{R}\right)^{3\gamma} \]

Parameters:

Name Type Description Default
gamma float or Property

Polytropic exponent: 1.0 is isothermal and 1.4 is adiabatic air.

required

VanDerWaalsGas

VanDerWaalsGas(gamma: ArrayLike | Property, h_frac: ArrayLike | Property)

Bases: GasModel

Hard-core corrected polytropic gas (van der Waals).

\[ p_\text{gas}(R) = P_{\text{gas},0} \left(\frac{R_0^3 - h^3}{R^3 - h^3}\right)^{\gamma} \]

where \(h = h_\text{frac} R_0\) is the van der Waals hard-core radius: the radius of the bubble's gas content compressed to its excluded volume. The hard core is a property of the gas, not of the bubble, so h_frac depends on the gas species and its equilibrium density. The pressure diverges as \(R \to h\) and the law is undefined for \(R \le h\), which is_admissible reports to the solver.

Parameters:

Name Type Description Default
gamma float or Property

Polytropic exponent.

required
h_frac float or Property

Hard-core radius as a fraction of R0 (dimensionless). Example 04 (examples/04_equations_of_motion.py) uses 1 / 8.86 ≈ 0.113.

required

is_admissible

is_admissible(state: BubbleState) -> Array

Return whether R lies outside the hard core, R > h_frac * R0.

Parameters:

Name Type Description Default
state BubbleState

State to check. Its leaves may be NaN or infinite.

required

Returns:

Type Description
Array

Boolean scalar.

Source code in jbubble/bubble/gas.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
def is_admissible(self, state: BubbleState) -> jax.Array:
    """Return whether `R` lies outside the hard core, `R > h_frac * R0`.

    Parameters
    ----------
    state : BubbleState
        State to check. Its leaves may be NaN or infinite.

    Returns
    -------
    jax.Array
        Boolean scalar.
    """
    hard_core = self.h_frac(state) * state.R0
    return hard_core < state.R

Shell models

ShellModel

ShellModel(sigma: ArrayLike | Property)

Bases: Module, ABC

Bubble shell or coating model.

Computes the total inward stress from the shell, including:

  • Laplace pressure from surface tension, \(2\sigma(R)/R\).
  • Shell viscous dissipation, for example \(4\kappa_s\dot{R}/R^2\).
  • Shell elastic restoring forces, for thick shells.

Every ShellModel holds a Property as its sigma field. The field's as_property converter turns a plain float into a Property.

Parameters:

Name Type Description Default
sigma float or Property

Surface tension law [N/m].

required

p_laplace

p_laplace(state: BubbleState) -> Array

Return the Laplace pressure from surface tension, \(2\sigma(R)/R\) [Pa].

Source code in jbubble/bubble/shell.py
63
64
65
def p_laplace(self, state: BubbleState) -> jax.Array:
    r"""Return the Laplace pressure from surface tension, $2\sigma(R)/R$ [Pa]."""
    return 2.0 * self.sigma(state) / state.R

p_elastic

p_elastic(state: BubbleState) -> Array

Return the elastic contribution from the shell [Pa].

Source code in jbubble/bubble/shell.py
67
68
69
70
@abc.abstractmethod
def p_elastic(self, state: BubbleState) -> jax.Array:
    """Return the elastic contribution from the shell [Pa]."""
    ...

p_viscous

p_viscous(state: BubbleState) -> Array

Return the viscous contribution from the shell [Pa].

Source code in jbubble/bubble/shell.py
72
73
74
75
@abc.abstractmethod
def p_viscous(self, state: BubbleState) -> jax.Array:
    """Return the viscous contribution from the shell [Pa]."""
    ...

NoShell

NoShell(sigma: ArrayLike | Property)

Bases: ShellModel

No shell coating, only Laplace pressure.

\[ p_\text{shell} = \frac{2\sigma(R)}{R} \]

Suitable for uncoated gas bubbles. Accepts a plain float for sigma, for example 72e-3 for water.

Parameters:

Name Type Description Default
sigma float or Property

Surface tension law [N/m].

required

LipidShell

LipidShell(sigma: ArrayLike | Property, kappa_s: ArrayLike | Property)

Bases: ShellModel

Thin lipid shell with surface viscosity.

\[ p_\text{shell} = \frac{2\sigma(R)}{R} + \frac{4\kappa_s\dot{R}}{R^2} \]

This is the shell model of Marmottant et al. (2005) and of its smoothed variants.

Parameters:

Name Type Description Default
sigma float or Property

Surface tension law [N/m].

required
kappa_s float or Property

Shell surface-dilatational viscosity [N s/m].

required
Notes

p_elastic returns zero for this model. The shell elasticity isn't absent: the surface tension law sigma encodes all of it. When sigma is state-dependent, as with MarmottantSurfaceTension or SmoothMarmottantSurfaceTension, the area-elasticity term \(\chi\left[(R/R_b)^2 - 1\right]\) enters through the Laplace pressure \(2\sigma(R)/R\), not through a separate p_elastic term. This matches how the literature writes the Marmottant model: the elastic and ruptured regimes modify \(\sigma(R)\) rather than adding an independent stress contribution.

ThickShell

ThickShell(
    sigma: ArrayLike | Property,
    d_s: ArrayLike | Property,
    G_s: ArrayLike | Property,
    mu_s: ArrayLike | Property,
)

Bases: ShellModel

Incompressible viscoelastic shell of finite thickness (Church 1995).

The shell is a layer of incompressible Kelvin-Voigt solid with shear modulus \(G_s\), shear viscosity \(\mu_s\), and thickness \(d_s\) at the equilibrium radius \(R_0\). Integrating its radial stress across the layer gives the shell stresses of the Church (1995) model in the finite-deformation form of Qin and Ferrara (2010, Eq. 10):

\[ \begin{aligned} p_\text{elastic} &= \frac{4}{3} G_s \left[1 - \left(\frac{R_0}{R}\right)^3\right] \frac{V_s}{R^3 - V_s}, \\ p_\text{viscous} &= 4 \mu_s \frac{V_s}{R^3 - V_s} \frac{\dot{R}}{R}, \end{aligned} \qquad V_s = R_0^3 - (R_0 - d_s)^3, \]

where \(R\) is the outer, liquid-side radius and \(V_s\) is the shell volume divided by \(4\pi/3\). Because the shell is incompressible, \(V_s\) stays constant, and \(R^3 - V_s\) is the cube of the inner radius. The total shell pressure is

\[ p_\text{shell} = \frac{2\sigma(R)}{R} + p_\text{elastic} + p_\text{viscous}. \]

For a thin shell, \(d_s \ll R_0\), so \(V_s \approx 3 R_0^2 d_s\), and the model reduces to the thin-shell model of Hoff et al. (2000):

\[ p_\text{elastic} \approx 12 G_s d_s \frac{R_0^2}{R^3} \left(1 - \frac{R_0}{R}\right), \qquad p_\text{viscous} \approx 12 \mu_s d_s \frac{R_0^2 \dot{R}}{R^4}. \]

The viscous terms agree exactly in that limit, and the elastic terms agree to first order in the strain. For small oscillations about \(R_0\), the shell therefore adds a stiffness of \(12 G_s d_s / R_0\) per unit radial strain and a damping of \(12 \mu_s d_s / R_0^2\) per unit wall velocity, with relative corrections of order \(d_s/R_0\).

Parameters:

Name Type Description Default
sigma float or Property

Surface tension law [N/m].

required
d_s float or Property

Shell thickness at the equilibrium radius \(R_0\) [m]. The shell volume, not the thickness, stays constant as the bubble moves.

required
G_s float or Property

Shell shear modulus [Pa]. It can be state-dependent, for example to model strain stiffening or strain softening.

required
mu_s float or Property

Shell shear viscosity [Pa s]. It can be state-dependent, for example to model shear thinning.

required
Notes

jbubble integrates a single radius, so this class simplifies the full Church and Qin-Ferrara equations as follows:

  • The gas model reads the outer radius \(R\) rather than the inner radius \((R^3 - V_s)^{1/3}\), so the gas stiffness comes out low by a relative amount of about \(3 d_s / R_0\).
  • The class neglects the shell's inertia, a relative error of order \(d_s / R_0\) in the inertial terms.
  • A single surface tension \(\sigma\) acts at \(R\), in place of separate gas-shell and shell-liquid tensions.
  • The shell is unstrained at \(R_0\), and initial_state balances the Laplace pressure with the gas pressure.

In Church's model, as quoted by Tu et al. (2009, Eqs. 2 and 3), \(R_1\) is the inner radius and \(R_{e1}\) is the unstrained inner radius. The viscous term is identical to Church's \(4 \mu_s V_s \dot{R}_1 / (R_1 R^3)\), written with the outer radius. The elastic term agrees to first order in the strain with Church's \(4 G_s (V_s / R^3)(1 - R_{e1}/R_1)\) if you set \(R_{e1} = R_0 - d_s\), which leaves the shell unstrained at \(R_0\), as in this class. Church's own \(R_{e1}\) is slightly larger, because Church's shell is strained at equilibrium so that it carries the Laplace pressure. The model needs \(R^3 > V_s\), that is, a positive inner radius.

References

Church, C. C. (1995). The effects of an elastic solid surface layer on the radial pulsations of gas bubbles. J. Acoust. Soc. Am. 97(3), 1510-1521. doi:10.1121/1.412091

Hoff, L., Sontum, P. C., & Hovem, J. M. (2000). Oscillations of polymeric microbubbles: Effect of the encapsulating shell. J. Acoust. Soc. Am. 107(4), 2272-2280. doi:10.1121/1.428557

Qin, S., & Ferrara, K. W. (2010). A model for the dynamics of ultrasound contrast agents in vivo. J. Acoust. Soc. Am. 128(3), 1511-1521. doi:10.1121/1.3409476

Tu, J., Guan, J., Qiu, Y., & Matula, T. J. (2009). Estimating the shell parameters of SonoVue microbubbles using light scattering. J. Acoust. Soc. Am. 126(6), 2954-2962. doi:10.1121/1.3242346

Surface tension Properties

These are Property subclasses that encode a state-dependent surface tension law. They are passed as the sigma argument to shell models.

MarmottantSurfaceTension

MarmottantSurfaceTension(
    R_buckle_ratio: ArrayLike,
    chi: ArrayLike | Property,
    sigma_rupture: ArrayLike | Property,
)

Bases: Property

Piecewise Marmottant surface tension law.

Three regimes, based on the radius \(R\) relative to the buckling radius \(R_b\) and the rupture radius \(R_r\):

\[ \sigma(R) = \begin{cases} 0 & R \le R_b \quad \text{(buckled)}, \\ \chi\left[(R/R_b)^2 - 1\right] & R_b < R < R_r \quad \text{(elastic)}, \\ \sigma_r & R \ge R_r \quad \text{(ruptured)}, \end{cases} \]

where \(R_b\) is R_buckle_ratio * state.R0, \(\sigma_r\) is sigma_rupture, and continuity of \(\sigma\) at the elastic-to-ruptured transition gives \(R_r = R_b\sqrt{1 + \sigma_r/\chi}\).

The surface tension at \(R_0\) is \(\sigma_0 = \chi\left[(1/r_b)^2 - 1\right]\), where \(r_b\) is R_buckle_ratio, so \(r_b = (1 + \sigma_0/\chi)^{-1/2}\). A ratio of 1 or more starts the bubble buckled.

Parameters:

Name Type Description Default
R_buckle_ratio float or Array

Buckling radius as a fraction of R0 (dimensionless).

required
chi float or Property

Shell elasticity [N/m].

required
sigma_rupture float or Property

Surface tension after rupture [N/m].

required
Notes

\(\sigma(R)\) has discontinuous first derivatives at the regime boundaries. For applications that need smooth gradients, such as gradient-based optimisation, use SmoothMarmottantSurfaceTension instead.

The as_property converter stores chi and sigma_rupture as Property instances. To replace one in an existing model with eqx.tree_at, which bypasses the converter, pass a Property or target its val leaf.

References

Marmottant, P., van der Meer, S., Emmer, M., Versluis, M., de Jong, N., Hilgenfeldt, S., & Lohse, D. (2005). A model for large amplitude oscillations of coated bubbles accounting for buckling and rupture. J. Acoust. Soc. Am. 118(6), 3499-3505. doi:10.1121/1.2109427

SmoothMarmottantSurfaceTension

SmoothMarmottantSurfaceTension(
    R_buckle_ratio: ArrayLike,
    chi: ArrayLike | Property,
    sigma_rupture: ArrayLike | Property,
    smoothing: ArrayLike = 0.01,
)

Bases: Property

Marmottant surface tension law with smoothed corners.

The piecewise Marmottant law clips the elastic surface tension to the interval \([0, \sigma_r]\). This law, which is jbubble's own construction, replaces the hard clip with a logistic-smoothed clamp whose width \(\varepsilon\), set by smoothing, is a fraction of \(\sigma_r\):

\[ \begin{aligned} y(R) &= \frac{\chi}{\sigma_r}\left[\left(\frac{R}{R_b}\right)^2 - 1\right], \qquad R_b = r_b R_0, \\ \sigma(R) &= \sigma_r \varepsilon\left[ \operatorname{softplus}\left(\frac{y}{\varepsilon}\right) - \operatorname{softplus}\left(\frac{y - 1}{\varepsilon}\right)\right], \end{aligned} \]

where \(r_b\) is R_buckle_ratio, \(\chi\) is chi, \(\sigma_r\) is sigma_rupture, and \(\operatorname{softplus}(x) = \ln(1 + e^x)\). Equivalently, \(\sigma\) is the Marmottant law averaged over a logistic spread of scale \(\varepsilon\sigma_r\) in the elastic surface tension, which rounds the buckling and rupture corners.

The law has the following properties:

  • It's infinitely differentiable in \(R\), \(\chi\), \(r_b\), and \(\sigma_r\).
  • It's non-decreasing in \(R\) and stays within \([0, \sigma_r]\).
  • It differs from the Marmottant law by at most \(\varepsilon \ln 2 \, \sigma_r\) for every \(\chi\) and \(r_b\), and it reaches that bound at the two corners. With the default smoothing = 0.01 and \(\sigma_r = 0.072\) N/m, the bound is 0.5 mN/m.
  • It converges uniformly to the Marmottant law as \(\varepsilon \to 0\).
  • It's well-posed for all positive R_buckle_ratio, chi, and sigma_rupture, including bubbles that start buckled (R_buckle_ratio of 1 or more) or ruptured.

Use this law for gradient-based fitting of a lipid shell. Use MarmottantSurfaceTension for the exact piecewise law in forward simulations.

Parameters:

Name Type Description Default
R_buckle_ratio float or Array

Buckling radius as a fraction of R0 (dimensionless). Must be positive.

required
chi float or Property

Shell elasticity [N/m]. Must be positive.

required
sigma_rupture float or Property

Surface tension after rupture [N/m]. Must be positive.

required
smoothing float or Array

Corner width \(\varepsilon\) as a fraction of sigma_rupture (dimensionless). Must be positive. Default: 0.01.

0.01

Raises:

Type Description
ValueError

If R_buckle_ratio, chi, sigma_rupture, or smoothing isn't positive. The constructor checks concrete values only. It skips the check for a value that's a JAX tracer, for example inside jax.jit or jax.grad, or a state-dependent Property, so keep such values in range yourself.

Notes

The default smoothing = 0.01 keeps the law close to the piecewise one without stiffening the ODE. Keller-Miksis benchmarks with R_buckle_ratio = 0.98, \(\chi\) from 0.1 to 1.5 N/m, R0 from 1 to 3 µm, 1 to 3 MHz, and 25 kPa to 1 MPa show that, at the default:

  • The median root-mean-square difference between the radius curves of the two laws is of order \(10^{-5} R_0\), about the size of the piecewise law's own error at the default solver tolerances. At 50 kPa and below, where the piecewise law's own error is smallest, the smoothing error dominates.
  • The derivative of the peak radius with respect to \(\chi\) has the same sign as the piecewise law's in every case tested.
  • Fitting \(\chi\) to radius curves from the piecewise law at 50 kPa recovers it to within 0.1%.
  • The solver takes about as many steps as it does for the piecewise law, with a median ratio between 0.92 and 1.0.

The smoothing biases the result when the bubble starts near a corner, that is, when \(\sigma_0 = \sigma(R_0)\) of the piecewise law lies within about \(5\varepsilon\sigma_r\) of 0 or of \(\sigma_r\). For example, R_buckle_ratio close to 1 starts the bubble near buckling. Then \(\sigma(R_0)\) shifts by up to \(\varepsilon \ln 2 \, \sigma_r\), and the elasticity at \(R_0\) drops to as little as half the Marmottant value. At smoothing = 0.01, this gives a radius bias of about \(10^{-3} R_0\) (root mean square) and a bias in fitted \(\chi\) of a few percent, up to 8% at 200 kPa. In that regime, use smoothing = 0.005, or use MarmottantSurfaceTension for forward-only simulations. The bubble still starts at equilibrium, because initial_state reads \(\sigma(R_0)\) from the law itself.

The as_property converter stores chi and sigma_rupture as Property instances.

GompertzSurfaceTension

GompertzSurfaceTension(
    R_buckle_ratio: ArrayLike,
    chi: ArrayLike | Property,
    sigma_rupture: ArrayLike | Property,
)

Bases: Property

Marmottant-Gompertz surface tension law (Gümmer et al. 2021).

A Gompertz function replaces the corners of the piecewise Marmottant law with a smooth curve that takes the same inputs (Gümmer, Schenke, and Denner 2021, Eqs. 11 and 13-15):

\[ \sigma(R) = \sigma_r \exp\left[-b \exp\left(c\left(1 - \frac{R}{R_b}\right)\right)\right], \]
\[ \begin{aligned} R_b &= r_b R_0, \qquad \sigma_0 = \chi\left[\left(\frac{R_0}{R_b}\right)^2 - 1\right], \\ b &= -\ln\left(\frac{\sigma_0}{\sigma_r}\right) \exp\left[-c\left(1 - \frac{R_0}{R_b}\right)\right], \\ c &= \frac{2\chi e}{\sigma_r}\sqrt{1 + \frac{\sigma_r}{2\chi}}, \end{aligned} \]

where \(r_b\) is R_buckle_ratio, \(\chi\) is chi, and \(\sigma_r\) is sigma_rupture (the paper's \(\sigma_c\)). The coefficient \(b\) pins \(\sigma(R_0) = \sigma_0\), the Marmottant elastic value. The coefficient \(c\) makes the maximum slope of the curve, \(c\,\sigma_r/(e R_b)\) where \(\sigma = \sigma_r/e\), equal to the Marmottant slope at \(R = R_b\sqrt{1 + \sigma_r/(2\chi)}\). The paper takes \(\sigma_0\) as its input; R_buckle_ratio carries the same information, with \(r_b = (1 + \sigma_0/\chi)^{-1/2}\) (Eq. 11).

The class evaluates the algebraically identical form

\[ \sigma(R) = \sigma_r \exp\left[\ln\left(\frac{\sigma_0}{\sigma_r}\right) \exp\left(\frac{c\,(R_0 - R)}{R_b}\right)\right], \]

and caps the inner exponent at 50, so a deep compression gives \(\sigma = 0\) with a finite gradient instead of an overflow.

Use this law to reproduce Gümmer et al. (2021). For gradient-based fitting, use SmoothMarmottantSurfaceTension, which converges to the Marmottant law. The Notes explain why.

Parameters:

Name Type Description Default
R_buckle_ratio float or Array

Buckling radius as a fraction of R0 (dimensionless). Must lie strictly between 0 and 1.

required
chi float or Property

Shell elasticity [N/m].

required
sigma_rupture float or Property

Asymptotic (ruptured) surface tension [N/m].

required

Raises:

Type Description
ValueError

If the law is ill-posed: it needs \(\chi > 0\) and \(0 < \sigma_0 < \sigma_r\), so R_buckle_ratio must lie strictly between 0 and 1. The constructor checks concrete values only. It skips the check when a parameter is a JAX tracer, for example inside jax.jit or jax.grad, or when chi or sigma_rupture is a state-dependent Property, so keep such values in range yourself.

Notes

The curve is anchored at \(R_0\), not at the Marmottant transitions, so no value of \(c\) makes it converge to the Marmottant law: as \(c\) grows, the curve tends to a step at \(R_0\). Its elasticity at \(R_0\) matches the Marmottant value only approximately, when \(\sigma_0\) is near \(\sigma_r/e\), and it falls towards zero as \(\sigma_0\) approaches 0 or \(\sigma_r\). With R_buckle_ratio fixed, \(\sigma_0\) grows in proportion to \(\chi\), so the mismatch changes as you vary \(\chi\). Fitted to radius curves from the Marmottant law, this law's \(\chi\) comes out biased by about 6% at R_buckle_ratio = 0.98 and by up to about 47% at R_buckle_ratio = 0.995.

The as_property converter stores chi and sigma_rupture as Property instances.

References

Gümmer, J., Schenke, S., & Denner, F. (2021). Modelling lipid-coated microbubbles in focused ultrasound applications at subresonance frequencies. Ultrasound Med. Biol. 47(10), 2958-2979. doi:10.1016/j.ultrasmedbio.2021.06.012


Medium models

MediumModel

MediumModel(
    mu: ArrayLike | Property,
    *,
    rho_L: ArrayLike = 998.0,
    c_L: ArrayLike = 1500.0,
)

Bases: Module, ABC

Surrounding medium (fluid or tissue) model.

Computes the total inward viscous and elastic stresses that the surrounding medium exerts on the bubble wall. For a Newtonian liquid this is \(4\mu\dot{R}/R\). Viscoelastic media, such as Kelvin-Voigt and neo-Hookean media, add elastic restoring forces.

Subclasses implement separate methods for the viscous and elastic contributions, and the default __call__ sums them to get the total medium pressure \(p_\text{medium}(\text{state})\).

The mu field's as_property converter turns a plain float into a Property.

The medium also holds the liquid's density rho_L and speed of sound c_L, which the equations of motion and the emission models read. They are plain constants, not Property fields, because the equations of motion assume a liquid of constant density and sound speed. Both are keyword-only and default to water at 20 °C, so a subclass can add required fields after them.

Parameters:

Name Type Description Default
mu float or Property

Viscous scaling parameter. For Newtonian, Kelvin-Voigt, and neo-Hookean media this is the dynamic viscosity [Pa s]. For PowerLawMedium it is the consistency index \(K\) [Pa sⁿ], which equals the dynamic viscosity when \(n = 1\).

required
rho_L float or Array

Liquid density [kg/m³]. Keyword-only. Default: 998.0 (water at 20 °C: 998.2 kg/m³, NIST).

998.0
c_L float or Array

Speed of sound in the liquid [m/s]. Keyword-only. Default: 1500.0, the round value that ultrasound modelling commonly uses for water (NIST gives 1482 m/s at 20 °C and 1497 m/s at 25 °C).

1500.0
References

NIST Chemistry WebBook, NIST Standard Reference Database 69, Thermophysical Properties of Fluid Systems: water at 0.101325 MPa. doi:10.18434/T4D303

p_viscous

p_viscous(state: BubbleState) -> Array

Return the viscous contribution to the medium pressure [Pa].

Source code in jbubble/bubble/medium.py
78
79
80
81
@abc.abstractmethod
def p_viscous(self, state: BubbleState) -> jax.Array:
    """Return the viscous contribution to the medium pressure [Pa]."""
    ...

p_elastic

p_elastic(state: BubbleState) -> Array

Return the elastic contribution to the medium pressure [Pa].

Source code in jbubble/bubble/medium.py
83
84
85
86
@abc.abstractmethod
def p_elastic(self, state: BubbleState) -> jax.Array:
    """Return the elastic contribution to the medium pressure [Pa]."""
    ...

NewtonianMedium

NewtonianMedium(
    mu: ArrayLike | Property,
    *,
    rho_L: ArrayLike = 998.0,
    c_L: ArrayLike = 1500.0,
)

Bases: MediumModel

Newtonian liquid medium.

\[ p_\text{medium} = \frac{4\mu\dot{R}}{R} \]

Parameters:

Name Type Description Default
mu float or Property

Dynamic viscosity [Pa s].

required
rho_L float or Array

Liquid density [kg/m³]. Keyword-only. Default: 998.0 (water at 20 °C: 998.2 kg/m³, NIST).

998.0
c_L float or Array

Speed of sound in the liquid [m/s]. Keyword-only. Default: 1500.0, the round value that ultrasound modelling commonly uses for water (NIST gives 1482 m/s at 20 °C and 1497 m/s at 25 °C).

1500.0

KelvinVoigtMedium

KelvinVoigtMedium(
    mu: ArrayLike | Property,
    G: ArrayLike | Property,
    *,
    rho_L: ArrayLike = 998.0,
    c_L: ArrayLike = 1500.0,
)

Bases: MediumModel

Kelvin-Voigt viscoelastic medium (Yang & Church 2005).

\[ p_\text{medium} = \frac{4\mu\dot{R}}{R} + \frac{4G}{3}\left[1 - \left(\frac{R_0}{R}\right)^3\right] \]

The elastic term integrates a linear (Hookean) elastic stress through the incompressible surrounding solid. Yang and Church take the displacement field \(u(r) = (R^3 - R_0^3)/(3r^2)\) of the incompressible radial motion, so the deviatoric stress difference is \(\tau_{rr} - \tau_{\theta\theta} = -2G(R^3 - R_0^3)/r^3\), and

\[ p_\text{elastic} = -2\int_R^\infty \frac{\tau_{rr} - \tau_{\theta\theta}}{r}\,\mathrm{d}r = \frac{4G}{3}\left[1 - \left(\frac{R_0}{R}\right)^3\right]. \]

The model reads \(R_0\) from state.R0. The elastic term is zero at \(R = R_0\), linearises to \(4G(R - R_0)/R_0\), which is the same small-strain limit as NeoHookeanMedium, and stays bounded by \(4G/3\) under expansion. For large strains, prefer the neo-Hookean model, whose constitutive law holds at finite strain.

Parameters:

Name Type Description Default
mu float or Property

Dynamic viscosity [Pa s].

required
G float or Property

Shear modulus [Pa]. It can be state-dependent, for example to model strain stiffening.

required
rho_L float or Array

Liquid density [kg/m³]. Keyword-only. Default: 998.0 (water at 20 °C: 998.2 kg/m³, NIST).

998.0
c_L float or Array

Speed of sound in the liquid [m/s]. Keyword-only. Default: 1500.0, the round value that ultrasound modelling commonly uses for water (NIST gives 1482 m/s at 20 °C and 1497 m/s at 25 °C).

1500.0
References

Yang, X., & Church, C. C. (2005). A model for the dynamics of gas bubbles in soft tissue. The Journal of the Acoustical Society of America, 118(6), 3595-3606. https://doi.org/10.1121/1.2118307

Warnez, M. T., & Johnsen, E. (2015). Numerical modeling of bubble dynamics in viscoelastic media with relaxation. Physics of Fluids, 27(6), 063103, Eq. 42. https://doi.org/10.1063/1.4922598

NeoHookeanMedium

NeoHookeanMedium(
    mu: ArrayLike | Property,
    G: ArrayLike | Property,
    *,
    rho_L: ArrayLike = 998.0,
    c_L: ArrayLike = 1500.0,
)

Bases: MediumModel

Neo-Hookean finite-strain viscoelastic medium.

Captures finite-strain behaviour at large oscillation amplitudes, where the Kelvin-Voigt linear approximation breaks down.

The elastic pressure comes from integrating the deviatoric stress difference \(\sigma_{\theta\theta} - \sigma_{rr}\) through the surrounding incompressible neo-Hookean solid. With the substitution \(v = r_0/r\), the integral collapses exactly to \(2G\int_{R_0/R}^{1} (1 + v^3)\,\mathrm{d}v\), which gives

\[ p_\text{elastic} = G\left[\frac{5}{2} - 2\frac{R_0}{R} - \frac{1}{2}\left(\frac{R_0}{R}\right)^4\right]. \]

Key behaviour:

  • Zero at \(R = R_0\): no elastic stress at equilibrium.
  • Reduces to \(4G(R - R_0)/R_0\) for small strains, identical to Kelvin-Voigt.
  • Saturates to \(5G/2\) as \(R \to \infty\). Physically, the material near the bubble thins to zero, so the elastic pressure plateaus.
  • Diverges to \(-\infty\) as \(R \to 0\): strong restoring force under compression.

The total medium pressure is

\[ p_\text{medium} = \frac{4\mu\dot{R}}{R} + G\left[\frac{5}{2} - 2\frac{R_0}{R} - \frac{1}{2}\left(\frac{R_0}{R}\right)^4\right]. \]

Parameters:

Name Type Description Default
mu float or Property

Dynamic viscosity [Pa s]. Accepts a plain float.

required
G float or Property

Shear modulus [Pa]. Accepts a plain float. It can be state-dependent, for example to model strain stiffening.

required
rho_L float or Array

Liquid density [kg/m³]. Keyword-only. Default: 998.0 (water at 20 °C: 998.2 kg/m³, NIST).

998.0
c_L float or Array

Speed of sound in the liquid [m/s]. Keyword-only. Default: 1500.0, the round value that ultrasound modelling commonly uses for water (NIST gives 1482 m/s at 20 °C and 1497 m/s at 25 °C).

1500.0

PowerLawMedium

PowerLawMedium(
    mu: ArrayLike | Property,
    n_exp: ArrayLike | Property,
    eps: float = 100.0,
    *,
    rho_L: ArrayLike = 998.0,
    c_L: ArrayLike = 1500.0,
)

Bases: MediumModel

Power-law (generalised Newtonian) surrounding medium.

The viscosity depends on the local shear rate, \(\eta = K\dot{\gamma}^{\,n-1}\), where \(\dot{\gamma} = \sqrt{2\mathbf{D}:\mathbf{D}}\) is the rheometric shear rate, the convention under which rheometers report \(K\) and \(n\). For the incompressible radial flow \(u = R^2\dot{R}/r^2\) the shear rate is \(\dot{\gamma}(r) = 2\sqrt{3}\,|R^2\dot{R}|/r^3\), which is \(\dot{\gamma}_w = 2\sqrt{3}\,|\dot{R}|/R\) at the bubble wall. Integrating the viscous stress through the liquid,

\[ p_\text{viscous} = -2\int_R^\infty \frac{\tau_{rr} - \tau_{\theta\theta}}{r}\,\mathrm{d}r = \frac{4K}{n}\,\dot{\gamma}_w^{\,n-1}\,\frac{\dot{R}}{R}. \]

The factor \(1/n\) comes from the radial variation of \(\eta\). For \(n = 1\) this is exactly the Newtonian result \(4K\dot{R}/R\).

The pure power law has an infinite viscosity at zero shear rate when \(n < 1\), which makes the right-hand side non-smooth whenever \(\dot{R}\) changes sign. The model regularises the wall shear rate smoothly:

\[ \dot{\gamma}_\text{eff} = \sqrt{12\left(\frac{\dot{R}}{R}\right)^2 + \varepsilon^2}, \qquad p_\text{viscous} = \frac{4K}{n}\,\dot{\gamma}_\text{eff}^{\,n-1}\, \frac{\dot{R}}{R}. \]

Special cases:

  • \(n < 1\): shear-thinning (biological tissue, blood, polymer gels).
  • \(n = 1\): Newtonian; recovers NewtonianMedium with viscosity \(K\).
  • \(n > 1\): shear-thickening.

Parameters:

Name Type Description Default
mu float or Property

Consistency index \(K\) [Pa sⁿ], in the rheometric convention \(\eta = K\dot{\gamma}^{\,n-1}\) with \(\dot{\gamma} = \sqrt{2\mathbf{D}:\mathbf{D}}\). The inherited field name is mu, but its dimensions are [Pa sⁿ], not [Pa s]. It equals the dynamic viscosity when \(n = 1\).

required
n_exp float or Property

Power-law exponent (dimensionless, positive).

required
eps float

Shear-rate regularisation \(\varepsilon\) [s⁻¹]. Default: 1e2.

100.0
rho_L float or Array

Liquid density [kg/m³]. Keyword-only. Default: 998.0 (water at 20 °C: 998.2 kg/m³, NIST).

998.0
c_L float or Array

Speed of sound in the liquid [m/s]. Keyword-only. Default: 1500.0, the round value that ultrasound modelling commonly uses for water (NIST gives 1482 m/s at 20 °C and 1497 m/s at 25 °C).

1500.0
Notes

The default \(\varepsilon = 10^2\) s⁻¹ is three or more orders of magnitude below the wall shear rates of a driven microbubble (a 1 % oscillation at 1 MHz gives \(\dot{\gamma}_w \approx 2\sqrt{3}\,(2\pi \cdot 10^6)(0.01) \approx 2 \times 10^5\) s⁻¹), so it changes the viscous pressure only near the instants where \(\dot{R}\) reverses. For a 2 µm bubble in a shear-thinning liquid (\(n = 0.7\)) at 100 kPa, the radius differs from the \(\varepsilon \to 0\) solution by less than \(10^{-7} R_0\).

\(\varepsilon\) also sets the viscosity at zero shear rate, \(K\varepsilon^{\,n-1}\), which plays the part of the finite zero-shear viscosity of a real shear-thinning liquid. A smaller \(\varepsilon\) raises it when \(n < 1\), so the right-hand side changes faster where \(\dot{R}\) reverses and the solver takes more steps there. The smooth floor keeps the right-hand side differentiable for any \(\varepsilon > 0\). jbubble 0.1 instead floored the shear rate with a max, whose kink at the floor made tight-tolerance solves at 300 kPa fail.

The earlier jbubble convention used the wall velocity gradient \(2|\dot{R}|/R\) as the shear rate, which overestimates the viscous pressure by \(\sqrt{3}^{\,1-n}\) for a \(K\) measured on a rheometer.

References

Kaykanat, S. I., & Uguz, K. (2024). Shape stability of a microbubble in a power-law liquid. The European Physical Journal Special Topics, Eq. 17, with \(M = 2m(2\sqrt{3})^{k-1}\). https://doi.org/10.1140/epjs/s11734-024-01174-7


Equations of motion

EquationOfMotion

EquationOfMotion(
    gas: GasModel,
    shell: ShellModel,
    medium: MediumModel,
    R0: ArrayLike,
    P_amb: ArrayLike,
)

Bases: Module, ABC, Generic

Macroscopic equation of motion for bubble dynamics.

Assembles a GasModel, a ShellModel, and a MediumModel into a complete ODE right-hand side. Concrete subclasses encode different formulations (Rayleigh-Plesset, Keller-Miksis, and others), which differ in how they relate the liquid-side boundary pressure \(p_L\) to the radial acceleration \(\ddot{R}\).

The type parameter StateType binds each equation of motion (EoM) to the state it operates on. The built-in EoMs use BubbleState; an EoM that carries extra degrees of freedom binds a BubbleState subclass, so the type checker catches a wrong state type before it fails at runtime deep inside a JAX trace.

The concrete helper p_L computes the liquid-side boundary pressure:

\[ p_L = p_\text{gas}(\text{state}) - p_\text{shell}(\text{state}) - p_\text{medium}(\text{state}) \]

Because p_L is a regular method on an Equinox module, JAX differentiates through it automatically. EoMs that need \(\mathrm{d}p_L/\mathrm{d}t\), such as Keller-Miksis, use jax.grad(self.p_L) instead of hand-coding analytical derivatives for every combination of components.

The EoM receives the driving acoustic pressure as a callable p_ac_fn, so EoMs that need \(\mathrm{d}p_\text{ac}/\mathrm{d}t\) compute it with jax.grad.

initial_state seeds the equilibrium configuration, R0 and P_gas0, into the state. The ODE carries them as frozen constants, with zero time derivatives in the standard case.

Two hooks tell solve_eom about the state: is_admissible marks the states where the right-hand side is defined, and state_scale gives the magnitude of each state field, which sets the meaning of the solver tolerances. Override them in a subclass that adds state fields.

Parameters:

Name Type Description Default
gas GasModel

Internal gas pressure model.

required
shell ShellModel

Shell or coating model.

required
medium MediumModel

Surrounding medium model. It also holds the liquid density \(\rho_L\) (medium.rho_L) and speed of sound \(c_L\) (medium.c_L).

required
R0 float or Array

Equilibrium bubble radius [m].

required
P_amb float or Array

Ambient (far-field) pressure [Pa].

required

p_L

p_L(state: BubbleState) -> Array

Liquid-side boundary pressure.

\[ p_L = p_\text{gas}(\text{state}) - p_\text{shell}(\text{state}) - p_\text{medium}(\text{state}) \]

Parameters:

Name Type Description Default
state BubbleState

Current bubble state.

required

Returns:

Type Description
Array

Boundary pressure [Pa].

Source code in jbubble/bubble/eom.py
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
def p_L(self, state: BubbleState) -> jax.Array:
    r"""Liquid-side boundary pressure.

    $$
    p_L = p_\text{gas}(\text{state}) - p_\text{shell}(\text{state})
        - p_\text{medium}(\text{state})
    $$

    Parameters
    ----------
    state : BubbleState
        Current bubble state.

    Returns
    -------
    jax.Array
        Boundary pressure [Pa].
    """
    return self.gas(state) - self.shell(state) - self.medium(state)

initial_state

initial_state(
    *, R: ArrayLike | None = None, R_dot: ArrayLike | None = None
) -> BubbleState

Return an initial state seeded with the equilibrium configuration.

By default the bubble starts at rest at its equilibrium radius. Seeds R0 and the Laplace-equilibrium gas pressure into the state:

\[ P_{\text{gas},0} = P_\text{amb} + \frac{2\sigma(R_0)}{R_0} \]

Pass R or R_dot to start away from equilibrium, for example eom.initial_state(R=1.2 * eom.R0) for a bubble released from rest at 1.2 times its equilibrium radius. R0 and P_gas0 stay at their equilibrium values, so the gas pressure at R follows the gas law.

Override this method for coupled systems with a larger state vector. An override must accept the same keyword arguments.

Parameters:

Name Type Description Default
R float or Array

Initial radius [m]. None uses R0.

None
R_dot float or Array

Initial wall velocity [m/s]. None uses 0.

None

Returns:

Type Description
BubbleState

State with R, R_dot, R0, and P_gas0 set.

Source code in jbubble/bubble/eom.py
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
def initial_state(
    self,
    *,
    R: ArrayLike | None = None,
    R_dot: ArrayLike | None = None,
) -> BubbleState:
    r"""Return an initial state seeded with the equilibrium configuration.

    By default the bubble starts at rest at its equilibrium radius. Seeds
    `R0` and the Laplace-equilibrium gas pressure into the state:

    $$
    P_{\text{gas},0} = P_\text{amb} + \frac{2\sigma(R_0)}{R_0}
    $$

    Pass `R` or `R_dot` to start away from equilibrium, for example
    `eom.initial_state(R=1.2 * eom.R0)` for a bubble released from rest
    at 1.2 times its equilibrium radius. `R0` and `P_gas0` stay at their
    equilibrium values, so the gas pressure at `R` follows the gas law.

    Override this method for coupled systems with a larger state vector.
    An override must accept the same keyword arguments.

    Parameters
    ----------
    R : float or jax.Array, optional
        Initial radius [m]. `None` uses `R0`.
    R_dot : float or jax.Array, optional
        Initial wall velocity [m/s]. `None` uses `0`.

    Returns
    -------
    BubbleState
        State with `R`, `R_dot`, `R0`, and `P_gas0` set.
    """
    R0 = jnp.asarray(self.R0)
    P_gas0 = (
        jnp.asarray(self.P_amb)
        + 2.0 * self.shell.sigma(BubbleState(R=R0, R0=R0)) / R0
    )
    R_init = R0 if R is None else jnp.asarray(R)
    R_dot_init = jnp.zeros_like(R0) if R_dot is None else jnp.asarray(R_dot)
    return BubbleState(R=R_init, R_dot=R_dot_init, R0=R0, P_gas0=P_gas0)

is_admissible

is_admissible(state: StateType) -> Array

Return whether the right-hand side is defined at state.

solve_eom never evaluates the equation of motion at an inadmissible state, so its gradients stay finite when an adaptive solver tries, and then rejects, a trial step that leaves the physical domain. The default requires R > 0 and defers to GasModel.is_admissible, which also excludes, for example, the van der Waals hard core.

Override this method in a subclass whose state has other fields with a restricted domain, and combine the result with super().is_admissible(state).

Parameters:

Name Type Description Default
state StateType

State to check. Its leaves may be NaN or infinite.

required

Returns:

Type Description
Array

Boolean scalar.

Source code in jbubble/bubble/eom.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
def is_admissible(self, state: StateType) -> jax.Array:
    """Return whether the right-hand side is defined at `state`.

    [`solve_eom`][jbubble.solver.solve_eom] never evaluates the equation
    of motion at an inadmissible state, so its gradients stay finite
    when an adaptive solver tries, and then rejects, a trial step that
    leaves the physical domain. The default requires `R > 0` and defers
    to [`GasModel.is_admissible`][jbubble.bubble.gas.GasModel.is_admissible],
    which also excludes, for example, the van der Waals hard core.

    Override this method in a subclass whose state has other fields with
    a restricted domain, and combine the result with
    `super().is_admissible(state)`.

    Parameters
    ----------
    state : StateType
        State to check. Its leaves may be NaN or infinite.

    Returns
    -------
    jax.Array
        Boolean scalar.
    """
    return (state.R > 0) & self.gas.is_admissible(state)

state_scale

state_scale(state: StateType) -> StateType

Return the characteristic magnitude of each state field.

solve_eom integrates the dimensionless state state / state_scale(state), so the step-size controller's rtol and atol mean the same thing for a 50 nm bubble as for a 50 µm one. The default scales are:

  • R and R0: the equilibrium radius state.R0 [m].
  • R_dot: the velocity scale \(\sqrt{P_\text{amb}/\rho_L}\) [m/s], with \(\rho_L\) from medium.rho_L, about 10 m/s in water at atmospheric pressure. It's the natural velocity of bubble dynamics: \(R_0\omega_0 \approx \sqrt{3\kappa P_\text{amb}/\rho_L}\) for the Minnaert frequency \(\omega_0\).
  • P_gas0: its own value state.P_gas0 [Pa], or P_amb if that value isn't positive.
  • Any other field of a BubbleState subclass: 1, that is, SI units.

Because R0 and P_gas0 are scaled by their own values, the solver sees them exactly: (x / x) * x == x in floating point.

Override this method to give extra state fields a scale. The scale must be positive and finite; the solver treats it as a constant.

Parameters:

Name Type Description Default
state StateType

Initial state, with a positive R0.

required

Returns:

Type Description
StateType

Scale for each field, with the same structure as state.

Source code in jbubble/bubble/eom.py
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
def state_scale(self, state: StateType) -> StateType:
    r"""Return the characteristic magnitude of each state field.

    [`solve_eom`][jbubble.solver.solve_eom] integrates the dimensionless
    state `state / state_scale(state)`, so the step-size controller's
    `rtol` and `atol` mean the same thing for a 50 nm bubble as for a
    50 µm one. The default scales are:

    - `R` and `R0`: the equilibrium radius `state.R0` [m].
    - `R_dot`: the velocity scale $\sqrt{P_\text{amb}/\rho_L}$
      [m/s], with $\rho_L$ from `medium.rho_L`, about 10 m/s in water
      at atmospheric pressure. It's the
      natural velocity of bubble dynamics: $R_0\omega_0 \approx
      \sqrt{3\kappa P_\text{amb}/\rho_L}$ for the Minnaert frequency
      $\omega_0$.
    - `P_gas0`: its own value `state.P_gas0` [Pa], or `P_amb` if that
      value isn't positive.
    - Any other field of a `BubbleState` subclass: `1`, that is, SI
      units.

    Because `R0` and `P_gas0` are scaled by their own values, the solver
    sees them exactly: `(x / x) * x == x` in floating point.

    Override this method to give extra state fields a scale. The scale
    must be positive and finite; the solver treats it as a constant.

    Parameters
    ----------
    state : StateType
        Initial state, with a positive `R0`.

    Returns
    -------
    StateType
        Scale for each field, with the same structure as `state`.
    """
    P_amb = jnp.asarray(self.P_amb)
    velocity = jnp.sqrt(P_amb / jnp.asarray(self.medium.rho_L))
    R0 = jnp.asarray(state.R0)
    P_gas0 = jnp.asarray(state.P_gas0)
    pressure = jnp.where(P_gas0 > 0, P_gas0, P_amb)
    ones = jax.tree_util.tree_map(jnp.ones_like, state)
    values = (R0, velocity, R0, pressure)
    return eqx.tree_at(
        lambda s: (s.R, s.R_dot, s.R0, s.P_gas0),
        ones,
        tuple(
            jnp.broadcast_to(v, jnp.shape(leaf)).astype(jnp.result_type(leaf))
            for v, leaf in zip(
                values, (state.R, state.R_dot, state.R0, state.P_gas0), strict=True
            )
        ),
    )

RayleighPlesset

RayleighPlesset(
    gas: GasModel,
    shell: ShellModel,
    medium: MediumModel,
    R0: ArrayLike,
    P_amb: ArrayLike,
)

Bases: EquationOfMotion

Rayleigh-Plesset equation of motion (incompressible liquid).

\[ R\ddot{R} + \frac{3}{2}\dot{R}^2 = \frac{1}{\rho_L}\left(p_L - P_\text{amb} - p_\text{ac}\right) \]

The simplest bubble dynamics EoM, which assumes an incompressible surrounding liquid. It takes only the parameters of EquationOfMotion, and reads \(\rho_L\) from medium.rho_L.

ModifiedRayleighPlesset

ModifiedRayleighPlesset(
    gas: GasModel,
    shell: ShellModel,
    medium: MediumModel,
    R0: ArrayLike,
    P_amb: ArrayLike,
)

Bases: EquationOfMotion

Modified Rayleigh-Plesset equation with gas radiation damping.

Adds a first-order compressibility correction to the gas pressure term only, as used by Marmottant et al. (2005):

\[ R\ddot{R} + \frac{3}{2}\dot{R}^2 = \frac{1}{\rho_L}\left(p_L + \frac{R}{c_L}\frac{\mathrm{d}p_\text{gas}}{\mathrm{d}t} - P_\text{amb} - p_\text{ac}\right) \]

where autodiff computes \(\mathrm{d}p_\text{gas}/\mathrm{d}t = (\partial p_\text{gas}/\partial R)\,\dot{R}\).

It takes only the parameters of EquationOfMotion, and reads \(\rho_L\) and \(c_L\) from medium.rho_L and medium.c_L.

KellerMiksis

KellerMiksis(
    gas: GasModel,
    shell: ShellModel,
    medium: MediumModel,
    R0: ArrayLike,
    P_amb: ArrayLike,
)

Bases: EquationOfMotion

Keller-Miksis equation of motion (first-order compressibility).

Accounts for liquid compressibility up to first order in the Mach number \(M = \dot{R}/c_L\):

\[ (1 - M) R\ddot{R} + \frac{3}{2}\left(1 - \frac{M}{3}\right)\dot{R}^2 = \frac{1 + M}{\rho_L}\left(p_L - P_\text{amb} - p_\text{ac}\right) + \frac{R}{\rho_L c_L}\left(\frac{\mathrm{d}p_L}{\mathrm{d}t} - \frac{\mathrm{d}p_\text{ac}}{\mathrm{d}t}\right) \]

JAX autodiff computes the time derivative \(\mathrm{d}p_L/\mathrm{d}t\) automatically, through the chain rule on p_L:

\[ \frac{\mathrm{d}p_L}{\mathrm{d}t} = \frac{\partial p_L}{\partial R}\dot{R} + \frac{\partial p_L}{\partial \dot{R}}\ddot{R} \]

The \(\ddot{R}\) term moves to the left-hand side, so this EoM works with any combination of gas, shell, and medium models without hand-coded derivatives.

It takes only the parameters of EquationOfMotion, and reads \(\rho_L\) and \(c_L\) from medium.rho_L and medium.c_L.