/ proof-of-concept / cymsim
Sign in
proof-of-concept / cymsim
cymsim / docs / model.md
35.4 KBPreviewCodeBlameHistoryRaw

The cymsim model: equations, discretization and method#

This document states what cymsim solves and how. Section 2 gives the continuous model, section 3 the geometry, section 4 the spatial discretization, section 5 the time integration, section 6 the excitation, section 7 the radiation model, section 8 the diagnostics and section 9 the records kept for other programs. Section 10 lists the limitations we are aware of. Each section names the module that implements it. Units are SI throughout.

1. Overview#

A crash cymbal is a thin, shallow, curved bronze shell. Struck hard, it does not simply ring at its linear modes: the transverse motion is many times the thickness, membrane stretching couples the modes, and energy injected at low frequency by the stroke climbs to high frequency over tens of milliseconds. This upward cascade is what gives a crash its build-up and its noisy, bright sustain, and it is entirely a nonlinear effect. A model that is meant to reproduce it must therefore keep the geometric nonlinearity of the shell, resolve frequencies up to the audio band, and run long enough for the cascade to develop.

cymsim does this with the smallest set of ingredients we found sufficient: the Marguerre-von Karman shallow-shell equations (the von Karman plate with an initial curvature), a flat-facet triangular finite-element discretization, an explicit leapfrog integrator with a stability-based sub-step rate, a rigid exciter coupled through a power-law contact spring, and a Rayleigh-integral listener. Everything from the element energies onward is written with JAX so that the internal force is the exact gradient of the discrete strain energy and the whole time step compiles to one kernel.

2. Governing equations#

Implemented in elements.py (strains, energies) and shell.py (damping, mass).

2.1 Kinematics#

Let Ω\Omega be the disk of radius RR in the (x,y)(x, y) plane. The undeformed mid-surface is the graph z=z0(x,y)z = z_0(x, y) of a shallow surface, meaning ∣∇z0∣≪1|\nabla z_0| \ll 1; we warn when max⁡∣∇z0∣\max |\nabla z_0| exceeds 0.4. The displacement of a mid-surface point has in-plane components u(x,y,t)u(x, y, t), v(x,y,t)v(x, y, t) along xx and yy and a transverse component w(x,y,t)w(x, y, t) along zz. The Marguerre-von Karman membrane strains are

εxx=u,x+12w,x2+z0,x w,x,εyy=v,y+12w,y2+z0,y w,y,γxy=u,y+v,x+w,xw,y+z0,x w,y+z0,y w,x,\varepsilon_{xx} = u_{,x} + \tfrac12 w_{,x}^2 + z_{0,x}\, w_{,x}, \qquad \varepsilon_{yy} = v_{,y} + \tfrac12 w_{,y}^2 + z_{0,y}\, w_{,y}, \qquad \gamma_{xy} = u_{,y} + v_{,x} + w_{,x} w_{,y} + z_{0,x}\, w_{,y} + z_{0,y}\, w_{,x},

and the curvatures are those of the plate, κ=(w,xx, w,yy, 2w,xy)\kappa = (w_{,xx},\, w_{,yy},\, 2 w_{,xy}). Setting z0=0z_0 = 0 recovers the von Karman plate. Dropping the quadratic terms 12w,x2\tfrac12 w_{,x}^2, 12w,y2\tfrac12 w_{,y}^2, w,xw,yw_{,x} w_{,y} gives the linear shallow shell with the same geometry; the configuration flag numerics.nonlinear = false does exactly this and is the reference against which the cascade is judged. The terms in z0z_0 are the linear coupling between transverse motion and membrane stretch that a curved shell has and a flat plate lacks; they are what make the bell and the bow stiffen the low modes.

2.2 Constitutive relations and strain energy#

The material is isotropic with Young's modulus EE, Poisson ratio ν\nu and density ρ\rho; the thickness h(x,y)h(x, y) varies. Membrane forces and bending moments are N=CεN = C \varepsilon and M=DκM = D \kappa with

C=Eh1−ν2(1ν0ν10001−ν2),D=Eh312(1−ν2)(1ν0ν10001−ν2).C = \frac{E h}{1 - \nu^2} \begin{pmatrix} 1 & \nu & 0 \\ \nu & 1 & 0 \\ 0 & 0 & \tfrac{1 - \nu}{2} \end{pmatrix}, \qquad D = \frac{E h^3}{12 (1 - \nu^2)} \begin{pmatrix} 1 & \nu & 0 \\ \nu & 1 & 0 \\ 0 & 0 & \tfrac{1 - \nu}{2} \end{pmatrix}.

The strain energy is

U[u,v,w]=12∫Ω(εTC ε+κTD κ)dA,U[u, v, w] = \frac12 \int_\Omega \left( \varepsilon^{\mathsf T} C\, \varepsilon + \kappa^{\mathsf T} D\, \kappa \right) dA ,

the first term being the membrane energy and the second the bending energy. Their ratio during a run is one of the diagnostics of how nonlinear the motion is.

2.3 Equations of motion and damping#

With ρh\rho h the mass per unit area and δU/δ(⋅)\delta U / \delta(\cdot) the variational derivative,

ρh u¨=−δUδu−2σ0ρh u˙,ρh v¨=−δUδv−2σ0ρh v˙,ρh w¨=−δUδw−2σ0ρh w˙+2σ1ρh ∇2w˙+fc,\rho h\, \ddot u = -\frac{\delta U}{\delta u} - 2 \sigma_0 \rho h\, \dot u, \qquad \rho h\, \ddot v = -\frac{\delta U}{\delta v} - 2 \sigma_0 \rho h\, \dot v, \qquad \rho h\, \ddot w = -\frac{\delta U}{\delta w} - 2 \sigma_0 \rho h\, \dot w + 2 \sigma_1 \rho h\, \nabla^2 \dot w + f_c ,

where fcf_c is the contact pressure of the exciter (section 6). Rotary inertia is neglected in the continuum model (it is reintroduced in modified form by the discretization, section 4.6). The losses follow the two-parameter form used throughout the sound-synthesis literature (Bilbao 2009): σ0\sigma_0 gives a frequency-independent decay rate, so that the 60 dB decay time of the low modes is T60≈6.91/σ0T_{60} \approx 6.91 / \sigma_0, and the σ1\sigma_1 term gives a wave of wavenumber kk the additional decay rate 2σ1k22 \sigma_1 k^2. For bending waves k2=ωρh/Dk^2 = \omega \sqrt{\rho h / D}, so this rate grows linearly with frequency and sets how far up the cascade can reach before losses win. We make no attempt to model the physical loss mechanisms (thermoelastic, viscoelastic, radiation) separately; σ0\sigma_0 and σ1\sigma_1 are tuning parameters that lump them.

2.4 Support#

The shell is free at the rim. A real cymbal rests on a felt washer at the bell, which we do not model; instead the six rigid-body motions of the shell are removed by projection at every step (section 5.3). This is the simplest device that keeps the cymbal from drifting away under the impulse of the stroke.

3. Geometry#

Implemented in geometry.py; parameters in Profile and Imperfections.

The profile is written in polar coordinates (r,ϕ)(r, \phi) on the projected disk. The shoulder is the junction between the bell and the bow; its radius rsh(ϕ)=Rsh(1+ϵovalcos⁡(2ϕ+ϕoval))r_{sh}(\phi) = R_{sh} (1 + \epsilon_{oval} \cos(2\phi + \phi_{oval})) carries an m=2m = 2 ovality. The bow coordinate s=clip((R−r)/(R−rsh(ϕ)), 0, 1)s = \mathrm{clip}\big((R - r) / (R - r_{sh}(\phi)),\, 0,\, 1\big) runs from 0 at the rim to 1 at the shoulder, and u=1−su = 1 - s. With rbr_b the distance from the (offset) bell centre and b(rb)=12(1+cos⁡πt)b(r_b) = \tfrac12 (1 + \cos \pi t), t=clip((rb−Rtop)/(rsh−Rtop), 0, 1)t = \mathrm{clip}\big((r_b - R_{top}) / (r_{sh} - R_{top}),\, 0,\, 1\big), a C1C^1 ramp that is 1 on the bell plateau and 0 beyond the shoulder, the mid-surface height is

z0=Hbow(1−up)+Hbell b(rb)+u∑mamcos⁡(mϕ+ϕm)+∑iAiexp⁡ ⁣(−∣x−xi∣22si2),z_0 = H_{bow} \left( 1 - u^{p} \right) + H_{bell}\, b(r_b) + u \sum_m a_m \cos(m \phi + \phi_m) + \sum_i A_i \exp\!\left( -\frac{|x - x_i|^2}{2 s_i^2} \right),

bow, bell, rim warp and hammer dents in that order. The bow exponent p=1p = 1 gives a cone and p>1p > 1 a bow that is curved near the rim and flat at the shoulder. The warp amplitudes are am=2awarp/ma_m = 2 a_{warp} / m for the listed modes with random phases; it vanishes at the shoulder and grows linearly to the rim, so the mean rim height stays z0=0z_0 = 0. The dents are Gaussian bumps of random sign (a cymbal is hammered from both sides), with centres uniform over the annulus between the shoulder and the rim and with jittered depth and width. The thickness is

h=hrim+(hsh−hrim) s+(hbell−hsh) b(rb),h = h_{rim} + (h_{sh} - h_{rim})\, s + (h_{bell} - h_{sh})\, b(r_b),

a linear taper along the bow and a smooth step into the bell. All random draws come from numpy.random.default_rng(seed) in a fixed order, so a run is reproducible from its configuration. The imperfections matter: a perfectly axisymmetric shell has degenerate mode pairs and symmetric coupling rules, and its cascade is markedly less rich than that of a real cymbal.

4. Spatial discretization#

4.1 Mesh#

Implemented in mesh.py. The finite elements live on the flat projected disk; the curvature enters only through z0z_0 in the strains. Nodes are placed on concentric rings of spacing Δr=R/K\Delta r = R / K with K=round(R/ℓ)K = \mathrm{round}(R / \ell) for a target element size ℓ\ell; ring kk carries nk=round(2πrk/Δr)≈6kn_k = \mathrm{round}(2\pi r_k / \Delta r) \approx 6k nodes and alternate rings are rotated by half a step. Consecutive rings are stitched by an angular merge: walking both rings in increasing angle, the next triangle uses whichever ring's next node comes first, which yields nk−1+nkn_{k-1} + n_k triangles per annulus and a valid triangulation. A Laplacian smoothing pass (mesh.smoothing, default 1) then relaxes the interior nodes with the rim nodes held on the circle. The mean element quality (inradius to circumradius ratio, 1 for an equilateral triangle) is about 0.96 and the minimum about 0.7. More passes improve the mean shape but shrink the smallest elements and thereby the stable time step, by about 10 percent for ten passes; the explicit step is set by the smallest elements in the thick bell (section 5.2), so a mesh graded coarser in the bell would be a worthwhile refinement. The rim is the polygon through the outer ring, so the mesh area is slightly below πR2\pi R^2.

4.2 Degrees of freedom#

Each node ii carries five unknowns qi=(ui,vi,wi,θx,i,θy,i)q_i = (u_i, v_i, w_i, \theta_{x,i}, \theta_{y,i}) with the DKT convention θx=w,y\theta_x = w_{,y} and θy=−w,x\theta_y = -w_{,x}. The state of the shell is the array qq of shape (N,5)(N, 5).

4.3 Membrane part: constant-strain triangle with the discrete initial surface#

On each triangle uu, vv and ww are interpolated linearly from their nodal values, so their gradients are constant: ∂xϕ=∑iGx,iϕi\partial_x \phi = \sum_i G_{x,i} \phi_i with GxG_x, GyG_y the usual CST operators. The initial surface is represented in the same space, that is, z0,xz_{0,x} and z0,yz_{0,y} on an element are the CST gradients of the nodal heights z0(xi,yi)z_0(x_i, y_i), not the analytic slope at the centroid. This choice is deliberate. A rigid tilt of the shell about the xx axis by an angle aa displaces a surface point by v=−az0v = -a z_0 and w=ayw = a y (section 4.7); its linearized strain is εyy=v,y+z0,yw,y=−a (Gz0)+a z0,y\varepsilon_{yy} = v_{,y} + z_{0,y} w_{,y} = -a\, (G z_0) + a\, z_{0,y}, which vanishes exactly only if z0,yz_{0,y} is the CST gradient of the nodal heights. With the analytic slope, the mismatch on a dented, coarse element left a rigid tilt with a spurious restoring force of order 10−410^{-4} of a typical stiffness; with the discrete slope the residual is at round-off (about 10−1610^{-16} in float64). The membrane energy of element ee is Um,e=12Ae εTCe εU_{m,e} = \tfrac12 A_e\, \varepsilon^{\mathsf T} C_e\, \varepsilon with CeC_e evaluated at the centroid thickness.

4.4 Bending part: the Discrete Kirchhoff Triangle#

Bending uses the DKT element of Batoz, Bathe and Ho (1980). Rotations βx,βy\beta_x, \beta_y are interpolated quadratically over the triangle from the three nodal (w,θx,θy)(w, \theta_x, \theta_y) triples, with the mid-side values fixed by imposing the Kirchhoff condition (rotation equals slope) discretely along each edge. The resulting curvature operator B(ξ,η)B(\xi, \eta), with κ=B Ue\kappa = B\, U_e for the nine bending DOFs UeU_e of the element, is linear in the area coordinates, so the element stiffness

Ke=∫AeBTDe B  dAK_e = \int_{A_e} B^{\mathsf T} D_e\, B\; dA

is integrated exactly by the three-point rule. The bending energy is Ub,e=12UeTKeUeU_{b,e} = \tfrac12 U_e^{\mathsf T} K_e U_e. DKT does not define a transverse displacement field inside the element, only the nodal values, which is why the membrane strains use the CST interpolation of ww; the pairing of a DKT bending triangle with a CST membrane triangle is the classical flat shell triangle, and the Marguerre terms supply the curvature coupling between them. The element stiffness is computed once in float64 by automatic differentiation of the shape functions and checked at build time: a rigid tilt gives zero force (residual about 10−1710^{-17}), the state w=x2/2w = x^2/2 has energy 12DAe\tfrac12 D A_e to 10−1010^{-10}, and the 15×1515 \times 15 tangent of the full element energy has exactly six zero eigenvalues (three translations and three rotations). cymsim check prints these.

4.5 Internal force by automatic differentiation#

The discrete strain energy is Uh(q)=∑e(Um,e(qe)+Ub,e(qe))U_h(q) = \sum_e \big( U_{m,e}(q_e) + U_{b,e}(q_e) \big) with qeq_e the (3,5)(3, 5) gather of the element's nodal values. The internal nodal force is

fint(q)=−∂Uh∂q,f_{int}(q) = -\frac{\partial U_h}{\partial q},

obtained by reverse-mode differentiation (jax.grad) of the summed energies; the gather, the per-element evaluation and the scatter are all inside the differentiated function. The force is therefore the exact gradient of the discrete energy, so the semi-discrete system is conservative up to the damping terms, and the energy diagnostic is meaningful. The tangent stiffness needed for the stability bound is the Hessian of the same function at q=0q = 0.

4.6 Lumped mass and rotational inertia#

The mass is lumped: element ee contributes me=ρheAe/3m_e = \rho h_e A_e / 3 to the uu, vv and ww entries of each of its nodes. The rotational entries receive

Ie=me(he212+fAe),I_e = m_e \left( \frac{h_e^2}{12} + f A_e \right),

where he2/12h_e^2 / 12 is the physical rotary inertia per unit mass and fAef A_e (default f=1/64f = 1/64) is an added inertia. The addition is a numerical device: with the physical value alone, the rotational DOFs of DKT have eigenfrequencies far above any bending wave the mesh can represent and would dictate an unusably small time step. The price is that bending waves whose wavelength is comparable to the element size are slowed slightly, by a relative amount of order (kheff)2/24(k h_{eff})^2 / 24 with heff2=he2+12fAeh_{eff}^2 = h_e^2 + 12 f A_e, which for 6 mm elements is a few percent at 20 kHz and well below one percent at 5 kHz. These are also the waves the mesh resolves least well, so we consider the trade acceptable; ff can be reduced at the cost of a smaller time step. A second, optional scaling inplane_mass_scale multiplies the in-plane mass; values above 1 slow the fast in-plane waves (cL≈3.8c_L \approx 3.8 km/s in bronze) and relax the time step further, but they also alter the in-plane dynamics that mediate the nonlinear coupling, so the default is 1.

4.7 Rigid-body modes of the curved shell#

The free shell has six rigid-body modes. Three are translations. The in-plane rotation is (u,v)=(−y,x)(u, v) = (-y, x). A tilt about the xx axis by an angle aa carries the surface point (x,y,z0)(x, y, z_0) to first order to (x, y−az0, z0+ay)(x,\, y - a z_0,\, z_0 + a y), so the mode is v=−z0v = -z_0, w=yw = y, θx=1\theta_x = 1; likewise the tilt about yy is u=z0u = z_0, w=−xw = -x, θy=1\theta_y = 1. The in-plane components proportional to z0z_0 are what distinguish the curved shell from the plate and are essential: without them a tilt stretches the shell. The six vectors are made MM-orthonormal by Gram-Schmidt. At build time we verify that the tangent stiffness annihilates all six to round-off. Note that this holds for the linearized operator only: the Marguerre-von Karman strains are invariant under a finite rotation only to first order, so a finite tilt of amplitude aa leaves a residual force of order a2a^2. This is a known property of the shallow-shell approximation, not of the discretization, and is harmless here because the projection of section 5.3 keeps the rigid amplitudes at zero.

4.8 The damping operator#

The σ1\sigma_1 term is discretized in weak form with the CST gradients. The nodal force on ww is

fσ1,i=2σ1∑e∋i(ρhA)e Ge,iT(Ge w˙e),f_{\sigma_1, i} = 2 \sigma_1 \sum_{e \ni i} (\rho h A)_e\, G_{e,i}^{\mathsf T} \big( G_e\, \dot w_e \big),

which is the assembly of 2σ1ρh∫∇w˙⋅∇ϕi dA2 \sigma_1 \rho h \int \nabla \dot w \cdot \nabla \phi_i\, dA. Its power w˙Tfσ1=2σ1∑e(ρhA)e∣∇w˙∣2≥0\dot w^{\mathsf T} f_{\sigma_1} = 2 \sigma_1 \sum_e (\rho h A)_e |\nabla \dot w|^2 \geq 0 is non-negative, so the term only ever removes energy, and a uniform velocity is undamped by it. The σ0\sigma_0 term is diagonal and acts on all five DOFs.

5. Time integration#

Implemented in simulate.py.

5.1 Leapfrog with implicit σ0\sigma_0#

With MM the diagonal lumped mass, one sub-step of size Δt\Delta t is

vn+1=P[vn+Δt M−1(fint(qn)+fc(qn,zsn)−fσ1(vn))1+2σ0Δt],qn+1=qn+Δt vn+1,v^{n+1} = P \left[ \frac{v^n + \Delta t\, M^{-1} \big( f_{int}(q^n) + f_c(q^n, z_s^n) - f_{\sigma_1}(v^n) \big)}{1 + 2 \sigma_0 \Delta t} \right], \qquad q^{n+1} = q^n + \Delta t\, v^{n+1},

where PP is the rigid-mode projection of section 5.3. This is the symplectic Euler scheme in (q,v)(q, v), equivalent to the central-difference (Stormer-Verlet) scheme in qq, second-order accurate and non-dissipative for the conservative part. The σ0\sigma_0 term is treated implicitly, which costs nothing because it is diagonal; the σ1\sigma_1 term is evaluated explicitly at vnv^n, which is accurate enough because 2σ1k2Δt≪12 \sigma_1 k^2 \Delta t \ll 1 for every wavenumber the mesh can carry at the default values. The exciter is advanced by the same symplectic Euler step (section 6.3).

5.2 Stability and the choice of the time step#

For the linear undamped system the scheme is stable when Δt≤2/ωmax\Delta t \leq 2 / \omega_{max}, with ωmax2\omega_{max}^2 the largest eigenvalue of M−1KM^{-1} K. Instead of solving that global eigenproblem we use the element bound (Irons; see Hughes 2000): λmax(M−1K)≤max⁡eλmax(Me−1Ke)\lambda_{max}(M^{-1} K) \leq \max_e \lambda_{max}(M_e^{-1} K_e), where KeK_e is the 15×1515 \times 15 tangent stiffness of the element at rest and MeM_e its share of the lumped mass. The bound is computed in float64 at build time and the limiting element (the thinnest, near the rim) is reported. The nonlinear membrane stiffening raises the instantaneous frequencies during large motion, so we take only a fraction cfl_safety (default 0.6) of the bound. The sub-step count per audio sample is m=⌈1/(fs csafety Δtbound)⌉m = \lceil 1 / (f_s\, c_{safety}\, \Delta t_{bound}) \rceil and Δt=1/(mfs)\Delta t = 1 / (m f_s), so that the radiation buffers decimate exactly to the audio rate. The run is extended by W/2=16W/2 = 16 audio samples beyond the requested duration so that the last samples of the decimated records (section 9) are final. If the energy diagnostic grows during a run the step is too large; the remedy is a smaller cfl_safety. The run aborts if the energy becomes non-finite.

5.3 Rigid-mode projection#

After each velocity update, v←v−∑k=16rk(rkTMv)v \leftarrow v - \sum_{k=1}^{6} r_k \big( r_k^{\mathsf T} M v \big) with the MM-orthonormal rigid modes rkr_k of section 4.7. This removes the linear and angular momentum that the stroke imparts, which is what a stand would do, and it also removes the slow rigid drift that round-off would otherwise accumulate. The projection changes only the rigid part of the velocity and hence removes only rigid kinetic energy; the vibrational energy is untouched. We emphasize that this is a proxy for the support, not a model of it: a felt washer at the bell would also add a local stiffness and some damping.

5.4 Precision and compilation#

Set-up (element stiffness, eigenvalue bound, rigid modes) is done in float64; the time loop runs in float32 by default (numerics.dtype), which is what a GPU does quickly. At the default parameters the displacement increment Δt v\Delta t\, v is about 10−310^{-3} of the displacement itself, so float32 loses a few of its 24 bits per step in qq without visible effect on the energy balance; float64 is available for verification. The step is compiled once with jax.jit as a fori_loop over steps_per_block sub-steps, and the Python loop runs over blocks, reporting energies between them.

6. Excitation#

Implemented in exciter.py; parameters in Hit, StickExciter, MalletExciter, ForceExciter.

6.1 Contact law#

Each exciter is a rigid body of mass mm that moves only along zz, with face height zs(t)z_s(t), coupled to the shell through PP springs at patch points pp. The surface height under point pp is wp+opw_p + o_p, where wp=∑iλp,iwiw_p = \sum_i \lambda_{p,i} w_i is the barycentric interpolation of the nodal transverse displacements in the containing element and opo_p is the height of the initial surface above the exciter's face plane, the best-fit plane through the patch points' z0z_0 values. The penetration and the force at point pp are

δp=max⁡(wp+op−zs, 0),Fp=kP δpα,F=∑pFp.\delta_p = \max(w_p + o_p - z_s,\, 0), \qquad F_p = \frac{k}{P}\, \delta_p^{\alpha}, \qquad F = \sum_p F_p .

The plate receives −Fp-F_p on the ww DOFs of the containing element, distributed with the weights λp,i\lambda_{p,i}, and the exciter obeys mz¨s=Fm \ddot z_s = F. The exponent α=1.5\alpha = 1.5 is the Hertz law for a hard sphere on a hard surface; α≈2\alpha \approx 2 to 33 fits felt. The contact spring is conservative (potential kδα+1/(P(α+1))k \delta^{\alpha+1} / (P(\alpha + 1))), so the exciter rebounds with whatever energy the shell has not absorbed; there is no contact hysteresis, which slightly overestimates the rebound of a felt mallet. The residual offsets opo_p mean that the face touches the curved surface at a few points first, which is what a rigid face on a curved shell does.

6.2 Contact patches#

A StickExciter contacts along a line of length ℓ\ell through the strike point, at an angle patch_angle from the radial direction, with PP equally spaced springs and a given total stiffness kk. A MalletExciter contacts on a disk: one spring at the centre and six on a hexagon of the head radius, and its stiffness is set from a target compression δc\delta_c at full stop on a rigid wall, 12mv2=kδcα+1/(α+1)\tfrac12 m v^2 = k \delta_c^{\alpha + 1} / (\alpha + 1). The set-up log reports, for each hit, the contact duration the exciter would have against a rigid wall,

τ=2c(α) δmaxv,c(α)=∫01dx1−xα+1,\tau = \frac{2 c(\alpha)\, \delta_{max}}{v}, \qquad c(\alpha) = \int_0^1 \frac{dx}{\sqrt{1 - x^{\alpha + 1}}},

which for α=1.5\alpha = 1.5 is Hertz's 2.94 δmax/v2.94\, \delta_{max} / v, together with the frequency 1/(2τ)1 / (2\tau) below which the stroke itself injects energy. For the default mallet this is about 120 Hz, so everything the listener hears above a few hundred hertz has come up the cascade. On the shell the actual contact is longer and softer than on a rigid wall because the surface gives way.

6.3 Schedule of hits#

A run has a list of hits, each with its own time, position, velocity and exciter. Every exciter exists for the whole run as a row of fixed-shape arrays (hits ×\times largest patch, padded with zero weights and a very negative offset so that padded points never touch), which lets one compiled step serve any number of hits. Before its release step nk=round(tk/Δt)n_k = \mathrm{round}(t_k / \Delta t) an exciter is inactive: it produces no force and is held far above the shell. At step nkn_k it is placed at zs=max⁡p(wp+op)+gapz_s = \max_p (w_p + o_p) + \text{gap}, just above the current surface, with velocity −vk-v_k. This matters for a train of hits, where the surface under a later hit is already moving by several thickness units. After the rebound the exciter simply moves away at constant speed; there is no gravity and no hand. Two consequences follow: an exciter never strikes twice on its own, and if the shell were to overtake a slowly rebounding exciter a second contact would occur (a real player pulls the stick back). The summary of each hit reports the peak force, the total time with F>0F > 0 and the rebound velocity.

6.4 Prescribed force#

A ForceExciter replaces the contact by a force given in advance, a raised cosine of peak F0F_0 and length TT applied at one point from the hit time tkt_k:

F(t)=12F0(1−cos⁡2π(t−tk)T),tk≤t≤tk+T,F(t) = \tfrac12 F_0 \left(1 - \cos \frac{2\pi (t - t_k)}{T}\right), \qquad t_k \le t \le t_k + T,

distributed to the containing element's ww DOFs with the barycentric weights and pushing the surface down. The force does not depend on the motion, so there is no rebound, and the hit's velocity is ignored. This is not a model of a stick or a mallet. It is the excitation used by modal plate models (Ducceschi and Touzé 2015, among others), and it exists so that cymsim can be run on exactly their problems.

7. Radiation#

Implemented in radiation.py and decimation.py; parameters in Listener.

The pressure at the listener point xLx_L from a baffled planar radiator is given by the Rayleigh integral, which we discretize with one point source per element:

p(xL,t)=ρair2π∑eAe aˉe ⁣(t−re/c)re,p(x_L, t) = \frac{\rho_{air}}{2\pi} \sum_e \frac{A_e\, \bar a_e\!\left( t - r_e / c \right)}{r_e},

where aˉe\bar a_e is the mean over the element's three nodes of the normal acceleration ai=ni⋅(u¨i,v¨i,w¨i)a_i = n_i \cdot (\ddot u_i, \ddot v_i, \ddot w_i), nin_i being the unit normal of the rest surface at node ii (the area-weighted mean of the adjacent elements' constant slopes, pointing to the +z+z side), and rer_e the distance from the element centroid, placed on the curved surface at height z0z_0, to the listener. The acceleration is taken from the integrator directly: for the leapfrog scheme (vn+1−vn)/Δt(v^{n+1} - v^n) / \Delta t is the exact central second difference of qq at step nn, so no differentiation of stored data is involved. This is exact for a rigid piston in an infinite baffle and an approximation here: the surface is shallow rather than flat, and the air does not load the shell (radiation damping is part of what σ0\sigma_0 and σ1\sigma_1 lump). At the bell, where the slope reaches 0.39, the normal component differs from w¨\ddot w by several percent, which is why we use it rather than w¨\ddot w.

A cymbal has no baffle. Its back face radiates with the opposite sign, and that sound reaches the listener by diffracting around the rim; at low frequency the two contributions nearly cancel, which is why an unbaffled plate is a weak low-frequency source. We estimate the back contribution with the same sum but with rer_e replaced by the shortest path over the rim, reback=min⁡j(∣xe−xjrim∣+∣xjrim−xL∣)r_e^{back} = \min_j \big( |x_e - x_j^{rim}| + |x_j^{rim} - x_L| \big) over 720 rim points, weighted by back_diffraction_gain, and subtract it: punbaffled=pfront−g pbackp_{unbaffled} = p_{front} - g\, p_{back}. This is a Kirchhoff-style edge-diffraction estimate, not a boundary-element solution, and it ignores shadowing of the front by the shell itself. Both signals are returned; the baffled one is a cleaner measure of the shell motion, the unbaffled one sounds more like a cymbal in a room.

Retarded times are handled at sub-step resolution: the delay of element ee in sub-steps, de=re/(cΔt)d_e = r_e / (c \Delta t), is split into an integer part and a fraction, and each element's contribution is added to two adjacent slots of a ring buffer with linear interpolation. After each block the host reads the slots of the finished sub-steps, clears them and decimates them to the audio rate with the kernel of section 9, the same kernel that filters the acceleration record. Since delay and filtering are both linear and time-invariant, the stored listener signal follows from the stored accelerations up to the linear-interpolation error at the sub-step scale, which is negligible; the test suite rebuilds the baffled signal from the record to 1.5×10−41.5 \times 10^{-4} relative rms.

8. Diagnostics#

Implemented in simulate.py (energies) and diagnostics.py (spectra). During the run the kinetic, membrane, bending and exciter energies are logged at n_progress points together with max⁡i∣wi∣/hi\max_i |w_i| / h_i, the nonlinearity meter: below about 1 the run is effectively linear, at 3 to 5 the cascade is clearly developed. The sum of the four energies can only decrease between the release of a hit and the next one, which is a direct check of the integrator. After the run, an STFT of the unbaffled pressure (256-point windows, 32-sample hop, about 0.7 ms) gives per-octave band energy envelopes from 60 Hz to 16 kHz; the onset of a band is the first time it reaches 20 dB below its own peak, and the spectral centroid is tracked against time. In a cascade the onsets are staggered with the high bands last and the centroid climbs over tens of milliseconds; in the linear reference run the bands rise together and the centroid is flat.

9. Records and the .cymsim file#

Implemented in decimation.py, simulate.py and cymsim_file.py; parameters in Output; file layout in format.md.

The listener model of section 7 is deliberately simple, and a natural use of the simulation is to feed the shell's motion to a different acoustic model: another listener position, a room, or a solver of the acoustic wave equation with the shell as a vibrating boundary. What such a model needs is the normal acceleration of the surface at the audio rate, and the question is how to obtain it without noise. Differentiating stored displacements twice is the wrong route: a second difference multiplies noise by ω2\omega^2, so the top of the band suffers most, it folds whatever the shell does above the audio Nyquist frequency back into the band, and in float32 the three-term difference of nearly equal numbers loses most of its digits. We therefore take the acceleration from the integrator, where it is exact for the scheme (section 7), and the only remaining step is the reduction from the sub-step rate, typically 2.4 MHz, to the audio rate.

That reduction has to be a proper low-pass filter, because the cascade puts real energy above 20 kHz and acceleration weights it by ω2\omega^2. We use a Kaiser-windowed sinc h(d)h(d) spanning W=32W = 32 audio samples (WmWm sub-steps) with β=8\beta = 8 and its −6-6 dB point at 0.46fs0.46 f_s: flat to about 0.38fs0.38 f_s, −23-23 dB at fs/2f_s / 2, below −80-80 dB from 0.54fs0.54 f_s. It is applied inside the compiled time loop through WW accumulator slots per node: at sub-step j0m+kkj_0 m + kk the node adds ai h(kk+(W/2−1−w) m)a_i\, h(kk + (W/2 - 1 - w)\, m) to slot ww, which belongs to audio sample j0+w−W/2+1j_0 + w - W/2 + 1; when the sub-step count crosses a sample boundary the oldest slot is complete, is written to the block's output and is reused for the newest sample. The cost is WW multiply-adds per node per sub-step, small next to the element force evaluation. The kernel is linear-phase and the read-out removes its delay, so sample jj is centred on t=j/fst = j / f_s; the price is that the newest W/2W/2 samples are not final, which is why the run is extended by W/2W/2 samples (section 5.2) and why a partial save stops W/2W/2 samples short of the current time.

The record is kept only on request (output.record_acceleration, default off), because it is large: one value per node per audio sample, stored as int16 against one float32 scale per sample, is 2Nfs2 N f_s bytes per simulated second, about 0.4 GB per second at the default mesh. The per-sample scale keeps the quantisation noise at 1.5×10−51.5 \times 10^{-5} of each sample's own peak through the 90 dB or more of a stroke's decay; the noise is incoherent across nodes and changes the built-in Rayleigh sum by less than 10−410^{-4} relative rms. Transverse displacements, in contrast, are kept only at a low frame rate (output.displacement_rate, default 60 Hz), in float32 and exactly at the frame times. At 60 Hz they are a strobe of the motion, useful for display and for the slow quasi-static deflection, but not a signal from which anything should be differentiated.

For comparisons at a single point, output.probes lists points (x,y)(x, y) on the projected domain where the normal acceleration is kept at the audio rate in float64: the nodal values are interpolated with the barycentric weights of the containing element, held for one block at the sub-step rate, and decimated on the host with the same kernel. A probe placed on a node reproduces that node's column of the acceleration record. Probe signals are returned in the result and its .npz file but are not yet written to the .cymsim file.

Both records, the two listener signals, the configuration, the mesh with its nodal normals, the energy log, the hit summaries and the integrator state are written to an HDF5 file with the extension .cymsim, described in format.md. The time series are resizable, so cymsim run -o run.cymsim --save-every 60 writes a consistent partial file every minute and extends it, and a run that is interrupted leaves a loadable file. The web companion writes and reads the same format.

10. Limitations and future work#

The model is a shallow shell, so it is only as good as the assumption ∣∇z0∣≪1|\nabla z_0| \ll 1; the default profile reaches a slope of 0.39 at the bell, which is at the edge of what shallow-shell theory is meant for, and a steep bell would call for a proper curved-shell formulation. The elements are flat facets with the curvature entering through z0z_0 alone, and the membrane strain uses the linear interpolation of ww while bending uses DKT rotations; this is a standard pairing but not a fully consistent one, and its convergence with element size has not been studied here. The lumped rotational inertia is inflated for the sake of the time step (section 4.6). The support is a rigid-mode projection rather than a model of the stand. The damping has two parameters and no physics behind them. The contact is one-dimensional along zz, with no friction, no tangential force, no hysteresis and no hand. The radiation is a Rayleigh sum with a heuristic back-side correction.

The natural next steps are, roughly in order of expected benefit: a convergence study of the cascade statistics with element size and precision; a bell support with local stiffness and damping in place of the projection; a contact model with hysteretic losses for felt; and a proper unbaffled radiation model. A train of hits already works structurally, but the behaviour of the contact model against a strongly moving surface deserves its own validation.

11. References#

12. Symbols and where to find them#

symbol meaning configuration / code
E,ν,ρE, \nu, \rho Young's modulus, Poisson ratio, density Material.E, .nu, .rho
σ0,σ1\sigma_0, \sigma_1 loss parameters Material.sigma0, .sigma1
z0(x,y)z_0(x, y), h(x,y)h(x, y) mid-surface height, thickness CymbalProfile.z0, .thickness
Ce,De,KeC_e, D_e, K_e membrane stiffness, bending rigidity, DKT stiffness of element ee ShellModel.Cm_e, .D_e, .Ke
Gx,GyG_x, G_y CST gradient operators Mesh.Gx, .Gy
MM, IeI_e, ff lumped mass, rotational inertia, its scaling factor ShellModel.M, .I_e, Numerics.rot_inertia_factor
rkr_k rigid-body modes ShellModel.Rm
ωmax\omega_{max}, Δt\Delta t, mm stability bound, sub-step, sub-steps per sample ShellModel.omega_max, Simulator.dt, .substeps
k,α,Pk, \alpha, P contact stiffness, exponent, springs per patch ContactPatch.k_total, .alpha, .n_points
zs,vsz_s, v_s exciter face height and velocity state entries zs, vs
pfront,pbackp_{front}, p_{back} Rayleigh front and diffracted back pressure SimulationResult.pressure_rayleigh, .pressure_unbaffled
nin_i, aia_i nodal normal of the rest surface, normal acceleration ShellModel.normal_node, SimulationResult.acceleration
h(d)h(d), WW decimation kernel, its span in audio samples decimation.decimation_kernel, DECIMATION_SPAN
↑↓ move↵openescclose